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
|
|
@ -43,6 +43,7 @@ const screens = [
|
|||
["chat", "/chat", null],
|
||||
["stats", "/stats", null],
|
||||
["exam", "/exam", null],
|
||||
["podcast", "/podcast/pod_contrast", null],
|
||||
["settings", "/", "settings"],
|
||||
["notfound", "/no-such-page", null],
|
||||
];
|
||||
|
|
@ -51,7 +52,27 @@ for (const [label, path, mode] of screens) {
|
|||
if (filter && !label.includes(filter)) continue;
|
||||
for (const theme of ["dark", "light", "aero"]) {
|
||||
const { context, page } = await makePage(browser, { width: 390, height: 844, deviceScaleFactor: 1 });
|
||||
await seed(page, { decks: [DECK], theme });
|
||||
await seed(page, {
|
||||
decks: [DECK],
|
||||
theme,
|
||||
extra: {
|
||||
"freezmo.podcasts.v1": JSON.stringify([
|
||||
{
|
||||
id: "pod_contrast",
|
||||
title: "Why the sky is blue",
|
||||
summary: "Rayleigh scattering, explained out loud.",
|
||||
language: "English",
|
||||
sourceText: "Rayleigh scattering makes the sky blue.",
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
sections: [
|
||||
{ heading: "Daylight", text: "Look up on a clear day. The sky is blue — a blue so obvious that it is easy to stop asking why." },
|
||||
{ heading: "Why", text: "Tiny molecules scatter short wavelengths far more than long ones." },
|
||||
],
|
||||
},
|
||||
]),
|
||||
},
|
||||
});
|
||||
await page.goto(`${BASE}${path}`, { waitUntil: "networkidle" });
|
||||
await page.waitForTimeout(1000);
|
||||
if (mode === "settings") {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue