From daba532b5f8129bd64169c6bd2821c1fa5185b04 Mon Sep 17 00:00:00 2001 From: Tom Boullay Date: Mon, 11 May 2026 09:09:34 +0200 Subject: [PATCH] add: dev dialogue manisfest validation panel --- src/components/editor/EditorSrtPanel.tsx | 107 +++++++++++++++++++++++ src/index.css | 95 ++++++++++++++++++++ 2 files changed, 202 insertions(+) diff --git a/src/components/editor/EditorSrtPanel.tsx b/src/components/editor/EditorSrtPanel.tsx index 449ca89..88c5f15 100644 --- a/src/components/editor/EditorSrtPanel.tsx +++ b/src/components/editor/EditorSrtPanel.tsx @@ -26,6 +26,12 @@ interface TextRange { end: number; } +interface DialogueValidationResult { + valid: boolean; + errors: string[]; + warnings: string[]; +} + type CueTimeEdge = "start" | "end"; const CUE_NUDGE_SECONDS = 0.1; @@ -301,6 +307,37 @@ async function saveSrtFile( } } +async function validateDialogueAssets(): Promise { + const response = await fetch("/api/validate-dialogues"); + const body = (await response.json().catch(() => null)) as + | Partial + | { error?: string } + | null; + + if (!body) { + throw new Error("Validation dialogues impossible"); + } + + if ( + "valid" in body && + typeof body.valid === "boolean" && + Array.isArray(body.errors) && + Array.isArray(body.warnings) + ) { + return { + valid: body.valid, + errors: body.errors.filter((item) => typeof item === "string"), + warnings: body.warnings.filter((item) => typeof item === "string"), + }; + } + + throw new Error( + "error" in body && body.error + ? body.error + : "Validation dialogues impossible", + ); +} + export function EditorSrtPanel(): React.JSX.Element { const textareaRef = useRef(null); const [voice, setVoice] = useState("narrateur"); @@ -308,6 +345,9 @@ export function EditorSrtPanel(): React.JSX.Element { const [content, setContent] = useState(""); const [status, setStatus] = useState("Chargement du SRT..."); const [isSaving, setIsSaving] = useState(false); + const [isValidatingDialogues, setIsValidatingDialogues] = useState(false); + const [dialogueValidationResult, setDialogueValidationResult] = + useState(null); const [manifest, setManifest] = useState(null); const [audioCurrentTime, setAudioCurrentTime] = useState(0); const [selectedDialogueId, setSelectedDialogueId] = useState(""); @@ -323,6 +363,11 @@ export function EditorSrtPanel(): React.JSX.Element { ) ?? null; const diagnostic = getSrtDiagnostic(content, expectedCueIndexes); const isSrtValid = diagnostic.errors.length === 0; + const dialogueValidationClass = dialogueValidationResult + ? dialogueValidationResult.valid + ? "is-valid" + : "is-invalid" + : "is-idle"; const srtTemplate = createSrtTemplate( selectedVoice.label, expectedCueIndexes, @@ -352,6 +397,26 @@ export function EditorSrtPanel(): React.JSX.Element { } } + async function handleValidateDialogues(): Promise { + setIsValidatingDialogues(true); + setDialogueValidationResult(null); + + try { + const result = await validateDialogueAssets(); + setDialogueValidationResult(result); + setStatus( + result.valid + ? "Validation dialogues terminee." + : "Validation dialogues terminee avec erreurs.", + ); + } catch (err) { + const message = err instanceof Error ? err.message : "Erreur inconnue"; + setStatus(`${message}. Verifie que le serveur Vite est lance.`); + } finally { + setIsValidatingDialogues(false); + } + } + function handleJumpToCue(cueIndex: number): void { const range = findCueBlockRange(content, cueIndex); @@ -615,6 +680,48 @@ export function EditorSrtPanel(): React.JSX.Element {

{status}

+
+
+
+ Manifeste dialogues + Audio, SRT FR et cues references +
+ +
+ + {dialogueValidationResult && ( +
+

+ {dialogueValidationResult.valid + ? "Manifeste valide." + : `${dialogueValidationResult.errors.length} erreur${dialogueValidationResult.errors.length > 1 ? "s" : ""} detectee${dialogueValidationResult.errors.length > 1 ? "s" : ""}.`} + {dialogueValidationResult.warnings.length > 0 && + ` ${dialogueValidationResult.warnings.length} warning${dialogueValidationResult.warnings.length > 1 ? "s" : ""}.`} +

+ {dialogueValidationResult.errors.length > 0 && ( +
    + {dialogueValidationResult.errors.map((error, index) => ( +
  • {error}
  • + ))} +
+ )} + {dialogueValidationResult.warnings.length > 0 && ( +
    + {dialogueValidationResult.warnings.map((warning, index) => ( +
  • {warning}
  • + ))} +
+ )} +
+ )} +
diff --git a/src/index.css b/src/index.css index a34832b..6a55483 100644 --- a/src/index.css +++ b/src/index.css @@ -1588,6 +1588,101 @@ canvas { line-height: 1.4; } +.editor-dialogue-validation { + display: grid; + gap: 8px; + padding: 10px; + border: 1px solid #242424; + border-radius: 14px; + background: #101010; +} + +.editor-dialogue-validation.is-valid { + border-color: rgba(134, 239, 172, 0.32); +} + +.editor-dialogue-validation.is-invalid { + border-color: rgba(248, 113, 113, 0.38); +} + +.editor-dialogue-validation__heading { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} + +.editor-dialogue-validation__heading div { + display: grid; + gap: 2px; +} + +.editor-dialogue-validation__heading strong { + color: #f2f2f2; + font-size: 0.76rem; + font-weight: 800; +} + +.editor-dialogue-validation__heading span { + color: #8d8d8d; + font-size: 0.68rem; + line-height: 1.35; +} + +.editor-dialogue-validation__heading button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + min-width: 92px; + padding: 8px 9px; + border: 1px solid #2f2f2f; + border-radius: 12px; + background: #151515; + color: #f2f2f2; + cursor: pointer; + font-size: 0.72rem; + font-weight: 800; +} + +.editor-dialogue-validation__heading button:hover { + border-color: #ffffff; + background: #202020; +} + +.editor-dialogue-validation__heading button:disabled { + cursor: not-allowed; + opacity: 0.45; +} + +.editor-dialogue-validation__result { + display: grid; + gap: 6px; + font-size: 0.72rem; + line-height: 1.4; +} + +.editor-dialogue-validation__result p { + margin: 0; + color: #d7d7d7; +} + +.editor-dialogue-validation__errors, +.editor-dialogue-validation__warnings { + display: grid; + gap: 4px; + margin: 0; + padding-left: 16px; +} + +.editor-dialogue-validation__errors { + color: #fca5a5; +} + +.editor-dialogue-validation__warnings { + color: #fde68a; +} + .editor-srt-diagnostic { display: grid; gap: 6px;