diff --git a/ai/compose.yml b/ai/compose.yml index dbca2ef..3c4c4e9 100755 --- a/ai/compose.yml +++ b/ai/compose.yml @@ -7,20 +7,13 @@ services: - default container_name: hermes restart: always - # Use the image default ENTRYPOINT ["/init", "/opt/hermes/docker/main-wrapper.sh"] - # for proper s6-overlay supervision. The CMD runs our multi-profile launcher - # which spawns per-profile gateways in background, then the default gateway - # in foreground (keeps the container alive). - command: ["/usr/local/bin/start-hermes.sh"] + # Gateway run enables the internal API server on port 8642 + command: gateway run environment: - HERMES_UID=10000 - HERMES_GID=10000 - OLLAMA_HOST=http://ollama-cpu:11434 - HERMES_DASHBOARD=1 - # Multi-profile: comma-separated list of profiles to run as gateways. - # start-hermes.sh reads this and starts one gateway per profile. - # Add profiles here when they exist on disk (e.g. default,researcher,writer) - - HERMES_PROFILES=ashley,claire,finn,matt,paul - API_SERVER_ENABLED=true - API_SERVER_PORT=8642 - API_SERVER_HOST=0.0.0.0 diff --git a/ai/hermes/Dockerfile b/ai/hermes/Dockerfile index e94a7ea..599a930 100644 --- a/ai/hermes/Dockerfile +++ b/ai/hermes/Dockerfile @@ -63,11 +63,6 @@ PYEOF # Launches one gateway process per profile (HERMES_PROFILES env var) COPY --chmod=0755 run-multi-gateways.sh /usr/local/bin/run-multi-gateways.sh -# ---------- Install s6-overlay compatible startup script ---------- -# Runs as the CMD via s6-overlay's main-program model. -# Replaces the old bash->tini->entrypoint.sh chain that caused SIGTERM crash loops. -COPY --chmod=0755 start-hermes.sh /usr/local/bin/start-hermes.sh - # ---------- Runtime ---------- USER hermes ENV HERMES_HOME=/opt/data diff --git a/ai/hermes/patch_tts_tool.py b/ai/hermes/patch_tts_tool.py deleted file mode 100644 index 0aa056b..0000000 --- a/ai/hermes/patch_tts_tool.py +++ /dev/null @@ -1,181 +0,0 @@ -#!/usr/bin/env python3 -"""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 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", -] - -found_paths = [] - -for p in CANDIDATE_PATHS: - if os.path.exists(p): - found_paths.append(p) - print(f"Found tts_tool.py at: {p}") - -# 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 not found_paths: - print("WARNING: tts_tool.py not found anywhere. Patching deferred to runtime.") - print(f"Searched: {CANDIDATE_PATHS}") - sys.exit(0) - -# --------------------------------------------------------------------------- -# 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: - _import_edge_tts() - except ImportError: - edge_available = False - - if edge_available: - logger.info("Generating speech with Edge TTS...") - try: - import concurrent.futures - with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool: - pool.submit( - lambda: asyncio.run(_generate_edge_tts(text, file_str, tts_config)) - ).result(timeout=60) - except RuntimeError: - asyncio.run(_generate_edge_tts(text, file_str, tts_config)) - elif _check_neutts_available(): - logger.info("Edge TTS not available, falling back to NeuTTS (local)...") - provider = "neutts" - _generate_neutts(text, file_str, tts_config) - else: - return json.dumps({ - "success": False, - "error": "No TTS provider available. Install edge-tts (pip install edge-tts) " - "or set up NeuTTS for local synthesis." - }, ensure_ascii=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: - 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] - _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": "Piper TTS not available. Install piper-tts and download a voice model." - }, ensure_ascii=False)''' - -# --------------------------------------------------------------------------- -# Apply the patch to all copies found -# --------------------------------------------------------------------------- -patched_any = False - -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.") diff --git a/ai/hermes/run-multi-gateways.sh b/ai/hermes/run-multi-gateways.sh deleted file mode 100755 index f23ac78..0000000 --- a/ai/hermes/run-multi-gateways.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/bash -# Multi-gateway launcher for HERMES_PROFILES env var. -# Reads comma-separated profile names, spawns one gateway per profile. -# Designed to run before the main entrypoint — gateways run in background. -set -e - -if [ -z "${HERMES_PROFILES}" ]; then - echo "HERMES_PROFILES not set — skipping multi-gateway launch" - exit 0 -fi - -# Source venv to make 'hermes' available (entrypoint.sh sources it later, -# but we need it NOW for the background gateways) -HERMES_BIN="/opt/hermes/.venv/bin/hermes" -if [ ! -x "$HERMES_BIN" ]; then - echo "ERROR: hermes binary not found at $HERMES_BIN" - exit 1 -fi - -mkdir -p /opt/data/logs - -IFS=',' read -ra PROFILES <<< "${HERMES_PROFILES}" -for profile in "${PROFILES[@]}"; do - profile="$(echo "${profile}" | xargs)" # trim whitespace - [ -z "${profile}" ] && continue - - echo "Starting gateway for profile: ${profile}" - nohup env API_SERVER_ENABLED=false API_SERVER_KEY= gosu hermes "$HERMES_BIN" --profile "${profile}" gateway run \ - >> "/opt/data/logs/gateway-${profile}.log" 2>&1 & -done - -echo "All gateways launched: ${HERMES_PROFILES}" diff --git a/ai/hermes/start-hermes.sh b/ai/hermes/start-hermes.sh deleted file mode 100644 index b6c3787..0000000 --- a/ai/hermes/start-hermes.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash -# Multi-profile + default gateway launcher — runs as the CMD via s6-overlay. -# -# The image's default ENTRYPOINT ["/init", "/opt/hermes/docker/main-wrapper.sh"] -# starts the s6 supervision tree, then exec's main-wrapper.sh with the CMD args. -# main-wrapper.sh sources the venv, drops to the hermes user via s6-setuidgid, -# and exec's this script. -# -# This script: -# 1. Launches per-profile background gateways (HERMES_PROFILES env var) -# 2. Starts the default gateway in foreground (keeps the container alive) -# -# Replaces the old approach of chaining bash -> tini -g -> deprecated entrypoint.sh -# which bypassed s6-overlay and caused the SIGTERM crash loop. - -set -e - -HERMES_BIN="/opt/hermes/.venv/bin/hermes" - -# --- Multi-profile gateways (background) --- -if [ -n "${HERMES_PROFILES:-}" ]; then - echo "[start-hermes] Launching per-profile gateways: ${HERMES_PROFILES}" - IFS=',' read -ra PROFILES <<< "${HERMES_PROFILES}" - for profile in "${PROFILES[@]}"; do - profile="$(echo "${profile}" | xargs)" # trim whitespace - [ -z "${profile}" ] && continue - echo "[start-hermes] -> background gateway for profile '${profile}'" - # No gosu/s6-setuidgid needed — we're already running as the hermes user - # (main-wrapper.sh drops privileges before exec'ing this script). - nohup "${HERMES_BIN}" --profile "${profile}" gateway run \ - >> "/opt/data/logs/gateway-${profile}.log" 2>&1 & - done - echo "[start-hermes] All profile gateways launched" -fi - -# --- Default gateway (foreground — keeps container alive) --- -echo "[start-hermes] Starting default gateway (foreground)" -exec "${HERMES_BIN}" gateway run