fix: send engine=edge from the first-party Edge callers
The settings UI writes localStorage immediately but persists the server engine through a debounced save, so a first-party Edge selection could fall through the new server default to a stale ElevenLabs/OpenAI engine. Both WebUI Edge callers now pass engine explicitly; regressions cover both call sites. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CNmJZVV9u6pBhcwVhwkPp6
This commit is contained in:
1 parent
a375aa9356
commit
cf62bb87bf
3 files changed
+145
-2
No files matched your search
+1
-1
@@ -1901,7 +1901,7 @@ window.renderTranscript=function(container, messages, opts){
|
||||
fetch(new URL('api/tts', document.baseURI || location.href).href, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({text: clean, voice, rate, pitch})
|
||||
body: JSON.stringify({text: clean, voice, rate, pitch, engine: 'edge'})
|
||||
})
|
||||
.then(r => {
|
||||
if(!r.ok) throw new Error('TTS request failed: ' + r.status);
|
||||
|
||||
+1
-1
@@ -9168,7 +9168,7 @@ function _playEdgeTtsChunked(text, btn){
|
||||
fetch(new URL('api/tts', document.baseURI || location.href).href, {
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/json'},
|
||||
body:JSON.stringify({text:chunk, voice:voice, rate:rate, pitch:pitch})
|
||||
body:JSON.stringify({text:chunk, voice:voice, rate:rate, pitch:pitch, engine:'edge'})
|
||||
})
|
||||
.then(function(r){
|
||||
if(!r.ok){
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
"""G7394-1: first-party Edge callers must send engine:'edge' explicitly.
|
||||
|
||||
The Settings selector writes localStorage immediately, then saves the server
|
||||
setting through a 350ms debounced autosave. During that window (or if the
|
||||
save fails) the WebUI dispatches the Edge branch while the server still holds
|
||||
ElevenLabs/OpenAI. Omitting engine lets the persisted-engine fallback silently
|
||||
play the stale provider.
|
||||
|
||||
These tests execute the production ui.js / boot.js callers with a local Edge
|
||||
selection and assert the /api/tts request body carries the explicit override.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.js_source_extract import extract_function
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
UI_JS = (ROOT / "static" / "ui.js").read_text(encoding="utf-8")
|
||||
BOOT_JS = (ROOT / "static" / "boot.js").read_text(encoding="utf-8")
|
||||
NODE = shutil.which("node")
|
||||
|
||||
pytestmark = pytest.mark.skipif(NODE is None, reason="node not on PATH")
|
||||
|
||||
|
||||
def _run_node(script: str) -> list[dict]:
|
||||
result = subprocess.run(
|
||||
[NODE, "-e", script],
|
||||
cwd=ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
)
|
||||
assert result.returncode == 0, (
|
||||
f"node subprocess failed:\n--- stdout ---\n{result.stdout}\n"
|
||||
f"--- stderr ---\n{result.stderr}"
|
||||
)
|
||||
return json.loads(result.stdout.strip().splitlines()[-1])
|
||||
|
||||
|
||||
_UI_HARNESS = r"""
|
||||
const requests = [];
|
||||
const store = {
|
||||
'hermes-tts-engine': 'edge',
|
||||
'hermes-tts-voice': 'en-US-AriaNeural',
|
||||
};
|
||||
globalThis.window = globalThis;
|
||||
globalThis.speechSynthesis = { cancel() {}, getVoices() { return []; } };
|
||||
globalThis.localStorage = {
|
||||
getItem: (k) => Object.prototype.hasOwnProperty.call(store, k) ? store[k] : null,
|
||||
};
|
||||
globalThis.document = {
|
||||
baseURI: 'http://localhost:8787/',
|
||||
querySelectorAll: () => [],
|
||||
};
|
||||
globalThis.location = { href: 'http://localhost:8787/' };
|
||||
globalThis.fetch = (url, opts) => {
|
||||
requests.push({ url: String(url), body: JSON.parse(opts.body) });
|
||||
return new Promise(() => {});
|
||||
};
|
||||
let _ttsSpeaking = false;
|
||||
let _playingEdgeAudio = null;
|
||||
let _ttsCurrentUtterance = null;
|
||||
let _ttsChunkQueue = [];
|
||||
let _ttsChunkIndex = 0;
|
||||
let _ttsActiveBtn = null;
|
||||
__UI_FNS__
|
||||
const row = { dataset: { rawText: 'Hello from Edge' } };
|
||||
const btn = { dataset: { speaking: '0' }, closest: () => row };
|
||||
speakMessage(btn);
|
||||
console.log(JSON.stringify(requests));
|
||||
"""
|
||||
|
||||
|
||||
_BOOT_HARNESS = r"""
|
||||
const requests = [];
|
||||
const store = {
|
||||
'hermes-tts-engine': 'edge',
|
||||
'hermes-tts-voice': 'en-US-AriaNeural',
|
||||
};
|
||||
globalThis.window = globalThis;
|
||||
globalThis.localStorage = {
|
||||
getItem: (k) => Object.prototype.hasOwnProperty.call(store, k) ? store[k] : null,
|
||||
};
|
||||
globalThis.document = {
|
||||
baseURI: 'http://localhost:8787/',
|
||||
querySelectorAll: () => [{ dataset: { rawText: 'Hello from Edge' } }],
|
||||
};
|
||||
globalThis.location = { href: 'http://localhost:8787/' };
|
||||
globalThis.fetch = (url, opts) => {
|
||||
requests.push({ url: String(url), body: JSON.parse(opts.body) });
|
||||
return new Promise(() => {});
|
||||
};
|
||||
const S = { session: { session_id: 'sid-edge' } };
|
||||
let _voiceModeActive = true;
|
||||
let _voiceModeThinkingSid = null;
|
||||
let _ttsSpeaking = false;
|
||||
let _playingEdgeAudio = null;
|
||||
function _setState() {}
|
||||
function _startListening() {}
|
||||
__BOOT_FNS__
|
||||
_speakResponse();
|
||||
console.log(JSON.stringify(requests));
|
||||
"""
|
||||
|
||||
|
||||
def test_play_edge_tts_chunked_sends_explicit_engine_when_server_still_elevenlabs():
|
||||
"""Local Edge selection (Listen button) must override persisted ElevenLabs."""
|
||||
fns = "\n".join(
|
||||
[
|
||||
extract_function(UI_JS, "_stripForTTS"),
|
||||
extract_function(UI_JS, "_splitForTTS"),
|
||||
extract_function(UI_JS, "_playEdgeTtsChunked"),
|
||||
extract_function(UI_JS, "stopTTS"),
|
||||
extract_function(UI_JS, "speakMessage"),
|
||||
]
|
||||
)
|
||||
requests = _run_node(_UI_HARNESS.replace("__UI_FNS__", fns))
|
||||
|
||||
assert len(requests) == 1
|
||||
assert requests[0]["url"].endswith("/api/tts")
|
||||
assert requests[0]["body"]["engine"] == "edge"
|
||||
assert requests[0]["body"]["text"] == "Hello from Edge"
|
||||
assert requests[0]["body"]["voice"] == "en-US-AriaNeural"
|
||||
|
||||
|
||||
def test_voice_mode_edge_branch_sends_explicit_engine_when_server_still_openai():
|
||||
"""Local Edge selection (voice-mode auto-read) must override persisted OpenAI."""
|
||||
fns = extract_function(BOOT_JS, "_speakResponse")
|
||||
requests = _run_node(_BOOT_HARNESS.replace("__BOOT_FNS__", fns))
|
||||
|
||||
assert len(requests) == 1
|
||||
assert requests[0]["url"].endswith("/api/tts")
|
||||
assert requests[0]["body"]["engine"] == "edge"
|
||||
assert requests[0]["body"]["text"] == "Hello from Edge"
|
||||
assert requests[0]["body"]["voice"] == "en-US-AriaNeural"
|
||||
Reference in new issue
Block a user