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

When a new AD cue is inserted in the middle of existing cues, the system
now automatically queues TTS regeneration for the new cue AND all cues
that shifted positions. This ensures MP3 file indices stay synchronized
with VTT cue indices, preventing cues from being silently dropped during
re-render.

Changes:
- VttEditor: Add onCueInserted callback to notify parent of insertions
- QCDetail: Track insertion context and queue TTS for all shifted cues
- rerender_accessible_video: Add warning log when cue/MP3 count mismatch

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
michael 2026-01-12 14:24:36 -06:00
parent dab294f18a
commit 577ed44dab
3 changed files with 56 additions and 5 deletions

View file

@ -198,6 +198,17 @@ async def _async_rerender_accessible_video(
logger.info(f"Downloaded {len(ad_segments)} AD cue segments")
# Validate VTT cue count matches MP3 count
vtt_cues = VTTParser.parse(ad_vtt_content)
downloaded_indices = set(idx for idx, _ in ad_segments)
if len(vtt_cues) != len(ad_segments):
missing_indices = set(range(len(vtt_cues))) - downloaded_indices
logger.warning(
f"VTT cue count ({len(vtt_cues)}) does not match MP3 count ({len(ad_segments)}). "
f"Missing MP3s for cue indices: {sorted(missing_indices)}. "
f"This may happen when a new AD cue is inserted but TTS wasn't regenerated for shifted cues."
)
# 4. Build placements with adjusted pause points
method = lang_output.get("accessible_video_method", "pause_insert")
pause_points = edit_state.get("pause_points", [])

View file

@ -40,11 +40,12 @@ interface VttEditorProps {
vttContent: string;
onChange: (content: string) => void;
onCueSave?: (cueIndex: number, vttContent: string) => Promise<void>;
onCueInserted?: (insertedIndex: number, totalCues: number) => void;
title: string;
readOnly?: boolean;
}
export function VttEditor({ vttContent, onChange, onCueSave, title, readOnly = false }: VttEditorProps) {
export function VttEditor({ vttContent, onChange, onCueSave, onCueInserted, title, readOnly = false }: VttEditorProps) {
const [cues, setCues] = useState<VTTCue[]>([]);
const [errors, setErrors] = useState<string[]>([]);
const [editingCue, setEditingCue] = useState<number | null>(null);
@ -159,6 +160,8 @@ export function VttEditor({ vttContent, onChange, onCueSave, title, readOnly = f
updatedCues.splice(insertIndex, 0, newCue);
updateCuesLocal(updatedCues);
// Notify parent about insertion so it can queue TTS for shifted cues
onCueInserted?.(insertIndex, updatedCues.length);
// Auto-enter edit mode for the new cue
setEditingCue(insertIndex);
};

View file

@ -42,6 +42,12 @@ export function QCDetail() {
const [pendingRegenerations, setPendingRegenerations] = useState<number[]>([]);
const [pausePointsModified, setPausePointsModified] = useState(false);
// Track AD cue insertions to queue TTS for shifted cues
const [lastInsertedAdCue, setLastInsertedAdCue] = useState<{
insertIndex: number;
totalCues: number;
} | null>(null);
// Fetch VTT content for selected language
const { data: vttContent, isLoading: vttLoading } = useJobVttContent(id!, selectedLanguage);
@ -97,6 +103,7 @@ export function QCDetail() {
useEffect(() => {
setPendingRegenerations([]);
setPausePointsModified(false);
setLastInsertedAdCue(null);
}, [selectedLanguage]);
// Sync pending regenerations from server edit state
@ -226,22 +233,51 @@ export function QCDetail() {
}
});
// Queue TTS regeneration for this cue
// Determine which cues need TTS regeneration
let cueIndicesToRegenerate: number[];
if (lastInsertedAdCue && cueIndex === lastInsertedAdCue.insertIndex) {
// This is the newly inserted cue being saved
// Queue TTS for this cue AND all shifted cues (from insertIndex to end)
cueIndicesToRegenerate = Array.from(
{ length: lastInsertedAdCue.totalCues - lastInsertedAdCue.insertIndex },
(_, i) => lastInsertedAdCue.insertIndex + i
);
setLastInsertedAdCue(null); // Clear insertion context
} else {
// Normal edit, just this cue
cueIndicesToRegenerate = [cueIndex];
}
// Queue TTS regeneration for the cues
try {
await queueTTSRegenerationMutation.mutateAsync({
jobId: id,
language: selectedLanguage,
cueIndices: [cueIndex],
cueIndices: cueIndicesToRegenerate,
});
// Update local pending regenerations
setPendingRegenerations(prev => [...new Set([...prev, cueIndex])]);
toast.toastOnly.success(`AD cue ${cueIndex + 1} saved and queued for TTS`);
setPendingRegenerations(prev => [...new Set([...prev, ...cueIndicesToRegenerate])]);
const cueCount = cueIndicesToRegenerate.length;
if (cueCount > 1) {
toast.toastOnly.success(
`AD cue inserted. Queued TTS for ${cueCount} cues (indices ${cueIndicesToRegenerate.join(', ')})`
);
} else {
toast.toastOnly.success(`AD cue ${cueIndex + 1} saved and queued for TTS`);
}
} catch (queueError) {
console.error('Failed to queue TTS regeneration:', queueError);
toast.toastOnly.success(`AD cue ${cueIndex + 1} saved (TTS queue failed - queue manually)`);
}
};
// Handler for AD cue insertions - tracks context for TTS queueing
const handleAdCueInserted = (insertedIndex: number, totalCues: number) => {
setLastInsertedAdCue({ insertIndex: insertedIndex, totalCues });
};
const handleApprove = async () => {
if (!id) return;
@ -695,6 +731,7 @@ export function QCDetail() {
vttContent={adVtt}
onChange={handleAdChange}
onCueSave={handleAdCueSave}
onCueInserted={handleAdCueInserted}
title={`Audio Description (${selectedLanguage.toUpperCase()})`}
readOnly={isProcessing}
/>