#!/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 "" [voice] [record_x] [record_y] # # Examples: # ./tts_voice_swap.sh "你好,我现在没办法说话,这是我用文字转成的语音。" # ./tts_voice_swap.sh "下午三点开会" zh-CN-YunxiNeural set -e TEXT="${1:?Usage: $0 \"\" [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