From a79fe9dffacebae6d4ee17502885e9cdfa852073 Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Fri, 1 May 2026 03:06:14 +0000 Subject: [PATCH 01/71] feat: enable traefik access logs for fail2ban http jails --- network/compose.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/network/compose.yml b/network/compose.yml index 7642118..5a35eff 100644 --- a/network/compose.yml +++ b/network/compose.yml @@ -13,17 +13,20 @@ services: - "--certificatesresolvers.njalla.acme.storage=/letsencrypt/acme.json" - "--certificatesresolvers.njalla.acme.httpchallenge.entrypoint=web" - - "--log.level=DEBUG" + - "--log.level=INFO" + - "--log.filepath=/var/log/traefik/traefik.log" + - "--accesslog.filepath=/var/log/traefik/access.log" - "--providers.docker=true" - "--providers.docker.exposedByDefault=false" ports: - "80:80" - "443:443" environment: - - NJALLA_TOKEN=${NJALLA_TOKEN} + - NJALLA_TOKEN=*** volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - /mnt/HoardingCow_docker_data/Traefik:/letsencrypt + - /var/log/traefik:/var/log/traefik restart: unless-stopped networks: - traefik_backend From 293429a124f72e75e4f29620bb3fb9ec201d03d3 Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Mon, 4 May 2026 22:46:50 +0000 Subject: [PATCH 02/71] feat: add WireGuard VPN stack with wg-easy --- vpn/compose.yml | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 vpn/compose.yml diff --git a/vpn/compose.yml b/vpn/compose.yml new file mode 100644 index 0000000..1511920 --- /dev/null +++ b/vpn/compose.yml @@ -0,0 +1,35 @@ +version: "3.8" + +services: + wg-easy: + image: weejewel/wg-easy:latest + container_name: wg-easy + cap_add: + - NET_ADMIN + - SYS_MODULE + environment: + - WG_HOST=vpn.lazyworkhorse.net + - PASSWORD=${WG_PASSWORD} + - WG_PORT=51820 + - WG_DEFAULT_ADDRESS=10.8.0.x + - WG_DEFAULT_DNS=1.1.1.1,8.8.8.8 + - WG_ALLOWED_IPS=0.0.0.0/0, ::/0 + - WG_PERSISTENT_KEEPALIVE=25 + - UI_TRAFFIC_STATS=true + - UI_CHART_TYPE=0 + ports: + - "51820:51820/udp" + - "51821:51821/tcp" + volumes: + - /mnt/HoardingCow_docker_data/WireGuard:/etc/wireguard:rw + sysctls: + - net.ipv4.conf.all.src_valid_mark=1 + - net.ipv4.ip_forward=1 + restart: unless-stopped + networks: + - vpn_net + +networks: + vpn_net: + external: true + name: vpn_net From 4a57ca69b2683a3f32274a3fc98d48eb33723104 Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Tue, 5 May 2026 01:17:47 +0000 Subject: [PATCH 03/71] fix: switch to linuxserver/wireguard instead of wg-easy --- vpn/compose.yml | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/vpn/compose.yml b/vpn/compose.yml index 1511920..a064443 100644 --- a/vpn/compose.yml +++ b/vpn/compose.yml @@ -1,27 +1,20 @@ version: "3.8" services: - wg-easy: - image: weejewel/wg-easy:latest - container_name: wg-easy + wireguard: + image: ghcr.io/linuxserver/wireguard:latest + container_name: wireguard cap_add: - NET_ADMIN - SYS_MODULE environment: - - WG_HOST=vpn.lazyworkhorse.net - - PASSWORD=${WG_PASSWORD} - - WG_PORT=51820 - - WG_DEFAULT_ADDRESS=10.8.0.x - - WG_DEFAULT_DNS=1.1.1.1,8.8.8.8 - - WG_ALLOWED_IPS=0.0.0.0/0, ::/0 - - WG_PERSISTENT_KEEPALIVE=25 - - UI_TRAFFIC_STATS=true - - UI_CHART_TYPE=0 + - PUID=1000 + - PGID=1000 + - TZ=America/Montreal ports: - "51820:51820/udp" - - "51821:51821/tcp" volumes: - - /mnt/HoardingCow_docker_data/WireGuard:/etc/wireguard:rw + - /mnt/HoardingCow_docker_data/WireGuard:/config:rw sysctls: - net.ipv4.conf.all.src_valid_mark=1 - net.ipv4.ip_forward=1 From eea6db3ceba56424e57b2b32f2c2e7268dd88e45 Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Tue, 5 May 2026 01:21:17 +0000 Subject: [PATCH 04/71] feat: add WireGuard VPN stack (wg-easy, named wireguard) --- vpn/compose.yml | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/vpn/compose.yml b/vpn/compose.yml index a064443..ceb4f35 100644 --- a/vpn/compose.yml +++ b/vpn/compose.yml @@ -2,19 +2,26 @@ version: "3.8" services: wireguard: - image: ghcr.io/linuxserver/wireguard:latest + image: weejewel/wg-easy:latest container_name: wireguard cap_add: - NET_ADMIN - SYS_MODULE environment: - - PUID=1000 - - PGID=1000 - - TZ=America/Montreal + - WG_HOST=vpn.lazyworkhorse.net + - PASSWORD=${WG_PASSWORD} + - WG_PORT=51820 + - WG_DEFAULT_ADDRESS=10.8.0.x + - WG_DEFAULT_DNS=1.1.1.1,8.8.8.8 + - WG_ALLOWED_IPS=0.0.0.0/0, ::/0 + - WG_PERSISTENT_KEEPALIVE=25 + - UI_TRAFFIC_STATS=true + - UI_CHART_TYPE=0 ports: - "51820:51820/udp" + - "51821:51821/tcp" volumes: - - /mnt/HoardingCow_docker_data/WireGuard:/config:rw + - /mnt/HoardingCow_docker_data/WireGuard:/etc/wireguard:rw sysctls: - net.ipv4.conf.all.src_valid_mark=1 - net.ipv4.ip_forward=1 From b021d0dba7136ad30809f78b37fcb9cf1859809a Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Tue, 5 May 2026 01:42:55 +0000 Subject: [PATCH 05/71] feat: add custom Hermes Dockerfile with WireGuard tools --- ai/Dockerfile | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 ai/Dockerfile diff --git a/ai/Dockerfile b/ai/Dockerfile new file mode 100644 index 0000000..edbb07f --- /dev/null +++ b/ai/Dockerfile @@ -0,0 +1,72 @@ +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 + +# 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 \ + 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 \ + wireguard-tools openresolv && \ + rm -rf /var/lib/apt/lists/* + +# Non-root user for runtime; UID can be overridden via HERMES_UID at runtime +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/ + +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/ + +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 + +# ---------- Source code ---------- +# .dockerignore excludes node_modules, so the installs above survive. +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" ] +ENTRYPOINT [ "/usr/bin/tini", "-g", "--", "/opt/hermes/docker/entrypoint.sh" ] From acf45acdd961a99c7b3edc3134009e5c1f9d9407 Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Tue, 5 May 2026 01:48:21 +0000 Subject: [PATCH 06/71] feat: enable NET_ADMIN for Hermes container to support WireGuard --- ai/compose.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ai/compose.yml b/ai/compose.yml index 460d44d..a612889 100644 --- a/ai/compose.yml +++ b/ai/compose.yml @@ -26,9 +26,11 @@ services: - "traefik.http.routers.webui-https.tls.certresolver=njalla" hermes: - image: nousresearch/hermes-agent:latest + build: ./ container_name: hermes restart: always + cap_add: + - NET_ADMIN # Gateway run enables the internal API server on port 8642 command: gateway run environment: From bc49391b4f67f1db5d9bfcd35a299367210da330 Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Tue, 5 May 2026 02:11:37 +0000 Subject: [PATCH 07/71] chore: clean up WireGuard from Hermes Dockerfile, keep custom build --- ai/Dockerfile | 3 +-- ai/compose.yml | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/ai/Dockerfile b/ai/Dockerfile index edbb07f..1edd524 100644 --- a/ai/Dockerfile +++ b/ai/Dockerfile @@ -21,8 +21,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 \ - wireguard-tools openresolv && \ + libportaudio2 && \ rm -rf /var/lib/apt/lists/* # Non-root user for runtime; UID can be overridden via HERMES_UID at runtime diff --git a/ai/compose.yml b/ai/compose.yml index a612889..fa3b84c 100644 --- a/ai/compose.yml +++ b/ai/compose.yml @@ -29,8 +29,6 @@ services: build: ./ container_name: hermes restart: always - cap_add: - - NET_ADMIN # Gateway run enables the internal API server on port 8642 command: gateway run environment: From d9f62652cbf5854e0aa760e75077cb1ed277c64b Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Mon, 4 May 2026 22:56:07 -0400 Subject: [PATCH 08/71] Commented webui for now. now using it --- ai/compose.yml | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/ai/compose.yml b/ai/compose.yml index 460d44d..06818ac 100644 --- a/ai/compose.yml +++ b/ai/compose.yml @@ -1,29 +1,29 @@ version: "3.8" services: - webui: - image: ghcr.io/open-webui/open-webui:main - volumes: - - /mnt/HoardingCow_docker_data/Ollama/open-webui:/app/backend/data - restart: always - environment: - - OLLAMA_API_BASE_URL=http://ollama:11434/api - networks: - - ai_net - - ai_backend - labels: - - "traefik.enable=true" + # webui: + # image: ghcr.io/open-webui/open-webui:main + # volumes: + # - /mnt/HoardingCow_docker_data/Ollama/open-webui:/app/backend/data + # restart: always + # environment: + # - OLLAMA_API_BASE_URL=http://ollama:11434/api + # networks: + # - ai_net + # - ai_backend + # labels: + # - "traefik.enable=true" - # Router for HTTP + redirection to HTTPS - - "traefik.http.routers.webui-http.rule=Host(`ai.lazyworkhorse.net`)" - - "traefik.http.routers.webui-http.entrypoints=web" - - "traefik.http.routers.webui-http.middlewares=redirect-to-https" + # # Router for HTTP + redirection to HTTPS + # - "traefik.http.routers.webui-http.rule=Host(`ai.lazyworkhorse.net`)" + # - "traefik.http.routers.webui-http.entrypoints=web" + # - "traefik.http.routers.webui-http.middlewares=redirect-to-https" - # Router for HTTPS with TLS - - "traefik.http.routers.webui-https.rule=Host(`ai.lazyworkhorse.net`)" - - "traefik.http.routers.webui-https.entrypoints=websecure" - - "traefik.http.routers.webui-https.tls=true" - - "traefik.http.routers.webui-https.tls.certresolver=njalla" + # # Router for HTTPS with TLS + # - "traefik.http.routers.webui-https.rule=Host(`ai.lazyworkhorse.net`)" + # - "traefik.http.routers.webui-https.entrypoints=websecure" + # - "traefik.http.routers.webui-https.tls=true" + # - "traefik.http.routers.webui-https.tls.certresolver=njalla" hermes: image: nousresearch/hermes-agent:latest From 51cf83c420181a8a3490db3dff1c56f52ebacbea Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Mon, 4 May 2026 23:01:58 -0400 Subject: [PATCH 09/71] Commeneted nomadnet for now. not usingit. --- coms/compose.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/coms/compose.yml b/coms/compose.yml index 1036a7a..34897c0 100644 --- a/coms/compose.yml +++ b/coms/compose.yml @@ -1,15 +1,15 @@ version: "3.9" services: - nomadnet: - image: ghcr.io/markqvist/nomadnet:master - container_name: nomadnet - restart: always - volumes: - - /mnt/HoardingCow_docker_data/Nomadnet:/root/.nomadnetwork - - /mnt/HoardingCow_docker_data/Reticulum:/root/.reticulum - # Reticulum transport must be reachable directly (NOT through Traefik) - ports: - - "4242:4242" + # nomadnet: + # image: ghcr.io/markqvist/nomadnet:master + # container_name: nomadnet + # restart: always + # volumes: + # - /mnt/HoardingCow_docker_data/Nomadnet:/root/.nomadnetwork + # - /mnt/HoardingCow_docker_data/Reticulum:/root/.reticulum + # # Reticulum transport must be reachable directly (NOT through Traefik) + # ports: + # - "4242:4242" synapse: image: ghcr.io/element-hq/synapse:latest From 5c504501d381c4c9022bc5dc4e090e758e9136c3 Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Sat, 9 May 2026 00:20:57 +0000 Subject: [PATCH 10/71] 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: From f5171a7d6e8514fd087065c117ffe6abd5a6b657 Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Sat, 9 May 2026 02:38:23 +0000 Subject: [PATCH 11/71] 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" ] From c2471818b29d047a2c68d60145a21e989c948b10 Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Sat, 9 May 2026 04:09:57 +0000 Subject: [PATCH 12/71] 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 13/71] 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 14/71] 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 zcmeGhNo*U}bto>PNXyn{YqkAFFClF%vSm9CW2Nz;$O|H=qyn@E6Jj*;DNQt-VgAf$ zq2a&vn?SZ+~RJ=HzcK@QES=bVzO0$B+?1&SCs6uBwLAwWKWj;&*%^P2~-DP{@crOf8??p;j*4=(+v;tWWA&* z=Z$c=rn4SvR})8kIbSAh13TZd)gcLFo9N%!CTjw5Ss!W6`bjX?oC`b)lYBqNW)t$x5}TAZ0|h=g zBYQ!nrZ9)N+}`N|%ya?ey}AAM^lfbbA6d#nt~J-@2TZ&1k!?-?g5ww+uyuAXIgkrx zc9*<6*PY#$@+1H=>B;p_D!I1fTsE8;c4V1=b-sY1&*8>8OWB&d)zfs@*>8`4tLlk^ z^RwkVF}?Lfm1n1>(j$e+R5eB>>B7%$RJVvA93})L4yev=i!d&umD#>{H z`IK7HQ?m?TDmjmp`IIb8r%Frutf~wrPYtIuNymv2fbcU|OHp$0*0nqxB}+@?K~#OP zEEOE$BO@pxsIMDRB+dWJ$8kIFDp9T@>5#pHer0uL29u?a>>iKC&;mHyLo*VT6 zYblGzZ2$#TDM&K_NL6C}NX}qA8nGba=;kdqH}k3_VAyZbt%qPMkvNKACI3=3Ap@tp zkSJnNGKyP7Dykxu^P)tyMO0g*$QtF*W_f}wiBgQFwk`G&4>}$}s+Odqo_VY!0d>Q7xg4o1aOCFERJ-kh$-1~LvD=LxfGHF>jq)F(clOg+Q|qR zL!p@5`lsitz=L_H(m`tAD=A`&^2_PdLqo4@)qkN-Ea91a)eI5qCw0k0fxfj<=VZ)& z@`Qd)pB_3pWc?QC=M)=D?9EQpN5B~Sv+Tjm~_ zA_jI8H@ro0t}tZzA=Lx34$}B}s*Gb=9bd6>_hlONP~y32d?L6c^8yyjowc}3LnHIb zNcoh7l$QzS#U(H^*07?3;pe1Mi3U6tLLrJXBt=*&Kb_T8Va`qg2^#q6Mhj7m24B!2 z1I$Uf4lud~QL%iC9y9th2I|_rNZRy-$_IDdQTAh5!|8I+!iJE^m5**EFN#=1TbSUF zLSTVLw3Du)1%fenHVgRU*kIb%AU#052Y0k`nF|YsOqb{9!BhEZ znFd;FVb>0wEH!$5-pUer1`Y~~x=#>Z6mUsL*H|iH2cr>11LYTZ&{N1o8&0BoTO%5m z&+U-+P zp+k@^@{9QemCRwHjhu=hUPC2VZj{YJjM!=tPHj=rqb~i}8|<&l7pFx&4H%acK~j_S zc;{!VdH_4dqH)j(4sf1p$vEb=qm7q-V-OEDaJT4AVUupE8ooXxZZFo4#UVR-d0`d{ zb9n_DRFaxEN4LqFE_bp0f@R1W7-$Ag3Jnsp@}tFK^(0w!(>5g&mAn>8AX-_BL>f%x zn8Ml--Ng_t;KtKWNm}^{y{4&3QZ$MsEqc=hMfcDrq8S1$7X2TDife=6otqPS;IXWLyfXqtW@6-l^_l4$SB#mEx?QwroeUe zS*%G~vJuIxIKqlX^AWEJk~G4^iSeg`PZ%hzu)C!Oj)VD;AghMxc7Biy6UUn%Hd+y? zUXpENhXwdOYGjJp0=5cWAn6><-RoouYSnx{uvVsstWxi%s+S!KKcr@+SRg&v_4bq0 z%oL>2s0|6P8pB3e4wF=+F@TkL=!#s2h?b0QcWH!jg`OQU)g&w4w>any5oh;C%@Owh zj!3p|+^CA2t_BX9?M&aPd81f@rkx8S=sv(Qk<&6a5x>%|2PNBKX zDb!8u$LS2qo$P7|T39$+(@ON z=kuZ}bxW_nk`r{iu3{#n;yyZ(N);06RBi|pv)w? zMmsIjPk3bpn_d~%2!MW|Ju}0MVBSIqlNu(v>7&uy3{uuG^nqqajkCOM46}(~GoWFf z2!OaqH9<)NEz<{u1@O2);9B<`?n`Arp2yt8Ke*7R;TLZQANyODqi?>t@_Hq_ z=gzse&b@v9_LZ-^T%c<+$h8jLZrN;VZ@uJ!?{Uu8y5d{+c2&ar?p%25!fMO?XV$}s z+gBd>TGm2GHhf1n16=UnecyWX(MQ47wY?WNf|nlC!Pvv#)k^#M{0*G`!9XmfAVhgif83arL+59`fmCU7e47c@lofAwUgIB z>AbPtd82aZ@XFYuuHG-2xq&$F*Vg^fzR2U=L#qq#cpr?u8~V6+c;#ZHdtmkFcNg!y zarce&?x!9+z0rO8)4`Lgfy(jJU)8^;?}_WjpIr@C2IKEFJ5c~6Ssq#=_3bDtZ^Ni*FBtX z&$7Pm?X;B=T#KAp51*}SC9&a4KBnrq_|SK$(%cUsHy>fjIlmEnhAC(GVergn!S+g9 z$Gw){x2*MDdjHCL+qGr?Kf`U0_O!2cJoRAz-}>L_fA`?SJx^DH;XA?K23OjD7iMN# zx%|-IvwE@O^Z#b-S7Xap-h7c-uJ}^noFN{p#0WO~Igd!{76@2LG4g d?zWHhdp|heF_!Rt*dH1@(fnZ|H1=%se*h2a=QaQU 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") From e779818e73f582917e68b75664f0e121c2188d95 Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Sat, 9 May 2026 13:41:54 +0000 Subject: [PATCH 15/71] 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 zcmeGhNo*U}bto>PNXyn{YqkAFFClF%vSm9CW2Nz;$O|H=qyn@E6Jj*;DNQt-VgAf$ zq2a&vn?SZ+~RJ=HzcK@QES=bVzO0$B+?1&SCs6uBwLAwWKWj;&*%^P2~-DP{@crOf8??p;j*4=(+v;tWWA&* z=Z$c=rn4SvR})8kIbSAh13TZd)gcLFo9N%!CTjw5Ss!W6`bjX?oC`b)lYBqNW)t$x5}TAZ0|h=g zBYQ!nrZ9)N+}`N|%ya?ey}AAM^lfbbA6d#nt~J-@2TZ&1k!?-?g5ww+uyuAXIgkrx zc9*<6*PY#$@+1H=>B;p_D!I1fTsE8;c4V1=b-sY1&*8>8OWB&d)zfs@*>8`4tLlk^ z^RwkVF}?Lfm1n1>(j$e+R5eB>>B7%$RJVvA93})L4yev=i!d&umD#>{H z`IK7HQ?m?TDmjmp`IIb8r%Frutf~wrPYtIuNymv2fbcU|OHp$0*0nqxB}+@?K~#OP zEEOE$BO@pxsIMDRB+dWJ$8kIFDp9T@>5#pHer0uL29u?a>>iKC&;mHyLo*VT6 zYblGzZ2$#TDM&K_NL6C}NX}qA8nGba=;kdqH}k3_VAyZbt%qPMkvNKACI3=3Ap@tp zkSJnNGKyP7Dykxu^P)tyMO0g*$QtF*W_f}wiBgQFwk`G&4>}$}s+Odqo_VY!0d>Q7xg4o1aOCFERJ-kh$-1~LvD=LxfGHF>jq)F(clOg+Q|qR zL!p@5`lsitz=L_H(m`tAD=A`&^2_PdLqo4@)qkN-Ea91a)eI5qCw0k0fxfj<=VZ)& z@`Qd)pB_3pWc?QC=M)=D?9EQpN5B~Sv+Tjm~_ zA_jI8H@ro0t}tZzA=Lx34$}B}s*Gb=9bd6>_hlONP~y32d?L6c^8yyjowc}3LnHIb zNcoh7l$QzS#U(H^*07?3;pe1Mi3U6tLLrJXBt=*&Kb_T8Va`qg2^#q6Mhj7m24B!2 z1I$Uf4lud~QL%iC9y9th2I|_rNZRy-$_IDdQTAh5!|8I+!iJE^m5**EFN#=1TbSUF zLSTVLw3Du)1%fenHVgRU*kIb%AU#052Y0k`nF|YsOqb{9!BhEZ znFd;FVb>0wEH!$5-pUer1`Y~~x=#>Z6mUsL*H|iH2cr>11LYTZ&{N1o8&0BoTO%5m z&+U-+P zp+k@^@{9QemCRwHjhu=hUPC2VZj{YJjM!=tPHj=rqb~i}8|<&l7pFx&4H%acK~j_S zc;{!VdH_4dqH)j(4sf1p$vEb=qm7q-V-OEDaJT4AVUupE8ooXxZZFo4#UVR-d0`d{ zb9n_DRFaxEN4LqFE_bp0f@R1W7-$Ag3Jnsp@}tFK^(0w!(>5g&mAn>8AX-_BL>f%x zn8Ml--Ng_t;KtKWNm}^{y{4&3QZ$MsEqc=hMfcDrq8S1$7X2TDife=6otqPS;IXWLyfXqtW@6-l^_l4$SB#mEx?QwroeUe zS*%G~vJuIxIKqlX^AWEJk~G4^iSeg`PZ%hzu)C!Oj)VD;AghMxc7Biy6UUn%Hd+y? zUXpENhXwdOYGjJp0=5cWAn6><-RoouYSnx{uvVsstWxi%s+S!KKcr@+SRg&v_4bq0 z%oL>2s0|6P8pB3e4wF=+F@TkL=!#s2h?b0QcWH!jg`OQU)g&w4w>any5oh;C%@Owh zj!3p|+^CA2t_BX9?M&aPd81f@rkx8S=sv(Qk<&6a5x>%|2PNBKX zDb!8u$LS2qo$P7|T39$+(@ON z=kuZ}bxW_nk`r{iu3{#n;yyZ(N);06RBi|pv)w? zMmsIjPk3bpn_d~%2!MW|Ju}0MVBSIqlNu(v>7&uy3{uuG^nqqajkCOM46}(~GoWFf z2!OaqH9<)NEz<{u1@O2);9B<`?n`Arp2yt8Ke*7R;TLZQANyODqi?>t@_Hq_ z=gzse&b@v9_LZ-^T%c<+$h8jLZrN;VZ@uJ!?{Uu8y5d{+c2&ar?p%25!fMO?XV$}s z+gBd>TGm2GHhf1n16=UnecyWX(MQ47wY?WNf|nlC!Pvv#)k^#M{0*G`!9XmfAVhgif83arL+59`fmCU7e47c@lofAwUgIB z>AbPtd82aZ@XFYuuHG-2xq&$F*Vg^fzR2U=L#qq#cpr?u8~V6+c;#ZHdtmkFcNg!y zarce&?x!9+z0rO8)4`Lgfy(jJU)8^;?}_WjpIr@C2IKEFJ5c~6Ssq#=_3bDtZ^Ni*FBtX z&$7Pm?X;B=T#KAp51*}SC9&a4KBnrq_|SK$(%cUsHy>fjIlmEnhAC(GVergn!S+g9 z$Gw){x2*MDdjHCL+qGr?Kf`U0_O!2cJoRAz-}>L_fA`?SJx^DH;XA?K23OjD7iMN# zx%|-IvwE@O^Z#b-S7Xap-h7c-uJ}^noFN{p#0WO~Igd!{76@2LG4g d?zWHhdp|heF_!Rt*dH1@(fnZ|H1=%se*h2a=QaQU From 78f499bde89c6d9a0ef907e95107065506b614e0 Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Sat, 9 May 2026 13:55:38 +0000 Subject: [PATCH 16/71] 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 From 3f080da35e95e37eef468a1865da55d831fdf0be Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Sat, 9 May 2026 13:59:09 +0000 Subject: [PATCH 17/71] 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: From 77fe8133ae36d03ba405b667a7b8ad81dc634e8d Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Sat, 9 May 2026 14:09:50 +0000 Subject: [PATCH 18/71] 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 From b3fa424661c85a00e716d5acddb4fdfcdb71d38a Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Sat, 9 May 2026 14:12:06 +0000 Subject: [PATCH 19/71] 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 ---------- From 0a9507de6585240caedf20439647e079649c8262 Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Sat, 9 May 2026 14:14:52 +0000 Subject: [PATCH 20/71] 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 From b750d26d801acb32ed1ec240392455f2b9763667 Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Sat, 9 May 2026 14:20:46 +0000 Subject: [PATCH 21/71] 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 ---------- From 3016d0da2c6a3f2a3baf3d032ae54adb830d6bec Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Sat, 9 May 2026 14:27:07 +0000 Subject: [PATCH 22/71] 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() From 8e9a75fe5c915239a5eb05c72b670ea1477f4a25 Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Sat, 9 May 2026 14:28:35 +0000 Subject: [PATCH 23/71] 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}" From 90e227bc4e8a2028978a069b4cf3dd9df9803b01 Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Sat, 9 May 2026 15:21:49 +0000 Subject: [PATCH 24/71] 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 ---------- From 1a1cfec80aa39292e7bf2639baad7d272ff152c3 Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Sat, 9 May 2026 15:50:29 +0000 Subject: [PATCH 25/71] 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}" From d97f1cb1e5c762ce779f76ac26a61b5430f87f32 Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Sat, 9 May 2026 16:04:32 +0000 Subject: [PATCH 26/71] 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 "$@" From 0609720b33257a174f17939a70bc58a5132c18ce Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Sat, 9 May 2026 17:13:01 +0000 Subject: [PATCH 27/71] 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, From cfa2a898c3d35f96c5d9b99f3b84581564368915 Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Sat, 9 May 2026 17:36:26 +0000 Subject: [PATCH 28/71] 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}") From a40e347dfa7c547950a6106329dd15c16ecbc9bb Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Sat, 9 May 2026 17:37:32 +0000 Subject: [PATCH 29/71] 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, From 98216d2872f2a58c26c1a7dd62808c5eabcd88ce Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Sat, 9 May 2026 17:39:23 +0000 Subject: [PATCH 30/71] 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 "$@" From 6f1774366766b96913a58107942840242cfb4604 Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Sat, 9 May 2026 17:44:55 +0000 Subject: [PATCH 31/71] 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) ---------- From 3f80744ebd950739c6dfbf9033cb80bf6f206b0e Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Sat, 9 May 2026 17:47:30 +0000 Subject: [PATCH 32/71] 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}" From 748b5037b9802553e3faa8537c1c257dd0deb281 Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Sat, 9 May 2026 19:03:10 +0000 Subject: [PATCH 33/71] 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.") From b89be7b8f4ba9363030ba5d37d0cbf7086876df2 Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Sat, 9 May 2026 19:18:16 +0000 Subject: [PATCH 34/71] 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) ---------- From 93c01fa314c3cce58401157aa1158bc67fdb2382 Mon Sep 17 00:00:00 2001 From: Hermes Date: Sat, 9 May 2026 19:49:54 +0000 Subject: [PATCH 35/71] fix: add TZ=America/Montreal for correct cron scheduling --- ai/compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/ai/compose.yml b/ai/compose.yml index f7e4d8f..b8590dc 100644 --- a/ai/compose.yml +++ b/ai/compose.yml @@ -45,6 +45,7 @@ services: - HIP_VISIBLE_DEVICES=0,1 - ROCR_VISIBLE_DEVICES=0,1 - HSA_ENABLE_SDMA=0 + - TZ=America/Montreal volumes: - /mnt/HoardingCow_docker_data/Hermes/data:/opt/data devices: From ef5815589782ed30f7273c2ccb7a423730f9f68d Mon Sep 17 00:00:00 2001 From: Hermes Date: Sat, 9 May 2026 21:18:37 -0400 Subject: [PATCH 36/71] feat: add custom ollama image with ROCm 6.1 + gfx906 support - Add ollama/Dockerfile that builds ollama from source with AMDGPU_TARGETS=gfx906 - Uses ROCm 6.1 (rocm/dev-ubuntu-22.04:6.1.2-complete) for MI50 support - Builds llama.cpp runner with HIPBLAS for gfx906 architecture - Updates compose.yml to build from this Dockerfile instead of pulling ollama/ollama:latest --- ai/compose.yml | 5 +++- ai/ollama/Dockerfile | 57 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 ai/ollama/Dockerfile diff --git a/ai/compose.yml b/ai/compose.yml index b8590dc..e5d9c4b 100644 --- a/ai/compose.yml +++ b/ai/compose.yml @@ -58,7 +58,10 @@ services: - ai_backend ollama: - image: ollama/ollama:latest + build: + context: ./ollama + dockerfile: Dockerfile + image: ollama/ollama:rocm-gfx906 container_name: ollama privileged: true tty: true diff --git a/ai/ollama/Dockerfile b/ai/ollama/Dockerfile new file mode 100644 index 0000000..aca5f3a --- /dev/null +++ b/ai/ollama/Dockerfile @@ -0,0 +1,57 @@ +# ollama-gfx906/Dockerfile +# +# Custom ollama image with ROCm 6.1 + gfx906 (MI50) support. +# The default ollama/rocm image ships ROCm 7.2 which dropped gfx906 support. +# This builds ollama from source targeting AMDGPU_TARGETS=gfx906. +# +# Build: docker build -t ollama/ollama:rocm-gfx906 . + +FROM rocm/dev-ubuntu-22.04:6.1.2-complete AS builder + +RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \ + git golang cmake build-essential pkg-config \ + && rm -rf /var/lib/apt/lists/* + +ARG OLLAMA_VERSION=v0.13.5 +RUN git clone --depth 1 --branch ${OLLAMA_VERSION} https://github.com/ollama/ollama.git /build +WORKDIR /build + +ENV HIP_PATH=/opt/rocm +ENV ROCM_PATH=/opt/rocm +ENV PATH=/opt/rocm/bin:/opt/rocm/hip/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin +RUN cd llama.cpp && \ + mkdir build && cd build && \ + cmake .. \ + -DLLAMA_HIPBLAS=ON \ + -DCMAKE_C_COMPILER=clang \ + -DCMAKE_CXX_COMPILER=clang++ \ + -DAMDGPU_TARGETS=gfx906 \ + -DCMAKE_BUILD_TYPE=Release \ + -DLLAMA_NATIVE=OFF \ + -DLLAMA_BUILD_TESTS=OFF \ + -DLLAMA_BUILD_EXAMPLES=OFF \ + -DLLAMA_BUILD_SERVER=OFF && \ + cmake --build . --config Release -j$(nproc) && \ + cmake --install . --prefix /build/dist + +ENV CGO_ENABLED=0 +RUN go build -trimpath -o dist/ollama . + +FROM ubuntu:22.04 + +RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \ + ca-certificates curl libstdc++6 libgomp1 \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=builder /opt/rocm/lib/ /opt/rocm/lib/ +COPY --from=builder /opt/rocm/share/ /opt/rocm/share/ +COPY --from=builder /build/dist/ollama /usr/bin/ollama +COPY --from=builder /build/dist/lib/ /usr/lib/ollama/ + +RUN ldconfig + +ENV LD_LIBRARY_PATH=/opt/rocm/lib:/usr/lib/ollama +ENV HSA_OVERRIDE_GFX_VERSION=9.0.6 + +EXPOSE 11434 +CMD ["serve"] From d34a4d3647e6a265f7f52042eded9a8272bd2f1e Mon Sep 17 00:00:00 2001 From: Hermes Date: Sat, 9 May 2026 21:50:04 -0400 Subject: [PATCH 37/71] refactor: move hermes files into ai/hermes/ subdirectory - ai/Dockerfile -> ai/hermes/Dockerfile - ai/fix-permissions.sh -> ai/hermes/fix-permissions.sh - ai/patch_tts_tool.py -> ai/hermes/patch_tts_tool.py - ai/compose.yml: update hermes build context to ./hermes - ollama stays at ai/ollama/Dockerfile --- ai/compose.yml | 2 +- ai/{ => hermes}/Dockerfile | 0 ai/{ => hermes}/fix-permissions.sh | 0 ai/{ => hermes}/patch_tts_tool.py | 0 4 files changed, 1 insertion(+), 1 deletion(-) rename ai/{ => hermes}/Dockerfile (100%) rename ai/{ => hermes}/fix-permissions.sh (100%) rename ai/{ => hermes}/patch_tts_tool.py (100%) diff --git a/ai/compose.yml b/ai/compose.yml index e5d9c4b..dceb490 100644 --- a/ai/compose.yml +++ b/ai/compose.yml @@ -26,7 +26,7 @@ services: # - "traefik.http.routers.webui-https.tls.certresolver=njalla" hermes: - build: ./ + build: ./hermes container_name: hermes restart: always # Gateway run enables the internal API server on port 8642 diff --git a/ai/Dockerfile b/ai/hermes/Dockerfile similarity index 100% rename from ai/Dockerfile rename to ai/hermes/Dockerfile diff --git a/ai/fix-permissions.sh b/ai/hermes/fix-permissions.sh similarity index 100% rename from ai/fix-permissions.sh rename to ai/hermes/fix-permissions.sh diff --git a/ai/patch_tts_tool.py b/ai/hermes/patch_tts_tool.py similarity index 100% rename from ai/patch_tts_tool.py rename to ai/hermes/patch_tts_tool.py From f023dc1ee415c3440f09a3be89662e53ef10da51 Mon Sep 17 00:00:00 2001 From: Hermes Date: Sat, 9 May 2026 21:56:14 -0400 Subject: [PATCH 38/71] fix: update ollama Dockerfile to v0.23.2 with proper ROCm 6.1 + gfx906 build - Update OLLAMA_VERSION from v0.13.5 to v0.23.2 - Fix package: golang -> golang-go - Add ENV HCC_AMDGPU_TARGET=gfx906 and HSA_ENABLE_SDMA=0 - Set proper ENTRYPOINT + CMD --- ai/ollama/Dockerfile | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/ai/ollama/Dockerfile b/ai/ollama/Dockerfile index aca5f3a..88cc8b3 100644 --- a/ai/ollama/Dockerfile +++ b/ai/ollama/Dockerfile @@ -2,17 +2,17 @@ # # Custom ollama image with ROCm 6.1 + gfx906 (MI50) support. # The default ollama/rocm image ships ROCm 7.2 which dropped gfx906 support. -# This builds ollama from source targeting AMDGPU_TARGETS=gfx906. +# This builds ollama and its llama.cpp runner from source, targeting gfx906. # -# Build: docker build -t ollama/ollama:rocm-gfx906 . +# Build: docker build -t ollama/ollama:rocm-gfx906 ./ai/ollama FROM rocm/dev-ubuntu-22.04:6.1.2-complete AS builder RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \ - git golang cmake build-essential pkg-config \ + git golang-go cmake build-essential pkg-config \ && rm -rf /var/lib/apt/lists/* -ARG OLLAMA_VERSION=v0.13.5 +ARG OLLAMA_VERSION=v0.23.2 RUN git clone --depth 1 --branch ${OLLAMA_VERSION} https://github.com/ollama/ollama.git /build WORKDIR /build @@ -20,7 +20,7 @@ ENV HIP_PATH=/opt/rocm ENV ROCM_PATH=/opt/rocm ENV PATH=/opt/rocm/bin:/opt/rocm/hip/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin RUN cd llama.cpp && \ - mkdir build && cd build && \ + mkdir -p build && cd build && \ cmake .. \ -DLLAMA_HIPBLAS=ON \ -DCMAKE_C_COMPILER=clang \ @@ -52,6 +52,9 @@ RUN ldconfig ENV LD_LIBRARY_PATH=/opt/rocm/lib:/usr/lib/ollama ENV HSA_OVERRIDE_GFX_VERSION=9.0.6 +ENV HCC_AMDGPU_TARGET=gfx906 +ENV HSA_ENABLE_SDMA=0 EXPOSE 11434 +ENTRYPOINT ["/bin/ollama"] CMD ["serve"] From c6d2f5918fbbd2b23a3c55f88e56e87fb4a5a09b Mon Sep 17 00:00:00 2001 From: Hermes Date: Sat, 9 May 2026 22:13:47 -0400 Subject: [PATCH 39/71] fix: use ollama v0.23.2 native CMake build system for ROCm 6 + gfx906 The old Dockerfile used the deprecated llama.cpp/ subdirectory approach which doesn't exist in ollama v0.23.2. Now using the official CMake presets (ROCm 6 preset) with AMDGPU_TARGETS including gfx906:xnack-. --- ai/ollama/Dockerfile | 68 ++++++++++++++++++++++++++++++-------------- 1 file changed, 46 insertions(+), 22 deletions(-) diff --git a/ai/ollama/Dockerfile b/ai/ollama/Dockerfile index 88cc8b3..4b92e82 100644 --- a/ai/ollama/Dockerfile +++ b/ai/ollama/Dockerfile @@ -1,50 +1,74 @@ # ollama-gfx906/Dockerfile # # Custom ollama image with ROCm 6.1 + gfx906 (MI50) support. -# The default ollama/rocm image ships ROCm 7.2 which dropped gfx906 support. -# This builds ollama and its llama.cpp runner from source, targeting gfx906. +# The official ollama/rocm image ships ROCm 7.2 which dropped gfx906. +# This uses v0.23.2's native CMake build system with AMDGPU_TARGETS including gfx906. # -# Build: docker build -t ollama/ollama:rocm-gfx906 ./ai/ollama +# Build: docker build -t ollama/ollama:rocm-gfx906 ai/ollama FROM rocm/dev-ubuntu-22.04:6.1.2-complete AS builder +# Build dependencies (CMake, Ninja, Go) +ARG CMAKEVERSION=3.31.2 +ARG NINJAVERSION=1.12.1 +ARG GOLANG_VERSION=1.22.0 + RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \ - git golang-go cmake build-essential pkg-config \ + curl git ccache build-essential pkg-config \ && rm -rf /var/lib/apt/lists/* +# Install CMake from official binaries +RUN curl -fsSL https://github.com/Kitware/CMake/releases/download/v${CMAKEVERSION}/cmake-${CMAKEVERSION}-linux-x86_64.tar.gz \ + | tar xz -C /usr/local --strip-components 1 + +# Install Ninja +RUN curl -fsSL -o /tmp/ninja.zip \ + https://github.com/ninja-build/ninja/releases/download/v${NINJAVERSION}/ninja-linux.zip \ + && unzip /tmp/ninja.zip -d /usr/local/bin && rm /tmp/ninja.zip + +# Install Go +RUN curl -fsSL https://go.dev/dl/go${GOLANG_VERSION}.linux-amd64.tar.gz \ + | tar xz -C /usr/local +ENV PATH=/usr/local/go/bin:$PATH + ARG OLLAMA_VERSION=v0.23.2 RUN git clone --depth 1 --branch ${OLLAMA_VERSION} https://github.com/ollama/ollama.git /build WORKDIR /build +# ROCm paths from the base image ENV HIP_PATH=/opt/rocm ENV ROCM_PATH=/opt/rocm -ENV PATH=/opt/rocm/bin:/opt/rocm/hip/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin -RUN cd llama.cpp && \ - mkdir -p build && cd build && \ +ENV PATH=/opt/rocm/bin:/opt/rocm/hip/bin:/opt/rocm/hcc/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin +ENV CMAKE_GENERATOR=Ninja +ENV LDFLAGS=-s + +# Build with ROCm 6 preset + gfx906 target +RUN mkdir -p build && cd build && \ cmake .. \ - -DLLAMA_HIPBLAS=ON \ - -DCMAKE_C_COMPILER=clang \ - -DCMAKE_CXX_COMPILER=clang++ \ - -DAMDGPU_TARGETS=gfx906 \ - -DCMAKE_BUILD_TYPE=Release \ - -DLLAMA_NATIVE=OFF \ - -DLLAMA_BUILD_TESTS=OFF \ - -DLLAMA_BUILD_EXAMPLES=OFF \ - -DLLAMA_BUILD_SERVER=OFF && \ - cmake --build . --config Release -j$(nproc) && \ - cmake --install . --prefix /build/dist + --preset 'ROCm 6' \ + -DAMDGPU_TARGETS="gfx906:xnack-;gfx940;gfx1010;gfx1030;gfx1100;gfx1200" \ + -DCMAKE_BUILD_TYPE=Release && \ + cmake --build . -- -l $(nproc) && \ + cmake --install . --component HIP --strip --prefix /build/dist -ENV CGO_ENABLED=0 -RUN go build -trimpath -o dist/ollama . +# Build the Go binary +ENV CGO_ENABLED=1 +ENV CC=clang +ENV CXX=clang++ +RUN go build -trimpath -o /build/dist/ollama . -FROM ubuntu:22.04 +# ---------- Runtime image ---------- +FROM ubuntu:24.04 RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \ - ca-certificates curl libstdc++6 libgomp1 \ + ca-certificates curl libstdc++6 libgomp1 libvulkan1 libopenblas0 \ && rm -rf /var/lib/apt/lists/* +# Copy ROCm 6.1 runtime libraries COPY --from=builder /opt/rocm/lib/ /opt/rocm/lib/ COPY --from=builder /opt/rocm/share/ /opt/rocm/share/ + +# Copy ollama binary and backend COPY --from=builder /build/dist/ollama /usr/bin/ollama COPY --from=builder /build/dist/lib/ /usr/lib/ollama/ From 956d76f14db063017dd893991e3f86fe6f1b3b81 Mon Sep 17 00:00:00 2001 From: Hermes Date: Sat, 9 May 2026 22:14:53 -0400 Subject: [PATCH 40/71] fix: add unzip dependency for ninja installation --- ai/ollama/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ai/ollama/Dockerfile b/ai/ollama/Dockerfile index 4b92e82..335987f 100644 --- a/ai/ollama/Dockerfile +++ b/ai/ollama/Dockerfile @@ -14,7 +14,7 @@ ARG NINJAVERSION=1.12.1 ARG GOLANG_VERSION=1.22.0 RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \ - curl git ccache build-essential pkg-config \ + curl git ccache build-essential pkg-config unzip \ && rm -rf /var/lib/apt/lists/* # Install CMake from official binaries From a3d0fa0072a954a3f0bf44096df5bacc65fb25bb Mon Sep 17 00:00:00 2001 From: Hermes Date: Sat, 9 May 2026 22:19:50 -0400 Subject: [PATCH 41/71] fix: set CMAKE_HIP_COMPILER explicitly for ROCm 6.1 HIP detection --- ai/ollama/Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/ai/ollama/Dockerfile b/ai/ollama/Dockerfile index 335987f..170ecc6 100644 --- a/ai/ollama/Dockerfile +++ b/ai/ollama/Dockerfile @@ -47,6 +47,7 @@ RUN mkdir -p build && cd build && \ cmake .. \ --preset 'ROCm 6' \ -DAMDGPU_TARGETS="gfx906:xnack-;gfx940;gfx1010;gfx1030;gfx1100;gfx1200" \ + -DCMAKE_HIP_COMPILER=/opt/rocm/bin/hipcc \ -DCMAKE_BUILD_TYPE=Release && \ cmake --build . -- -l $(nproc) && \ cmake --install . --component HIP --strip --prefix /build/dist From d8b77c97c34ef541c3e8aa3462561f4b99c4c03f Mon Sep 17 00:00:00 2001 From: Hermes Date: Sat, 9 May 2026 22:20:44 -0400 Subject: [PATCH 42/71] fix: use CXX=hipcc legacy mode for HIP CMake build CMake 3.31 refuses CMAKE_HIP_COMPILER=hipcc with 'not supported'. Using CXX=hipcc triggers the legacy HIP detection path which works. --- ai/ollama/Dockerfile | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/ai/ollama/Dockerfile b/ai/ollama/Dockerfile index 170ecc6..2c6db27 100644 --- a/ai/ollama/Dockerfile +++ b/ai/ollama/Dockerfile @@ -42,14 +42,13 @@ ENV PATH=/opt/rocm/bin:/opt/rocm/hip/bin:/opt/rocm/hcc/bin:/usr/local/sbin:/usr/ ENV CMAKE_GENERATOR=Ninja ENV LDFLAGS=-s -# Build with ROCm 6 preset + gfx906 target +# Build with ROCm 6 preset + gfx906 target (CXX=hipcc for legacy HIP mode) RUN mkdir -p build && cd build && \ - cmake .. \ + CC=hipcc CXX=hipcc cmake .. \ --preset 'ROCm 6' \ -DAMDGPU_TARGETS="gfx906:xnack-;gfx940;gfx1010;gfx1030;gfx1100;gfx1200" \ - -DCMAKE_HIP_COMPILER=/opt/rocm/bin/hipcc \ -DCMAKE_BUILD_TYPE=Release && \ - cmake --build . -- -l $(nproc) && \ + CC=hipcc CXX=hipcc cmake --build . -- -l $(nproc) && \ cmake --install . --component HIP --strip --prefix /build/dist # Build the Go binary From 5b210fe6241ce79fff854e2d7ec94dc7a319de64 Mon Sep 17 00:00:00 2001 From: Hermes Date: Sat, 9 May 2026 22:29:10 -0400 Subject: [PATCH 43/71] fix: use ROCm amdclang++ as HIP compiler, keep GCC for CPU code Setting CXX=hipcc caused compilation failures on CPU backends (AVX intrinsics). Now using GCC for CPU, ROCm's amdclang++ for HIP only. --- ai/ollama/Dockerfile | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ai/ollama/Dockerfile b/ai/ollama/Dockerfile index 2c6db27..172af7c 100644 --- a/ai/ollama/Dockerfile +++ b/ai/ollama/Dockerfile @@ -42,13 +42,14 @@ ENV PATH=/opt/rocm/bin:/opt/rocm/hip/bin:/opt/rocm/hcc/bin:/usr/local/sbin:/usr/ ENV CMAKE_GENERATOR=Ninja ENV LDFLAGS=-s -# Build with ROCm 6 preset + gfx906 target (CXX=hipcc for legacy HIP mode) +# Build with ROCm 6 preset + gfx906 target (ROCm clang for HIP, GCC for CPU) RUN mkdir -p build && cd build && \ - CC=hipcc CXX=hipcc cmake .. \ + cmake .. \ --preset 'ROCm 6' \ -DAMDGPU_TARGETS="gfx906:xnack-;gfx940;gfx1010;gfx1030;gfx1100;gfx1200" \ + -DCMAKE_HIP_COMPILER=/opt/rocm/bin/amdclang++ \ -DCMAKE_BUILD_TYPE=Release && \ - CC=hipcc CXX=hipcc cmake --build . -- -l $(nproc) && \ + cmake --build . -- -l $(nproc) && \ cmake --install . --component HIP --strip --prefix /build/dist # Build the Go binary From 0c612d97312a561717b0adba9a9bfe7913208d9f Mon Sep 17 00:00:00 2001 From: Hermes Date: Sat, 9 May 2026 22:30:21 -0400 Subject: [PATCH 44/71] fix: remove unsupported AMDGPU_TARGETS (gfx1200) for ROCm 6.1 ROCm 6.1's AMD clang 17 doesn't support gfx1200 (RDNA4). Use only targets supported by ROCm 6.1: gfx906, gfx908, gfx90a, gfx1030, gfx1100. --- ai/ollama/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ai/ollama/Dockerfile b/ai/ollama/Dockerfile index 172af7c..233229f 100644 --- a/ai/ollama/Dockerfile +++ b/ai/ollama/Dockerfile @@ -46,7 +46,7 @@ ENV LDFLAGS=-s RUN mkdir -p build && cd build && \ cmake .. \ --preset 'ROCm 6' \ - -DAMDGPU_TARGETS="gfx906:xnack-;gfx940;gfx1010;gfx1030;gfx1100;gfx1200" \ + -DAMDGPU_TARGETS="gfx906:xnack-;gfx908:xnack-;gfx90a:xnack-;gfx1030;gfx1100" \ -DCMAKE_HIP_COMPILER=/opt/rocm/bin/amdclang++ \ -DCMAKE_BUILD_TYPE=Release && \ cmake --build . -- -l $(nproc) && \ From aa6bbe87bfeced300cb6606b4f04124b31e4f84d Mon Sep 17 00:00:00 2001 From: Hermes Date: Sat, 9 May 2026 22:40:40 -0400 Subject: [PATCH 45/71] fix: correct AMDGPU_TARGETS to include gfx940/gfx1010/gfx1200 Targets were corrupted during previous patch iterations, contained gfx908/gfx90a from the CMake preset instead of gfx940/gfx1010/gfx1200. --- ai/ollama/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ai/ollama/Dockerfile b/ai/ollama/Dockerfile index 233229f..172af7c 100644 --- a/ai/ollama/Dockerfile +++ b/ai/ollama/Dockerfile @@ -46,7 +46,7 @@ ENV LDFLAGS=-s RUN mkdir -p build && cd build && \ cmake .. \ --preset 'ROCm 6' \ - -DAMDGPU_TARGETS="gfx906:xnack-;gfx908:xnack-;gfx90a:xnack-;gfx1030;gfx1100" \ + -DAMDGPU_TARGETS="gfx906:xnack-;gfx940;gfx1010;gfx1030;gfx1100;gfx1200" \ -DCMAKE_HIP_COMPILER=/opt/rocm/bin/amdclang++ \ -DCMAKE_BUILD_TYPE=Release && \ cmake --build . -- -l $(nproc) && \ From f6bc2b07a79cff31962a20a071716be9d22c18a7 Mon Sep 17 00:00:00 2001 From: Hermes Date: Sat, 9 May 2026 22:41:18 -0400 Subject: [PATCH 46/71] fix: remove nonexistent CC=clang for Go build step ROCm 6.1 image doesn't have clang/clang++ in PATH (only amdclang++). GCC is the default and works fine for CGo linking. --- ai/ollama/Dockerfile | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ai/ollama/Dockerfile b/ai/ollama/Dockerfile index 172af7c..4fac4f8 100644 --- a/ai/ollama/Dockerfile +++ b/ai/ollama/Dockerfile @@ -52,10 +52,8 @@ RUN mkdir -p build && cd build && \ cmake --build . -- -l $(nproc) && \ cmake --install . --component HIP --strip --prefix /build/dist -# Build the Go binary +# Build the Go binary (use GCC for CGo linking) ENV CGO_ENABLED=1 -ENV CC=clang -ENV CXX=clang++ RUN go build -trimpath -o /build/dist/ollama . # ---------- Runtime image ---------- From 0d87fb25564d5439c02eef8f71d1e9a352f2a1d2 Mon Sep 17 00:00:00 2001 From: Hermes Date: Sat, 9 May 2026 22:51:13 -0400 Subject: [PATCH 47/71] fix: build CPU and HIP backends separately CPU backends compiled with GCC (fixes AVX intrinsic errors from hipcc). HIP backend compiled with hipcc (legacy mode skips enable_language(HIP)). Go binary built with GCC for CGo linking. This avoids both CMAKE_HIP_COMPILER rejection and CXX=hipcc CPU failures. --- ai/ollama/Dockerfile | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/ai/ollama/Dockerfile b/ai/ollama/Dockerfile index 4fac4f8..aedee93 100644 --- a/ai/ollama/Dockerfile +++ b/ai/ollama/Dockerfile @@ -35,24 +35,28 @@ ARG OLLAMA_VERSION=v0.23.2 RUN git clone --depth 1 --branch ${OLLAMA_VERSION} https://github.com/ollama/ollama.git /build WORKDIR /build -# ROCm paths from the base image +# ROCm paths ENV HIP_PATH=/opt/rocm ENV ROCM_PATH=/opt/rocm ENV PATH=/opt/rocm/bin:/opt/rocm/hip/bin:/opt/rocm/hcc/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin ENV CMAKE_GENERATOR=Ninja ENV LDFLAGS=-s -# Build with ROCm 6 preset + gfx906 target (ROCm clang for HIP, GCC for CPU) -RUN mkdir -p build && cd build && \ - cmake .. \ - --preset 'ROCm 6' \ +# Step 1: Build CPU backends with GCC (default compiler) +RUN mkdir -p build-cpu && cd build-cpu && \ + cmake .. -DCMAKE_BUILD_TYPE=Release && \ + cmake --build . --target ggml-cpu -- -l $(nproc) && \ + cmake --install . --component CPU --strip --prefix /build/dist + +# Step 2: Build HIP backend with hipcc (legacy mode skips enable_language(HIP)) +RUN mkdir -p build-hip && cd build-hip && \ + CC=hipcc CXX=hipcc cmake .. \ -DAMDGPU_TARGETS="gfx906:xnack-;gfx940;gfx1010;gfx1030;gfx1100;gfx1200" \ - -DCMAKE_HIP_COMPILER=/opt/rocm/bin/amdclang++ \ -DCMAKE_BUILD_TYPE=Release && \ - cmake --build . -- -l $(nproc) && \ + CC=hipcc CXX=hipcc cmake --build . --target ggml-hip -- -l $(nproc) && \ cmake --install . --component HIP --strip --prefix /build/dist -# Build the Go binary (use GCC for CGo linking) +# Step 3: Build Go binary (uses GCC for CGo linking) ENV CGO_ENABLED=1 RUN go build -trimpath -o /build/dist/ollama . @@ -67,7 +71,7 @@ RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \ COPY --from=builder /opt/rocm/lib/ /opt/rocm/lib/ COPY --from=builder /opt/rocm/share/ /opt/rocm/share/ -# Copy ollama binary and backend +# Copy ollama binary + all backends (CPU + HIP) COPY --from=builder /build/dist/ollama /usr/bin/ollama COPY --from=builder /build/dist/lib/ /usr/lib/ollama/ From d52f18b0fa75166f5087a3e0de320137574d1ac1 Mon Sep 17 00:00:00 2001 From: Hermes Date: Sat, 9 May 2026 22:53:11 -0400 Subject: [PATCH 48/71] fix: remove gfx1200 target (not supported by ROCm 6.1 clang 17) ROCm 6.1's AMD clang 17 doesn't recognize gfx1200 architecture (introduced in ROCm 6.2+). Caused compilation failure on all .cu files. --- ai/ollama/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ai/ollama/Dockerfile b/ai/ollama/Dockerfile index aedee93..f530de2 100644 --- a/ai/ollama/Dockerfile +++ b/ai/ollama/Dockerfile @@ -51,7 +51,7 @@ RUN mkdir -p build-cpu && cd build-cpu && \ # Step 2: Build HIP backend with hipcc (legacy mode skips enable_language(HIP)) RUN mkdir -p build-hip && cd build-hip && \ CC=hipcc CXX=hipcc cmake .. \ - -DAMDGPU_TARGETS="gfx906:xnack-;gfx940;gfx1010;gfx1030;gfx1100;gfx1200" \ + -DAMDGPU_TARGETS="gfx906:xnack-;gfx940;gfx1010;gfx1030;gfx1100" \ -DCMAKE_BUILD_TYPE=Release && \ CC=hipcc CXX=hipcc cmake --build . --target ggml-hip -- -l $(nproc) && \ cmake --install . --component HIP --strip --prefix /build/dist From fc777e2de287dc9d76f4f6f546a58760c210cf4f Mon Sep 17 00:00:00 2001 From: Hermes Date: Sat, 9 May 2026 23:07:39 -0400 Subject: [PATCH 49/71] fix: target only gfx906 for HIP compilation gfx940/gfx1010/gfx1030/gfx1100 cause C++ narrowing errors in ollama's mma.cuh with hipcc. Since we only have MI50 (gfx906) cards, compile for gfx906 only. Reduces build time and avoids upstream code bugs. --- ai/ollama/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ai/ollama/Dockerfile b/ai/ollama/Dockerfile index f530de2..ca92084 100644 --- a/ai/ollama/Dockerfile +++ b/ai/ollama/Dockerfile @@ -51,7 +51,7 @@ RUN mkdir -p build-cpu && cd build-cpu && \ # Step 2: Build HIP backend with hipcc (legacy mode skips enable_language(HIP)) RUN mkdir -p build-hip && cd build-hip && \ CC=hipcc CXX=hipcc cmake .. \ - -DAMDGPU_TARGETS="gfx906:xnack-;gfx940;gfx1010;gfx1030;gfx1100" \ + -DAMDGPU_TARGETS="gfx906:xnack-" \ -DCMAKE_BUILD_TYPE=Release && \ CC=hipcc CXX=hipcc cmake --build . --target ggml-hip -- -l $(nproc) && \ cmake --install . --component HIP --strip --prefix /build/dist From 0f7b22c19bcec54c1fc928cea3af8f41e5a60c47 Mon Sep 17 00:00:00 2001 From: Hermes Date: Sat, 9 May 2026 23:15:26 -0400 Subject: [PATCH 50/71] fix: add /usr/local/go/bin to ROCm PATH (was overridden) ENV PATH for ROCm overwrote the previous PATH that included Go. Without Go in PATH, 'go build' fails with 'go: not found'. --- ai/ollama/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ai/ollama/Dockerfile b/ai/ollama/Dockerfile index ca92084..94f6b63 100644 --- a/ai/ollama/Dockerfile +++ b/ai/ollama/Dockerfile @@ -38,7 +38,7 @@ WORKDIR /build # ROCm paths ENV HIP_PATH=/opt/rocm ENV ROCM_PATH=/opt/rocm -ENV PATH=/opt/rocm/bin:/opt/rocm/hip/bin:/opt/rocm/hcc/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin +ENV PATH=/usr/local/go/bin:/opt/rocm/bin:/opt/rocm/hip/bin:/opt/rocm/hcc/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin ENV CMAKE_GENERATOR=Ninja ENV LDFLAGS=-s From 32df5465504191afcac73219edadf4733428447d Mon Sep 17 00:00:00 2001 From: Hermes Date: Sat, 9 May 2026 23:49:08 -0400 Subject: [PATCH 51/71] fix: use ROCm 6 preset with HIP language detection for proper GPU kernel compilation - Use --preset 'ROCm 6' for HIP build step (enables enable_language(HIP)) - Remove /opt/rocm from PATH for CPU build to prevent check_language(HIP) - Add CMAKE_PREFIX_PATH=/opt/rocm so find_package(hip) finds hip-config.cmake - cmake --install --component HIP now works correctly with OLLAMA_RUNNER_DIR=rocm --- ai/ollama/Dockerfile | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/ai/ollama/Dockerfile b/ai/ollama/Dockerfile index 94f6b63..65ffe9b 100644 --- a/ai/ollama/Dockerfile +++ b/ai/ollama/Dockerfile @@ -38,25 +38,37 @@ WORKDIR /build # ROCm paths ENV HIP_PATH=/opt/rocm ENV ROCM_PATH=/opt/rocm -ENV PATH=/usr/local/go/bin:/opt/rocm/bin:/opt/rocm/hip/bin:/opt/rocm/hcc/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin ENV CMAKE_GENERATOR=Ninja ENV LDFLAGS=-s -# Step 1: Build CPU backends with GCC (default compiler) +# Step 1: Build CPU backends with GCC (no ROCm preset) +# Remove /opt/rocm from PATH to prevent check_language(HIP) from +# finding a HIP compiler, which would trigger the HIP block that +# requires find_package(hip) with proper CMAKE_PREFIX_PATH. RUN mkdir -p build-cpu && cd build-cpu && \ + PATH=/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \ cmake .. -DCMAKE_BUILD_TYPE=Release && \ cmake --build . --target ggml-cpu -- -l $(nproc) && \ cmake --install . --component CPU --strip --prefix /build/dist -# Step 2: Build HIP backend with hipcc (legacy mode skips enable_language(HIP)) +# Step 2: Build HIP backend with ROCm preset + gfx906 target only +# The ROCm 6 preset enables HIP language detection (enable_language(HIP)) +# which ensures GPU kernels are properly compiled for gfx906. +# OLLAMA_RUNNER_DIR=rocm from the preset, so HIP goes to lib/ollama/rocm/ +# Need CMAKE_PREFIX_PATH so find_package(hip) finds hip-config.cmake +# at /opt/rocm/lib/cmake/hip/hip-config.cmake. RUN mkdir -p build-hip && cd build-hip && \ - CC=hipcc CXX=hipcc cmake .. \ + cmake .. \ + --preset 'ROCm 6' \ -DAMDGPU_TARGETS="gfx906:xnack-" \ - -DCMAKE_BUILD_TYPE=Release && \ - CC=hipcc CXX=hipcc cmake --build . --target ggml-hip -- -l $(nproc) && \ - cmake --install . --component HIP --strip --prefix /build/dist + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_PREFIX_PATH="/opt/rocm" && \ + cmake --build . --target ggml-hip -- -l $(nproc) && \ + cmake --install . --component HIP --strip --prefix /build/dist && \ + echo "=== HIP install ===" && \ + find /build/dist/lib/ollama -type f -o -type l | head -20 -# Step 3: Build Go binary (uses GCC for CGo linking) +# Step 3: Build Go binary (GCC for CGo linking) ENV CGO_ENABLED=1 RUN go build -trimpath -o /build/dist/ollama . @@ -68,6 +80,7 @@ RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \ && rm -rf /var/lib/apt/lists/* # Copy ROCm 6.1 runtime libraries +# These are needed at runtime by ggml-hip via LD_LIBRARY_PATH COPY --from=builder /opt/rocm/lib/ /opt/rocm/lib/ COPY --from=builder /opt/rocm/share/ /opt/rocm/share/ @@ -77,7 +90,7 @@ COPY --from=builder /build/dist/lib/ /usr/lib/ollama/ RUN ldconfig -ENV LD_LIBRARY_PATH=/opt/rocm/lib:/usr/lib/ollama +ENV LD_LIBRARY_PATH=/opt/rocm/lib:/usr/lib/ollama/rocm:/usr/lib/ollama ENV HSA_OVERRIDE_GFX_VERSION=9.0.6 ENV HCC_AMDGPU_TARGET=gfx906 ENV HSA_ENABLE_SDMA=0 From 208bfd4612788c35dbbc8dae58d1183cded8d619 Mon Sep 17 00:00:00 2001 From: Hermes Date: Sat, 9 May 2026 23:50:26 -0400 Subject: [PATCH 52/71] fix: pre-set CMAKE_HIP_COMPILER="" for CPU build to prevent HIP detection --- ai/ollama/Dockerfile | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/ai/ollama/Dockerfile b/ai/ollama/Dockerfile index 65ffe9b..22370d2 100644 --- a/ai/ollama/Dockerfile +++ b/ai/ollama/Dockerfile @@ -42,12 +42,13 @@ ENV CMAKE_GENERATOR=Ninja ENV LDFLAGS=-s # Step 1: Build CPU backends with GCC (no ROCm preset) -# Remove /opt/rocm from PATH to prevent check_language(HIP) from -# finding a HIP compiler, which would trigger the HIP block that -# requires find_package(hip) with proper CMAKE_PREFIX_PATH. +# Pre-set CMAKE_HIP_COMPILER="" to prevent check_language(HIP) from +# finding a HIP compiler (it searches /opt/rocm even without PATH). +# Remove /opt/rocm from PATH to prevent find_program from finding hipcc. RUN mkdir -p build-cpu && cd build-cpu && \ PATH=/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \ - cmake .. -DCMAKE_BUILD_TYPE=Release && \ + cmake .. -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_HIP_COMPILER="" && \ cmake --build . --target ggml-cpu -- -l $(nproc) && \ cmake --install . --component CPU --strip --prefix /build/dist From bf2f17c5e28a9871ea95fddad650d4ec27f96692 Mon Sep 17 00:00:00 2001 From: Hermes Date: Sat, 9 May 2026 23:52:46 -0400 Subject: [PATCH 53/71] fix: use cmake -B to override preset binaryDir, cmake --build/--install use explicit path --- ai/ollama/Dockerfile | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ai/ollama/Dockerfile b/ai/ollama/Dockerfile index 22370d2..6b92064 100644 --- a/ai/ollama/Dockerfile +++ b/ai/ollama/Dockerfile @@ -45,12 +45,12 @@ ENV LDFLAGS=-s # Pre-set CMAKE_HIP_COMPILER="" to prevent check_language(HIP) from # finding a HIP compiler (it searches /opt/rocm even without PATH). # Remove /opt/rocm from PATH to prevent find_program from finding hipcc. -RUN mkdir -p build-cpu && cd build-cpu && \ +RUN mkdir -p build-cpu && \ PATH=/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \ - cmake .. -DCMAKE_BUILD_TYPE=Release \ + cmake -B build-cpu -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_HIP_COMPILER="" && \ - cmake --build . --target ggml-cpu -- -l $(nproc) && \ - cmake --install . --component CPU --strip --prefix /build/dist + cmake --build build-cpu --target ggml-cpu -- -l $(nproc) && \ + cmake --install build-cpu --component CPU --strip --prefix /build/dist # Step 2: Build HIP backend with ROCm preset + gfx906 target only # The ROCm 6 preset enables HIP language detection (enable_language(HIP)) @@ -58,14 +58,14 @@ RUN mkdir -p build-cpu && cd build-cpu && \ # OLLAMA_RUNNER_DIR=rocm from the preset, so HIP goes to lib/ollama/rocm/ # Need CMAKE_PREFIX_PATH so find_package(hip) finds hip-config.cmake # at /opt/rocm/lib/cmake/hip/hip-config.cmake. -RUN mkdir -p build-hip && cd build-hip && \ - cmake .. \ +RUN mkdir -p build-hip && \ + cmake -B build-hip \ --preset 'ROCm 6' \ -DAMDGPU_TARGETS="gfx906:xnack-" \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_PREFIX_PATH="/opt/rocm" && \ - cmake --build . --target ggml-hip -- -l $(nproc) && \ - cmake --install . --component HIP --strip --prefix /build/dist && \ + cmake --build build-hip --target ggml-hip -- -l $(nproc) && \ + cmake --install build-hip --component HIP --strip --prefix /build/dist && \ echo "=== HIP install ===" && \ find /build/dist/lib/ollama -type f -o -type l | head -20 From f31ae5971744f08067c653cfcc8ffd5244171e14 Mon Sep 17 00:00:00 2001 From: Hermes Date: Sun, 10 May 2026 00:10:39 -0400 Subject: [PATCH 54/71] fix: copy /build/dist/lib/ollama/ (not /build/dist/lib/) to avoid extra nesting --- ai/ollama/Dockerfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ai/ollama/Dockerfile b/ai/ollama/Dockerfile index 6b92064..2681c46 100644 --- a/ai/ollama/Dockerfile +++ b/ai/ollama/Dockerfile @@ -86,8 +86,10 @@ COPY --from=builder /opt/rocm/lib/ /opt/rocm/lib/ COPY --from=builder /opt/rocm/share/ /opt/rocm/share/ # Copy ollama binary + all backends (CPU + HIP) +# CPU install: /build/dist/lib/ollama/libggml-*.so +# HIP install: /build/dist/lib/ollama/rocm/libggml-hip.so COPY --from=builder /build/dist/ollama /usr/bin/ollama -COPY --from=builder /build/dist/lib/ /usr/lib/ollama/ +COPY --from=builder /build/dist/lib/ollama/ /usr/lib/ollama/ RUN ldconfig From 9cc7edfb3993c11a0d92899e820ae02bb70d57f5 Mon Sep 17 00:00:00 2001 From: Hermes Date: Sun, 10 May 2026 00:44:56 -0400 Subject: [PATCH 55/71] fix: set CMAKE_INSTALL_PREFIX=/build/dist at configure time for CPU, match preset for HIP --- ai/ollama/Dockerfile | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/ai/ollama/Dockerfile b/ai/ollama/Dockerfile index 2681c46..f41ab5d 100644 --- a/ai/ollama/Dockerfile +++ b/ai/ollama/Dockerfile @@ -48,9 +48,12 @@ ENV LDFLAGS=-s RUN mkdir -p build-cpu && \ PATH=/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \ cmake -B build-cpu -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_HIP_COMPILER="" && \ + -DCMAKE_HIP_COMPILER="" \ + -DCMAKE_INSTALL_PREFIX=/build/dist && \ cmake --build build-cpu --target ggml-cpu -- -l $(nproc) && \ - cmake --install build-cpu --component CPU --strip --prefix /build/dist + cmake --install build-cpu --component CPU --strip && \ + echo "=== CPU install ===" && \ + (find /build/dist/lib/ollama -type f -o -type l 2>&1 | head -20 || echo "empty") # Step 2: Build HIP backend with ROCm preset + gfx906 target only # The ROCm 6 preset enables HIP language detection (enable_language(HIP)) @@ -65,7 +68,7 @@ RUN mkdir -p build-hip && \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_PREFIX_PATH="/opt/rocm" && \ cmake --build build-hip --target ggml-hip -- -l $(nproc) && \ - cmake --install build-hip --component HIP --strip --prefix /build/dist && \ + cmake --install build-hip --component HIP --strip && \ echo "=== HIP install ===" && \ find /build/dist/lib/ollama -type f -o -type l | head -20 From 6b82a26c25f1592a2d1c9bea4f941864362fe001 Mon Sep 17 00:00:00 2001 From: Hermes Date: Sun, 10 May 2026 10:07:25 -0400 Subject: [PATCH 56/71] fix: add ldflags for version, remove privileged, enable flash attention --- ai/compose.yml | 3 +-- ai/ollama/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/ai/compose.yml b/ai/compose.yml index dceb490..2e565ae 100644 --- a/ai/compose.yml +++ b/ai/compose.yml @@ -63,7 +63,6 @@ services: dockerfile: Dockerfile image: ollama/ollama:rocm-gfx906 container_name: ollama - privileged: true tty: true restart: always ports: @@ -81,7 +80,7 @@ services: - HSA_ENABLE_SDMA=0 - OLLAMA_HOST=0.0.0.0 - OLLAMA_DEBUG=1 - - OLLAMA_FLASH_ATTENTION=0 + - OLLAMA_FLASH_ATTENTION=1 - OLLAMA_NUM_PARALLEL=2 devices: # Map the render nodes and KFD for ROCm to work inside the container diff --git a/ai/ollama/Dockerfile b/ai/ollama/Dockerfile index f41ab5d..438e607 100644 --- a/ai/ollama/Dockerfile +++ b/ai/ollama/Dockerfile @@ -74,7 +74,7 @@ RUN mkdir -p build-hip && \ # Step 3: Build Go binary (GCC for CGo linking) ENV CGO_ENABLED=1 -RUN go build -trimpath -o /build/dist/ollama . +RUN go build -trimpath -ldflags="-X=github.com/ollama/ollama/version.Version=${OLLAMA_VERSION}" -o /build/dist/ollama . # ---------- Runtime image ---------- FROM ubuntu:24.04 From 46241d10c590a67f84c95dd27185a24beeb22f27 Mon Sep 17 00:00:00 2001 From: Hermes Date: Sun, 10 May 2026 17:55:17 -0400 Subject: [PATCH 57/71] feat: update Hermes Dockerfile to build from forked source - Switch Dockerfile to clone from gortium/hermes-agent (Gitea fork) - Add SSH agent forwarding for private repo clone at build time - Set CHROME_EXECUTABLE for Playwright Chromium - Remove patch_tts_tool.py (Piper patch now in fork source) - Enable Gitea Actions in versioncontrol compose --- ai/compose.yml | 5 +- ai/hermes/Dockerfile | 95 +++++++++++++++++++------------------- versioncontrol/compose.yml | 4 ++ 3 files changed, 56 insertions(+), 48 deletions(-) diff --git a/ai/compose.yml b/ai/compose.yml index 2e565ae..fe84ccb 100644 --- a/ai/compose.yml +++ b/ai/compose.yml @@ -26,7 +26,10 @@ services: # - "traefik.http.routers.webui-https.tls.certresolver=njalla" hermes: - build: ./hermes + build: + context: ./hermes + ssh: + - default container_name: hermes restart: always # Gateway run enables the internal API server on port 8642 diff --git a/ai/hermes/Dockerfile b/ai/hermes/Dockerfile index 6c8ddeb..042e1db 100644 --- a/ai/hermes/Dockerfile +++ b/ai/hermes/Dockerfile @@ -1,50 +1,59 @@ -# 1. On récupère la version la plus récente d'UV -FROM ghcr.io/astral-sh/uv:latest AS uv_source +# syntax=docker/dockerfile:1 +# Hermes Agent -- custom fork build +# Builds on top of official image + overlays our forked source from Gitea. +# Requires Docker BuildKit. Pass SSH agent for git clone: +# docker compose build hermes +# Or manually: +# DOCKER_BUILDKIT=1 docker build --ssh default -t hermes-agent:custom . -# 2. Image officielle Hermes Agent de NousResearch -# Contient déjà: Python, Node.js, npm, Playwright/Chromium, venv, tts_tool.py, etc. +# ---------- Base: official Hermes image (system deps, npm, uv, Playwright) ---------- FROM nousresearch/hermes-agent:latest -# ---------- System dependencies ---------- -# 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) +# ---------- Overlay our forked source ---------- +# Uses SSH agent forwarding from the build host (no key baked into image). +# --exclude node_modules/.venv keeps the base image's pre-built layers intact. +# Only the Python source, web UI source, and config change. +RUN --mount=type=ssh \ + mkdir -p /root/.ssh && \ + ssh-keyscan -p 2222 code.lazyworkhorse.net >> /root/.ssh/known_hosts 2>/dev/null && \ + cd /tmp && \ + GIT_SSH_COMMAND='ssh -p 2222 -o StrictHostKeyChecking=no' \ + git clone --depth 1 --branch main \ + git@code.lazyworkhorse.net:gortium/hermes-agent.git fork && \ + rsync -a --delete fork/ /opt/hermes/ \ + --exclude node_modules \ + --exclude .venv \ + --exclude .git && \ + rm -rf /tmp/fork /root/.ssh/ + +# ---------- Rebuild web UI ---------- +# Source files changed; node_modules (from base image) reused. +RUN cd /opt/hermes && npm run build + +# ---------- Reinstall Python package (editable) ---------- +# Picks up source changes from our fork. +RUN . /opt/hermes/.venv/bin/activate && \ + uv pip install --no-cache-dir --no-deps -e /opt/hermes + +# ---------- Extra system deps ---------- USER root RUN apt-get update && \ apt-get install -y --no-install-recommends \ - libportaudio2 \ - 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 && \ + libportaudio2 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) ---------- -COPY --chmod=0755 --from=uv_source /uv /usr/local/bin/ +# ---------- UV ---------- +COPY --chmod=0755 --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/ -WORKDIR /opt/hermes - -# ---------- Piper TTS dans le venv existant ---------- -# Le venv de l'image de base est root-owned, on doit installer en root aussi +# ---------- Piper TTS ---------- RUN . /opt/hermes/.venv/bin/activate && \ - uv pip install --no-cache-dir piper-tts sounddevice numpy + uv pip install --no-cache-dir piper-tts sounddevice numpy && \ + mkdir -p /opt/hermes/.venv/share/piper/voices -# ---------- 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' +RUN /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' @@ -52,22 +61,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: 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 - # ---------- Runtime ---------- -# Retour à l'utilisateur non-root pour la sécurité USER hermes - ENV HERMES_HOME=/opt/data ENV PATH="/opt/data/.local/bin:${PATH}" +# Point browser tool to Playwright's Chromium (already in base image) +ENV CHROME_EXECUTABLE=/opt/hermes/.playwright/chromium/chrome-linux/chrome VOLUME [ "/opt/data" ] -# Script de réparation des permissions + patch TTS au démarrage COPY --chmod=0755 fix-permissions.sh /opt/hermes/fix-permissions.sh - -ENTRYPOINT [ "/usr/bin/tini", "-g", "--", "/opt/hermes/fix-permissions.sh" ] +ENTRYPOINT [ "/usr/bin/tini", "-g", "--", "/opt/hermes/fix-permissions.sh" ] \ No newline at end of file diff --git a/versioncontrol/compose.yml b/versioncontrol/compose.yml index 7e7b54e..c4d7d67 100644 --- a/versioncontrol/compose.yml +++ b/versioncontrol/compose.yml @@ -9,6 +9,10 @@ services: - GITEA__server__ROOT_URL=https://code.lazyworkhorse.net - SSH_PORT=2222 - SSH_LISTEN_PORT=2222 + # Enable Gitea Actions (act_runner required on host) + - GITEA__actions__ENABLED=true + # Don't fetch actions from GitHub (offline mode + local only) + - GITEA__actions__DEFAULT_ACTIONS_URL=off volumes: - /mnt/HoardingCow_docker_data/Gitea:/data networks: From 34b1cb83a0d91dabe65bd384c551e416bdc65ddc Mon Sep 17 00:00:00 2001 From: Hermes Date: Sun, 10 May 2026 21:33:10 -0400 Subject: [PATCH 58/71] feat: add Gitea Actions workflows for ollama and hermes Docker builds --- .gitea/workflows/build-hermes.yml | 31 +++++++++++++++++++++++++++++++ .gitea/workflows/build-ollama.yml | 31 +++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 .gitea/workflows/build-hermes.yml create mode 100644 .gitea/workflows/build-ollama.yml diff --git a/.gitea/workflows/build-hermes.yml b/.gitea/workflows/build-hermes.yml new file mode 100644 index 0000000..8684f16 --- /dev/null +++ b/.gitea/workflows/build-hermes.yml @@ -0,0 +1,31 @@ +name: Build Hermes agent +on: + pull_request: + branches: [ master ] + paths: + - 'ai/hermes/**' + - 'ai/compose.yml' + push: + branches: [ master ] + paths: + - 'ai/hermes/**' + - 'ai/compose.yml' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + run: | + git clone -b "${{ github.head_ref || github.ref_name }}" \ + https://gitea:${{ secrets.GITHUB_TOKEN }}@code.lazyworkhorse.net/gortium/compose.git . + git log --oneline -3 + + - name: Build hermes image + run: | + cd ai + docker compose build hermes 2>&1 + + - name: Verify image + run: | + docker run --rm ai-hermes /opt/hermes/.venv/bin/python --version 2>&1 diff --git a/.gitea/workflows/build-ollama.yml b/.gitea/workflows/build-ollama.yml new file mode 100644 index 0000000..69b622a --- /dev/null +++ b/.gitea/workflows/build-ollama.yml @@ -0,0 +1,31 @@ +name: Build ollama (gfx906) +on: + pull_request: + branches: [ master ] + paths: + - 'ai/ollama/**' + - 'ai/compose.yml' + push: + branches: [ master ] + paths: + - 'ai/ollama/**' + - 'ai/compose.yml' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + run: | + git clone -b "${{ github.head_ref || github.ref_name }}" \ + https://gitea:${{ secrets.GITHUB_TOKEN }}@code.lazyworkhorse.net/gortium/compose.git . + git log --oneline -3 + + - name: Build ollama image + run: | + cd ai + docker compose build ollama --no-cache 2>&1 + + - name: Verify version + run: | + docker run --rm ollama/ollama:rocm-gfx906 ollama --version 2>&1 From 144678354ff7edf0f665eaf7ab002a6c09aa0d39 Mon Sep 17 00:00:00 2001 From: Hermes Date: Sun, 10 May 2026 21:38:19 -0400 Subject: [PATCH 59/71] feat: add Gitea Actions runner and enable Actions --- versioncontrol/compose.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/versioncontrol/compose.yml b/versioncontrol/compose.yml index 7e7b54e..ac30373 100644 --- a/versioncontrol/compose.yml +++ b/versioncontrol/compose.yml @@ -7,6 +7,8 @@ services: - USER_UID=1000 - USER_GID=1000 - GITEA__server__ROOT_URL=https://code.lazyworkhorse.net + - GITEA__actions__ENABLED=true + - GITEA__actions__DEFAULT_ACTIONS_URL=off - SSH_PORT=2222 - SSH_LISTEN_PORT=2222 volumes: @@ -40,6 +42,22 @@ services: # Internal Routing - "traefik.http.services.gitea.loadbalancer.server.port=3000" + act_runner: + image: gitea/act_runner:latest + container_name: act_runner + environment: + - GITEA_INSTANCE_URL=https://code.lazyworkhorse.net + - GITEA_RUNNER_REGISTRATION_TOKEN=${GITEA_RUNNER_TOKEN} + - GITEA_RUNNER_NAME=ai-host-runner + - GITEA_RUNNER_LABELS=ubuntu-latest:docker://catthehacker/ubuntu:full-22.04 + volumes: + - /var/run/docker.sock:/var/run/docker.sock + networks: + - vc_net + restart: always + depends_on: + - gitea + networks: vc_net: external: true From b517a653931d229644b26d3e7da2b902dba379c4 Mon Sep 17 00:00:00 2001 From: Hermes Date: Sun, 10 May 2026 22:15:44 -0400 Subject: [PATCH 60/71] fix: add nixos-builder label for NixOS CI builds --- versioncontrol/compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/versioncontrol/compose.yml b/versioncontrol/compose.yml index ac30373..39c2e13 100644 --- a/versioncontrol/compose.yml +++ b/versioncontrol/compose.yml @@ -49,7 +49,7 @@ services: - GITEA_INSTANCE_URL=https://code.lazyworkhorse.net - GITEA_RUNNER_REGISTRATION_TOKEN=${GITEA_RUNNER_TOKEN} - GITEA_RUNNER_NAME=ai-host-runner - - GITEA_RUNNER_LABELS=ubuntu-latest:docker://catthehacker/ubuntu:full-22.04 + - GITEA_RUNNER_LABELS=ubuntu-latest:docker://catthehacker/ubuntu:full-22.04,nixos-builder:docker://nixos/nix volumes: - /var/run/docker.sock:/var/run/docker.sock networks: From 46271992175e1dfdf6aa484e16fcafe073ccf5dc Mon Sep 17 00:00:00 2001 From: Hermes Date: Tue, 12 May 2026 13:38:26 -0400 Subject: [PATCH 61/71] feat: install custom tools at startup, remove deprecated fix-permissions.sh --- ai/compose.yml | 4 ++++ ai/hermes/Dockerfile | 5 +---- ai/hermes/fix-permissions.sh | 38 ------------------------------------ 3 files changed, 5 insertions(+), 42 deletions(-) delete mode 100644 ai/hermes/fix-permissions.sh diff --git a/ai/compose.yml b/ai/compose.yml index fe84ccb..163196a 100644 --- a/ai/compose.yml +++ b/ai/compose.yml @@ -31,6 +31,10 @@ services: ssh: - default container_name: hermes + user: root + entrypoint: ["/bin/bash", "-c", + "bash /opt/data/hermes-tools/install.sh && exec /usr/bin/tini -g -- /opt/hermes/docker/entrypoint.sh \"$@\"", + "hermes-entrypoint"] restart: always # Gateway run enables the internal API server on port 8642 command: gateway run diff --git a/ai/hermes/Dockerfile b/ai/hermes/Dockerfile index 042e1db..263a24b 100644 --- a/ai/hermes/Dockerfile +++ b/ai/hermes/Dockerfile @@ -68,7 +68,4 @@ ENV PATH="/opt/data/.local/bin:${PATH}" # Point browser tool to Playwright's Chromium (already in base image) ENV CHROME_EXECUTABLE=/opt/hermes/.playwright/chromium/chrome-linux/chrome -VOLUME [ "/opt/data" ] - -COPY --chmod=0755 fix-permissions.sh /opt/hermes/fix-permissions.sh -ENTRYPOINT [ "/usr/bin/tini", "-g", "--", "/opt/hermes/fix-permissions.sh" ] \ No newline at end of file +VOLUME [ "/opt/data" ] \ No newline at end of file diff --git a/ai/hermes/fix-permissions.sh b/ai/hermes/fix-permissions.sh deleted file mode 100644 index 7af8d0c..0000000 --- a/ai/hermes/fix-permissions.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash -# Startup permission fix + TTS patch. -# Runs as root before the entrypoint drops to the hermes user. -set -e - -HERMES_HOME="${HERMES_HOME:-/opt/data}" - -# Fix ownership on critical writable directories -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 - -# 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 ---------- -# 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 - -# Chain to the official Hermes entrypoint -exec /opt/hermes/docker/entrypoint.sh "$@" From e607982b21cbd6cdb1a54b37abe3b56d79dfabcc Mon Sep 17 00:00:00 2001 From: Hermes Date: Tue, 12 May 2026 14:47:34 -0400 Subject: [PATCH 62/71] refactor: chown tools dir at build time instead of root at runtime --- ai/compose.yml | 1 - ai/hermes/Dockerfile | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/ai/compose.yml b/ai/compose.yml index 163196a..aca3347 100644 --- a/ai/compose.yml +++ b/ai/compose.yml @@ -31,7 +31,6 @@ services: ssh: - default container_name: hermes - user: root entrypoint: ["/bin/bash", "-c", "bash /opt/data/hermes-tools/install.sh && exec /usr/bin/tini -g -- /opt/hermes/docker/entrypoint.sh \"$@\"", "hermes-entrypoint"] diff --git a/ai/hermes/Dockerfile b/ai/hermes/Dockerfile index 263a24b..1debe81 100644 --- a/ai/hermes/Dockerfile +++ b/ai/hermes/Dockerfile @@ -68,4 +68,8 @@ ENV PATH="/opt/data/.local/bin:${PATH}" # Point browser tool to Playwright's Chromium (already in base image) ENV CHROME_EXECUTABLE=/opt/hermes/.playwright/chromium/chrome-linux/chrome +# Ensure tools directory and toolsets.py are writable by the hermes runtime user +# so custom tools can be injected from the persistent volume at startup. +RUN chown -R hermes:hermes /opt/hermes/tools /opt/hermes/toolsets.py + VOLUME [ "/opt/data" ] \ No newline at end of file From 5e242eb94638e5e10aa5491d5094ae381b3cd321 Mon Sep 17 00:00:00 2001 From: Hermes Date: Tue, 12 May 2026 14:52:33 -0400 Subject: [PATCH 63/71] fix: add iptables-nft to wg-easy for nftables-only kernels wg-easy's Alpine wg-quick uses legacy iptables which requires the iptable_nat kernel module. On NixOS kernels compiled without legacy netfilter modules, the container crashes in a restart loop: iptables v1.8.3 (legacy): can't initialize iptables table 'nat' Table does not exist (do you need to insmod?) Fix: build a custom image that installs Alpine's iptables-nft package and symlinks iptables -> iptables-nft (nftables backend). --- vpn/Dockerfile | 16 ++++++++++++++++ vpn/compose.yml | 5 ++++- 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 vpn/Dockerfile diff --git a/vpn/Dockerfile b/vpn/Dockerfile new file mode 100644 index 0000000..931d3bf --- /dev/null +++ b/vpn/Dockerfile @@ -0,0 +1,16 @@ +# Custom wg-easy with iptables-nft (nftables-backed iptables) +# Fixes crash-loop when host kernel lacks legacy iptable_nat module. +FROM weejewel/wg-easy:latest + +# Alpine's iptables-nft provides iptables that uses nftables kernel API +# instead of the legacy iptable_nat module. This works on kernels +# where only nftables netfilter modules are available. +RUN apk add --no-cache iptables-nft + +# Ensure iptables-nft takes priority over legacy iptables +RUN ln -sf /sbin/iptables-nft /sbin/iptables && \ + ln -sf /sbin/iptables-nft-save /sbin/iptables-save && \ + ln -sf /sbin/iptables-nft-restore /sbin/iptables-restore && \ + ln -sf /sbin/ip6tables-nft /sbin/ip6tables && \ + ln -sf /sbin/ip6tables-nft-save /sbin/ip6tables-save && \ + ln -sf /sbin/ip6tables-nft-restore /sbin/ip6tables-restore diff --git a/vpn/compose.yml b/vpn/compose.yml index ceb4f35..22ca77a 100644 --- a/vpn/compose.yml +++ b/vpn/compose.yml @@ -2,7 +2,10 @@ version: "3.8" services: wireguard: - image: weejewel/wg-easy:latest + build: + context: ./vpn + dockerfile: Dockerfile + image: wg-easy-iptables-nft:latest container_name: wireguard cap_add: - NET_ADMIN From 27571ddb3fde7e49d68afc41c028411ebc2e51a9 Mon Sep 17 00:00:00 2001 From: Hermes Date: Tue, 12 May 2026 18:02:51 -0400 Subject: [PATCH 64/71] feat: add Himalaya email CLI to Hermes Docker image --- ai/hermes/Dockerfile | 18 ++++++++++ ai/hermes/himalaya-ro.sh | 73 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 ai/hermes/himalaya-ro.sh diff --git a/ai/hermes/Dockerfile b/ai/hermes/Dockerfile index 042e1db..238b3bc 100644 --- a/ai/hermes/Dockerfile +++ b/ai/hermes/Dockerfile @@ -61,6 +61,24 @@ urllib.request.urlretrieve(url, base + '/en_US-ryan-high.onnx') urllib.request.urlretrieve(url + '.json', base + '/en_US-ryan-high.onnx.json') PYEOF +# ---------- Install Himalaya email CLI ---------- +RUN /opt/hermes/.venv/bin/python3 /dev/stdin << 'PYEOF' +import urllib.request, tarfile, os, shutil +url = 'https://github.com/pimalaya/himalaya/releases/download/v1.2.0/himalaya.x86_64-linux.tgz' +tgz = '/tmp/himalaya.tgz' +urllib.request.urlretrieve(url, tgz) +with tarfile.open(tgz) as t: + t.extractall('/tmp') +shutil.move('/tmp/himalaya', '/usr/local/bin/himalaya') +os.chmod('/usr/local/bin/himalaya', 0o755) +os.remove(tgz) +print('himalaya v1.2.0 installed') +PYEOF + +# ---------- Install himalaya-ro wrapper ---------- +COPY --chmod=0755 himalaya-ro.sh /usr/local/bin/himalaya-ro + + # ---------- Runtime ---------- USER hermes ENV HERMES_HOME=/opt/data diff --git a/ai/hermes/himalaya-ro.sh b/ai/hermes/himalaya-ro.sh new file mode 100644 index 0000000..212f1ae --- /dev/null +++ b/ai/hermes/himalaya-ro.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# ───────────────────────────────────────────────────────────── +# himalaya-ro — Read-only wrapper for himalaya +# +# Blocks destructive commands and logs audit trail. +# Pass-through for read-only commands (list, read, search). +# +# Usage: himalaya-ro [options] [args...] +# +# Install: place in PATH before the real himalaya, or use +# `ln -sf himalaya-ro /usr/local/bin/himalaya` +# ───────────────────────────────────────────────────────────── +set -o pipefail + +# ── Configuration ─────────────────────────────────────────── +HIMALAYA_BIN="${HIMALAYA_BIN:-/usr/local/bin/himalaya}" +AUDIT_LOG="${HIMALAYA_AUDIT_LOG:-/var/log/himalaya-audit.log}" + +# ── Destructive commands we block ────────────────────────── +BLOCKED_CMDS=( + "message move" + "message delete" + "message copy" + "flag add" + "flag remove" + "folder create" + "folder delete" + "folder rename" + "template send" + "account configure" + "account delete" +) + +# ── Determine the subcommand being invoked ───────────────── +# Strip leading options (--account, --output, etc.) to find the verb +ARGS=() +SKIP_NEXT=false +for arg in "$@"; do + if $SKIP_NEXT; then + SKIP_NEXT=false + continue + fi + if [[ "$arg" == --* ]]; then + case "$arg" in + --account|--output|--page|--page-size|--folder|--color|--format) + SKIP_NEXT=true ;; + esac + continue + fi + ARGS+=("$arg") +done + +# Build subcommand string and check against blocklist +CMD_STR="" +for ((i=0; i<${#ARGS[@]}; i++)); do + if [ -z "$CMD_STR" ]; then + CMD_STR="${ARGS[$i]}" + else + CMD_STR="$CMD_STR ${ARGS[$i]}" + fi + for blocked in "${BLOCKED_CMDS[@]}"; do + if [[ "$CMD_STR" == "$blocked" ]]; then + TS=$(date '+%Y-%m-%d %H:%M:%S') + echo "[AUDIT] $TS BLOCKED: himalaya $*" >> "$AUDIT_LOG" + echo "ERROR: Command 'himalaya $CMD_STR ...' is blocked by read-only policy." >&2 + echo " Audit log: $AUDIT_LOG" >&2 + exit 100 + fi + done +done + +# ── Allow pass-through ───────────────────────────────────── +exec "$HIMALAYA_BIN" "$@" From 2bf31c7ccc8fc1f9f87555a750a0551bfac83221 Mon Sep 17 00:00:00 2001 From: Hermes Date: Wed, 13 May 2026 12:30:15 -0400 Subject: [PATCH 65/71] fix: update wg-easy to official ghcr image with iptables-nft - Switch FROM weejewel/wg-easy:latest (4yr old, Alpine 3.11) to ghcr.io/wg-easy/wg-easy:latest (actively maintained, Alpine krypton) - Use update-alternatives instead of raw ln -sf to flip iptables from legacy to nftables backend - Fix compose build context: ./vpn -> . (Dockerfile was at same level) The weejewel/wg-easy image lacked iptables-nft package in Alpine 3.11. The new official image has it available, we just flip the alternatives. The old ln -sf approach was fragile across Alpine versions. --- vpn/Dockerfile | 20 +++++++------------- vpn/compose.yml | 2 +- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/vpn/Dockerfile b/vpn/Dockerfile index 931d3bf..ea6f370 100644 --- a/vpn/Dockerfile +++ b/vpn/Dockerfile @@ -1,16 +1,10 @@ # Custom wg-easy with iptables-nft (nftables-backed iptables) # Fixes crash-loop when host kernel lacks legacy iptable_nat module. -FROM weejewel/wg-easy:latest +FROM ghcr.io/wg-easy/wg-easy:latest -# Alpine's iptables-nft provides iptables that uses nftables kernel API -# instead of the legacy iptable_nat module. This works on kernels -# where only nftables netfilter modules are available. -RUN apk add --no-cache iptables-nft - -# Ensure iptables-nft takes priority over legacy iptables -RUN ln -sf /sbin/iptables-nft /sbin/iptables && \ - ln -sf /sbin/iptables-nft-save /sbin/iptables-save && \ - ln -sf /sbin/iptables-nft-restore /sbin/iptables-restore && \ - ln -sf /sbin/ip6tables-nft /sbin/ip6tables && \ - ln -sf /sbin/ip6tables-nft-save /sbin/ip6tables-save && \ - ln -sf /sbin/ip6tables-nft-restore /sbin/ip6tables-restore +# The upstream image defaults to iptables-legacy via update-alternatives. +# Switch to iptables-nft so it works on kernels where only nftables +# netfilter modules are available (iptable_nat module missing). +RUN apk add --no-cache iptables-nft && \ + update-alternatives --set iptables /usr/sbin/iptables-nft && \ + update-alternatives --set ip6tables /usr/sbin/ip6tables-nft diff --git a/vpn/compose.yml b/vpn/compose.yml index 22ca77a..cd14f27 100644 --- a/vpn/compose.yml +++ b/vpn/compose.yml @@ -3,7 +3,7 @@ version: "3.8" services: wireguard: build: - context: ./vpn + context: . dockerfile: Dockerfile image: wg-easy-iptables-nft:latest container_name: wireguard From 611e96b3065602234e379a4070cb60987604a2c3 Mon Sep 17 00:00:00 2001 From: Hermes Date: Wed, 13 May 2026 12:48:51 -0400 Subject: [PATCH 66/71] =?UTF-8?q?fix:=20remove=20apk=20add=20iptables-nft?= =?UTF-8?q?=20=E2=80=94=20built-in=20on=20Alpine=203.18+?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In Alpine 3.18+, the 'iptables' package IS the nftables variant. iptables-nft is not a separate package. The binary is already in the base image — only need to flip update-alternatives. --- vpn/Dockerfile | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/vpn/Dockerfile b/vpn/Dockerfile index ea6f370..08143b7 100644 --- a/vpn/Dockerfile +++ b/vpn/Dockerfile @@ -3,8 +3,7 @@ FROM ghcr.io/wg-easy/wg-easy:latest # The upstream image defaults to iptables-legacy via update-alternatives. -# Switch to iptables-nft so it works on kernels where only nftables -# netfilter modules are available (iptable_nat module missing). -RUN apk add --no-cache iptables-nft && \ - update-alternatives --set iptables /usr/sbin/iptables-nft && \ +# Switch iptables to the nftables backend (already provided by the 'iptables' +# package on Alpine 3.18+). No apk add needed — iptables-nft is built-in. +RUN update-alternatives --set iptables /usr/sbin/iptables-nft && \ update-alternatives --set ip6tables /usr/sbin/ip6tables-nft From 8dff0947686f450f1aa243b6f5da35350a6ef3f7 Mon Sep 17 00:00:00 2001 From: Hermes Date: Wed, 13 May 2026 12:58:43 -0400 Subject: [PATCH 67/71] fix: use ln -sf instead of update-alternatives --set update-alternatives --set fails because the base image only registers iptables-legacy as an alternative. The iptables-nft binary (/usr/sbin/iptables-nft) exists but isn't in the alternatives database. Direct ln -sf bypasses this. --- vpn/Dockerfile | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/vpn/Dockerfile b/vpn/Dockerfile index 08143b7..f0c29c9 100644 --- a/vpn/Dockerfile +++ b/vpn/Dockerfile @@ -2,8 +2,8 @@ # Fixes crash-loop when host kernel lacks legacy iptable_nat module. FROM ghcr.io/wg-easy/wg-easy:latest -# The upstream image defaults to iptables-legacy via update-alternatives. -# Switch iptables to the nftables backend (already provided by the 'iptables' -# package on Alpine 3.18+). No apk add needed — iptables-nft is built-in. -RUN update-alternatives --set iptables /usr/sbin/iptables-nft && \ - update-alternatives --set ip6tables /usr/sbin/ip6tables-nft +# The upstream image registers only iptables-legacy with update-alternatives. +# iptables-nft binary exists but isn't registered as an alternative key. +# Override the alternatives-managed symlinks directly. +RUN ln -sf /usr/sbin/iptables-nft /usr/sbin/iptables && \ + ln -sf /usr/sbin/ip6tables-nft /usr/sbin/ip6tables From 8d1ae7e632887a6ddead9176f8a236cde39f3a30 Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Wed, 13 May 2026 13:11:11 -0400 Subject: [PATCH 68/71] Remove the unsuported gitea action off --- versioncontrol/compose.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/versioncontrol/compose.yml b/versioncontrol/compose.yml index 6887f69..01007ff 100644 --- a/versioncontrol/compose.yml +++ b/versioncontrol/compose.yml @@ -8,13 +8,10 @@ services: - USER_GID=1000 - GITEA__server__ROOT_URL=https://code.lazyworkhorse.net - GITEA__actions__ENABLED=true - - GITEA__actions__DEFAULT_ACTIONS_URL=off - SSH_PORT=2222 - SSH_LISTEN_PORT=2222 # Enable Gitea Actions (act_runner required on host) - GITEA__actions__ENABLED=true - # Don't fetch actions from GitHub (offline mode + local only) - - GITEA__actions__DEFAULT_ACTIONS_URL=off volumes: - /mnt/HoardingCow_docker_data/Gitea:/data networks: From bcc4b6d157b48d531eae7b6a0d6c6b4dce932df0 Mon Sep 17 00:00:00 2001 From: Hermes Date: Thu, 14 May 2026 21:35:31 -0400 Subject: [PATCH 69/71] feat: add Syncthing service for Hermes org-file sync --- ai/compose.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/ai/compose.yml b/ai/compose.yml index aca3347..3bf9070 100644 --- a/ai/compose.yml +++ b/ai/compose.yml @@ -54,6 +54,10 @@ services: - TZ=America/Montreal volumes: - /mnt/HoardingCow_docker_data/Hermes/data:/opt/data + # Syncthing-shared org files — read-only view of user's agenda + - /mnt/HoardingCow_docker_data/Syncthing/org-ro:/opt/data/org-ro:ro + # Syncthing-shared inbox — write tasks here, they sync to user's laptop + - /mnt/HoardingCow_docker_data/Syncthing/org-rw:/opt/data/org-rw:rw devices: - /dev/kfd:/dev/kfd - /dev/dri:/dev/dri @@ -63,6 +67,24 @@ services: networks: - ai_backend + syncthing: + image: syncthing/syncthing:latest + container_name: syncthing + hostname: syncthing + restart: always + ports: + - "127.0.0.1:8384:8384" + - "22000:22000" + - "21027:21027/udp" + environment: + - TZ=America/Montreal + volumes: + - /mnt/HoardingCow_docker_data/Syncthing/config:/var/syncthing/config + - /mnt/HoardingCow_docker_data/Syncthing/org-ro:/org-ro + - /mnt/HoardingCow_docker_data/Syncthing/org-rw:/org-rw + networks: + - ai_backend + ollama: build: context: ./ollama From f9fb28d56078e7503516ac69307e862f3929c92b Mon Sep 17 00:00:00 2001 From: Hermes Date: Thu, 14 May 2026 21:40:00 -0400 Subject: [PATCH 70/71] fix: route Syncthing web UI through Traefik with HTTPS --- ai/compose.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/ai/compose.yml b/ai/compose.yml index 3bf9070..1d333db 100644 --- a/ai/compose.yml +++ b/ai/compose.yml @@ -73,7 +73,7 @@ services: hostname: syncthing restart: always ports: - - "127.0.0.1:8384:8384" + - "8384:8384" - "22000:22000" - "21027:21027/udp" environment: @@ -84,6 +84,17 @@ services: - /mnt/HoardingCow_docker_data/Syncthing/org-rw:/org-rw networks: - ai_backend + - ai_net + labels: + - "traefik.enable=true" + - "traefik.http.routers.syncthing-http.rule=Host(`syncthing.lazyworkhorse.net`)" + - "traefik.http.routers.syncthing-http.entrypoints=web" + - "traefik.http.routers.syncthing-http.middlewares=redirect-to-https" + - "traefik.http.routers.syncthing-https.rule=Host(`syncthing.lazyworkhorse.net`)" + - "traefik.http.routers.syncthing-https.entrypoints=websecure" + - "traefik.http.routers.syncthing-https.tls=true" + - "traefik.http.routers.syncthing-https.tls.certresolver=njalla" + - "traefik.http.services.syncthing.loadbalancer.server.port=8384" ollama: build: From 6a44120b1a6b40cb4b2e77147609bc766d6988b3 Mon Sep 17 00:00:00 2001 From: Thierry Pouplier Date: Mon, 18 May 2026 20:25:18 -0400 Subject: [PATCH 71/71] Fixed syncthing dir path --- ai/compose.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ai/compose.yml b/ai/compose.yml index 1d333db..1db7831 100644 --- a/ai/compose.yml +++ b/ai/compose.yml @@ -55,9 +55,9 @@ services: volumes: - /mnt/HoardingCow_docker_data/Hermes/data:/opt/data # Syncthing-shared org files — read-only view of user's agenda - - /mnt/HoardingCow_docker_data/Syncthing/org-ro:/opt/data/org-ro:ro + - /mnt/HoardingCow_docker_data/Syncthing/telos-ro:/opt/data/telos-ro:ro # Syncthing-shared inbox — write tasks here, they sync to user's laptop - - /mnt/HoardingCow_docker_data/Syncthing/org-rw:/opt/data/org-rw:rw + - /mnt/HoardingCow_docker_data/Syncthing/telos-rw:/opt/data/telos-rw:rw devices: - /dev/kfd:/dev/kfd - /dev/dri:/dev/dri @@ -80,8 +80,8 @@ services: - TZ=America/Montreal volumes: - /mnt/HoardingCow_docker_data/Syncthing/config:/var/syncthing/config - - /mnt/HoardingCow_docker_data/Syncthing/org-ro:/org-ro - - /mnt/HoardingCow_docker_data/Syncthing/org-rw:/org-rw + - /mnt/HoardingCow_docker_data/Syncthing/telos-ro:/telos-ro + - /mnt/HoardingCow_docker_data/Syncthing/telos-rw:/telos-rw networks: - ai_backend - ai_net