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
|
|
@ -171,6 +171,69 @@ export function buildGenerationUserPrompt({
|
|||
return lines.filter(Boolean).join("\n");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Podcast mode */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* The episode is heard, not read: no markdown, no lists, no headings inside
|
||||
* the prose. Sections are chapters (shown in the player); the text of each one
|
||||
* is what the voice reads, so it has to survive being spoken aloud.
|
||||
*/
|
||||
export const PODCAST_SYSTEM_PROMPT = `You are Freezmo's podcast writer. You turn a student's own study notes into a short spoken episode — one friendly, curious host talking the listener through the material, the way a good explainer podcast does.
|
||||
|
||||
${LANGUAGE_RULES}
|
||||
|
||||
Hard rules:
|
||||
- Use ONLY information contained in the provided notes. Never invent facts, definitions, numbers, dates or formulas. If the notes are thin, the episode is shorter — never padded with outside knowledge.
|
||||
- Write for the ear. Short sentences. Natural spoken rhythm. Contractions are fine.
|
||||
- Plain prose only: no markdown, no bullet points, no headings inside a section's text, no stage directions, no speaker labels, no sound effects, no "in this episode" boilerplate beyond a short spoken greeting if it fits naturally.
|
||||
- No visual references ("as you can see", "the table above"), no page numbers, no "the notes" — talk to the listener about the subject itself.
|
||||
- Explain, don't just list: connect ideas, give the why, use one small analogy when the notes support it, and call back to earlier parts of the episode where it helps.
|
||||
- Do not read the notes back verbatim; rewrite them into spoken language while keeping every fact and number exactly as written.
|
||||
- Spell out anything that is painful to hear as symbols (say "for example", not "e.g."); keep formulas and units spoken naturally.
|
||||
- Cover the whole notes, in a sensible teaching order.
|
||||
|
||||
Required JSON shape:
|
||||
{
|
||||
"title": "short episode title, max 8 words, in the notes' language",
|
||||
"language": "English name of the language you wrote in",
|
||||
"summary": "one spoken-style sentence about the episode",
|
||||
"sections": [
|
||||
{ "heading": "2-4 word chapter label", "text": "60-140 words of narration" }
|
||||
]
|
||||
}`;
|
||||
|
||||
export function buildPodcastUserPrompt({
|
||||
notes,
|
||||
minutes,
|
||||
language,
|
||||
focus,
|
||||
}: {
|
||||
notes: string;
|
||||
/** Target episode length in minutes (words are estimated at ~150/min). */
|
||||
minutes: number;
|
||||
language?: string;
|
||||
focus?: string;
|
||||
}): string {
|
||||
const words = Math.round(minutes * 150);
|
||||
const sections = Math.max(3, Math.min(8, Math.round(minutes)));
|
||||
const lines = [
|
||||
`Write a ${minutes}-minute podcast episode (about ${words} words, ${sections} sections) from the notes below.`,
|
||||
language
|
||||
? `LANGUAGE: write everything in ${language}. Do not translate into English.`
|
||||
: "LANGUAGE: detect the language of the notes below and write everything in that same language.",
|
||||
focus ? `The listener especially wants the episode to cover: ${focus}` : "",
|
||||
"",
|
||||
"=== STUDENT NOTES (the only allowed source of facts) ===",
|
||||
notes.slice(0, 48_000),
|
||||
"=== END OF NOTES ===",
|
||||
"",
|
||||
"Respond with the JSON object only.",
|
||||
];
|
||||
return lines.filter(Boolean).join("\n");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Short-answer grading */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue