Podcast voices: natural provider TTS, and system-voice hygiene
Two reports, one root: the platform's speech engine. "Loud cracking at the end of every sentence" is the browser/OS TTS popping between its internal sentence chunks (an idle cancel makes it worse). Hygiene for that path: never cancel when nothing is pending, normalise the spoken text (control characters, ellipses, dashes), and a 140ms beat between sections so speech never runs into the engine's tail. The real fix is a second voice source that does not use the platform engine: - lib/providers carries TTS profiles (OpenAI gpt-4o-mini-tts, Groq playai-tts, any OpenAI-compatible /audio/speech behind the custom provider), male voices first. - /api/tts proxies them with the same guards and the key still in headers. - the picker gains System / Natural, with the provider's voice list. - lib/podcast-audio plays one clip per section through a hidden <audio> element, measures the clip's real duration and gives each word a slice of it proportional to its spoken length - the highlight follows the audio exactly, and pause/seek/chapters become real audio operations. Clips cache per session, the next section generates while the current plays, and a voice change disposes the element and its blobs. Speed applies live (setRate) without dropping the cache. podcast-test stubs /api/tts with a real silent WAV and pins the path: the voice list, the request body, exactly one playing element, and the highlight riding currentTime. 43 checks. tests/lib.mjs launches Chromium with the autoplay policy relaxed (a real device has the listener's gesture). Version 0.7.6, SW v98.
This commit is contained in:
parent
9882d775e7
commit
8e1cd0855d
13 changed files with 997 additions and 63 deletions
|
|
@ -307,6 +307,14 @@ const en: Record<string, string> = {
|
|||
"podcast.voiceLocal": "installed",
|
||||
"podcast.voiceNote": "Voices come from your browser and system — pick the one that sounds best to you.",
|
||||
"podcast.voiceIos": "On iPhone, Safari offers web pages a single voice per language — the system's preferred one. For more (and better) voices: Settings → Accessibility → Spoken Content → Voices → your language, download a voice in Enhanced or Premium quality and make it the preferred one. Freezmo then reads with it.",
|
||||
"podcast.voiceSystem": "System voices",
|
||||
"podcast.voiceNatural": "Natural voice",
|
||||
"podcast.voiceMale": "male",
|
||||
"podcast.voiceFemale": "female",
|
||||
"podcast.voiceNaturalNote": "Generated by your own provider key — the words are timed to the audio, so the highlight still follows the voice.",
|
||||
"podcast.voiceNeedsKey": "Add an OpenAI or Groq key in Settings → Providers to use natural voices.",
|
||||
"podcast.voiceNoSystem": "This device reports no system voices. The natural voice works anywhere — or add a voice in your system settings.",
|
||||
"podcast.voiceGenerating": "Generating the voice…",
|
||||
"podcast.voiceRobotic": "Only the basic system voices are installed. For a natural host voice, add one in your system settings (Windows: Speech; macOS: Spoken Content; Chrome ships the Google voices).",
|
||||
"podcast.tapToJump": "Tap any word to jump there.",
|
||||
"podcast.noVoice": "This browser has no speech voice installed, so the transcript cannot be read aloud here. You can still read along.",
|
||||
|
|
@ -808,6 +816,8 @@ const en: Record<string, string> = {
|
|||
"api.streamLost": "The connection dropped mid-run. Check your connection and try again.",
|
||||
"api.almost": "Almost there…",
|
||||
"api.noDeck": "No deck came back from the server. Check your Ollama settings and try again.",
|
||||
"api.noPodcast": "No script came back from the server. Check the model setup and try again.",
|
||||
"api.noAudio": "The voice came back empty. Try another voice or provider.",
|
||||
"api.incomplete": "The server finished without sending a deck.",
|
||||
"api.noActivities": "The generated deck had no usable activities. Try again.",
|
||||
"api.gradingCancelled": "Grading was cancelled.",
|
||||
|
|
@ -822,6 +832,10 @@ const en: Record<string, string> = {
|
|||
"server.bodyNotJson": "Request body was not valid JSON.",
|
||||
"server.invalidRequest": "Invalid request: {detail}",
|
||||
"server.providerModelNeeded": "Pick the generation model in Settings → Providers.",
|
||||
"server.ttsNeedsProvider": "Natural voices come from a provider — add a provider key in Settings → Providers first.",
|
||||
"server.ttsUnsupported": "{provider} does not offer text-to-speech. OpenAI, Groq or a custom OpenAI-compatible server will.",
|
||||
"server.ttsFailed": "{provider} could not generate the voice for this part.",
|
||||
"server.ttsTimeout": "{provider} took too long to generate the voice.",
|
||||
"server.needContent": "Add at least one photo of your notes or paste some text to build a deck.",
|
||||
"server.needAssignment":
|
||||
"Add the assignment first — paste its text, photograph it, or add a document.",
|
||||
|
|
@ -1380,6 +1394,14 @@ const cs: Record<string, string> = {
|
|||
"podcast.voiceLocal": "nainstalovaný",
|
||||
"podcast.voiceNote": "Hlasy dodává prohlížeč a systém — vyber si ten, co zní nejlíp.",
|
||||
"podcast.voiceIos": "Na iPhonu nabízí Safari webovým stránkám jen jeden hlas na jazyk — ten preferovaný v systému. Pro další (a lepší) hlasy: Nastavení → Zpřístupnění → Mluvený obsah → Hlasy → tvůj jazyk, stáhni si hlas ve verzi Vylepšený nebo Prémiový a nastav ho jako preferovaný. Freezmo pak čte s ním.",
|
||||
"podcast.voiceSystem": "Systémové hlasy",
|
||||
"podcast.voiceNatural": "Přirozený hlas",
|
||||
"podcast.voiceMale": "mužský",
|
||||
"podcast.voiceFemale": "ženský",
|
||||
"podcast.voiceNaturalNote": "Generuje ho klíč tvého providera — slova jsou časovaná podle audia, takže zvýraznění drží s hlasem.",
|
||||
"podcast.voiceNeedsKey": "Pro přirozené hlasy si v Nastavení → Provideři přidej klíč OpenAI nebo Groq.",
|
||||
"podcast.voiceNoSystem": "Tohle zařízení nehlásí žádné systémové hlasy. Přirozený hlas funguje všude — nebo si hlas přidej v nastavení systému.",
|
||||
"podcast.voiceGenerating": "Generuji hlas…",
|
||||
"podcast.voiceRobotic": "Jsou nainstalované jen základní systémové hlasy. Pro přirozený hlas moderátora si nějaký přidej v nastavení systému (Windows: Řeč; macOS: Mluvený obsah; Chrome má hlasy Google).",
|
||||
"podcast.tapToJump": "Klikni na kterékoli slovo a přeskočíš tam.",
|
||||
"podcast.noVoice": "Tento prohlížeč nemá nainstalovaný hlas, takže se přepis nedá přečíst nahlas. Číst ho ale můžeš dál.",
|
||||
|
|
@ -1881,6 +1903,8 @@ const cs: Record<string, string> = {
|
|||
"api.streamLost": "Připojení se během generování přerušilo. Zkontrolujte připojení a zkuste to znovu.",
|
||||
"api.almost": "Už to bude…",
|
||||
"api.noDeck": "Ze serveru nepřišla žádná sada. Zkontroluj nastavení Ollamy a zkus to znovu.",
|
||||
"api.noPodcast": "Ze serveru nepřišel žádný scénář. Zkontroluj nastavení modelu a zkus to znovu.",
|
||||
"api.noAudio": "Hlas přišel prázdný. Zkus jiný hlas nebo providera.",
|
||||
"api.incomplete": "Server skončil, aniž by poslal sadu.",
|
||||
"api.noActivities": "Vygenerovaná sada neměla žádné použitelné aktivity. Zkus to znovu.",
|
||||
"api.gradingCancelled": "Známkování bylo zrušeno.",
|
||||
|
|
@ -1895,6 +1919,10 @@ const cs: Record<string, string> = {
|
|||
"server.bodyNotJson": "Tělo požadavku nebylo platné JSON.",
|
||||
"server.invalidRequest": "Neplatný požadavek: {detail}",
|
||||
"server.providerModelNeeded": "Vyberte model pro generování v Nastavení → Poskytovatelé.",
|
||||
"server.ttsNeedsProvider": "Přirozené hlasy dodává provider — nejdřív si v Nastavení → Provideři přidej klíč.",
|
||||
"server.ttsUnsupported": "{provider} text-to-speech neumí. Umí ho OpenAI, Groq nebo vlastní OpenAI-compatible server.",
|
||||
"server.ttsFailed": "{provider} nedokázal pro tuhle část vygenerovat hlas.",
|
||||
"server.ttsTimeout": "{provider} generoval hlas příliš dlouho.",
|
||||
"server.needContent": "Přidej aspoň jednu fotku poznámek nebo vlož text, ze kterého sada vznikne.",
|
||||
"server.needAssignment":
|
||||
"Nejdřív přidej zadání — vlož jeho text, vyfoť ho nebo přidej dokument.",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue