The generate_signed_url() was called with expiration=3600 as an integer,
but GCS expects a datetime or timedelta. Now uses gcs_service.get_signed_url()
which properly calculates the expiration timestamp.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The AccessibleVideoEditStateResponse schema expects string timestamps
but the API was passing raw datetime objects from MongoDB. Now converts
last_render_at and requested_at to ISO format strings.
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>
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>
- 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>
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>
The google-cloud-storage library made blob.generation read-only,
causing job deletion to fail silently (0 GCS files deleted).
Using bucket.delete_blob(name) instead avoids generation checking.
🤖 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>
Replace 3-stage redundant deletion with single prefix-based approach.
All job files are under {job_id}/ prefix, so listing and deleting by
prefix is simpler and catches all files including new types like
accessible_video.mp4 and ad_cues/*.mp3.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The Created By filter dropdown was empty because client_id was not
being returned by the API. Added client_id to JobResponse schema
and included it in the list_jobs response.
🤖 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>
- 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>
The method field (overlay/pause_insert) is metadata, not a downloadable
file. Including it in the downloads dict caused the frontend to render
a broken download link.
🤖 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 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 import was using a non-existent module path `..deps` instead of
`...core.dependencies`, causing the API container to fail on startup.
🤖 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>
- 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>