Dmitry1313 commited on
Commit
a6049bd
·
verified ·
1 Parent(s): 37ebc3f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +295 -61
app.py CHANGED
@@ -1,85 +1,319 @@
1
  #!/usr/bin/env python3
2
- import os
3
- import tempfile
4
- import subprocess
5
- import logging
6
- import shutil
7
- import uuid
8
- import time
9
- from fastapi import FastAPI, File, UploadFile, HTTPException
10
- from fastapi.responses import Response
11
 
12
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
 
 
 
 
 
 
 
 
 
 
13
  logger = logging.getLogger(__name__)
14
 
15
- app = FastAPI(title="FaceFusion API")
 
 
 
 
 
 
 
 
 
 
 
16
 
17
- FACEFUSION_DIR = "/facefusion"
18
- FACEFUSION_SCRIPT = os.path.join(FACEFUSION_DIR, "facefusion.py")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
- # Используем проверенную модель, которая точно есть в словаре
21
- MODEL_NAME = "inswapper_128"
22
- EXECUTION_PROVIDERS = ["cpu"]
 
 
 
 
 
 
 
 
23
 
24
  @app.get("/health")
25
  async def health():
26
- return {"status": "ok"}
27
-
28
- @app.post("/swap")
29
- async def swap_face(source: UploadFile = File(...), target: UploadFile = File(...)):
30
- temp_dir = tempfile.mkdtemp()
31
- source_path = os.path.join(temp_dir, f"source_{uuid.uuid4().hex}.jpg")
32
- target_path = os.path.join(temp_dir, f"target_{uuid.uuid4().hex}.jpg")
33
- output_path = os.path.join(temp_dir, f"output_{uuid.uuid4().hex}.jpg")
34
 
 
 
 
35
  try:
36
- with open(source_path, "wb") as f:
37
- f.write(await source.read())
38
- with open(target_path, "wb") as f:
39
- f.write(await target.read())
 
 
 
 
 
 
40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  cmd = [
42
- "python", FACEFUSION_SCRIPT,
43
  "headless-run",
44
- "--source", source_path,
45
- "--target", target_path,
46
- "--output-path", output_path,
47
- "--processors", "face_swapper",
48
- "--face-swapper-model", MODEL_NAME,
49
- "--execution-providers", *EXECUTION_PROVIDERS,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  ]
51
-
52
- logger.info(f"Running command: {' '.join(cmd)}")
53
- start = time.time()
54
-
 
 
 
 
55
  process = subprocess.Popen(
56
  cmd,
57
  stdout=subprocess.PIPE,
58
  stderr=subprocess.PIPE,
59
- text=True
 
 
60
  )
61
-
62
- stdout, stderr = process.communicate(timeout=180)
63
- elapsed = time.time() - start
64
- logger.info(f"Processing took {elapsed:.2f} seconds")
65
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  if process.returncode != 0:
67
- logger.error(f"FaceFusion error: {stderr}")
68
- raise HTTPException(status_code=500, detail=f"FaceFusion error: {stderr}")
69
-
70
- if not os.path.exists(output_path):
71
- raise HTTPException(status_code=500, detail="Output file not created")
72
-
73
- with open(output_path, "rb") as f:
74
- image_data = f.read()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
 
76
- return Response(content=image_data, media_type="image/jpeg")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
 
78
- except subprocess.TimeoutExpired:
79
- logger.error("Process timed out")
80
- raise HTTPException(status_code=504, detail="Processing timeout")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  except Exception as e:
82
- logger.exception("Unhandled exception")
83
- raise HTTPException(status_code=500, detail=str(e))
84
- finally:
85
- shutil.rmtree(temp_dir, ignore_errors=True)
 
 
 
 
 
 
 
 
 
 
 
1
  #!/usr/bin/env python3
2
+ """
3
+ 🎨 FaceFusion API для Hugging Face Spaces
4
+ CPU оптимизация + высокое качество замены лица
5
+ inswapper_128 + gfpgan + proper masking
6
+ Таймаут 10 минут для CPU
7
+ """
 
 
 
8
 
9
+ import os, sys, tempfile, subprocess, logging, shutil, uuid, time, json
10
+ from pathlib import Path
11
+ from fastapi import FastAPI, File, UploadFile, HTTPException, BackgroundTasks
12
+ from fastapi.responses import Response, JSONResponse
13
+
14
+ # 🔥 Настройка логирования
15
+ logging.basicConfig(
16
+ level=logging.INFO,
17
+ format='%(asctime)s [%(levelname)s] %(message)s',
18
+ handlers=[logging.StreamHandler(sys.stdout)]
19
+ )
20
  logger = logging.getLogger(__name__)
21
 
22
+ app = FastAPI(title="FaceFusion CPU API", version="1.0")
23
+
24
+ # 🔥 Пути
25
+ FACEFUSION_DIR = Path("/facefusion")
26
+ FACEFUSION_PY = FACEFUSION_DIR / "facefusion.py"
27
+ CACHE_DIR = Path("/tmp/facefusion_cache")
28
+ CACHE_DIR.mkdir(parents=True, exist_ok=True)
29
+
30
+ # 🔥 Проверка установки
31
+ if not FACEFUSION_PY.exists():
32
+ logger.error(f"❌ FaceFusion not found at {FACEFUSION_PY}")
33
+ raise RuntimeError("FaceFusion not installed — check Dockerfile")
34
 
35
+ # 🔥 ОПТИМАЛЬНЫЕ НАСТРОЙКИ ДЛЯ CPU + КАЧЕСТВО
36
+ CONFIG = {
37
+ # Модель замены лица (единственная стабильная для CPU)
38
+ "face_swapper": "inswapper_128",
39
+
40
+ # Улучшение лица (gfpgan легче codeformer, но даёт хорошее качество)
41
+ "face_enhancer": "gfpgan",
42
+ "face_enhancer_blend": 75, # 75% blending для естественного вида
43
+
44
+ # Маски для лучшего blending
45
+ "face_mask_types": ["box", "occlusion"],
46
+ "face_mask_blur": 0.6, # Мягкие края маски
47
+ "face_mask_padding": [10, 10, 10, 10], # Отступы маски [top, right, bottom, left]
48
+
49
+ # Разрешение (512-768 оптимально для CPU, 1024 слишком медленно)
50
+ "output_image_resolution": 768,
51
+
52
+ # CPU оптимизация
53
+ "execution_providers": ["cpu"],
54
+ "cpu_threads": 4, # 4 потока — баланс скорости/RAM
55
+ "cpu_thread_count": 4,
56
+
57
+ # Прочее
58
+ "skip_download": True, # Не качать модели каждый раз
59
+ "log_level": "error", # Меньше логов = быстрее
60
+ "timeout_seconds": 600, # 10 минут для CPU
61
+ }
62
 
63
+ # 🔥 Кэширование моделей
64
+ os.environ["FACEFUSION_CACHE"] = str(CACHE_DIR)
65
+ os.environ["OMP_NUM_THREADS"] = str(CONFIG["cpu_threads"])
66
+
67
+ @app.get("/")
68
+ async def root():
69
+ return {
70
+ "service": "FaceFusion CPU API",
71
+ "status": "running",
72
+ "config": {k: v for k, v in CONFIG.items() if k != "face_mask_padding"}
73
+ }
74
 
75
  @app.get("/health")
76
  async def health():
77
+ """Health check для Render/HF"""
78
+ return {"status": "ok", "cache_size_mb": sum(f.stat().st_size for f in CACHE_DIR.rglob('*')) / 1024 / 1024}
 
 
 
 
 
 
79
 
80
+ @app.get("/debug/cli")
81
+ async def debug_cli():
82
+ """Показывает доступные CLI аргументы FaceFusion"""
83
  try:
84
+ result = subprocess.run(
85
+ ["python", str(FACEFUSION_PY), "headless-run", "--help"],
86
+ capture_output=True, text=True, cwd=FACEFUSION_DIR, timeout=30
87
+ )
88
+ # Извлекаем только аргументы связанные с face_swapper
89
+ lines = result.stdout.split('\n')
90
+ relevant = [l.strip() for l in lines if 'face-swapper' in l.lower() or 'processor' in l.lower() or 'execution' in l.lower()]
91
+ return {"available_args": relevant[:20], "full_help_available": True}
92
+ except Exception as e:
93
+ return {"error": str(e)}
94
 
95
+ @app.post("/swap")
96
+ async def swap_face(
97
+ source: UploadFile = File(..., description="Фото лица (источник)"),
98
+ target: UploadFile = File(..., description="Целевое изображение"),
99
+ enhance: bool = True # Параметр: включать ли улучшение лица
100
+ ):
101
+ """
102
+ Замена лица с высоким качеством на CPU
103
+
104
+ Args:
105
+ source: Фото лица для замены (анфас, хорошее освещение)
106
+ target: Изображение куда вставить лицо
107
+ enhance: Включить GFPGAN улучшение (True = лучше качество, но медленнее)
108
+ """
109
+ temp_dir = None
110
+ start_time = time.time()
111
+
112
+ try:
113
+ # 🔥 Создаём временную директорию
114
+ temp_dir = tempfile.mkdtemp(prefix="ff_")
115
+ source_path = Path(temp_dir) / "source.jpg"
116
+ target_path = Path(temp_dir) / "target.jpg"
117
+ output_path = Path(temp_dir) / "output.png"
118
+
119
+ logger.info(f"📥 New request: source={source.filename}, target={target.filename}, enhance={enhance}")
120
+
121
+ # Сохраняем файлы
122
+ source_path.write_bytes(await source.read())
123
+ target_path.write_bytes(await target.read())
124
+ logger.info(f"📁 Files saved: {source_path.stat().st_size}b + {target_path.stat().st_size}b")
125
+
126
+ # 🔥 Формируем CLI команду с ОПТИМАЛЬНЫМИ аргументами для CPU+качество
127
  cmd = [
128
+ "python", str(FACEFUSION_PY),
129
  "headless-run",
130
+
131
+ # Файлы
132
+ "--sources", str(source_path),
133
+ "--targets", str(target_path),
134
+ "--output-path", str(output_path),
135
+
136
+ # Процессоры
137
+ "--processors", "face_swapper" + (f" face_enhancer" if enhance else ""),
138
+
139
+ # Модель замены лица
140
+ "--face-swapper-model", CONFIG["face_swapper"],
141
+
142
+ # Модель улучшения (если включено)
143
+ *(["--face-enhancer-model", CONFIG["face_enhancer"],
144
+ "--face-enhancer-blend", str(CONFIG["face_enhancer_blend"])] if enhance else []),
145
+
146
+ # Маски для качественного blending
147
+ "--face-mask-types", *CONFIG["face_mask_types"],
148
+ "--face-mask-blur", str(CONFIG["face_mask_blur"]),
149
+ "--face-mask-padding", *map(str, CONFIG["face_mask_padding"]),
150
+
151
+ # Разрешение
152
+ "--output-image-resolution", str(CONFIG["output_image_resolution"]),
153
+
154
+ # CPU оптимизация
155
+ "--execution-providers", *CONFIG["execution_providers"],
156
+ "--execution-thread-count", str(CONFIG["cpu_thread_count"]),
157
+
158
+ # Прочее
159
+ "--skip-download" if CONFIG["skip_download"] else None,
160
+ "--log-level", CONFIG["log_level"],
161
  ]
162
+
163
+ # Убираем None значения
164
+ cmd = [arg for arg in cmd if arg is not None]
165
+
166
+ logger.info(f"🚀 Running FaceFusion (enhance={enhance})...")
167
+ logger.info(f" Command: {' '.join(cmd)}")
168
+
169
+ # 🔥 Запускаем процесс
170
  process = subprocess.Popen(
171
  cmd,
172
  stdout=subprocess.PIPE,
173
  stderr=subprocess.PIPE,
174
+ text=True,
175
+ cwd=str(FACEFUSION_DIR),
176
+ env={**os.environ, "OMP_NUM_THREADS": str(CONFIG["cpu_threads"])}
177
  )
178
+
179
+ # Ждём с таймаутом
180
+ try:
181
+ stdout, stderr = process.communicate(timeout=CONFIG["timeout_seconds"])
182
+ except subprocess.TimeoutExpired:
183
+ process.kill()
184
+ process.communicate()
185
+ logger.error(f"⏱️ Timeout after {CONFIG['timeout_seconds']}s")
186
+ raise HTTPException(504, f"⏱️ Timeout: обработка заняла больше {CONFIG['timeout_seconds']} секунд")
187
+
188
+ elapsed = time.time() - start_time
189
+ logger.info(f"⏱️ Processing time: {elapsed:.1f}s")
190
+
191
+ # Логируем вывод
192
+ if stdout and CONFIG["log_level"] == "debug":
193
+ logger.debug(f"📋 STDOUT: {stdout[-500:]}")
194
+ if stderr:
195
+ logger.info(f"📋 STDERR: {stderr[-300:]}")
196
+
197
+ # 🔥 Проверяем результат
198
  if process.returncode != 0:
199
+ error_msg = stderr.strip()[-400:] if stderr else "Unknown error"
200
+ logger.error(f"FaceFusion failed (code {process.returncode}): {error_msg}")
201
+ raise HTTPException(500, f"FaceFusion error: {error_msg}")
202
+
203
+ # Ищем выходной файл (может быть с другим расширением)
204
+ result_file = None
205
+ for ext in [".png", ".jpg", ".jpeg", ".webp"]:
206
+ candidate = output_path.with_suffix(ext)
207
+ if candidate.exists():
208
+ result_file = candidate
209
+ break
210
+
211
+ if not result_file or not result_file.exists():
212
+ # Попробуем найти любой image в output директории
213
+ for f in Path(temp_dir).glob("output*"):
214
+ if f.suffix.lower() in [".png", ".jpg", ".jpeg", ".webp"]:
215
+ result_file = f
216
+ break
217
+
218
+ if not result_file:
219
+ raise HTTPException(500, "Output image not created")
220
+
221
+ # 🔥 Читаем и возвращаем результат
222
+ image_data = result_file.read_bytes()
223
+ result_size = len(image_data)
224
+
225
+ logger.info(f"✅ Success! Returned {result_size / 1024:.1f}KB in {elapsed:.1f}s")
226
+
227
+ return Response(
228
+ content=image_data,
229
+ media_type="image/png",
230
+ headers={
231
+ "X-Processing-Time": f"{elapsed:.1f}",
232
+ "X-Result-Size": str(result_size),
233
+ "Cache-Control": "no-store"
234
+ }
235
+ )
236
+
237
+ except HTTPException:
238
+ raise
239
+ except Exception as e:
240
+ logger.exception(f"💥 Unexpected error: {type(e).__name__}: {e}")
241
+ raise HTTPException(500, f"Internal error: {str(e)[:200]}")
242
+
243
+ finally:
244
+ # 🔥 Очистка временных файлов
245
+ if temp_dir and Path(temp_dir).exists():
246
+ try:
247
+ shutil.rmtree(temp_dir, ignore_errors=True)
248
+ logger.debug(f"🧹 Cleaned temp dir: {temp_dir}")
249
+ except Exception as e:
250
+ logger.warning(f"⚠️ Could not clean temp dir: {e}")
251
 
252
+ @app.post("/swap/batch")
253
+ async def swap_batch(
254
+ source: UploadFile = File(...),
255
+ targets: list[UploadFile] = File(...),
256
+ enhance: bool = True
257
+ ):
258
+ """
259
+ Обработка нескольких целевых изображений за один запрос
260
+ (полезно для генерации нескольких вариантов)
261
+ """
262
+ if len(targets) > 5:
263
+ raise HTTPException(400, "Maximum 5 target images per batch")
264
+
265
+ results = []
266
+ for i, target in enumerate(targets):
267
+ try:
268
+ # Создаём fake Request для повторного использования swap_face
269
+ from starlette.datastructures import UploadFile as SFUploadFile
270
+ result = await swap_face(
271
+ source=source,
272
+ target=target,
273
+ enhance=enhance
274
+ )
275
+ results.append({"index": i, "status": "ok", "size": len(result.body)})
276
+ except Exception as e:
277
+ results.append({"index": i, "status": "error", "error": str(e)[:100]})
278
+
279
+ return JSONResponse(content={"batch_results": results})
280
 
281
+ # 🔥 Pre-warm: загрузка моделей при старте (опционально)
282
+ @app.on_event("startup")
283
+ async def startup():
284
+ logger.info("🚀 FaceFusion CPU API starting...")
285
+ logger.info(f"📁 Cache dir: {CACHE_DIR}")
286
+ logger.info(f"⚙️ Config: swapper={CONFIG['face_swapper']}, enhancer={CONFIG['face_enhancer']}, resolution={CONFIG['output_image_resolution']}")
287
+
288
+ # 🔥 Предзагрузка моделей (чтобы первый запрос не таймаутил)
289
+ try:
290
+ logger.info("📦 Pre-downloading models...")
291
+ preload_cmd = [
292
+ "python", str(FACEFUSION_PY),
293
+ "force-download",
294
+ "--execution-providers", "cpu"
295
+ ]
296
+ result = subprocess.run(
297
+ preload_cmd,
298
+ capture_output=True, text=True,
299
+ cwd=FACEFUSION_DIR, timeout=120
300
+ )
301
+ if result.returncode == 0:
302
+ logger.info("✅ Models pre-downloaded")
303
+ else:
304
+ logger.warning(f"⚠️ Pre-download warning: {result.stderr[-200:]}")
305
  except Exception as e:
306
+ logger.warning(f"⚠️ Could not pre-download models: {e}")
307
+
308
+ logger.info("✅ API ready!")
309
+
310
+ if __name__ == "__main__":
311
+ import uvicorn
312
+ # 🔥 Запуск на порту 7860 (требование HF Spaces)
313
+ uvicorn.run(
314
+ app,
315
+ host="0.0.0.0",
316
+ port=7860,
317
+ log_level="info",
318
+ timeout_keep_alive=300 # 5 минут keep-alive для долгих запросов
319
+ )