diff --git a/backend/app/api/v1/routes_jobs.py b/backend/app/api/v1/routes_jobs.py index 4d82af9..b53122b 100644 --- a/backend/app/api/v1/routes_jobs.py +++ b/backend/app/api/v1/routes_jobs.py @@ -14,6 +14,7 @@ from ...models.job import JobStatus, RequestedOutputs from ...models.user import User, UserRole from ...schemas.job import ( ApproveEnglishRequest, + ApproveSourceRequest, AssetValidationResponse, BulkDeleteRequest, BulkDeleteResponse, @@ -45,7 +46,8 @@ router = APIRouter(prefix="/jobs", tags=["jobs"]) @router.post("", response_model=JobResponse, status_code=status.HTTP_201_CREATED) async def create_job( title: str = Form(...), - language: str = Form("en"), + source_is_english: bool = Form(True), # True = English source, False = other language (auto-detect) + language_hint: Optional[str] = Form(None), # Optional hint when source_is_english=False requested_outputs: str = Form(...), # JSON string file: UploadFile = File(...), current_user: User = Depends(get_current_user), @@ -75,6 +77,11 @@ async def create_job( file, f"{job_id}/source.mp4" ) + # Determine initial language setting + # If English: set to "en" + # If not English: set to "auto" (will be detected by AI) or use hint + initial_language = "en" if source_is_english else (language_hint or "auto") + # Create job document job_data = { "_id": job_id, @@ -84,7 +91,8 @@ async def create_job( "filename": f"{job_id}/source.mp4", "original_filename": file.filename, "gcs_uri": gcs_uri, - "language": language + "language": initial_language, + "language_hint": language_hint if not source_is_english else None, }, "requested_outputs": outputs.dict(), "status": JobStatus.CREATED.value, @@ -335,18 +343,37 @@ async def get_job( ) -@router.post("/{job_id}/actions/approve_english", response_model=JobResponse) -async def approve_english( +@router.post("/{job_id}/actions/approve_source", response_model=JobResponse) +async def approve_source( job_id: str, - request: ApproveEnglishRequest, + request: ApproveSourceRequest, current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.PRODUCTION, UserRole.ADMIN)), db: AsyncIOMotorDatabase = Depends(get_database), ): + """Approve the source language version (works for any language)""" + # First, get the job to determine the source language + job_doc = await db.jobs.find_one({"_id": job_id}) + if not job_doc: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Job not found" + ) + + if job_doc["status"] != JobStatus.PENDING_QC.value: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Job not in pending QC status" + ) + + # Determine the appropriate status based on source language + source_language = job_doc["source"].get("language", "en") + new_status = JobStatus.APPROVED_ENGLISH if source_language == "en" else JobStatus.APPROVED_SOURCE + result = await db.jobs.find_one_and_update( {"_id": job_id, "status": JobStatus.PENDING_QC.value}, { "$set": { - "status": JobStatus.APPROVED_ENGLISH.value, + "status": new_status.value, "review.notes": request.notes or "", "review.reviewer_id": str(current_user.id), "updated_at": datetime.utcnow() @@ -354,7 +381,7 @@ async def approve_english( "$push": { "review.history": { "at": datetime.utcnow(), - "status": JobStatus.APPROVED_ENGLISH.value, + "status": new_status.value, "by": str(current_user.id), "notes": request.notes or "" } @@ -372,7 +399,7 @@ async def approve_english( # Trigger translation and synthesis pipeline immediately try: translate_and_synthesize_task.delay(job_id) - logger.info(f"Triggered translation task for approved job {job_id}") + logger.info(f"Triggered translation task for approved job {job_id} (source: {source_language})") except Exception as e: logger.error(f"Failed to trigger translation task for job {job_id}: {e}") # Don't fail the approval, just log the error @@ -390,6 +417,22 @@ async def approve_english( ) +@router.post("/{job_id}/actions/approve_english", response_model=JobResponse) +async def approve_english( + job_id: str, + request: ApproveEnglishRequest, + current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.PRODUCTION, UserRole.ADMIN)), + db: AsyncIOMotorDatabase = Depends(get_database), +): + """Legacy endpoint - redirects to approve_source for backwards compatibility""" + return await approve_source( + job_id, + ApproveSourceRequest(notes=request.notes), + current_user, + db + ) + + @router.post("/{job_id}/actions/reject", response_model=JobResponse) async def reject_job( job_id: str, @@ -656,11 +699,11 @@ async def get_job_downloads( @router.get("/{job_id}/vtt", response_model=VttContentResponse) async def get_job_vtt_content( job_id: str, - language: str = "en", + language: Optional[str] = None, # If None, defaults to source language current_user: User = Depends(get_current_user), db: AsyncIOMotorDatabase = Depends(get_database), ): - """Get VTT content for editing""" + """Get VTT content for editing. If language is not specified, returns source language content.""" job_doc = await db.jobs.find_one({"_id": job_id}) if not job_doc: raise HTTPException( @@ -676,8 +719,11 @@ async def get_job_vtt_content( detail="Access denied" ) + # Default to source language if not specified + target_language = language or job_doc["source"].get("language", "en") + outputs = job_doc.get("outputs", {}) - lang_output = outputs.get(language, {}) + lang_output = outputs.get(target_language, {}) response = VttContentResponse() @@ -709,7 +755,7 @@ async def update_job_vtt_content( current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.PRODUCTION, UserRole.ADMIN)), db: AsyncIOMotorDatabase = Depends(get_database), ): - """Update VTT content for a job""" + """Update VTT content for a job. If language is not specified, updates source language content.""" job_doc = await db.jobs.find_one({"_id": job_id}) if not job_doc: raise HTTPException( @@ -724,8 +770,11 @@ async def update_job_vtt_content( detail="VTT content can only be edited during QC phase" ) + # Default to source language if not specified + target_language = request.language or job_doc["source"].get("language", "en") + outputs = job_doc.get("outputs", {}) - lang_output = outputs.get(request.language, {}) + lang_output = outputs.get(target_language, {}) # Validate and update captions VTT if request.captions_vtt is not None: @@ -740,7 +789,7 @@ async def update_job_vtt_content( # Upload updated VTT new_captions_uri = await upload_vtt_to_gcs( request.captions_vtt, - f"{job_id}/{request.language}/captions.vtt" + f"{job_id}/{target_language}/captions.vtt" ) lang_output["captions_vtt_gcs"] = new_captions_uri @@ -757,12 +806,12 @@ async def update_job_vtt_content( # Upload updated VTT new_ad_uri = await upload_vtt_to_gcs( request.audio_description_vtt, - f"{job_id}/{request.language}/ad.vtt" + f"{job_id}/{target_language}/ad.vtt" ) lang_output["ad_vtt_gcs"] = new_ad_uri # Update job with new VTT content - outputs[request.language] = lang_output + outputs[target_language] = lang_output result = await db.jobs.find_one_and_update( {"_id": job_id}, @@ -776,7 +825,7 @@ async def update_job_vtt_content( "at": datetime.utcnow(), "status": "vtt_updated", "by": str(current_user.id), - "notes": f"Updated VTT content for {request.language}" + "notes": f"Updated VTT content for {target_language}" } } }, diff --git a/backend/app/models/job.py b/backend/app/models/job.py index 5529978..5ea6c9a 100644 --- a/backend/app/models/job.py +++ b/backend/app/models/job.py @@ -10,7 +10,8 @@ class JobStatus(str, Enum): INGESTING = "ingesting" AI_PROCESSING = "ai_processing" PENDING_QC = "pending_qc" - APPROVED_ENGLISH = "approved_english" + APPROVED_ENGLISH = "approved_english" # For English source videos + APPROVED_SOURCE = "approved_source" # For non-English source videos REJECTED = "rejected" QC_FEEDBACK = "qc_feedback" TRANSLATING = "translating" @@ -18,13 +19,20 @@ class JobStatus(str, Enum): PENDING_FINAL_REVIEW = "pending_final_review" COMPLETED = "completed" + @classmethod + def is_approved(cls, status: str) -> bool: + """Check if status indicates source approval (any language)""" + return status in [cls.APPROVED_ENGLISH.value, cls.APPROVED_SOURCE.value] + class Source(BaseModel): filename: str original_filename: Optional[str] = None gcs_uri: str duration_s: Optional[float] = None - language: constr(min_length=2, max_length=10) = "en" + language: constr(min_length=2, max_length=10) = "en" # Final source language (from detection or explicit) + language_hint: Optional[str] = None # User-provided hint for non-English videos + detected_language: Optional[str] = None # AI-detected language from Gemini class RequestedOutputs(BaseModel): @@ -82,7 +90,8 @@ class Job(BaseModel): class JobCreate(BaseModel): title: str - language: str = "en" + source_is_english: bool = True # True = English source, False = other language (auto-detect) + language_hint: Optional[str] = None # Optional hint when source_is_english=False requested_outputs: RequestedOutputs diff --git a/backend/app/schemas/job.py b/backend/app/schemas/job.py index c83007a..3015889 100644 --- a/backend/app/schemas/job.py +++ b/backend/app/schemas/job.py @@ -26,7 +26,8 @@ class JobListResponse(BaseModel): class JobCreateRequest(BaseModel): title: str - language: str = "en" + source_is_english: bool = True # True = English source, False = other language (auto-detect) + language_hint: Optional[str] = None # Optional hint when source_is_english=False requested_outputs: RequestedOutputs @@ -39,6 +40,11 @@ class ApproveEnglishRequest(BaseModel): notes: Optional[str] = None +class ApproveSourceRequest(BaseModel): + """Request to approve source language content (works for any language)""" + notes: Optional[str] = None + + class RejectJobRequest(BaseModel): notes: str @@ -50,7 +56,7 @@ class CompleteJobRequest(BaseModel): class VttUpdateRequest(BaseModel): captions_vtt: Optional[str] = None audio_description_vtt: Optional[str] = None - language: str = "en" + language: Optional[str] = None # If None, defaults to source language class VttTimingAdjustRequest(BaseModel): diff --git a/backend/app/services/translate.py b/backend/app/services/translate.py index be08fda..24d1caf 100644 --- a/backend/app/services/translate.py +++ b/backend/app/services/translate.py @@ -14,9 +14,16 @@ class TranslateService: logger.warning("Google Translate API key not configured") self.client = None - async def translate_vtt(self, vtt_content: str, target_language: str) -> str: + async def translate_vtt( + self, vtt_content: str, target_language: str, source_language: str = "en" + ) -> str: """ - Translate VTT content while preserving timing and structure + Translate VTT content while preserving timing and structure. + + Args: + vtt_content: The VTT file content to translate + target_language: The language code to translate to (e.g., 'es', 'fr') + source_language: The source language code (default: 'en') """ if not self.client: raise ValueError("Google Translate not configured") @@ -35,7 +42,7 @@ class TranslateService: results = self.client.translate( texts_to_translate, target_language=target_language, - source_language="en" + source_language=source_language # Use parameter instead of hardcoded "en" ) # Rebuild VTT with translated text diff --git a/backend/app/tasks/ingest_and_ai.py b/backend/app/tasks/ingest_and_ai.py index 4f6ed71..0fe7dd4 100644 --- a/backend/app/tasks/ingest_and_ai.py +++ b/backend/app/tasks/ingest_and_ai.py @@ -192,7 +192,7 @@ async def ingest_and_ai_task_impl(job_id: str): # Final safety check for required fields required_fields = ["captions_vtt", "audio_description_vtt"] missing_fields = [field for field in required_fields if field not in ai_result] - + if missing_fields: logger.error(f"Missing required fields after AI processing: {missing_fields}") # Create fallback content for missing fields @@ -200,26 +200,49 @@ async def ingest_and_ai_task_impl(job_id: str): ai_result["audio_description_vtt"] = "WEBVTT\n\n00:00:00.000 --> 00:00:05.000\nVideo content with visual elements." logger.info("Created fallback audio_description_vtt") - # Upload VTT files to GCS + # Get detected language from Gemini response + detected_language = ai_result.get("language", "en") + language_hint = job_doc["source"].get("language_hint") + initial_language = job_doc["source"].get("language", "en") + + # Log if there's a mismatch between hint/initial and detected language + if language_hint and language_hint != detected_language: + logger.warning( + f"Language mismatch for job {job_id}: " + f"hint={language_hint}, detected={detected_language}" + ) + elif initial_language != "auto" and initial_language != detected_language: + logger.info( + f"Language detection for job {job_id}: " + f"initial={initial_language}, detected={detected_language}" + ) + + # Use detected language for output storage + source_language = detected_language + logger.info(f"Using detected language '{source_language}' for job {job_id}") + + # Upload VTT files to GCS using detected language captions_gcs_uri = await upload_vtt_to_gcs( ai_result["captions_vtt"], - f"{job_id}/en/captions.vtt" + f"{job_id}/{source_language}/captions.vtt" ) ad_gcs_uri = await upload_vtt_to_gcs( ai_result["audio_description_vtt"], - f"{job_id}/en/ad.vtt" + f"{job_id}/{source_language}/ad.vtt" ) - # Update job with AI results and outputs + # Update job with AI results, detected language, and outputs await db.jobs.update_one( {"_id": job_id}, { "$set": { "status": JobStatus.PENDING_QC.value, + "source.language": source_language, # Update with detected language + "source.detected_language": detected_language, "ai.ingestion_json": ai_result, "ai.confidence": ai_result["confidence"], - "outputs.en": { + f"outputs.{source_language}": { "captions_vtt_gcs": captions_gcs_uri, "ad_vtt_gcs": ad_gcs_uri }, diff --git a/backend/app/tasks/translate_and_synthesize.py b/backend/app/tasks/translate_and_synthesize.py index 2f511f6..5c492c1 100644 --- a/backend/app/tasks/translate_and_synthesize.py +++ b/backend/app/tasks/translate_and_synthesize.py @@ -135,11 +135,14 @@ async def _async_translate_and_synthesize(job_id: str): job_title = job_doc.get("title", "Untitled Job") logger.info(f"✅ Found job document for {job_id} ({job_title}), status: {job_doc.get('status', 'UNKNOWN')}") - if job_doc["status"] != JobStatus.APPROVED_ENGLISH.value: - logger.warning(f"⚠️ Job {job_id} not in approved_english status (current: {job_doc['status']}), skipping") + # Check for any approved status (English or non-English source) + if not JobStatus.is_approved(job_doc["status"]): + logger.warning(f"⚠️ Job {job_id} not in approved status (current: {job_doc['status']}), skipping") return - - logger.info(f"✅ Job {job_id} is in correct status, proceeding with translation") + + # Get source language from job + source_language = job_doc["source"].get("language", "en") + logger.info(f"✅ Job {job_id} is in correct status, proceeding with translation (source: {source_language})") # Update status to translating await db.jobs.update_one( @@ -159,18 +162,20 @@ async def _async_translate_and_synthesize(job_id: str): } ) - # Get English VTT content - en_outputs = job_doc["outputs"]["en"] + # Get source language VTT content + source_outputs = job_doc["outputs"].get(source_language) + if not source_outputs: + raise ValueError(f"No outputs found for source language {source_language}") - # Download English VTT files - captions_blob_path = en_outputs["captions_vtt_gcs"].replace(f"gs://{settings.gcs_bucket}/", "") - ad_blob_path = en_outputs["ad_vtt_gcs"].replace(f"gs://{settings.gcs_bucket}/", "") + # Download source language VTT files + captions_blob_path = source_outputs["captions_vtt_gcs"].replace(f"gs://{settings.gcs_bucket}/", "") + ad_blob_path = source_outputs["ad_vtt_gcs"].replace(f"gs://{settings.gcs_bucket}/", "") captions_blob = gcs_service.bucket.blob(captions_blob_path) ad_blob = gcs_service.bucket.blob(ad_blob_path) - en_captions_vtt = captions_blob.download_as_text() - en_ad_vtt = ad_blob.download_as_text() + source_captions_vtt = captions_blob.download_as_text() + source_ad_vtt = ad_blob.download_as_text() # Process each requested language requested_languages = job_doc["requested_outputs"]["languages"] @@ -179,22 +184,22 @@ async def _async_translate_and_synthesize(job_id: str): updated_outputs = job_doc.get("outputs", {}) for language in requested_languages: - if language == "en": - continue # Skip English as it's already processed + if language == source_language: + continue # Skip source language as it's already processed - logger.info(f"Processing language: {language}") + logger.info(f"Processing language: {language} (from source: {source_language})") try: if language in transcreation_languages: # Use transcreation for cultural adaptation with retry async def transcreate(): return await gemini_service.transcreate_content( - en_captions_vtt, - en_ad_vtt, + source_captions_vtt, + source_ad_vtt, language, brief="Standard accessibility content" ) - + result = await retry_with_backoff(transcreate, max_retries=3) translated_captions = result["captions_vtt"] translated_ad = result["audio_description_vtt"] @@ -203,11 +208,15 @@ async def _async_translate_and_synthesize(job_id: str): else: # Use standard translation with retry async def translate_captions(): - return await translate_service.translate_vtt(en_captions_vtt, language) - + return await translate_service.translate_vtt( + source_captions_vtt, language, source_language=source_language + ) + async def translate_ad(): - return await translate_service.translate_vtt(en_ad_vtt, language) - + return await translate_service.translate_vtt( + source_ad_vtt, language, source_language=source_language + ) + translated_captions = await retry_with_backoff(translate_captions, max_retries=3) translated_ad = await retry_with_backoff(translate_ad, max_retries=3) origin = "translate" @@ -260,7 +269,7 @@ async def _async_translate_and_synthesize(job_id: str): # Generate TTS for languages that need MP3 if job_doc["requested_outputs"]["audio_description_mp3"]: - await _generate_tts_for_languages(job_id, updated_outputs, db) + await _generate_tts_for_languages(job_id, updated_outputs, db, source_language) # Update final status await db.jobs.update_one( @@ -314,16 +323,16 @@ async def _async_translate_and_synthesize(job_id: str): client.close() -async def _generate_tts_for_languages(job_id: str, outputs: dict[str, Any], db): +async def _generate_tts_for_languages(job_id: str, outputs: dict[str, Any], db, source_language: str = "en"): """Generate TTS audio for each language's audio description""" - # Always generate English MP3 - if "en" in outputs: - await _generate_language_tts(job_id, "en", outputs["en"], db) + # Always generate source language MP3 first + if source_language in outputs and "ad_vtt_gcs" in outputs[source_language]: + await _generate_language_tts(job_id, source_language, outputs[source_language], db) # Generate for other languages for language, lang_output in outputs.items(): - if language != "en" and "ad_vtt_gcs" in lang_output: + if language != source_language and "ad_vtt_gcs" in lang_output: await _generate_language_tts(job_id, language, lang_output, db) diff --git a/frontend/src/components/StatusBadge.tsx b/frontend/src/components/StatusBadge.tsx index 8b9a1e1..e0f63ce 100644 --- a/frontend/src/components/StatusBadge.tsx +++ b/frontend/src/components/StatusBadge.tsx @@ -16,6 +16,7 @@ export function StatusBadge({ status }: StatusBadgeProps) { case 'pending_qc': return 'bg-yellow-100 text-yellow-800'; case 'approved_english': + case 'approved_source': return 'bg-green-100 text-green-800'; case 'rejected': return 'bg-red-100 text-red-800'; @@ -43,7 +44,9 @@ export function StatusBadge({ status }: StatusBadgeProps) { case 'pending_qc': return 'Pending QC'; case 'approved_english': - return 'Approved English'; + return 'Approved (EN)'; + case 'approved_source': + return 'Approved for Translation'; case 'rejected': return 'Rejected'; case 'translating': diff --git a/frontend/src/hooks/useJob.ts b/frontend/src/hooks/useJob.ts index ec1121a..b7cd408 100644 --- a/frontend/src/hooks/useJob.ts +++ b/frontend/src/hooks/useJob.ts @@ -36,9 +36,9 @@ export function useJobDownloads(jobId: string) { }); } -export function useJobVttContent(jobId: string, language: string = 'en') { +export function useJobVttContent(jobId: string, language?: string) { return useQuery({ - queryKey: ['jobs', jobId, 'vtt', language], + queryKey: ['jobs', jobId, 'vtt', language || 'source'], queryFn: () => apiClient.getJobVttContent(jobId, language), enabled: !!jobId, staleTime: 30000, // 30 seconds @@ -86,7 +86,7 @@ export function useUpdateJob() { export function useApproveEnglish() { const queryClient = useQueryClient(); - + return useMutation({ mutationFn: ({ id, notes }: { id: string; notes?: string }) => apiClient.approveEnglish(id, notes), @@ -97,6 +97,19 @@ export function useApproveEnglish() { }); } +export function useApproveSource() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ id, notes }: { id: string; notes?: string }) => + apiClient.approveSource(id, notes), + onSuccess: (_, { id }) => { + queryClient.invalidateQueries({ queryKey: ['jobs', id] }); + queryClient.invalidateQueries({ queryKey: ['jobs'] }); + }, + }); +} + export function useRejectJob() { const queryClient = useQueryClient(); @@ -125,13 +138,14 @@ export function useCompleteJob() { export function useUpdateJobVtt() { const queryClient = useQueryClient(); - + return useMutation({ mutationFn: ({ id, data }: { id: string; data: VttUpdateRequest }) => apiClient.updateJobVttContent(id, data), onSuccess: (_, { id, data }) => { queryClient.invalidateQueries({ queryKey: ['jobs', id] }); - queryClient.invalidateQueries({ queryKey: ['jobs', id, 'vtt', data.language] }); + // Invalidate the VTT cache for the specific language or 'source' if not specified + queryClient.invalidateQueries({ queryKey: ['jobs', id, 'vtt', data.language || 'source'] }); }, }); } diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 5bcef38..8e897fe 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -141,7 +141,10 @@ class ApiClient { async createJob(data: JobCreateRequest, file: File, onUploadProgress?: (progressEvent: { loaded: number; total: number }) => void): Promise { const formData = new FormData(); formData.append('title', data.title); - formData.append('language', data.language); + formData.append('source_is_english', String(data.source_is_english)); + if (data.language_hint) { + formData.append('language_hint', data.language_hint); + } formData.append('requested_outputs', JSON.stringify(data.requested_outputs)); formData.append('file', file); @@ -168,7 +171,12 @@ class ApiClient { } async approveEnglish(id: string, notes?: string): Promise { - const response = await this.client.post(`/jobs/${id}/actions/approve_english`, { notes }); + // Legacy method - calls approve_source for backwards compatibility + return this.approveSource(id, notes); + } + + async approveSource(id: string, notes?: string): Promise { + const response = await this.client.post(`/jobs/${id}/actions/approve_source`, { notes }); return response.data; } @@ -192,8 +200,9 @@ class ApiClient { return response.data; } - async getJobVttContent(id: string, language: string = 'en'): Promise { - const response = await this.client.get(`/jobs/${id}/vtt?language=${language}`); + async getJobVttContent(id: string, language?: string): Promise { + const params = language ? `?language=${language}` : ''; + const response = await this.client.get(`/jobs/${id}/vtt${params}`); return response.data; } diff --git a/frontend/src/routes/admin/QCDetail.tsx b/frontend/src/routes/admin/QCDetail.tsx index e734e0d..3a13c88 100644 --- a/frontend/src/routes/admin/QCDetail.tsx +++ b/frontend/src/routes/admin/QCDetail.tsx @@ -11,7 +11,9 @@ export function QCDetail() { const navigate = useNavigate(); const toast = useToastContext(); const { data: job, isLoading, error } = useJob(id!); - const { data: vttContent, isLoading: vttLoading } = useJobVttContent(id!, 'en'); + // Get source language from job (default to 'en' for backwards compatibility) + const sourceLanguage = job?.source?.language || 'en'; + const { data: vttContent, isLoading: vttLoading } = useJobVttContent(id!, sourceLanguage); const { data: downloads } = useJobDownloads(id!); const approveEnglishMutation = useApproveEnglish(); const rejectJobMutation = useRejectJob(); @@ -99,14 +101,14 @@ export function QCDetail() { const saveVttChanges = async () => { if (!id || !hasUnsavedChanges) return; - + try { await updateVttMutation.mutateAsync({ id, data: { captions_vtt: captionsVtt, audio_description_vtt: adVtt, - language: 'en' + language: sourceLanguage // Use source language instead of hardcoded 'en' } }); setHasUnsavedChanges(false); @@ -166,12 +168,12 @@ export function QCDetail() { const handleTimingAdjustment = async () => { if (!id || timingOffset === 0) return; - + try { await adjustTimingMutation.mutateAsync({ id, offsetSeconds: timingOffset, - language: 'en', + language: sourceLanguage, // Use source language instead of hardcoded 'en' adjustCaptions, adjustAudioDescription, }); @@ -218,6 +220,7 @@ export function QCDetail() {
Source: {job.source.filename} + Language: {sourceLanguage.toUpperCase()} {job.source.duration_s && ( Duration: {Math.round(job.source.duration_s)}s )} @@ -506,7 +509,7 @@ export function QCDetail() { disabled={isProcessing} className="px-6 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 disabled:opacity-50" > - {approveEnglishMutation.isPending ? 'Approving...' : 'Approve English Version'} + {approveEnglishMutation.isPending ? 'Approving...' : `Approve ${sourceLanguage.toUpperCase()} Version`}