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
|
|
@ -99,7 +99,7 @@ for (const viewport of [{ width: 390, height: 844 }, { width: 360, height: 740 }
|
|||
|
||||
await page.goto(`${BASE}/create`, { waitUntil: "networkidle" });
|
||||
await page.waitForTimeout(800);
|
||||
const switcher = page.getByRole("group", { name: "Study deck or homework" });
|
||||
const switcher = page.getByRole("group", { name: "Study deck, homework or podcast" });
|
||||
check("mode switch on the New deck screen", await switcher.isVisible());
|
||||
check("mode switch marks the deck side active", (await switcher.locator('[aria-current="page"]').innerText()) === "Study deck");
|
||||
await switcher.getByRole("link", { name: "Make my homework" }).click();
|
||||
|
|
@ -107,9 +107,9 @@ for (const viewport of [{ width: 390, height: 844 }, { width: 360, height: 740 }
|
|||
check("mode switch lands on the homework wizard", await page.getByRole("heading", { name: "The assignment" }).isVisible());
|
||||
check(
|
||||
"mode switch marks the homework side active",
|
||||
(await page.getByRole("group", { name: "Study deck or homework" }).locator('[aria-current="page"]').innerText()) === "Make my homework",
|
||||
(await page.getByRole("group", { name: "Study deck, homework or podcast" }).locator('[aria-current="page"]').innerText()) === "Make my homework",
|
||||
);
|
||||
await page.getByRole("group", { name: "Study deck or homework" }).getByRole("link", { name: "Study deck" }).click();
|
||||
await page.getByRole("group", { name: "Study deck, homework or podcast" }).getByRole("link", { name: "Study deck" }).click();
|
||||
await page.waitForTimeout(900);
|
||||
check("mode switch returns to the deck wizard", await page.getByRole("heading", { name: "Your notes" }).isVisible());
|
||||
await context.close();
|
||||
|
|
@ -125,7 +125,7 @@ for (const viewport of [{ width: 390, height: 844 }, { width: 360, height: 740 }
|
|||
const switcherTop = async () => {
|
||||
await page.waitForTimeout(700);
|
||||
return page.evaluate(() => {
|
||||
const switcher = document.querySelector('[role="group"][aria-label="Study deck or homework"]');
|
||||
const switcher = document.querySelector('[role="group"][aria-label="Study deck, homework or podcast"]');
|
||||
return switcher ? Math.round(switcher.getBoundingClientRect().top) : null;
|
||||
});
|
||||
};
|
||||
|
|
@ -170,18 +170,18 @@ for (const viewport of [{ width: 390, height: 844 }, { width: 360, height: 740 }
|
|||
await page.waitForTimeout(300);
|
||||
await page.getByRole("button", { name: "Next" }).click();
|
||||
await page.waitForTimeout(300);
|
||||
check("loading: mode switch is there before the run", await page.getByRole("group", { name: "Study deck or homework" }).isVisible());
|
||||
check("loading: mode switch is there before the run", await page.getByRole("group", { name: "Study deck, homework or podcast" }).isVisible());
|
||||
await page.getByRole("button", { name: "Freeze my deck" }).click();
|
||||
await page.waitForTimeout(800);
|
||||
check("loading: deck run reaches the loading screen", await page.getByRole("button", { name: "Cancel" }).isVisible());
|
||||
check("loading: deck run hides the mode switch", (await page.getByRole("group", { name: "Study deck or homework" }).count()) === 0);
|
||||
check("loading: deck run hides the mode switch", (await page.getByRole("group", { name: "Study deck, homework or podcast" }).count()) === 0);
|
||||
await page.getByRole("button", { name: "Cancel" }).click();
|
||||
await page.waitForTimeout(900);
|
||||
check(
|
||||
"loading: deck Cancel returns to the wizard (notice + retry)",
|
||||
(await page.getByRole("heading", { name: "Ready to freeze" }).isVisible()) && (await page.getByRole("button", { name: "Retry" }).isVisible()),
|
||||
);
|
||||
check("loading: mode switch returns after the cancel", await page.getByRole("group", { name: "Study deck or homework" }).isVisible());
|
||||
check("loading: mode switch returns after the cancel", await page.getByRole("group", { name: "Study deck, homework or podcast" }).isVisible());
|
||||
|
||||
await page.goto(`${BASE}/homework`, { waitUntil: "networkidle" });
|
||||
await page.waitForTimeout(700);
|
||||
|
|
@ -192,7 +192,7 @@ for (const viewport of [{ width: 390, height: 844 }, { width: 360, height: 740 }
|
|||
check("homework: AI disclaimer near the start cards", await page.getByText("Freezmo uses AI and can make mistakes").isVisible());
|
||||
await page.getByRole("button", { name: /Just make it/ }).click();
|
||||
await page.waitForTimeout(800);
|
||||
check("loading: homework run hides the mode switch", (await page.getByRole("group", { name: "Study deck or homework" }).count()) === 0);
|
||||
check("loading: homework run hides the mode switch", (await page.getByRole("group", { name: "Study deck, homework or podcast" }).count()) === 0);
|
||||
await page.getByRole("button", { name: "Cancel" }).click();
|
||||
await page.waitForTimeout(900);
|
||||
check("loading: homework Cancel returns to the wizard", await page.getByRole("heading", { name: "The assignment" }).isVisible());
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue