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
|
|
@ -261,6 +261,60 @@ export type HomeworkMakeRecord = Extract<HomeworkRecord, { mode: "make" }>;
|
|||
export type HomeworkGuideRecord = Extract<HomeworkRecord, { mode: "guide" }>;
|
||||
export type HomeworkCheckRecord = Extract<HomeworkRecord, { mode: "check" }>;
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Podcasts (notes turned into a spoken episode) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
export type PodcastSection = { heading: string; text: string };
|
||||
|
||||
/** As produced by the model (no id yet). */
|
||||
export const generatedPodcastSchema = z.object({
|
||||
title: z.string().min(1).max(200),
|
||||
language: z.string().max(60).optional(),
|
||||
summary: z.string().max(400).optional(),
|
||||
sections: z
|
||||
.array(
|
||||
z.object({
|
||||
heading: z.string().min(1).max(120),
|
||||
text: z.string().min(1).max(4000),
|
||||
}),
|
||||
)
|
||||
.min(1)
|
||||
.max(12),
|
||||
});
|
||||
export type GeneratedPodcast = z.infer<typeof generatedPodcastSchema>;
|
||||
|
||||
/** Stored episode: the script plus where it came from and where the listener
|
||||
* got to (so reopening it resumes, Spotify-style). */
|
||||
export type Podcast = {
|
||||
id: string;
|
||||
title: string;
|
||||
summary?: string;
|
||||
language?: string;
|
||||
sourceText: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
sections: PodcastSection[];
|
||||
progress?: { section: number; word: number; at: number };
|
||||
};
|
||||
|
||||
export const podcastSchema: z.ZodType<Podcast> = z.object({
|
||||
id: z.string().min(1).max(80),
|
||||
title: z.string().min(1).max(200),
|
||||
summary: z.string().max(400).optional(),
|
||||
language: z.string().max(60).optional(),
|
||||
sourceText: z.string().max(40_000),
|
||||
createdAt: z.number(),
|
||||
updatedAt: z.number(),
|
||||
sections: z
|
||||
.array(z.object({ heading: z.string().min(1).max(120), text: z.string().min(1).max(4000) }))
|
||||
.min(1)
|
||||
.max(12),
|
||||
progress: z
|
||||
.object({ section: z.number().int().min(0), word: z.number().int().min(0), at: z.number() })
|
||||
.optional(),
|
||||
});
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Client-side deck types (activity + id/xp, plus storage metadata) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue