perf(hermes): pipeline conversational voice playback
Some checks failed
Tests / Declarative: Post Actions failed: 2, passed: 191

This commit is contained in:
jenkins 2026-08-10 01:21:51 -03:00
parent 8cdf38f781
commit fb2a30d459
7 changed files with 45 additions and 23 deletions

View File

@ -12,6 +12,18 @@ ADD --checksum=sha256:4cabf7c3a638017137f34a1516522032d4fe3f38228a843cc9b764ddcb
ADD --checksum=sha256:db42b97d9859f257bc1561b8ed980e7fb2398402050a74ddd6cbec931a92412f --chmod=0444 \
https://huggingface.co/rhasspy/piper-voices/resolve/ea046e8458f6acd997706d6e6066a022b42f6fb1/en/en_US/lessac/high/en_US-lessac-high.onnx.json?download=true \
/opt/models/piper/en_US-lessac-high.onnx.json
ADD --checksum=sha256:5efe09e69902187827af646e1a6e9d269dee769f9877d17b16b1b46eeaaf019f --chmod=0444 \
https://huggingface.co/rhasspy/piper-voices/resolve/ea046e8458f6acd997706d6e6066a022b42f6fb1/en/en_US/lessac/medium/en_US-lessac-medium.onnx?download=true \
/opt/models/piper/en_US-lessac-medium.onnx
ADD --checksum=sha256:efe19c417bed055f2d69908248c6ba650fa135bc868b0e6abb3da181dab690a0 --chmod=0444 \
https://huggingface.co/rhasspy/piper-voices/resolve/ea046e8458f6acd997706d6e6066a022b42f6fb1/en/en_US/lessac/medium/en_US-lessac-medium.onnx.json?download=true \
/opt/models/piper/en_US-lessac-medium.onnx.json
ADD --checksum=sha256:f7d01dde371555732c4c314111ac79672b1a5ce2fc19266ab42178fd8df7f375 --chmod=0444 \
https://huggingface.co/rhasspy/piper-voices/resolve/ea046e8458f6acd997706d6e6066a022b42f6fb1/en/en_US/lessac/low/en_US-lessac-low.onnx?download=true \
/opt/models/piper/en_US-lessac-low.onnx
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
RUN chmod 0555 /opt/models /opt/models/piper
COPY dockerfiles/hermes-jetson-tts-server.py /opt/atlas/hermes-jetson-tts-server.py
@ -19,11 +31,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'); assert stat.S_IMODE(p.stat().st_mode)==0o555; assert stat.S_IMODE((p/'en_US-lessac-high.onnx').stat().st_mode)==0o444; v=PiperVoice.load(p/'en_US-lessac-high.onnx', p/'en_US-lessac-high.onnx.json', use_cuda=False, download_dir=p); assert v.config.sample_rate > 0"
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)"
ENV HERMES_TTS_HOST=0.0.0.0 \
HERMES_TTS_PORT=9001 \
HERMES_TTS_VOICE=en_US-lessac-high \
HERMES_TTS_VOICE=en_US-lessac-medium \
HERMES_TTS_CACHE=/opt/models/piper \
OMP_NUM_THREADS=2 \
PYTHONDONTWRITEBYTECODE=1 \

View File

