LogicGoInfotechSpaces commited on
Commit
84e896b
·
1 Parent(s): b3b1893

docs: update README files for Docker-only API setup

Browse files
Files changed (3) hide show
  1. README.md +50 -6
  2. api/README.md +209 -45
  3. 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: gradio
7
- sdk_version: 5.49.0
8
- app_file: app.py
9
  pinned: true
10
  disable_embedding: false
11
- short_description: Video deep fake
12
  ---
13
- facefusion MIT License
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 in MongoDB
11
  - **Job Status Tracking**: Monitor processing jobs with real-time status
12
 
13
- ## API Endpoints
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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": "string",
35
- "target_video_id": "string"
 
 
 
 
 
 
 
 
 
36
  }
37
  ```
38
 
@@ -41,11 +101,25 @@ Body: {
41
  GET /api/job/{job_id}
42
  ```
43
 
44
- ### 5. Get Result Video
 
 
 
 
 
 
 
 
 
 
 
 
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
- ## Setup
57
-
58
- 1. Install dependencies:
59
- ```bash
60
- pip install -r api/requirements.txt
61
  ```
62
-
63
- 2. Set environment variables (optional - MongoDB Atlas connection is already configured):
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
- 3. Start MongoDB server
 
 
 
 
 
 
 
 
 
69
 
70
- 4. Run the API:
71
  ```bash
72
- cd api
73
- python main.py
 
74
  ```
75
 
76
- The API will be available at `http://localhost:8000`
 
 
 
 
 
77
 
78
- ## Usage Example
79
 
80
- 1. Upload source image:
 
 
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": "image_id_here",
103
- "target_video_id": "video_id_here"
104
  }'
105
  ```
106
 
107
- 4. Check job status:
108
  ```bash
109
- curl -X GET "http://localhost:8000/api/job/job_id_here"
110
  ```
111
 
112
- 5. Download result:
113
  ```bash
114
- curl -X GET "http://localhost:8000/api/result-video/result_id_here" \
115
- --output result.mp4
116
  ```
117
 
118
- ## Database Schema
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
 
120
- ### Source Images Collection
 
 
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)