feat(audio): add offline local neural voice via Piper (sv + en)
Announcements can use Piper, a free local neural TTS, instead of the browser's system voice. No account, no paid service, and it keeps working with no internet at the track. - npm run tts:install (or Settings -> Audio -> Install) downloads the standalone Piper release for Linux/Windows/macOS plus Swedish and English voices into piper/ once - server/tts.js generates each line once, one at a time, and caches it as WAV under data/tts/; a new line takes ~0.3 s with the standalone build - src/piper_speech.js plays it and falls back to the browser voice if Piper is missing, errors, or takes over 2.5 s, so no announcement is lost and timing is never held up - "Pre-generate phrases and driver names" renders every fixed phrase and name in both languages before race day Browser voice stays the default. Generation routes fall under the admin PIN. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
29
tests/browser_tts_settings.js
Normal file
29
tests/browser_tts_settings.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
// The voice engine panel in Settings -> Audio: renders its Swedish/English voice status,
|
||||
// and the engine choice persists to the backend and survives a reload. Whether Piper is
|
||||
// actually installed on the test machine doesn't matter here - generation and fallback
|
||||
// are covered by tests/tts.test.js, tests/piper_speech.test.js and the integration test.
|
||||
export async function runTtsSettingsSmoke({ evaluate, navigate, savedState, waitFor }) {
|
||||
const openAudioTab = async () => {
|
||||
await navigate("settings", "settingsForm");
|
||||
await evaluate(`document.querySelector('[data-settings-tab="audio"]').click()`);
|
||||
await waitFor(async () => Boolean(await evaluate(`document.getElementById("ttsEngine")`)), "voice engine panel rendered");
|
||||
};
|
||||
|
||||
await openAudioTab();
|
||||
const panelText = await evaluate(`document.getElementById("ttsSettings").textContent`);
|
||||
assert.match(panelText, /Svenska/);
|
||||
assert.match(panelText, /English/);
|
||||
assert.equal(await evaluate(`document.getElementById("ttsEngine").value`), "browser", "the browser voice stays the default");
|
||||
|
||||
await evaluate(`(() => { const select = document.getElementById("ttsEngine"); select.value = "piper"; select.dispatchEvent(new Event("change")); })()`);
|
||||
await waitFor(async () => (await savedState()).settings.ttsEngine === "piper", "engine choice saved to the backend");
|
||||
|
||||
await openAudioTab();
|
||||
assert.equal(await evaluate(`document.getElementById("ttsEngine").value`), "piper", "the choice survives a reload");
|
||||
console.log("PASS the voice engine panel shows Swedish/English status and the Piper choice persists across a reload");
|
||||
|
||||
await evaluate(`(() => { const select = document.getElementById("ttsEngine"); select.value = "browser"; select.dispatchEvent(new Event("change")); })()`);
|
||||
await waitFor(async () => (await savedState()).settings.ttsEngine === "browser", "engine reset for the rest of the suite");
|
||||
}
|
||||
Reference in New Issue
Block a user