Spaces:
Running
Running
| 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), | |
| } | |
| def root(): | |
| return { | |
| "service": settings.app_name, | |
| "version": settings.app_version, | |
| "status": "online", | |
| "docs": "/docs", | |
| } | |
| 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, | |
| }, | |
| } | |
| 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)) | |
| 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"): | |
| def spaces_config(): | |
| return {"ready": True} | |