tts_voice_swap.sh
3.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#!/bin/bash
# tts_voice_swap.sh — text → TTS → REAL WeChat voice message (delivered)
#
# This is the ONLY route that actually delivers synthetic voice. Fully-synthetic
# sends (SEND_VOICE_INTERNAL) build the message shell but can never upload the SILK
# to WeChat's CDN (upload is locked inside jg0.x's Kotlin coroutine). So instead we:
#
# 1. TTS the text → SILK V3 (same format WeChat records).
# 2. Arm the wechathook swap (ARM_VOICE_SWAP) with our SILK.
# 3. Trigger a REAL recording by holding "按住 说话" (input swipe, same point).
# WeChat records ambient audio, but at the CDN-upload step (jg0.x.w) the hook
# replaces the recorded SILK bytes (w21.t.a) with our TTS → WeChat uploads and
# delivers OUR audio, reusing its entire real send pipeline.
#
# PRECONDITION: WeChat must already be open on the TARGET chat, in voice-input mode
# (the bottom bar shows "按住 说话"). This script does NOT navigate there — pair it
# with an A11y opener for full automation. Test-only with 文件传输助手 (filehelper).
#
# Usage:
# ./tts_voice_swap.sh "<text>" [voice] [record_x] [record_y]
#
# Examples:
# ./tts_voice_swap.sh "你好,我现在没办法说话,这是我用文字转成的语音。"
# ./tts_voice_swap.sh "下午三点开会" zh-CN-YunxiNeural
set -e
TEXT="${1:?Usage: $0 \"<text>\" [voice] [record_x] [record_y]}"
VOICE="${2:-zh-CN-YunxiNeural}"
# Record-button center (device-specific). Defaults are for Pixel 10 Pro Fold cover
# screen 1080x2364 with WeChat's "按住 说话" bar. Override via args if your layout differs.
REC_X="${3:-466}"
REC_Y="${4:-2247}"
ADB="${ADB:-/opt/homebrew/share/android-commandlinetools/platform-tools/adb}"
SILK_WASM="${SILK_WASM:-/opt/homebrew/lib/node_modules/openclaw/node_modules/silk-wasm}"
WXUID="${WXUID:-u0_a336}"
MM_CACHE="/data/data/com.tencent.mm/cache/hook_voice_src.amr"
TMPDIR="$(mktemp -d /tmp/ttsswap_XXXXXX)"
trap "rm -rf $TMPDIR" EXIT
echo "[1/5] TTS: \"$TEXT\" (voice=$VOICE)"
python3 -c "
import asyncio, edge_tts
asyncio.run(edge_tts.Communicate('''$TEXT''', '$VOICE').save('$TMPDIR/out.mp3'))
"
echo "[2/5] MP3 → PCM 16k mono → SILK V3"
ffmpeg -y -i "$TMPDIR/out.mp3" -ar 16000 -ac 1 -f s16le "$TMPDIR/out.pcm" -loglevel error
DUR=$(node -e "
const silk=require('$SILK_WASM'), fs=require('fs');
silk.encode(fs.readFileSync('$TMPDIR/out.pcm'),16000).then(r=>{fs.writeFileSync('$TMPDIR/out.silk',r.data);process.stdout.write(String(r.duration));});
")
echo " SILK $(wc -c < $TMPDIR/out.silk | tr -d ' ') bytes, duration=${DUR}ms"
echo "[3/5] Push SILK to WeChat cache"
$ADB push "$TMPDIR/out.silk" /sdcard/hook_voice_tmp.silk -q
$ADB shell "su -c 'cp /sdcard/hook_voice_tmp.silk $MM_CACHE && chmod 644 $MM_CACHE && chown $WXUID:$WXUID $MM_CACHE && rm /sdcard/hook_voice_tmp.silk'"
echo "[4/5] Arm swap (duration=$DUR)"
$ADB shell "su -c 'am broadcast -a com.hermes.wechat.ARM_VOICE_SWAP --es src_path $MM_CACHE --ei duration $DUR'" >/dev/null
# Hold the record button for TTS duration + 700ms so WeChat records long enough.
HOLD=$(( DUR + 700 ))
echo "[5/5] Hold '按住 说话' at ($REC_X,$REC_Y) for ${HOLD}ms → real recording → swap fires"
$ADB shell input swipe "$REC_X" "$REC_Y" "$REC_X" "$REC_Y" "$HOLD"
sleep 2
LOG=$($ADB shell "su -c 'ls -t /data/adb/lspd/log/*.log | head -1'" | tr -d '\r')
if $ADB shell "su -c 'grep -a \"VOICE SWAP DONE\" \"$LOG\"'" | tr -d '\r' | tail -1 | grep -q "VOICE SWAP DONE"; then
echo "✓ Swap fired: $($ADB shell "su -c 'grep -a \"VOICE SWAP DONE\" \"$LOG\"'" | tr -d '\r' | tail -1 | sed 's/.*VOICE SWAP DONE/VOICE SWAP DONE/')"
echo "✓ Voice sent. Recipient receives your TTS audio."
else
echo "✗ Swap did not fire — check: chat open? voice mode? record coords ($REC_X,$REC_Y) correct?"
echo " (a real recording must reach jg0.x.w; verify the hold actually recorded a voice bubble)"
fi