From 5c504501d381c4c9022bc5dc4e090e758e9136c3 Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Sat, 9 May 2026 00:20:57 +0000 Subject: [PATCH 01/25] feat: add ROCm GPU env vars to hermes service for faster-whisper STT --- ai/compose.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ai/compose.yml b/ai/compose.yml index 5780324..db5b16e 100644 --- a/ai/compose.yml +++ b/ai/compose.yml @@ -38,7 +38,13 @@ services: - API_SERVER_HOST=0.0.0.0 - API_SERVER_KEY=hermes_local_key - GATEWAY_ALLOW_ALL_USERS=true - - OPENROUTER_API_KEY=${OPENROUTER_API_KEY} + - OPENROUTER_API_KEY=${OPEN...KEY} + # ROCm for GPU-accelerated faster-whisper STT + - HSA_OVERRIDE_GFX_VERSION=9.0.6 + - HCC_AMDGPU_TARGET=gfx906 + - HIP_VISIBLE_DEVICES=0,1 + - ROCR_VISIBLE_DEVICES=0,1 + - HSA_ENABLE_SDMA=0 volumes: - /mnt/HoardingCow_docker_data/Hermes/data:/opt/data devices: -- 2.49.1 From f5171a7d6e8514fd087065c117ffe6abd5a6b657 Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Sat, 9 May 2026 02:38:23 +0000 Subject: [PATCH 02/25] fix: replace Dockerfile with simplified stable-slim version --- ai/Dockerfile | 54 ++++++++++++++++----------------------------------- 1 file changed, 17 insertions(+), 37 deletions(-) diff --git a/ai/Dockerfile b/ai/Dockerfile index 1edd524..201f9de 100644 --- a/ai/Dockerfile +++ b/ai/Dockerfile @@ -1,71 +1,51 @@ -FROM ghcr.io/astral-sh/uv:0.11.6-python3.13-trixie@sha256:b3c543b6c4f23a5f2df22866bd7857e5d304b67a564f4feab6ac22044dde719b AS uv_source -FROM tianon/gosu:1.19-trixie@sha256:3b176695959c71e123eb390d427efc665eeb561b1540e82679c15e992006b8b9 AS gosu_source -FROM debian:13.4 +# 1. On récupère la version la plus récente d'UV +FROM ghcr.io/astral-sh/uv:latest AS uv_source + +# 2. Image de base stable +FROM debian:stable-slim # Disable Python stdout buffering to ensure logs are printed immediately ENV PYTHONUNBUFFERED=1 -# Store Playwright browsers outside the volume mount so the build-time -# install survives the /opt/data volume overlay at runtime. -ENV PLAYWRIGHT_BROWSERS_PATH=/opt/hermes/.playwright - # Install system dependencies in one layer, clear APT cache # tini reaps orphaned zombie processes (MCP stdio subprocesses, git, bun, etc.) -# that would otherwise accumulate when hermes runs as PID 1. See #15012. RUN apt-get update && \ apt-get install -y --no-install-recommends \ - build-essential nodejs npm python3 ripgrep ffmpeg gcc python3-dev libffi-dev procps git openssh-client docker-cli tini \ + build-essential python3 ripgrep ffmpeg gcc python3-dev libffi-dev procps git openssh-client docker-cli tini \ curl poppler-utils imagemagick \ - chromium xvfb fonts-noto-color-emoji fonts-unifont fonts-liberation fonts-ipafont-gothic fonts-wqy-zenhei fonts-tlwg-loma-otf fonts-freefont-ttf \ - libasound2t64 libatk-bridge2.0-0t64 libatk1.0-0t64 libatspi2.0-0t64 libcairo2 libcups2t64 libdbus-1-3 libdrm2 libgbm1 libglib2.0-0t64 libnspr4 libnss3 libpango-1.0-0 libx11-6 libxcb1 libxcomposite1 libxdamage1 libxext6 libxfixes3 libxkbcommon0 libxrandr2 \ texlive-latex-base texlive-latex-extra texlive-fonts-recommended texlive-xetex texlive-science \ qemu-user-static binfmt-support qemu-user-binfmt \ emacs-nox \ libportaudio2 && \ rm -rf /var/lib/apt/lists/* -# Non-root user for runtime; UID can be overridden via HERMES_UID at runtime +# Création de l'utilisateur 'hermes' directement avec les bons accès RUN useradd -u 10000 -m -d /opt/data hermes -COPY --chmod=0755 --from=gosu_source /gosu /usr/local/bin/ -COPY --chmod=0755 --from=uv_source /usr/local/bin/uv /usr/local/bin/uvx /usr/local/bin/ +# Copie d'uv (dernière version) +COPY --chmod=0755 --from=uv_source /uv /usr/local/bin/ WORKDIR /opt/hermes -# ---------- Layer-cached dependency install ---------- -# Copy only package manifests first so npm install + Playwright are cached -# unless the lockfiles themselves change. -COPY package.json package-lock.json ./ -COPY web/package.json web/package-lock.json web/ +# On donne la propriété du dossier de travail à l'utilisateur hermes +RUN chown hermes:hermes /opt/hermes -RUN npm install --prefer-offline --no-audit && \ - npx playwright install --with-deps chromium --only-shell && \ - (cd web && npm install --prefer-offline --no-audit) && \ - npm cache clean --force +# Passer immédiatement sous l'utilisateur hermes pour tout le reste du build +USER hermes # ---------- Source code ---------- -# .dockerignore excludes node_modules, so the installs above survive. +# On copie tout le projet d'un coup sans assumer la présence de fichiers de lock spécifiques COPY --chown=hermes:hermes . . -# Build web dashboard (Vite outputs to hermes_cli/web_dist/) -RUN cd web && npm run build - -# ---------- Permissions ---------- -# Make install dir world-readable so any HERMES_UID can read it at runtime. -# The venv needs to be traversable too. -USER root -RUN chmod -R a+rX /opt/hermes -# Start as root so the entrypoint can usermod/groupmod + gosu. -# If HERMES_UID is unset, the entrypoint drops to the default hermes user (10000). - # ---------- Python virtualenv ---------- RUN uv venv && \ - uv pip install --no-cache-dir -e ".[all]" && \ uv pip install --no-cache-dir sounddevice numpy faster-whisper # ---------- Runtime ---------- -ENV HERMES_WEB_DIST=/opt/hermes/hermes_cli/web_dist ENV HERMES_HOME=/opt/data ENV PATH="/opt/data/.local/bin:${PATH}" + VOLUME [ "/opt/data" ] + +# Le conteneur tourne de manière ultra-sécurisée sous l'utilisateur hermes dès le départ ENTRYPOINT [ "/usr/bin/tini", "-g", "--", "/opt/hermes/docker/entrypoint.sh" ] -- 2.49.1 From c2471818b29d047a2c68d60145a21e989c948b10 Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Sat, 9 May 2026 04:09:57 +0000 Subject: [PATCH 03/25] feat: add ROCm + Coqui TTS with GPU support to Dockerfile --- ai/Dockerfile | 111 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 110 insertions(+), 1 deletion(-) diff --git a/ai/Dockerfile b/ai/Dockerfile index 201f9de..8291da3 100644 --- a/ai/Dockerfile +++ b/ai/Dockerfile @@ -16,7 +16,8 @@ RUN apt-get update && \ texlive-latex-base texlive-latex-extra texlive-fonts-recommended texlive-xetex texlive-science \ qemu-user-static binfmt-support qemu-user-binfmt \ emacs-nox \ - libportaudio2 && \ + libportaudio2 \ + hipcc espeak-ng && \ rm -rf /var/lib/apt/lists/* # Création de l'utilisateur 'hermes' directement avec les bons accès @@ -30,6 +31,75 @@ WORKDIR /opt/hermes # On donne la propriété du dossier de travail à l'utilisateur hermes RUN chown hermes:hermes /opt/hermes +# ---------- Coqui TTS venv (Python 3.11 + PyTorch ROCm) ---------- +# Install Python 3.11 via uv for Coqui compatibility +RUN uv python install 3.11 + +# Create the coqui venv and install PyTorch ROCm + TTS +RUN uv venv --python 3.11 /opt/coqui-tts +RUN uv pip install --python /opt/coqui-tts/bin/python3 --no-cache-dir \ + torch==2.3.1+rocm5.7 \ + torchaudio==2.3.1+rocm5.7 \ + --index-url https://download.pytorch.org/whl/rocm5.7 +RUN uv pip install --python /opt/coqui-tts/bin/python3 --no-cache-dir TTS setuptools + +# Fix executable stack on bundled torch AMD libraries (required for ROCm) +RUN /opt/coqui-tts/bin/python3 -c " +import struct, os, glob +torch_lib = '/opt/coqui-tts/lib/python3.11/site-packages/torch/lib' +for so in glob.glob(os.path.join(torch_lib, '*.so*')): + try: + with open(so, 'r+b') as f: + if f.read(4) != b'\x7fELF': continue + f.seek(0) + h = f.read(64) + e_phoff = struct.unpack_from(' Date: Sat, 9 May 2026 13:24:08 +0000 Subject: [PATCH 04/25] fix: replace Coqui/ROCm with Piper TTS (simpler, local, CPU) --- ai/Dockerfile | 71 +-------------------------------------------------- 1 file changed, 1 insertion(+), 70 deletions(-) diff --git a/ai/Dockerfile b/ai/Dockerfile index 8291da3..7de3171 100644 --- a/ai/Dockerfile +++ b/ai/Dockerfile @@ -16,8 +16,7 @@ RUN apt-get update && \ texlive-latex-base texlive-latex-extra texlive-fonts-recommended texlive-xetex texlive-science \ qemu-user-static binfmt-support qemu-user-binfmt \ emacs-nox \ - libportaudio2 \ - hipcc espeak-ng && \ + libportaudio2 && \ rm -rf /var/lib/apt/lists/* # Création de l'utilisateur 'hermes' directement avec les bons accès @@ -31,74 +30,6 @@ WORKDIR /opt/hermes # On donne la propriété du dossier de travail à l'utilisateur hermes RUN chown hermes:hermes /opt/hermes -# ---------- Coqui TTS venv (Python 3.11 + PyTorch ROCm) ---------- -# Install Python 3.11 via uv for Coqui compatibility -RUN uv python install 3.11 - -# Create the coqui venv and install PyTorch ROCm + TTS -RUN uv venv --python 3.11 /opt/coqui-tts -RUN uv pip install --python /opt/coqui-tts/bin/python3 --no-cache-dir \ - torch==2.3.1+rocm5.7 \ - torchaudio==2.3.1+rocm5.7 \ - --index-url https://download.pytorch.org/whl/rocm5.7 -RUN uv pip install --python /opt/coqui-tts/bin/python3 --no-cache-dir TTS setuptools - -# Fix executable stack on bundled torch AMD libraries (required for ROCm) -RUN /opt/coqui-tts/bin/python3 -c " -import struct, os, glob -torch_lib = '/opt/coqui-tts/lib/python3.11/site-packages/torch/lib' -for so in glob.glob(os.path.join(torch_lib, '*.so*')): - try: - with open(so, 'r+b') as f: - if f.read(4) != b'\x7fELF': continue - f.seek(0) - h = f.read(64) - e_phoff = struct.unpack_from(' Date: Sat, 9 May 2026 13:41:37 +0000 Subject: [PATCH 05/25] fix: clean Dockerfile with Piper TTS, external patch script --- ai/Dockerfile | 53 ++----- ai/__pycache__/patch_tts_tool.cpython-313.pyc | Bin 0 -> 7398 bytes ai/patch_tts_tool.py | 147 ++++++++++++++++++ 3 files changed, 161 insertions(+), 39 deletions(-) create mode 100644 ai/__pycache__/patch_tts_tool.cpython-313.pyc create mode 100644 ai/patch_tts_tool.py diff --git a/ai/Dockerfile b/ai/Dockerfile index 7de3171..87bdd34 100644 --- a/ai/Dockerfile +++ b/ai/Dockerfile @@ -38,49 +38,24 @@ USER hermes # On copie tout le projet d'un coup sans assumer la présence de fichiers de lock spécifiques COPY --chown=hermes:hermes . . -# ---------- Python virtualenv ---------- +# ---------- Python virtualenv avec Piper TTS ---------- RUN uv venv && \ - uv pip install --no-cache-dir sounddevice numpy faster-whisper + uv pip install --no-cache-dir piper-tts sounddevice numpy faster-whisper -# ---------- Patch tts_tool.py to add Coqui provider ---------- -RUN /opt/hermes/.venv/bin/python3 -c " -tts_path = '/opt/hermes/.venv/lib/python3.13/site-packages/tools/tts_tool.py' -with open(tts_path) as f: - code = f.read() -coqui_block = ''' - elif provider == \"coqui\": - logger.info(\"Generating speech with Coqui TTS (GPU, local)...\") - import subprocess - coqui_python = \"/opt/coqui-tts/bin/python3\" - coqui_script = \"/opt/coqui-tts/bin/coqui_synth.py\" - coqui_config = tts_config.get(\"coqui\", {}) - model = coqui_config.get(\"model\", \"tts_models/en/vctk/vits\") - use_gpu = coqui_config.get(\"use_gpu\", True) - speaker = coqui_config.get(\"speaker\", \"\") - cmd = [ - coqui_python, coqui_script, - \"--text\", text, - \"--out\", file_str, - \"--model\", model, - ] - if use_gpu: - cmd.append(\"--gpu\") - if speaker: - cmd.extend([\"--speaker\", speaker]) - result = subprocess.run(cmd, capture_output=True, text=True, timeout=120) - if result.returncode != 0: - stderr = result.stderr.strip() - raise RuntimeError(f\"Coqui TTS failed: {stderr or 'unknown error'}\") - logger.info(\"Coqui TTS audio saved: %s\", file_str) -''' -code = code.replace( - ' else:\n # Default: Edge TTS (free), with NeuTTS as local fallback', - coqui_block + ' else:\n # Default: Edge TTS (free), with NeuTTS as local fallback' -) -with open(tts_path, 'w') as f: - f.write(code) +# ---------- Télécharger la voix Piper Ryan ---------- +RUN mkdir -p /opt/hermes/.venv/share/piper/voices && \ + /opt/hermes/.venv/bin/python3 -c " +import urllib.request +base = '/opt/hermes/.venv/share/piper/voices' +url = 'https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/ryan/high/en_US-ryan-high.onnx' +urllib.request.urlretrieve(url + '?download=true', base + '/en_US-ryan-high.onnx') +urllib.request.urlretrieve(url + '.onnx.json?download=true', base + '/en_US-ryan-high.onnx.json') " +# ---------- Patch tts_tool.py: remplacer Coqui par Piper, supprimer Edge ---------- +COPY ai/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 + # ---------- Runtime ---------- ENV HERMES_HOME=/opt/data ENV PATH="/opt/data/.local/bin:${PATH}" diff --git a/ai/__pycache__/patch_tts_tool.cpython-313.pyc b/ai/__pycache__/patch_tts_tool.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ac2d1aea31f4e8d479e5a70819eacd9c8cfd0631 GIT binary patch literal 7398 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 literal 0 HcmV?d00001 diff --git a/ai/patch_tts_tool.py b/ai/patch_tts_tool.py new file mode 100644 index 0000000..4791132 --- /dev/null +++ b/ai/patch_tts_tool.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +"""Patch Hermes TTS tool to add Piper provider and remove Edge TTS fallback.""" +import sys + +tts_path = '/opt/hermes/.venv/lib/python3.13/site-packages/tools/tts_tool.py' + +with open(tts_path) as f: + code = f.read() + +# Replace the Coqui provider block with Piper +old_coqui = ' elif provider == "coqui":' +new_piper = ''' elif provider == "piper": + 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") + if not os.path.exists(model_path): + raise FileNotFoundError(f"Piper voice model not found: {model_path}") + cmd = [piper_binary, "--model", model_path, "--output-raw"] + proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.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)''' + +if old_coqui in code: + code = code.replace(old_coqui, new_piper) + print("Coqui -> Piper replaced") +else: + # Fresh Hermes install - add Piper after the last provider (kittentts) + if 'provider == "piper"' in code: + print("Piper already present, skipping coqui replacement") + else: + print("Stock Hermes - adding Piper provider after kittentts...") + marker = ' elif provider == "kittentts":' + if marker in code: + # Find the end of the kittentts block and insert Piper before else + lines = code.split('\n') + kit_idx = None + for i, line in enumerate(lines): + if line.strip().startswith('elif provider == "kittentts":'): + kit_idx = i + break + if kit_idx is not None: + # Find the next blank line + else block + for i in range(kit_idx, len(lines)): + if lines[i].strip() == 'else:': + # Insert Piper block before this line + indent = ' ' + piper_lines = new_piper.split('\n') + insert = piper_lines + [''] + lines[i:i] = insert + code = '\n'.join(lines) + print("Piper provider added after kittentts") + break + +# Replace the Edge fallback with Piper fallback +old_edge = ''' 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_piper_fallback = ''' else: + # Default: Piper TTS (local, CPU, no cloud) + piper_available = False + 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) + 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) + else: + return json.dumps({ + "success": False, + "error": "No TTS provider available. Install Piper TTS (pip install piper-tts) " + "and download a voice model." + }, ensure_ascii=False)''' + +if old_edge in code: + code = code.replace(old_edge, new_piper_fallback) + print("Edge fallback replaced with Piper") +else: + print("Edge fallback NOT found, checking if already Piper...") + if 'Default: Piper TTS' in code: + print("Piper fallback already present, skipping") + else: + print("ERROR: Could not find Edge fallback") + # Debug: show what the else block looks like + import re + match = re.search(r' else:\n # Default:', code) + if match: + print("Found else block at", match.start()) + sys.exit(1) + +with open(tts_path, 'w') as f: + f.write(code) +print("tts_tool.py patched successfully") -- 2.49.1 From e779818e73f582917e68b75664f0e121c2188d95 Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Sat, 9 May 2026 13:41:54 +0000 Subject: [PATCH 06/25] chore: remove pycache --- ai/__pycache__/patch_tts_tool.cpython-313.pyc | Bin 7398 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 ai/__pycache__/patch_tts_tool.cpython-313.pyc diff --git a/ai/__pycache__/patch_tts_tool.cpython-313.pyc b/ai/__pycache__/patch_tts_tool.cpython-313.pyc deleted file mode 100644 index ac2d1aea31f4e8d479e5a70819eacd9c8cfd0631..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7398 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 -- 2.49.1 From 78f499bde89c6d9a0ef907e95107065506b614e0 Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Sat, 9 May 2026 13:55:38 +0000 Subject: [PATCH 07/25] fix: use full OPENROUTER_API_KEY variable name --- ai/compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ai/compose.yml b/ai/compose.yml index db5b16e..f7e4d8f 100644 --- a/ai/compose.yml +++ b/ai/compose.yml @@ -38,7 +38,7 @@ services: - API_SERVER_HOST=0.0.0.0 - API_SERVER_KEY=hermes_local_key - GATEWAY_ALLOW_ALL_USERS=true - - OPENROUTER_API_KEY=${OPEN...KEY} + - OPENROUTER_API_KEY=${OPENROUTER_API_KEY} # ROCm for GPU-accelerated faster-whisper STT - HSA_OVERRIDE_GFX_VERSION=9.0.6 - HCC_AMDGPU_TARGET=gfx906 -- 2.49.1 From 3f080da35e95e37eef468a1865da55d831fdf0be Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Sat, 9 May 2026 13:59:09 +0000 Subject: [PATCH 08/25] fix: clean patch script - only target Edge, no Coqui references --- ai/Dockerfile | 2 +- ai/patch_tts_tool.py | 75 +++++++------------------------------------- 2 files changed, 13 insertions(+), 64 deletions(-) diff --git a/ai/Dockerfile b/ai/Dockerfile index 87bdd34..bdd22c8 100644 --- a/ai/Dockerfile +++ b/ai/Dockerfile @@ -52,7 +52,7 @@ urllib.request.urlretrieve(url + '?download=true', base + '/en_US-ryan-high.onnx urllib.request.urlretrieve(url + '.onnx.json?download=true', base + '/en_US-ryan-high.onnx.json') " -# ---------- Patch tts_tool.py: remplacer Coqui par Piper, supprimer Edge ---------- +# ---------- Patch tts_tool.py: replace Edge TTS fallback with Piper ---------- COPY ai/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 diff --git a/ai/patch_tts_tool.py b/ai/patch_tts_tool.py index 4791132..0187d3c 100644 --- a/ai/patch_tts_tool.py +++ b/ai/patch_tts_tool.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Patch Hermes TTS tool to add Piper provider and remove Edge TTS fallback.""" +"""Patch Hermes TTS tool: remove Edge TTS, replace with Piper as default/fallback.""" import sys tts_path = '/opt/hermes/.venv/lib/python3.13/site-packages/tools/tts_tool.py' @@ -7,58 +7,6 @@ tts_path = '/opt/hermes/.venv/lib/python3.13/site-packages/tools/tts_tool.py' with open(tts_path) as f: code = f.read() -# Replace the Coqui provider block with Piper -old_coqui = ' elif provider == "coqui":' -new_piper = ''' elif provider == "piper": - 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") - if not os.path.exists(model_path): - raise FileNotFoundError(f"Piper voice model not found: {model_path}") - cmd = [piper_binary, "--model", model_path, "--output-raw"] - proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.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)''' - -if old_coqui in code: - code = code.replace(old_coqui, new_piper) - print("Coqui -> Piper replaced") -else: - # Fresh Hermes install - add Piper after the last provider (kittentts) - if 'provider == "piper"' in code: - print("Piper already present, skipping coqui replacement") - else: - print("Stock Hermes - adding Piper provider after kittentts...") - marker = ' elif provider == "kittentts":' - if marker in code: - # Find the end of the kittentts block and insert Piper before else - lines = code.split('\n') - kit_idx = None - for i, line in enumerate(lines): - if line.strip().startswith('elif provider == "kittentts":'): - kit_idx = i - break - if kit_idx is not None: - # Find the next blank line + else block - for i in range(kit_idx, len(lines)): - if lines[i].strip() == 'else:': - # Insert Piper block before this line - indent = ' ' - piper_lines = new_piper.split('\n') - insert = piper_lines + [''] - lines[i:i] = insert - code = '\n'.join(lines) - print("Piper provider added after kittentts") - break - # Replace the Edge fallback with Piper fallback old_edge = ''' else: # Default: Edge TTS (free), with NeuTTS as local fallback @@ -89,8 +37,8 @@ old_edge = ''' else: "or set up NeuTTS for local synthesis." }, ensure_ascii=False)''' -new_piper_fallback = ''' else: - # Default: Piper TTS (local, CPU, no cloud) +new_piper = ''' else: + # Default: Piper TTS (local, CPU, no cloud, no Microsoft) piper_available = False try: piper_binary = "/opt/hermes/.venv/bin/piper" @@ -127,19 +75,20 @@ new_piper_fallback = ''' else: }, ensure_ascii=False)''' if old_edge in code: - code = code.replace(old_edge, new_piper_fallback) + code = code.replace(old_edge, new_piper) print("Edge fallback replaced with Piper") else: - print("Edge fallback NOT found, checking if already Piper...") if 'Default: Piper TTS' in code: - print("Piper fallback already present, skipping") + print("Piper fallback already present") else: - print("ERROR: Could not find Edge fallback") - # Debug: show what the else block looks like + print("ERROR: Could not find Edge fallback in tts_tool.py") + # Debug output import re - match = re.search(r' else:\n # Default:', code) - if match: - print("Found else block at", match.start()) + 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: -- 2.49.1 From 77fe8133ae36d03ba405b667a7b8ad81dc634e8d Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Sat, 9 May 2026 14:09:50 +0000 Subject: [PATCH 09/25] fix: Dockerfile heredoc for voice download instead of multi-line -c --- ai/Dockerfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ai/Dockerfile b/ai/Dockerfile index bdd22c8..b6ed67d 100644 --- a/ai/Dockerfile +++ b/ai/Dockerfile @@ -44,13 +44,13 @@ RUN uv venv && \ # ---------- Télécharger la voix Piper Ryan ---------- RUN mkdir -p /opt/hermes/.venv/share/piper/voices && \ - /opt/hermes/.venv/bin/python3 -c " + /opt/hermes/.venv/bin/python3 /dev/stdin << 'PYEOF' && rm -f /tmp/dl_voice.py import urllib.request base = '/opt/hermes/.venv/share/piper/voices' url = 'https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/ryan/high/en_US-ryan-high.onnx' -urllib.request.urlretrieve(url + '?download=true', base + '/en_US-ryan-high.onnx') -urllib.request.urlretrieve(url + '.onnx.json?download=true', base + '/en_US-ryan-high.onnx.json') -" +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 ---------- COPY ai/patch_tts_tool.py /tmp/patch_tts_tool.py -- 2.49.1 From b3fa424661c85a00e716d5acddb4fdfcdb71d38a Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Sat, 9 May 2026 14:12:06 +0000 Subject: [PATCH 10/25] fix: correct COPY path for patch_tts_tool.py (build context is ai/) --- ai/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ai/Dockerfile b/ai/Dockerfile index b6ed67d..1413696 100644 --- a/ai/Dockerfile +++ b/ai/Dockerfile @@ -53,7 +53,7 @@ urllib.request.urlretrieve(url + '.json', base + '/en_US-ryan-high.onnx.json') PYEOF # ---------- Patch tts_tool.py: replace Edge TTS fallback with Piper ---------- -COPY ai/patch_tts_tool.py /tmp/patch_tts_tool.py +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 # ---------- Runtime ---------- -- 2.49.1 From 0a9507de6585240caedf20439647e079649c8262 Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Sat, 9 May 2026 14:14:52 +0000 Subject: [PATCH 11/25] fix: add ca-certificates for HuggingFace download --- ai/Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ai/Dockerfile b/ai/Dockerfile index 1413696..7342300 100644 --- a/ai/Dockerfile +++ b/ai/Dockerfile @@ -16,7 +16,8 @@ RUN apt-get update && \ texlive-latex-base texlive-latex-extra texlive-fonts-recommended texlive-xetex texlive-science \ qemu-user-static binfmt-support qemu-user-binfmt \ emacs-nox \ - libportaudio2 && \ + libportaudio2 \ + ca-certificates && \ rm -rf /var/lib/apt/lists/* # Création de l'utilisateur 'hermes' directement avec les bons accès -- 2.49.1 From b750d26d801acb32ed1ec240392455f2b9763667 Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Sat, 9 May 2026 14:20:46 +0000 Subject: [PATCH 12/25] feat: switch to Norman voice (US male, medium) --- ai/Dockerfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ai/Dockerfile b/ai/Dockerfile index 7342300..1d319cf 100644 --- a/ai/Dockerfile +++ b/ai/Dockerfile @@ -43,14 +43,14 @@ COPY --chown=hermes:hermes . . RUN uv venv && \ uv pip install --no-cache-dir piper-tts sounddevice numpy faster-whisper -# ---------- Télécharger la voix Piper Ryan ---------- +# ---------- Télécharger la voix Piper Norman ---------- RUN mkdir -p /opt/hermes/.venv/share/piper/voices && \ /opt/hermes/.venv/bin/python3 /dev/stdin << 'PYEOF' && rm -f /tmp/dl_voice.py import urllib.request base = '/opt/hermes/.venv/share/piper/voices' -url = 'https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/ryan/high/en_US-ryan-high.onnx' -urllib.request.urlretrieve(url, base + '/en_US-ryan-high.onnx') -urllib.request.urlretrieve(url + '.json', base + '/en_US-ryan-high.onnx.json') +url = 'https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/norman/medium/en_US-norman-medium.onnx' +urllib.request.urlretrieve(url, base + '/en_US-norman-medium.onnx') +urllib.request.urlretrieve(url + '.json', base + '/en_US-norman-medium.onnx.json') PYEOF # ---------- Patch tts_tool.py: replace Edge TTS fallback with Piper ---------- -- 2.49.1 From 3016d0da2c6a3f2a3baf3d032ae54adb830d6bec Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Sat, 9 May 2026 14:27:07 +0000 Subject: [PATCH 13/25] fix: patch source tts_tool.py path, not site-packages --- ai/patch_tts_tool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ai/patch_tts_tool.py b/ai/patch_tts_tool.py index 0187d3c..31db781 100644 --- a/ai/patch_tts_tool.py +++ b/ai/patch_tts_tool.py @@ -2,7 +2,7 @@ """Patch Hermes TTS tool: remove Edge TTS, replace with Piper as default/fallback.""" import sys -tts_path = '/opt/hermes/.venv/lib/python3.13/site-packages/tools/tts_tool.py' +tts_path = '/opt/hermes/tools/tts_tool.py' with open(tts_path) as f: code = f.read() -- 2.49.1 From 8e9a75fe5c915239a5eb05c72b670ea1477f4a25 Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Sat, 9 May 2026 14:28:35 +0000 Subject: [PATCH 14/25] fix: remove patch step from Dockerfile (build context is just ai/) --- ai/Dockerfile | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/ai/Dockerfile b/ai/Dockerfile index 1d319cf..36ca11a 100644 --- a/ai/Dockerfile +++ b/ai/Dockerfile @@ -52,11 +52,7 @@ url = 'https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/norman/ urllib.request.urlretrieve(url, base + '/en_US-norman-medium.onnx') urllib.request.urlretrieve(url + '.json', base + '/en_US-norman-medium.onnx.json') PYEOF - -# ---------- Patch tts_tool.py: replace Edge TTS fallback with Piper ---------- -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 - + # ---------- Runtime ---------- ENV HERMES_HOME=/opt/data ENV PATH="/opt/data/.local/bin:${PATH}" -- 2.49.1 From 90e227bc4e8a2028978a069b4cf3dd9df9803b01 Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Sat, 9 May 2026 15:21:49 +0000 Subject: [PATCH 15/25] feat: switch back to Ryan high quality voice --- ai/Dockerfile | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ai/Dockerfile b/ai/Dockerfile index 36ca11a..c2a0b08 100644 --- a/ai/Dockerfile +++ b/ai/Dockerfile @@ -43,14 +43,14 @@ COPY --chown=hermes:hermes . . RUN uv venv && \ uv pip install --no-cache-dir piper-tts sounddevice numpy faster-whisper -# ---------- Télécharger la voix Piper Norman ---------- +# ---------- Télécharger la voix Piper Ryan (high quality) ---------- RUN mkdir -p /opt/hermes/.venv/share/piper/voices && \ - /opt/hermes/.venv/bin/python3 /dev/stdin << 'PYEOF' && rm -f /tmp/dl_voice.py + /opt/hermes/.venv/bin/python3 /dev/stdin << 'PYEOF' import urllib.request base = '/opt/hermes/.venv/share/piper/voices' -url = 'https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/norman/medium/en_US-norman-medium.onnx' -urllib.request.urlretrieve(url, base + '/en_US-norman-medium.onnx') -urllib.request.urlretrieve(url + '.json', base + '/en_US-norman-medium.onnx.json') +url = 'https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/ryan/high/en_US-ryan-high.onnx' +urllib.request.urlretrieve(url, base + '/en_US-ryan-high.onnx') +urllib.request.urlretrieve(url + '.json', base + '/en_US-ryan-high.onnx.json') PYEOF # ---------- Runtime ---------- -- 2.49.1 From 1a1cfec80aa39292e7bf2639baad7d272ff152c3 Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Sat, 9 May 2026 15:50:29 +0000 Subject: [PATCH 16/25] fix: add atomic write permission fix (preserves file mode on os.replace) --- ai/Dockerfile | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/ai/Dockerfile b/ai/Dockerfile index c2a0b08..45806e7 100644 --- a/ai/Dockerfile +++ b/ai/Dockerfile @@ -53,6 +53,55 @@ urllib.request.urlretrieve(url, base + '/en_US-ryan-high.onnx') urllib.request.urlretrieve(url + '.json', base + '/en_US-ryan-high.onnx.json') PYEOF +# ---------- 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, +# so group-readable files silently collapse to owner-private 0600. +# This affects: skills, sessions, memories, and any file written atomically. +RUN /opt/hermes/.venv/bin/python3 /dev/stdin << 'PYEOF' +import os + +patches = [ + ("/opt/hermes/tools/skill_manager_tool.py", [ + ("# Restore existing file mode if present", True), # already patched + ]), + ("/opt/hermes/tools/skills_sync.py", [ + ("# Restore existing file mode if present", True), # already patched + ]), +] + +for fpath, checks in patches: + if not os.path.exists(fpath): + print(f"SKIP {fpath} (not found)") + continue + with open(fpath) as f: + code = f.read() + all_ok = all(marker in code for marker, _ in checks) + if all_ok: + print(f"OK {fpath} (already patched)") + continue + print(f"PATCH {fpath}") + # _atomic_write_text in skill_manager_tool.py + code = code.replace( + " os.replace(temp_path, file_path)", + " if file_path.exists():\n" + " existing_mode = file_path.stat().st_mode\n" + " os.chmod(temp_path, existing_mode)\n" + " os.replace(temp_path, file_path)", + ) + # _write_manifest in skills_sync.py + code = code.replace( + " os.replace(tmp_path, MANIFEST_FILE)", + " if MANIFEST_FILE.exists():\n" + " existing_mode = MANIFEST_FILE.stat().st_mode\n" + " os.chmod(tmp_path, existing_mode)\n" + " os.replace(tmp_path, MANIFEST_FILE)", + ) + with open(fpath, 'w') as f: + f.write(code) + print(f"DONE {fpath}") +PYEOF + # ---------- Runtime ---------- ENV HERMES_HOME=/opt/data ENV PATH="/opt/data/.local/bin:${PATH}" -- 2.49.1 From d97f1cb1e5c762ce779f76ac26a61b5430f87f32 Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Sat, 9 May 2026 16:04:32 +0000 Subject: [PATCH 17/25] fix: add startup permission fix for data volume (chown critical dirs on boot) --- ai/Dockerfile | 6 +++++- ai/fix-permissions.sh | 31 +++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 ai/fix-permissions.sh diff --git a/ai/Dockerfile b/ai/Dockerfile index 45806e7..1a8c03a 100644 --- a/ai/Dockerfile +++ b/ai/Dockerfile @@ -108,5 +108,9 @@ ENV PATH="/opt/data/.local/bin:${PATH}" VOLUME [ "/opt/data" ] +# Copie du script de réparation des permissions (lancement au démarrage) +COPY --chmod=0755 fix-permissions.sh /opt/hermes/fix-permissions.sh + # Le conteneur tourne de manière ultra-sécurisée sous l'utilisateur hermes dès le départ -ENTRYPOINT [ "/usr/bin/tini", "-g", "--", "/opt/hermes/docker/entrypoint.sh" ] +# fix-permissions.sh chown les répertoires critiques avant de chaîner vers entrypoint.sh +ENTRYPOINT [ "/usr/bin/tini", "-g", "--", "/opt/hermes/fix-permissions.sh" ] diff --git a/ai/fix-permissions.sh b/ai/fix-permissions.sh new file mode 100644 index 0000000..2a11fd2 --- /dev/null +++ b/ai/fix-permissions.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# Startup permission fix for the Hermes data volume. +# Runs as root before the entrypoint drops to the hermes user. +# Fixes files that were created by root (host agent, cron jobs, etc.) +# becoming inaccessible to the hermes runtime user. +set -e + +HERMES_HOME="${HERMES_HOME:-/opt/data}" + +# Fix ownership on critical writable directories so hermes user can access them +chown -R hermes:hermes \ + "$HERMES_HOME/sessions" \ + "$HERMES_HOME/checkpoints" \ + "$HERMES_HOME/skills" \ + "$HERMES_HOME/memories" \ + "$HERMES_HOME/workspace" \ + "$HERMES_HOME/pastes" \ + "$HERMES_HOME/logs" \ + "$HERMES_HOME/cron" \ + "$HERMES_HOME/plans" \ + "$HERMES_HOME/hooks" \ + "$HERMES_HOME/cache" \ + 2>/dev/null || true + +# Also fix the data volume root if it's wrong +if [ "$(stat -c %u "$HERMES_HOME" 2>/dev/null)" != "$(id -u hermes)" ]; then + chown hermes:hermes "$HERMES_HOME" 2>/dev/null || true +fi + +# Now chain to the real entrypoint +exec /opt/hermes/docker/entrypoint.sh "$@" -- 2.49.1 From 0609720b33257a174f17939a70bc58a5132c18ce Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Sat, 9 May 2026 17:13:01 +0000 Subject: [PATCH 18/25] fix: reinstate tts_tool.py patch step in Dockerfile Commit 8e9a75f removed the COPY+RUN of patch_tts_tool.py because the build context was thought to be insufficient. The build context is ai/ which contains both the Dockerfile and patch_tts_tool.py, so COPY works fine. Without this step the tts_tool.py silently falls through to Edge TTS as its default provider even when config.yaml says provider: piper, because 'piper' is not a recognized provider in the unpatched code. This caused the female Edge TTS voice (AriaNeural) instead of the configured Ryan High male voice. --- ai/Dockerfile | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ai/Dockerfile b/ai/Dockerfile index 1a8c03a..9849066 100644 --- a/ai/Dockerfile +++ b/ai/Dockerfile @@ -53,6 +53,14 @@ 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, -- 2.49.1 From cfa2a898c3d35f96c5d9b99f3b84581564368915 Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Sat, 9 May 2026 17:36:26 +0000 Subject: [PATCH 19/25] fix: move TTS patch from build-time to runtime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- ai/Dockerfile | 8 ----- ai/fix-permissions.sh | 10 ++++++ ai/patch_tts_tool.py | 74 +++++++++++++++++++++++++++++++++---------- 3 files changed, 67 insertions(+), 25 deletions(-) diff --git a/ai/Dockerfile b/ai/Dockerfile index 9849066..1a8c03a 100644 --- a/ai/Dockerfile +++ b/ai/Dockerfile @@ -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, diff --git a/ai/fix-permissions.sh b/ai/fix-permissions.sh index 2a11fd2..c1fb3d3 100644 --- a/ai/fix-permissions.sh +++ b/ai/fix-permissions.sh @@ -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 "$@" diff --git a/ai/patch_tts_tool.py b/ai/patch_tts_tool.py index 31db781..2dc17cb 100644 --- a/ai/patch_tts_tool.py +++ b/ai/patch_tts_tool.py @@ -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}") -- 2.49.1 From a40e347dfa7c547950a6106329dd15c16ecbc9bb Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Sat, 9 May 2026 17:37:32 +0000 Subject: [PATCH 20/25] fix: install hermes-agent from pip so build-time TTS patch works The Dockerfile starts from debian:stable-slim, not from the official Hermes image. Without installing hermes-agent from pip, there is no tools/tts_tool.py in the image at build time, so the patch script crashes with FileNotFoundError. Adding hermes-agent to uv pip install gives us tts_tool.py in the venv site-packages, so the COPY+RUN patch step works cleanly. Also keep the runtime fallback in fix-permissions.sh for cases where the volume's site-packages differ from the image. --- ai/Dockerfile | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ai/Dockerfile b/ai/Dockerfile index 1a8c03a..3f5da57 100644 --- a/ai/Dockerfile +++ b/ai/Dockerfile @@ -41,7 +41,7 @@ COPY --chown=hermes:hermes . . # ---------- Python virtualenv avec Piper TTS ---------- RUN uv venv && \ - uv pip install --no-cache-dir piper-tts sounddevice numpy faster-whisper + uv pip install --no-cache-dir hermes-agent piper-tts sounddevice numpy faster-whisper # ---------- Télécharger la voix Piper Ryan (high quality) ---------- RUN mkdir -p /opt/hermes/.venv/share/piper/voices && \ @@ -53,6 +53,13 @@ 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. +# hermes-agent is installed from pip so tools/tts_tool.py exists in the venv. +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, -- 2.49.1 From 98216d2872f2a58c26c1a7dd62808c5eabcd88ce Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Sat, 9 May 2026 17:39:23 +0000 Subject: [PATCH 21/25] refactor: use official Hermes Agent image as base, not debian:stable-slim Starting from debian:stable-slim required re-installing everything (Hermes source, Node.js, Playwright, etc.) which was redundant and fragile. The official nousresearch/hermes-agent image already has all that. Now the Dockerfile: - FROM nousresearch/hermes-agent:latest (has tts_tool.py, Playwright, etc.) - Install Piper + voice model on top - Patch tts_tool.py at build time (Edge fallback -> Piper) - Runtime fallback in fix-permissions.sh for volume resilience Cleaner, smaller Dockerfile, and the build-time patch can find tts_tool.py because it's in the base image's venv. --- ai/Dockerfile | 95 ++++++------------------------------------- ai/fix-permissions.sh | 15 +++---- 2 files changed, 19 insertions(+), 91 deletions(-) diff --git a/ai/Dockerfile b/ai/Dockerfile index 3f5da57..698bd37 100644 --- a/ai/Dockerfile +++ b/ai/Dockerfile @@ -1,47 +1,30 @@ # 1. On récupère la version la plus récente d'UV FROM ghcr.io/astral-sh/uv:latest AS uv_source -# 2. Image de base stable -FROM debian:stable-slim +# 2. Image officielle Hermes Agent de NousResearch +# Contient déjà: Python, Node.js, npm, Playwright/Chromium, venv, tts_tool.py, etc. +FROM nousresearch/hermes-agent:latest -# Disable Python stdout buffering to ensure logs are printed immediately -ENV PYTHONUNBUFFERED=1 - -# Install system dependencies in one layer, clear APT cache -# tini reaps orphaned zombie processes (MCP stdio subprocesses, git, bun, etc.) +# ---------- System dependencies ---------- +# Piper a besoin de libportaudio2, et HuggingFace a besoin de ca-certificates +USER root RUN apt-get update && \ apt-get install -y --no-install-recommends \ - build-essential python3 ripgrep ffmpeg gcc python3-dev libffi-dev procps git openssh-client docker-cli tini \ - curl poppler-utils imagemagick \ - texlive-latex-base texlive-latex-extra texlive-fonts-recommended texlive-xetex texlive-science \ - qemu-user-static binfmt-support qemu-user-binfmt \ - emacs-nox \ libportaudio2 \ ca-certificates && \ rm -rf /var/lib/apt/lists/* -# Création de l'utilisateur 'hermes' directement avec les bons accès -RUN useradd -u 10000 -m -d /opt/data hermes - -# Copie d'uv (dernière version) +# ---------- UV (hyperfast pip alternative) ---------- COPY --chmod=0755 --from=uv_source /uv /usr/local/bin/ WORKDIR /opt/hermes -# On donne la propriété du dossier de travail à l'utilisateur hermes -RUN chown hermes:hermes /opt/hermes - # ---------- Hermes venv ---------- -# Passer immédiatement sous l'utilisateur hermes pour tout le reste du build USER hermes -# ---------- Source code ---------- -# On copie tout le projet d'un coup sans assumer la présence de fichiers de lock spécifiques -COPY --chown=hermes:hermes . . - # ---------- Python virtualenv avec Piper TTS ---------- RUN uv venv && \ - uv pip install --no-cache-dir hermes-agent piper-tts sounddevice numpy faster-whisper + uv pip install --no-cache-dir piper-tts sounddevice numpy # ---------- Télécharger la voix Piper Ryan (high quality) ---------- RUN mkdir -p /opt/hermes/.venv/share/piper/voices && \ @@ -52,62 +35,12 @@ url = 'https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/ryan/hi 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. -# hermes-agent is installed from pip so tools/tts_tool.py exists in the venv. + +# ---------- Patch tts_tool.py: remplacer Edge TTS par Piper ---------- +# Edge TTS appelle les serveurs Microsoft — on ne veut jamais ça. +# Piper roule localement sur CPU, aucun cloud, aucune donnée qui sort. 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, -# so group-readable files silently collapse to owner-private 0600. -# This affects: skills, sessions, memories, and any file written atomically. -RUN /opt/hermes/.venv/bin/python3 /dev/stdin << 'PYEOF' -import os - -patches = [ - ("/opt/hermes/tools/skill_manager_tool.py", [ - ("# Restore existing file mode if present", True), # already patched - ]), - ("/opt/hermes/tools/skills_sync.py", [ - ("# Restore existing file mode if present", True), # already patched - ]), -] - -for fpath, checks in patches: - if not os.path.exists(fpath): - print(f"SKIP {fpath} (not found)") - continue - with open(fpath) as f: - code = f.read() - all_ok = all(marker in code for marker, _ in checks) - if all_ok: - print(f"OK {fpath} (already patched)") - continue - print(f"PATCH {fpath}") - # _atomic_write_text in skill_manager_tool.py - code = code.replace( - " os.replace(temp_path, file_path)", - " if file_path.exists():\n" - " existing_mode = file_path.stat().st_mode\n" - " os.chmod(temp_path, existing_mode)\n" - " os.replace(temp_path, file_path)", - ) - # _write_manifest in skills_sync.py - code = code.replace( - " os.replace(tmp_path, MANIFEST_FILE)", - " if MANIFEST_FILE.exists():\n" - " existing_mode = MANIFEST_FILE.stat().st_mode\n" - " os.chmod(tmp_path, existing_mode)\n" - " os.replace(tmp_path, MANIFEST_FILE)", - ) - with open(fpath, 'w') as f: - f.write(code) - print(f"DONE {fpath}") -PYEOF # ---------- Runtime ---------- ENV HERMES_HOME=/opt/data @@ -115,9 +48,7 @@ ENV PATH="/opt/data/.local/bin:${PATH}" VOLUME [ "/opt/data" ] -# Copie du script de réparation des permissions (lancement au démarrage) +# Script de réparation des permissions + patch TTS au démarrage COPY --chmod=0755 fix-permissions.sh /opt/hermes/fix-permissions.sh -# Le conteneur tourne de manière ultra-sécurisée sous l'utilisateur hermes dès le départ -# fix-permissions.sh chown les répertoires critiques avant de chaîner vers entrypoint.sh ENTRYPOINT [ "/usr/bin/tini", "-g", "--", "/opt/hermes/fix-permissions.sh" ] diff --git a/ai/fix-permissions.sh b/ai/fix-permissions.sh index c1fb3d3..7af8d0c 100644 --- a/ai/fix-permissions.sh +++ b/ai/fix-permissions.sh @@ -1,13 +1,11 @@ #!/bin/bash -# Startup permission fix for the Hermes data volume. +# Startup permission fix + TTS patch. # Runs as root before the entrypoint drops to the hermes user. -# Fixes files that were created by root (host agent, cron jobs, etc.) -# becoming inaccessible to the hermes runtime user. set -e HERMES_HOME="${HERMES_HOME:-/opt/data}" -# Fix ownership on critical writable directories so hermes user can access them +# Fix ownership on critical writable directories chown -R hermes:hermes \ "$HERMES_HOME/sessions" \ "$HERMES_HOME/checkpoints" \ @@ -22,20 +20,19 @@ chown -R hermes:hermes \ "$HERMES_HOME/cache" \ 2>/dev/null || true -# Also fix the data volume root if it's wrong +# Fix data volume root ownership 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. +# Fallback runtime patch in case the volume's site-packages differ from the image. +# Idempotent: if already patched, 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 +# Chain to the official Hermes entrypoint exec /opt/hermes/docker/entrypoint.sh "$@" -- 2.49.1 From 6f1774366766b96913a58107942840242cfb4604 Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Sat, 9 May 2026 17:44:55 +0000 Subject: [PATCH 22/25] fix: install into existing venv instead of recreating it The nousresearch/hermes-agent:latest base image already has a venv with hermes-agent installed at /opt/hermes/.venv/. Running 'uv venv' on top of it either fails or wipes the existing install. Fix: activate the existing venv first, then pip install into it. --- ai/Dockerfile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ai/Dockerfile b/ai/Dockerfile index 698bd37..57c4638 100644 --- a/ai/Dockerfile +++ b/ai/Dockerfile @@ -22,8 +22,10 @@ WORKDIR /opt/hermes # ---------- Hermes venv ---------- USER hermes -# ---------- Python virtualenv avec Piper TTS ---------- -RUN uv venv && \ +# ---------- Piper TTS dans le venv existant ---------- +# Le venv existe déjà dans l'image de base (hermes-agent installé). +# On ajoute simplement Piper et ses dépendences. +RUN . /opt/hermes/.venv/bin/activate && \ uv pip install --no-cache-dir piper-tts sounddevice numpy # ---------- Télécharger la voix Piper Ryan (high quality) ---------- -- 2.49.1 From 3f80744ebd950739c6dfbf9033cb80bf6f206b0e Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Sat, 9 May 2026 17:47:30 +0000 Subject: [PATCH 23/25] fix: install piper-tts as root (venv is root-owned in base image) The nousresearch/hermes-agent:latest image creates its venv as root. Running 'uv pip install' as USER hermes fails with Permission denied on the site-packages directory. Fix: keep USER root while modifying the venv, then switch back to USER hermes for runtime. --- ai/Dockerfile | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/ai/Dockerfile b/ai/Dockerfile index 57c4638..f9ded5b 100644 --- a/ai/Dockerfile +++ b/ai/Dockerfile @@ -19,12 +19,8 @@ COPY --chmod=0755 --from=uv_source /uv /usr/local/bin/ WORKDIR /opt/hermes -# ---------- Hermes venv ---------- -USER hermes - # ---------- Piper TTS dans le venv existant ---------- -# Le venv existe déjà dans l'image de base (hermes-agent installé). -# On ajoute simplement Piper et ses dépendences. +# Le venv de l'image de base est root-owned, on doit installer en root aussi RUN . /opt/hermes/.venv/bin/activate && \ uv pip install --no-cache-dir piper-tts sounddevice numpy @@ -45,6 +41,9 @@ 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 # ---------- Runtime ---------- +# Retour à l'utilisateur non-root pour la sécurité +USER hermes + ENV HERMES_HOME=/opt/data ENV PATH="/opt/data/.local/bin:${PATH}" -- 2.49.1 From 748b5037b9802553e3faa8537c1c257dd0deb281 Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Sat, 9 May 2026 19:03:10 +0000 Subject: [PATCH 24/25] 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.") -- 2.49.1 From b89be7b8f4ba9363030ba5d37d0cbf7086876df2 Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Sat, 9 May 2026 19:18:16 +0000 Subject: [PATCH 25/25] chore: restore system packages lost in base image migration The migration from debian:stable-slim to nousresearch/hermes-agent:latest dropped several packages that were previously installed. This restores: - poppler-utils, imagemagick (PDF/image processing) - texlive-latex-base, latex-extra, fonts-recommended, xetex, science - qemu-user-static, binfmt-support (cross-compilation) - emacs-nox (text editing) These were added in PRs 3/5, 4/5, 5/5 and earlier commits of the compose repo. The official image already has git, curl, ffmpeg, python3, gcc, openssh, ripgrep, tini, docker-cli, etc. --- ai/Dockerfile | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/ai/Dockerfile b/ai/Dockerfile index f9ded5b..6c8ddeb 100644 --- a/ai/Dockerfile +++ b/ai/Dockerfile @@ -6,12 +6,30 @@ FROM ghcr.io/astral-sh/uv:latest AS uv_source FROM nousresearch/hermes-agent:latest # ---------- System dependencies ---------- -# Piper a besoin de libportaudio2, et HuggingFace a besoin de ca-certificates +# The official hermes-agent image already has: git, curl, ffmpeg, python3, +# gcc, build-essential, openssh-client, procps, tini, ripgrep, docker-cli, +# libportaudio2, ca-certificates, etc. +# +# These extras we need to add back: +# - poppler-utils, imagemagick (PDF/image processing) +# - texlive-* (LaTeX typesetting for reports) +# - qemu-user-static, binfmt-support (QEMU cross-compilation) +# - emacs-nox (text editing in container) USER root RUN apt-get update && \ apt-get install -y --no-install-recommends \ libportaudio2 \ - ca-certificates && \ + ca-certificates \ + poppler-utils \ + imagemagick \ + texlive-latex-base \ + texlive-latex-extra \ + texlive-fonts-recommended \ + texlive-xetex \ + texlive-science \ + qemu-user-static \ + binfmt-support \ + emacs-nox && \ rm -rf /var/lib/apt/lists/* # ---------- UV (hyperfast pip alternative) ---------- -- 2.49.1