#!/data/data/com.termux/files/usr/bin/bash # voice_synth.sh — synthesize speech and send it as a WeChat voice message. # Runs FULLY ON-DEVICE in Termux (where Hermes lives): no Mac, no local TTS model. # # text → TTS (edge-tts cloud, free / or Doubao) → MP3 # → ffmpeg 16k mono PCM → silk-wasm SILK V3 (Tencent \x02#!SILK_V3 variant) # → stage into WeChat's data dir (uid + SELinux ctx) # → am broadcast SEND_VOICE_CGI (internal uploadvoice cgi, our synthetic voice send) # # The recipient gets a playable voice AND the sender sees a sent bubble. # # Usage: # voice_synth.sh "" [voice] [engine] # talker id as in rcontact.username (e.g. filehelper, wxid_xxx, yibooo) # what to say (UTF-8; for Doubao you can embed the rich scene prompt) # [voice] edge-tts voice name (default zh-CN-YunxiNeural male; e.g. zh-CN-XiaoxiaoNeural female) # [engine] edge (default, free cloud, no quota) | doubao (needs DOUBAO_KEY + quota) # # Test with filehelper first. edge-tts needs internet (it's a cloud service, not a local model). set -e WXID="$1"; TEXT="$2"; VOICE="${3:-zh-CN-YunxiNeural}"; ENGINE="${4:-edge}" [ -z "$WXID" ] || [ -z "$TEXT" ] && { echo "usage: voice_synth.sh \"\" [voice] [engine]"; exit 1; } PREFIX=/data/data/com.termux/files/usr SILK_WASM="$PREFIX/lib/node_modules/silk-wasm" export TMPDIR="$PREFIX/tmp"; mkdir -p "$TMPDIR" # Termux tmp (root/adb env may force /data/local/tmp) WORK=$(mktemp -d "$TMPDIR/voicesynth.XXXXXX") trap 'rm -rf "$WORK"' EXIT MP3="$WORK/tts.mp3"; PCM="$WORK/tts.pcm"; SILK="$WORK/tts.silk" # ── 1. text → MP3 ──────────────────────────────────────────────────────────── case "$ENGINE" in edge) echo "[1/3] edge-tts ($VOICE) → MP3" edge-tts --voice "$VOICE" --text "$TEXT" --write-media "$MP3" >/dev/null ;; doubao) : "${DOUBAO_KEY:?doubao engine needs DOUBAO_KEY env var}" echo "[1/3] Doubao seed-audio-1.0 → MP3" PROMPT=$(python3 -c "import json,sys;print(json.dumps(sys.argv[1]))" "$TEXT") curl -s --max-time 120 -X POST 'https://openspeech.bytedance.com/api/v3/tts/create' \ -H 'Content-Type: application/json' -H "X-Api-Key: $DOUBAO_KEY" \ --data-raw "{\"model\":\"seed-audio-1.0\",\"text_prompt\":$PROMPT,\"audio_config\":{\"format\":\"mp3\",\"sample_rate\":48000},\"watermark\":{}}" \ | python3 -c "import json,sys,base64; d=json.load(sys.stdin); sys.exit('Doubao error: '+str(d)) if 'audio' not in d else open('$MP3','wb').write(base64.b64decode(d['audio']))" ;; *) echo "unknown engine: $ENGINE (use edge|doubao)"; exit 1 ;; esac # ── 2. MP3 → 16k mono PCM → SILK V3 ────────────────────────────────────────── echo "[2/3] MP3 → 16k mono PCM → SILK V3" ffmpeg -y -i "$MP3" -ar 16000 -ac 1 -f s16le "$PCM" -loglevel error DUR=$(node -e "const s=require('$SILK_WASM'),fs=require('fs');s.encode(fs.readFileSync('$PCM'),16000).then(r=>{fs.writeFileSync('$SILK',r.data);process.stdout.write(String(Math.round(r.duration)));})") [ -s "$SILK" ] || { echo "SILK encode failed"; exit 1; } # ── 3. stage into WeChat data dir + fire SEND_VOICE_CGI (root) ──────────────── echo "[3/3] stage SILK + SEND_VOICE_CGI → $WXID (${DUR}ms)" WX_UID=$(su -c "stat -c %U /data/user/0/com.tencent.mm" | tr -d '\r') STAGE=/data/data/com.tencent.mm/files/hook_voice_cgi.silk su -c "cp '$SILK' '$STAGE' && chown $WX_UID:$WX_UID '$STAGE' && chmod 600 '$STAGE'; restorecon '$STAGE' 2>/dev/null; \ am broadcast -a com.hermes.wechat.SEND_VOICE_CGI --es to '$WXID' --es src_path '$STAGE' --ei duration $DUR" echo "done: sent ${DUR}ms voice to $WXID via $ENGINE."