Commit 00e0032691b1a8e8dbb387880872a95148092607
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) -> tuple[str, int]: | @@ -286,15 +286,26 @@ def _tts_to_silk(text: str, voice: str, tmp_dir: str) -> 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('{pcm}'), 16000).then(function(r) {{ | @@ -315,7 +326,7 @@ silk.encode(fs.readFileSync('{pcm}'), 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 |