Commit
·
84e896b
1
Parent(s):
b3b1893
docs: update README files for Docker-only API setup
Browse files- README.md +50 -6
- api/README.md +209 -45
- api_server.py +437 -0
README.md
CHANGED
|
@@ -1,14 +1,58 @@
|
|
| 1 |
---
|
| 2 |
-
title: Video Face Swap
|
| 3 |
emoji: 👱🏻♀️
|
| 4 |
colorFrom: pink
|
| 5 |
colorTo: indigo
|
| 6 |
-
sdk:
|
| 7 |
-
sdk_version:
|
| 8 |
-
app_file:
|
| 9 |
pinned: true
|
| 10 |
disable_embedding: false
|
| 11 |
-
short_description:
|
| 12 |
---
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Video Face Swap API
|
| 3 |
emoji: 👱🏻♀️
|
| 4 |
colorFrom: pink
|
| 5 |
colorTo: indigo
|
| 6 |
+
sdk: docker
|
| 7 |
+
sdk_version: latest
|
| 8 |
+
app_file: api_server.py
|
| 9 |
pinned: true
|
| 10 |
disable_embedding: false
|
| 11 |
+
short_description: GPU-accelerated face swap video processing API
|
| 12 |
---
|
| 13 |
+
|
| 14 |
+
# Face Swap Video API
|
| 15 |
+
|
| 16 |
+
GPU-accelerated FastAPI backend for face swap video processing with MongoDB storage.
|
| 17 |
+
|
| 18 |
+
## 🚀 Quick Start
|
| 19 |
+
|
| 20 |
+
### Docker Deployment (Recommended)
|
| 21 |
+
|
| 22 |
+
```bash
|
| 23 |
+
docker-compose up --build
|
| 24 |
+
```
|
| 25 |
+
|
| 26 |
+
The API will be available at `http://localhost:8000`
|
| 27 |
+
|
| 28 |
+
See [API Documentation](api/README.md) for detailed usage and endpoints.
|
| 29 |
+
|
| 30 |
+
## Features
|
| 31 |
+
|
| 32 |
+
- ✅ GPU acceleration with CUDA support
|
| 33 |
+
- ✅ Asynchronous face swap processing
|
| 34 |
+
- ✅ MongoDB Atlas integration
|
| 35 |
+
- ✅ RESTful API with Swagger documentation
|
| 36 |
+
- ✅ Result video download URLs
|
| 37 |
+
- ✅ Job status tracking
|
| 38 |
+
|
| 39 |
+
## API Endpoints
|
| 40 |
+
|
| 41 |
+
- `POST /api/source-image` - Upload source image
|
| 42 |
+
- `POST /api/target-video` - Upload target video
|
| 43 |
+
- `POST /api/face-swap` - Start face swap processing
|
| 44 |
+
- `GET /api/job/{job_id}` - Get job status
|
| 45 |
+
- `GET /api/result-video/{result_video_id}` - Download result video
|
| 46 |
+
- `GET /docs` - Interactive API documentation
|
| 47 |
+
|
| 48 |
+
## Requirements
|
| 49 |
+
|
| 50 |
+
- Docker with NVIDIA GPU support (nvidia-docker2)
|
| 51 |
+
- MongoDB Atlas account (or local MongoDB)
|
| 52 |
+
- CUDA 12.1+ compatible GPU
|
| 53 |
+
|
| 54 |
+
## License
|
| 55 |
+
|
| 56 |
+
MIT License - Based on DeepFakeAI/facefusion
|
| 57 |
+
|
| 58 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
api/README.md
CHANGED
|
@@ -1,29 +1,80 @@
|
|
| 1 |
# Face Swap Video API
|
| 2 |
|
| 3 |
-
FastAPI backend for face swap video processing with MongoDB storage.
|
| 4 |
|
| 5 |
## Features
|
| 6 |
|
| 7 |
- **Source Image Upload**: Upload and store source images in MongoDB
|
| 8 |
- **Target Video Upload**: Upload and store target videos in MongoDB
|
| 9 |
-
- **Face Swap Processing**: Process face swaps asynchronously
|
| 10 |
-
- **Result Video Storage**: Store processed result videos
|
| 11 |
- **Job Status Tracking**: Monitor processing jobs with real-time status
|
| 12 |
|
| 13 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
### 1. Source Image Upload
|
| 16 |
```
|
| 17 |
POST /api/source-image
|
| 18 |
Content-Type: multipart/form-data
|
| 19 |
-
Body: image file
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
```
|
| 21 |
|
| 22 |
### 2. Target Video Upload
|
| 23 |
```
|
| 24 |
POST /api/target-video
|
| 25 |
Content-Type: multipart/form-data
|
| 26 |
-
Body: video file
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
```
|
| 28 |
|
| 29 |
### 3. Start Face Swap Processing
|
|
@@ -31,8 +82,17 @@ Body: video file
|
|
| 31 |
POST /api/face-swap
|
| 32 |
Content-Type: application/json
|
| 33 |
Body: {
|
| 34 |
-
"source_image_id": "
|
| 35 |
-
"target_video_id": "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
}
|
| 37 |
```
|
| 38 |
|
|
@@ -41,11 +101,25 @@ Body: {
|
|
| 41 |
GET /api/job/{job_id}
|
| 42 |
```
|
| 43 |
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
```
|
| 46 |
GET /api/result-video/{result_video_id}
|
| 47 |
```
|
| 48 |
|
|
|
|
|
|
|
| 49 |
### 6. List All Items
|
| 50 |
```
|
| 51 |
GET /api/source-images
|
|
@@ -53,71 +127,112 @@ GET /api/target-videos
|
|
| 53 |
GET /api/result-videos
|
| 54 |
```
|
| 55 |
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
1. Install dependencies:
|
| 59 |
-
```bash
|
| 60 |
-
pip install -r api/requirements.txt
|
| 61 |
```
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
```bash
|
| 65 |
-
export MONGODB_URL="mongodb+srv://itishalogicgo_db_user:HR837xi0B9yh2vZK@cluster0.jeeytpz.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0"
|
| 66 |
```
|
| 67 |
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
|
| 70 |
-
4. Run the API:
|
| 71 |
```bash
|
| 72 |
-
|
| 73 |
-
|
|
|
|
| 74 |
```
|
| 75 |
|
| 76 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
-
## Usage
|
| 79 |
|
| 80 |
-
|
|
|
|
|
|
|
| 81 |
```bash
|
| 82 |
curl -X POST "http://localhost:8000/api/source-image" \
|
| 83 |
-H "accept: application/json" \
|
| 84 |
-
-H "Content-Type: multipart/form-data" \
|
| 85 |
-F "file=@source.jpg"
|
| 86 |
```
|
| 87 |
|
| 88 |
-
2. Upload target video
|
| 89 |
```bash
|
| 90 |
curl -X POST "http://localhost:8000/api/target-video" \
|
| 91 |
-H "accept: application/json" \
|
| 92 |
-
-H "Content-Type: multipart/form-data" \
|
| 93 |
-F "file=@target.mp4"
|
| 94 |
```
|
| 95 |
|
| 96 |
-
3. Start face swap
|
| 97 |
```bash
|
| 98 |
curl -X POST "http://localhost:8000/api/face-swap" \
|
| 99 |
-
-H "accept: application/json" \
|
| 100 |
-H "Content-Type: application/json" \
|
| 101 |
-d '{
|
| 102 |
-
"source_image_id": "
|
| 103 |
-
"target_video_id": "
|
| 104 |
}'
|
| 105 |
```
|
| 106 |
|
| 107 |
-
4. Check job status
|
| 108 |
```bash
|
| 109 |
-
curl -X GET "http://localhost:8000/api/job/
|
| 110 |
```
|
| 111 |
|
| 112 |
-
5. Download result
|
| 113 |
```bash
|
| 114 |
-
curl -
|
| 115 |
-
|
| 116 |
```
|
| 117 |
|
| 118 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
|
| 120 |
-
|
|
|
|
|
|
|
| 121 |
```json
|
| 122 |
{
|
| 123 |
"_id": "ObjectId",
|
|
@@ -130,7 +245,7 @@ curl -X GET "http://localhost:8000/api/result-video/result_id_here" \
|
|
| 130 |
}
|
| 131 |
```
|
| 132 |
|
| 133 |
-
### Target Videos Collection
|
| 134 |
```json
|
| 135 |
{
|
| 136 |
"_id": "ObjectId",
|
|
@@ -143,7 +258,7 @@ curl -X GET "http://localhost:8000/api/result-video/result_id_here" \
|
|
| 143 |
}
|
| 144 |
```
|
| 145 |
|
| 146 |
-
### Result Videos Collection
|
| 147 |
```json
|
| 148 |
{
|
| 149 |
"_id": "ObjectId",
|
|
@@ -157,17 +272,66 @@ curl -X GET "http://localhost:8000/api/result-video/result_id_here" \
|
|
| 157 |
}
|
| 158 |
```
|
| 159 |
|
| 160 |
-
### Processing Jobs Collection
|
| 161 |
```json
|
| 162 |
{
|
| 163 |
"_id": "ObjectId",
|
| 164 |
-
"job_id": "string",
|
| 165 |
"source_image_id": "string",
|
| 166 |
"target_video_id": "string",
|
| 167 |
-
"status": "string",
|
| 168 |
"created_at": "datetime",
|
| 169 |
-
"progress": "number",
|
| 170 |
"result_video_id": "string",
|
|
|
|
| 171 |
"error": "string"
|
| 172 |
}
|
| 173 |
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# Face Swap Video API
|
| 2 |
|
| 3 |
+
FastAPI backend for face swap video processing with MongoDB storage and GPU acceleration.
|
| 4 |
|
| 5 |
## Features
|
| 6 |
|
| 7 |
- **Source Image Upload**: Upload and store source images in MongoDB
|
| 8 |
- **Target Video Upload**: Upload and store target videos in MongoDB
|
| 9 |
+
- **Face Swap Processing**: Process face swaps asynchronously with GPU acceleration
|
| 10 |
+
- **Result Video Storage**: Store processed result videos with HTTPS download URLs
|
| 11 |
- **Job Status Tracking**: Monitor processing jobs with real-time status
|
| 12 |
|
| 13 |
+
## 🚀 Docker Deployment (Recommended)
|
| 14 |
+
|
| 15 |
+
### Prerequisites
|
| 16 |
+
- Docker with NVIDIA GPU support (nvidia-docker2)
|
| 17 |
+
- NVIDIA GPU with CUDA support
|
| 18 |
+
|
| 19 |
+
### Quick Start
|
| 20 |
+
|
| 21 |
+
1. **Build and run with Docker Compose:**
|
| 22 |
+
```bash
|
| 23 |
+
docker-compose up --build
|
| 24 |
+
```
|
| 25 |
+
|
| 26 |
+
2. **Or build and run with Docker:**
|
| 27 |
+
```bash
|
| 28 |
+
docker build -t face-swap-api .
|
| 29 |
+
docker run --gpus all -p 8000:8000 \
|
| 30 |
+
-e MONGODB_URL="your_mongodb_url" \
|
| 31 |
+
face-swap-api
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
The API will be available at `http://localhost:8000`
|
| 35 |
+
|
| 36 |
+
## 📡 API Endpoints
|
| 37 |
+
|
| 38 |
+
### Base URL
|
| 39 |
+
```
|
| 40 |
+
http://localhost:8000 (Local)
|
| 41 |
+
https://your-domain.com/api (Production)
|
| 42 |
+
```
|
| 43 |
|
| 44 |
### 1. Source Image Upload
|
| 45 |
```
|
| 46 |
POST /api/source-image
|
| 47 |
Content-Type: multipart/form-data
|
| 48 |
+
Body: image file (jpg, png, etc.)
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
**Response:**
|
| 52 |
+
```json
|
| 53 |
+
{
|
| 54 |
+
"id": "64f7b8c9e1234567890abcde",
|
| 55 |
+
"filename": "source.jpg",
|
| 56 |
+
"file_path": "/path/to/file",
|
| 57 |
+
"uploaded_at": "2024-01-15T10:30:00.000Z",
|
| 58 |
+
"status": "uploaded"
|
| 59 |
+
}
|
| 60 |
```
|
| 61 |
|
| 62 |
### 2. Target Video Upload
|
| 63 |
```
|
| 64 |
POST /api/target-video
|
| 65 |
Content-Type: multipart/form-data
|
| 66 |
+
Body: video file (mp4, mov, etc.)
|
| 67 |
+
```
|
| 68 |
+
|
| 69 |
+
**Response:**
|
| 70 |
+
```json
|
| 71 |
+
{
|
| 72 |
+
"id": "64f7b8c9e1234567890abcdf",
|
| 73 |
+
"filename": "target.mp4",
|
| 74 |
+
"file_path": "/path/to/file",
|
| 75 |
+
"uploaded_at": "2024-01-15T10:30:00.000Z",
|
| 76 |
+
"status": "uploaded"
|
| 77 |
+
}
|
| 78 |
```
|
| 79 |
|
| 80 |
### 3. Start Face Swap Processing
|
|
|
|
| 82 |
POST /api/face-swap
|
| 83 |
Content-Type: application/json
|
| 84 |
Body: {
|
| 85 |
+
"source_image_id": "SOURCE_IMAGE_ID",
|
| 86 |
+
"target_video_id": "TARGET_VIDEO_ID"
|
| 87 |
+
}
|
| 88 |
+
```
|
| 89 |
+
|
| 90 |
+
**Response:**
|
| 91 |
+
```json
|
| 92 |
+
{
|
| 93 |
+
"job_id": "550e8400-e29b-41d4-a716-446655440000",
|
| 94 |
+
"status": "queued",
|
| 95 |
+
"progress": 0.0
|
| 96 |
}
|
| 97 |
```
|
| 98 |
|
|
|
|
| 101 |
GET /api/job/{job_id}
|
| 102 |
```
|
| 103 |
|
| 104 |
+
**Response (when completed):**
|
| 105 |
+
```json
|
| 106 |
+
{
|
| 107 |
+
"job_id": "550e8400-e29b-41d4-a716-446655440000",
|
| 108 |
+
"status": "completed",
|
| 109 |
+
"progress": 100.0,
|
| 110 |
+
"result_video_id": "64f7b8c9e1234567890abce0",
|
| 111 |
+
"result_video_url": "https://your-domain.com/api/result-video/64f7b8c9e1234567890abce0",
|
| 112 |
+
"error": null
|
| 113 |
+
}
|
| 114 |
+
```
|
| 115 |
+
|
| 116 |
+
### 5. Download Result Video
|
| 117 |
```
|
| 118 |
GET /api/result-video/{result_video_id}
|
| 119 |
```
|
| 120 |
|
| 121 |
+
Returns the processed video file directly.
|
| 122 |
+
|
| 123 |
### 6. List All Items
|
| 124 |
```
|
| 125 |
GET /api/source-images
|
|
|
|
| 127 |
GET /api/result-videos
|
| 128 |
```
|
| 129 |
|
| 130 |
+
### 7. Health Check
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
```
|
| 132 |
+
GET /api/health
|
| 133 |
+
GET /
|
|
|
|
|
|
|
| 134 |
```
|
| 135 |
|
| 136 |
+
## 🐳 Docker Setup
|
| 137 |
+
|
| 138 |
+
### Dockerfile
|
| 139 |
+
The Dockerfile uses:
|
| 140 |
+
- **Base Image**: `nvidia/cuda:12.1.0-cudnn8-runtime-ubuntu22.04`
|
| 141 |
+
- **GPU Support**: CUDA 12.1 with cuDNN 8
|
| 142 |
+
- **Python**: 3.10
|
| 143 |
+
- **ONNX Runtime**: GPU version for fast inference
|
| 144 |
+
|
| 145 |
+
### Environment Variables
|
| 146 |
|
|
|
|
| 147 |
```bash
|
| 148 |
+
MONGODB_URL=mongodb+srv://... # MongoDB connection string
|
| 149 |
+
BASE_URL=http://localhost:8000 # Base URL for download links
|
| 150 |
+
CUDA_VISIBLE_DEVICES=0 # GPU device ID
|
| 151 |
```
|
| 152 |
|
| 153 |
+
### Docker Compose
|
| 154 |
+
|
| 155 |
+
The `docker-compose.yml` includes:
|
| 156 |
+
- GPU resource allocation
|
| 157 |
+
- Volume mounts for uploads and models
|
| 158 |
+
- Automatic restart on failure
|
| 159 |
|
| 160 |
+
## 📝 Usage Examples
|
| 161 |
|
| 162 |
+
### cURL Examples
|
| 163 |
+
|
| 164 |
+
1. **Upload source image:**
|
| 165 |
```bash
|
| 166 |
curl -X POST "http://localhost:8000/api/source-image" \
|
| 167 |
-H "accept: application/json" \
|
|
|
|
| 168 |
-F "file=@source.jpg"
|
| 169 |
```
|
| 170 |
|
| 171 |
+
2. **Upload target video:**
|
| 172 |
```bash
|
| 173 |
curl -X POST "http://localhost:8000/api/target-video" \
|
| 174 |
-H "accept: application/json" \
|
|
|
|
| 175 |
-F "file=@target.mp4"
|
| 176 |
```
|
| 177 |
|
| 178 |
+
3. **Start face swap:**
|
| 179 |
```bash
|
| 180 |
curl -X POST "http://localhost:8000/api/face-swap" \
|
|
|
|
| 181 |
-H "Content-Type: application/json" \
|
| 182 |
-d '{
|
| 183 |
+
"source_image_id": "SOURCE_IMAGE_ID",
|
| 184 |
+
"target_video_id": "TARGET_VIDEO_ID"
|
| 185 |
}'
|
| 186 |
```
|
| 187 |
|
| 188 |
+
4. **Check job status:**
|
| 189 |
```bash
|
| 190 |
+
curl -X GET "http://localhost:8000/api/job/JOB_ID"
|
| 191 |
```
|
| 192 |
|
| 193 |
+
5. **Download result video:**
|
| 194 |
```bash
|
| 195 |
+
curl -L -o result.mp4 \
|
| 196 |
+
"http://localhost:8000/api/result-video/RESULT_VIDEO_ID"
|
| 197 |
```
|
| 198 |
|
| 199 |
+
### Python Example
|
| 200 |
+
|
| 201 |
+
```python
|
| 202 |
+
import requests
|
| 203 |
+
|
| 204 |
+
BASE_URL = "http://localhost:8000"
|
| 205 |
+
|
| 206 |
+
# 1. Upload source image
|
| 207 |
+
with open("source.jpg", "rb") as f:
|
| 208 |
+
response = requests.post(f"{BASE_URL}/api/source-image", files={"file": f})
|
| 209 |
+
source_id = response.json()["id"]
|
| 210 |
+
|
| 211 |
+
# 2. Upload target video
|
| 212 |
+
with open("target.mp4", "rb") as f:
|
| 213 |
+
response = requests.post(f"{BASE_URL}/api/target-video", files={"file": f})
|
| 214 |
+
target_id = response.json()["id"]
|
| 215 |
+
|
| 216 |
+
# 3. Start face swap
|
| 217 |
+
response = requests.post(
|
| 218 |
+
f"{BASE_URL}/api/face-swap",
|
| 219 |
+
json={"source_image_id": source_id, "target_video_id": target_id}
|
| 220 |
+
)
|
| 221 |
+
job_id = response.json()["job_id"]
|
| 222 |
+
|
| 223 |
+
# 4. Poll for completion
|
| 224 |
+
while True:
|
| 225 |
+
status = requests.get(f"{BASE_URL}/api/job/{job_id}").json()
|
| 226 |
+
if status["status"] == "completed":
|
| 227 |
+
result_url = status["result_video_url"]
|
| 228 |
+
print(f"Download URL: {result_url}")
|
| 229 |
+
break
|
| 230 |
+
time.sleep(5)
|
| 231 |
+
```
|
| 232 |
|
| 233 |
+
## 🗄️ Database Schema
|
| 234 |
+
|
| 235 |
+
### Source Images Collection (`source_images`)
|
| 236 |
```json
|
| 237 |
{
|
| 238 |
"_id": "ObjectId",
|
|
|
|
| 245 |
}
|
| 246 |
```
|
| 247 |
|
| 248 |
+
### Target Videos Collection (`target_videos`)
|
| 249 |
```json
|
| 250 |
{
|
| 251 |
"_id": "ObjectId",
|
|
|
|
| 258 |
}
|
| 259 |
```
|
| 260 |
|
| 261 |
+
### Result Videos Collection (`result_videos`)
|
| 262 |
```json
|
| 263 |
{
|
| 264 |
"_id": "ObjectId",
|
|
|
|
| 272 |
}
|
| 273 |
```
|
| 274 |
|
| 275 |
+
### Processing Jobs Collection (`processing_jobs`)
|
| 276 |
```json
|
| 277 |
{
|
| 278 |
"_id": "ObjectId",
|
| 279 |
+
"job_id": "string (UUID)",
|
| 280 |
"source_image_id": "string",
|
| 281 |
"target_video_id": "string",
|
| 282 |
+
"status": "string (queued|processing|completed|failed)",
|
| 283 |
"created_at": "datetime",
|
| 284 |
+
"progress": "number (0-100)",
|
| 285 |
"result_video_id": "string",
|
| 286 |
+
"result_video_url": "string",
|
| 287 |
"error": "string"
|
| 288 |
}
|
| 289 |
```
|
| 290 |
+
|
| 291 |
+
## 🔧 Configuration
|
| 292 |
+
|
| 293 |
+
### MongoDB Connection
|
| 294 |
+
- **Connection String**: Set via `MONGODB_URL` environment variable
|
| 295 |
+
- **Database**: `face_swap_video`
|
| 296 |
+
- **Collections**: `source_images`, `target_videos`, `result_videos`, `processing_jobs`
|
| 297 |
+
|
| 298 |
+
### GPU Configuration
|
| 299 |
+
- **CUDA Version**: 12.1
|
| 300 |
+
- **cuDNN**: 8
|
| 301 |
+
- **ONNX Runtime**: GPU-enabled
|
| 302 |
+
- **Device**: Automatically detects and uses available GPU
|
| 303 |
+
|
| 304 |
+
## 📊 API Documentation
|
| 305 |
+
|
| 306 |
+
Once running, visit:
|
| 307 |
+
- **Swagger UI**: `http://localhost:8000/docs`
|
| 308 |
+
- **ReDoc**: `http://localhost:8000/redoc`
|
| 309 |
+
|
| 310 |
+
## 🚨 Troubleshooting
|
| 311 |
+
|
| 312 |
+
### GPU Not Detected
|
| 313 |
+
Check NVIDIA drivers:
|
| 314 |
+
```bash
|
| 315 |
+
nvidia-smi
|
| 316 |
+
```
|
| 317 |
+
|
| 318 |
+
### MongoDB Connection Issues
|
| 319 |
+
Verify connection string and network access to MongoDB Atlas.
|
| 320 |
+
|
| 321 |
+
### Model Download Failures
|
| 322 |
+
Ensure `TOKEN` or `HF_TOKEN` environment variable is set for Hugging Face downloads.
|
| 323 |
+
|
| 324 |
+
## 📦 Files Structure
|
| 325 |
+
|
| 326 |
+
```
|
| 327 |
+
.
|
| 328 |
+
├── api_server.py # Main API server (no Gradio)
|
| 329 |
+
├── Dockerfile # Docker image with GPU support
|
| 330 |
+
├── docker-compose.yml # Docker Compose configuration
|
| 331 |
+
├── requirements.txt # Python dependencies
|
| 332 |
+
├── DeepFakeAI/ # Face swap processing library
|
| 333 |
+
└── uploads/ # Uploaded files directory
|
| 334 |
+
├── source_images/
|
| 335 |
+
├── target_videos/
|
| 336 |
+
└── result_videos/
|
| 337 |
+
```
|
api_server.py
ADDED
|
@@ -0,0 +1,437 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI, File, UploadFile, HTTPException, BackgroundTasks
|
| 2 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 3 |
+
from pydantic import BaseModel
|
| 4 |
+
from typing import Optional, List
|
| 5 |
+
import os
|
| 6 |
+
import uuid
|
| 7 |
+
import asyncio
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
import motor.motor_asyncio
|
| 10 |
+
from bson import ObjectId
|
| 11 |
+
import json
|
| 12 |
+
import shutil
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
from fastapi.responses import FileResponse, StreamingResponse, JSONResponse
|
| 15 |
+
|
| 16 |
+
# Import face swap functionality
|
| 17 |
+
import sys
|
| 18 |
+
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 19 |
+
import DeepFakeAI.globals as DF_G
|
| 20 |
+
from DeepFakeAI import utilities as DF_U
|
| 21 |
+
from DeepFakeAI.processors.frame.modules import face_swapper as DF_FS
|
| 22 |
+
|
| 23 |
+
app = FastAPI(title="Face Swap Video API", version="1.0.0")
|
| 24 |
+
|
| 25 |
+
# CORS middleware
|
| 26 |
+
app.add_middleware(
|
| 27 |
+
CORSMiddleware,
|
| 28 |
+
allow_origins=["*"],
|
| 29 |
+
allow_credentials=True,
|
| 30 |
+
allow_methods=["*"],
|
| 31 |
+
allow_headers=["*"],
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
# MongoDB connection
|
| 35 |
+
MONGODB_URL = os.getenv("MONGODB_URL", "mongodb+srv://itishalogicgo_db_user:HR837xi0B9yh2vZK@cluster0.jeeytpz.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0")
|
| 36 |
+
DATABASE_NAME = "face_swap_video"
|
| 37 |
+
client = motor.motor_asyncio.AsyncIOMotorClient(MONGODB_URL)
|
| 38 |
+
db = client[DATABASE_NAME]
|
| 39 |
+
|
| 40 |
+
# Collections
|
| 41 |
+
source_images_collection = db["source_images"]
|
| 42 |
+
target_videos_collection = db["target_videos"]
|
| 43 |
+
result_videos_collection = db["result_videos"]
|
| 44 |
+
jobs_collection = db["processing_jobs"]
|
| 45 |
+
|
| 46 |
+
# Upload directories
|
| 47 |
+
UPLOAD_DIR = Path("uploads")
|
| 48 |
+
SOURCE_IMAGES_DIR = UPLOAD_DIR / "source_images"
|
| 49 |
+
TARGET_VIDEOS_DIR = UPLOAD_DIR / "target_videos"
|
| 50 |
+
RESULT_VIDEOS_DIR = UPLOAD_DIR / "result_videos"
|
| 51 |
+
|
| 52 |
+
# Create directories
|
| 53 |
+
for dir_path in [UPLOAD_DIR, SOURCE_IMAGES_DIR, TARGET_VIDEOS_DIR, RESULT_VIDEOS_DIR]:
|
| 54 |
+
dir_path.mkdir(parents=True, exist_ok=True)
|
| 55 |
+
|
| 56 |
+
def _run_local_faceswap(source_image_path: str, target_video_path: str) -> Optional[str]:
|
| 57 |
+
# Configure defaults for local pipeline
|
| 58 |
+
DF_G.source_path = source_image_path
|
| 59 |
+
DF_G.target_path = target_video_path
|
| 60 |
+
DF_G.output_video_encoder = 'libx264'
|
| 61 |
+
DF_G.output_video_quality = 20
|
| 62 |
+
DF_G.temp_frame_format = 'png'
|
| 63 |
+
DF_G.temp_frame_quality = 95
|
| 64 |
+
DF_G.keep_temp = False
|
| 65 |
+
DF_G.skip_audio = False
|
| 66 |
+
# Face processing options
|
| 67 |
+
DF_G.face_recognition = ['many']
|
| 68 |
+
DF_G.reference_frame_number = 0
|
| 69 |
+
DF_G.execution_thread_count = 2
|
| 70 |
+
DF_G.execution_queue_count = 2
|
| 71 |
+
# Prefer CUDA (GPU) if available; fallback to CPU
|
| 72 |
+
try:
|
| 73 |
+
DF_G.execution_providers = DF_U.decode_execution_providers(['cuda', 'cpu'])
|
| 74 |
+
except:
|
| 75 |
+
DF_G.execution_providers = DF_U.decode_execution_providers(['cpu'])
|
| 76 |
+
# Fix invalid OMP thread settings
|
| 77 |
+
try:
|
| 78 |
+
import os as _os
|
| 79 |
+
_os.environ["OMP_NUM_THREADS"] = "1"
|
| 80 |
+
except:
|
| 81 |
+
pass
|
| 82 |
+
|
| 83 |
+
# Ensure model exists
|
| 84 |
+
model_dir = DF_U.resolve_relative_path('../.assets/models')
|
| 85 |
+
os.makedirs(model_dir, exist_ok=True)
|
| 86 |
+
model_path = os.path.join(model_dir, 'inswapper_128.onnx')
|
| 87 |
+
if not os.path.exists(model_path):
|
| 88 |
+
from huggingface_hub import hf_hub_download
|
| 89 |
+
token = os.environ.get('TOKEN') or os.environ.get('HF_TOKEN')
|
| 90 |
+
for repo_id in ['zihaomu/inswapper_128.onnx', 'linyi/inswapper_128.onnx', 'banodoco/inswapper_128.onnx']:
|
| 91 |
+
try:
|
| 92 |
+
model_path = hf_hub_download(repo_id=repo_id, filename='inswapper_128.onnx', token=token)
|
| 93 |
+
break
|
| 94 |
+
except:
|
| 95 |
+
continue
|
| 96 |
+
if os.path.exists(model_path):
|
| 97 |
+
os.environ['INSWAPPER_PATH'] = model_path
|
| 98 |
+
DF_FS.pre_check()
|
| 99 |
+
|
| 100 |
+
# Extract frames
|
| 101 |
+
fps = DF_U.detect_fps(target_video_path) or 12.0
|
| 102 |
+
DF_U.create_temp(target_video_path)
|
| 103 |
+
ok = DF_U.extract_frames(target_video_path, fps)
|
| 104 |
+
if not ok:
|
| 105 |
+
return None
|
| 106 |
+
temp_frames = DF_U.get_temp_frame_paths(target_video_path)
|
| 107 |
+
if not temp_frames:
|
| 108 |
+
return None
|
| 109 |
+
|
| 110 |
+
# Process frames
|
| 111 |
+
DF_FS.process_video(source_image_path, temp_frames)
|
| 112 |
+
|
| 113 |
+
# Rebuild video and restore audio
|
| 114 |
+
if not DF_U.create_video(target_video_path, fps):
|
| 115 |
+
return None
|
| 116 |
+
out_path = DF_U.normalize_output_path(source_image_path, target_video_path, str(RESULT_VIDEOS_DIR / f"out_{uuid.uuid4().hex}.mp4"))
|
| 117 |
+
DF_U.restore_audio(target_video_path, out_path)
|
| 118 |
+
DF_U.clear_temp(target_video_path)
|
| 119 |
+
return out_path
|
| 120 |
+
|
| 121 |
+
# Pydantic models
|
| 122 |
+
class SourceImageResponse(BaseModel):
|
| 123 |
+
id: str
|
| 124 |
+
filename: str
|
| 125 |
+
file_path: str
|
| 126 |
+
uploaded_at: datetime
|
| 127 |
+
status: str
|
| 128 |
+
|
| 129 |
+
class TargetVideoResponse(BaseModel):
|
| 130 |
+
id: str
|
| 131 |
+
filename: str
|
| 132 |
+
file_path: str
|
| 133 |
+
uploaded_at: datetime
|
| 134 |
+
status: str
|
| 135 |
+
|
| 136 |
+
class ResultVideoResponse(BaseModel):
|
| 137 |
+
id: str
|
| 138 |
+
source_image_id: str
|
| 139 |
+
target_video_id: str
|
| 140 |
+
result_file_path: str
|
| 141 |
+
created_at: datetime
|
| 142 |
+
status: str
|
| 143 |
+
processing_time: Optional[float] = None
|
| 144 |
+
|
| 145 |
+
class FaceSwapRequest(BaseModel):
|
| 146 |
+
source_image_id: str
|
| 147 |
+
target_video_id: str
|
| 148 |
+
|
| 149 |
+
class JobStatus(BaseModel):
|
| 150 |
+
job_id: str
|
| 151 |
+
status: str
|
| 152 |
+
progress: Optional[float] = None
|
| 153 |
+
result_video_id: Optional[str] = None
|
| 154 |
+
result_video_url: Optional[str] = None # HTTPS download URL
|
| 155 |
+
error: Optional[str] = None
|
| 156 |
+
|
| 157 |
+
# Base URL for generating download links
|
| 158 |
+
BASE_URL = os.getenv("BASE_URL", "https://logicgoinfotechspaces-face-swap-video.hf.space")
|
| 159 |
+
|
| 160 |
+
def get_result_video_url(result_video_id: str) -> str:
|
| 161 |
+
"""Generate HTTPS download URL for result video"""
|
| 162 |
+
return f"{BASE_URL}/api/result-video/{result_video_id}"
|
| 163 |
+
|
| 164 |
+
# Helper functions
|
| 165 |
+
def save_file_to_disk(file: UploadFile, directory: Path) -> str:
|
| 166 |
+
"""Save uploaded file to disk and return the file path"""
|
| 167 |
+
file_extension = Path(file.filename).suffix
|
| 168 |
+
unique_filename = f"{uuid.uuid4().hex}{file_extension}"
|
| 169 |
+
file_path = directory / unique_filename
|
| 170 |
+
|
| 171 |
+
with open(file_path, "wb") as buffer:
|
| 172 |
+
shutil.copyfileobj(file.file, buffer)
|
| 173 |
+
|
| 174 |
+
return str(file_path)
|
| 175 |
+
|
| 176 |
+
async def process_face_swap(job_id: str, source_image_path: str, target_video_path: str):
|
| 177 |
+
"""Background task to process face swap"""
|
| 178 |
+
try:
|
| 179 |
+
# Update job status to processing
|
| 180 |
+
await jobs_collection.update_one(
|
| 181 |
+
{"job_id": job_id},
|
| 182 |
+
{"$set": {"status": "processing", "progress": 0.0}}
|
| 183 |
+
)
|
| 184 |
+
|
| 185 |
+
# Run face swap
|
| 186 |
+
result_path = _run_local_faceswap(source_image_path, target_video_path)
|
| 187 |
+
|
| 188 |
+
if result_path and os.path.exists(result_path):
|
| 189 |
+
# Save result to MongoDB
|
| 190 |
+
result_doc = {
|
| 191 |
+
"source_image_path": source_image_path,
|
| 192 |
+
"target_video_path": target_video_path,
|
| 193 |
+
"result_file_path": result_path,
|
| 194 |
+
"created_at": datetime.utcnow(),
|
| 195 |
+
"status": "completed",
|
| 196 |
+
"job_id": job_id
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
result = await result_videos_collection.insert_one(result_doc)
|
| 200 |
+
result_video_id = str(result.inserted_id)
|
| 201 |
+
|
| 202 |
+
# Update job status to completed
|
| 203 |
+
await jobs_collection.update_one(
|
| 204 |
+
{"job_id": job_id},
|
| 205 |
+
{"$set": {
|
| 206 |
+
"status": "completed",
|
| 207 |
+
"progress": 100.0,
|
| 208 |
+
"result_video_id": result_video_id,
|
| 209 |
+
"result_video_url": get_result_video_url(result_video_id)
|
| 210 |
+
}}
|
| 211 |
+
)
|
| 212 |
+
else:
|
| 213 |
+
# Update job status to failed
|
| 214 |
+
await jobs_collection.update_one(
|
| 215 |
+
{"job_id": job_id},
|
| 216 |
+
{"$set": {
|
| 217 |
+
"status": "failed",
|
| 218 |
+
"error": "Face swap processing failed"
|
| 219 |
+
}}
|
| 220 |
+
)
|
| 221 |
+
|
| 222 |
+
except Exception as e:
|
| 223 |
+
# Update job status to failed
|
| 224 |
+
await jobs_collection.update_one(
|
| 225 |
+
{"job_id": job_id},
|
| 226 |
+
{"$set": {
|
| 227 |
+
"status": "failed",
|
| 228 |
+
"error": str(e)
|
| 229 |
+
}}
|
| 230 |
+
)
|
| 231 |
+
|
| 232 |
+
# API Endpoints
|
| 233 |
+
|
| 234 |
+
@app.post("/api/source-image", response_model=SourceImageResponse)
|
| 235 |
+
async def upload_source_image(file: UploadFile = File(...)):
|
| 236 |
+
"""Upload and store source image in MongoDB"""
|
| 237 |
+
if not file.content_type.startswith('image/'):
|
| 238 |
+
raise HTTPException(status_code=400, detail="File must be an image")
|
| 239 |
+
|
| 240 |
+
try:
|
| 241 |
+
# Save file to disk
|
| 242 |
+
file_path = save_file_to_disk(file, SOURCE_IMAGES_DIR)
|
| 243 |
+
|
| 244 |
+
# Store metadata in MongoDB
|
| 245 |
+
doc = {
|
| 246 |
+
"filename": file.filename,
|
| 247 |
+
"file_path": file_path,
|
| 248 |
+
"uploaded_at": datetime.utcnow(),
|
| 249 |
+
"status": "uploaded",
|
| 250 |
+
"content_type": file.content_type,
|
| 251 |
+
"file_size": os.path.getsize(file_path)
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
result = await source_images_collection.insert_one(doc)
|
| 255 |
+
|
| 256 |
+
return SourceImageResponse(
|
| 257 |
+
id=str(result.inserted_id),
|
| 258 |
+
filename=file.filename,
|
| 259 |
+
file_path=file_path,
|
| 260 |
+
uploaded_at=doc["uploaded_at"],
|
| 261 |
+
status=doc["status"]
|
| 262 |
+
)
|
| 263 |
+
|
| 264 |
+
except Exception as e:
|
| 265 |
+
raise HTTPException(status_code=500, detail=f"Error uploading source image: {str(e)}")
|
| 266 |
+
|
| 267 |
+
@app.post("/api/target-video", response_model=TargetVideoResponse)
|
| 268 |
+
async def upload_target_video(file: UploadFile = File(...)):
|
| 269 |
+
"""Upload and store target video in MongoDB"""
|
| 270 |
+
if not file.content_type.startswith('video/'):
|
| 271 |
+
raise HTTPException(status_code=400, detail="File must be a video")
|
| 272 |
+
|
| 273 |
+
try:
|
| 274 |
+
# Save file to disk
|
| 275 |
+
file_path = save_file_to_disk(file, TARGET_VIDEOS_DIR)
|
| 276 |
+
|
| 277 |
+
# Store metadata in MongoDB
|
| 278 |
+
doc = {
|
| 279 |
+
"filename": file.filename,
|
| 280 |
+
"file_path": file_path,
|
| 281 |
+
"uploaded_at": datetime.utcnow(),
|
| 282 |
+
"status": "uploaded",
|
| 283 |
+
"content_type": file.content_type,
|
| 284 |
+
"file_size": os.path.getsize(file_path)
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
result = await target_videos_collection.insert_one(doc)
|
| 288 |
+
|
| 289 |
+
return TargetVideoResponse(
|
| 290 |
+
id=str(result.inserted_id),
|
| 291 |
+
filename=file.filename,
|
| 292 |
+
file_path=file_path,
|
| 293 |
+
uploaded_at=doc["uploaded_at"],
|
| 294 |
+
status=doc["status"]
|
| 295 |
+
)
|
| 296 |
+
|
| 297 |
+
except Exception as e:
|
| 298 |
+
raise HTTPException(status_code=500, detail=f"Error uploading target video: {str(e)}")
|
| 299 |
+
|
| 300 |
+
@app.post("/api/face-swap", response_model=JobStatus)
|
| 301 |
+
async def start_face_swap(request: FaceSwapRequest, background_tasks: BackgroundTasks):
|
| 302 |
+
"""Start face swap processing"""
|
| 303 |
+
try:
|
| 304 |
+
# Get source image and target video from MongoDB
|
| 305 |
+
source_image = await source_images_collection.find_one({"_id": ObjectId(request.source_image_id)})
|
| 306 |
+
target_video = await target_videos_collection.find_one({"_id": ObjectId(request.target_video_id)})
|
| 307 |
+
|
| 308 |
+
if not source_image:
|
| 309 |
+
raise HTTPException(status_code=404, detail="Source image not found")
|
| 310 |
+
if not target_video:
|
| 311 |
+
raise HTTPException(status_code=404, detail="Target video not found")
|
| 312 |
+
|
| 313 |
+
# Create job record
|
| 314 |
+
job_id = str(uuid.uuid4())
|
| 315 |
+
job_doc = {
|
| 316 |
+
"job_id": job_id,
|
| 317 |
+
"source_image_id": request.source_image_id,
|
| 318 |
+
"target_video_id": request.target_video_id,
|
| 319 |
+
"status": "queued",
|
| 320 |
+
"created_at": datetime.utcnow(),
|
| 321 |
+
"progress": 0.0
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
await jobs_collection.insert_one(job_doc)
|
| 325 |
+
|
| 326 |
+
# Start background processing
|
| 327 |
+
background_tasks.add_task(
|
| 328 |
+
process_face_swap,
|
| 329 |
+
job_id,
|
| 330 |
+
source_image["file_path"],
|
| 331 |
+
target_video["file_path"]
|
| 332 |
+
)
|
| 333 |
+
|
| 334 |
+
return JobStatus(
|
| 335 |
+
job_id=job_id,
|
| 336 |
+
status="queued",
|
| 337 |
+
progress=0.0
|
| 338 |
+
)
|
| 339 |
+
|
| 340 |
+
except Exception as e:
|
| 341 |
+
raise HTTPException(status_code=500, detail=f"Error starting face swap: {str(e)}")
|
| 342 |
+
|
| 343 |
+
@app.get("/api/job/{job_id}", response_model=JobStatus)
|
| 344 |
+
async def get_job_status(job_id: str):
|
| 345 |
+
"""Get job status"""
|
| 346 |
+
job = await jobs_collection.find_one({"job_id": job_id})
|
| 347 |
+
if not job:
|
| 348 |
+
raise HTTPException(status_code=404, detail="Job not found")
|
| 349 |
+
|
| 350 |
+
result_video_url = None
|
| 351 |
+
if job.get("result_video_id"):
|
| 352 |
+
result_video_url = get_result_video_url(job["result_video_id"])
|
| 353 |
+
|
| 354 |
+
return JobStatus(
|
| 355 |
+
job_id=job["job_id"],
|
| 356 |
+
status=job["status"],
|
| 357 |
+
progress=job.get("progress"),
|
| 358 |
+
result_video_id=job.get("result_video_id"),
|
| 359 |
+
result_video_url=result_video_url,
|
| 360 |
+
error=job.get("error")
|
| 361 |
+
)
|
| 362 |
+
|
| 363 |
+
@app.get("/api/result-video/{result_video_id}")
|
| 364 |
+
async def get_result_video(result_video_id: str):
|
| 365 |
+
"""Get result video file"""
|
| 366 |
+
result = await result_videos_collection.find_one({"_id": ObjectId(result_video_id)})
|
| 367 |
+
if not result:
|
| 368 |
+
raise HTTPException(status_code=404, detail="Result video not found")
|
| 369 |
+
|
| 370 |
+
if not os.path.exists(result["result_file_path"]):
|
| 371 |
+
raise HTTPException(status_code=404, detail="Result video file not found")
|
| 372 |
+
|
| 373 |
+
return FileResponse(
|
| 374 |
+
path=result["result_file_path"],
|
| 375 |
+
media_type="video/mp4",
|
| 376 |
+
filename=f"face_swap_result_{result_video_id}.mp4"
|
| 377 |
+
)
|
| 378 |
+
|
| 379 |
+
@app.get("/api/source-images", response_model=List[SourceImageResponse])
|
| 380 |
+
async def list_source_images():
|
| 381 |
+
"""List all source images"""
|
| 382 |
+
cursor = source_images_collection.find().sort("uploaded_at", -1)
|
| 383 |
+
images = []
|
| 384 |
+
async for doc in cursor:
|
| 385 |
+
images.append(SourceImageResponse(
|
| 386 |
+
id=str(doc["_id"]),
|
| 387 |
+
filename=doc["filename"],
|
| 388 |
+
file_path=doc["file_path"],
|
| 389 |
+
uploaded_at=doc["uploaded_at"],
|
| 390 |
+
status=doc["status"]
|
| 391 |
+
))
|
| 392 |
+
return images
|
| 393 |
+
|
| 394 |
+
@app.get("/api/target-videos", response_model=List[TargetVideoResponse])
|
| 395 |
+
async def list_target_videos():
|
| 396 |
+
"""List all target videos"""
|
| 397 |
+
cursor = target_videos_collection.find().sort("uploaded_at", -1)
|
| 398 |
+
videos = []
|
| 399 |
+
async for doc in cursor:
|
| 400 |
+
videos.append(TargetVideoResponse(
|
| 401 |
+
id=str(doc["_id"]),
|
| 402 |
+
filename=doc["filename"],
|
| 403 |
+
file_path=doc["file_path"],
|
| 404 |
+
uploaded_at=doc["uploaded_at"],
|
| 405 |
+
status=doc["status"]
|
| 406 |
+
))
|
| 407 |
+
return videos
|
| 408 |
+
|
| 409 |
+
@app.get("/api/result-videos", response_model=List[ResultVideoResponse])
|
| 410 |
+
async def list_result_videos():
|
| 411 |
+
"""List all result videos"""
|
| 412 |
+
cursor = result_videos_collection.find().sort("created_at", -1)
|
| 413 |
+
results = []
|
| 414 |
+
async for doc in cursor:
|
| 415 |
+
results.append(ResultVideoResponse(
|
| 416 |
+
id=str(doc["_id"]),
|
| 417 |
+
source_image_id=doc.get("source_image_path", ""),
|
| 418 |
+
target_video_id=doc.get("target_video_path", ""),
|
| 419 |
+
result_file_path=doc["result_file_path"],
|
| 420 |
+
created_at=doc["created_at"],
|
| 421 |
+
status=doc["status"],
|
| 422 |
+
processing_time=doc.get("processing_time")
|
| 423 |
+
))
|
| 424 |
+
return results
|
| 425 |
+
|
| 426 |
+
@app.get("/api/health")
|
| 427 |
+
async def api_health():
|
| 428 |
+
return {"status": "ok", "time": datetime.utcnow().isoformat()}
|
| 429 |
+
|
| 430 |
+
@app.get("/")
|
| 431 |
+
async def root():
|
| 432 |
+
"""Health check endpoint"""
|
| 433 |
+
return {"message": "Face Swap Video API is running", "version": "1.0.0"}
|
| 434 |
+
|
| 435 |
+
if __name__ == "__main__":
|
| 436 |
+
import uvicorn
|
| 437 |
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|