Commit 00e0032691b1a8e8dbb387880872a95148092607

Authored by yibo
1 parent aabda230

fix(phoneuse): 修复 send_voice_tts_via_hook 自复制 bug + 补全 PATH 检测

- 去掉 send_voice_tts_via_hook 里多余的 staging 步骤:
  生成 SILK 后直接把临时路径传给 send_voice_via_hook,
  由它统一做 cp → wechat_cache + broadcast(避免 cp A→A 报错)
- _tts_to_silk: ffmpeg/node 路径检测增加 Termux 全路径候选,
  root su shell 下 PATH 不含 Termux bin 时也能找到

验证:device 侧 Python 直调 send_voice_tts_via_hook → filehelper 5620ms 语音 OK

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Showing 1 changed file with 19 additions and 30 deletions
phoneuse/wechat_hook_send.py
@@ -286,15 +286,26 @@ def _tts_to_silk(text: str, voice: str, tmp_dir: str) -&gt; tuple[str, int]: @@ -286,15 +286,26 @@ def _tts_to_silk(text: str, voice: str, tmp_dir: str) -&gt; tuple[str, int]:
286 asyncio.run(_synth()) 286 asyncio.run(_synth())
287 287
288 # 2. ffmpeg → PCM16LE 16kHz mono 288 # 2. ffmpeg → PCM16LE 16kHz mono
289 - if not shutil.which("ffmpeg"): 289 + # shutil.which misses Termux bin when running under root su; probe extra paths
  290 + _FFMPEG_CANDIDATES = ["ffmpeg",
  291 + "/data/data/com.termux/files/usr/bin/ffmpeg",
  292 + "/usr/bin/ffmpeg", "/usr/local/bin/ffmpeg"]
  293 + ffmpeg_bin = next((p for p in _FFMPEG_CANDIDATES if shutil.which(p) or
  294 + (os.path.isfile(p) and os.access(p, os.X_OK))), None)
  295 + if not ffmpeg_bin:
290 raise RuntimeError("ffmpeg not found: (Termux) pkg install ffmpeg") 296 raise RuntimeError("ffmpeg not found: (Termux) pkg install ffmpeg")
291 subprocess.run( 297 subprocess.run(
292 - ["ffmpeg", "-y", "-i", mp3, "-ar", "16000", "-ac", "1", "-f", "s16le", pcm], 298 + [ffmpeg_bin, "-y", "-i", mp3, "-ar", "16000", "-ac", "1", "-f", "s16le", pcm],
293 check=True, capture_output=True 299 check=True, capture_output=True
294 ) 300 )
295 301
296 # 3. Node.js silk-wasm → SILK V3 (already includes \x02#!SILK_V3 prefix) 302 # 3. Node.js silk-wasm → SILK V3 (already includes \x02#!SILK_V3 prefix)
297 - if not shutil.which("node"): 303 + _NODE_CANDIDATES = ["node",
  304 + "/data/data/com.termux/files/usr/bin/node",
  305 + "/usr/bin/node", "/usr/local/bin/node"]
  306 + node_bin = next((p for p in _NODE_CANDIDATES if shutil.which(p) or
  307 + (os.path.isfile(p) and os.access(p, os.X_OK))), None)
  308 + if not node_bin:
298 raise RuntimeError("node not found: (Termux) pkg install nodejs") 309 raise RuntimeError("node not found: (Termux) pkg install nodejs")
299 310
300 # Locate silk-wasm: check global npm, then openclaw bundle 311 # Locate silk-wasm: check global npm, then openclaw bundle
@@ -315,7 +326,7 @@ silk.encode(fs.readFileSync(&#39;{pcm}&#39;), 16000).then(function(r) {{ @@ -315,7 +326,7 @@ silk.encode(fs.readFileSync(&#39;{pcm}&#39;), 16000).then(function(r) {{
315 process.stdout.write(String(r.duration)); 326 process.stdout.write(String(r.duration));
316 }}).catch(function(e) {{ process.stderr.write(String(e)); process.exit(1); }}); 327 }}).catch(function(e) {{ process.stderr.write(String(e)); process.exit(1); }});
317 """ 328 """
318 - r = subprocess.run(["node", "-e", node_script], capture_output=True, text=True, timeout=60) 329 + r = subprocess.run([node_bin, "-e", node_script], capture_output=True, text=True, timeout=60)
319 if r.returncode != 0: 330 if r.returncode != 0:
320 raise RuntimeError(f"silk-wasm encode failed: {r.stderr or r.stdout}. " 331 raise RuntimeError(f"silk-wasm encode failed: {r.stderr or r.stdout}. "
321 "Install: npm install -g silk-wasm") 332 "Install: npm install -g silk-wasm")
@@ -357,32 +368,10 @@ def send_voice_tts_via_hook( @@ -357,32 +368,10 @@ def send_voice_tts_via_hook(
357 except Exception as e: 368 except Exception as e:
358 return {"status": "error", "error": f"TTS/SILK generation failed: {e}"} 369 return {"status": "error", "error": f"TTS/SILK generation failed: {e}"}
359 370
360 - wechat_cache = "/data/data/com.tencent.mm/cache/hook_voice_src.amr"  
361 - if _ON_DEVICE:  
362 - stage = _root_sh(  
363 - f'cp "{silk_path}" "{wechat_cache}" && '  
364 - f'chmod 644 "{wechat_cache}" && '  
365 - f'chown u0_a330:u0_a330 "{wechat_cache}"'  
366 - )  
367 - else:  
368 - adb = _adb_prefix()  
369 - remote_tmp = "/sdcard/hook_voice_tts_tmp.amr"  
370 - subprocess.run(adb + ["push", silk_path, remote_tmp],  
371 - check=True, capture_output=True)  
372 - stage = _root_sh(  
373 - f'cp "{remote_tmp}" "{wechat_cache}" && '  
374 - f'chmod 644 "{wechat_cache}" && '  
375 - f'chown u0_a330:u0_a330 "{wechat_cache}" && '  
376 - f'rm -f "{remote_tmp}"'  
377 - )  
378 -  
379 - if stage.returncode != 0:  
380 - return {"status": "error",  
381 - "error": f"failed to stage SILK: {stage.stderr or stage.stdout}"}  
382 -  
383 - # TemporaryDirectory is cleaned up; wechat_cache is intact  
384 - result = send_voice_via_hook(wxid=wxid, silk_path=wechat_cache,  
385 - duration_ms=duration_ms) 371 + # Delegate staging + broadcast to send_voice_via_hook (it handles both
  372 + # on-device and off-device paths). Must call while tmp dir is still alive.
  373 + result = send_voice_via_hook(wxid=wxid, silk_path=silk_path,
  374 + duration_ms=duration_ms)
386 result["tts_text"] = text[:100] 375 result["tts_text"] = text[:100]
387 result["tts_voice"] = voice 376 result["tts_voice"] = voice
388 return result 377 return result