from __future__ import annotations import hashlib import os from pathlib import Path from fastapi import FastAPI, File, HTTPException, UploadFile from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from app.config import settings from app.jobs import JobStatus, create_job, get_job, job_to_dict from app.pipeline.orchestrator import run_pipeline app = FastAPI( title=settings.app_name, version=settings.app_version, description="Detect-X forensic media analysis API (Alpha)", ) origins = [o.strip() for o in settings.cors_origins.split(",") if o.strip()] app.add_middleware( CORSMiddleware, allow_origins=origins if origins != ["*"] else ["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) def models_ready() -> dict[str, bool]: models_path = Path(settings.models_dir) visual = models_path / settings.visual_model_file audio = models_path / settings.audio_model_file return { "visual_model": visual.exists(), "audio_model": audio.exists(), "models_dir": str(models_path), } @app.get("/") def root(): return { "service": settings.app_name, "version": settings.app_version, "status": "online", "docs": "/docs", } @app.get("/health") def health(): ready = models_ready() return { "status": "healthy" if all(ready.values()) else "degraded", "models": ready, "limits": { "max_upload_mb": settings.max_upload_mb, "max_video_seconds": settings.max_video_seconds, }, } @app.post("/analyze") async def analyze(file: UploadFile = File(...)): if not file.filename: raise HTTPException(status_code=400, detail="Filename is required") content = await file.read() size_mb = len(content) / (1024 * 1024) if size_mb > settings.max_upload_mb: raise HTTPException( status_code=413, detail=f"File exceeds {settings.max_upload_mb} MB limit", ) file_hash = hashlib.sha256(content).hexdigest() job = create_job(file.filename) job.status = JobStatus.RUNNING job.progress = 5 job.result = {"sha256": file_hash, "size_bytes": len(content)} def on_progress(stage: str, progress: int) -> None: job.current_stage = stage job.progress = progress try: job.result = run_pipeline( job=job, content=content, filename=file.filename, on_progress=on_progress, ) job.result["sha256"] = file_hash job.result["size_bytes"] = len(content) job.status = JobStatus.COMPLETED job.progress = 100 job.current_stage = "report" except ValueError as exc: job.status = JobStatus.FAILED job.error = str(exc) raise HTTPException(status_code=400, detail=str(exc)) from exc except Exception as exc: job.status = JobStatus.FAILED job.error = str(exc) raise HTTPException(status_code=500, detail=f"Analysis failed: {exc}") from exc return JSONResponse(job_to_dict(job)) @app.get("/jobs/{job_id}") def job_status(job_id: str): job = get_job(job_id) if not job: raise HTTPException(status_code=404, detail="Job not found") return job_to_dict(job) if os.getenv("SPACE_ID"): @app.get("/spaces-config") def spaces_config(): return {"ready": True}