Port the original #27 detected-language pipeline onto the verified PR #39 prerequisite while preserving the current-main conversation instrument and host continuity changes. Keep voice selection server-side with no user selector or client voice field. Reuse 207c16ab only for its stricter exact-code trust boundary, omitting malformed or absent language so Piper defaults to Amy.
163 lines
5.9 KiB
Python
163 lines
5.9 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"
|
|
AGENT_FIXTURE = ROOT / "testing/fixtures/hermes-agent"
|
|
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"
|
|
agent_target = tmp_path / "hermes-agent"
|
|
shutil.copytree(FIXTURE, target)
|
|
shutil.copytree(AGENT_FIXTURE, agent_target)
|
|
env = os.environ.copy()
|
|
env["HERMES_WEBUI_PATCH_ROOT"] = str(target)
|
|
env["HERMES_AGENT_PATCH_ROOT"] = str(agent_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]
|
|
assert '"input": text' in atlas_route
|
|
assert '"voice"' not in atlas_route
|
|
assert 'request_payload["language"] = _atlas_language' in atlas_route
|
|
|
|
|
|
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 "const request={text:chunk,engine:'atlas'}" in script
|
|
assert "if(language) request.language=language" in script
|
|
assert "speakResponse(generation)" in script
|
|
assert "window._voiceModeImmediateSend" in script
|
|
assert "mute" not in script.lower()
|
|
assert "const TTS_LANGUAGES=['en','ru','es']" in script
|