atlas-iac/testing/tests/test_hermes_voice_instrument.py
Hermes Agent 724656d841 feat(hermes-tts): prepare fixed multilingual voice policy
Supersede draft PR #26 with a merge-safe prerequisite: bake and preload the amy, irina, and claude Piper models, route only validated server-side language to fixed voices, and leave the live voice deployment manifest unchanged.

Remove the pinned WebUI speaker selector and its persisted preference, omit client voice fields from every outbound TTS path, and keep hands-free Voice Mode and the conversation instrument intact. Hostile or legacy voice fields remain ignored by the Piper server.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-21 13:45:31 +00:00

161 lines
5.7 KiB
Python

"""Shipped patch and DOM contracts for the hands-free conversation instrument."""
from __future__ import annotations
import os
from pathlib import Path
import re
import shutil
import subprocess
import sys
ROOT = Path(__file__).resolve().parents[2]
FIXTURE = ROOT / "testing/fixtures/hermes-webui-0.52.181"
PATCHER = ROOT / "dockerfiles/hermes-webui-atlas-patch.py"
VOICE_JS = ROOT / "dockerfiles/hermes-webui-atlas-voice.js"
VOICE_CSS = ROOT / "dockerfiles/hermes-webui-atlas-voice.css"
DOM_PROBE = ROOT / "testing/probes/hermes_voice_instrument_probe.js"
MEDIARECORDER_FIXTURE = (
ROOT / "testing/fixtures/mediarecorder/chromium-webm-opus.json"
)
def _patched_fixture(tmp_path: Path) -> Path:
target = tmp_path / "hermes-webui"
shutil.copytree(FIXTURE, target)
env = os.environ.copy()
env["HERMES_WEBUI_PATCH_ROOT"] = str(target)
subprocess.run(
[sys.executable, str(PATCHER)],
cwd=ROOT,
env=env,
check=True,
capture_output=True,
text=True,
)
return target
def test_real_upstream_fixture_receives_visual_instrument_contract(tmp_path: Path):
"""Apply the production patcher to exact fragments from pinned WebUI 0.52.181."""
target = _patched_fixture(tmp_path)
index = (target / "static/index.html").read_text(encoding="utf-8")
assert index.count('id="voiceInstrumentStyles"') == 1
assert (
'href="static/atlas-voice.css?v=__WEBUI_VERSION__"' in index
)
assert 'role="status"' in index
assert 'aria-live="polite"' in index
assert 'aria-atomic="true"' in index
for layer in (
"voice-instrument-halo",
"voice-instrument-ripple",
"voice-instrument-orbit",
"voice-instrument-core",
"voice-instrument-symbol",
):
assert layer in index
assert '<button' not in index[index.index('id="voiceModeBar"') : index.index('<textarea')]
def test_patched_webui_has_no_user_voice_choice_or_client_voice_field(
tmp_path: Path,
):
"""The pinned settings DOM and every TTS path leave speakers to policy."""
target = _patched_fixture(tmp_path)
index = (target / "static/index.html").read_text(encoding="utf-8")
ui = (target / "static/ui.js").read_text(encoding="utf-8")
panels = (target / "static/panels.js").read_text(encoding="utf-8")
boot = (target / "static/boot.js").read_text(encoding="utf-8")
i18n = (target / "static/i18n.js").read_text(encoding="utf-8")
config = (target / "api/config.py").read_text(encoding="utf-8")
routes = (target / "api/routes.py").read_text(encoding="utf-8")
assert "settingsTtsVoice" not in index
assert "settings_label_tts_voice" not in index
assert "settings_desc_tts_voice" not in index
assert "Default system voice" not in index
assert 'id="settingsTtsEngine"' in index
assert 'id="btnVoiceMode"' in index
assert 'id="voiceModeBar"' in index
assert "hermes-tts-voice" not in ui
assert "voice:voice" not in ui
assert (
"body:JSON.stringify({text:chunk, rate:rate, pitch:pitch, "
"engine:engineOverride||'edge'})"
) in ui
assert "settingsTtsVoice" not in panels
assert "tts_voice" not in panels
assert "localStorage.removeItem('hermes-tts-voice')" in panels
assert panels.count("hermes-tts-voice") == 1
assert "hermes-tts-voice" not in boot
assert "tts_voice" not in boot
assert "text: clean, voice" not in boot
assert '"tts_voice"' not in config
assert "settings_label_tts_voice" not in i18n
assert "settings_desc_tts_voice" not in i18n
atlas_route = routes.split('if engine == "atlas":', 1)[1].split(
"# ── ElevenLabs TTS", 1
)[0]
request_body = atlas_route.split("request_body = json.dumps({", 1)[1].split(
"}).encode", 1
)[0]
assert '"input": text' in request_body
assert '"voice"' not in request_body
assert '"language"' not in request_body
def test_visual_states_have_distinct_layers_finite_error_and_reduced_motion():
css = VOICE_CSS.read_text(encoding="utf-8")
for state in ("listening", "transcribing", "thinking", "speaking", "error"):
assert f".voice-mode-indicator.{state}" in css
for animation in (
"voice-instrument-breathe",
"voice-instrument-orbit",
"voice-instrument-speaking-pulse",
):
assert f"@keyframes {animation}" in css
assert (
".voice-mode-indicator.speaking.is-playing .voice-instrument-halo"
in css
)
error_rules = "\n".join(
match.group(0)
for match in re.finditer(r"[^{}]*\.error[^{}]*\{[^{}]*\}", css)
)
assert "animation:" not in error_rules
assert "@media (prefers-reduced-motion: reduce)" in css
reduced = css.split("@media (prefers-reduced-motion: reduce)", 1)[1]
assert "animation: none !important" in reduced
assert "transition: none !important" in reduced
def test_dom_probe_exercises_actual_injected_voice_script():
result = subprocess.run(
["node", str(DOM_PROBE), str(VOICE_JS), str(MEDIARECORDER_FIXTURE)],
cwd=ROOT,
check=True,
capture_output=True,
text=True,
)
assert result.stdout.strip() == "voice instrument DOM contract passed"
def test_visual_slice_preserves_private_voice_request_and_capture_contract():
script = VOICE_JS.read_text(encoding="utf-8")
assert script.count("navigator.mediaDevices.getUserMedia(") == 1
assert "form.append('file',new File([blob],'voice-input.'+ext" in script
assert "fetch('/api/transcribe',{method:'POST',body:form})" in script
assert "body:JSON.stringify({text:chunk,engine:'atlas'})" in script
assert "speakResponse(generation)" in script
assert "window._voiceModeImmediateSend" in script
assert "mute" not in script.lower()
assert "language" not in script.lower()