Spaces:
Sleeping
Sleeping
| // 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; | |
| } | |