When the user edits the audio description VTT and saves, the system now:
1. Compares original vs current AD cue text
2. Identifies which cues were modified
3. Automatically queues TTS regeneration for modified cues
4. Updates the Render Changes panel to show queued regenerations
This enables the "Render Changes" button when AD script edits are saved,
ensuring the accessible video can be re-rendered with updated TTS audio.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Reorder workflow: translations now happen BEFORE QC Review step
- Add language tabs to switch between translated languages in QC
- Add video mode tabs (Original Video / Accessible Video)
- Add interactive timeline preview showing video segments and AD cues
- Enable pause point adjustment with millisecond precision
- Add TTS regeneration queue for selective cue re-synthesis
- Add re-render controls with optional Whisper refinement
- Persist video segments and TTS MP3s to GCS for editability
- Add new RENDERING_QC job status for re-render operations
- Create 5 new API endpoints for accessible video editing
- Add rerender_accessible_video.py Celery task
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When TTS synthesis fails after 3 retries, the system now:
- Sends problematic cue text to Gemini for TTS-safe rewriting
- Updates the VTT file in GCS with rewritten text
- Retries TTS synthesis with the new text
- Records successful rewrites in job.tts_rewrites field
UI changes:
- JobDetail shows amber caution box with original/rewritten text
- JobsList shows warning icon next to jobs with rewrites
- Error display clarifies text shown is "after rewrite attempt"
Files changed:
- backend/app/models/job.py: Add tts_rewrites field
- backend/app/prompts/gemini_tts_rewrite.md: New prompt template
- backend/app/services/gemini.py: Add rewrite_tts_cue method
- backend/app/tasks/tts_synthesis.py: Add VTT update utilities
- backend/app/tasks/translate_and_synthesize.py: Rewrite+retry logic
- frontend/src/types/api.ts: Add TTSRewriteItem type
- frontend/src/routes/jobs/JobDetail.tsx: Caution display
- frontend/src/routes/jobs/JobsList.tsx: Warning indicator
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace sequential browser-based bulk download with server-side zip
generation. When users select "Download All Files" from bulk actions,
the system now creates a single organized .zip file containing all
job assets.
Changes:
- Add POST /jobs/bulk/download endpoint that streams zip to client
- Add BulkDownloadRequest schema for the new endpoint
- Create zip_download.py service with streaming zip generation
- Update frontend to call new endpoint and download single zip file
- Organize files in zip by job title and language subdirectories
Zip structure:
accessible_video_YYYYMMDD_HHMMSS.zip
└── {job_title}/
├── source.mp4
└── {lang}/
├── captions.vtt
├── ad.vtt
├── ad.mp3
├── accessible_video.mp4
└── accessible_captions.vtt
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Use typeof checks for proper type narrowing of unknown error fields
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add POST /jobs/{id}/actions/retry_tts endpoint for retrying TTS
- Frontend shows TTS-specific error details (cue index, blocked text)
- Add "Retry TTS Generation" button on failed jobs
- Guides users to edit problematic AD text before retrying
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The useMemo hook was placed after early returns (isLoading, error),
which violates React's rules of hooks. Hooks must be called
unconditionally on every render.
Replaced with simple inline computation since the operation is cheap.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Optimize the accessible video workflow by eliminating the dedicated
Gemini video analysis call for pause point estimation. Instead:
- Use AD VTT cue start times as initial pause points for Whisper refinement
- Add user-selectable accessible video method (pause_insert/overlay) at QC approval
- Add bulk approval API endpoint with method selection
- Add method selector UI to QCDetail page
- Add bulk approval modal to QCList for jobs with accessible video
Benefits:
- Eliminates expensive Gemini API call with video upload
- Faster workflow (~5-15 seconds saved per job)
- Cost savings on Gemini video analysis
- User control over accessible video integration method
Backend changes:
- Add accessible_video_method to RequestedOutputs and ApproveSourceRequest
- Add POST /jobs/bulk/approve endpoint
- Replace Gemini call with _build_placements_from_ad_vtt() helper
- Mark analyze_accessible_video_placement() as deprecated
Frontend changes:
- Add method selector radio buttons to QCDetail
- Add bulk approval modal with method selection to QCList
- Update API client and React Query hooks
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Remove the "Original Video Language" control from job upload form.
All videos now use AI auto-detection for source language, simplifying
the UX and eliminating potential for incorrect manual selection.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Use explicit null returns and String() casts for unknown types
from job.error Record to satisfy TypeScript strict mode.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add RENDER_FAILED job status for when video rendering fails
- Fix _check_accessible_video_completion to detect failures and transition
job status accordingly (was stuck in RENDERING_VIDEO forever)
- Store detailed error info in job.error including failed_languages array
- Call completion check after failures to properly update job status
- Broadcast WebSocket notification on render failures
Frontend:
- Add render_failed to JobStatus type and StatusBadge (red styling)
- Add tts_failed and render_failed to JobsList STATUS_LABELS
- Enhance JobDetail error display with:
- Warning icon and prominent styling
- Error type and message
- Failed languages list with per-language errors
- Timestamp of when error occurred
- Update ProgressIndicator to handle failed states with red dot
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Changed "Target Languages" to "Target Languages for Translation"
for clarity in the new job form.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The Languages column now shows total languages (source + translations)
instead of just translation count, matching other parts of the UI.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add a new "Video Native Mode" translation option that re-processes the
video through Gemini for each target language, generating captions and
audio descriptions directly from visual context. This produces more
natural and culturally appropriate content compared to traditional VTT
text translation.
Changes:
- Add translation_mode field to RequestedOutputs (video_native | traditional)
- Create gemini_ingestion_targeted.md prompt for target language generation
- Add extract_accessibility_targeted() method to Gemini service
- Modify translate_and_synthesize task to handle both translation modes
- Add Translation Mode UI selector in NewJob screen (video_native is default)
- Remove transcreation UI (replaced by video_native mode)
- Remove Google Translate service (replaced by Gemini translation)
- Add LanguageSelector component with searchable dropdown
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Deduplicate job IDs to prevent processing same job twice
- Convert GCS blob iterator to list upfront to avoid stale generations
- Clear blob.generation before delete to handle concurrent deletions
- Catch NotFound errors gracefully for already-deleted blobs
- Don't re-raise GCS errors - cleanup failures shouldn't block deletion
- Treat already-deleted jobs as successful (idempotent delete)
- Disable action dropdown during bulk operations in UI
- Show spinner with "Please wait" message during deletion
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add comprehensive error handling for TTS synthesis failures:
Backend:
- Add TTS_FAILED status to JobStatus enum for failed synthesis jobs
- Add TTSSynthesisError exception with cue index and context tracking
- Improve null-safe error handling in Gemini TTS response parsing
- Add _synthesize_cue_with_retry() with exponential backoff (3 attempts)
- Enhanced error logging with text preview and model context
Frontend:
- Add TTS_FAILED status styling (red badge) in StatusBadge component
- Add tts_failed to JobStatus TypeScript type
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The video event listener effect had an empty dependency array, causing it
to run only once on mount. For batch uploads where downloads load slower,
the video element wasn't rendered yet when the effect ran, so listeners
were never attached. Adding currentVideoUrl as a dependency ensures the
effect re-runs when the video becomes available.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Logs VTT content fetching, parsing, and current caption state
to help diagnose subtitle overlay issues in final review mode.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The "Download All Files" function was missing accessible_video_mp4 and
accessible_captions_vtt files that the backend provides. Updated both
the bulk download in JobsList and the individual Downloads page to
include all available file types.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Show contextual buttons when jobs are ready for review:
- QC Review button (blue) when status is pending_qc
- Final Review button (purple) when status is pending_final_review
Buttons appear dynamically via WebSocket updates without refresh.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Added header rows to clarify that "Original only" refers to
"Languages Requested" in both Pending and Completed sections.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Backend returns UTC dates without timezone indicator. Added
parseUTCDate helper that appends 'Z' to ensure JavaScript
correctly interprets dates as UTC and converts to local time.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Users can see the filename on the job detail page if needed.
Search still works on filename even though it's not displayed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add created_by_name field to JobResponse schema and API
- Batch-fetch user names in list_jobs endpoint for efficiency
- Convert JobsList from card layout to sortable data table
- Add search box (job name, filename, created by user)
- Add user filter dropdown (populated from current jobs)
- Add status filter dropdown (individual statuses from current jobs)
- Add date range filter (All Time, Last 7 Days, Last 30 Days)
- Add sortable columns: Job Name, Created By, Date Created, Status
- Fetch all jobs for full client-side filtering capability
- Add responsive horizontal scroll for mobile
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Stabilized video URLs in VideoReviewPlayer to prevent the video from
resetting when WebSocket updates trigger React Query refetches. The
fix stores the initial video URL for each tab and reuses it instead
of using the refreshed signed URL, preventing unnecessary remounts.
Changes:
- Added stableVideoUrls state to cache video URLs per tab
- Removed key prop from video element to prevent forced remounts
- Event listeners now attached once on mount, not on URL change
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When jobs with accessible video option enabled enter video rendering
phase, the status now transitions to 'rendering_video' so users can
see why processing is taking longer. This provides better visibility
into the video rendering pipeline.
Changes:
- Added RENDERING_VIDEO status to JobStatus enum
- Updated render_accessible_video task to set new status
- Added status display to StatusBadge, jobStatusMessages
- Included new status in JobsList Translation filter group
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Added staleTime: 0 and refetchOnMount: 'always' to useJobs hook to
ensure the jobs list always fetches fresh data when navigating to
the All Jobs tab after creating a new job.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Clarifies the UI when a job has no additional translation languages
requested, making it clearer that the job contains only the original
language assets.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When a video's source language is non-English (e.g., German), the
language asset was incorrectly marked as 'Translated'. Now correctly
shows 'Original' with a green badge for the primary/source language.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add validation for accessible_video_gcs (file exists, size 0.1MB-5GB)
- Add validation for retimed_captions_vtt_gcs when accessible video exists
- Add AD Videos count to asset validation panel
- Include retimed captions in VTT file count
- Remove AI confidence from validation panel and backend checks
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix video event listeners not re-attaching when video element remounts
(add activeTab?.videoUrl to useEffect dependency array)
- Add retimed_captions_vtt to VTT API response for accessible videos
- Use retimed captions for accessible video tab in VideoReviewPlayer
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add a comprehensive video review feature to the Final Review page that allows
reviewers to watch videos with caption overlays and add timestamped notes.
Backend:
- New ReviewNote model for MongoDB with job_id, asset_key, timestamp, content
- CRUD API endpoints at /jobs/{job_id}/review-notes
- Owner-only edit/delete permissions (admins can bypass)
- Database indexes for efficient querying
Frontend:
- VideoReviewPlayer component with video player and caption overlay
- NotesSidebar for viewing/adding notes with auto-highlight when video reaches timestamp
- SyncedCaptionList with auto-scroll and click-to-seek
- AssetTabs for switching between languages and accessible videos
- React Query hooks with 30s polling for collaborative updates
Features:
- Notes persist to database and are shared across all reviewers
- Notes highlight for 5 seconds when video playback reaches their timestamp
- Click note to seek video to that position
- Pause video to add note at current timestamp
- Accessible videos use retimed captions when available
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add new deliverable type that renders video with audio descriptions embedded.
Supports two AI-determined methods:
- Direct Overlay: ducks original audio and overlays AD TTS (for minimal dialogue)
- Pause-Insert: freeze-frame video, insert AD, re-time subtitles (for significant dialogue)
Backend:
- Add Pydantic schemas for Gemini analysis response
- Add Gemini prompt and analyze_accessible_video_placement() method
- Add video_renderer.py service using FFmpeg for both rendering methods
- Add vtt_retimer.py service for pause-insert subtitle adjustment
- Add render_accessible_video.py Celery task
- Modify TTS service to return individual per-cue segments
- Update translate_and_synthesize.py to save segments and trigger rendering
- Update download endpoint to include accessible video outputs
Frontend:
- Add accessible_video_mp4 checkbox to NewJob form
- Update TypeScript types for new deliverable
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add checkbox selection for pending final review jobs
- Add bulk actions toolbar with Complete/Return to QC options
- Mirror QC Review bulk action pattern with parallel API calls
- Rejection notes required, completion notes optional
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The zod schema required title to be non-empty, but in multi-upload mode
the title field is hidden (titles are auto-generated from filenames).
This caused form validation to fail silently when clicking upload.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add multi-file drag-and-drop to upload multiple videos at once
- Each video creates its own job using filename as title
- Single file upload preserves current UX with editable pre-filled title
- Multi-upload mode shows file list, individual progress bars, and summary
- Parallel uploads (max 3 concurrent) with error handling and retry
- Settings (language, outputs, TTS) apply to all jobs in batch
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add "Download All Files" option to the bulk actions menu on the All Jobs page.
When selected, downloads all assets (source video, VTTs, MP3s for all languages)
from jobs in approved_english, approved_source, or completed status.
- Shows confirmation modal with eligible/ineligible job counts
- Downloads files sequentially with progress indicator
- Skips jobs not in approved/completed status with warning
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- QCDetail passes sourceLanguage to VoiceSelector instead of hardcoded 'en'
- VoiceSelector uses first language in selectedLanguages for default voice preview
- Removes hardcoded English assumption in displayLanguages logic
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add model selection (flash vs pro) for quality control
- Add speed slider (0.5x - 2.0x) for pacing adjustment
- Add style presets (neutral, calm, energetic, professional, warm, documentary)
- Add custom style prompt option for advanced customization
- New /tts/options endpoint returns available TTS options
- Voice preview now tests all settings so users hear exact output
- Backward compatible: all new fields have sensible defaults
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The preview button was caching audio but not invalidating the cache
when the user selected a different voice, causing the same audio to
play regardless of which voice was selected.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add Gemini TTS service with 30 voices and 24 languages
- Add TTS API endpoints for voice listing and preview
- Add per-language voice selection in job creation form
- Add voice override at QC approval stage
- Add VoiceSelector and VoicePreviewButton components
- Update TTSPreferences model with provider and voice mapping
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add sourceLanguage prop to VideoWithCaptions component
- Create shared getLanguageLabel utility for language code mapping
- Update QCDetail and JobDetail to pass source language
- Fix status messages to say "source content" instead of "English content"
- Update Downloads page to display proper language names
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The setValueAs transformation wasn't working correctly with radio
buttons in react-hook-form, causing the form value to remain a string
instead of being converted to a boolean. This caused Zod validation
to fail silently since the schema expects a boolean.
Fixed by using controlled radio buttons with explicit onChange handlers
that call setValue() with the proper boolean value.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Upload form now has "English / Different language" radio with optional language hint
- Gemini auto-detects language and saves outputs to outputs.{detected_language}
- QC review dynamically loads/saves VTT for source language
- New APPROVED_SOURCE status for non-English videos (APPROVED_ENGLISH kept for backwards compat)
- Translation pipeline reads from source language and passes source_language to Google Translate
- All existing English jobs continue to work unchanged
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>