add: dev dialogue manisfest validation panel
This commit is contained in:
@@ -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"}`}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user