import type { DialogueDefinition, DialogueManifest, DialogueSpeaker, DialogueVoice, DialogueVoiceId, } from "@/types/dialogues/dialogues"; const VALID_VOICE_IDS = new Set([ "narrateur", "fermier", "electricienne", ]); const VALID_SPEAKERS = new Set([ "Narrateur", "Fermier", "Electricienne", ]); export function parseDialogueManifest(data: unknown): DialogueManifest { if (!isRecord(data)) { throw new Error("Dialogue manifest must be an object"); } if (data.version !== 1) { throw new Error("Unsupported dialogue manifest version"); } if (!Array.isArray(data.voices) || !Array.isArray(data.dialogues)) { throw new Error("Dialogue manifest requires voices and dialogues arrays"); } const voices = data.voices.map(parseDialogueVoice); const voiceIds = new Set(voices.map((voice) => voice.id)); const dialogues = data.dialogues.map((dialogue) => parseDialogueDefinition(dialogue, voiceIds), ); return { version: 1, voices, dialogues, }; } function parseDialogueVoice(data: unknown): DialogueVoice { if (!isRecord(data)) { throw new Error("Dialogue voice must be an object"); } if (!isDialogueVoiceId(data.id)) { throw new Error("Dialogue voice has an invalid id"); } if (!isDialogueSpeaker(data.speaker)) { throw new Error(`Dialogue voice ${data.id} has an invalid speaker`); } if (!isRecord(data.subtitles)) { throw new Error(`Dialogue voice ${data.id} must define subtitles`); } const subtitles: DialogueVoice["subtitles"] = {}; const frSubtitle = getOptionalPath(data.subtitles.fr); const enSubtitle = getOptionalPath(data.subtitles.en); if (frSubtitle) subtitles.fr = frSubtitle; if (enSubtitle) subtitles.en = enSubtitle; return { id: data.id, speaker: data.speaker, subtitles, }; } function parseDialogueDefinition( data: unknown, voiceIds: Set, ): DialogueDefinition { if (!isRecord(data)) { throw new Error("Dialogue definition must be an object"); } if (typeof data.id !== "string" || data.id.length === 0) { throw new Error("Dialogue definition has an invalid id"); } if (!isDialogueVoiceId(data.voice) || !voiceIds.has(data.voice)) { throw new Error(`Dialogue ${data.id} references an unknown voice`); } if (typeof data.audio !== "string" || data.audio.length === 0) { throw new Error(`Dialogue ${data.id} has an invalid audio path`); } // Support both subtitleCueIndex (legacy) and subtitleCueIndices (new) const subtitleCueIndex = data.subtitleCueIndex; const subtitleCueIndices = data.subtitleCueIndices; const hasLegacyIndex = typeof subtitleCueIndex === "number" && Number.isInteger(subtitleCueIndex) && subtitleCueIndex >= 1; const hasNewIndices = Array.isArray(subtitleCueIndices) && subtitleCueIndices.length > 0 && subtitleCueIndices.every( (idx) => typeof idx === "number" && Number.isInteger(idx) && idx >= 1, ); if (!hasLegacyIndex && !hasNewIndices) { throw new Error( `Dialogue ${data.id} must have subtitleCueIndex or subtitleCueIndices`, ); } const timecode = data.timecode; if (timecode !== undefined && typeof timecode !== "number") { throw new Error(`Dialogue ${data.id} has an invalid timecode`); } const dialogue: DialogueDefinition = { id: data.id, voice: data.voice, audio: data.audio, }; if (hasNewIndices) { dialogue.subtitleCueIndices = subtitleCueIndices as number[]; } else if (hasLegacyIndex) { dialogue.subtitleCueIndex = subtitleCueIndex; } if (timecode !== undefined) dialogue.timecode = timecode; return dialogue; } function getOptionalPath(value: unknown): string | undefined { return typeof value === "string" && value.length > 0 ? value : undefined; } function isDialogueVoiceId(value: unknown): value is DialogueVoiceId { return ( typeof value === "string" && VALID_VOICE_IDS.has(value as DialogueVoiceId) ); } function isDialogueSpeaker(value: unknown): value is DialogueSpeaker { return ( typeof value === "string" && VALID_SPEAKERS.has(value as DialogueSpeaker) ); } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null; }