Podcast mode: notes read aloud with a lyric-style word highlight
The third segment of the note-powered wizards (Study deck / Homework / Podcast) takes the same intake - photos, documents, pasted text, the share target - and turns it into a spoken episode. - prompts: PODCAST_SYSTEM_PROMPT writes for the ear (plain prose, no markdown, no visual references), chaptered into 3-8 sections around a length target. - /api/podcast mirrors /api/generate's plumbing (rate limit, model gate, provider keys, heartbeats, the 2x deadline, the optional vision pass). - storage: frezmo.podcasts.v1, zod-validated, included in the backup and the encrypted sync payload; a new library tab lists episodes with listen / rename / delete-with-undo, and the wizard switch deep-links in. - player: the transcript is word-wrapped spans and the word under the voice rides a lyric-style highlight that keeps itself centred. Two clocks: exact onboundary events where the browser reports them, a per-word length timer where it does not (iOS Safari); the first boundary retires the timer. Pause = cancel + re-speak from the word, which makes resume, chapter jumps and tap-any-word-to-seek the same operation; the position saves on pause, page-hide and unmount, so reopening resumes. Speed rebuilds the engine and keeps playing; the Aero soundtrack pauses itself when the voice starts; a browser with no voice gets an honest note and a readable transcript. - the script sits on a glass pane (plain paragraphs over Aero's grass failed contrast; the scan now covers the player too). tests/podcast-test.mjs (17th suite) stubs the model and the speech engine and drives the highlight by hand: 26 checks. All suites green, contrast clean. Version 0.7.3, SW v95.
This commit is contained in:
parent
84a94581dc
commit
d95c5c7151
30 changed files with 2575 additions and 26 deletions
116
lib/i18n-dict.ts
116
lib/i18n-dict.ts
|
|
@ -162,7 +162,7 @@ const en: Record<string, string> = {
|
|||
"settings.exportAll": "Export all data",
|
||||
"settings.exportNote": "Decks, homework, progress and settings — never your API key.",
|
||||
"settings.importBackup": "Import backup",
|
||||
"settings.importPreview": "This backup has {decks} decks, {homework} homework results, {folders} folders and {chats} chats.",
|
||||
"settings.importPreview": "This backup has {decks} decks, {homework} homework results, {podcasts} podcasts, {folders} folders and {chats} chats.",
|
||||
"settings.importMergeNote": "Merge keeps what's here and adds the backup; matching IDs are replaced.",
|
||||
"settings.importReplaceNote": "Replace wipes this browser's data and uses the backup instead.",
|
||||
"settings.importMerge": "Merge",
|
||||
|
|
@ -274,7 +274,61 @@ const en: Record<string, string> = {
|
|||
"homework.nav": "New homework",
|
||||
"wizard.mode.deck": "Study deck",
|
||||
"wizard.mode.homework": "Make my homework",
|
||||
"wizard.mode.aria": "Study deck or homework",
|
||||
"wizard.mode.podcast": "Podcast",
|
||||
"wizard.mode.aria": "Study deck, homework or podcast",
|
||||
|
||||
/* Podcast mode (notes → a spoken episode with a highlighted transcript) */
|
||||
"podcast.title": "New podcast",
|
||||
"podcast.subtitle": "Freezmo reads your notes back to you as a short episode — and highlights every word as it is spoken, like lyrics.",
|
||||
"podcast.addNotes": "Add your notes",
|
||||
"podcast.addPhotos": "Photos",
|
||||
"podcast.addDocs": "Documents",
|
||||
"podcast.addFirst": "Add notes first",
|
||||
"podcast.generate": "Create the episode",
|
||||
"podcast.lengthAria": "Episode length",
|
||||
"podcast.lengthHelp": "A rough target — the script stays as long as the notes need.",
|
||||
"podcast.focusLabel": "What should it focus on? (optional)",
|
||||
"podcast.minutes.one": "{n} minute",
|
||||
"podcast.minutes.few": "{n} minutes",
|
||||
"podcast.minutes.many": "{n} minutes",
|
||||
"podcast.sectionCount.one": "{n} section",
|
||||
"podcast.sectionCount.few": "{n} sections",
|
||||
"podcast.sectionCount.many": "{n} sections",
|
||||
"podcast.wordCount.one": "{n} word",
|
||||
"podcast.wordCount.few": "{n} words",
|
||||
"podcast.wordCount.many": "{n} words",
|
||||
"podcast.sectionOf": "Part {n} of {total}",
|
||||
"podcast.play": "Play the episode",
|
||||
"podcast.pause": "Pause the episode",
|
||||
"podcast.restart": "Start from the beginning",
|
||||
"podcast.speedAria": "Playback speed",
|
||||
"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.",
|
||||
"podcast.loadingEpisode": "Loading the episode",
|
||||
"podcast.notFound": "Episode not found",
|
||||
"podcast.notFoundBody": "It was probably deleted, or it lives in another browser's storage.",
|
||||
"podcast.loading.read1": "Reading your notes…",
|
||||
"podcast.loading.read2": "Making sense of the pages…",
|
||||
"podcast.loading.write1": "Writing the script…",
|
||||
"podcast.loading.write2": "Finding the through-line…",
|
||||
"podcast.loading.write3": "Polishing the spoken bits…",
|
||||
"podcast.tip1": "The transcript highlights every word as it is read — tap any word to jump there.",
|
||||
"podcast.tip2": "A four-minute walk is a great length for one subject.",
|
||||
|
||||
/* Library: the podcasts tab */
|
||||
"tabPodcasts": "Podcasts",
|
||||
"podcast.library.empty": "No episodes yet",
|
||||
"podcast.library.emptyBody": "Turn a page of notes into a spoken episode and it will wait here.",
|
||||
"podcast.library.new": "New podcast",
|
||||
"podcast.card.sections": "{n} parts",
|
||||
"podcast.card.minutes": "~{n} min",
|
||||
"podcast.card.actions": "Episode actions",
|
||||
"podcast.card.open": "Listen",
|
||||
"podcast.card.rename": "Rename",
|
||||
"podcast.card.delete": "Delete episode",
|
||||
"podcast.card.deleted": "Episode deleted",
|
||||
"podcast.card.renamed": "Episode renamed",
|
||||
"podcast.regenerate": "Write it again",
|
||||
"homeworkHeroTitle": "Make my homework",
|
||||
"homework.subtitle": "Paste it, photograph it or add a file — then pick how I should help.",
|
||||
"homework.phone.assignment": "The assignment",
|
||||
|
|
@ -1175,7 +1229,7 @@ const cs: Record<string, string> = {
|
|||
"settings.exportAll": "Exportovat všechna data",
|
||||
"settings.exportNote": "Sady, úkoly, pokrok a nastavení — nikdy tvůj API klíč.",
|
||||
"settings.importBackup": "Importovat zálohu",
|
||||
"settings.importPreview": "Záloha obsahuje {decks} sad, {homework} výsledků úkolů, {folders} složek a {chats} chatů.",
|
||||
"settings.importPreview": "Záloha obsahuje {decks} sad, {homework} výsledků úkolů, {podcasts} podcastů, {folders} složek a {chats} chatů.",
|
||||
"settings.importMergeNote": "Sloučení ponechá stávající data a přidá zálohu; stejná ID se nahradí.",
|
||||
"settings.importReplaceNote": "Nahrazení smaže data v tomto prohlížeči a použije zálohu.",
|
||||
"settings.importMerge": "Sloučit",
|
||||
|
|
@ -1287,7 +1341,61 @@ const cs: Record<string, string> = {
|
|||
"homework.nav": "Nový úkol",
|
||||
"wizard.mode.deck": "Studijní sada",
|
||||
"wizard.mode.homework": "Udělej mi úkol",
|
||||
"wizard.mode.aria": "Studijní sada nebo úkol",
|
||||
"wizard.mode.podcast": "Podcast",
|
||||
"wizard.mode.aria": "Studijní sada, úkol nebo podcast",
|
||||
|
||||
/* Podcast mode (notes → a spoken episode with a highlighted transcript) */
|
||||
"podcast.title": "Nový podcast",
|
||||
"podcast.subtitle": "Freezmo ti tvoje poznámky přečte jako krátkou epizodu — a každé slovo při čtení zvýrazní, jako texty písní.",
|
||||
"podcast.addNotes": "Přidej poznámky",
|
||||
"podcast.addPhotos": "Fotky",
|
||||
"podcast.addDocs": "Dokumenty",
|
||||
"podcast.addFirst": "Nejdřív přidej poznámky",
|
||||
"podcast.generate": "Vytvořit epizodu",
|
||||
"podcast.lengthAria": "Délka epizody",
|
||||
"podcast.lengthHelp": "Jen orientační cíl — scénář zůstane tak dlouhý, jak poznámky potřebují.",
|
||||
"podcast.focusLabel": "Na co se má zaměřit? (nepovinné)",
|
||||
"podcast.minutes.one": "{n} minuta",
|
||||
"podcast.minutes.few": "{n} minuty",
|
||||
"podcast.minutes.many": "{n} minut",
|
||||
"podcast.sectionCount.one": "{n} část",
|
||||
"podcast.sectionCount.few": "{n} části",
|
||||
"podcast.sectionCount.many": "{n} částí",
|
||||
"podcast.wordCount.one": "{n} slovo",
|
||||
"podcast.wordCount.few": "{n} slova",
|
||||
"podcast.wordCount.many": "{n} slov",
|
||||
"podcast.sectionOf": "Část {n} z {total}",
|
||||
"podcast.play": "Přehrát epizodu",
|
||||
"podcast.pause": "Pozastavit epizodu",
|
||||
"podcast.restart": "Začít od začátku",
|
||||
"podcast.speedAria": "Rychlost přehrávání",
|
||||
"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.",
|
||||
"podcast.loadingEpisode": "Načítání epizody",
|
||||
"podcast.notFound": "Epizoda nenalezena",
|
||||
"podcast.notFoundBody": "Nejspíš byla smazaná, nebo je uložená v jiném prohlížeči.",
|
||||
"podcast.loading.read1": "Čtu tvoje poznámky…",
|
||||
"podcast.loading.read2": "Dávám stránky dohromady…",
|
||||
"podcast.loading.write1": "Píšu scénář…",
|
||||
"podcast.loading.write2": "Hledám hlavní linii…",
|
||||
"podcast.loading.write3": "Ladím mluvené pasáže…",
|
||||
"podcast.tip1": "Přepis zvýrazňuje každé čtené slovo — kliknutím na slovo se tam přesuneš.",
|
||||
"podcast.tip2": "Čtyřminutová procházka je pro jedno téma tak akorát.",
|
||||
|
||||
/* Library: the podcasts tab */
|
||||
"tabPodcasts": "Podcasty",
|
||||
"podcast.library.empty": "Zatím žádné epizody",
|
||||
"podcast.library.emptyBody": "Proměň stránku poznámek v mluvenou epizodu a počká tady na tebe.",
|
||||
"podcast.library.new": "Nový podcast",
|
||||
"podcast.card.sections": "{n} částí",
|
||||
"podcast.card.minutes": "~{n} min",
|
||||
"podcast.card.actions": "Akce epizody",
|
||||
"podcast.card.open": "Poslouchat",
|
||||
"podcast.card.rename": "Přejmenovat",
|
||||
"podcast.card.delete": "Smazat epizodu",
|
||||
"podcast.card.deleted": "Epizoda smazána",
|
||||
"podcast.card.renamed": "Epizoda přejmenována",
|
||||
"podcast.regenerate": "Napsat znovu",
|
||||
"homeworkHeroTitle": "Udělej mi úkol",
|
||||
"homework.subtitle": "Vlož ho, vyfoť nebo přidej soubor — a vyber, jak ti mám pomoct.",
|
||||
"homework.phone.assignment": "Zadání",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue