Files
La-Fabrik/src/utils/dialogues/dialogueManifestValidation.ts
T
Tom Boullay 02c1fb33d0
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
feat(dialogues): support multi-cue subtitles
2026-05-30 04:00:25 +02:00

159 lines
4.2 KiB
TypeScript

import type {
DialogueDefinition,
DialogueManifest,
DialogueSpeaker,
DialogueVoice,
DialogueVoiceId,
} from "@/types/dialogues/dialogues";
const VALID_VOICE_IDS = new Set<DialogueVoiceId>([
"narrateur",
"fermier",
"electricienne",
]);
const VALID_SPEAKERS = new Set<DialogueSpeaker>([
"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<DialogueVoiceId>,
): 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<string, unknown> {
return typeof value === "object" && value !== null;
}