add: dev dialogue manisfest validation panel

This commit is contained in:
Tom Boullay
2026-05-11 09:09:34 +02:00
parent b088db2a8b
commit daba532b5f
2 changed files with 202 additions and 0 deletions
+107
View File
@@ -26,6 +26,12 @@ interface TextRange {
end: number; end: number;
} }
interface DialogueValidationResult {
valid: boolean;
errors: string[];
warnings: string[];
}
type CueTimeEdge = "start" | "end"; type CueTimeEdge = "start" | "end";
const CUE_NUDGE_SECONDS = 0.1; const CUE_NUDGE_SECONDS = 0.1;
@@ -301,6 +307,37 @@ async function saveSrtFile(
} }
} }
async function validateDialogueAssets(): Promise<DialogueValidationResult> {
const response = await fetch("/api/validate-dialogues");
const body = (await response.json().catch(() => null)) as
| Partial<DialogueValidationResult>
| { 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 { export function EditorSrtPanel(): React.JSX.Element {
const textareaRef = useRef<HTMLTextAreaElement>(null); const textareaRef = useRef<HTMLTextAreaElement>(null);
const [voice, setVoice] = useState<DialogueVoiceId>("narrateur"); const [voice, setVoice] = useState<DialogueVoiceId>("narrateur");
@@ -308,6 +345,9 @@ export function EditorSrtPanel(): React.JSX.Element {
const [content, setContent] = useState(""); const [content, setContent] = useState("");
const [status, setStatus] = useState("Chargement du SRT..."); const [status, setStatus] = useState("Chargement du SRT...");
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [isValidatingDialogues, setIsValidatingDialogues] = useState(false);
const [dialogueValidationResult, setDialogueValidationResult] =
useState<DialogueValidationResult | null>(null);
const [manifest, setManifest] = useState<DialogueManifest | null>(null); const [manifest, setManifest] = useState<DialogueManifest | null>(null);
const [audioCurrentTime, setAudioCurrentTime] = useState(0); const [audioCurrentTime, setAudioCurrentTime] = useState(0);
const [selectedDialogueId, setSelectedDialogueId] = useState(""); const [selectedDialogueId, setSelectedDialogueId] = useState("");
@@ -323,6 +363,11 @@ export function EditorSrtPanel(): React.JSX.Element {
) ?? null; ) ?? null;
const diagnostic = getSrtDiagnostic(content, expectedCueIndexes); const diagnostic = getSrtDiagnostic(content, expectedCueIndexes);
const isSrtValid = diagnostic.errors.length === 0; const isSrtValid = diagnostic.errors.length === 0;
const dialogueValidationClass = dialogueValidationResult
? dialogueValidationResult.valid
? "is-valid"
: "is-invalid"
: "is-idle";
const srtTemplate = createSrtTemplate( const srtTemplate = createSrtTemplate(
selectedVoice.label, selectedVoice.label,
expectedCueIndexes, expectedCueIndexes,
@@ -352,6 +397,26 @@ export function EditorSrtPanel(): React.JSX.Element {
} }
} }
async function handleValidateDialogues(): Promise<void> {
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 { function handleJumpToCue(cueIndex: number): void {
const range = findCueBlockRange(content, cueIndex); const range = findCueBlockRange(content, cueIndex);
@@ -615,6 +680,48 @@ export function EditorSrtPanel(): React.JSX.Element {
</div> </div>
<p className="editor-srt-status">{status}</p> <p className="editor-srt-status">{status}</p>
<div className={`editor-dialogue-validation ${dialogueValidationClass}`}>
<div className="editor-dialogue-validation__heading">
<div>
<strong>Manifeste dialogues</strong>
<span>Audio, SRT FR et cues references</span>
</div>
<button
type="button"
disabled={isValidatingDialogues}
onClick={() => void handleValidateDialogues()}
>
<RefreshCw size={14} aria-hidden="true" />
{isValidatingDialogues ? "Validation..." : "Validate"}
</button>
</div>
{dialogueValidationResult && (
<div className="editor-dialogue-validation__result">
<p>
{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" : ""}.`}
</p>
{dialogueValidationResult.errors.length > 0 && (
<ul className="editor-dialogue-validation__errors">
{dialogueValidationResult.errors.map((error, index) => (
<li key={`${error}-${index}`}>{error}</li>
))}
</ul>
)}
{dialogueValidationResult.warnings.length > 0 && (
<ul className="editor-dialogue-validation__warnings">
{dialogueValidationResult.warnings.map((warning, index) => (
<li key={`${warning}-${index}`}>{warning}</li>
))}
</ul>
)}
</div>
)}
</div>
<div <div
className={`editor-srt-diagnostic ${isSrtValid ? "is-valid" : "is-invalid"}`} className={`editor-srt-diagnostic ${isSrtValid ? "is-valid" : "is-invalid"}`}
> >
+95
View File
@@ -1588,6 +1588,101 @@ canvas {
line-height: 1.4; 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 { .editor-srt-diagnostic {
display: grid; display: grid;
gap: 6px; gap: 6px;