Spaces:
Sleeping
Sleeping
Merge pull request #1 from Tobkubos/backend-setup
Browse files- QUICKSTART.md +49 -0
- README.md +0 -1
- TODO.md +2 -0
- backend/.env.example +28 -0
- backend/.gitignore +57 -0
- backend/README.md +133 -0
- backend/app/__init__.py +13 -0
- backend/app/api/__init__.py +1 -0
- backend/app/api/routes.py +174 -0
- backend/app/core/__init__.py +1 -0
- backend/app/core/config.py +39 -0
- backend/app/core/logging_config.py +35 -0
- backend/app/models/__init__.py +1 -0
- backend/app/models/schemas.py +112 -0
- backend/app/services/__init__.py +1 -0
- backend/app/services/detector/__init__.py +37 -0
- backend/app/services/detector/base.py +38 -0
- backend/app/services/detector/mock.py +56 -0
- backend/app/services/download.py +92 -0
- backend/app/services/image_analyzer.py +26 -0
- backend/app/services/queue.py +97 -0
- backend/app/services/text_analyzer.py +26 -0
- backend/app/utils/__init__.py +1 -0
- backend/app/utils/exceptions.py +53 -0
- backend/main.py +5 -0
- backend/requirements.txt +6 -0
- backend/setup.py +68 -0
QUICKSTART.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Quick Reference Guide
|
| 2 |
+
|
| 3 |
+
## π Starting the Backend
|
| 4 |
+
|
| 5 |
+
### First Time Setup (Windows)
|
| 6 |
+
```powershell
|
| 7 |
+
cd backend
|
| 8 |
+
python setup.py
|
| 9 |
+
venv\Scripts\activate
|
| 10 |
+
python main.py
|
| 11 |
+
```
|
| 12 |
+
|
| 13 |
+
### First Time Setup (macOS/Linux)
|
| 14 |
+
```bash
|
| 15 |
+
cd backend
|
| 16 |
+
python setup.py
|
| 17 |
+
source venv/bin/activate
|
| 18 |
+
python main.py
|
| 19 |
+
|
| 20 |
+
or
|
| 21 |
+
|
| 22 |
+
"uvicorn app:app --reload --host 127.0.0.1 --port 8000"
|
| 23 |
+
"uvicorn app:app --host 127.0.0.1 --port 8000"
|
| 24 |
+
```
|
| 25 |
+
|
| 26 |
+
### Subsequent Times
|
| 27 |
+
```bash
|
| 28 |
+
cd backend
|
| 29 |
+
run.bat
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
## π‘ API Quick Test
|
| 33 |
+
|
| 34 |
+
### Health Check
|
| 35 |
+
```bash
|
| 36 |
+
curl http://localhost:8000/
|
| 37 |
+
```
|
| 38 |
+
|
| 39 |
+
### Analyze a File
|
| 40 |
+
```bash
|
| 41 |
+
curl -X POST http://localhost:8000/analyze \
|
| 42 |
+
-H "Content-Type: application/json" \
|
| 43 |
+
-d '{"file_url": "https://example.com/video.mp4"}'
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
### Documentation
|
| 47 |
+
- **Swagger UI**: http://localhost:8000/docs
|
| 48 |
+
- **ReDoc**: http://localhost:8000/redoc
|
| 49 |
+
|
README.md
CHANGED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
# DiscordBot
|
|
|
|
|
|
TODO.md
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
BezpieczeΕstwo (SSRF - Server-Side Request Forgery): TwΓ³j backend pobiera pliki z dowolnego przekazanego adresu URL. ZΕoΕliwy uΕΌytkownik mΓ³gΕby podaΔ URL wskazujΔ
cy na wewnΔtrzne zasoby Twojej sieci (np. http://localhost:8080/admin). Warto zaimplementowaΔ w download_file walidacjΔ, ktΓ³ra pozwala na pobieranie plikΓ³w wyΕΔ
cznie z zaufanych domen (np. tylko z *.discordapp.com i *.media.discordapp.net).
|
| 2 |
+
|
backend/.env.example
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Deepfake Detection Service - Environment Variables
|
| 2 |
+
|
| 3 |
+
# Application Settings
|
| 4 |
+
APP_NAME=Deepfake Detection Service
|
| 5 |
+
APP_VERSION=1.0.0
|
| 6 |
+
DEBUG=True
|
| 7 |
+
RELOAD=True
|
| 8 |
+
|
| 9 |
+
# Server Configuration
|
| 10 |
+
HOST=127.0.0.1
|
| 11 |
+
PORT=8000
|
| 12 |
+
|
| 13 |
+
# File Handling
|
| 14 |
+
DOWNLOAD_TIMEOUT=30
|
| 15 |
+
MAX_FILE_SIZE=104857600
|
| 16 |
+
|
| 17 |
+
# ML Model Configuration
|
| 18 |
+
DEFAULT_DETECTOR_MODEL=mock
|
| 19 |
+
# Supported models: mock, deepseek, openai, etc.
|
| 20 |
+
|
| 21 |
+
# Redis Configuration (for future queuing)
|
| 22 |
+
REDIS_ENABLED=False
|
| 23 |
+
REDIS_URL=redis://localhost:6379
|
| 24 |
+
REDIS_QUEUE_NAME=deepfake_analysis
|
| 25 |
+
|
| 26 |
+
# Logging
|
| 27 |
+
LOG_LEVEL=INFO
|
| 28 |
+
LOG_FILE=
|
backend/.gitignore
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
*.so
|
| 6 |
+
.Python
|
| 7 |
+
build/
|
| 8 |
+
develop-eggs/
|
| 9 |
+
dist/
|
| 10 |
+
downloads/
|
| 11 |
+
eggs/
|
| 12 |
+
.eggs/
|
| 13 |
+
lib/
|
| 14 |
+
lib64/
|
| 15 |
+
parts/
|
| 16 |
+
sdist/
|
| 17 |
+
var/
|
| 18 |
+
wheels/
|
| 19 |
+
*.egg-info/
|
| 20 |
+
.installed.cfg
|
| 21 |
+
*.egg
|
| 22 |
+
|
| 23 |
+
# Virtual Environments
|
| 24 |
+
venv/
|
| 25 |
+
ENV/
|
| 26 |
+
env/
|
| 27 |
+
.venv
|
| 28 |
+
|
| 29 |
+
# IDE
|
| 30 |
+
.vscode/
|
| 31 |
+
.idea/
|
| 32 |
+
*.swp
|
| 33 |
+
*.swo
|
| 34 |
+
*~
|
| 35 |
+
.DS_Store
|
| 36 |
+
|
| 37 |
+
# Environment variables
|
| 38 |
+
.env
|
| 39 |
+
.env.local
|
| 40 |
+
.env.*.local
|
| 41 |
+
|
| 42 |
+
# Logs
|
| 43 |
+
logs/
|
| 44 |
+
*.log
|
| 45 |
+
*.log.*
|
| 46 |
+
|
| 47 |
+
# Redis
|
| 48 |
+
dump.rdb
|
| 49 |
+
|
| 50 |
+
# Testing
|
| 51 |
+
.pytest_cache/
|
| 52 |
+
.coverage
|
| 53 |
+
htmlcov/
|
| 54 |
+
|
| 55 |
+
# Temporary files
|
| 56 |
+
*.tmp
|
| 57 |
+
*.temp
|
backend/README.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Deepfake Detection Service Backend
|
| 2 |
+
|
| 3 |
+
### Prerequisites
|
| 4 |
+
- Python 3.8+
|
| 5 |
+
- pip or conda
|
| 6 |
+
|
| 7 |
+
### Installation
|
| 8 |
+
|
| 9 |
+
1. **Navigate to the backend directory:**
|
| 10 |
+
```bash
|
| 11 |
+
cd backend
|
| 12 |
+
```
|
| 13 |
+
|
| 14 |
+
2. **Create a virtual environment (recommended):**
|
| 15 |
+
```bash
|
| 16 |
+
# Using venv
|
| 17 |
+
python -m venv venv
|
| 18 |
+
|
| 19 |
+
# Activate virtual environment
|
| 20 |
+
# On Windows:
|
| 21 |
+
venv\Scripts\activate
|
| 22 |
+
# On macOS/Linux:
|
| 23 |
+
source venv/bin/activate
|
| 24 |
+
```
|
| 25 |
+
|
| 26 |
+
3. **Install dependencies:**
|
| 27 |
+
```bash
|
| 28 |
+
pip install -r requirements.txt
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
4. **Run the server:**
|
| 32 |
+
```bash
|
| 33 |
+
python main.py
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
The server will start on `http://127.0.0.1:8000`
|
| 37 |
+
|
| 38 |
+
## π API Documentation
|
| 39 |
+
|
| 40 |
+
Once the server is running, interactive API documentation is available at:
|
| 41 |
+
- Swagger UI: `http://127.0.0.1:8000/docs`
|
| 42 |
+
- ReDoc: `http://127.0.0.1:8000/redoc`
|
| 43 |
+
|
| 44 |
+
## π API Endpoints
|
| 45 |
+
|
| 46 |
+
### Health Check
|
| 47 |
+
```bash
|
| 48 |
+
GET /
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
Returns service status and available models.
|
| 52 |
+
|
| 53 |
+
**Response:**
|
| 54 |
+
```json
|
| 55 |
+
{
|
| 56 |
+
"status": "ok",
|
| 57 |
+
"service": "Deepfake Detection Service",
|
| 58 |
+
"version": "1.0.0",
|
| 59 |
+
"available_models": ["mock"]
|
| 60 |
+
}
|
| 61 |
+
```
|
| 62 |
+
|
| 63 |
+
### Analyze File
|
| 64 |
+
```bash
|
| 65 |
+
POST /analyze
|
| 66 |
+
Content-Type: application/json
|
| 67 |
+
|
| 68 |
+
{
|
| 69 |
+
"file_url": "https://example.com/video.mp4",
|
| 70 |
+
"model": "mock"
|
| 71 |
+
}
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
**Request Parameters:**
|
| 75 |
+
- `file_url` (required): URL of the file to analyze
|
| 76 |
+
- `model` (optional): Detector model to use. Defaults to configured model
|
| 77 |
+
|
| 78 |
+
**Response (200 OK):**
|
| 79 |
+
```json
|
| 80 |
+
{
|
| 81 |
+
"is_deepfake": true,
|
| 82 |
+
"confidence": 0.847,
|
| 83 |
+
"analysis_time": 1.234,
|
| 84 |
+
"model_used": "mock"
|
| 85 |
+
}
|
| 86 |
+
```
|
| 87 |
+
|
| 88 |
+
**Error Responses:**
|
| 89 |
+
- `400 Bad Request`: Invalid URL, file too large, or unsupported model
|
| 90 |
+
- `408 Request Timeout`: File download timed out
|
| 91 |
+
- `500 Internal Server Error`: Server error during analysis
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
### Using curl:
|
| 95 |
+
```bash
|
| 96 |
+
curl -X POST http://localhost:8000/analyze \
|
| 97 |
+
-H "Content-Type: application/json" \
|
| 98 |
+
-d '{"file_url": "https://example.com/video.mp4"}'
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
The API provides comprehensive error handling:
|
| 103 |
+
|
| 104 |
+
```python
|
| 105 |
+
# Invalid URL
|
| 106 |
+
{
|
| 107 |
+
"error": "Invalid URL format",
|
| 108 |
+
"status_code": 400,
|
| 109 |
+
"details": null
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
# File too large
|
| 113 |
+
{
|
| 114 |
+
"error": "File size exceeds maximum allowed size of 104857600 bytes",
|
| 115 |
+
"status_code": 400,
|
| 116 |
+
"details": null
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
# Download timeout
|
| 120 |
+
{
|
| 121 |
+
"error": "File download timed out",
|
| 122 |
+
"status_code": 408,
|
| 123 |
+
"details": null
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
# Unsupported model
|
| 127 |
+
{
|
| 128 |
+
"error": "Detector model 'invalid' is not supported. Available models: mock",
|
| 129 |
+
"status_code": 400,
|
| 130 |
+
"details": null
|
| 131 |
+
}
|
| 132 |
+
```
|
| 133 |
+
|
backend/app/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI
|
| 2 |
+
from app.api.routes import router as api_router
|
| 3 |
+
from app.core.logging_config import setup_logging
|
| 4 |
+
|
| 5 |
+
setup_logging()
|
| 6 |
+
|
| 7 |
+
app = FastAPI(
|
| 8 |
+
title="Deepfake Detection Service",
|
| 9 |
+
description="Backend service for deepfake detection with support for multiple ML models",
|
| 10 |
+
version="1.0.0",
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
app.include_router(api_router)
|
backend/app/api/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""API routes and endpoints."""
|
backend/app/api/routes.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from fastapi import APIRouter, HTTPException
|
| 3 |
+
|
| 4 |
+
from app.models.schemas import (
|
| 5 |
+
AnalysisRequest,
|
| 6 |
+
AnalysisResponse,
|
| 7 |
+
ErrorResponse,
|
| 8 |
+
HealthResponse,
|
| 9 |
+
TextAnalysisRequest,
|
| 10 |
+
ImageAnalysisRequest,
|
| 11 |
+
VideoAnalysisRequest,
|
| 12 |
+
FileAnalysisRequest,
|
| 13 |
+
)
|
| 14 |
+
from app.services.download import download_file
|
| 15 |
+
from app.services.text_analyzer import analyze_text
|
| 16 |
+
from app.services.image_analyzer import analyze_image
|
| 17 |
+
from app.services.detector import get_detector
|
| 18 |
+
from app.core.config import get_settings
|
| 19 |
+
from app.utils.exceptions import DeepfakeDetectionError
|
| 20 |
+
|
| 21 |
+
logger = logging.getLogger(__name__)
|
| 22 |
+
|
| 23 |
+
router = APIRouter()
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
@router.get(
|
| 27 |
+
"/",
|
| 28 |
+
response_model=HealthResponse,
|
| 29 |
+
tags=["Health"],
|
| 30 |
+
summary="Health check endpoint",
|
| 31 |
+
)
|
| 32 |
+
async def health_check() -> HealthResponse:
|
| 33 |
+
settings = get_settings()
|
| 34 |
+
logger.info("Health check endpoint accessed")
|
| 35 |
+
|
| 36 |
+
available_models = ["mock"]
|
| 37 |
+
supported_types = ["text", "image", "video", "file"]
|
| 38 |
+
|
| 39 |
+
return HealthResponse(
|
| 40 |
+
status="ok",
|
| 41 |
+
service="Deepfake Detection Service",
|
| 42 |
+
version=settings.APP_VERSION,
|
| 43 |
+
available_models=available_models,
|
| 44 |
+
supported_types=supported_types,
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
@router.post(
|
| 49 |
+
"/analyze",
|
| 50 |
+
response_model=AnalysisResponse,
|
| 51 |
+
responses={
|
| 52 |
+
400: {"model": ErrorResponse, "description": "Bad request"},
|
| 53 |
+
408: {"model": ErrorResponse, "description": "Request timeout"},
|
| 54 |
+
500: {"model": ErrorResponse, "description": "Internal server error"},
|
| 55 |
+
},
|
| 56 |
+
tags=["Analysis"],
|
| 57 |
+
summary="Analyze content for deepfake detection",
|
| 58 |
+
)
|
| 59 |
+
async def analyze(request: AnalysisRequest) -> AnalysisResponse:
|
| 60 |
+
settings = get_settings()
|
| 61 |
+
detector_model = None
|
| 62 |
+
|
| 63 |
+
if isinstance(request, TextAnalysisRequest):
|
| 64 |
+
detector_model = request.model or settings.DEFAULT_DETECTOR_MODEL
|
| 65 |
+
logger.info(f"Received text analysis request, length: {len(request.text)} chars, model: {detector_model}")
|
| 66 |
+
|
| 67 |
+
try:
|
| 68 |
+
detector = get_detector(detector_model)
|
| 69 |
+
except ValueError as e:
|
| 70 |
+
logger.error(f"Invalid detector model: {str(e)}")
|
| 71 |
+
raise HTTPException(status_code=400, detail=str(e))
|
| 72 |
+
|
| 73 |
+
text_bytes = request.text.encode('utf-8')
|
| 74 |
+
analysis_result = await detector.detect(text_bytes)
|
| 75 |
+
|
| 76 |
+
logger.info(f"Text analysis completed. Result: {analysis_result}")
|
| 77 |
+
|
| 78 |
+
return AnalysisResponse(
|
| 79 |
+
is_deepfake=analysis_result["is_deepfake"],
|
| 80 |
+
confidence=analysis_result["confidence"],
|
| 81 |
+
analysis_time=analysis_result["analysis_time"],
|
| 82 |
+
model_used=detector_model,
|
| 83 |
+
content_type="text",
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
elif isinstance(request, ImageAnalysisRequest):
|
| 87 |
+
detector_model = request.model or settings.DEFAULT_DETECTOR_MODEL
|
| 88 |
+
logger.info(f"Received image analysis request for URL: {request.image_url}, model: {detector_model}")
|
| 89 |
+
|
| 90 |
+
try:
|
| 91 |
+
detector = get_detector(detector_model)
|
| 92 |
+
except ValueError as e:
|
| 93 |
+
logger.error(f"Invalid detector model: {str(e)}")
|
| 94 |
+
raise HTTPException(status_code=400, detail=str(e))
|
| 95 |
+
|
| 96 |
+
try:
|
| 97 |
+
image_bytes = await download_file(str(request.image_url))
|
| 98 |
+
if not image_bytes:
|
| 99 |
+
raise HTTPException(status_code=500, detail="Failed to download image")
|
| 100 |
+
except DeepfakeDetectionError as e:
|
| 101 |
+
raise HTTPException(status_code=e.status_code, detail=e.message)
|
| 102 |
+
|
| 103 |
+
analysis_result = await detector.detect(image_bytes)
|
| 104 |
+
|
| 105 |
+
logger.info(f"Image analysis completed. Result: {analysis_result}")
|
| 106 |
+
|
| 107 |
+
return AnalysisResponse(
|
| 108 |
+
is_deepfake=analysis_result["is_deepfake"],
|
| 109 |
+
confidence=analysis_result["confidence"],
|
| 110 |
+
analysis_time=analysis_result["analysis_time"],
|
| 111 |
+
model_used=detector_model,
|
| 112 |
+
content_type="image",
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
elif isinstance(request, VideoAnalysisRequest):
|
| 116 |
+
detector_model = request.model or settings.DEFAULT_DETECTOR_MODEL
|
| 117 |
+
logger.info(f"Received video analysis request for URL: {request.video_url}, model: {detector_model}")
|
| 118 |
+
|
| 119 |
+
try:
|
| 120 |
+
detector = get_detector(detector_model)
|
| 121 |
+
except ValueError as e:
|
| 122 |
+
logger.error(f"Invalid detector model: {str(e)}")
|
| 123 |
+
raise HTTPException(status_code=400, detail=str(e))
|
| 124 |
+
|
| 125 |
+
try:
|
| 126 |
+
video_bytes = await download_file(str(request.video_url))
|
| 127 |
+
if not video_bytes:
|
| 128 |
+
raise HTTPException(status_code=500, detail="Failed to download video")
|
| 129 |
+
except DeepfakeDetectionError as e:
|
| 130 |
+
raise HTTPException(status_code=e.status_code, detail=e.message)
|
| 131 |
+
|
| 132 |
+
analysis_result = await detector.detect(video_bytes)
|
| 133 |
+
|
| 134 |
+
logger.info(f"Video analysis completed. Result: {analysis_result}")
|
| 135 |
+
|
| 136 |
+
return AnalysisResponse(
|
| 137 |
+
is_deepfake=analysis_result["is_deepfake"],
|
| 138 |
+
confidence=analysis_result["confidence"],
|
| 139 |
+
analysis_time=analysis_result["analysis_time"],
|
| 140 |
+
model_used=detector_model,
|
| 141 |
+
content_type="video",
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
elif isinstance(request, FileAnalysisRequest):
|
| 145 |
+
detector_model = request.model or settings.DEFAULT_DETECTOR_MODEL
|
| 146 |
+
logger.info(f"Received file analysis request for URL: {request.file_url}, model: {detector_model}")
|
| 147 |
+
|
| 148 |
+
try:
|
| 149 |
+
detector = get_detector(detector_model)
|
| 150 |
+
except ValueError as e:
|
| 151 |
+
logger.error(f"Invalid detector model: {str(e)}")
|
| 152 |
+
raise HTTPException(status_code=400, detail=str(e))
|
| 153 |
+
|
| 154 |
+
try:
|
| 155 |
+
file_bytes = await download_file(str(request.file_url))
|
| 156 |
+
if not file_bytes:
|
| 157 |
+
raise HTTPException(status_code=500, detail="Failed to download file")
|
| 158 |
+
except DeepfakeDetectionError as e:
|
| 159 |
+
raise HTTPException(status_code=e.status_code, detail=e.message)
|
| 160 |
+
|
| 161 |
+
analysis_result = await detector.detect(file_bytes)
|
| 162 |
+
|
| 163 |
+
logger.info(f"File analysis completed. Result: {analysis_result}")
|
| 164 |
+
|
| 165 |
+
return AnalysisResponse(
|
| 166 |
+
is_deepfake=analysis_result["is_deepfake"],
|
| 167 |
+
confidence=analysis_result["confidence"],
|
| 168 |
+
analysis_time=analysis_result["analysis_time"],
|
| 169 |
+
model_used=detector_model,
|
| 170 |
+
content_type="file",
|
| 171 |
+
)
|
| 172 |
+
|
| 173 |
+
else:
|
| 174 |
+
raise HTTPException(status_code=400, detail="Unsupported content type")
|
backend/app/core/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Core application configuration and setup."""
|
backend/app/core/config.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from typing import Optional
|
| 3 |
+
from functools import lru_cache
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class Settings:
|
| 7 |
+
"""Application settings with environment variable support."""
|
| 8 |
+
|
| 9 |
+
# Application
|
| 10 |
+
APP_NAME: str = "Deepfake Detection Service"
|
| 11 |
+
APP_VERSION: str = "1.0.0"
|
| 12 |
+
DEBUG: bool = os.getenv("DEBUG", "True").lower() == "true"
|
| 13 |
+
|
| 14 |
+
# Server
|
| 15 |
+
HOST: str = os.getenv("HOST", "127.0.0.1")
|
| 16 |
+
PORT: int = int(os.getenv("PORT", "8000"))
|
| 17 |
+
|
| 18 |
+
# File handling
|
| 19 |
+
DOWNLOAD_TIMEOUT: int = int(os.getenv("DOWNLOAD_TIMEOUT", "30"))
|
| 20 |
+
MAX_FILE_SIZE: int = int(os.getenv("MAX_FILE_SIZE", str(100 * 1024 * 1024))) # 100 MB
|
| 21 |
+
|
| 22 |
+
# ML Model configuration
|
| 23 |
+
DEFAULT_DETECTOR_MODEL: str = os.getenv("DEFAULT_DETECTOR_MODEL", "mock")
|
| 24 |
+
# Supported models: "mock", "deepseek", "openai", etc. (easy to add more)
|
| 25 |
+
|
| 26 |
+
# Redis configuration (for future queuing)
|
| 27 |
+
REDIS_ENABLED: bool = os.getenv("REDIS_ENABLED", "False").lower() == "true"
|
| 28 |
+
REDIS_URL: str = os.getenv("REDIS_URL", "redis://localhost:6379")
|
| 29 |
+
REDIS_QUEUE_NAME: str = os.getenv("REDIS_QUEUE_NAME", "deepfake_analysis")
|
| 30 |
+
|
| 31 |
+
# Logging
|
| 32 |
+
LOG_LEVEL: str = os.getenv("LOG_LEVEL", "INFO")
|
| 33 |
+
LOG_FILE: Optional[str] = os.getenv("LOG_FILE", None)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
@lru_cache()
|
| 37 |
+
def get_settings() -> Settings:
|
| 38 |
+
"""Get cached application settings."""
|
| 39 |
+
return Settings()
|
backend/app/core/logging_config.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import logging.handlers
|
| 3 |
+
from app.core.config import get_settings
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def setup_logging():
|
| 7 |
+
"""Configure logging for the application."""
|
| 8 |
+
settings = get_settings()
|
| 9 |
+
|
| 10 |
+
# Create logger
|
| 11 |
+
logger = logging.getLogger()
|
| 12 |
+
logger.setLevel(getattr(logging, settings.LOG_LEVEL))
|
| 13 |
+
|
| 14 |
+
# Remove existing handlers
|
| 15 |
+
logger.handlers.clear()
|
| 16 |
+
|
| 17 |
+
# Console handler
|
| 18 |
+
console_handler = logging.StreamHandler()
|
| 19 |
+
console_handler.setLevel(getattr(logging, settings.LOG_LEVEL))
|
| 20 |
+
formatter = logging.Formatter(
|
| 21 |
+
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
| 22 |
+
)
|
| 23 |
+
console_handler.setFormatter(formatter)
|
| 24 |
+
logger.addHandler(console_handler)
|
| 25 |
+
|
| 26 |
+
# File handler (if specified)
|
| 27 |
+
if settings.LOG_FILE:
|
| 28 |
+
file_handler = logging.handlers.RotatingFileHandler(
|
| 29 |
+
settings.LOG_FILE,
|
| 30 |
+
maxBytes=10485760, # 10 MB
|
| 31 |
+
backupCount=5,
|
| 32 |
+
)
|
| 33 |
+
file_handler.setLevel(getattr(logging, settings.LOG_LEVEL))
|
| 34 |
+
file_handler.setFormatter(formatter)
|
| 35 |
+
logger.addHandler(file_handler)
|
backend/app/models/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Data models and schemas."""
|
backend/app/models/schemas.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel, HttpUrl, Field
|
| 2 |
+
from typing import Optional, Union, Literal
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class TextAnalysisRequest(BaseModel):
|
| 6 |
+
content_type: Literal["text"]
|
| 7 |
+
text: str = Field(..., description="Text content to analyze for deepfake detection")
|
| 8 |
+
model: Optional[str] = Field(None, description="Detector model to use")
|
| 9 |
+
|
| 10 |
+
class Config:
|
| 11 |
+
json_schema_extra = {
|
| 12 |
+
"example": {
|
| 13 |
+
"content_type": "text",
|
| 14 |
+
"text": "Some text that might be AI-generated",
|
| 15 |
+
"model": "mock"
|
| 16 |
+
}
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class ImageAnalysisRequest(BaseModel):
|
| 21 |
+
content_type: Literal["image"]
|
| 22 |
+
image_url: HttpUrl = Field(..., description="URL of the image to analyze")
|
| 23 |
+
model: Optional[str] = Field(None, description="Detector model to use")
|
| 24 |
+
|
| 25 |
+
class Config:
|
| 26 |
+
json_schema_extra = {
|
| 27 |
+
"example": {
|
| 28 |
+
"content_type": "image",
|
| 29 |
+
"image_url": "https://example.com/image.jpg",
|
| 30 |
+
"model": "mock"
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class VideoAnalysisRequest(BaseModel):
|
| 36 |
+
content_type: Literal["video"]
|
| 37 |
+
video_url: HttpUrl = Field(..., description="URL of the video to analyze")
|
| 38 |
+
model: Optional[str] = Field(None, description="Detector model to use")
|
| 39 |
+
|
| 40 |
+
class Config:
|
| 41 |
+
json_schema_extra = {
|
| 42 |
+
"example": {
|
| 43 |
+
"content_type": "video",
|
| 44 |
+
"video_url": "https://example.com/video.mp4",
|
| 45 |
+
"model": "mock"
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class FileAnalysisRequest(BaseModel):
|
| 51 |
+
content_type: Literal["file"]
|
| 52 |
+
file_url: HttpUrl = Field(..., description="URL of the file to analyze")
|
| 53 |
+
model: Optional[str] = Field(None, description="Detector model to use")
|
| 54 |
+
|
| 55 |
+
class Config:
|
| 56 |
+
json_schema_extra = {
|
| 57 |
+
"example": {
|
| 58 |
+
"content_type": "file",
|
| 59 |
+
"file_url": "https://example.com/video.mp4",
|
| 60 |
+
"model": "mock"
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
AnalysisRequest = Union[
|
| 66 |
+
TextAnalysisRequest,
|
| 67 |
+
ImageAnalysisRequest,
|
| 68 |
+
VideoAnalysisRequest,
|
| 69 |
+
FileAnalysisRequest,
|
| 70 |
+
]
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
class AnalysisResponse(BaseModel):
|
| 74 |
+
is_deepfake: bool = Field(..., description="Whether the content is detected as a deepfake")
|
| 75 |
+
confidence: float = Field(..., ge=0.0, le=1.0, description="Confidence score between 0.0 and 1.0")
|
| 76 |
+
analysis_time: float = Field(..., description="Time taken for analysis in seconds")
|
| 77 |
+
model_used: str = Field(..., description="The detector model that was used")
|
| 78 |
+
content_type: str = Field(..., description="Type of content analyzed (text/image/video/file)")
|
| 79 |
+
|
| 80 |
+
class Config:
|
| 81 |
+
json_schema_extra = {
|
| 82 |
+
"example": {
|
| 83 |
+
"is_deepfake": True,
|
| 84 |
+
"confidence": 0.847,
|
| 85 |
+
"analysis_time": 1.234,
|
| 86 |
+
"model_used": "mock",
|
| 87 |
+
"content_type": "image"
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
class ErrorResponse(BaseModel):
|
| 93 |
+
error: str = Field(..., description="Error message")
|
| 94 |
+
status_code: int = Field(..., description="HTTP status code")
|
| 95 |
+
details: Optional[str] = Field(None, description="Additional error details")
|
| 96 |
+
|
| 97 |
+
class Config:
|
| 98 |
+
json_schema_extra = {
|
| 99 |
+
"example": {
|
| 100 |
+
"error": "Invalid URL format",
|
| 101 |
+
"status_code": 400,
|
| 102 |
+
"details": "The provided URL is not valid"
|
| 103 |
+
}
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
class HealthResponse(BaseModel):
|
| 108 |
+
status: str = Field(..., description="Service status")
|
| 109 |
+
service: str = Field(..., description="Service name")
|
| 110 |
+
version: str = Field(..., description="Service version")
|
| 111 |
+
available_models: list = Field(..., description="Available detector models")
|
| 112 |
+
supported_types: list = Field(..., description="Supported content types")
|
backend/app/services/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Service layer for business logic."""
|
backend/app/services/detector/__init__.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Detector models for deepfake detection."""
|
| 2 |
+
|
| 3 |
+
from app.services.detector.base import BaseDetector
|
| 4 |
+
from app.services.detector.mock import MockDetector
|
| 5 |
+
|
| 6 |
+
__all__ = ["BaseDetector", "MockDetector", "get_detector"]
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def get_detector(model_name: str = "mock") -> BaseDetector:
|
| 10 |
+
"""
|
| 11 |
+
Factory function to get detector instance by model name.
|
| 12 |
+
|
| 13 |
+
Args:
|
| 14 |
+
model_name: Name of the detector model
|
| 15 |
+
|
| 16 |
+
Returns:
|
| 17 |
+
Instance of the requested detector
|
| 18 |
+
|
| 19 |
+
Raises:
|
| 20 |
+
ValueError: If model is not supported
|
| 21 |
+
"""
|
| 22 |
+
detectors = {
|
| 23 |
+
"mock": MockDetector,
|
| 24 |
+
# Future models:
|
| 25 |
+
# "deepseek": DeepseekDetector,
|
| 26 |
+
# "openai": OpenAIDetector,
|
| 27 |
+
# "huggingface": HuggingFaceDetector,
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
if model_name not in detectors:
|
| 31 |
+
available = ", ".join(detectors.keys())
|
| 32 |
+
raise ValueError(
|
| 33 |
+
f"Detector model '{model_name}' is not supported. "
|
| 34 |
+
f"Available models: {available}"
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
return detectors[model_name]()
|
backend/app/services/detector/base.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Base detector class defining the interface for all detectors."""
|
| 2 |
+
|
| 3 |
+
from abc import ABC, abstractmethod
|
| 4 |
+
from typing import Dict, Any
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class BaseDetector(ABC):
|
| 8 |
+
"""
|
| 9 |
+
Abstract base class for deepfake detectors.
|
| 10 |
+
|
| 11 |
+
All detector implementations should inherit from this class and implement
|
| 12 |
+
the detect() method.
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
def __init__(self, model_name: str):
|
| 16 |
+
"""
|
| 17 |
+
Initialize the detector.
|
| 18 |
+
|
| 19 |
+
Args:
|
| 20 |
+
model_name: Name of the detector model
|
| 21 |
+
"""
|
| 22 |
+
self.model_name = model_name
|
| 23 |
+
|
| 24 |
+
@abstractmethod
|
| 25 |
+
async def detect(self, file_bytes: bytes) -> Dict[str, Any]:
|
| 26 |
+
"""
|
| 27 |
+
Detect if file is a deepfake.
|
| 28 |
+
|
| 29 |
+
Args:
|
| 30 |
+
file_bytes: The file contents as bytes
|
| 31 |
+
|
| 32 |
+
Returns:
|
| 33 |
+
Dictionary containing:
|
| 34 |
+
- is_deepfake: Boolean indicating if file is a deepfake
|
| 35 |
+
- confidence: Float between 0.0 and 1.0
|
| 36 |
+
- analysis_time: Float representing processing time
|
| 37 |
+
"""
|
| 38 |
+
pass
|
backend/app/services/detector/mock.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Mock detector implementation for testing and development."""
|
| 2 |
+
|
| 3 |
+
import asyncio
|
| 4 |
+
import logging
|
| 5 |
+
import time
|
| 6 |
+
from typing import Dict, Any
|
| 7 |
+
|
| 8 |
+
from app.services.detector.base import BaseDetector
|
| 9 |
+
|
| 10 |
+
logger = logging.getLogger(__name__)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class MockDetector(BaseDetector):
|
| 14 |
+
"""
|
| 15 |
+
Mock detector for testing and development.
|
| 16 |
+
|
| 17 |
+
Simulates deepfake detection without requiring actual ML models.
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
def __init__(self):
|
| 21 |
+
"""Initialize the mock detector."""
|
| 22 |
+
super().__init__("mock")
|
| 23 |
+
|
| 24 |
+
async def detect(self, file_bytes: bytes) -> Dict[str, Any]:
|
| 25 |
+
"""
|
| 26 |
+
Simulate deepfake detection with a random result.
|
| 27 |
+
|
| 28 |
+
Args:
|
| 29 |
+
file_bytes: The file contents as bytes
|
| 30 |
+
|
| 31 |
+
Returns:
|
| 32 |
+
Dictionary with is_deepfake, confidence, and analysis_time
|
| 33 |
+
"""
|
| 34 |
+
logger.info("Starting mock deepfake analysis...")
|
| 35 |
+
|
| 36 |
+
start_time = time.time()
|
| 37 |
+
|
| 38 |
+
# Simulate processing delay (1 to 2 seconds)
|
| 39 |
+
delay = 1.0 + (hash(file_bytes) % 100) / 100.0
|
| 40 |
+
await asyncio.sleep(delay)
|
| 41 |
+
|
| 42 |
+
analysis_time = time.time() - start_time
|
| 43 |
+
|
| 44 |
+
# Simulate ML model output (deterministic based on file content hash)
|
| 45 |
+
file_hash = hash(file_bytes) % 100
|
| 46 |
+
is_deepfake = file_hash > 50 # ~50% chance
|
| 47 |
+
confidence = (file_hash % 100) / 100.0
|
| 48 |
+
|
| 49 |
+
result = {
|
| 50 |
+
"is_deepfake": is_deepfake,
|
| 51 |
+
"confidence": round(confidence, 3),
|
| 52 |
+
"analysis_time": round(analysis_time, 3),
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
logger.info(f"Mock analysis completed. Result: {result}")
|
| 56 |
+
return result
|
backend/app/services/download.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import httpx
|
| 3 |
+
|
| 4 |
+
from app.core.config import get_settings
|
| 5 |
+
from app.utils.exceptions import (
|
| 6 |
+
FileDownloadError,
|
| 7 |
+
InvalidURLError,
|
| 8 |
+
DownloadTimeoutError,
|
| 9 |
+
FileSizeError,
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
logger = logging.getLogger(__name__)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
async def download_file(file_url: str) -> bytes:
|
| 16 |
+
"""
|
| 17 |
+
Asynchronously download a file from the given URL.
|
| 18 |
+
|
| 19 |
+
Args:
|
| 20 |
+
file_url: The URL of the file to download
|
| 21 |
+
|
| 22 |
+
Returns:
|
| 23 |
+
The file contents as bytes
|
| 24 |
+
|
| 25 |
+
Raises:
|
| 26 |
+
InvalidURLError: If the URL is invalid
|
| 27 |
+
DownloadTimeoutError: If download times out
|
| 28 |
+
FileSizeError: If file size exceeds limit
|
| 29 |
+
FileDownloadError: If download fails for other reasons
|
| 30 |
+
"""
|
| 31 |
+
settings = get_settings()
|
| 32 |
+
logger.info(f"Starting file download from: {file_url}")
|
| 33 |
+
|
| 34 |
+
try:
|
| 35 |
+
async with httpx.AsyncClient(timeout=settings.DOWNLOAD_TIMEOUT) as client:
|
| 36 |
+
response = await client.get(file_url, follow_redirects=True)
|
| 37 |
+
|
| 38 |
+
# Check for HTTP errors
|
| 39 |
+
if response.status_code != 200:
|
| 40 |
+
logger.error(
|
| 41 |
+
f"Failed to download file. Status code: {response.status_code}"
|
| 42 |
+
)
|
| 43 |
+
raise FileDownloadError(
|
| 44 |
+
f"Failed to download file. Server returned status code {response.status_code}",
|
| 45 |
+
status_code=response.status_code,
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
# Check content length header
|
| 49 |
+
content_length = response.headers.get("content-length")
|
| 50 |
+
if content_length and int(content_length) > settings.MAX_FILE_SIZE:
|
| 51 |
+
logger.error(
|
| 52 |
+
f"File size exceeds maximum allowed size: {content_length} bytes"
|
| 53 |
+
)
|
| 54 |
+
raise FileSizeError(
|
| 55 |
+
f"File size exceeds maximum allowed size of {settings.MAX_FILE_SIZE} bytes"
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
file_bytes = response.content
|
| 59 |
+
|
| 60 |
+
# Check actual content size
|
| 61 |
+
if len(file_bytes) > settings.MAX_FILE_SIZE:
|
| 62 |
+
logger.error(
|
| 63 |
+
f"Downloaded file exceeds maximum size: {len(file_bytes)} bytes"
|
| 64 |
+
)
|
| 65 |
+
raise FileSizeError(
|
| 66 |
+
f"File size exceeds maximum allowed size of {settings.MAX_FILE_SIZE} bytes"
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
logger.info(
|
| 70 |
+
f"File download completed successfully. Size: {len(file_bytes)} bytes"
|
| 71 |
+
)
|
| 72 |
+
return file_bytes
|
| 73 |
+
|
| 74 |
+
except httpx.InvalidURL as e:
|
| 75 |
+
logger.error(f"Invalid URL provided: {file_url} - {str(e)}")
|
| 76 |
+
raise InvalidURLError("Invalid URL format")
|
| 77 |
+
except httpx.TimeoutException as e:
|
| 78 |
+
logger.error(f"Download timeout for URL: {file_url}")
|
| 79 |
+
raise DownloadTimeoutError("File download timed out. Please try again with a faster source.")
|
| 80 |
+
except (FileSizeError, InvalidURLError, DownloadTimeoutError):
|
| 81 |
+
# Re-raise custom exceptions
|
| 82 |
+
raise
|
| 83 |
+
except httpx.RequestError as e:
|
| 84 |
+
logger.error(f"Failed to download file from {file_url}: {str(e)}")
|
| 85 |
+
raise FileDownloadError(
|
| 86 |
+
"Failed to download file from the provided URL. Please check the URL and try again."
|
| 87 |
+
)
|
| 88 |
+
except Exception as e:
|
| 89 |
+
logger.error(f"Unexpected error during file download: {str(e)}")
|
| 90 |
+
raise FileDownloadError(
|
| 91 |
+
"An unexpected error occurred during file download."
|
| 92 |
+
)
|
backend/app/services/image_analyzer.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import time
|
| 3 |
+
from typing import Dict, Any
|
| 4 |
+
|
| 5 |
+
logger = logging.getLogger(__name__)
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
async def analyze_image(image_bytes: bytes) -> Dict[str, Any]:
|
| 9 |
+
start_time = time.time()
|
| 10 |
+
|
| 11 |
+
logger.info(f"Starting image analysis, size: {len(image_bytes)} bytes")
|
| 12 |
+
|
| 13 |
+
image_hash = hash(image_bytes) % 100
|
| 14 |
+
is_deepfake = image_hash > 50
|
| 15 |
+
confidence = (image_hash % 100) / 100.0
|
| 16 |
+
|
| 17 |
+
analysis_time = time.time() - start_time
|
| 18 |
+
|
| 19 |
+
result = {
|
| 20 |
+
"is_deepfake": is_deepfake,
|
| 21 |
+
"confidence": round(confidence, 3),
|
| 22 |
+
"analysis_time": round(analysis_time, 3),
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
logger.info(f"Image analysis completed. Result: {result}")
|
| 26 |
+
return result
|
backend/app/services/queue.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from typing import Optional, Any, Dict
|
| 3 |
+
import json
|
| 4 |
+
|
| 5 |
+
from app.core.config import get_settings
|
| 6 |
+
|
| 7 |
+
logger = logging.getLogger(__name__)
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class QueueService:
|
| 11 |
+
"""
|
| 12 |
+
Queue service for managing asynchronous analysis tasks.
|
| 13 |
+
|
| 14 |
+
Currently uses in-memory queue, can be extended to use Redis.
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
def __init__(self):
|
| 18 |
+
"""Initialize the queue service."""
|
| 19 |
+
self.settings = get_settings()
|
| 20 |
+
self.redis_client = None
|
| 21 |
+
|
| 22 |
+
if self.settings.REDIS_ENABLED:
|
| 23 |
+
self._initialize_redis()
|
| 24 |
+
|
| 25 |
+
def _initialize_redis(self):
|
| 26 |
+
"""Initialize Redis connection (future implementation)."""
|
| 27 |
+
# This will be implemented when Redis support is added
|
| 28 |
+
logger.info(
|
| 29 |
+
f"Redis queue service initialized: {self.settings.REDIS_URL}"
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
async def enqueue_analysis(
|
| 33 |
+
self,
|
| 34 |
+
file_url: str,
|
| 35 |
+
model: str,
|
| 36 |
+
task_id: str,
|
| 37 |
+
) -> bool:
|
| 38 |
+
"""
|
| 39 |
+
Enqueue an analysis task.
|
| 40 |
+
|
| 41 |
+
Args:
|
| 42 |
+
file_url: URL of the file to analyze
|
| 43 |
+
model: Detector model to use
|
| 44 |
+
task_id: Unique task identifier
|
| 45 |
+
|
| 46 |
+
Returns:
|
| 47 |
+
True if successful, False otherwise
|
| 48 |
+
"""
|
| 49 |
+
task_data = {
|
| 50 |
+
"task_id": task_id,
|
| 51 |
+
"file_url": file_url,
|
| 52 |
+
"model": model,
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
logger.info(f"Enqueuing analysis task: {task_id}")
|
| 56 |
+
|
| 57 |
+
if self.settings.REDIS_ENABLED:
|
| 58 |
+
# Future: Push to Redis queue
|
| 59 |
+
# await self.redis_client.lpush(
|
| 60 |
+
# self.settings.REDIS_QUEUE_NAME,
|
| 61 |
+
# json.dumps(task_data)
|
| 62 |
+
# )
|
| 63 |
+
pass
|
| 64 |
+
|
| 65 |
+
return True
|
| 66 |
+
|
| 67 |
+
async def get_task_result(self, task_id: str) -> Optional[Dict[str, Any]]:
|
| 68 |
+
"""
|
| 69 |
+
Get analysis result for a task.
|
| 70 |
+
|
| 71 |
+
Args:
|
| 72 |
+
task_id: Task identifier
|
| 73 |
+
|
| 74 |
+
Returns:
|
| 75 |
+
Analysis result or None if not found
|
| 76 |
+
"""
|
| 77 |
+
logger.info(f"Retrieving result for task: {task_id}")
|
| 78 |
+
|
| 79 |
+
if self.settings.REDIS_ENABLED:
|
| 80 |
+
# Future: Get from Redis
|
| 81 |
+
# result = await self.redis_client.get(f"result:{task_id}")
|
| 82 |
+
# return json.loads(result) if result else None
|
| 83 |
+
pass
|
| 84 |
+
|
| 85 |
+
return None
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
# Singleton instance
|
| 89 |
+
_queue_service: Optional[QueueService] = None
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def get_queue_service() -> QueueService:
|
| 93 |
+
"""Get or create the queue service singleton."""
|
| 94 |
+
global _queue_service
|
| 95 |
+
if _queue_service is None:
|
| 96 |
+
_queue_service = QueueService()
|
| 97 |
+
return _queue_service
|
backend/app/services/text_analyzer.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import time
|
| 3 |
+
from typing import Dict, Any
|
| 4 |
+
|
| 5 |
+
logger = logging.getLogger(__name__)
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
async def analyze_text(text: str) -> Dict[str, Any]:
|
| 9 |
+
start_time = time.time()
|
| 10 |
+
|
| 11 |
+
logger.info(f"Starting text analysis, length: {len(text)} chars")
|
| 12 |
+
|
| 13 |
+
text_hash = hash(text) % 100
|
| 14 |
+
is_deepfake = text_hash > 50
|
| 15 |
+
confidence = (text_hash % 100) / 100.0
|
| 16 |
+
|
| 17 |
+
analysis_time = time.time() - start_time
|
| 18 |
+
|
| 19 |
+
result = {
|
| 20 |
+
"is_deepfake": is_deepfake,
|
| 21 |
+
"confidence": round(confidence, 3),
|
| 22 |
+
"analysis_time": round(analysis_time, 3),
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
logger.info(f"Text analysis completed. Result: {result}")
|
| 26 |
+
return result
|
backend/app/utils/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Utility functions and exception classes."""
|
backend/app/utils/exceptions.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Custom exception classes."""
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class DeepfakeDetectionError(Exception):
|
| 5 |
+
"""Base exception for deepfake detection service."""
|
| 6 |
+
|
| 7 |
+
def __init__(self, message: str, status_code: int = 500):
|
| 8 |
+
self.message = message
|
| 9 |
+
self.status_code = status_code
|
| 10 |
+
super().__init__(self.message)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class FileDownloadError(DeepfakeDetectionError):
|
| 14 |
+
"""Exception raised when file download fails."""
|
| 15 |
+
|
| 16 |
+
def __init__(self, message: str, status_code: int = 400):
|
| 17 |
+
super().__init__(message, status_code)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class InvalidURLError(DeepfakeDetectionError):
|
| 21 |
+
"""Exception raised when URL is invalid."""
|
| 22 |
+
|
| 23 |
+
def __init__(self, message: str = "Invalid URL format"):
|
| 24 |
+
super().__init__(message, 400)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class DownloadTimeoutError(DeepfakeDetectionError):
|
| 28 |
+
"""Exception raised when file download times out."""
|
| 29 |
+
|
| 30 |
+
def __init__(self, message: str = "File download timed out"):
|
| 31 |
+
super().__init__(message, 408)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class FileSizeError(DeepfakeDetectionError):
|
| 35 |
+
"""Exception raised when file size exceeds limit."""
|
| 36 |
+
|
| 37 |
+
def __init__(self, message: str = "File size exceeds maximum allowed size"):
|
| 38 |
+
super().__init__(message, 400)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class DetectorError(DeepfakeDetectionError):
|
| 42 |
+
"""Exception raised when detector model fails."""
|
| 43 |
+
|
| 44 |
+
def __init__(self, message: str = "Deepfake detection failed"):
|
| 45 |
+
super().__init__(message, 500)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class UnsupportedModelError(DeepfakeDetectionError):
|
| 49 |
+
"""Exception raised when requested model is not supported."""
|
| 50 |
+
|
| 51 |
+
def __init__(self, model_name: str):
|
| 52 |
+
message = f"Detector model '{model_name}' is not supported"
|
| 53 |
+
super().__init__(message, 400)
|
backend/main.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import uvicorn
|
| 2 |
+
from app import app
|
| 3 |
+
|
| 4 |
+
if __name__ == "__main__":
|
| 5 |
+
uvicorn.run(app, host="127.0.0.1", port=8000)
|
backend/requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.115.0
|
| 2 |
+
uvicorn[standard]==0.30.0
|
| 3 |
+
httpx==0.27.0
|
| 4 |
+
pydantic==2.8.2
|
| 5 |
+
pydantic-settings==2.3.1
|
| 6 |
+
python-multipart==0.0.6
|
backend/setup.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
import subprocess
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def run_command(command, description):
|
| 7 |
+
"""Run a command and handle errors."""
|
| 8 |
+
print(f"\n{'='*60}")
|
| 9 |
+
print(f"π {description}")
|
| 10 |
+
print(f"{'='*60}")
|
| 11 |
+
try:
|
| 12 |
+
result = subprocess.run(command, shell=True, check=True)
|
| 13 |
+
return result.returncode == 0
|
| 14 |
+
except subprocess.CalledProcessError as e:
|
| 15 |
+
print(f"β Error: {description} failed")
|
| 16 |
+
return False
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def main():
|
| 20 |
+
"""Setup the development environment."""
|
| 21 |
+
print("\n" + "="*60)
|
| 22 |
+
print("π Deepfake Detection Service - Backend Setup")
|
| 23 |
+
print("="*60)
|
| 24 |
+
print(f"β
Python version: {sys.version}")
|
| 25 |
+
|
| 26 |
+
# Determine OS for venv activation
|
| 27 |
+
is_windows = sys.platform == "win32"
|
| 28 |
+
venv_path = "venv"
|
| 29 |
+
|
| 30 |
+
# Create virtual environment
|
| 31 |
+
if not run_command(
|
| 32 |
+
f"{sys.executable} -m venv {venv_path}",
|
| 33 |
+
"Creating virtual environment"
|
| 34 |
+
):
|
| 35 |
+
sys.exit(1)
|
| 36 |
+
|
| 37 |
+
# Activate venv and install dependencies
|
| 38 |
+
if is_windows:
|
| 39 |
+
activate_cmd = f"{venv_path}\\Scripts\\activate && pip install -r requirements.txt"
|
| 40 |
+
else:
|
| 41 |
+
activate_cmd = f"source {venv_path}/bin/activate && pip install -r requirements.txt"
|
| 42 |
+
|
| 43 |
+
if not run_command(activate_cmd, "Installing dependencies"):
|
| 44 |
+
sys.exit(1)
|
| 45 |
+
|
| 46 |
+
# Create .env file if it doesn't exist
|
| 47 |
+
if not os.path.exists(".env"):
|
| 48 |
+
run_command("copy .env.example .env" if is_windows else "cp .env.example .env",
|
| 49 |
+
"Creating .env file from template")
|
| 50 |
+
|
| 51 |
+
print("\n" + "="*60)
|
| 52 |
+
print("β
Setup completed successfully!")
|
| 53 |
+
print("="*60)
|
| 54 |
+
print("\nπ Next steps:")
|
| 55 |
+
print(f" 1. Activate virtual environment:")
|
| 56 |
+
if is_windows:
|
| 57 |
+
print(f" {venv_path}\\Scripts\\activate")
|
| 58 |
+
else:
|
| 59 |
+
print(f" source {venv_path}/bin/activate")
|
| 60 |
+
print(f"\n 2. Start the server:")
|
| 61 |
+
print(f" python main.py")
|
| 62 |
+
print(f"\n 3. Visit http://127.0.0.1:8000/docs for interactive API docs")
|
| 63 |
+
print(f"\n 4. Check .env file for configuration options")
|
| 64 |
+
print("\n" + "="*60)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
if __name__ == "__main__":
|
| 68 |
+
main()
|