feat(dialogues): support multi-cue subtitles
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
This commit is contained in:
@@ -6,6 +6,10 @@ import type {
|
||||
DialogueSpeaker,
|
||||
DialogueVoiceId,
|
||||
} from "@/types/dialogues/dialogues";
|
||||
import {
|
||||
getDialogueCueIndices,
|
||||
getDialogueFirstCueIndex,
|
||||
} from "@/types/dialogues/dialogues";
|
||||
import { loadDialogueManifest } from "@/utils/dialogues/loadDialogueManifest";
|
||||
import { playDialogueById } from "@/utils/dialogues/playDialogue";
|
||||
import { parseSrt } from "@/utils/subtitles/parseSrt";
|
||||
@@ -34,7 +38,7 @@ function getNextCueIndex(
|
||||
): number {
|
||||
const cueIndexes = manifest.dialogues
|
||||
.filter((dialogue) => dialogue.voice === voice)
|
||||
.map((dialogue) => dialogue.subtitleCueIndex);
|
||||
.flatMap((dialogue) => getDialogueCueIndices(dialogue));
|
||||
|
||||
return Math.max(0, ...cueIndexes) + 1;
|
||||
}
|
||||
@@ -93,12 +97,15 @@ async function createFrenchSrtCue(
|
||||
manifest: DialogueManifest,
|
||||
dialogue: DialogueDefinition,
|
||||
): Promise<void> {
|
||||
const firstCueIndex = getDialogueFirstCueIndex(dialogue);
|
||||
if (firstCueIndex === undefined) return;
|
||||
|
||||
const srtPath = getFrenchSrtPath(dialogue.voice);
|
||||
const response = await fetch(srtPath);
|
||||
const content = response.ok ? await response.text() : "";
|
||||
const nextContent = appendSrtCueIfMissing(
|
||||
content,
|
||||
dialogue.subtitleCueIndex,
|
||||
firstCueIndex,
|
||||
getVoiceSpeaker(manifest, dialogue.voice),
|
||||
);
|
||||
|
||||
@@ -122,7 +129,8 @@ function getManifestErrors(manifest: DialogueManifest | null): string[] {
|
||||
errors.push(`${label}: audio doit commencer par /sounds/dialogue/.`);
|
||||
}
|
||||
|
||||
if (!Number.isInteger(dialogue.subtitleCueIndex)) {
|
||||
const cueIndices = getDialogueCueIndices(dialogue);
|
||||
if (cueIndices.length === 0) {
|
||||
errors.push(`${label}: cue SRT invalide.`);
|
||||
}
|
||||
|
||||
@@ -160,9 +168,18 @@ function getPatchedDialogue(
|
||||
id: patch.id ?? dialogue.id,
|
||||
voice: patch.voice ?? dialogue.voice,
|
||||
audio: patch.audio ?? dialogue.audio,
|
||||
subtitleCueIndex: patch.subtitleCueIndex ?? dialogue.subtitleCueIndex,
|
||||
};
|
||||
|
||||
if (patch.subtitleCueIndex !== undefined) {
|
||||
nextDialogue.subtitleCueIndex = patch.subtitleCueIndex;
|
||||
} else if (dialogue.subtitleCueIndex !== undefined) {
|
||||
nextDialogue.subtitleCueIndex = dialogue.subtitleCueIndex;
|
||||
}
|
||||
|
||||
if (dialogue.subtitleCueIndices !== undefined) {
|
||||
nextDialogue.subtitleCueIndices = dialogue.subtitleCueIndices;
|
||||
}
|
||||
|
||||
if ("timecode" in patch) {
|
||||
if (patch.timecode !== undefined) nextDialogue.timecode = patch.timecode;
|
||||
} else if (dialogue.timecode !== undefined) {
|
||||
@@ -252,8 +269,9 @@ export function EditorDialogueManifestPanel(): React.JSX.Element {
|
||||
|
||||
try {
|
||||
await createFrenchSrtCue(nextManifest, dialogue);
|
||||
const cueIndex = getDialogueFirstCueIndex(dialogue) ?? "?";
|
||||
setStatus(
|
||||
`Nouveau dialogue ajoute avec cue FR ${dialogue.subtitleCueIndex}. Sauvegarde le manifeste pour le garder.`,
|
||||
`Nouveau dialogue ajoute avec cue FR ${cueIndex}. Sauvegarde le manifeste pour le garder.`,
|
||||
);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Erreur inconnue";
|
||||
@@ -333,12 +351,13 @@ export function EditorDialogueManifestPanel(): React.JSX.Element {
|
||||
async function handleCreateFrenchSrtCue(): Promise<void> {
|
||||
if (!manifest || !selectedDialogue) return;
|
||||
|
||||
const cueIndex = getDialogueFirstCueIndex(selectedDialogue) ?? "?";
|
||||
setIsCreatingSrtCue(true);
|
||||
setStatus(`Creation de la cue FR ${selectedDialogue.subtitleCueIndex}...`);
|
||||
setStatus(`Creation de la cue FR ${cueIndex}...`);
|
||||
|
||||
try {
|
||||
await createFrenchSrtCue(manifest, selectedDialogue);
|
||||
setStatus(`Cue FR ${selectedDialogue.subtitleCueIndex} prete.`);
|
||||
setStatus(`Cue FR ${cueIndex} prete.`);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Erreur inconnue";
|
||||
setStatus(message);
|
||||
@@ -478,7 +497,7 @@ export function EditorDialogueManifestPanel(): React.JSX.Element {
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
value={selectedDialogue.subtitleCueIndex}
|
||||
value={getDialogueFirstCueIndex(selectedDialogue) ?? ""}
|
||||
onChange={(event) =>
|
||||
updateSelectedDialogue({
|
||||
subtitleCueIndex: Math.max(1, Number(event.target.value)),
|
||||
|
||||
@@ -7,6 +7,10 @@ import type {
|
||||
DialogueSpeaker,
|
||||
DialogueVoiceId,
|
||||
} from "@/types/dialogues/dialogues";
|
||||
import {
|
||||
getDialogueCueIndices,
|
||||
getDialogueFirstCueIndex,
|
||||
} from "@/types/dialogues/dialogues";
|
||||
import { logger } from "@/utils/core/Logger";
|
||||
import { loadDialogueManifest } from "@/utils/dialogues/loadDialogueManifest";
|
||||
import {
|
||||
@@ -181,7 +185,7 @@ function getExpectedCueIndexes(
|
||||
voice: DialogueVoiceId,
|
||||
): number[] {
|
||||
return getExpectedDialogues(manifest, voice)
|
||||
.map((dialogue) => dialogue.subtitleCueIndex)
|
||||
.flatMap((dialogue) => getDialogueCueIndices(dialogue))
|
||||
.filter(
|
||||
(cueIndex, index, cueIndexes) => cueIndexes.indexOf(cueIndex) === index,
|
||||
)
|
||||
@@ -196,7 +200,11 @@ function getExpectedDialogues(
|
||||
|
||||
return [...manifest.dialogues]
|
||||
.filter((dialogue) => dialogue.voice === voice)
|
||||
.sort((a, b) => a.subtitleCueIndex - b.subtitleCueIndex);
|
||||
.sort((a, b) => {
|
||||
const aIndex = getDialogueFirstCueIndex(a) ?? 0;
|
||||
const bIndex = getDialogueFirstCueIndex(b) ?? 0;
|
||||
return aIndex - bIndex;
|
||||
});
|
||||
}
|
||||
|
||||
function findCueBlockRange(
|
||||
@@ -577,7 +585,7 @@ export function EditorSrtPanel(): React.JSX.Element {
|
||||
)}
|
||||
{expectedDialogues.map((dialogue) => (
|
||||
<option key={dialogue.id} value={dialogue.id}>
|
||||
Cue {dialogue.subtitleCueIndex} - {dialogue.id}
|
||||
Cue {getDialogueFirstCueIndex(dialogue) ?? "?"} - {dialogue.id}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
@@ -585,7 +593,7 @@ export function EditorSrtPanel(): React.JSX.Element {
|
||||
|
||||
{selectedDialogue && (
|
||||
<div className="editor-srt-audio-card">
|
||||
<span>Cue {selectedDialogue.subtitleCueIndex}</span>
|
||||
<span>Cue {getDialogueFirstCueIndex(selectedDialogue) ?? "?"}</span>
|
||||
<strong>{selectedDialogue.id}</strong>
|
||||
<audio
|
||||
key={selectedDialogue.audio}
|
||||
@@ -609,39 +617,52 @@ export function EditorSrtPanel(): React.JSX.Element {
|
||||
<div className="editor-srt-time-actions">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
handleSetCueTime(selectedDialogue.subtitleCueIndex, "start")
|
||||
disabled={
|
||||
getDialogueFirstCueIndex(selectedDialogue) === undefined
|
||||
}
|
||||
onClick={() => {
|
||||
const cueIndex = getDialogueFirstCueIndex(selectedDialogue);
|
||||
if (cueIndex !== undefined)
|
||||
handleSetCueTime(cueIndex, "start");
|
||||
}}
|
||||
>
|
||||
Set start
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
handleSetCueTime(selectedDialogue.subtitleCueIndex, "end")
|
||||
disabled={
|
||||
getDialogueFirstCueIndex(selectedDialogue) === undefined
|
||||
}
|
||||
onClick={() => {
|
||||
const cueIndex = getDialogueFirstCueIndex(selectedDialogue);
|
||||
if (cueIndex !== undefined) handleSetCueTime(cueIndex, "end");
|
||||
}}
|
||||
>
|
||||
Set end
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
handleNudgeCue(
|
||||
selectedDialogue.subtitleCueIndex,
|
||||
-CUE_NUDGE_SECONDS,
|
||||
)
|
||||
disabled={
|
||||
getDialogueFirstCueIndex(selectedDialogue) === undefined
|
||||
}
|
||||
onClick={() => {
|
||||
const cueIndex = getDialogueFirstCueIndex(selectedDialogue);
|
||||
if (cueIndex !== undefined)
|
||||
handleNudgeCue(cueIndex, -CUE_NUDGE_SECONDS);
|
||||
}}
|
||||
>
|
||||
-100ms
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
handleNudgeCue(
|
||||
selectedDialogue.subtitleCueIndex,
|
||||
CUE_NUDGE_SECONDS,
|
||||
)
|
||||
disabled={
|
||||
getDialogueFirstCueIndex(selectedDialogue) === undefined
|
||||
}
|
||||
onClick={() => {
|
||||
const cueIndex = getDialogueFirstCueIndex(selectedDialogue);
|
||||
if (cueIndex !== undefined)
|
||||
handleNudgeCue(cueIndex, CUE_NUDGE_SECONDS);
|
||||
}}
|
||||
>
|
||||
+100ms
|
||||
</button>
|
||||
@@ -649,9 +670,15 @@ export function EditorSrtPanel(): React.JSX.Element {
|
||||
<button
|
||||
className="editor-srt-jump-button"
|
||||
type="button"
|
||||
onClick={() => handleJumpToCue(selectedDialogue.subtitleCueIndex)}
|
||||
disabled={
|
||||
getDialogueFirstCueIndex(selectedDialogue) === undefined
|
||||
}
|
||||
onClick={() => {
|
||||
const cueIndex = getDialogueFirstCueIndex(selectedDialogue);
|
||||
if (cueIndex !== undefined) handleJumpToCue(cueIndex);
|
||||
}}
|
||||
>
|
||||
Aller a la cue {selectedDialogue.subtitleCueIndex}
|
||||
Aller a la cue {getDialogueFirstCueIndex(selectedDialogue) ?? "?"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user