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 <noreply@anthropic.com>
This commit is contained in:
Hermes Agent 2026-08-20 17:28:55 +00:00
parent 11bd04cce5
commit e0f1a4504c
5 changed files with 24 additions and 8 deletions

View File

@ -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 \

View File

@ -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")))

View File

@ -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={

View File

@ -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:

View File

@ -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"])