diff --git a/phoneuse/wechat_hook_send.py b/phoneuse/wechat_hook_send.py index 6383290..880ab7f 100644 --- a/phoneuse/wechat_hook_send.py +++ b/phoneuse/wechat_hook_send.py @@ -286,15 +286,26 @@ def _tts_to_silk(text: str, voice: str, tmp_dir: str) -> tuple[str, int]: asyncio.run(_synth()) # 2. ffmpeg → PCM16LE 16kHz mono - if not shutil.which("ffmpeg"): + # shutil.which misses Termux bin when running under root su; probe extra paths + _FFMPEG_CANDIDATES = ["ffmpeg", + "/data/data/com.termux/files/usr/bin/ffmpeg", + "/usr/bin/ffmpeg", "/usr/local/bin/ffmpeg"] + ffmpeg_bin = next((p for p in _FFMPEG_CANDIDATES if shutil.which(p) or + (os.path.isfile(p) and os.access(p, os.X_OK))), None) + if not ffmpeg_bin: raise RuntimeError("ffmpeg not found: (Termux) pkg install ffmpeg") subprocess.run( - ["ffmpeg", "-y", "-i", mp3, "-ar", "16000", "-ac", "1", "-f", "s16le", pcm], + [ffmpeg_bin, "-y", "-i", mp3, "-ar", "16000", "-ac", "1", "-f", "s16le", pcm], check=True, capture_output=True ) # 3. Node.js silk-wasm → SILK V3 (already includes \x02#!SILK_V3 prefix) - if not shutil.which("node"): + _NODE_CANDIDATES = ["node", + "/data/data/com.termux/files/usr/bin/node", + "/usr/bin/node", "/usr/local/bin/node"] + node_bin = next((p for p in _NODE_CANDIDATES if shutil.which(p) or + (os.path.isfile(p) and os.access(p, os.X_OK))), None) + if not node_bin: raise RuntimeError("node not found: (Termux) pkg install nodejs") # Locate silk-wasm: check global npm, then openclaw bundle @@ -315,7 +326,7 @@ silk.encode(fs.readFileSync('{pcm}'), 16000).then(function(r) {{ process.stdout.write(String(r.duration)); }}).catch(function(e) {{ process.stderr.write(String(e)); process.exit(1); }}); """ - r = subprocess.run(["node", "-e", node_script], capture_output=True, text=True, timeout=60) + r = subprocess.run([node_bin, "-e", node_script], capture_output=True, text=True, timeout=60) if r.returncode != 0: raise RuntimeError(f"silk-wasm encode failed: {r.stderr or r.stdout}. " "Install: npm install -g silk-wasm") @@ -357,32 +368,10 @@ def send_voice_tts_via_hook( except Exception as e: return {"status": "error", "error": f"TTS/SILK generation failed: {e}"} - wechat_cache = "/data/data/com.tencent.mm/cache/hook_voice_src.amr" - if _ON_DEVICE: - stage = _root_sh( - f'cp "{silk_path}" "{wechat_cache}" && ' - f'chmod 644 "{wechat_cache}" && ' - f'chown u0_a330:u0_a330 "{wechat_cache}"' - ) - else: - adb = _adb_prefix() - remote_tmp = "/sdcard/hook_voice_tts_tmp.amr" - subprocess.run(adb + ["push", silk_path, remote_tmp], - check=True, capture_output=True) - stage = _root_sh( - f'cp "{remote_tmp}" "{wechat_cache}" && ' - f'chmod 644 "{wechat_cache}" && ' - f'chown u0_a330:u0_a330 "{wechat_cache}" && ' - f'rm -f "{remote_tmp}"' - ) - - if stage.returncode != 0: - return {"status": "error", - "error": f"failed to stage SILK: {stage.stderr or stage.stdout}"} - - # TemporaryDirectory is cleaned up; wechat_cache is intact - result = send_voice_via_hook(wxid=wxid, silk_path=wechat_cache, - duration_ms=duration_ms) + # Delegate staging + broadcast to send_voice_via_hook (it handles both + # on-device and off-device paths). Must call while tmp dir is still alive. + result = send_voice_via_hook(wxid=wxid, silk_path=silk_path, + duration_ms=duration_ms) result["tts_text"] = text[:100] result["tts_voice"] = voice return result