solvion-iq Cursor commited on
Commit
8084565
·
1 Parent(s): 2d110e6

Deploy Detect-X API with Docker and models

Browse files

Co-authored-by: Cursor <cursoragent@cursor.com>

Dockerfile ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ RUN apt-get update && apt-get install -y --no-install-recommends \
4
+ ffmpeg \
5
+ libgl1 \
6
+ libglib2.0-0 \
7
+ && rm -rf /var/lib/apt/lists/*
8
+
9
+ WORKDIR /app
10
+
11
+ COPY backend/requirements.txt .
12
+ RUN pip install --no-cache-dir -r requirements.txt
13
+
14
+ COPY backend/app ./app
15
+ COPY models ./models
16
+
17
+ ENV MODELS_DIR=/app/models
18
+ ENV PORT=7860
19
+
20
+ EXPOSE 7860
21
+
22
+ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,10 +1,23 @@
1
  ---
2
- title: DetectX
3
- emoji: 🐠
4
- colorFrom: green
5
- colorTo: red
6
  sdk: docker
 
7
  pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Detect-X
3
+ emoji: 🔬
4
+ colorFrom: gray
5
+ colorTo: blue
6
  sdk: docker
7
+ app_port: 7860
8
  pinned: false
9
  ---
10
 
11
+ # Detect-X
12
+
13
+ Digital forensics platform — photo, video, and audio integrity analysis.
14
+
15
+ | Layer | Host |
16
+ |-------|------|
17
+ | UI | Vercel (`frontend/`) |
18
+ | API | Hugging Face Space (Docker, root `Dockerfile`) |
19
+ | Code | GitHub `solvion-iq/DetectX` |
20
+
21
+ See [DEPLOY.md](./DEPLOY.md) for setup steps.
22
+
23
+ **Alpha** — analytical observations only, not a legal verdict.
backend/app/__init__.py ADDED
File without changes
backend/app/config.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic_settings import BaseSettings
2
+
3
+
4
+ class Settings(BaseSettings):
5
+ app_name: str = "Detect-X API"
6
+ app_version: str = "0.1.0-alpha"
7
+ cors_origins: str = "*"
8
+ max_upload_mb: int = 100
9
+ max_video_seconds: int = 90
10
+ job_retention_hours: int = 2
11
+ models_dir: str = "/app/models"
12
+ visual_model_file: str = "IDF_Solid_Final.pth"
13
+ audio_model_file: str = "IDF_Audio_Scientific_Solid.pth"
14
+
15
+ class Config:
16
+ env_file = ".env"
17
+
18
+
19
+ settings = Settings()
backend/app/jobs.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import uuid
4
+ from dataclasses import dataclass, field
5
+ from datetime import datetime, timezone
6
+ from enum import Enum
7
+ from typing import Any
8
+
9
+
10
+ class JobStatus(str, Enum):
11
+ QUEUED = "queued"
12
+ RUNNING = "running"
13
+ COMPLETED = "completed"
14
+ FAILED = "failed"
15
+
16
+
17
+ STAGES = [
18
+ "ingest",
19
+ "metadata",
20
+ "audio_extract",
21
+ "frame_extract",
22
+ "visual_model",
23
+ "audio_model",
24
+ "ela_analysis",
25
+ "face_sync",
26
+ "dna_extract",
27
+ "findings",
28
+ "report",
29
+ ]
30
+
31
+
32
+ @dataclass
33
+ class Job:
34
+ id: str
35
+ filename: str
36
+ status: JobStatus = JobStatus.QUEUED
37
+ progress: int = 0
38
+ current_stage: str = STAGES[0]
39
+ created_at: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
40
+ error: str | None = None
41
+ result: dict[str, Any] = field(default_factory=dict)
42
+
43
+
44
+ _jobs: dict[str, Job] = {}
45
+
46
+
47
+ def create_job(filename: str) -> Job:
48
+ job = Job(id=str(uuid.uuid4()), filename=filename)
49
+ _jobs[job.id] = job
50
+ return job
51
+
52
+
53
+ def get_job(job_id: str) -> Job | None:
54
+ return _jobs.get(job_id)
55
+
56
+
57
+ def job_to_dict(job: Job) -> dict[str, Any]:
58
+ return {
59
+ "id": job.id,
60
+ "filename": job.filename,
61
+ "status": job.status.value,
62
+ "progress": job.progress,
63
+ "current_stage": job.current_stage,
64
+ "created_at": job.created_at,
65
+ "error": job.error,
66
+ "result": job.result,
67
+ "stages": STAGES,
68
+ }
backend/app/main.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import os
5
+ from pathlib import Path
6
+
7
+ from fastapi import FastAPI, File, HTTPException, UploadFile
8
+ from fastapi.middleware.cors import CORSMiddleware
9
+ from fastapi.responses import JSONResponse
10
+
11
+ from app.config import settings
12
+ from app.jobs import JobStatus, create_job, get_job, job_to_dict
13
+
14
+ app = FastAPI(
15
+ title=settings.app_name,
16
+ version=settings.app_version,
17
+ description="Detect-X forensic media analysis API (Alpha)",
18
+ )
19
+
20
+ origins = [o.strip() for o in settings.cors_origins.split(",") if o.strip()]
21
+ app.add_middleware(
22
+ CORSMiddleware,
23
+ allow_origins=origins if origins != ["*"] else ["*"],
24
+ allow_credentials=True,
25
+ allow_methods=["*"],
26
+ allow_headers=["*"],
27
+ )
28
+
29
+
30
+ def models_ready() -> dict[str, bool]:
31
+ models_path = Path(settings.models_dir)
32
+ visual = models_path / settings.visual_model_file
33
+ audio = models_path / settings.audio_model_file
34
+ return {
35
+ "visual_model": visual.exists(),
36
+ "audio_model": audio.exists(),
37
+ "models_dir": str(models_path),
38
+ }
39
+
40
+
41
+ @app.get("/")
42
+ def root():
43
+ return {
44
+ "service": settings.app_name,
45
+ "version": settings.app_version,
46
+ "status": "online",
47
+ "docs": "/docs",
48
+ }
49
+
50
+
51
+ @app.get("/health")
52
+ def health():
53
+ ready = models_ready()
54
+ return {
55
+ "status": "healthy" if all(ready.values()) else "degraded",
56
+ "models": ready,
57
+ "limits": {
58
+ "max_upload_mb": settings.max_upload_mb,
59
+ "max_video_seconds": settings.max_video_seconds,
60
+ },
61
+ }
62
+
63
+
64
+ @app.post("/analyze")
65
+ async def analyze(file: UploadFile = File(...)):
66
+ if not file.filename:
67
+ raise HTTPException(status_code=400, detail="Filename is required")
68
+
69
+ content = await file.read()
70
+ size_mb = len(content) / (1024 * 1024)
71
+ if size_mb > settings.max_upload_mb:
72
+ raise HTTPException(
73
+ status_code=413,
74
+ detail=f"File exceeds {settings.max_upload_mb} MB limit",
75
+ )
76
+
77
+ file_hash = hashlib.sha256(content).hexdigest()
78
+ job = create_job(file.filename)
79
+ job.status = JobStatus.RUNNING
80
+ job.progress = 5
81
+ job.result = {
82
+ "sha256": file_hash,
83
+ "size_bytes": len(content),
84
+ "message": "Alpha stub: full pipeline will run in next build step",
85
+ "findings": [],
86
+ "dna": {"visual": None, "audio": None},
87
+ }
88
+ job.status = JobStatus.COMPLETED
89
+ job.progress = 100
90
+ job.current_stage = "report"
91
+
92
+ return JSONResponse(job_to_dict(job))
93
+
94
+
95
+ @app.get("/jobs/{job_id}")
96
+ def job_status(job_id: str):
97
+ job = get_job(job_id)
98
+ if not job:
99
+ raise HTTPException(status_code=404, detail="Job not found")
100
+ return job_to_dict(job)
101
+
102
+
103
+ if os.getenv("SPACE_ID"):
104
+ # Hugging Face Spaces compatibility
105
+ @app.get("/spaces-config")
106
+ def spaces_config():
107
+ return {"ready": True}
backend/requirements.txt ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.115.6
2
+ uvicorn[standard]==0.34.0
3
+ python-multipart==0.0.20
4
+ pydantic==2.10.4
5
+ pydantic-settings==2.7.0
6
+ numpy==2.2.1
7
+ Pillow==11.1.0
8
+ opencv-python-headless==4.10.0.84
9
+ librosa==0.10.2.post1
10
+ soundfile==0.13.1
11
+ torch==2.5.1
12
+ torchvision==0.20.1
13
+ reportlab==4.2.5
14
+ aiofiles==24.1.0
models/IDF_Audio_Scientific_Solid.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:aa2924731774cd3c55adf19fa8494df4f103d834b85e229746314bc37c41abb9
3
+ size 42954421
models/IDF_Solid_Final.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1c093ef94befa540e1ec4951de4e18751da2f3153c8457d1ab6f1f30c46b0b48
3
+ size 172464639