@ -186,6 +186,19 @@
});
}
async function fetchSpeech(chunk){
const response=await fetch('/api/tts',{
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({text:chunk,engine:'atlas'}),
});
if(!response.ok){
const payload=await response.json().catch(function(){return {};});
throw new Error(payload.error||('Local speech request failed: '+response.status));
}
return response.blob();
}
async function speakResponse(token){
if(!active||token!==generation) return;
const currentSession=(typeof S!=='undefined'&&S.session)?S.session.session_id:null;
@ -200,20 +213,14 @@
const text=cleanForSpeech(rows[rows.length-1].dataset.rawText||'');
if(!text){restartSoon(token,250);return;}
setState('speaking');
const chunks=typeof window._splitForTTS==='function'?window._splitForTTS(text,900):[text];
const chunks=typeof window._splitForTTS==='function'?window._splitForTTS(text,280):[text];
try{
for(const chunk of chunks){
let pending=fetchSpeech(chunks[0]);
for(let index=0;index<chunks.length;index+=1){
if(!active||token!==generation) return;
const response=await fetch('/api/tts',{
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({text:chunk,engine:'atlas'}),
});
if(!response.ok){
const payload=await response.json().catch(function(){return {};});
throw new Error(payload.error||('Local speech request failed: '+response.status));
}
await playBlob(await response.blob(),token);
const blob=await pending;
if(index+1<chunks.length) pending=fetchSpeech(chunks[index+1]);
await playBlob(blob,token);
}
}catch(error){
if(active&&token===generation) toast((error&&error.message)||'Local speech is unavailable');

View File

@ -350,7 +350,7 @@ spec:
requests: {cpu: 250m, memory: 512Mi}
limits: {cpu: "2", memory: 4Gi}
- name: webui
image: registry.bstein.dev/bstein/hermes-webui@sha256:06f195df381abc60e97f31c0676044faffd1a51cb365cda77a919eee04827c71
image: registry.bstein.dev/bstein/hermes-webui@sha256:6eb16f76b236e10b210a31e69c7e68103cbf921892d813561d63f8aab0a98c32
imagePullPolicy: IfNotPresent
command: [/bin/sh, -ec]
args:

View File

@ -226,7 +226,7 @@ spec:
requests: {cpu: 250m, memory: 512Mi}
limits: {cpu: "1", memory: 2Gi}
- name: webui
image: registry.bstein.dev/bstein/hermes-webui@sha256:06f195df381abc60e97f31c0676044faffd1a51cb365cda77a919eee04827c71
image: registry.bstein.dev/bstein/hermes-webui@sha256:6eb16f76b236e10b210a31e69c7e68103cbf921892d813561d63f8aab0a98c32
imagePullPolicy: IfNotPresent
command: [/bin/sh, -ec]
args:

View File

@ -345,7 +345,7 @@ spec:
cpu: "2"
memory: 4Gi
- name: webui
image: registry.bstein.dev/bstein/hermes-webui@sha256:06f195df381abc60e97f31c0676044faffd1a51cb365cda77a919eee04827c71
image: registry.bstein.dev/bstein/hermes-webui@sha256:6eb16f76b236e10b210a31e69c7e68103cbf921892d813561d63f8aab0a98c32
imagePullPolicy: IfNotPresent
command: [/bin/sh, -ec]
args:

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-high
ai.bstein.dev/model: piper-en-us-lessac-medium
ai.bstein.dev/gpu: CPU-only on routing node
spec:
automountServiceAccountToken: false
@ -123,7 +123,7 @@ spec:
kubernetes.io/hostname: titan-20
containers:
- name: tts
image: registry.bstein.dev/bstein/hermes-jetson-tts@sha256:82fbed67bd871e821e4fd745a49c91a3749dc6ad45ea4841dc3fb7cfbcd53e61
image: registry.bstein.dev/bstein/hermes-jetson-tts@sha256:5cb9e57faab46365bff606c559af57b9505892be2909aea1ac523568d478b2cc
imagePullPolicy: IfNotPresent
ports:
- {name: http, containerPort: 9001, protocol: TCP}
@ -131,9 +131,9 @@ 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-high}
- {name: HERMES_TTS_VOICE, value: en_US-lessac-medium}
- {name: HERMES_TTS_CACHE, value: /opt/models/piper}
- {name: HERMES_TTS_ONNX_THREADS, value: "4"}
- {name: HERMES_TTS_ONNX_THREADS, value: "2"}
startupProbe:
httpGet: {path: /health, port: http}
periodSeconds: 5

View File

@ -182,6 +182,8 @@ def test_chat_voice_uses_private_jetson_services_and_shared_auto_route():
assert "/api/transcribe" in voice_script
assert "/api/tts" in voice_script
assert "speakResponse(generation)" in voice_script
assert "window._splitForTTS(text,280)" in voice_script
assert "pending=fetchSpeech(chunks[index+1])" in voice_script
assert "restartSoon(token,450)" in voice_script
@ -194,7 +196,7 @@ 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") == 2
assert tts_dockerfile.count("--chmod=0444") == 6
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()
@ -241,7 +243,8 @@ 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_ONNX_THREADS"] == "4"
assert tts_env["HERMES_TTS_VOICE"] == "en_US-lessac-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"])
assert all("hostPath" not in volume for volume in tts["volumes"])