fix: queue TTS regeneration for shifted cues when deleting AD cue

When an AD cue is deleted, all subsequent cues shift positions but their
MP3 files remain at the old indices. This adds handling to automatically
queue TTS regeneration for all cues that shifted after a deletion.

Changes:
- VttEditor: Add onCueDeleted callback to notify parent of deletions
- QCDetail: Track deletion context and queue TTS for all shifted cues

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
michael 2026-01-12 14:29:25 -06:00
parent 577ed44dab
commit df721850e0
2 changed files with 35 additions and 3 deletions

View file

@ -41,11 +41,12 @@ interface VttEditorProps {
onChange: (content: string) => void;
onCueSave?: (cueIndex: number, vttContent: string) => Promise<void>;
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<VTTCue[]>([]);
const [errors, setErrors] = useState<string[]>([]);
const [editingCue, setEditingCue] = useState<number | null>(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);
};

View file

@ -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}
/>