Commit graph

110 commits

Author SHA1 Message Date
Vadym Samoilenko
02c4240e6d fix: remove cost.oliver.agency links from QCDetail and FinalDetail
Replace inline anchor links with plain text — the correct dashboard URL
is only on the UserList button.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 11:36:03 +01:00
Vadym Samoilenko
848f7697f3 fix: revert cost tracker URL in QCDetail/FinalDetail inline links
Only the main AI Cost Dashboard button in UserList should use the new URL.
The inline helper links inside the Cost Tracker Project ID inputs stay on cost.oliver.agency.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 11:35:36 +01:00
Vadym Samoilenko
09550cfca0 feat: audit log integration sweep + cost tracker URL fix + audit log admin UI
- Fix cost tracker dashboard URL (cost.oliver.agency → optical-dev.oliver.solutions/cost-tracker/analytics)
  in UserList, QCDetail, FinalDetail; centralise into src/lib/costTracker.ts

- Wire audit logging across backend routes (was 1 call site, now covers all key events):
  · routes_auth: LOGIN_SUCCESS/FAILURE for local + MS SSO, LOGOUT
  · routes_files: FILE_UPLOAD on signed URL generation
  · routes_jobs: JOB_CREATE, JOB_APPROVE, JOB_REJECT, JOB_STATUS_CHANGE, JOB_DELETE, VTT_EDIT
  · routes_admin: USER_CREATE, USER_UPDATE, USER_ROLE_CHANGE, USER_DEACTIVATE

- Add Audit Log admin UI page (/admin/audit-log):
  · Three tabs: All Events (paginated, server-side filters), Security Events, User Activity
  · Filters: action group, severity, success/failure, free-text search
  · Click-to-expand row shows IP, request ID, resource, details JSON
  · Wired into App.tsx (RoleGate: production + admin) and sidebar nav

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 11:34:06 +01:00
Vadym Samoilenko
0f15d192cb feat(saas): Phase 4 — org settings IA, sidebar org-switcher, invitation UI
- Add /no-access empty-state page for zero-membership users
- Add /org/:orgSlug/settings/* routes (members, teams, invitations, general)
- OrgSettingsLayout with tab nav (NavLink-based)
- OrgMembersPage: member table with search, role editor, remove action
- OrgInvitationsPage: list with status badges + revoke
- OrgTeamsPage: read-only teams list
- OrgGeneralPage: org info display
- InviteMemberModal: email + role form → POST /organizations/:id/invitations
- Sidebar: org-switcher (single label / multi-org dropdown), currentOrgSlug
  derived from route params or first membership, Settings gear link at bottom

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 17:02:11 +01:00
Vadym Samoilenko
00fb1aacc6 feat(saas): Phase 2 — invitation flow, email templates, MS SSO zero-membership
Backend:
- models/invitation.py — Invitation model + create/accept/preview schemas
- routes_invitations.py — org-scoped POST/GET/DELETE + public preview/accept endpoints
  Single-use token via find_one_and_update; sha256(token) stored in DB, plaintext in email URL
- emailer.py — _send() helper; send_invitation_email, send_welcome_email, send_password_reset_email
  send_completion_email refactored to use _send()
- migration_2026-04-28-000002 — creates invitations collection with TTL index (30d audit trail)
- routes_auth.py — new MS SSO users provisioned with zero memberships instead of role=PRODUCTION;
  they land on "no access" page until an admin invites them
- main.py — registers invitations_org_router and invitations_router

Frontend:
- routes/AcceptInvite.tsx — public page at /accept-invite?token=...
  Four states: new user (name+password), existing user (confirm), MS user, already-member
- App.tsx — /accept-invite route outside RequireAuth
- types/api.ts — Invitation, InvitationCreate, InvitationPreview, InvitationAcceptRequest/Response
- lib/api.ts — listInvitations, createInvitation, revokeInvitation, previewInvitation, acceptInvitation
- hooks/useClients.ts — useInvitations, useCreateInvitation, useRevokeInvitation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 16:52:08 +01:00
Vadym Samoilenko
6f1be645ce feat(saas): Phase 0+1 — Organization/Membership entities and dev branch
Introduces the multi-tenant SaaS foundation alongside the existing
client/team/project model (zero-downtime shim period):

Backend:
- app/models/organization.py — Organization + OrgRole enum (OWNER/ADMIN/MANAGER/MEMBER/VIEWER)
- app/models/membership.py — Membership model with MemberDetail for enriched responses
- app/services/membership_service.py — upsert/remove/list/has_org_role helpers
- app/api/v1/routes_organizations.py — /organizations CRUD + /members sub-resource + /me/memberships
- main.py — registers organizations router
- migrations: create memberships collection (unique index) + backfill from pm_client_ids/team members

Frontend:
- types/api.ts — Organization, OrgRole, Membership, OrganizationCreateRequest types; Client marked @deprecated
- hooks/useClients.ts — useOrganizations, useOrganization, useOrgMembers, useAddOrgMember,
  useUpdateOrgMember, useRemoveOrgMember, useMyMemberships
- lib/api.ts — listOrganizations, getOrganization, createOrganization, updateOrganization,
  listOrgMembers, addOrgMember, updateOrgMember, removeOrgMember, getMyMemberships

Reads fall back to the clients collection during transition; all writes go to organizations.
Existing /clients endpoints and hooks are untouched.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 16:46:24 +01:00
Vadym Samoilenko
269ab09fa6 fix: serialize Client/Team/Project with id not _id + guard undefined client hooks
Pydantic v2 + FastAPI serializes Field(alias="_id") as _id in JSON,
so client.id was always undefined on the frontend — causing option
values to fall back to text content ("3M") and firing /clients/3M/teams 404s.

- Remove Field(alias="_id") from Client/Team/Project models; id is now a
  plain string field populated explicitly in _client_from_doc etc.
- API now returns id not _id, matching the TypeScript Client interface
- Add clientId !== "undefined" guard to useTeams, usePMs, useProjects

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 16:18:28 +01:00
Vadym Samoilenko
bbd324e3c7 feat: add Project Manager role + client/team assignment panel in admin user editor
- Add project_manager to all role dropdowns (UserList filter, create modal, UserDetail edit form)
- Add indigo badge color for project_manager in user list table
- Expose pm_client_ids in UserResponse schema and all admin user endpoints
- Add pm_client_ids to frontend User type
- Add UserAssignmentsPanel to UserDetail sidebar: PM users see client toggle list; other roles see client → team membership picker
- Add flexible hooks (useTeamsForClient, useAssignPMAny, useRemovePMAny, useAddTeamMemberAny, useRemoveTeamMemberAny)
- Fix useClient guard against literal "undefined" string causing 404 requests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 15:58:55 +01:00
Vadym Samoilenko
9af1ac3e95 fix: correct useAuthStore import path in ClientDetail
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 15:45:24 +01:00
Vadym Samoilenko
acdbb34b9b fix: cost_tracker uses /usage/record endpoint + correct allow/deny_reason fields; suppress edit-state 404 retries
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 15:11:39 +01:00
Vadym Samoilenko
2b721d182b feat: Client → Team → Project isolation system with Project Manager role
Backend:
- New UserRole.PROJECT_MANAGER with pm_client_ids[] on User model
- New models: Client (slug-based), Team (member_user_ids[]), Project (client-scoped)
- Job model gains project_id field
- New GET/POST/PATCH/DELETE /clients, /clients/{id}/teams, /clients/{id}/projects,
  /clients/{id}/pm routes (admin-only client CRUD; PM or admin for teams/projects)
- get_accessible_project_ids() helper: staff→all, PM→their clients' projects,
  CLIENT→projects from teams they belong to (with legacy owner fallback)
- list_jobs, get_job, bulk_download, get_vtt_content, delete_job all use new isolation

Frontend:
- UserRole type gains 'project_manager'
- Job, JobCreateRequest gain project_id field
- Client, Team, Project, PMUser types added
- ApiClient: full client/team/project/PM CRUD methods
- useClients hook with all query/mutation hooks
- Admin pages: ClientList + ClientDetail (teams, members, projects, PM assignment)
- NewJob form: client + project picker (shown when clients exist)
- Sidebar: Clients nav item for admin and project_manager roles
- Routes: /admin/clients and /admin/clients/:clientId behind RoleGate

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 15:11:13 +01:00
Vadym Samoilenko
1aa078db94 fix: suppress downloads API call for non-completed jobs on Downloads page
useJobDownloads was firing for any non-early status (pending_qc, translating,
etc.), hitting the backend /downloads endpoint which returns 400 when no outputs
exist yet. React Query then retried 3+ times.

Downloads page only renders content for completed jobs anyway, so disable the
query for any other status by passing 'created' (which is in EARLY_STATUSES).
JobDetail is unaffected — it uses the hook separately with its own status guard.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 14:07:34 +01:00
Vadym Samoilenko
26bfedd7c7 feat: add cost_tracker_project_id assignment UI to QC and Final Review
- PATCH /jobs/{job_id} endpoint for updating title and cost_tracker_project_id
- cost_tracker_project_id exposed on JobResponse (GET /jobs/{id})
- Inline project ID field in QCDetail and FinalDetail — saved via PATCH
- "AI Cost Dashboard" link in UserList header
- cost_tracker_project_id added to Job type and JobUpdateRequest schema

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 11:40:13 +01:00
Vadym Samoilenko
cf761c4bb6 feat: add linguist role and user management navigation
- Add LINGUIST role to UserRole enum (backend + frontend)
- Grant linguists access to QC Review, Final Review, review notes, and VTT editing
- Add MongoDB migration to update schema validator with linguist role
- Add admin seed: vadymsamoilenko@oliver.agency is promoted to admin on startup
- Add User Management sidebar link for admin users
- Fix Login.tsx role type cast to use UserRole instead of hardcoded union

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 11:46:33 +01:00
Vadym Samoilenko
084c37d1a7 fix: add SDH captions and descriptive transcript to QC Download Assets panel
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 13:51:34 +00:00
Vadym Samoilenko
83919c19b5 fix: add SDH captions to downloads page
SDH (sdh_captions_vtt) was missing from the Downloads page type labels
and filename extensions map.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 13:47:35 +00:00
Vadym Samoilenko
05a2fe5101 fix: timeline disappears after QC re-render
- Set staleTime=0 on useAccessibleVideoEditState so edit-state is
  immediately refetched when invalidated after render completes
- Force prevJobStatusRef='rendering_qc' at render start so the
  pending_qc completion effect always fires, even when fast renders
  complete before the 10s polling interval catches rendering_qc status

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 12:31:06 +00:00
Vadym Samoilenko
6f963ff7c4 feat: DCMP compliance, descriptive transcript, new languages, QA bug fixes
- Rewrote VTT translation to two-step (text-only → Gemini → apply to original timestamps) preventing caption timing desync
- Added polling fallback for all processing states and Safari visibilitychange WebSocket reconnect
- Added 11 new TTS languages (cs, da, fi, hu, no, sk, sv, es-419, pt-BR, fr-CA)
- Updated caption/AD prompts to DCMP Captioning Key & Description Key standards (line splitting, ♪ music notation, italic tags, caption positioning, ethics guidelines)
- Added descriptive transcript generation (WCAG 2.1 §1.2.1) combining captions + AD into plain text
- Fixed amix normalize=0 to prevent audio loss in rendered videos
- Fixed AD re-timing double-count when source_ms is None
- Fixed cue block numbering to be 1-based in VttEditor and Timeline Preview

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 11:50:43 +00:00
Vadym Samoilenko
f4ddcce066 fix: resolve QA-reported bugs — MP3/VTT desync, crashes, notifications, and more
**BUG-1 & BUG-2 — Wrong audio plays after re-render / MP3 doesn't match text**
Root cause: audio files were named by index (cue_0.mp3, cue_1.mp3). When a cue
was inserted or deleted, all following indices shifted but old MP3 files kept
their original names, so re-render would play the wrong audio for the wrong cue.
Fix: renamed files to cue_N_CONTENTHASH.mp3 and introduced an ad_cue_manifest
stored in the job document that maps each cue index to its correct GCS URI.
Re-render now reads from the manifest instead of guessing by filename.
Also: editing AD cue text in the VTT editor now automatically queues TTS
regeneration for changed cues — no more silent mismatches.

**BUG-3 — App crash / state desync when uploading VTT or clearing TTS queue**
Fixed handleVttFileUpload to only update local editor state after the server
confirms the save — previously local state was updated first, so a network
error left the UI showing content that wasn't actually saved.
Fixed handleClearRegenerationQueue to only remove items from local state if
the server removal succeeded — previously all items were cleared regardless.

**BUG-4 — AI generates different audio descriptions every time**
Added GenerateContentConfig(temperature=0.2, top_p=0.8, top_k=40) to the
Gemini API call so output is more consistent across runs.

**BUG-5 — On-screen text inconsistently described**
Strengthened the AI prompt rule from a vague suggestion to a mandatory
requirement with an explicit format: "Text on screen reads: [exact text]".
Applied to both gemini_ingestion.md and gemini_ingestion_targeted.md.

**BUG-6 — No notification when re-render finishes**
Added rendering_qc toast notification and a dismissible green banner that
appears in QCDetail when re-render transitions to pending_qc. The banner
auto-dismisses after 10 seconds. Also increased WebSocket reconnect attempts
from 5 to 15 and capped backoff at 60s to prevent falling back to manual refresh.

**BUG-7 — Timeline preview looks accurate but isn't after edits**
Added isStale prop to TimelinePreview. The timeline now shows an amber tint
and "Preview may be outdated" label whenever there are unsaved pause point
changes, pending TTS regenerations, or a new VTT has been uploaded.

**BUG-8 — ElevenLabs API errors break TTS with no fallback**
Added try/except fallback chain in _synthesize_single_cue: if the configured
provider fails, it automatically retries with google, then gemini.

**BUG-9 — Concurrent re-render requests cause race conditions**
Made the PENDING_QC → RENDERING_QC status transition conditional (only
succeeds if the job is still in PENDING_QC). Returns HTTP 409 if a re-render
is already in progress. The completion transition back to PENDING_QC is also
conditional so a cancelled/overridden render doesn't corrupt job state.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 13:23:55 +00:00
Vadym Samoilenko
c413fcb747 feat: add SDH (Subtitles for Deaf and Hard of Hearing) caption output
SDH captions extend standard VTT with speaker identification labels,
sound effects [PHONE RINGS], music notation ♪, and off-screen indicators.

- Add sdh_vtt flag to RequestedOutputs model and frontend form
- Add sdh_captions_vtt_gcs field to LangOutput model
- Inject SDH generation instructions into both Gemini prompts via
  {SDH_FIELD} and {SDH_GUIDELINES} placeholders when requested
- Upload sdh_captions.vtt to GCS in ingest task
- Pass SDH through video_native translation (Gemini generates it directly)
  and traditional translation (translate source SDH VTT via Gemini)
- Expose sdh_captions_vtt in downloads endpoint and bulk zip export

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 15:02:18 +00:00
Vadym Samoilenko
2e8a8dc287 feat: add brand context, ethics guidelines, and improved AD prompt rules
- Add brand_context field (job model, API, frontend form) so clients can
  list brand names present in their video; Gemini uses these names instead
  of generic descriptors (e.g. "Sellotape" not "sticky tape")
- Add ethical guidelines section to both Gemini prompts covering
  person-first language, consistent race/gender description only when
  plot-relevant, no guessing at unconfirmed identity
- Revamp audio description rules: priority ordering (essential →
  high-priority → time-permitting), pre-teaching placement, no cinematic
  jargon, succinct style replacing the former "20% longer" instruction
- Thread brand_context through full stack: routes → job doc → ingest
  task → translate task → both Gemini prompt templates

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 14:46:09 +00:00
Vadym Samoilenko
539e11caca fix: poll for rendering_qc status and refresh timeline preview on completion
Fixes race condition where timeline preview never updated after AD VTT
re-render: useJob now polls every 5s during rendering_qc, and QCDetail
invalidates the edit-state query when the job transitions back to pending_qc.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 19:27:49 +00:00
Vadym Samoilenko
76ca74d5a5 fix: enable Render Changes button after AD VTT upload
adVttUploaded was missing from the hasChanges condition in
RerenderControls, leaving the button greyed out after an upload.
Pass the flag as a prop and include it in hasChanges; also show
an "Audio Description script was replaced" status message.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 15:58:23 +00:00
Vadym Samoilenko
e10d90219c feat: add download assets panel and VTT file upload to QC review
Reviewers can now download individual job assets (source video,
captions VTT, AD VTT, AD MP3, accessible video) directly from the
QC detail page. They can also replace captions or AD VTT scripts by
uploading a revised file, with a banner prompting re-render when the
AD script is replaced.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 14:52:00 +00:00
Vadym Samoilenko
d2d393c5c7 fix: skip downloads fetch for jobs still in early processing stages
useJobDownloads now accepts jobStatus and disables the query when the
job is in created/ingesting/ai_processing, preventing spurious 400s
from /jobs/{id}/downloads before any outputs exist.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 14:29:16 +00:00
Vadym Samoilenko
222826baa7 fix: propagate ElevenLabs voice fetch errors to frontend
- elevenlabs_voices.py: re-raise exception on first fetch failure
  (empty cache) instead of silently returning empty list
- routes_tts.py: catch get_voices() exception and return available=False
  with the error detail; add optional error field to ProviderVoicesResponse
- VoiceSelector: show actual API error message when available=false

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 14:27:45 +00:00
Vadym Samoilenko
a22fe5c1bc fix: surface ElevenLabs config errors and add availability flag
- Extract actual error message from blob response in previewVoice so
  users see the real API error instead of generic "Failed to generate preview"
- VoicePreviewButton now reads err.message from thrown Error objects
- Add available: bool field to ProviderVoicesResponse; returns false
  when ELEVENLABS_API_KEY is not configured so the frontend can react
  proactively instead of hitting a 400 on preview
- VoiceSelector shows a descriptive config warning when available=false

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 14:17:00 +00:00
Vadym Samoilenko
1e177a6d5c feat: add ElevenLabs voice selection to frontend and backend
Add dynamic ElevenLabs voice catalog with provider toggle in the UI,
allowing users to browse ElevenLabs voices, configure stability and
similarity boost settings, and preview/synthesize with ElevenLabs TTS.

Backend:
- New elevenlabs_voices.py service with 1-hour cached API fetching
- TTS routes support ?provider= query param for voices and options
- Preview endpoint routes to ElevenLabs or Gemini based on provider
- stability/similarity_boost params flow through TTS synthesis pipeline
- TTSPreferences model extended with ElevenLabs-specific fields
- Deprecated hardcoded elevenlabs_voices config (now fetched dynamically)

Frontend:
- Provider toggle (Gemini/ElevenLabs) in VoiceSelector
- ElevenLabsSettingsPanel with stability and similarity boost sliders
- VoicePreviewButton supports provider-specific preview parameters
- API client passes provider param to voices, options, and preview endpoints
- New VoiceInfo, ProviderVoicesResponse, ProviderOptionsResponse types

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 13:58:56 +00:00
michael
0c3102b77f feat: add Return to QC action for jobs in resting statuses
Allow production/admin users to move jobs back to pending_qc from
completed, pending_final_review, rejected, qc_feedback, tts_failed,
render_failed, approved_english, and approved_source statuses. Includes
single-job endpoint, bulk endpoint, JobDetail inline form with required
notes, bulk action in JobsList with confirmation modal, and a Review
Notes card on the job overview tab.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 13:18:02 -06:00
michael
89a902d392 fix: prevent pause point input reset during editing
Changed useEffect dependency from full pausePoint object to just
cue_index. This prevents the input from resetting when parent re-renders
cause the pausePoint object reference to change while editing the same
pause point.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 10:09:07 -06:00
michael
106ca49f6f fix: allow free-form editing of pause point timestamp input
The input was reformatting with .toFixed(3) on every keystroke, causing
backspace to appear to insert random digits. Changed to string-based
input state with conversion/validation only on blur or save.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 14:50:44 -06:00
michael
df721850e0 fix: queue TTS regeneration for shifted cues when deleting AD cue
When an AD cue is deleted, all subsequent cues shift positions but their
MP3 files remain at the old indices. This adds handling to automatically
queue TTS regeneration for all cues that shifted after a deletion.

Changes:
- VttEditor: Add onCueDeleted callback to notify parent of deletions
- QCDetail: Track deletion context and queue TTS for all shifted cues

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 14:29:25 -06:00
michael
577ed44dab 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>
2026-01-12 14:24:36 -06:00
michael
dab294f18a feat: streamline QC approval to skip translation pipeline
QC approval now transitions jobs directly to pending_final_review
since translation, TTS, and accessible video rendering happen before
QC review. Removes unnecessary translate_and_synthesize_task trigger
on approval.

- Update approve_source() to use PENDING_FINAL_REVIEW status
- Update bulk_approve_jobs() to use PENDING_FINAL_REVIEW status
- Remove translate_and_synthesize_task.delay() calls from both endpoints
- Update JobDetail progress indicator to reflect new flow
- Update CLAUDE.md state machine documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 10:37:37 -06:00
michael
48bcea349e feat: add cue numbering to AD cues in VttEditor
Display 0-based cue index badges in the editable AD cue list to match
the numbering shown on the timeline preview, helping users associate
timeline markers with their corresponding editable cues.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 10:28:48 -06:00
michael
0919dbf7bd feat: move accessible video method selection to job creation
Since accessible video is now rendered immediately on upload, the method
selection (pause_insert vs overlay) is moved from QC Review to the New
Job panel. The bulk approval modal for selecting the method is removed.

- Add method selector UI to NewJob.tsx below accessible video checkbox
- Remove method selector from QCDetail.tsx approval flow
- Remove bulk approval modal from QCList.tsx

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 10:25:40 -06:00
michael
e371dc401a feat: add save button to voice settings panel for TTS regeneration
Add ability to save voice settings changes in QC Review screen without
needing to approve the job. When saved, all TTS segments are regenerated
across all languages with the new voice settings.

Changes:
- Add PUT /jobs/{id}/tts-preferences endpoint to update TTS preferences
- Add UpdateTTSPreferencesRequest schema
- Add updateTTSPreferences API method and useUpdateTTSPreferences hook
- Add Save Voice Settings button with change detection to QCDetail

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 09:05:56 -06:00
michael
b9c2fd93ac feat: streamline VTT cue editing to single-step save
Eliminate the two-step save process for VTT cue edits. Previously users
had to (1) save individual cue edits, then (2) click "Save Changes" in
a separate yellow notification box. Now saving a cue immediately persists
to the database and queues TTS regeneration for AD cues.

Changes:
- Add onCueSave callback prop to VttEditor for immediate persistence
- Add per-cue saving indicators and error handling with retry
- Remove hasUnsavedChanges state and yellow "Unsaved Changes" box
- Remove Ctrl+S keyboard shortcut (no longer needed)
- AD cue saves automatically queue TTS regeneration

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 08:52:23 -06:00
michael
9d56306052 fix: display pause point timing in seconds instead of milliseconds
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 08:32:34 -06:00
michael
f47820a6a4 fix: make source_ms optional for backward compatibility with existing jobs
Existing jobs in the database don't have source_ms field. Making it
optional allows the API to load these jobs without validation errors.
The re-render task already handles the fallback to original_ms when
source_ms is None.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 07:04:08 -06:00
michael
a6cd4cde07 fix: store source video coordinates in pause points for correct re-rendering
The re-render task was using pause point coordinates from the accessible
video timeline (which includes freeze frame durations) instead of the
original source video coordinates. This caused pause points to exceed
the source video duration and get clamped incorrectly.

Changes:
- Add source_ms field to PausePointData model to store source video cut point
- Update video_renderer.py to populate source_ms when building pause points
- Update rerender_accessible_video.py to use source_ms for placement calculations
- Apply user adjustments as relative offsets (delta-based adjustment)
- Update API responses and TypeScript types to include source_ms
- Add backward compatibility fallback for jobs without source_ms

Note: Existing jobs need to be re-processed from initial render to populate
the new source_ms field.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 10:48:41 -06:00
michael
676490ac65 feat: auto-queue TTS regeneration when AD script cues are edited
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>
2026-01-11 09:59:04 -06:00
michael
aa6777d2c2 feat: add QC accessible video review and editing capabilities
- 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>
2026-01-11 08:32:27 -06:00
michael
e44210ea64 feat: auto-rewrite TTS cues that fail synthesis
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>
2026-01-05 14:42:50 -06:00
michael
83e4752327 feat: add server-side zip download for bulk job downloads
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>
2026-01-04 15:57:57 -06:00
michael
8bd9be6353 fix: TypeScript type narrowing for TTS error display
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>
2026-01-03 21:41:12 -06:00
michael
6915cf46af feat: add TTS retry functionality with detailed error reporting
- 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>
2026-01-03 21:39:59 -06:00
michael
9436aa4c6b fix: remove useMemo hook after early returns in QCList
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>
2026-01-03 19:22:29 -06:00
michael
c512bdc184 feat: use AD VTT pause points instead of Gemini video analysis
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>
2026-01-03 19:05:45 -06:00
michael
3689653135 refactor: remove manual language selection, always auto-detect
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>
2026-01-03 18:42:45 -06:00