diff --git a/public/sounds/dialogue/dialogues.json b/public/sounds/dialogue/dialogues.json new file mode 100644 index 0000000..ada518e --- /dev/null +++ b/public/sounds/dialogue/dialogues.json @@ -0,0 +1,188 @@ +{ + "version": 1, + "voices": [ + { + "id": "narrateur", + "speaker": "Narrateur", + "subtitles": { + "fr": "/sounds/dialogue/subtitles/fr/narrateur.srt", + "en": "/sounds/dialogue/subtitles/en/narrateur.srt" + } + }, + { + "id": "fermier", + "speaker": "Fermier", + "subtitles": { + "fr": "/sounds/dialogue/subtitles/fr/fermier.srt", + "en": "/sounds/dialogue/subtitles/en/fermier.srt" + } + }, + { + "id": "leonie", + "speaker": "Leonie", + "subtitles": { + "fr": "/sounds/dialogue/subtitles/fr/leonie.srt", + "en": "/sounds/dialogue/subtitles/en/leonie.srt" + } + } + ], + "dialogues": [ + { + "id": "narrateur_bienvenueaaltera", + "voice": "narrateur", + "audio": "/sounds/dialogue/narrateur_bienvenueaaltera.mp3", + "subtitleCueIndex": 1, + "timecode": 0 + }, + { + "id": "narrateur_intro_prenom", + "voice": "narrateur", + "audio": "/sounds/dialogue/narrateur_intro_prenom.mp3", + "subtitleCueIndex": 2 + }, + { + "id": "narrateur_intro_apresprenom", + "voice": "narrateur", + "audio": "/sounds/dialogue/narrateur_intro_apresprenom.mp3", + "subtitleCueIndex": 3 + }, + { + "id": "narrateur_ordreebike", + "voice": "narrateur", + "audio": "/sounds/dialogue/narrateur_ordreebike.mp3", + "subtitleCueIndex": 4 + }, + { + "id": "narrateur_ebikecasse", + "voice": "narrateur", + "audio": "/sounds/dialogue/narrateur_ebikecassé.mp3", + "subtitleCueIndex": 5 + }, + { + "id": "narrateur_galetscan", + "voice": "narrateur", + "audio": "/sounds/dialogue/narrateur_galetscan.mp3", + "subtitleCueIndex": 6 + }, + { + "id": "narrateur_ebikerepare", + "voice": "narrateur", + "audio": "/sounds/dialogue/narrateur_ebikeréparé.mp3", + "subtitleCueIndex": 7 + }, + { + "id": "narrateur_ordredemandedelaide", + "voice": "narrateur", + "audio": "/sounds/dialogue/narrateur_ordredemandedelaide.mp3", + "subtitleCueIndex": 8 + }, + { + "id": "narrateur_coupureelec", + "voice": "narrateur", + "audio": "/sounds/dialogue/narrateur_coupureélec.mp3", + "subtitleCueIndex": 9 + }, + { + "id": "narrateur_poteaueleccasse", + "voice": "narrateur", + "audio": "/sounds/dialogue/narrateur_poteauéleccassé.mp3", + "subtitleCueIndex": 10 + }, + { + "id": "narrateur_courantrepare", + "voice": "narrateur", + "audio": "/sounds/dialogue/narrateur_courantréparé.mp3", + "subtitleCueIndex": 11 + }, + { + "id": "narrateur_routeversferme", + "voice": "narrateur", + "audio": "/sounds/dialogue/narrateur_routeversferme.mp3", + "subtitleCueIndex": 12 + }, + { + "id": "narrateur_arriveferme", + "voice": "narrateur", + "audio": "/sounds/dialogue/narrateur_arrivéferme.mp3", + "subtitleCueIndex": 13 + }, + { + "id": "narrateur_fouillelecentre", + "voice": "narrateur", + "audio": "/sounds/dialogue/narrateur_fouillelecentre.mp3", + "subtitleCueIndex": 14 + }, + { + "id": "narrateur_interactiontuyauxlac", + "voice": "narrateur", + "audio": "/sounds/dialogue/narrateur_interactiontuyauxlac.mp3", + "subtitleCueIndex": 15 + }, + { + "id": "narrateur_interactionrefroidisseur", + "voice": "narrateur", + "audio": "/sounds/dialogue/narrateur_interactionrefroidisseur.mp3", + "subtitleCueIndex": 16 + }, + { + "id": "narrateur_refroidisseurcasse", + "voice": "narrateur", + "audio": "/sounds/dialogue/narrateur_refroidisseurcassé.mp3", + "subtitleCueIndex": 17 + }, + { + "id": "narrateur_createurdepluiecree", + "voice": "narrateur", + "audio": "/sounds/dialogue/narrateur_createurdepluiecréé.mp3", + "subtitleCueIndex": 18 + }, + { + "id": "narrateur_remerciement", + "voice": "narrateur", + "audio": "/sounds/dialogue/narrateur_remerciement.mp3", + "subtitleCueIndex": 19 + }, + { + "id": "narrateur_bonnechance", + "voice": "narrateur", + "audio": "/sounds/dialogue/narrateur_bonnechance.mp3", + "subtitleCueIndex": 20 + }, + { + "id": "narrateur_presentationatelier", + "voice": "narrateur", + "audio": "/sounds/dialogue/narrateur_présentationatelier.mp3", + "subtitleCueIndex": 21 + }, + { + "id": "narrateur_presentationoutils", + "voice": "narrateur", + "audio": "/sounds/dialogue/narrateur_présentationoutils.mp3", + "subtitleCueIndex": 22 + }, + { + "id": "narrateur_histoireleonie", + "voice": "narrateur", + "audio": "/sounds/dialogue/narrateur_histoireleonie.mp3", + "subtitleCueIndex": 23 + }, + { + "id": "fermier_coupdemain", + "voice": "fermier", + "audio": "/sounds/dialogue/fermier_coupdemain.mp3", + "subtitleCueIndex": 1 + }, + { + "id": "fermier_coupdemain_2", + "voice": "fermier", + "audio": "/sounds/dialogue/fermier_coupdemain_2.mp3", + "subtitleCueIndex": 2 + }, + { + "id": "fermier_findemission", + "voice": "fermier", + "audio": "/sounds/dialogue/fermier_findemission.mp3", + "subtitleCueIndex": 3 + } + ] +} diff --git a/src/types/dialogues/dialogues.ts b/src/types/dialogues/dialogues.ts new file mode 100644 index 0000000..33a1b48 --- /dev/null +++ b/src/types/dialogues/dialogues.ts @@ -0,0 +1,24 @@ +import type { SubtitleLanguage } from "@/managers/stores/useSettingsStore"; + +export type DialogueVoiceId = "narrateur" | "fermier" | "leonie"; +export type DialogueSpeaker = "Narrateur" | "Fermier" | "Leonie"; + +export interface DialogueVoice { + id: DialogueVoiceId; + speaker: DialogueSpeaker; + subtitles: Partial>; +} + +export interface DialogueDefinition { + id: string; + voice: DialogueVoiceId; + audio: string; + subtitleCueIndex: number; + timecode?: number; +} + +export interface DialogueManifest { + version: 1; + voices: DialogueVoice[]; + dialogues: DialogueDefinition[]; +} diff --git a/src/utils/dialogues/dialogueManifestValidation.ts b/src/utils/dialogues/dialogueManifestValidation.ts new file mode 100644 index 0000000..e23954a --- /dev/null +++ b/src/utils/dialogues/dialogueManifestValidation.ts @@ -0,0 +1,140 @@ +import type { + DialogueDefinition, + DialogueManifest, + DialogueSpeaker, + DialogueVoice, + DialogueVoiceId, +} from "@/types/dialogues/dialogues"; + +const VALID_VOICE_IDS = new Set([ + "narrateur", + "fermier", + "leonie", +]); +const VALID_SPEAKERS = new Set([ + "Narrateur", + "Fermier", + "Leonie", +]); + +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`); + } + + const subtitleCueIndex = data.subtitleCueIndex; + if ( + typeof subtitleCueIndex !== "number" || + !Number.isInteger(subtitleCueIndex) || + subtitleCueIndex < 1 + ) { + throw new Error(`Dialogue ${data.id} has an invalid subtitle cue index`); + } + + 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, + 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; +}