import os import logging import aiofiles from fastapi import APIRouter, Depends, UploadFile, File, HTTPException from fastapi.responses import FileResponse from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from ..models.database import get_session from ..models.tables import Video, Segment, Job, VideoStatus, JobStatus from ..models.schemas import ( VideoOut, VideoListOut, SegmentOut, SegmentUpdate, TranscriptOut, ProcessResponse, ) from ..services.video_utils import get_video_duration from ..config import settings from ..tasks.celery_app import celery_app logger = logging.getLogger(__name__) router = APIRouter() @router.get("", response_model=VideoListOut) async def list_videos(session: AsyncSession = Depends(get_session)): result = await session.execute(select(Video).order_by(Video.created_at.desc())) videos = result.scalars().all() return VideoListOut(videos=[VideoOut.model_validate(v) for v in videos]) @router.get("/{video_id}", response_model=VideoOut) async def get_video(video_id: int, session: AsyncSession = Depends(get_session)): result = await session.execute(select(Video).where(Video.id == video_id)) video = result.scalar_one_or_none() if not video: raise HTTPException(404, "Video not found") return VideoOut.model_validate(video) @router.post("/upload", response_model=VideoOut) async def upload_video( file: UploadFile = File(...), session: AsyncSession = Depends(get_session) ): if not file.filename: raise HTTPException(400, "No filename") allowed = {".mp4", ".mkv", ".mov", ".avi", ".webm", ".m4v"} ext = os.path.splitext(file.filename)[1].lower() if ext not in allowed: raise HTTPException( 400, f"Unsupported format: {ext}. Allowed: {', '.join(allowed)}" ) dest = os.path.join(settings.video_dir, file.filename) async with aiofiles.open(dest, "wb") as f: while chunk := await file.read(64 * 1024): await f.write(chunk) duration = get_video_duration(dest) video = Video( filename=file.filename, filepath=dest, duration=duration, status=VideoStatus.uploaded.value, ) session.add(video) await session.commit() await session.refresh(video) return VideoOut.model_validate(video) @router.delete("/{video_id}") async def delete_video(video_id: int, session: AsyncSession = Depends(get_session)): result = await session.execute(select(Video).where(Video.id == video_id)) video = result.scalar_one_or_none() if not video: raise HTTPException(404, "Video not found") if os.path.exists(video.filepath): os.remove(video.filepath) if video.output_path: output_full = os.path.join(settings.video_dir, video.output_path) if os.path.exists(output_full): os.remove(output_full) await session.delete(video) await session.commit() return {"ok": True} @router.post("/{video_id}/process", response_model=ProcessResponse) async def start_processing(video_id: int, session: AsyncSession = Depends(get_session)): result = await session.execute(select(Video).where(Video.id == video_id)) video = result.scalar_one_or_none() if not video: raise HTTPException(404, "Video not found") if video.status not in (VideoStatus.uploaded.value, VideoStatus.error.value): raise HTTPException(400, f"Cannot process video in status '{video.status}'") existing_job = await session.execute( select(Job).where( Job.video_id == video_id, Job.job_type == "process", Job.status.in_([JobStatus.pending.value, JobStatus.running.value]), ) ) if existing_job.scalar_one_or_none(): raise HTTPException(400, "A process job is already running for this video") job = Job(video_id=video_id, job_type="process", status=JobStatus.pending.value) session.add(job) await session.commit() await session.refresh(job) video.status = VideoStatus.queued.value await session.commit() celery_app.send_task( "app.tasks.process.process_video", kwargs=dict(video_id=video_id, job_id=job.id) ) return ProcessResponse(job_id=job.id, message="Processing started") @router.get("/{video_id}/transcript", response_model=TranscriptOut) async def get_transcript(video_id: int, session: AsyncSession = Depends(get_session)): result = await session.execute( select(Segment).where(Segment.video_id == video_id).order_by(Segment.start_time) ) segments = result.scalars().all() return TranscriptOut( video_id=video_id, segments=[SegmentOut.model_validate(s) for s in segments], ) @router.put("/{video_id}/transcript/{segment_id}", response_model=SegmentOut) async def update_segment( video_id: int, segment_id: int, update: SegmentUpdate, session: AsyncSession = Depends(get_session), ): result = await session.execute( select(Segment).where(Segment.id == segment_id, Segment.video_id == video_id) ) seg = result.scalar_one_or_none() if not seg: raise HTTPException(404, "Segment not found") if update.english_text is not None: seg.english_text = update.english_text if update.danish_text is not None: seg.danish_text = update.danish_text if update.speaker_label is not None: seg.speaker_label = update.speaker_label await session.commit() await session.refresh(seg) return SegmentOut.model_validate(seg) @router.get("/{video_id}/file") async def stream_video(video_id: int, session: AsyncSession = Depends(get_session)): result = await session.execute(select(Video).where(Video.id == video_id)) video = result.scalar_one_or_none() if not video: raise HTTPException(404, "Video not found") if not os.path.exists(video.filepath): raise HTTPException(404, "Video file not found on disk") return FileResponse(video.filepath, media_type="video/mp4", filename=video.filename) @router.get("/file/{filename}") async def stream_output(filename: str): full_path = os.path.join(settings.video_dir, filename) if not os.path.exists(full_path): raise HTTPException(404, "File not found") return FileResponse(full_path) @router.post("/{video_id}/generate", response_model=ProcessResponse) async def start_generation(video_id: int, session: AsyncSession = Depends(get_session)): result = await session.execute(select(Video).where(Video.id == video_id)) video = result.scalar_one_or_none() if not video: raise HTTPException(404, "Video not found") if video.status != VideoStatus.review.value: raise HTTPException(400, f"Cannot generate audio in status '{video.status}'") existing_job = await session.execute( select(Job).where( Job.video_id == video_id, Job.job_type == "generate", Job.status.in_([JobStatus.pending.value, JobStatus.running.value]), ) ) if existing_job.scalar_one_or_none(): raise HTTPException(400, "A generate job is already running for this video") job = Job(video_id=video_id, job_type="generate", status=JobStatus.pending.value) session.add(job) await session.commit() await session.refresh(job) video.status = VideoStatus.generating.value await session.commit() celery_app.send_task( "app.tasks.generate.generate_audio", kwargs=dict(video_id=video_id, job_id=job.id), ) return ProcessResponse(job_id=job.id, message="Audio generation started")