Dmitry1313 commited on
Commit
5e0fd6e
·
verified ·
1 Parent(s): 97a333e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +77 -164
app.py CHANGED
@@ -1,14 +1,14 @@
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
  # 🔥 Настройка логирования
@@ -19,7 +19,31 @@ logging.basicConfig(
19
  )
20
  logger = logging.getLogger(__name__)
21
 
22
- app = FastAPI(title="FaceFusion CPU API", version="1.0")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
  # 🔥 Пути
25
  FACEFUSION_DIR = Path("/facefusion")
@@ -30,100 +54,81 @@ CACHE_DIR.mkdir(parents=True, exist_ok=True)
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",
@@ -134,16 +139,16 @@ async def swap_face(
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"]),
@@ -151,32 +156,27 @@ async def swap_face(
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:
@@ -188,19 +188,15 @@ async def swap_face(
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)
@@ -208,8 +204,7 @@ async def swap_face(
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
@@ -218,102 +213,20 @@ async def swap_face(
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
- )
 
1
  #!/usr/bin/env python3
2
  """
3
+ 🎨 FaceFusion API для Hugging Face Spaces — ИСПРАВЛЕННЫЕ АРГУМЕНТЫ
4
  ✅ CPU оптимизация + высокое качество замены лица
5
+ Правильные названия моделей для FaceFusion 3.x
 
6
  """
7
 
8
  import os, sys, tempfile, subprocess, logging, shutil, uuid, time, json
9
  from pathlib import Path
10
+ from contextlib import asynccontextmanager
11
+ from fastapi import FastAPI, File, UploadFile, HTTPException
12
  from fastapi.responses import Response, JSONResponse
13
 
14
  # 🔥 Настройка логирования
 
19
  )
20
  logger = logging.getLogger(__name__)
21
 
22
+ # 🔥 Lifespan вместо @app.on_event (исправление DeprecationWarning)
23
+ @asynccontextmanager
24
+ async def lifespan(app: FastAPI):
25
+ """Startup/shutdown events"""
26
+ logger.info("🚀 FaceFusion CPU API starting...")
27
+ logger.info(f"📁 Cache dir: {CACHE_DIR}")
28
+ logger.info(f"⚙️ Config: swapper={CONFIG['face_swapper']}, enhancer={CONFIG['face_enhancer']}")
29
+
30
+ # Предзагрузка моделей
31
+ try:
32
+ logger.info("📦 Pre-downloading models...")
33
+ preload_cmd = ["python", str(FACEFUSION_PY), "force-download", "--execution-provider", "cpu"]
34
+ result = subprocess.run(preload_cmd, capture_output=True, text=True, cwd=FACEFUSION_DIR, timeout=120)
35
+ if result.returncode == 0:
36
+ logger.info("✅ Models pre-downloaded")
37
+ else:
38
+ logger.warning(f"⚠️ Pre-download: {result.stderr[-200:]}")
39
+ except Exception as e:
40
+ logger.warning(f"⚠️ Could not pre-download: {e}")
41
+
42
+ logger.info("✅ API ready!")
43
+ yield
44
+ logger.info("👋 API shutting down...")
45
+
46
+ app = FastAPI(title="FaceFusion CPU API", version="1.0", lifespan=lifespan)
47
 
48
  # 🔥 Пути
49
  FACEFUSION_DIR = Path("/facefusion")
 
54
  # 🔥 Проверка установки
55
  if not FACEFUSION_PY.exists():
56
  logger.error(f"❌ FaceFusion not found at {FACEFUSION_PY}")
57
+ raise RuntimeError("FaceFusion not installed")
58
 
59
+ # 🔥 ОПТИМАЛЬНЫЕ НАСТРОЙКИ ДЛЯ CPU + КАЧЕСТВО (ИСПРАВЛЕННЫЕ НАЗВАНИЯ)
60
  CONFIG = {
61
+ # Модель замены лица
62
+ "face_swapper": "inswapper_128", # ✅ Доступна в твоей версии
63
 
64
+ # Модель улучшения лица ИСПРАВЛЕНО: gfpgan_1.4 вместо gfpgan
65
+ "face_enhancer": "gfpgan_1.4", # ✅ Правильное название!
66
+ "face_enhancer_blend": 75,
67
 
68
+ # Маски
69
  "face_mask_types": ["box", "occlusion"],
70
+ "face_mask_blur": 0.6,
71
+ "face_mask_padding": [10, 10, 10, 10],
72
 
73
+ # Разрешение
74
  "output_image_resolution": 768,
75
 
76
+ # CPU — ИСПРАВЛЕНО: execution-provider (единственное число!)
77
+ "execution_provider": "cpu", # ✅ Не "execution-providers"!
78
+ "execution_thread_count": 4,
 
79
 
80
  # Прочее
81
+ "skip_download": True,
82
+ "log_level": "error",
83
+ "timeout_seconds": 600,
84
  }
85
 
 
86
  os.environ["FACEFUSION_CACHE"] = str(CACHE_DIR)
87
+ os.environ["OMP_NUM_THREADS"] = str(CONFIG["execution_thread_count"])
88
 
89
  @app.get("/")
90
  async def root():
91
+ return {"service": "FaceFusion CPU API", "status": "running"}
 
 
 
 
92
 
93
  @app.get("/health")
94
  async def health():
95
+ return {"status": "ok"}
 
96
 
97
+ @app.get("/debug/args")
98
+ async def debug_args():
99
+ """Показывает доступные аргументы FaceFusion"""
100
  try:
101
  result = subprocess.run(
102
  ["python", str(FACEFUSION_PY), "headless-run", "--help"],
103
  capture_output=True, text=True, cwd=FACEFUSION_DIR, timeout=30
104
  )
 
105
  lines = result.stdout.split('\n')
106
+ relevant = [l.strip() for l in lines if any(kw in l.lower() for kw in ['face-swapper', 'face-enhancer', 'execution', 'processor'])]
107
+ return {"available_args": relevant[:30]}
108
  except Exception as e:
109
  return {"error": str(e)}
110
 
111
  @app.post("/swap")
112
  async def swap_face(
113
+ source: UploadFile = File(...),
114
+ target: UploadFile = File(...),
115
+ enhance: bool = True
116
  ):
 
 
 
 
 
 
 
 
117
  temp_dir = None
118
  start_time = time.time()
119
 
120
  try:
 
121
  temp_dir = tempfile.mkdtemp(prefix="ff_")
122
  source_path = Path(temp_dir) / "source.jpg"
123
  target_path = Path(temp_dir) / "target.jpg"
124
  output_path = Path(temp_dir) / "output.png"
125
 
126
+ logger.info(f"📥 Request: source={source.filename}, enhance={enhance}")
127
 
 
128
  source_path.write_bytes(await source.read())
129
  target_path.write_bytes(await target.read())
 
130
 
131
+ # 🔥 Формируем CLI команду с ИСПРАВЛЕННЫМИ аргументами
132
  cmd = [
133
  "python", str(FACEFUSION_PY),
134
  "headless-run",
 
139
  "--output-path", str(output_path),
140
 
141
  # Процессоры
142
+ "--processors", "face_swapper" + (" face_enhancer" if enhance else ""),
143
 
144
  # Модель замены лица
145
  "--face-swapper-model", CONFIG["face_swapper"],
146
 
147
+ # Модель улучшения ИСПРАВЛЕНО: gfpgan_1.4
148
  *(["--face-enhancer-model", CONFIG["face_enhancer"],
149
  "--face-enhancer-blend", str(CONFIG["face_enhancer_blend"])] if enhance else []),
150
 
151
+ # Маски
152
  "--face-mask-types", *CONFIG["face_mask_types"],
153
  "--face-mask-blur", str(CONFIG["face_mask_blur"]),
154
  "--face-mask-padding", *map(str, CONFIG["face_mask_padding"]),
 
156
  # Разрешение
157
  "--output-image-resolution", str(CONFIG["output_image_resolution"]),
158
 
159
+ # CPU — ИСПРАВЛЕНО: execution-provider (единственное!)
160
+ "--execution-provider", CONFIG["execution_provider"],
161
+ "--execution-thread-count", str(CONFIG["execution_thread_count"]),
162
 
163
  # Прочее
164
  "--skip-download" if CONFIG["skip_download"] else None,
165
  "--log-level", CONFIG["log_level"],
166
  ]
167
 
168
+ # Убираем None
169
  cmd = [arg for arg in cmd if arg is not None]
170
 
171
+ logger.info(f"🚀 Running FaceFusion...")
172
  logger.info(f" Command: {' '.join(cmd)}")
173
 
 
174
  process = subprocess.Popen(
175
+ cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
176
+ text=True, cwd=str(FACEFUSION_DIR),
177
+ env={**os.environ, "OMP_NUM_THREADS": str(CONFIG["execution_thread_count"])}
 
 
 
178
  )
179
 
 
180
  try:
181
  stdout, stderr = process.communicate(timeout=CONFIG["timeout_seconds"])
182
  except subprocess.TimeoutExpired:
 
188
  elapsed = time.time() - start_time
189
  logger.info(f"⏱️ Processing time: {elapsed:.1f}s")
190
 
 
 
 
191
  if stderr:
192
  logger.info(f"📋 STDERR: {stderr[-300:]}")
193
 
 
194
  if process.returncode != 0:
195
  error_msg = stderr.strip()[-400:] if stderr else "Unknown error"
196
  logger.error(f"❌ FaceFusion failed (code {process.returncode}): {error_msg}")
197
  raise HTTPException(500, f"FaceFusion error: {error_msg}")
198
 
199
+ # Ищем выходной файл
200
  result_file = None
201
  for ext in [".png", ".jpg", ".jpeg", ".webp"]:
202
  candidate = output_path.with_suffix(ext)
 
204
  result_file = candidate
205
  break
206
 
207
+ if not result_file:
 
208
  for f in Path(temp_dir).glob("output*"):
209
  if f.suffix.lower() in [".png", ".jpg", ".jpeg", ".webp"]:
210
  result_file = f
 
213
  if not result_file:
214
  raise HTTPException(500, "Output image not created")
215
 
 
216
  image_data = result_file.read_bytes()
217
+ logger.info(f"✅ Success! Returned {len(image_data) / 1024:.1f}KB in {elapsed:.1f}s")
218
 
219
+ return Response(content=image_data, media_type="image/png", headers={"Cache-Control": "no-store"})
 
 
 
 
 
 
 
 
 
 
220
 
221
  except HTTPException:
222
  raise
223
  except Exception as e:
224
+ logger.exception(f"💥 Unexpected error: {e}")
225
  raise HTTPException(500, f"Internal error: {str(e)[:200]}")
 
226
  finally:
 
227
  if temp_dir and Path(temp_dir).exists():
228
+ shutil.rmtree(temp_dir, ignore_errors=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
 
230
  if __name__ == "__main__":
231
  import uvicorn
232
+ uvicorn.run(app, host="0.0.0.0", port=7860, log_level="info", timeout_keep_alive=300)