import type { TalkingHeadProject, TalkingHeadSnapshot } from "./contracts.ts"; export interface MediaTimelineOperationInput { kind: "timeline"; segments: Array<{ id: string; sourcePath: string; sourceStartSeconds: number; sourceEndSeconds: number; }>; overlays: Array<{ id: string; sourcePath: string; outputStartSeconds: number; outputEndSeconds: number; sourceStartSeconds?: number; fit: "cover" | "contain"; audio: "keep-primary"; }>; musicTrack?: { sourcePath: string; sourceStartSeconds: number; sourceEndSeconds: number; outputStartSeconds: number; outputEndSeconds: number; playback: "trim" | "loop"; fadeInSeconds: number; fadeOutSeconds: number; ducking: "dialogue-sidechain"; mix: { method: "ebu-r128-dialogue-relative"; dialogueAnalysisPath: string; musicAnalysisPath: string; targetMusicBelowDialogueLu: number; maxTruePeakDbtp: number; }; }; } function seconds(milliseconds: number): number { return milliseconds / 1_000; } export function toMediaTimelineOperation( project: TalkingHeadProject, snapshot: TalkingHeadSnapshot, options: { allowDraft?: boolean } = {}, ): MediaTimelineOperationInput { if (snapshot.editorialStatus !== "content-reviewed" && !options.allowDraft) { throw new Error("A-roll has not completed full editorial review; use an explicitly labeled preview or submit an editorial plan"); } return { kind: "timeline", segments: snapshot.aroll.map((segment) => ({ id: segment.id, sourcePath: project.source.path, sourceStartSeconds: seconds(segment.sourceStartMs), sourceEndSeconds: seconds(segment.sourceEndMs), })), overlays: snapshot.broll.map((placement) => ({ id: placement.id, sourcePath: placement.assetPath, outputStartSeconds: seconds(placement.outputStartMs), outputEndSeconds: seconds(placement.outputEndMs), ...(placement.assetStartMs === undefined ? {} : { sourceStartSeconds: seconds(placement.assetStartMs) }), fit: placement.fit, audio: placement.audio, })), ...(snapshot.bgm ? { musicTrack: { sourcePath: snapshot.bgm.assetPath, sourceStartSeconds: seconds(snapshot.bgm.sourceStartMs), sourceEndSeconds: seconds(snapshot.bgm.sourceEndMs), outputStartSeconds: seconds(snapshot.bgm.outputStartMs), outputEndSeconds: seconds(snapshot.bgm.outputEndMs), playback: snapshot.bgm.playback, fadeInSeconds: seconds(snapshot.bgm.fadeInMs), fadeOutSeconds: seconds(snapshot.bgm.fadeOutMs), ducking: snapshot.bgm.ducking, mix: { method: snapshot.bgm.mix.method, dialogueAnalysisPath: snapshot.bgm.mix.dialogueAnalysisPath, musicAnalysisPath: snapshot.bgm.mix.musicAnalysisPath, targetMusicBelowDialogueLu: snapshot.bgm.mix.targetMusicBelowDialogueLu, maxTruePeakDbtp: snapshot.bgm.mix.maxTruePeakDbtp, }, }, } : {}), }; } export function timelineDelivery(project: TalkingHeadProject, snapshot: TalkingHeadSnapshot): { editorialStatus: NonNullable; listeningStatus: "pending"; mediaOperation?: MediaTimelineOperationInput; previewMediaOperation?: MediaTimelineOperationInput; } { const operation = toMediaTimelineOperation(project, snapshot, { allowDraft: true }); return { editorialStatus: snapshot.editorialStatus ?? "draft", listeningStatus: "pending" as const, ...(snapshot.editorialStatus === "content-reviewed" ? { mediaOperation: operation } : { previewMediaOperation: operation }), }; }