From 7aafc4da5f97389576d10b87e36952a572b15b82 Mon Sep 17 00:00:00 2001 From: Tom Boullay Date: Sun, 10 May 2026 00:25:45 +0100 Subject: [PATCH] update: validation/errors srt --- src/components/editor/EditorSrtPanel.tsx | 89 +++++++++++++++++++++++- src/index.css | 33 +++++++++ 2 files changed, 121 insertions(+), 1 deletion(-) diff --git a/src/components/editor/EditorSrtPanel.tsx b/src/components/editor/EditorSrtPanel.tsx index b031a20..e0d61aa 100644 --- a/src/components/editor/EditorSrtPanel.tsx +++ b/src/components/editor/EditorSrtPanel.tsx @@ -5,12 +5,18 @@ import type { DialogueSpeaker, DialogueVoiceId, } from "@/types/dialogues/dialogues"; +import { parseSrt } from "@/utils/subtitles/parseSrt"; interface SrtVoiceOption { id: DialogueVoiceId; label: DialogueSpeaker; } +interface SrtDiagnostic { + cueCount: number; + errors: string[]; +} + const SRT_VOICES: SrtVoiceOption[] = [ { id: "narrateur", label: "Narrateur" }, { id: "fermier", label: "Fermier" }, @@ -22,6 +28,8 @@ const DEFAULT_SRT_VOICE: SrtVoiceOption = { }; const SRT_LANGUAGES: SubtitleLanguage[] = ["fr", "en"]; +const SRT_TIME_LINE_PATTERN = + /^\d{2}:\d{2}:\d{2},\d{3} --> \d{2}:\d{2}:\d{2},\d{3}$/; function getSrtPath( voice: DialogueVoiceId, @@ -34,6 +42,62 @@ function createEmptySrtTemplate(speaker: DialogueSpeaker): string { return `1\n00:00:00,000 --> 00:00:02,000\n${speaker}: Nouveau sous-titre\n`; } +function getSrtDiagnostic(content: string): SrtDiagnostic { + const normalizedContent = content.replace(/^\uFEFF/, "").replace(/\r/g, ""); + const blocks = normalizedContent + .trim() + .split(/\n{2,}/) + .filter(Boolean); + const cues = parseSrt(content); + const errors: string[] = []; + const indexes = new Set(); + + if (blocks.length === 0) { + errors.push("Le fichier SRT est vide."); + } + + blocks.forEach((block, blockIndex) => { + const lines = block + .split("\n") + .map((line) => line.trim()) + .filter(Boolean); + const displayIndex = blockIndex + 1; + const cueIndex = Number(lines[0]); + + if (lines.length < 3) { + errors.push( + `Bloc ${displayIndex}: il manque un index, un timecode ou un texte.`, + ); + return; + } + + if (!Number.isInteger(cueIndex)) { + errors.push(`Bloc ${displayIndex}: l'index doit etre un nombre entier.`); + } else if (indexes.has(cueIndex)) { + errors.push(`Bloc ${displayIndex}: l'index ${cueIndex} est duplique.`); + } else { + indexes.add(cueIndex); + } + + if (!SRT_TIME_LINE_PATTERN.test(lines[1] ?? "")) { + errors.push( + `Bloc ${displayIndex}: le timecode doit utiliser HH:MM:SS,mmm --> HH:MM:SS,mmm.`, + ); + } + }); + + if (blocks.length > 0 && cues.length !== blocks.length) { + errors.push( + "Un ou plusieurs blocs ont une duree invalide ou un timecode illisible.", + ); + } + + return { + cueCount: cues.length, + errors, + }; +} + function downloadSrtFile( voice: DialogueVoiceId, language: SubtitleLanguage, @@ -75,8 +139,15 @@ export function EditorSrtPanel(): React.JSX.Element { const [isSaving, setIsSaving] = useState(false); const selectedVoice = SRT_VOICES.find((item) => item.id === voice) ?? DEFAULT_SRT_VOICE; + const diagnostic = getSrtDiagnostic(content); + const isSrtValid = diagnostic.errors.length === 0; async function handleSave(): Promise { + if (!isSrtValid) { + setStatus("Corrige les erreurs SRT avant de sauvegarder."); + return; + } + setIsSaving(true); setStatus("Sauvegarde du SRT..."); @@ -183,7 +254,7 @@ export function EditorSrtPanel(): React.JSX.Element {