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>
Bug 1: Editing any AD cue never updated descriptive_transcript.txt in GCS.
Bug 2A: Uploading replacement CC or AD .vtt had the same root cause.
After saving captions or AD VTT, read the other stream from GCS if not
provided in the request, merge both via generate_descriptive_transcript(),
upload the result to {job_id}/{lang}/descriptive_transcript.txt, and
update lang_output["descriptive_transcript_gcs"] before the DB write.
Bug 2B (CC-only job → 400 on empty audio_description_vtt): already fixed
by the existing `if request.audio_description_vtt:` guard (empty string
and None are both falsy) and frontend `adVtt || undefined` sending no
field rather than an empty string.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Hardcoded --concurrency=8 with 512MB memory limit caused 1162+ OOM restarts.
Default is 2; set TTS_WORKER_CONCURRENCY in .env.production to override.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- NewBrief: use useAllProjects() (was useProjects('') which never fired)
- NewBrief: expand languages from 12 to 52 options with region variants
- NewBrief: add Assign To dropdown from org members
- Backend: add GET /clients/all-projects endpoint for cross-client project listing
- Backend: add assignee_id to JobBriefCreate/JobBriefResponse models + routes
- notify.py: send completion email to PMs (pm_client_ids) not client user — fixes email never arriving (was looking up users._id by client entity ID)
- Downloads: add Download All button that fetches all files sequentially
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- glossary_service: add step-by-step debug/warning logs at each early-return point so
the exact failure reason is visible in worker logs (project not found, no active version, etc.)
- glossary_service: guard against source_term_lower=None in ahocorasick automaton build
- glossary_service: guard against target_locale=None in _get_translation
- glossary_service: add full traceback to the outer exception catch for easier debugging
- JobsList: fix statusFilter stale state — useEffect now always syncs with URL params,
clearing the filter when no ?status= param is present (previously the filter was never
cleared, so navigating from /jobs?status=X to /jobs kept the old filter)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>