Gemini TTS allows 10 RPM; with concurrency=8 the rate limit is hit quickly.
The previous backoff (1-3s) was far too short — the API returns retryDelay ~37s.
Both synthesize_cue_task (Celery retry countdown) and GeminiTTSService
(_synthesize_cue_with_retry sleep) now parse the retryDelay from the 429
error message and use it (+ 5s buffer) instead of the exponential guess.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When EN is approved on a source-only job (no target languages), the translation
branch was skipped entirely, leaving the accessible video render pipeline never
dispatched. Added elif branch: if accessible_video_mp4 is requested and there
are no target languages to translate, dispatch translate_and_synthesize_task
(which will skip translation, run TTS for source language, and dispatch the
render task).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Used wrong field name sdh_captions instead of sdh_vtt, causing
TypeScript build failure on optical-dev.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
accessible_video_mp4 and sdh_captions were missing from the
Outputs section render — only 3 of 5 fields were displayed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
get_redis() is an async function but was called without await in
_cached_memberships(), causing RuntimeWarning and silently bypassing
the Redis membership cache on every request — all membership lookups
were hitting MongoDB instead of cache.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Per-cue MP3s (ad_cue_manifest) are required by render_accessible_video_task regardless
of whether the assembled ad.mp3 is requested as a client download. Previously, jobs with
accessible_video_mp4=True but audio_description_mp3=False would silently skip TTS, leaving
render tasks never dispatched and jobs stuck in tts_generating indefinitely.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Gemini occasionally produces captions where a cue's start_time is
earlier than the previous cue's end_time. Add VTTEditor.fix_overlapping_cues()
that trims each cue's end_time to 1ms before the next cue's start, applied
to both captions and AD VTT immediately after AI generation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
job.error persists after retry succeeds — only show it when status is
tts_failed / render_failed / processing_failed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Gemini occasionally returns response.text=None under load or safety filters.
Treat it as a retriable error so the fallback chain is used.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- gemini-3.1-flash-preview doesn't exist; replace with gemini-3-flash-preview
- GET /jobs/{id}/downloads: return empty {} instead of 400 when job has no
outputs (e.g. processing_failed before AI stage completes)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Routes all generate_content calls through _generate() which retries
gemini-3.1-flash-preview then gemini-2.5-pro when primary model hits
RESOURCE_EXHAUSTED. Cost tracker records actual model used.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When retranslate=True, _generate_tts_for_languages was receiving
the full outputs dict (all 9 languages) and regenerating TTS + render
for every language on every single-language retranslation task.
That multiplied API calls by 8x and triggered unnecessary renders.
Now passes only the target language outputs when retranslate=True.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
concurrent retranslation tasks (concurrency:2) were each replacing the
entire outputs doc, so the last writer silently overwrote the others.
Now each task only writes outputs.<lang> for the languages it processed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds "↺ Retranslate broken (N)" button in the Languages panel header.
Visible to production/admin when EN is approved and there are languages
with video_native origin or missing captions_vtt_gcs.
Confirm modal shows each broken language with its failure reason,
then queues individual retranslation tasks sequentially.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bug fixes:
- Bug 1a: source_has_ad flag prevents AI generating AD over existing professional AD;
JobBrief/Job models, gemini service prompt conditional, NewBrief UI checkbox
- Bug 1b: disable native textTracks on video element to prevent double captions
- Bug 2: caption ALL audible speech including off-screen narrators (prompt fix)
- Bug 3: DCMP §6.01 disfluency removal for EN/ES/FR/DE/IT (prompt + post-pass)
- Bug 4: VTT cue settings (line:0%, position:) preserved through parser round-trip
- Bug 5: Whisper word-level timestamp alignment via new caption_aligner service
- Bug 6: assert_cue_alignment used .start/.end; renamed to .start_time/.end_time
- New migration: backfill source_has_ad=False on existing jobs and job_briefs
Also fix retranslation error handling to preserve existing GCS URIs on failure
so video_native captions remain accessible if retranslation fails.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add §6 EN-First Translation Pipeline to Production, Linguist, Admin, and
Project Manager guides explaining the new flow: translations are generated
only after English QC is approved, preserving 1:1 cue structure.
Documents origin badges (⚠ video-native), the amber TranslationGateBanner
on target-language cards, the ↺ Re-translate from EN button, and the
blue info note on the New Job form. Adds 5 new screenshots captured from
the deployed optical-dev environment.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add §6 EN-First Translation Pipeline to Production, Linguist, Admin, and
Project Manager guides explaining the new flow: translations are generated
only after English QC is approved, preserving 1:1 cue structure.
Documents origin badges (⚠ video-native), the amber TranslationGateBanner
on target-language cards, the ↺ Re-translate from EN button, and the
blue info note on the New Job form. Adds 5 new screenshots captured from
the deployed optical-dev environment.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
approve_source is callable by any qualified role (reviewer, linguist,
production, admin) — not just linguists. Now correctly dispatches the
translation pipeline when target languages are untranslated, regardless
of who approves the source. Without this fix, only language_qc.approve_language
(EN path) would trigger translation, leaving other roles stuck.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All translations now derive strictly from the approved English master VTT,
eliminating the cue-count and timestamp drift reported by linguists
(e.g. PL AD = 11 cues vs EN AD = 17 cues).
Key changes:
- Remove video_native translation mode entirely; all languages go through
translate_vtt() which guarantees 1:1 cue alignment with EN master
- Transcreation languages now use translate_vtt(style="transcreate") —
same cue-preserving contract, culturally-adapted instructions
- Post-translation cue alignment validator added (VTTEditor.assert_cue_alignment)
- After ingestion, job moves to PENDING_QC (EN-only) instead of TRANSLATING;
translation pipeline dispatches automatically when EN QC is approved
- New POST /jobs/{id}/retranslate-language endpoint for PM/admin to fix
legacy video_native jobs on demand
- Frontend: origin badge (EN-aligned / transcreated / video-native warning),
EN-first gate banner on target-language cards, Re-translate from EN button
with confirm modal, removed translation mode selector from NewJob
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wrap the render condition in parentheses and include 'submitting' so
TypeScript does not narrow decisionState to 'idle'|'error' inside the
review form block, eliminating four TS2367 comparison errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- POST /public/share/{token}/decision — unauthenticated approve/reject via share token
- approve: validates assets, sets status completed, triggers notification
- reject: sets status qc_feedback, stores client name + notes in review history
- ShareView: review form (name, comments, Approve / Return for Corrections)
- shows only when job is pending_final_review
- confirmation screen after decision
- api.ts: submitShareDecision()
- Hide 'client' role from UserList/UserDetail dropdowns
- Hide 'Client' guide tab from Help
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add \b word boundaries to SQL injection and command injection regex patterns
to prevent false positives on names like "Josh Smith" (sh\s+), "Norm " (rm\s+)
- Change default role in CreateUserModal from 'client' to 'admin'
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
MongoDB 7.0 rejects the invalid key with code 28811, causing 500 on
GET /organizations/{id}/members.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New FAQ guide (faq.md) covering: re-render accessible video, stuck jobs,
linguist/reviewer assignment, downloads, TTS voice, briefs, status colours
- extractSections() parses markdown body text per section; search now
matches against section body text, not just heading text
- FAQ tab added between Overview and Client in the sidebar
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
u.get("key", "") returns None when key exists with null value in MongoDB,
causing Pydantic ValidationError on MemberDetail.email/full_name: str → 500.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Resolves TS2448/TS2454 — useMemo blocks referenced captionsVtt and
retimedCaptionsVtt before their useState declarations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add retimed captions overlay to accessible video player in QCDetail;
falls back to original captions if retimed VTT not yet generated
- Extend listUsers to accept comma-separated roles (e.g. linguist,admin)
so admin/production users appear in linguist/reviewer assignment dropdowns
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Projects:
- PM now sees all active projects (same as admin/production) — was filtering
to empty when pm_client_ids and org memberships were both unset
Assign To:
- Replaced useOrganizations()+useOrgMembers() with a new GET /admin/brief-assignees
endpoint accessible to all authenticated users — returns active admin/PM/production
users sorted by name; shows role next to name in dropdown
Requested Outputs:
- Added SDH Captions (VTT), Descriptive Transcript, Accessible Video (MP4)
- Accessible Video shows Pause Insert / Voice Overlay radio selector
- Added descriptive_transcript field to RequestedOutputs model (backend + frontend)
Access:
- Brief routes now open to 'client' role in addition to admin/PM/production
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Deleted route files, App.tsx routes, Sidebar nav item, and Dashboard
"Awaiting Upload" card. The feature wasn't ready (Project/Assign To dropdowns
were empty for non-admin users) and isn't needed at this stage.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Platform admins query GET /organizations (not memberships) so currentOrgId
was always null — hiding the Settings nav link. Now falls back to the first
org from useOrganizations() for admins, gated with enabled:isPlatformAdmin
to avoid 403 for non-admin roles.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The backend /organizations/{org_id}/members endpoint queries memberships
by organization_id (_id hex string), but the sidebar was building the URL
from organization_slug (e.g. "3m-test"), causing 403 on every Settings page
load ("Failed to load members.").
- Sidebar: derive currentOrgId from organization_id; option values = org ID
- OrgSettingsLayout: alias orgSlug param as orgId for clarity
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cloud Run-generated freeze segments caused FFprobe to return code 1 with
empty stderr when dispatched to the Celery ffmpeg queue, crashing the
render for every language. The freeze segments are created to an exact
pre-computed duration (ad_duration + silence_before + silence_after),
so probing is unnecessary — assign that value directly instead.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove `default` from <track> element so browser doesn't render native
captions on top of the custom overlay (was causing double display)
- Remove positionTop logic — always render overlay at bottom-14 (above controls)
regardless of VTT line hints; applies to both VideoWithCaptions and VideoReviewPlayer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Navbar: add WebSocket connection dot (green/yellow/red/gray) from GlobalWebSocketContext
- Profile page: /profile route shows email, full_name, role, auth_provider, languages
- JobResponse: expose failure and error fields (were stored in MongoDB but not returned)
so frontend now shows actual render error message instead of generic fallback
- render_accessible_video: write JOB_TASK_FAILED audit log entry on render failure
with language, error detail, step=render
- rerender_accessible_video: same audit log on re-render failure, step=rerender
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend (Phase A):
- A1: Adaptive silence buffer — natural_gap_ms persisted per cue; renderer computes
per-cue silence_before/silence_after instead of fixed 500ms; per-cue silence files
- A2: Forward-preferred snap — snap_pause_point prefers boundaries up to 4s ahead
over boundaries within 1.5s behind, reducing mid-scene cuts
- A3: Min-gap validation — pause points with < 200ms gap trigger forward search
to the next acceptable gap
- natural_gap_ms added to PausePointData model and api.ts type
- New config fields: whisper_snap_forward_window, whisper_snap_backward_window,
ad_silence_buffer_default, ad_silence_buffer_min_after, ad_min_acceptable_gap
- Tests: test_whisper_snap.py (13 tests), test_video_renderer_buffers.py
Frontend (Phase B):
- B1: Drag pause-point markers — pointer state machine with 3px move threshold,
clamp to min/max bounds, click-without-move still opens PausePointEditor
- B2: Drag freeze blocks — orange blocks translate with linked pause point
- B3: Time tooltip visible during drag, hidden on release
- Tests: TimelinePreview.drag.test.tsx (10 tests)
Fixes:
- Share link pointed to ai-sandbox.oliver.solutions — added app_url to Settings
with correct optical-dev.oliver.solutions default; share_url now configurable
via APP_URL env var
- Removed all ai-sandbox.oliver.solutions references from docker-compose,
apache config, docs, and scripts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sidebar My QC Queue badge was showing org-wide pending_qc job count
instead of the current user personal assigned tasks. Now uses
useMyQCQueueCount which sums the linguist and reviewer queue totals
from the same me/language-qc-queue API the queue page uses.
Refresh button now shows a spinner and Refreshing label while the
refetch is in progress so users can see the action took effect.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
list_org_members and _membership_from_doc used bracket access on role_in_org
which raises KeyError if the field is absent (old docs or direct DB inserts).
Also handles ValueError if the stored value doesn't match a valid OrgRole.
Falls back to OrgRole.MEMBER in both cases.
Fixes 500 on GET /organizations/{org_id}/members.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
assign, assign-reviewer, reassign-reviewer, and bulk-assign endpoints
were gated to project_manager/production/admin only, but the Reviewer
QC Detail page exposes Assign buttons to reviewer users.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Captures admin, client, linguist, reviewer, production, project-manager,
and global help screenshots from optical-dev using Playwright MCP.
All markdown-referenced filenames now have corresponding PNG files.
Placeholders used where live data or role permissions prevent full capture.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Markdown guides use /help-screenshots/... (root-relative). With Vite
base=/video-accessibility/, images were requested at the wrong URL.
Custom img renderer now prepends import.meta.env.BASE_URL so paths
resolve correctly on both /video-accessibility/ and local dev.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Screenshots captured via Playwright against optical-dev. Covers:
global (login + interface), client, linguist, reviewer, production,
project-manager, admin — all 25 PNGs under frontend/public/help-screenshots/.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Frontend sends audio_description_vtt: "" for CC-only jobs.
Pydantic validator converts "" to None before validation,
so the backend skips VTT format validation and returns 200
instead of 400.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>