update: validation/errors srt
This commit is contained in:
@@ -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<number>();
|
||||
|
||||
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<void> {
|
||||
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 {
|
||||
<button
|
||||
className="editor-action-button editor-action-button-primary"
|
||||
type="button"
|
||||
disabled={isSaving}
|
||||
disabled={isSaving || !isSrtValid}
|
||||
onClick={() => void handleSave()}
|
||||
>
|
||||
<Save size={15} aria-hidden="true" />
|
||||
@@ -200,6 +271,22 @@ export function EditorSrtPanel(): React.JSX.Element {
|
||||
</div>
|
||||
|
||||
<p className="editor-srt-status">{status}</p>
|
||||
<div
|
||||
className={`editor-srt-diagnostic ${isSrtValid ? "is-valid" : "is-invalid"}`}
|
||||
>
|
||||
<strong>
|
||||
{isSrtValid
|
||||
? `${diagnostic.cueCount} cue${diagnostic.cueCount > 1 ? "s" : ""} valide${diagnostic.cueCount > 1 ? "s" : ""}`
|
||||
: `${diagnostic.errors.length} erreur${diagnostic.errors.length > 1 ? "s" : ""} SRT`}
|
||||
</strong>
|
||||
{!isSrtValid && (
|
||||
<ul>
|
||||
{diagnostic.errors.map((error) => (
|
||||
<li key={error}>{error}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1472,6 +1472,39 @@ canvas {
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.editor-srt-diagnostic {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
padding: 9px 10px;
|
||||
border: 1px solid #242424;
|
||||
border-radius: 12px;
|
||||
background: #101010;
|
||||
font-size: 0.72rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.editor-srt-diagnostic.is-valid {
|
||||
border-color: rgba(134, 239, 172, 0.32);
|
||||
color: #86efac;
|
||||
}
|
||||
|
||||
.editor-srt-diagnostic.is-invalid {
|
||||
border-color: rgba(248, 113, 113, 0.38);
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
.editor-srt-diagnostic strong {
|
||||
font-size: 0.74rem;
|
||||
}
|
||||
|
||||
.editor-srt-diagnostic ul {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
margin: 0;
|
||||
padding-left: 16px;
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
/* Editor responsive layout */
|
||||
@media (max-width: 768px) {
|
||||
.editor-error h2 {
|
||||
|
||||
Reference in New Issue
Block a user