add: audio preview

This commit is contained in:
Tom Boullay
2026-05-10 00:31:16 +01:00
parent 48f2c9ef80
commit f55be58c0b
2 changed files with 103 additions and 5 deletions
+51 -4
View File
@@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
import { Download, RefreshCw, Save } from "lucide-react"; import { Download, RefreshCw, Save } from "lucide-react";
import type { SubtitleLanguage } from "@/managers/stores/useSettingsStore"; import type { SubtitleLanguage } from "@/managers/stores/useSettingsStore";
import type { import type {
DialogueDefinition,
DialogueManifest, DialogueManifest,
DialogueSpeaker, DialogueSpeaker,
DialogueVoiceId, DialogueVoiceId,
@@ -144,10 +145,7 @@ function getExpectedCueIndexes(
manifest: DialogueManifest | null, manifest: DialogueManifest | null,
voice: DialogueVoiceId, voice: DialogueVoiceId,
): number[] { ): number[] {
if (!manifest) return []; return getExpectedDialogues(manifest, voice)
return manifest.dialogues
.filter((dialogue) => dialogue.voice === voice)
.map((dialogue) => dialogue.subtitleCueIndex) .map((dialogue) => dialogue.subtitleCueIndex)
.filter( .filter(
(cueIndex, index, cueIndexes) => cueIndexes.indexOf(cueIndex) === index, (cueIndex, index, cueIndexes) => cueIndexes.indexOf(cueIndex) === index,
@@ -155,6 +153,17 @@ function getExpectedCueIndexes(
.sort((a, b) => a - b); .sort((a, b) => a - b);
} }
function getExpectedDialogues(
manifest: DialogueManifest | null,
voice: DialogueVoiceId,
): DialogueDefinition[] {
if (!manifest) return [];
return [...manifest.dialogues]
.filter((dialogue) => dialogue.voice === voice)
.sort((a, b) => a.subtitleCueIndex - b.subtitleCueIndex);
}
function downloadSrtFile( function downloadSrtFile(
voice: DialogueVoiceId, voice: DialogueVoiceId,
language: SubtitleLanguage, language: SubtitleLanguage,
@@ -197,6 +206,7 @@ export function EditorSrtPanel(): React.JSX.Element {
const [manifest, setManifest] = useState<DialogueManifest | null>(null); const [manifest, setManifest] = useState<DialogueManifest | null>(null);
const selectedVoice = const selectedVoice =
SRT_VOICES.find((item) => item.id === voice) ?? DEFAULT_SRT_VOICE; SRT_VOICES.find((item) => item.id === voice) ?? DEFAULT_SRT_VOICE;
const expectedDialogues = getExpectedDialogues(manifest, voice);
const expectedCueIndexes = getExpectedCueIndexes(manifest, voice); const expectedCueIndexes = getExpectedCueIndexes(manifest, voice);
const diagnostic = getSrtDiagnostic(content, expectedCueIndexes); const diagnostic = getSrtDiagnostic(content, expectedCueIndexes);
const isSrtValid = diagnostic.errors.length === 0; const isSrtValid = diagnostic.errors.length === 0;
@@ -204,6 +214,11 @@ export function EditorSrtPanel(): React.JSX.Element {
selectedVoice.label, selectedVoice.label,
expectedCueIndexes, expectedCueIndexes,
); );
const [selectedDialogueId, setSelectedDialogueId] = useState("");
const selectedDialogue =
expectedDialogues.find((dialogue) => dialogue.id === selectedDialogueId) ??
expectedDialogues[0] ??
null;
async function handleSave(): Promise<void> { async function handleSave(): Promise<void> {
if (!isSrtValid) { if (!isSrtValid) {
@@ -310,6 +325,38 @@ export function EditorSrtPanel(): React.JSX.Element {
</label> </label>
</div> </div>
<div className="editor-srt-preview">
<label>
Dialogue audio
<select
value={selectedDialogue?.id ?? ""}
onChange={(event) => setSelectedDialogueId(event.target.value)}
disabled={expectedDialogues.length === 0}
>
{expectedDialogues.length === 0 && (
<option value="">Aucun dialogue</option>
)}
{expectedDialogues.map((dialogue) => (
<option key={dialogue.id} value={dialogue.id}>
Cue {dialogue.subtitleCueIndex} - {dialogue.id}
</option>
))}
</select>
</label>
{selectedDialogue && (
<div className="editor-srt-audio-card">
<span>Cue {selectedDialogue.subtitleCueIndex}</span>
<strong>{selectedDialogue.id}</strong>
<audio
key={selectedDialogue.audio}
controls
src={selectedDialogue.audio}
/>
</div>
)}
</div>
<textarea <textarea
className="editor-srt-textarea" className="editor-srt-textarea"
value={content} value={content}
+52 -1
View File
@@ -1450,11 +1450,62 @@ canvas {
} }
.editor-srt-textarea:focus, .editor-srt-textarea:focus,
.editor-srt-controls select:focus { .editor-srt-controls select:focus,
.editor-srt-preview select:focus {
border-color: #ffffff; border-color: #ffffff;
outline: none; outline: none;
} }
.editor-srt-preview {
display: grid;
gap: 8px;
padding: 10px;
border: 1px solid #1f1f1f;
border-radius: 16px;
background: #070707;
}
.editor-srt-preview label {
display: grid;
gap: 5px;
color: #8d8d8d;
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.editor-srt-preview select {
width: 100%;
padding: 9px 10px;
border: 1px solid #242424;
border-radius: 12px;
background: #101010;
color: #f2f2f2;
}
.editor-srt-audio-card {
display: grid;
gap: 6px;
color: #f2f2f2;
}
.editor-srt-audio-card span {
color: #8d8d8d;
font-size: 0.72rem;
}
.editor-srt-audio-card strong {
font-size: 0.78rem;
line-height: 1.3;
word-break: break-word;
}
.editor-srt-audio-card audio {
width: 100%;
height: 34px;
}
.editor-srt-actions { .editor-srt-actions {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);