From e0f1a4504c8f8682f57662424698a0f3dc7d6611 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 20 Aug 2026 17:28:55 +0000 Subject: [PATCH] fix(hermes-voice): switch Atlas Piper TTS to hfc_female, bake amy as alternate Brad's 2026-08-20 decision: default the private Hermes chat TTS voice to a warmer, pleasant feminine voice (en_US-hfc_female-medium) instead of Lessac. en_US-amy-medium is baked into the same image as a selectable alternate so the choice can be revisited without another Jenkins build. Lessac models are kept (not deleted) since the task didn't require reclaiming image size; the image grows by ~120.6 MiB (2 medium voices, onnx+json, from the pinned rhasspy/piper-voices revision already used for lessac). The Piper server only reads HERMES_TTS_VOICE at process start; the "voice" field in the WebUI's request body is not consulted by the server (dockerfiles/hermes-jetson-tts-server.py:59-72). That WebUI string is aligned here for honesty, but the env var remains the effective control -- align image digest and env var rollout order (see PR description). Co-Authored-By: Claude Sonnet 5 --- dockerfiles/Dockerfile.hermes-jetson-tts | 16 ++++++++++++++-- dockerfiles/hermes-jetson-tts-server.py | 2 +- dockerfiles/hermes-webui-atlas-patch.py | 2 +- services/hermes/voice-deployment.yaml | 4 ++-- testing/tests/test_hermes_chat_quality.py | 8 ++++++-- 5 files changed, 24 insertions(+), 8 deletions(-) diff --git a/dockerfiles/Dockerfile.hermes-jetson-tts b/dockerfiles/Dockerfile.hermes-jetson-tts index deba4080..f37c0c80 100644 --- a/dockerfiles/Dockerfile.hermes-jetson-tts +++ b/dockerfiles/Dockerfile.hermes-jetson-tts @@ -24,6 +24,18 @@ ADD --checksum=sha256:f7d01dde371555732c4c314111ac79672b1a5ce2fc19266ab42178fd8d ADD --checksum=sha256:45754dfdebb3b8661c3fc564713772deec6e064feeb5b4e9594857dc7305193a --chmod=0444 \ https://huggingface.co/rhasspy/piper-voices/resolve/ea046e8458f6acd997706d6e6066a022b42f6fb1/en/en_US/lessac/low/en_US-lessac-low.onnx.json?download=true \ /opt/models/piper/en_US-lessac-low.onnx.json +ADD --checksum=sha256:914c473788fc1fa8b63ace1cdcdb44588f4ae523d3ab37df1536616835a140b7 --chmod=0444 \ + https://huggingface.co/rhasspy/piper-voices/resolve/ea046e8458f6acd997706d6e6066a022b42f6fb1/en/en_US/hfc_female/medium/en_US-hfc_female-medium.onnx?download=true \ + /opt/models/piper/en_US-hfc_female-medium.onnx +ADD --checksum=sha256:03f1fa0622b80463283592d97aca9f6e89aec345a5c56b7257723e0093c58b6c --chmod=0444 \ + https://huggingface.co/rhasspy/piper-voices/resolve/ea046e8458f6acd997706d6e6066a022b42f6fb1/en/en_US/hfc_female/medium/en_US-hfc_female-medium.onnx.json?download=true \ + /opt/models/piper/en_US-hfc_female-medium.onnx.json +ADD --checksum=sha256:b3a6e47b57b8c7fbe6a0ce2518161a50f59a9cdd8a50835c02cb02bdd6206c18 --chmod=0444 \ + https://huggingface.co/rhasspy/piper-voices/resolve/ea046e8458f6acd997706d6e6066a022b42f6fb1/en/en_US/amy/medium/en_US-amy-medium.onnx?download=true \ + /opt/models/piper/en_US-amy-medium.onnx +ADD --checksum=sha256:95a23eb4d42909d38df73bb9ac7f45f597dbfcde2d1bf9526fdeaf5466977d77 --chmod=0444 \ + https://huggingface.co/rhasspy/piper-voices/resolve/ea046e8458f6acd997706d6e6066a022b42f6fb1/en/en_US/amy/medium/en_US-amy-medium.onnx.json?download=true \ + /opt/models/piper/en_US-amy-medium.onnx.json RUN chmod 0555 /opt/models /opt/models/piper COPY dockerfiles/hermes-jetson-tts-server.py /opt/atlas/hermes-jetson-tts-server.py @@ -31,11 +43,11 @@ RUN chmod 0555 /opt/atlas/hermes-jetson-tts-server.py # Load the actual pinned voice during the ARM64 build. This catches package or # model-format drift before the image can reach Flux. -RUN python -c "import stat; from pathlib import Path; from piper import PiperVoice; p=Path('/opt/models/piper'); models=[p/'en_US-lessac-high.onnx',p/'en_US-lessac-medium.onnx',p/'en_US-lessac-low.onnx']; assert stat.S_IMODE(p.stat().st_mode)==0o555; assert all(stat.S_IMODE(model.stat().st_mode)==0o444 for model in models); voices=[PiperVoice.load(model,Path(str(model)+'.json'),use_cuda=False,download_dir=p) for model in models]; assert all(voice.config.sample_rate>0 for voice in voices)" +RUN python -c "import stat; from pathlib import Path; from piper import PiperVoice; p=Path('/opt/models/piper'); models=[p/'en_US-lessac-high.onnx',p/'en_US-lessac-medium.onnx',p/'en_US-lessac-low.onnx',p/'en_US-hfc_female-medium.onnx',p/'en_US-amy-medium.onnx']; assert stat.S_IMODE(p.stat().st_mode)==0o555; assert all(stat.S_IMODE(model.stat().st_mode)==0o444 for model in models); voices=[PiperVoice.load(model,Path(str(model)+'.json'),use_cuda=False,download_dir=p) for model in models]; assert all(voice.config.sample_rate>0 for voice in voices)" ENV HERMES_TTS_HOST=0.0.0.0 \ HERMES_TTS_PORT=9001 \ - HERMES_TTS_VOICE=en_US-lessac-medium \ + HERMES_TTS_VOICE=en_US-hfc_female-medium \ HERMES_TTS_CACHE=/opt/models/piper \ OMP_NUM_THREADS=2 \ PYTHONDONTWRITEBYTECODE=1 \ diff --git a/dockerfiles/hermes-jetson-tts-server.py b/dockerfiles/hermes-jetson-tts-server.py index 3abdc28b..4a318a8b 100644 --- a/dockerfiles/hermes-jetson-tts-server.py +++ b/dockerfiles/hermes-jetson-tts-server.py @@ -17,7 +17,7 @@ from piper import PiperConfig, PiperVoice, SynthesisConfig HOST = os.getenv("HERMES_TTS_HOST", "0.0.0.0") PORT = int(os.getenv("HERMES_TTS_PORT", "9001")) -VOICE_NAME = os.getenv("HERMES_TTS_VOICE", "en_US-lessac-high") +VOICE_NAME = os.getenv("HERMES_TTS_VOICE", "en_US-hfc_female-medium") CACHE_DIR = Path(os.getenv("HERMES_TTS_CACHE", "/cache/piper")) MAX_TEXT_CHARS = 5000 ONNX_THREADS = max(1, int(os.getenv("HERMES_TTS_ONNX_THREADS", "4"))) diff --git a/dockerfiles/hermes-webui-atlas-patch.py b/dockerfiles/hermes-webui-atlas-patch.py index 20d04399..7b5a8864 100644 --- a/dockerfiles/hermes-webui-atlas-patch.py +++ b/dockerfiles/hermes-webui-atlas-patch.py @@ -63,7 +63,7 @@ atlas = ''' # ── Atlas private Jetson TTS ────────── request_body = json.dumps({ "model": "piper", "input": text, - "voice": "en_US-lessac-high", + "voice": "en_US-hfc_female-medium", "speed": speed, }).encode("utf-8") request = Request(atlas_url, data=request_body, headers={ diff --git a/services/hermes/voice-deployment.yaml b/services/hermes/voice-deployment.yaml index 721b8df6..8a08a315 100644 --- a/services/hermes/voice-deployment.yaml +++ b/services/hermes/voice-deployment.yaml @@ -114,7 +114,7 @@ spec: app: hermes-tts annotations: ai.bstein.dev/role: private-chat-text-to-speech - ai.bstein.dev/model: piper-en-us-lessac-medium + ai.bstein.dev/model: piper-en-us-hfc-female-medium ai.bstein.dev/gpu: CPU-only beside Whisper on the voice node spec: automountServiceAccountToken: false @@ -131,7 +131,7 @@ spec: - {name: HOME, value: /tmp} - {name: XDG_CACHE_HOME, value: /tmp/cache} - {name: HERMES_TTS_PORT, value: "9001"} - - {name: HERMES_TTS_VOICE, value: en_US-lessac-medium} + - {name: HERMES_TTS_VOICE, value: en_US-hfc_female-medium} - {name: HERMES_TTS_CACHE, value: /opt/models/piper} - {name: HERMES_TTS_ONNX_THREADS, value: "2"} startupProbe: diff --git a/testing/tests/test_hermes_chat_quality.py b/testing/tests/test_hermes_chat_quality.py index 4cf45ffb..d97fc413 100644 --- a/testing/tests/test_hermes_chat_quality.py +++ b/testing/tests/test_hermes_chat_quality.py @@ -398,7 +398,11 @@ def test_voice_models_are_baked_and_runtime_has_no_public_egress(): assert "HERMES_STT_CACHE=/opt/models/whisper" in stt_dockerfile assert "ADD --checksum=sha256:4cabf7c3" in tts_dockerfile assert "ADD --checksum=sha256:db42b97d" in tts_dockerfile - assert tts_dockerfile.count("--chmod=0444") == 6 + assert "ADD --checksum=sha256:914c473788fc1fa8b63ace1cdcdb44588f4ae523d3ab37df1536616835a140b7" in tts_dockerfile + assert "ADD --checksum=sha256:03f1fa0622b80463283592d97aca9f6e89aec345a5c56b7257723e0093c58b6c" in tts_dockerfile + assert "ADD --checksum=sha256:b3a6e47b57b8c7fbe6a0ce2518161a50f59a9cdd8a50835c02cb02bdd6206c18" in tts_dockerfile + assert "ADD --checksum=sha256:95a23eb4d42909d38df73bb9ac7f45f597dbfcde2d1bf9526fdeaf5466977d77" in tts_dockerfile + assert tts_dockerfile.count("--chmod=0444") == 10 assert "chmod 0555 /opt/models /opt/models/piper" in tts_dockerfile assert "HERMES_TTS_CACHE=/opt/models/piper" in tts_dockerfile tts_server = (ROOT / "dockerfiles" / "hermes-jetson-tts-server.py").read_text() @@ -449,7 +453,7 @@ def test_voice_workloads_have_deliberate_xavier_placement(): tts_env = { item["name"]: item["value"] for item in tts["containers"][0]["env"] } - assert tts_env["HERMES_TTS_VOICE"] == "en_US-lessac-medium" + assert tts_env["HERMES_TTS_VOICE"] == "en_US-hfc_female-medium" assert tts_env["HERMES_TTS_ONNX_THREADS"] == "2" assert tts["containers"][0]["resources"]["limits"]["cpu"] == "4" assert all("hostPath" not in volume for volume in stt["volumes"])