diff --git a/backend/app/models/job.py b/backend/app/models/job.py index 3ac54ca..41b0515 100644 --- a/backend/app/models/job.py +++ b/backend/app/models/job.py @@ -18,6 +18,7 @@ class JobStatus(str, Enum): TTS_GENERATING = "tts_generating" TTS_FAILED = "tts_failed" # TTS synthesis failed after retries, requires reprocessing RENDERING_VIDEO = "rendering_video" # Accessible video rendering in progress + RENDER_FAILED = "render_failed" # Accessible video rendering failed, requires reprocessing PENDING_FINAL_REVIEW = "pending_final_review" COMPLETED = "completed" diff --git a/backend/app/tasks/render_accessible_video.py b/backend/app/tasks/render_accessible_video.py index d035874..352a8d2 100644 --- a/backend/app/tasks/render_accessible_video.py +++ b/backend/app/tasks/render_accessible_video.py @@ -293,6 +293,10 @@ async def _async_render_accessible_video(job_id: str, language: str): } ) + # Check if all videos are now finished (completed or failed) to update job status + # This ensures the job transitions to RENDER_FAILED if all languages have finished + await _check_accessible_video_completion(job_id, db) + raise finally: @@ -308,48 +312,95 @@ async def _check_accessible_video_completion(job_id: str, db): progress = job_doc.get("accessible_video_progress", {}) requested_languages = job_doc["requested_outputs"]["languages"] - # Check if all requested languages have completed accessible video - all_complete = True - _any_failed = False + # Check status of all requested languages + all_finished = True # All languages have either completed or failed + any_failed = False + failed_languages = [] for language in requested_languages: lang_progress = progress.get(language, {}) status = lang_progress.get("status", "pending") if status == "failed": - _any_failed = True # noqa: F841 - reserved for future use - elif status != "completed": - all_complete = False + any_failed = True + failed_languages.append({ + "language": language, + "error": lang_progress.get("error_message", "Unknown error") + }) + elif status not in ["completed", "failed"]: + # Still pending or rendering + all_finished = False - if all_complete: - logger.info(f"All accessible videos complete for job {job_id}") + job_title = job_doc.get("title", "Untitled Job") - # If job is still in TTS_GENERATING or RENDERING_VIDEO, transition to PENDING_FINAL_REVIEW - if job_doc["status"] in [JobStatus.TTS_GENERATING.value, JobStatus.RENDERING_VIDEO.value]: - await db.jobs.update_one( - {"_id": job_id}, - { - "$set": { - "status": JobStatus.PENDING_FINAL_REVIEW.value, - "updated_at": datetime.utcnow() - }, - "$push": { - "review.history": { - "at": datetime.utcnow(), - "status": JobStatus.PENDING_FINAL_REVIEW.value, - "by": "system" + # Only update job status if all languages have finished processing + if all_finished: + if any_failed: + # Some or all videos failed - transition to RENDER_FAILED + logger.error(f"Accessible video rendering failed for job {job_id}: {len(failed_languages)} language(s) failed") + + # Build error summary + error_summary = "; ".join([f"{f['language']}: {f['error']}" for f in failed_languages]) + + if job_doc["status"] in [JobStatus.TTS_GENERATING.value, JobStatus.RENDERING_VIDEO.value]: + await db.jobs.update_one( + {"_id": job_id}, + { + "$set": { + "status": JobStatus.RENDER_FAILED.value, + "error": { + "type": "render_failure", + "failed_languages": failed_languages, + "message": f"Video rendering failed for {len(failed_languages)} language(s): {error_summary}", + "timestamp": datetime.utcnow().isoformat() + }, + "updated_at": datetime.utcnow() + }, + "$push": { + "review.history": { + "at": datetime.utcnow(), + "status": JobStatus.RENDER_FAILED.value, + "by": "system", + "notes": f"Rendering failed for: {', '.join([f['language'] for f in failed_languages])}" + } } } - } - ) + ) - job_title = job_doc.get("title", "Untitled Job") - broadcast_status_update( - job_id, - JobStatus.PENDING_FINAL_REVIEW.value, - job_title=job_title, - message=f"{job_title} has all accessible videos complete - ready for Final Review" - ) + broadcast_status_update( + job_id, + JobStatus.RENDER_FAILED.value, + job_title=job_title, + message=f"{job_title} - Video rendering failed for {len(failed_languages)} language(s). Manual reprocessing required." + ) + else: + # All videos completed successfully + logger.info(f"All accessible videos complete for job {job_id}") + + if job_doc["status"] in [JobStatus.TTS_GENERATING.value, JobStatus.RENDERING_VIDEO.value]: + await db.jobs.update_one( + {"_id": job_id}, + { + "$set": { + "status": JobStatus.PENDING_FINAL_REVIEW.value, + "updated_at": datetime.utcnow() + }, + "$push": { + "review.history": { + "at": datetime.utcnow(), + "status": JobStatus.PENDING_FINAL_REVIEW.value, + "by": "system" + } + } + } + ) + + broadcast_status_update( + job_id, + JobStatus.PENDING_FINAL_REVIEW.value, + job_title=job_title, + message=f"{job_title} has all accessible videos complete - ready for Final Review" + ) async def _refine_pause_points_with_whisper( diff --git a/frontend/src/components/StatusBadge.tsx b/frontend/src/components/StatusBadge.tsx index 98a57f9..cb8fa03 100644 --- a/frontend/src/components/StatusBadge.tsx +++ b/frontend/src/components/StatusBadge.tsx @@ -20,6 +20,7 @@ export function StatusBadge({ status }: StatusBadgeProps) { return 'bg-green-100 text-green-800'; case 'rejected': case 'tts_failed': + case 'render_failed': return 'bg-red-100 text-red-800'; case 'translating': return 'bg-blue-100 text-blue-800'; @@ -54,6 +55,8 @@ export function StatusBadge({ status }: StatusBadgeProps) { return 'Rejected'; case 'tts_failed': return 'TTS Failed'; + case 'render_failed': + return 'Render Failed'; case 'translating': return 'Translating'; case 'tts_generating': diff --git a/frontend/src/routes/jobs/JobDetail.tsx b/frontend/src/routes/jobs/JobDetail.tsx index 242f14d..338afd4 100644 --- a/frontend/src/routes/jobs/JobDetail.tsx +++ b/frontend/src/routes/jobs/JobDetail.tsx @@ -16,6 +16,7 @@ const ProgressIndicator = ({ status }: { status: string }) => { 'approved', // Generic - matches both approved_english and approved_source 'translating', 'tts_generating', + 'rendering_video', 'pending_final_review', 'completed' ]; @@ -24,6 +25,12 @@ const ProgressIndicator = ({ status }: { status: string }) => { const normalizedStatus = (status === 'approved_english' || status === 'approved_source') ? 'approved' : status; const currentIndex = steps.indexOf(normalizedStatus); const isRejected = status === 'rejected'; + const isFailed = status === 'tts_failed' || status === 'render_failed'; + + // For failed states, find which step they failed at + const failedAtStep = status === 'tts_failed' ? 'tts_generating' : + status === 'render_failed' ? 'rendering_video' : null; + const failedAtIndex = failedAtStep ? steps.indexOf(failedAtStep) : -1; if (isRejected) { return ( @@ -36,6 +43,37 @@ const ProgressIndicator = ({ status }: { status: string }) => { ); } + if (isFailed) { + const failedStepLabel = failedAtStep?.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); + return ( +
+ Processing failed at {failedStepLabel}. Check error details for more information. +
+{String(job.error.message || 'Unknown error')}
++ {String(job.error.message || 'An error occurred during processing')} +
+ {job.error.type && ( ++ Type: {String(job.error.type).replace(/_/g, ' ')} +
+ )} + {/* Show failed languages for render failures */} + {job.error.failed_languages && Array.isArray(job.error.failed_languages) && ( +Failed Languages:
++ {formatDistanceToNow(new Date(String(job.error.timestamp)))} ago +
+ )}