Podcast player: say why it cannot speak, with a way out
"The podcasts don't even work on my Linux setup on Firefox." Firefox on Linux speaks through speech-dispatcher; without it speechSynthesis is present but empty and silent - getVoices() is empty, speak() does nothing, no events - so the player looked broken. - pressing play with zero installed voices (or a start that never arrives within 1.7s) raises a banner naming the cause, with the Linux fix (sudo apt install speech-dispatcher) and the iOS one where it applies; - the banner offers "Use the natural voice" when a provider key exists (it switches the source, keeps playing and remembers it), "Add a provider key" when not (opens Settings) and a link into the picker; - natural-voice errors (refused autoplay, provider refusal) show in that same banner instead of hiding inside the picker. podcast-test fakes a present-but-empty speechSynthesis and pins the banner, the Linux line and the one-tap switch. 48 checks. Version 0.7.8, SW v100.
This commit is contained in:
parent
5d82612e2a
commit
08a145b087
6 changed files with 147 additions and 8 deletions
|
|
@ -73,7 +73,8 @@ Object.defineProperty(window, "speechSynthesis", {
|
|||
configurable: true,
|
||||
value: {
|
||||
speak(utterance) { window.__utterances.push(utterance); queueMicrotask(() => utterance.onstart && utterance.onstart({ name: "start", charIndex: 0 })); },
|
||||
cancel() {}, getVoices() { return []; }, addEventListener() {}, removeEventListener() {}, speaking: false, paused: false,
|
||||
cancel() {}, getVoices() { return [{ name: "Daniel", lang: "en-GB", voiceURI: "daniel", localService: true, default: true }]; },
|
||||
addEventListener() {}, removeEventListener() {}, speaking: false, paused: false,
|
||||
},
|
||||
});
|
||||
`;
|
||||
|
|
@ -160,7 +161,8 @@ Object.defineProperty(window, "speechSynthesis", {
|
|||
},
|
||||
cancel() { window.__cancelCount += 1; this._speaking = false; this._paused = true; },
|
||||
resume() { this._paused = false; },
|
||||
getVoices() { return []; },
|
||||
/* One installed voice: a working engine, which is what this stub is. */
|
||||
getVoices() { return [{ name: "Daniel", lang: "en-GB", voiceURI: "daniel", localService: true, default: true }]; },
|
||||
},
|
||||
});
|
||||
`;
|
||||
|
|
@ -383,6 +385,57 @@ try {
|
|||
await context.close();
|
||||
}
|
||||
|
||||
/* 4b2b. no speech engine behind the browser (Firefox on Linux) ------- */
|
||||
{
|
||||
const { context, page } = await makePage(browser, { width: 390, height: 844, deviceScaleFactor: 1, isMobile: true, hasTouch: true });
|
||||
/* speechSynthesis exists but is empty and silent — exactly what Firefox
|
||||
does on a Linux box without speech-dispatcher. */
|
||||
await page.addInitScript(() => {
|
||||
class FakeUtterance {
|
||||
constructor(text) { this.text = String(text); this.rate = 1; this.lang = "en-US"; }
|
||||
}
|
||||
Object.defineProperty(window, "SpeechSynthesisUtterance", { configurable: true, value: FakeUtterance });
|
||||
Object.defineProperty(window, "speechSynthesis", {
|
||||
configurable: true,
|
||||
value: {
|
||||
speak() { /* nothing at all: no engine, no events */ },
|
||||
cancel() {},
|
||||
getVoices: () => [],
|
||||
addEventListener() {},
|
||||
removeEventListener() {},
|
||||
speaking: false,
|
||||
paused: false,
|
||||
},
|
||||
});
|
||||
localStorage.setItem(
|
||||
"freezmo.providers.v1",
|
||||
JSON.stringify({ keys: { openai: "sk-test" }, models: {}, customBaseUrl: "", generation: null }),
|
||||
);
|
||||
});
|
||||
await seed(page, { decks: [], theme: "dark", extra: { "freezmo.podcasts.v1": JSON.stringify([EPISODE_RECORD]) } });
|
||||
await page.goto(`${APP}/podcast/pod_local`, { waitUntil: "networkidle" });
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.getByRole("button", { name: "Play the episode" }).click();
|
||||
await page.waitForTimeout(400);
|
||||
const banner = await page.getByText(/speech engine behind it/).count();
|
||||
check("no engine: the banner explains what is missing", banner === 1, String(banner));
|
||||
check(
|
||||
"no engine: it names the Linux fix",
|
||||
(await page.getByText(/speech-dispatcher/).count()) === 1,
|
||||
);
|
||||
const toNatural = page.getByRole("button", { name: "Use the natural voice" });
|
||||
check("no engine: the one-tap way out is offered", (await toNatural.count()) === 1);
|
||||
await toNatural.click();
|
||||
await page.waitForTimeout(400);
|
||||
check(
|
||||
"no engine: the switch sticks",
|
||||
(await page.evaluate(() => localStorage.getItem("freezmo.podcast-natural.v1")))?.includes("natural"),
|
||||
String(await page.evaluate(() => localStorage.getItem("freezmo.podcast-natural.v1"))),
|
||||
);
|
||||
await context.close();
|
||||
}
|
||||
|
||||
/* 4b3. natural voices: the provider TTS behind the same player ------- */
|
||||
{
|
||||
const { context, page } = await makePage(browser, { width: 390, height: 844, deviceScaleFactor: 1, isMobile: true, hasTouch: true });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue