diff --git a/frontend/src/components/VttEditor/VttEditor.tsx b/frontend/src/components/VttEditor/VttEditor.tsx index 32f9d31..69b3cda 100644 --- a/frontend/src/components/VttEditor/VttEditor.tsx +++ b/frontend/src/components/VttEditor/VttEditor.tsx @@ -41,11 +41,12 @@ interface VttEditorProps { onChange: (content: string) => void; onCueSave?: (cueIndex: number, vttContent: string) => Promise; onCueInserted?: (insertedIndex: number, totalCues: number) => void; + onCueDeleted?: (deletedIndex: number, totalCuesAfterDelete: number) => void; title: string; readOnly?: boolean; } -export function VttEditor({ vttContent, onChange, onCueSave, onCueInserted, title, readOnly = false }: VttEditorProps) { +export function VttEditor({ vttContent, onChange, onCueSave, onCueInserted, onCueDeleted, title, readOnly = false }: VttEditorProps) { const [cues, setCues] = useState([]); const [errors, setErrors] = useState([]); const [editingCue, setEditingCue] = useState(null); @@ -170,6 +171,8 @@ export function VttEditor({ vttContent, onChange, onCueSave, onCueInserted, titl const updatedCues = cues.filter((_, i) => i !== index); const newVttContent = updateCuesLocal(updatedCues); setCueToDelete(null); + // Notify parent about deletion so it can queue TTS for shifted cues + onCueDeleted?.(index, updatedCues.length); // Save after delete - use index of deleted cue (will save full VTT) saveCue(index, newVttContent); }; diff --git a/frontend/src/routes/admin/QCDetail.tsx b/frontend/src/routes/admin/QCDetail.tsx index 2ade5f7..4e9d4b8 100644 --- a/frontend/src/routes/admin/QCDetail.tsx +++ b/frontend/src/routes/admin/QCDetail.tsx @@ -48,6 +48,12 @@ export function QCDetail() { totalCues: number; } | null>(null); + // Track AD cue deletions to queue TTS for shifted cues + const [lastDeletedAdCue, setLastDeletedAdCue] = useState<{ + deletedIndex: number; + totalCuesAfterDelete: number; + } | null>(null); + // Fetch VTT content for selected language const { data: vttContent, isLoading: vttLoading } = useJobVttContent(id!, selectedLanguage); @@ -104,6 +110,7 @@ export function QCDetail() { setPendingRegenerations([]); setPausePointsModified(false); setLastInsertedAdCue(null); + setLastDeletedAdCue(null); }, [selectedLanguage]); // Sync pending regenerations from server edit state @@ -235,6 +242,7 @@ export function QCDetail() { // Determine which cues need TTS regeneration let cueIndicesToRegenerate: number[]; + let actionDescription = 'saved'; if (lastInsertedAdCue && cueIndex === lastInsertedAdCue.insertIndex) { // This is the newly inserted cue being saved @@ -244,12 +252,27 @@ export function QCDetail() { (_, i) => lastInsertedAdCue.insertIndex + i ); setLastInsertedAdCue(null); // Clear insertion context + actionDescription = 'inserted'; + } else if (lastDeletedAdCue && cueIndex === lastDeletedAdCue.deletedIndex) { + // A cue was deleted - queue TTS for all cues that shifted (from deleted index to end) + // Note: cueIndex here points to the cue that now occupies the deleted position + cueIndicesToRegenerate = Array.from( + { length: lastDeletedAdCue.totalCuesAfterDelete - lastDeletedAdCue.deletedIndex }, + (_, i) => lastDeletedAdCue.deletedIndex + i + ); + setLastDeletedAdCue(null); // Clear deletion context + actionDescription = 'deleted'; } else { // Normal edit, just this cue cueIndicesToRegenerate = [cueIndex]; } - // Queue TTS regeneration for the cues + // Queue TTS regeneration for the cues (skip if no cues to regenerate, e.g., deleted last cue) + if (cueIndicesToRegenerate.length === 0) { + toast.toastOnly.success('AD cue deleted'); + return; + } + try { await queueTTSRegenerationMutation.mutateAsync({ jobId: id, @@ -262,7 +285,7 @@ export function QCDetail() { const cueCount = cueIndicesToRegenerate.length; if (cueCount > 1) { toast.toastOnly.success( - `AD cue inserted. Queued TTS for ${cueCount} cues (indices ${cueIndicesToRegenerate.join(', ')})` + `AD cue ${actionDescription}. Queued TTS for ${cueCount} cues (indices ${cueIndicesToRegenerate.join(', ')})` ); } else { toast.toastOnly.success(`AD cue ${cueIndex + 1} saved and queued for TTS`); @@ -278,6 +301,11 @@ export function QCDetail() { setLastInsertedAdCue({ insertIndex: insertedIndex, totalCues }); }; + // Handler for AD cue deletions - tracks context for TTS queueing + const handleAdCueDeleted = (deletedIndex: number, totalCuesAfterDelete: number) => { + setLastDeletedAdCue({ deletedIndex, totalCuesAfterDelete }); + }; + const handleApprove = async () => { if (!id) return; @@ -732,6 +760,7 @@ export function QCDetail() { onChange={handleAdChange} onCueSave={handleAdCueSave} onCueInserted={handleAdCueInserted} + onCueDeleted={handleAdCueDeleted} title={`Audio Description (${selectedLanguage.toUpperCase()})`} readOnly={isProcessing} />