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.
125 lines
4.9 KiB
Docker
125 lines
4.9 KiB
Docker
# 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
|
|
|
|
# Install system dependencies in one layer, clear APT cache
|
|
# tini reaps orphaned zombie processes (MCP stdio subprocesses, git, bun, etc.)
|
|
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)
|
|
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 piper-tts sounddevice numpy faster-whisper
|
|
|
|
# ---------- 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'
|
|
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')
|
|
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,
|
|
# 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}"
|
|
|
|
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
|
|
# 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" ]
|