import asyncio import json import uuid from fastapi import APIRouter from sqlmodel import Session, select from sse_starlette.sse import EventSourceResponse from app.api.deps import CurrentUser from app.core.db import engine from app.models import ( ClipStatus, CutToMusicVideo, ProcessingJob, Project, ProjectStatus, ) router = APIRouter(prefix="/sse", tags=["sse"]) @router.get("/projects/{id}/progress") async def project_progress_stream( current_user: CurrentUser, id: uuid.UUID, ): """ SSE endpoint for real-time project progress updates. """ async def event_generator(): # Initial authorization check with Session(engine) as db_session: project = db_session.get(Project, id) if not project or project.owner_id != current_user.id: yield { "event": "error", "data": json.dumps({"error": "Project not found or access denied"}), } return while True: with Session(engine) as db_session: project = db_session.get(Project, id) if not project: break # Get latest job jobs = db_session.exec( select(ProcessingJob) .where(ProcessingJob.project_id == id) .order_by(ProcessingJob.created_at.desc()) ).all() current_job = jobs[0] if jobs else None # Get cut-to-music video statuses ctm_videos = db_session.exec( select(CutToMusicVideo).where(CutToMusicVideo.project_id == id) ).all() ctm_data = [ { "id": str(v.id), "run_index": v.run_index, "status": v.status.value, "progress_percent": v.progress_percent, } for v in sorted(ctm_videos, key=lambda v: v.run_index) ] data = { "project_status": project.status.value, "job_status": current_job.status if current_job else None, "progress": current_job.progress_percent if current_job else 0, "current_step": current_job.current_step if current_job else None, "error_message": project.error_message, "cut_to_music_videos": ctm_data, } yield { "event": "progress", "data": json.dumps(data), } # Stop when clips are done AND all cut-to-music videos have settled clips_done = project.status in [ProjectStatus.COMPLETED, ProjectStatus.FAILED] ctm_all_settled = all( v.status in (ClipStatus.COMPLETED, ClipStatus.FAILED) for v in ctm_videos ) if ctm_videos else True if clips_done and ctm_all_settled: break await asyncio.sleep(1) return EventSourceResponse(event_generator())