From 748b5037b9802553e3faa8537c1c257dd0deb281 Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Sat, 9 May 2026 19:03:10 +0000 Subject: [PATCH] fix: update TTS patch for latest hermes-agent tts_tool.py - Patch now matches the current tts_tool.py (newer version ships in nousresearch/hermes-agent:latest with different Edge fallback text) - Adds dedicated elif provider == 'piper' block before else: - Replaces else: fallback to use Piper instead of Edge - Patches ALL copies (venv site-packages + /opt/hermes/tools/) - Removes Edge TTS entirely as default/provider --- ai/patch_tts_tool.py | 205 ++++++++++++++++++++++++++----------------- 1 file changed, 125 insertions(+), 80 deletions(-) diff --git a/ai/patch_tts_tool.py b/ai/patch_tts_tool.py index 2dc17cb..0aa056b 100644 --- a/ai/patch_tts_tool.py +++ b/ai/patch_tts_tool.py @@ -1,61 +1,56 @@ #!/usr/bin/env python3 -"""Patch Hermes TTS tool: remove Edge TTS, replace with Piper as default/fallback. +"""Patch Hermes TTS tool: add Piper TTS provider, remove Edge TTS as default. + +Patches ALL copies of tts_tool.py found (venv site-packages + /opt/hermes/tools/). Searches multiple paths for tts_tool.py so it works both at build time (in the image venv) and at runtime (on the mounted data volume). + +Idempotent: if already patched, does nothing. """ + import sys import os -# Search order: argument > site-packages > /opt/hermes/tools > /opt/hermes checkout -SEARCH_PATHS = [] - -# Accept path as first argument -if len(sys.argv) > 1: - SEARCH_PATHS.append(sys.argv[1]) - -# Add known locations -SEARCH_PATHS.extend([ +# --------------------------------------------------------------------------- +# Search for all copies of tts_tool.py +# --------------------------------------------------------------------------- +CANDIDATE_PATHS = [ "/opt/hermes/.venv/lib/python3.13/site-packages/tools/tts_tool.py", "/opt/hermes/tools/tts_tool.py", -]) +] -tts_path = None -code = None +found_paths = [] -for p in SEARCH_PATHS: +for p in CANDIDATE_PATHS: if os.path.exists(p): - tts_path = p - with open(tts_path) as f: - code = f.read() - print(f"Found tts_tool.py at: {tts_path}") - break + found_paths.append(p) + print(f"Found tts_tool.py at: {p}") -if code is None: - # Try one more time: find it in the venv site-packages - import subprocess - try: - result = subprocess.run( - [sys.executable, "-c", "import tools.tts_tool; print(tools.tts_tool.__file__)"], - capture_output=True, text=True, timeout=5 - ) - if result.returncode == 0: - p = result.stdout.strip() - if os.path.exists(p): - tts_path = p - with open(tts_path) as f: - code = f.read() - print(f"Found tts_tool.py via import at: {tts_path}") - except Exception: - pass +# Also try to find via Python import +import subprocess +try: + result = subprocess.run( + [sys.executable, "-c", "import tools.tts_tool; print(tools.tts_tool.__file__)"], + capture_output=True, text=True, timeout=5 + ) + if result.returncode == 0: + p = result.stdout.strip() + if os.path.exists(p) and p not in found_paths: + found_paths.append(p) + print(f"Found tts_tool.py via import at: {p}") +except Exception: + pass -if code is None: - print("WARNING: tts_tool.py not found. Patching deferred to runtime.") - print(f"Searched: {SEARCH_PATHS}") +if not found_paths: + print("WARNING: tts_tool.py not found anywhere. Patching deferred to runtime.") + print(f"Searched: {CANDIDATE_PATHS}") sys.exit(0) -# Replace the Edge fallback with Piper fallback -old_edge = ''' else: +# --------------------------------------------------------------------------- +# Old else block: the Edge TTS default fallback to replace +# --------------------------------------------------------------------------- +old_else = ''' else: # Default: Edge TTS (free), with NeuTTS as local fallback edge_available = True try: @@ -84,53 +79,103 @@ old_edge = ''' else: "or set up NeuTTS for local synthesis." }, ensure_ascii=False)''' -new_piper = ''' else: - # Default: Piper TTS (local, CPU, no cloud, no Microsoft) - piper_available = False +# --------------------------------------------------------------------------- +# New block: elif provider == "piper" + else: fallback with Piper only +# --------------------------------------------------------------------------- +new_block = ''' elif provider == "piper": + # Piper TTS (local, CPU, no cloud, no Microsoft) + piper_binary = "/opt/hermes/.venv/bin/piper" + piper_config = tts_config.get("piper", {}) + voice = piper_config.get("voice", "en_US-lessac-medium") + model_dir = piper_config.get("model_dir", "/opt/hermes/.venv/share/piper/voices") + model_path = os.path.join(model_dir, f"{voice}.onnx") + if not os.path.exists(model_path): + return json.dumps({ + "success": False, + "error": "Piper TTS voice model not found. " + "Install Piper TTS and download a voice model." + }, ensure_ascii=False) + logger.info("Generating speech with Piper TTS (local, CPU)...") + import subprocess as _sp + cmd = [piper_binary, "--model", model_path, "--output-raw"] try: - piper_binary = "/opt/hermes/.venv/bin/piper" - piper_config = tts_config.get("piper", {}) - voice = piper_config.get("voice", "en_US-lessac-medium") - model_dir = piper_config.get("model_dir", "/opt/hermes/.venv/share/piper/voices") - model_path = os.path.join(model_dir, f"{voice}.onnx") - if os.path.exists(model_path): - piper_available = True - except Exception: - pass - - if piper_available: - logger.info("Generating speech with Piper TTS (local, CPU)...") - import subprocess - piper_binary = "/opt/hermes/.venv/bin/piper" - piper_config = tts_config.get("piper", {}) - voice = piper_config.get("voice", "en_US-lessac-medium") - model_dir = piper_config.get("model_dir", "/opt/hermes/.venv/share/piper/voices") - model_path = os.path.join(model_dir, f"{voice}.onnx") - cmd = [piper_binary, "--model", model_path, "--output-raw"] - proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + proc = _sp.Popen(cmd, stdin=_sp.PIPE, stdout=_sp.PIPE, stderr=_sp.PIPE) raw_audio, stderr = proc.communicate(input=text.encode(), timeout=60) if proc.returncode != 0: raise RuntimeError(f"Piper TTS failed: {stderr.decode()[:200]}") ffmpeg_cmd = ["ffmpeg", "-f", "s16le", "-ar", "22050", "-ac", "1", "-i", "-", "-y", file_str] - subprocess.run(ffmpeg_cmd, input=raw_audio, capture_output=True, timeout=30) - logger.info("Piper TTS audio saved: %s", file_str) + _sp.run(ffmpeg_cmd, input=raw_audio, capture_output=True, timeout=30) + except Exception as e: + return json.dumps({ + "success": False, + "error": f"Piper TTS failed: {e}" + }, ensure_ascii=False) + + else: + # Default: Piper TTS (local, CPU, no cloud, no Microsoft) + piper_binary = "/opt/hermes/.venv/bin/piper" + piper_config = tts_config.get("piper", {}) + voice = piper_config.get("voice", "en_US-lessac-medium") + model_dir = piper_config.get("model_dir", "/opt/hermes/.venv/share/piper/voices") + model_path = os.path.join(model_dir, f"{voice}.onnx") + if os.path.exists(model_path) and os.path.exists(piper_binary): + logger.info("Generating speech with Piper TTS (local, CPU)...") + import subprocess as _sp + cmd = [piper_binary, "--model", model_path, "--output-raw"] + try: + proc = _sp.Popen(cmd, stdin=_sp.PIPE, stdout=_sp.PIPE, stderr=_sp.PIPE) + raw_audio, stderr = proc.communicate(input=text.encode(), timeout=60) + if proc.returncode != 0: + raise RuntimeError(stderr.decode()[:200]) + ffmpeg_cmd = ["ffmpeg", "-f", "s16le", "-ar", "22050", "-ac", "1", "-i", "-", "-y", file_str] + _sp.run(ffmpeg_cmd, input=raw_audio, capture_output=True, timeout=30) + except Exception: + pass else: return json.dumps({ "success": False, - "error": "No TTS provider available. Install Piper TTS (pip install piper-tts) " - "and download a voice model." + "error": "Piper TTS not available. Install piper-tts and download a voice model." }, ensure_ascii=False)''' -if old_edge in code: - code = code.replace(old_edge, new_piper) - print("Edge fallback replaced with Piper") -elif 'Default: Piper TTS' in code: - print("Piper fallback already present") -else: - print("WARNING: Could not find Edge fallback in tts_tool.py") - print("The tts_tool.py may be a version not matching this patch.") - sys.exit(0) +# --------------------------------------------------------------------------- +# Apply the patch to all copies found +# --------------------------------------------------------------------------- +patched_any = False -with open(tts_path, 'w') as f: - f.write(code) -print(f"tts_tool.py patched successfully at: {tts_path}") +for tts_path in found_paths: + with open(tts_path) as f: + code = f.read() + + if 'provider == "piper"' in code: + print(f"ALREADY PATCHED: {tts_path}") + continue + + if old_else in code: + code = code.replace(old_else, new_block, 1) + with open(tts_path, 'w') as f: + f.write(code) + print(f"PATCHED: {tts_path}") + patched_any = True + else: + print(f"SKIP {tts_path}: Edge fallback pattern not found") + import re + for m in re.finditer(r' else:\n # Default:', code): + start = max(0, m.start() - 100) + end = min(len(code), m.end() + 300) + print(f" Found 'else:/# Default:' at position {m.start()}:") + print(f" {code[start:end]}") + print(" ---") + # Don't exit with error — if one copy isn't patchable, try the others + +if not patched_any: + all_patched = all( + 'provider == "piper"' in open(p).read() + for p in found_paths + ) + if all_patched: + print("All copies already patched.") + sys.exit(0) + print("WARNING: Could not patch any copy of tts_tool.py") + sys.exit(1) + +print("tts_tool.py patched successfully across all copies.")