97 lines
4.0 KiB
Python
97 lines
4.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Apply fail-closed Atlas voice integration patches to pinned Hermes WebUI."""
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT = Path("/opt/hermes-webui")
|
|
|
|
|
|
def replace_exact(path: Path, before: str, after: str, count: int = 1) -> None:
|
|
"""Replace an exact upstream fragment and fail when the pin has drifted."""
|
|
source = path.read_text(encoding="utf-8")
|
|
if source.count(before) != count:
|
|
raise SystemExit(f"Atlas voice patch context changed in {path}: {before[:80]!r}")
|
|
path.write_text(source.replace(before, after, count), encoding="utf-8")
|
|
|
|
|
|
index = ROOT / "static/index.html"
|
|
replace_exact(
|
|
index,
|
|
'<option value="browser">Browser speech synthesis</option><option value="edge">Edge TTS (server)</option>',
|
|
'<option value="atlas">Atlas Jetson (private)</option><option value="browser">Browser speech synthesis</option><option value="edge">Edge TTS (server)</option>',
|
|
)
|
|
replace_exact(
|
|
index,
|
|
'<script src="static/boot.js?v=__WEBUI_VERSION__" defer></script>',
|
|
'<script src="static/boot.js?v=__WEBUI_VERSION__" defer></script>\n<script src="static/atlas-voice.js?v=__WEBUI_VERSION__" defer></script>',
|
|
)
|
|
|
|
ui = ROOT / "static/ui.js"
|
|
replace_exact(ui, "function _playEdgeTtsChunked(text, btn){", "function _playEdgeTtsChunked(text, btn, engineOverride){")
|
|
replace_exact(
|
|
ui,
|
|
"body:JSON.stringify({text:chunk, voice:voice, rate:rate, pitch:pitch})",
|
|
"body:JSON.stringify({text:chunk, voice:voice, rate:rate, pitch:pitch, engine:engineOverride||'edge'})",
|
|
)
|
|
replace_exact(
|
|
ui,
|
|
"if(engine==='edge'){\n _playEdgeTtsChunked(clean, btn);",
|
|
"if(engine==='edge'||engine==='atlas'){\n _playEdgeTtsChunked(clean, btn, engine);",
|
|
)
|
|
replace_exact(
|
|
ui,
|
|
"if(engine==='edge'){\n _playEdgeTtsChunked(clean, null);",
|
|
"if(engine==='edge'||engine==='atlas'){\n _playEdgeTtsChunked(clean, null, engine);",
|
|
)
|
|
|
|
routes = ROOT / "api/routes.py"
|
|
marker = " # ── ElevenLabs TTS ──────────────────────────────────────────────────\n"
|
|
atlas = ''' # ── Atlas private Jetson TTS ─────────────────────────────────────────
|
|
if engine == "atlas":
|
|
atlas_url = os.getenv("HERMES_WEBUI_ATLAS_TTS_URL", "").strip()
|
|
expected_url = "http://hermes-tts.hermes.svc.cluster.local:9001/v1/audio/speech"
|
|
if atlas_url != expected_url:
|
|
from api.helpers import bad as _bad
|
|
return _bad(handler, "Atlas private TTS is not configured", 503)
|
|
speed = 1.0
|
|
if rate_str:
|
|
try:
|
|
speed = max(0.5, min(2.0, 1.0 + (float(rate_str.rstrip("%")) / 100.0)))
|
|
except ValueError:
|
|
speed = 1.0
|
|
request_body = json.dumps({
|
|
"model": "piper",
|
|
"input": text,
|
|
"voice": "en_US-lessac-high",
|
|
"speed": speed,
|
|
}).encode("utf-8")
|
|
request = Request(atlas_url, data=request_body, headers={
|
|
"Content-Type": "application/json",
|
|
"Accept": "audio/wav",
|
|
})
|
|
try:
|
|
with _tts_open(
|
|
request,
|
|
timeout=45,
|
|
opener_factory=lambda: build_opener(ProxyHandler({}), _NoRedirectTtsHandler()),
|
|
) as response:
|
|
audio_data = _buffer_tts_audio_response(response)
|
|
except Exception:
|
|
logger.exception("Atlas private TTS generation failed")
|
|
from api.helpers import bad as _bad
|
|
return _bad(handler, "Atlas private TTS generation failed", 502)
|
|
handler.send_response(200)
|
|
handler.send_header("Content-Type", "audio/wav")
|
|
handler.send_header("Cache-Control", "no-store")
|
|
handler.send_header("Content-Length", str(len(audio_data)))
|
|
handler.end_headers()
|
|
try:
|
|
handler.wfile.write(audio_data)
|
|
except (BrokenPipeError, ConnectionResetError):
|
|
pass
|
|
return True
|
|
|
|
'''
|
|
replace_exact(routes, marker, atlas + marker)
|