// The transcript→EDL engine. This is the heart of "edit by deleting text": // kept words collapse into contiguous source-time runs (clips); the EDL is their // left-packed (ripple) projection onto the timeline. Deleting a word splits/shrinks // the run; the timeline reflows with no gaps. export interface Word { id: string; text: string; // display text (trimmed) startMs: number; // source-relative endMs: number; deleted?: boolean; } export interface Clip { id: string; sourceInMs: number; sourceOutMs: number; wordIds: string[]; } export interface EDLItem extends Clip { timelineStartMs: number; durationMs: number; } /** Collapse contiguous kept words into clips; a deletion breaks the run. */ export function buildClips(words: Word[]): Clip[] { const clips: Clip[] = []; let cur: Clip | null = null; for (const w of words) { if (w.deleted) { cur = null; continue; } if (!cur) { cur = { id: "c_" + w.id, sourceInMs: w.startMs, sourceOutMs: w.endMs, wordIds: [w.id] }; clips.push(cur); } else { cur.sourceOutMs = w.endMs; cur.wordIds.push(w.id); } } return clips; } /** Left-pack (ripple) projection: each clip starts where the previous ended. */ export function buildEDL(words: Word[]): EDLItem[] { let t = 0; return buildClips(words).map((c) => { const durationMs = c.sourceOutMs - c.sourceInMs; const item: EDLItem = { ...c, timelineStartMs: t, durationMs }; t += durationMs; return item; }); } export const totalMs = (edl: EDLItem[]) => edl.reduce((s, i) => s + i.durationMs, 0); /** timeline ms → which clip + the equivalent source ms (for audio playback / seeking). */ export function timelineToSource(edl: EDLItem[], tlMs: number) { for (const it of edl) { if (tlMs >= it.timelineStartMs && tlMs < it.timelineStartMs + it.durationMs) { return { clip: it, sourceMs: it.sourceInMs + (tlMs - it.timelineStartMs) }; } } return null; } /** source ms of a word's start → its position on the (reflowed) timeline, or null if cut. */ export function wordTimelineStart(edl: EDLItem[], word: Word): number | null { for (const it of edl) { if (it.wordIds.includes(word.id)) { return it.timelineStartMs + (word.startMs - it.sourceInMs); } } return null; }