Commit 8ce4471111fec7852a43f3ffab85bf133ee689cc
1 parent
c4f8424b
feat: 语音全合成接入 wechat_send.sh + android_mcp.py MCP + skill
三种消息在使用层齐了(文字/图片/语音全走内部 API 全合成): - wechat_send.sh voice: 弃用旧 SEND_VOICE_INTERNAL(hot-swap 相关),改走 SEND_VOICE_CGI 全合成。SILK 须 WeChat 进程可读——动态取 WeChat uid(stat /data/user/0/com.tencent.mm), stage 进 /data/data/com.tencent.mm/files/ + chown + restorecon(/sdcard EACCES、 root cp 进沙盒需正确 owner+ctx)。 - wechat_hook_send.py: 新增 send_voice_cgi_via_hook(同上 staging + 广播 SEND_VOICE_CGI)。 - android_mcp.py: send_wechat_voice 改调 send_voice_cgi_via_hook,docstring 更新为全合成。 - skill_wechat_send/SKILL.md: 语音段改写为 SEND_VOICE_CGI 全合成用法。 真机验证:wechat_send.sh voice + MCP send_voice_cgi_via_hook 均 status=sent + hook svrMsgId>0(uploadvoice cgi 服务器接受)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Showing
5 changed files
with
218 additions
and
21 deletions
phoneuse/android_mcp.py
| ... | ... | @@ -889,17 +889,22 @@ def _build_server(): |
| 889 | 889 | @mcp.tool() |
| 890 | 890 | def send_wechat_voice(to: str, silk_path: str, duration_ms: int = 3000) -> str: |
| 891 | 891 | """ |
| 892 | - Send a voice message to a WeChat contact via the internal hook API (no UI simulation). | |
| 892 | + Send a FULLY-SYNTHETIC voice message to a WeChat contact (no recording, no hot-swap). | |
| 893 | 893 | |
| 894 | - Uses WeChat's w11.r0 factory (MsgType 34). The hook injects the correct | |
| 895 | - voice file ID into the DB insert so the message is stored with the right | |
| 896 | - SILK file reference and is immediately playable. | |
| 894 | + Fires SEND_VOICE_CGI → the hook chunks the SILK, builds WeChat's own | |
| 895 | + /cgi-bin/micromsg-bin/uploadvoice request (audio inline), dispatches it via the | |
| 896 | + NetSceneQueue, and inserts a local sent bubble. Result: the recipient gets a | |
| 897 | + playable voice AND the sender sees a sent bubble. Verified end-to-end on WeChat | |
| 898 | + 8.0.74 (recipient + Mac both received). Delivery is async; confirm via UI if needed. | |
| 899 | + | |
| 900 | + The SILK is auto-staged into WeChat's data dir (its uid + SELinux context) so the | |
| 901 | + WeChat process can read it — a device /sdcard path alone would EACCES. | |
| 897 | 902 | |
| 898 | 903 | Args: |
| 899 | - to: Contact wxid (e.g. 'filehelper'). IMPORTANT: test only with 'filehelper'. | |
| 900 | - silk_path: Absolute device path to a SILK V3 file (Tencent variant, | |
| 901 | - starts with \\x02#!SILK_V3). Generate with silkv3-encoder | |
| 902 | - or convert from PCM via silk-v3-encoder. | |
| 904 | + to: Contact wxid as in rcontact.username (e.g. 'filehelper', 'wxid_xxx', | |
| 905 | + 'yibooo'). Test with 'filehelper' first to avoid account risk. | |
| 906 | + silk_path: Absolute device path to a SILK V3 file (Tencent variant, starts | |
| 907 | + with \\x02#!SILK_V3), e.g. edge-tts → ffmpeg 16k mono → silk-wasm. | |
| 903 | 908 | duration_ms: Voice duration in milliseconds shown in WeChat UI. |
| 904 | 909 | """ |
| 905 | 910 | import sys |
| ... | ... | @@ -907,8 +912,8 @@ def _build_server(): |
| 907 | 912 | if _base not in sys.path: |
| 908 | 913 | sys.path.insert(0, _base) |
| 909 | 914 | try: |
| 910 | - from wechat_hook_send import send_voice_via_hook | |
| 911 | - result = send_voice_via_hook(wxid=to, silk_path=silk_path, duration_ms=duration_ms) | |
| 915 | + from wechat_hook_send import send_voice_cgi_via_hook | |
| 916 | + result = send_voice_cgi_via_hook(wxid=to, silk_path=silk_path, duration_ms=duration_ms) | |
| 912 | 917 | return json.dumps(result, ensure_ascii=False) |
| 913 | 918 | except Exception as e: |
| 914 | 919 | return json.dumps({"status": "error", "error": str(e)}) | ... | ... |
phoneuse/wechat_hook_send.py
| ... | ... | @@ -24,6 +24,7 @@ from ui_control_tools import _run, _adb_prefix, _ON_DEVICE |
| 24 | 24 | |
| 25 | 25 | SEND_ACTION = "com.hermes.wechat.SEND_MSG_INTERNAL" |
| 26 | 26 | SEND_IMAGE_ACTION = "com.hermes.wechat.SEND_IMAGE_INTERNAL" |
| 27 | +SEND_VOICE_CGI_ACTION = "com.hermes.wechat.SEND_VOICE_CGI" | |
| 27 | 28 | _WECHAT_PKG = "com.tencent.mm" |
| 28 | 29 | _WECHAT_MAIN = "com.tencent.mm/.ui.LauncherUI" |
| 29 | 30 | |
| ... | ... | @@ -249,6 +250,69 @@ def send_image_via_hook(wxid: str, image_path: str) -> dict[str, Any]: |
| 249 | 250 | } |
| 250 | 251 | |
| 251 | 252 | |
| 253 | +def send_voice_cgi_via_hook(wxid: str, silk_path: str, duration_ms: int = 3000) -> dict[str, Any]: | |
| 254 | + """Send a FULLY-SYNTHETIC voice to *wxid* via the internal uploadvoice cgi. | |
| 255 | + | |
| 256 | + No recording, no hot-swap. The hook chunks the SILK, builds WeChat's own | |
| 257 | + /cgi-bin/micromsg-bin/uploadvoice request (audio inline), dispatches it via the | |
| 258 | + NetSceneQueue (modelbase.i.l() + subscribe), and inserts a local f9 bubble marked | |
| 259 | + sent. Result: the recipient gets a playable voice AND the sender sees a sent bubble. | |
| 260 | + Verified end-to-end on WeChat 8.0.74 (recipient + Mac both received; both sides visible). | |
| 261 | + | |
| 262 | + The hook reads *silk_path* AS the WeChat process, so the SILK is first staged into | |
| 263 | + WeChat's own data dir (its uid + app_data_file SELinux context) — /sdcard gives | |
| 264 | + EACCES and a plain root cp into the sandbox needs chown + restorecon. | |
| 265 | + | |
| 266 | + Args: | |
| 267 | + wxid: talker id as in rcontact.username (e.g. 'filehelper', 'wxid_xxx', 'yibooo'). | |
| 268 | + Test with 'filehelper' first to avoid account risk. | |
| 269 | + silk_path: device path to a SILK V3 file (Tencent variant, \\x02#!SILK_V3), | |
| 270 | + e.g. from edge-tts → ffmpeg 16k mono → silk-wasm. | |
| 271 | + duration_ms: voice duration in milliseconds shown in WeChat UI. | |
| 272 | + """ | |
| 273 | + if not wxid or not silk_path: | |
| 274 | + return {"status": "error", "error": "wxid and silk_path required"} | |
| 275 | + if not _wechat_running(): | |
| 276 | + if not _start_wechat(): | |
| 277 | + return {"status": "error", "error": "WeChat not running and failed to start"} | |
| 278 | + time.sleep(3) | |
| 279 | + | |
| 280 | + # Resolve WeChat's uid (changes across reinstalls) and stage the SILK into its data | |
| 281 | + # dir with the matching owner + SELinux context so the WeChat process can read it. | |
| 282 | + uid_res = _root_sh("stat -c %U /data/user/0/com.tencent.mm") | |
| 283 | + wx_uid = (uid_res.stdout or "").strip() | |
| 284 | + if not wx_uid: | |
| 285 | + return {"status": "error", "error": f"could not resolve WeChat uid: {uid_res.stderr or '(no output)'}"} | |
| 286 | + stage_path = "/data/data/com.tencent.mm/files/hook_voice_cgi.silk" | |
| 287 | + stage = _root_sh( | |
| 288 | + f'cp "{silk_path}" "{stage_path}" && ' | |
| 289 | + f'chown {wx_uid}:{wx_uid} "{stage_path}" && ' | |
| 290 | + f'chmod 600 "{stage_path}"; restorecon "{stage_path}" 2>/dev/null; true' | |
| 291 | + ) | |
| 292 | + if stage.returncode != 0: | |
| 293 | + return {"status": "error", "error": f"failed to stage SILK: {stage.stderr or stage.stdout}"} | |
| 294 | + | |
| 295 | + cmd = ( | |
| 296 | + f"am broadcast -a {SEND_VOICE_CGI_ACTION} " | |
| 297 | + f"--es to {wxid} --es src_path {stage_path} --ei duration {duration_ms}" | |
| 298 | + ) | |
| 299 | + try: | |
| 300 | + r = _root_sh(cmd) | |
| 301 | + except subprocess.TimeoutExpired: | |
| 302 | + return {"status": "error", "error": "voice cgi broadcast timed out"} | |
| 303 | + stdout = (r.stdout or "").strip() | |
| 304 | + stderr = (r.stderr or "").strip() | |
| 305 | + if r.returncode == 0 and ("result=" in stdout or "Broadcast completed" in stdout): | |
| 306 | + return { | |
| 307 | + "status": "sent", | |
| 308 | + "to": wxid, | |
| 309 | + "method": "uploadvoice_cgi", | |
| 310 | + "verified": False, # cgi is async; confirm via UI / recipient | |
| 311 | + "detail": stdout[:200], | |
| 312 | + } | |
| 313 | + return {"status": "error", "error": f"broadcast rc={r.returncode}: {stderr or stdout or '(no output)'}"} | |
| 314 | + | |
| 315 | + | |
| 252 | 316 | def send_voice_via_hook(wxid: str, silk_path: str, duration_ms: int = 3000) -> dict[str, Any]: |
| 253 | 317 | """Send a voice message to *wxid* via the wechathook internal API. |
| 254 | 318 | ... | ... |
xhs_lsposed_a16_fix/backups/wechat_send_20260707_voice-cgi.sh
0 → 100755
| 1 | +#!/bin/bash | |
| 2 | +# wechat_send.sh — unified WeChat send CLI via the internal hook | |
| 3 | +# | |
| 4 | +# Usage: | |
| 5 | +# ./wechat_send.sh text <wxid> <message> | |
| 6 | +# ./wechat_send.sh image <wxid> <device_image_path> | |
| 7 | +# ./wechat_send.sh voice <wxid> <local_silk_path> [duration_ms] | |
| 8 | +# | |
| 9 | +# All send operations go through the wechathook LSPosed module's internal API, | |
| 10 | +# which calls WeChat's own w11.r0/w11.s1 factory — no UI simulation. | |
| 11 | +# <wxid>: talker id as stored in WeChat DB (e.g. 'filehelper', 'wxid_xxx'). | |
| 12 | +# IMPORTANT: test-only with 'filehelper' to avoid account risk. | |
| 13 | +# | |
| 14 | +# Voice notes (SEND_VOICE_CGI — fully synthetic, no recording/hot-swap): | |
| 15 | +# <local_silk_path> — local SILK V3 file (Tencent prefix \x02#!SILK_V3), e.g. from | |
| 16 | +# edge-tts → ffmpeg 16k mono → silk-wasm. | |
| 17 | +# [duration_ms] — playback duration in ms (default: 3000). | |
| 18 | +# Stages the SILK into WeChat's own data dir (uid + app_data_file ctx) then broadcasts | |
| 19 | +# the internal uploadvoice cgi: uploads audio inline + delivers to the recipient AND | |
| 20 | +# inserts a sent bubble on the sender side. Verified end-to-end + both-sides visible. | |
| 21 | +# | |
| 22 | +# Examples: | |
| 23 | +# ./wechat_send.sh text filehelper "hello world" | |
| 24 | +# ./wechat_send.sh image filehelper /tmp/screenshot.png | |
| 25 | +# ./wechat_send.sh voice filehelper /tmp/tts_out.silk 4617 | |
| 26 | + | |
| 27 | +set -e | |
| 28 | + | |
| 29 | +ADB="${ADB:-adb}" | |
| 30 | +WECHAT_CACHE="/data/data/com.tencent.mm/cache/hook_voice_src.amr" | |
| 31 | + | |
| 32 | +usage() { | |
| 33 | + echo "Usage: $0 text <wxid> <message>" | |
| 34 | + echo " $0 image <wxid> <device_path>" | |
| 35 | + echo " $0 voice <wxid> <local_silk_path> [duration_ms]" | |
| 36 | + exit 1 | |
| 37 | +} | |
| 38 | + | |
| 39 | +[ $# -lt 3 ] && usage | |
| 40 | + | |
| 41 | +CMD="$1"; WXID="$2"; shift 2 | |
| 42 | + | |
| 43 | +case "$CMD" in | |
| 44 | +text) | |
| 45 | + PAYLOAD="$*" | |
| 46 | + B64_WXID=$(printf '%s' "$WXID" | base64 | tr -d '\n') | |
| 47 | + B64_TEXT=$(printf '%s' "$PAYLOAD" | base64 | tr -d '\n') | |
| 48 | + INNER=$(printf 'TO=$(printf "%%s" "%s" | base64 -d); TEXT=$(printf "%%s" "%s" | base64 -d); am broadcast -a com.hermes.wechat.SEND_MSG_INTERNAL --es to "$TO" --es text "$TEXT"' \ | |
| 49 | + "$B64_WXID" "$B64_TEXT") | |
| 50 | + B64_INNER=$(printf '%s' "$INNER" | base64 | tr -d '\n') | |
| 51 | + echo "Sending text to $WXID..." | |
| 52 | + $ADB shell "su -c 'echo $B64_INNER | base64 -d | sh'" | |
| 53 | + ;; | |
| 54 | + | |
| 55 | +image) | |
| 56 | + # Internal API: WeChat's own b80.u.oj (insert+prebuild) + v70.w.oj (CDN upload). | |
| 57 | + # No UI, no contact picker — fire-and-forget to <wxid>. Verified full-res delivery. | |
| 58 | + IMAGE_PATH="$1" | |
| 59 | + [ -z "$IMAGE_PATH" ] && usage | |
| 60 | + EXT="${IMAGE_PATH##*.}" | |
| 61 | + # Stage into /sdcard so the WeChat process (runs the hook) can read it, regardless | |
| 62 | + # of whether the source path is root-only. If already under /sdcard this is a no-op copy. | |
| 63 | + SHARE="/sdcard/wechat_send_tmp.$EXT" | |
| 64 | + echo "Staging image to $SHARE..." | |
| 65 | + $ADB shell "su -c 'cp \"$IMAGE_PATH\" \"$SHARE\" && chmod 644 \"$SHARE\"'" | |
| 66 | + echo "Sending image to $WXID via internal API..." | |
| 67 | + $ADB shell "su -c 'am broadcast -a com.hermes.wechat.SEND_IMAGE_INTERNAL --es to \"$WXID\" --es path \"$SHARE\"'" | |
| 68 | + echo "Sent. (Verify via WeChat UI screenshot; full image uploads async via CDN.)" | |
| 69 | + ;; | |
| 70 | + | |
| 71 | +voice) | |
| 72 | + # Fully-synthetic voice via the internal uploadvoice cgi (no recording, no hot-swap). | |
| 73 | + # Uploads the SILK inline + delivers to the recipient AND inserts a sent bubble on the | |
| 74 | + # sender side. The hook reads src_path AS the WeChat process, so the SILK must live in | |
| 75 | + # WeChat's own data dir with its uid + app_data_file SELinux context — /sdcard gives | |
| 76 | + # EACCES and a root cp into the sandbox needs chown+restorecon to the right owner. | |
| 77 | + SILK_PATH="$1" | |
| 78 | + DURATION="${2:-3000}" | |
| 79 | + [ -z "$SILK_PATH" ] && usage | |
| 80 | + [ ! -f "$SILK_PATH" ] && { echo "File not found: $SILK_PATH"; exit 1; } | |
| 81 | + | |
| 82 | + WX_UID=$($ADB shell "su -c 'stat -c %U /data/user/0/com.tencent.mm'" | tr -d '\r') | |
| 83 | + [ -z "$WX_UID" ] && { echo "Could not resolve WeChat uid"; exit 1; } | |
| 84 | + STAGE="/data/data/com.tencent.mm/files/hook_voice_cgi.silk" | |
| 85 | + | |
| 86 | + echo "Staging SILK into WeChat data dir (uid=$WX_UID)..." | |
| 87 | + $ADB push "$SILK_PATH" /sdcard/hook_voice_tmp.silk >/dev/null | |
| 88 | + $ADB shell "su -c 'cp /sdcard/hook_voice_tmp.silk $STAGE'" | |
| 89 | + $ADB shell "su -c 'chown $WX_UID:$WX_UID $STAGE'" | |
| 90 | + $ADB shell "su -c 'chmod 600 $STAGE'" | |
| 91 | + $ADB shell "su -c 'restorecon $STAGE'" 2>/dev/null | |
| 92 | + $ADB shell "su -c 'rm -f /sdcard/hook_voice_tmp.silk'" | |
| 93 | + | |
| 94 | + echo "Sending synthetic voice to $WXID (duration=${DURATION}ms)..." | |
| 95 | + $ADB shell "su -c 'am broadcast -a com.hermes.wechat.SEND_VOICE_CGI --es to $WXID --es src_path $STAGE --ei duration $DURATION'" | |
| 96 | + echo "Sent. (Recipient gets a playable voice; sender sees a sent bubble. Verify via WeChat UI.)" | |
| 97 | + ;; | |
| 98 | + | |
| 99 | +*) | |
| 100 | + echo "Unknown command: $CMD" | |
| 101 | + usage | |
| 102 | + ;; | |
| 103 | +esac | ... | ... |
xhs_lsposed_a16_fix/skill_wechat_send/SKILL.md
| ... | ... | @@ -48,9 +48,17 @@ adb shell su -c 'am broadcast -a com.hermes.wechat.SEND_IMAGE_INTERNAL --es to y |
| 48 | 48 | `su` 能读到的路径(如 `/sdcard/...`)。发前若图片在 Mac 上,先 `adb push` 到设备。 |
| 49 | 49 | 2026-07-07 真机验证:filehelper + 真人(火星人)均全图收到。详见技术文档 §3。 |
| 50 | 50 | |
| 51 | -### ✅ 语音(TTS→SILK,半自动 hot-swap) | |
| 52 | -纯合成卡「发送中」,用 hot-swap:让微信真录一段 + 偷换 SILK 字节。 | |
| 53 | -端到端脚本 `../tts_voice_swap.sh`(需先打开目标聊天并处语音模式)。详见技术文档 §2。 | |
| 51 | +### ✅ 语音(TTS→SILK,全合成,无录音/无 hot-swap) | |
| 52 | +```bash | |
| 53 | +adb shell su -c 'am broadcast -a com.hermes.wechat.SEND_VOICE_CGI --es to yibooo --es src_path <设备上SILK> --ei duration 3000' | |
| 54 | +``` | |
| 55 | +或用封装脚本(自动把 SILK stage 进 WeChat 数据目录):`./wechat_send.sh voice yibooo /path/to.silk 3000` | |
| 56 | + | |
| 57 | +走微信内部 `uploadvoice` cgi:音频内联上传 + 投递给接收方,同时插发送方本地已发送气泡。 | |
| 58 | +真机验证:接收方(火星人/Mac)收到能播 + 发送方本地正常显示。SILK 来源 = edge-tts→ffmpeg | |
| 59 | +16k mono→silk-wasm。**SILK 须 WeChat 进程可读**——封装脚本已处理(stage 进 | |
| 60 | +`/data/data/com.tencent.mm/files/` + chown WeChat uid + restorecon);直接 `/sdcard` 会 EACCES。 | |
| 61 | +旧的半自动 hot-swap(`../tts_voice_swap.sh` + `ARM_VOICE_SWAP`)已被全合成取代,可弃用。 | |
| 54 | 62 | |
| 55 | 63 | ## 不要做 |
| 56 | 64 | - **不要用 `dk5.s5.Ni` 发文字**——那是发名片的(会得到失败的空名片)。 | ... | ... |
xhs_lsposed_a16_fix/wechat_send.sh
| ... | ... | @@ -11,10 +11,13 @@ |
| 11 | 11 | # <wxid>: talker id as stored in WeChat DB (e.g. 'filehelper', 'wxid_xxx'). |
| 12 | 12 | # IMPORTANT: test-only with 'filehelper' to avoid account risk. |
| 13 | 13 | # |
| 14 | -# Voice notes: | |
| 15 | -# <local_silk_path> — local SILK V3 file (Tencent prefix \x02#!SILK_V3). | |
| 14 | +# Voice notes (SEND_VOICE_CGI — fully synthetic, no recording/hot-swap): | |
| 15 | +# <local_silk_path> — local SILK V3 file (Tencent prefix \x02#!SILK_V3), e.g. from | |
| 16 | +# edge-tts → ffmpeg 16k mono → silk-wasm. | |
| 16 | 17 | # [duration_ms] — playback duration in ms (default: 3000). |
| 17 | -# The script pushes the SILK to WeChat's cache dir and fires the broadcast. | |
| 18 | +# Stages the SILK into WeChat's own data dir (uid + app_data_file ctx) then broadcasts | |
| 19 | +# the internal uploadvoice cgi: uploads audio inline + delivers to the recipient AND | |
| 20 | +# inserts a sent bubble on the sender side. Verified end-to-end + both-sides visible. | |
| 18 | 21 | # |
| 19 | 22 | # Examples: |
| 20 | 23 | # ./wechat_send.sh text filehelper "hello world" |
| ... | ... | @@ -66,17 +69,31 @@ image) |
| 66 | 69 | ;; |
| 67 | 70 | |
| 68 | 71 | voice) |
| 72 | + # Fully-synthetic voice via the internal uploadvoice cgi (no recording, no hot-swap). | |
| 73 | + # Uploads the SILK inline + delivers to the recipient AND inserts a sent bubble on the | |
| 74 | + # sender side. The hook reads src_path AS the WeChat process, so the SILK must live in | |
| 75 | + # WeChat's own data dir with its uid + app_data_file SELinux context — /sdcard gives | |
| 76 | + # EACCES and a root cp into the sandbox needs chown+restorecon to the right owner. | |
| 69 | 77 | SILK_PATH="$1" |
| 70 | 78 | DURATION="${2:-3000}" |
| 71 | 79 | [ -z "$SILK_PATH" ] && usage |
| 72 | 80 | [ ! -f "$SILK_PATH" ] && { echo "File not found: $SILK_PATH"; exit 1; } |
| 73 | 81 | |
| 74 | - echo "Pushing SILK to WeChat cache..." | |
| 75 | - $ADB push "$SILK_PATH" /sdcard/hook_voice_tmp.amr | |
| 76 | - $ADB shell "su -c 'cp /sdcard/hook_voice_tmp.amr $WECHAT_CACHE && chmod 644 $WECHAT_CACHE && chown u0_a330:u0_a330 $WECHAT_CACHE && rm /sdcard/hook_voice_tmp.amr'" | |
| 82 | + WX_UID=$($ADB shell "su -c 'stat -c %U /data/user/0/com.tencent.mm'" | tr -d '\r') | |
| 83 | + [ -z "$WX_UID" ] && { echo "Could not resolve WeChat uid"; exit 1; } | |
| 84 | + STAGE="/data/data/com.tencent.mm/files/hook_voice_cgi.silk" | |
| 77 | 85 | |
| 78 | - echo "Sending voice to $WXID (duration=${DURATION}ms)..." | |
| 79 | - $ADB shell "su -c 'am broadcast -a com.hermes.wechat.SEND_VOICE_INTERNAL --es to $WXID --es src_path $WECHAT_CACHE --ei duration $DURATION'" | |
| 86 | + echo "Staging SILK into WeChat data dir (uid=$WX_UID)..." | |
| 87 | + $ADB push "$SILK_PATH" /sdcard/hook_voice_tmp.silk >/dev/null | |
| 88 | + $ADB shell "su -c 'cp /sdcard/hook_voice_tmp.silk $STAGE'" | |
| 89 | + $ADB shell "su -c 'chown $WX_UID:$WX_UID $STAGE'" | |
| 90 | + $ADB shell "su -c 'chmod 600 $STAGE'" | |
| 91 | + $ADB shell "su -c 'restorecon $STAGE'" 2>/dev/null | |
| 92 | + $ADB shell "su -c 'rm -f /sdcard/hook_voice_tmp.silk'" | |
| 93 | + | |
| 94 | + echo "Sending synthetic voice to $WXID (duration=${DURATION}ms)..." | |
| 95 | + $ADB shell "su -c 'am broadcast -a com.hermes.wechat.SEND_VOICE_CGI --es to $WXID --es src_path $STAGE --ei duration $DURATION'" | |
| 96 | + echo "Sent. (Recipient gets a playable voice; sender sees a sent bubble. Verify via WeChat UI.)" | |
| 80 | 97 | ;; |
| 81 | 98 | |
| 82 | 99 | *) | ... | ... |