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>
161 lines
5.7 KiB
Python
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()
|