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:
@@ -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')
|
||||
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 ----------
|
||||
# Fixes https://github.com/NousResearch/hermes-agent/issues/14181
|
||||
# tempfile.mkstemp() creates files as 0600; os.replace() preserves that mode,
|
||||
|
||||
@@ -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
|
||||
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
|
||||
exec /opt/hermes/docker/entrypoint.sh "$@"
|
||||
|
||||
@@ -1,11 +1,58 @@
|
||||
#!/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 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:
|
||||
code = f.read()
|
||||
# Accept path as first argument
|
||||
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
|
||||
old_edge = ''' else:
|
||||
@@ -77,20 +124,13 @@ new_piper = ''' else:
|
||||
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:
|
||||
if 'Default: Piper TTS' in code:
|
||||
print("Piper fallback already present")
|
||||
else:
|
||||
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)
|
||||
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)
|
||||
|
||||
with open(tts_path, 'w') as f:
|
||||
f.write(code)
|
||||
print("tts_tool.py patched successfully")
|
||||
print(f"tts_tool.py patched successfully at: {tts_path}")
|
||||
|
||||
Reference in New Issue
Block a user