Dmitry1313 commited on
Commit
1b5ef7b
·
verified ·
1 Parent(s): 388bdd0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +186 -48
app.py CHANGED
@@ -1,10 +1,11 @@
1
  #!/usr/bin/env python3
2
  """
3
- 🎨 FaceFusion API для Hugging Face Spaces — FINAL FIXED VERSION
4
- Disable NSFW via ENV variable (not CLI arg)
5
  ✅ Read UploadFile once and reuse bytes
6
- Allow model re-download if corrupted
7
  ✅ ghost_3_256 maximum quality
 
8
  """
9
 
10
  import os, sys, tempfile, subprocess, logging, shutil, time
@@ -13,31 +14,46 @@ from contextlib import asynccontextmanager
13
  from fastapi import FastAPI, File, UploadFile, HTTPException, Query
14
  from fastapi.responses import Response
15
 
16
- logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s', handlers=[logging.StreamHandler(sys.stdout)])
 
 
 
 
 
17
  logger = logging.getLogger(__name__)
18
 
19
  @asynccontextmanager
20
  async def lifespan(app: FastAPI):
 
21
  logger.info("🚀 FaceFusion CPU API (ghost_3_256) starting...")
22
  logger.info("📁 Cache: /tmp/facefusion_cache")
23
  logger.info("⚙️ Default: ghost_3_256 + gfpgan_1.4")
24
  logger.info("🔕 NSFW filter: disabled via ENV")
25
  logger.info("🔄 Model re-download: enabled")
 
26
  logger.info("⏱️ Timeout: 900s")
27
  logger.info("✅ API ready!")
28
  yield
29
  logger.info("👋 API shutting down...")
30
 
31
- app = FastAPI(title="FaceFusion CPU API — High Quality", version="2.2", lifespan=lifespan)
 
 
 
 
32
 
 
33
  FACEFUSION_DIR = Path("/facefusion")
34
  FACEFUSION_PY = FACEFUSION_DIR / "facefusion.py"
35
  CACHE_DIR = Path("/tmp/facefusion_cache")
36
  CACHE_DIR.mkdir(parents=True, exist_ok=True)
37
 
 
38
  if not FACEFUSION_PY.exists():
39
- raise RuntimeError(f"FaceFusion not found at {FACEFUSION_PY}")
 
40
 
 
41
  CONFIG = {
42
  "face_swapper": "ghost_3_256",
43
  "face_swapper_fallback": "inswapper_128",
@@ -46,42 +62,83 @@ CONFIG = {
46
  "face_mask_types": ["box", "occlusion"],
47
  "face_mask_blur": 0.6,
48
  "face_mask_padding": [10, 10, 10, 10],
49
- "output_image_resolution": "768",
50
  "output_image_quality": "90",
51
  "timeout_high_quality": 900,
52
  "timeout_standard": 600,
53
- "skip_download": False, # 🔥 Разрешить перескачку если модель повреждена
54
  "log_level": "error",
55
  }
56
 
57
  # 🔥 Отключаем NSFW фильтр через ENV переменную
58
  os.environ["FACEFUSION_CONTENT_ANALYSER"] = "none"
59
 
 
60
  AVAILABLE_SWAPPERS = ["ghost_3_256", "ghost_2_256", "ghost_1_256", "inswapper_128", "simswap_256"]
61
 
 
 
 
 
62
  @app.get("/")
63
  async def root():
 
64
  return {
65
  "service": "FaceFusion CPU API — High Quality",
66
- "version": "2.2",
67
  "default_swapper": CONFIG["face_swapper"],
 
 
68
  "nsfw_disabled": os.environ.get("FACEFUSION_CONTENT_ANALYSER") == "none",
 
69
  "status": "running"
70
  }
71
 
72
  @app.get("/health")
73
  async def health():
 
74
  return {"status": "ok"}
75
 
76
  @app.post("/swap")
77
  async def swap_face(
78
- source: UploadFile = File(...),
79
- target: UploadFile = File(...),
80
- swapper_model: str = Query(default="ghost_3_256", enum=AVAILABLE_SWAPPERS),
81
- enhancer_model: str = Query(default="gfpgan_1.4"),
82
- enhance_blend: int = Query(default=75, ge=0, le=100),
83
- use_fallback: bool = Query(default=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  ):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  # 🔥 ЧИТАЕМ ФАЙЛЫ ОДИН РАЗ И СОХРАНЯЕМ В ПАМЯТЬ
86
  try:
87
  source_bytes = await source.read()
@@ -94,9 +151,11 @@ async def swap_face(
94
 
95
  logger.info(f"📥 Request: source={len(source_bytes)}b, target={len(target_bytes)}b")
96
 
 
97
  if enhancer_model and enhancer_model.lower() == "none":
98
  enhancer_model = None
99
 
 
100
  models_to_try = [swapper_model]
101
  if use_fallback and swapper_model != CONFIG["face_swapper_fallback"]:
102
  models_to_try.append(CONFIG["face_swapper_fallback"])
@@ -110,8 +169,9 @@ async def swap_face(
110
  result = await _process_face_swap(
111
  source_bytes=source_bytes,
112
  target_bytes=target_bytes,
 
113
  swapper_model=model,
114
- enhancer_model=enhancer_model if attempt == 1 else None,
115
  enhance_blend=enhance_blend,
116
  timeout=CONFIG["timeout_high_quality"] if model.startswith("ghost") else CONFIG["timeout_standard"]
117
  )
@@ -126,59 +186,81 @@ async def swap_face(
126
 
127
  raise HTTPException(500, "Unexpected error")
128
 
 
 
 
 
129
  async def _process_face_swap(
130
  source_bytes: bytes,
131
  target_bytes: bytes,
 
132
  swapper_model: str,
133
  enhancer_model: str,
134
  enhance_blend: int,
135
  timeout: int
136
  ):
 
137
  temp_dir = None
138
 
139
  try:
140
  temp_dir = tempfile.mkdtemp(prefix="ff_")
 
 
141
  source_path = Path(temp_dir) / "source.jpg"
142
  target_path = Path(temp_dir) / "target.jpg"
143
- output_path = Path(temp_dir) / "output.png"
144
 
145
- logger.info(f"🔧 Processing: swapper={swapper_model}, enhancer={enhancer_model}")
 
 
 
146
 
 
147
  source_path.write_bytes(source_bytes)
148
  target_path.write_bytes(target_bytes)
149
 
 
150
  processors = ["face_swapper"]
151
  if enhancer_model:
152
  processors.append("face_enhancer")
153
 
154
- # 🔥 CLI команда БЕЗ --skip-nsfw-filter (не поддерживается)
155
  cmd = [
156
  "python", str(FACEFUSION_PY),
157
  "headless-run",
 
 
158
  "-s", str(source_path),
159
  "-t", str(target_path),
160
- "-o", str(output_path),
 
 
161
  "--processors", *processors,
162
  "--face-swapper-model", swapper_model,
 
 
163
  "--face-mask-types", *CONFIG["face_mask_types"],
164
  "--face-mask-blur", str(CONFIG["face_mask_blur"]),
165
  "--face-mask-padding", *map(str, CONFIG["face_mask_padding"]),
166
- "--output-image-resolution", CONFIG["output_image_resolution"],
 
167
  "--output-image-quality", CONFIG["output_image_quality"],
168
- # ❌ УБРАЛ: --skip-nsfw-filter (не поддерживается в этой версии)
 
169
  "--log-level", CONFIG["log_level"],
170
  ]
171
 
172
- # 🔥 skip-download=False по умолчанию — разрешаем перескачку моделей
173
  if CONFIG["skip_download"]:
174
  cmd.append("--skip-download")
175
 
 
176
  if enhancer_model:
177
  cmd.extend([
178
  "--face-enhancer-model", enhancer_model,
179
  "--face-enhancer-blend", str(enhance_blend),
180
  ])
181
 
 
182
  cmd = [arg for arg in cmd if arg is not None]
183
 
184
  logger.info(f"🚀 Running: {' '.join(cmd)}")
@@ -188,8 +270,12 @@ async def _process_face_swap(
188
  env = {**os.environ, "OMP_NUM_THREADS": "4", "FACEFUSION_CONTENT_ANALYSER": "none"}
189
 
190
  process = subprocess.Popen(
191
- cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
192
- text=True, cwd=str(FACEFUSION_DIR), env=env
 
 
 
 
193
  )
194
 
195
  try:
@@ -197,48 +283,51 @@ async def _process_face_swap(
197
  except subprocess.TimeoutExpired:
198
  process.kill()
199
  process.communicate()
200
- raise HTTPException(504, f"⏱️ Timeout: >{timeout}s")
 
201
 
202
  elapsed = time.time() - process_start
203
  logger.info(f"⏱️ Done in {elapsed:.1f}s, code={process.returncode}")
204
 
 
205
  if stderr:
206
- # Фильтруем harmless warnings
207
- stderr_clean = '\n'.join(l for l in stderr.split('\n') if 'iCCP' not in l and 'libpng' not in l)
208
- if stderr_clean.strip():
 
 
209
  logger.info(f"📋 STDERR: {stderr_clean[-400:]}")
210
 
 
211
  if process.returncode != 0:
212
  error_msg = stderr.strip()[-400:] if stderr else "Unknown error"
213
  logger.error(f"❌ FaceFusion failed: {error_msg}")
214
  raise RuntimeError(f"FaceFusion error: {error_msg}")
215
 
216
- # Ищем результат
217
- result_file = None
218
- for ext in [".png", ".jpg", ".jpeg", ".webp"]:
219
- candidate = output_path.with_suffix(ext)
220
- if candidate.exists():
221
- result_file = candidate
222
- break
223
-
224
  if not result_file:
225
- for f in Path(temp_dir).glob("output*"):
226
- if f.suffix.lower() in [".png", ".jpg", ".jpeg", ".webp"]:
227
- result_file = f
228
- break
229
 
230
  if not result_file or not result_file.exists():
231
  raise RuntimeError("Output image not created")
232
 
 
233
  image_data = result_file.read_bytes()
 
234
  logger.info(f"✅ Success: {len(image_data)/1024:.1f}KB in {elapsed:.1f}s")
235
 
236
  return Response(
237
  content=image_data,
238
- media_type="image/png",
239
  headers={
240
  "X-Processing-Time": f"{elapsed:.1f}",
241
  "X-Swapper-Model": swapper_model,
 
 
242
  "Cache-Control": "no-store"
243
  }
244
  )
@@ -249,17 +338,66 @@ async def _process_face_swap(
249
  logger.exception(f"💥 Error: {e}")
250
  raise
251
  finally:
 
252
  if temp_dir and Path(temp_dir).exists():
253
- shutil.rmtree(temp_dir, ignore_errors=True)
 
 
 
 
 
 
 
 
254
 
255
  @app.post("/swap/quick")
256
- async def swap_quick(source: UploadFile = File(...), target: UploadFile = File(...)):
257
- return await swap_face(source=source, target=target, swapper_model="inswapper_128", enhancer_model="none", use_fallback=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
 
259
  @app.post("/swap/hq")
260
- async def swap_hq(source: UploadFile = File(...), target: UploadFile = File(...), with_enhancer: bool = True):
261
- return await swap_face(source=source, target=target, swapper_model="ghost_3_256", enhancer_model="gfpgan_1.4" if with_enhancer else "none", enhance_blend=80, use_fallback=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
 
263
  if __name__ == "__main__":
264
  import uvicorn
265
- uvicorn.run(app, host="0.0.0.0", port=7860, log_level="info", timeout_keep_alive=900)
 
 
 
 
 
 
 
1
  #!/usr/bin/env python3
2
  """
3
+ 🎨 FaceFusion API для Hugging Face Spaces — FINAL VERSION
4
+ Output всегда JPEG независимо от входного формата
5
  ✅ Read UploadFile once and reuse bytes
6
+ NSFW disabled via ENV
7
  ✅ ghost_3_256 maximum quality
8
+ ✅ Fallback на inswapper_128 если ghost не работает
9
  """
10
 
11
  import os, sys, tempfile, subprocess, logging, shutil, time
 
14
  from fastapi import FastAPI, File, UploadFile, HTTPException, Query
15
  from fastapi.responses import Response
16
 
17
+ # 🔥 Настройка логирования
18
+ logging.basicConfig(
19
+ level=logging.INFO,
20
+ format='%(asctime)s [%(levelname)s] %(message)s',
21
+ handlers=[logging.StreamHandler(sys.stdout)]
22
+ )
23
  logger = logging.getLogger(__name__)
24
 
25
  @asynccontextmanager
26
  async def lifespan(app: FastAPI):
27
+ """Startup/Shutdown события"""
28
  logger.info("🚀 FaceFusion CPU API (ghost_3_256) starting...")
29
  logger.info("📁 Cache: /tmp/facefusion_cache")
30
  logger.info("⚙️ Default: ghost_3_256 + gfpgan_1.4")
31
  logger.info("🔕 NSFW filter: disabled via ENV")
32
  logger.info("🔄 Model re-download: enabled")
33
+ logger.info("📐 Output format: always JPEG")
34
  logger.info("⏱️ Timeout: 900s")
35
  logger.info("✅ API ready!")
36
  yield
37
  logger.info("👋 API shutting down...")
38
 
39
+ app = FastAPI(
40
+ title="FaceFusion CPU API — High Quality",
41
+ version="2.4",
42
+ lifespan=lifespan
43
+ )
44
 
45
+ # 🔥 Пути
46
  FACEFUSION_DIR = Path("/facefusion")
47
  FACEFUSION_PY = FACEFUSION_DIR / "facefusion.py"
48
  CACHE_DIR = Path("/tmp/facefusion_cache")
49
  CACHE_DIR.mkdir(parents=True, exist_ok=True)
50
 
51
+ # 🔥 Проверка установки
52
  if not FACEFUSION_PY.exists():
53
+ logger.error(f"FaceFusion not found at {FACEFUSION_PY}")
54
+ raise RuntimeError("FaceFusion not installed — check Dockerfile")
55
 
56
+ # 🔥 КОНФИГУРАЦИЯ
57
  CONFIG = {
58
  "face_swapper": "ghost_3_256",
59
  "face_swapper_fallback": "inswapper_128",
 
62
  "face_mask_types": ["box", "occlusion"],
63
  "face_mask_blur": 0.6,
64
  "face_mask_padding": [10, 10, 10, 10],
 
65
  "output_image_quality": "90",
66
  "timeout_high_quality": 900,
67
  "timeout_standard": 600,
68
+ "skip_download": False,
69
  "log_level": "error",
70
  }
71
 
72
  # 🔥 Отключаем NSFW фильтр через ENV переменную
73
  os.environ["FACEFUSION_CONTENT_ANALYSER"] = "none"
74
 
75
+ # 🔥 Доступные модели
76
  AVAILABLE_SWAPPERS = ["ghost_3_256", "ghost_2_256", "ghost_1_256", "inswapper_128", "simswap_256"]
77
 
78
+ # ============================================
79
+ # 🔥 ЭНДПОИНТЫ
80
+ # ============================================
81
+
82
  @app.get("/")
83
  async def root():
84
+ """Информация о сервисе"""
85
  return {
86
  "service": "FaceFusion CPU API — High Quality",
87
+ "version": "2.4",
88
  "default_swapper": CONFIG["face_swapper"],
89
+ "default_enhancer": CONFIG["face_enhancer"],
90
+ "output_format": "jpeg (always)",
91
  "nsfw_disabled": os.environ.get("FACEFUSION_CONTENT_ANALYSER") == "none",
92
+ "available_swappers": AVAILABLE_SWAPPERS,
93
  "status": "running"
94
  }
95
 
96
  @app.get("/health")
97
  async def health():
98
+ """Health check для мониторинга"""
99
  return {"status": "ok"}
100
 
101
  @app.post("/swap")
102
  async def swap_face(
103
+ source: UploadFile = File(..., description="Фото лица для замены (анфас, хорошее освещение)"),
104
+ target: UploadFile = File(..., description="Целевое изображение"),
105
+ swapper_model: str = Query(
106
+ default="ghost_3_256",
107
+ description="Модель замены лица",
108
+ enum=AVAILABLE_SWAPPERS
109
+ ),
110
+ enhancer_model: str = Query(
111
+ default="gfpgan_1.4",
112
+ description="Модель улучшения лица (или 'none')"
113
+ ),
114
+ enhance_blend: int = Query(
115
+ default=75,
116
+ ge=0, le=100,
117
+ description="Сила blending улучшения (0-100)"
118
+ ),
119
+ use_fallback: bool = Query(
120
+ default=True,
121
+ description="Использовать fallback модель если основная не работает"
122
+ )
123
  ):
124
+ """
125
+ 🎨 Замена лица с высоким качеством
126
+
127
+ **Модели:**
128
+ - `ghost_3_256` — лучшее качество (256px, рекомендуется)
129
+ - `inswapper_128` — стандартное (128px, быстрее)
130
+
131
+ **Улучшение лица:**
132
+ - `gfpgan_1.4` — хороший баланс качества/скорости
133
+ - `none` — без улучшения
134
+
135
+ **Время обработки:**
136
+ - ghost_3_256: 60-120 сек
137
+ - inswapper_128: 30-60 сек
138
+ - +enhancer: +20-40 сек
139
+
140
+ **Выход:** Всегда JPEG независимо от входного формата
141
+ """
142
  # 🔥 ЧИТАЕМ ФАЙЛЫ ОДИН РАЗ И СОХРАНЯЕМ В ПАМЯТЬ
143
  try:
144
  source_bytes = await source.read()
 
151
 
152
  logger.info(f"📥 Request: source={len(source_bytes)}b, target={len(target_bytes)}b")
153
 
154
+ # Нормализуем enhancer_model
155
  if enhancer_model and enhancer_model.lower() == "none":
156
  enhancer_model = None
157
 
158
+ # Формируем список моделей для попытки
159
  models_to_try = [swapper_model]
160
  if use_fallback and swapper_model != CONFIG["face_swapper_fallback"]:
161
  models_to_try.append(CONFIG["face_swapper_fallback"])
 
169
  result = await _process_face_swap(
170
  source_bytes=source_bytes,
171
  target_bytes=target_bytes,
172
+ target_filename=target.filename,
173
  swapper_model=model,
174
+ enhancer_model=enhancer_model if attempt == 1 else None, # Без enhancer для fallback
175
  enhance_blend=enhance_blend,
176
  timeout=CONFIG["timeout_high_quality"] if model.startswith("ghost") else CONFIG["timeout_standard"]
177
  )
 
186
 
187
  raise HTTPException(500, "Unexpected error")
188
 
189
+ # ============================================
190
+ # 🔥 ВНУТРЕННЯЯ ФУНКЦИЯ ОБРАБОТКИ
191
+ # ============================================
192
+
193
  async def _process_face_swap(
194
  source_bytes: bytes,
195
  target_bytes: bytes,
196
+ target_filename: str,
197
  swapper_model: str,
198
  enhancer_model: str,
199
  enhance_blend: int,
200
  timeout: int
201
  ):
202
+ """Обработка замены лица"""
203
  temp_dir = None
204
 
205
  try:
206
  temp_dir = tempfile.mkdtemp(prefix="ff_")
207
+
208
+ # 🔥 Входные файлы (любой формат)
209
  source_path = Path(temp_dir) / "source.jpg"
210
  target_path = Path(temp_dir) / "target.jpg"
 
211
 
212
+ # 🔥 ВЫХОД ВСЕГДА .jpg
213
+ output_path = Path(temp_dir) / "output.jpg"
214
+
215
+ logger.info(f"🔧 Processing: swapper={swapper_model}, enhancer={enhancer_model}, output=jpg")
216
 
217
+ # Записываем байты
218
  source_path.write_bytes(source_bytes)
219
  target_path.write_bytes(target_bytes)
220
 
221
+ # Формируем список процессоров
222
  processors = ["face_swapper"]
223
  if enhancer_model:
224
  processors.append("face_enhancer")
225
 
226
+ # 🔥 CLI команда
227
  cmd = [
228
  "python", str(FACEFUSION_PY),
229
  "headless-run",
230
+
231
+ # Файлы
232
  "-s", str(source_path),
233
  "-t", str(target_path),
234
+ "-o", str(output_path), # 🔥 Всегда .jpg!
235
+
236
+ # Процессоры и модель
237
  "--processors", *processors,
238
  "--face-swapper-model", swapper_model,
239
+
240
+ # Маски для качественного blending
241
  "--face-mask-types", *CONFIG["face_mask_types"],
242
  "--face-mask-blur", str(CONFIG["face_mask_blur"]),
243
  "--face-mask-padding", *map(str, CONFIG["face_mask_padding"]),
244
+
245
+ # Качество
246
  "--output-image-quality", CONFIG["output_image_quality"],
247
+
248
+ # Логирование
249
  "--log-level", CONFIG["log_level"],
250
  ]
251
 
252
+ # Skip download если нужно
253
  if CONFIG["skip_download"]:
254
  cmd.append("--skip-download")
255
 
256
+ # Enhancer если указан
257
  if enhancer_model:
258
  cmd.extend([
259
  "--face-enhancer-model", enhancer_model,
260
  "--face-enhancer-blend", str(enhance_blend),
261
  ])
262
 
263
+ # Убираем None значения
264
  cmd = [arg for arg in cmd if arg is not None]
265
 
266
  logger.info(f"🚀 Running: {' '.join(cmd)}")
 
270
  env = {**os.environ, "OMP_NUM_THREADS": "4", "FACEFUSION_CONTENT_ANALYSER": "none"}
271
 
272
  process = subprocess.Popen(
273
+ cmd,
274
+ stdout=subprocess.PIPE,
275
+ stderr=subprocess.PIPE,
276
+ text=True,
277
+ cwd=str(FACEFUSION_DIR),
278
+ env=env
279
  )
280
 
281
  try:
 
283
  except subprocess.TimeoutExpired:
284
  process.kill()
285
  process.communicate()
286
+ logger.error(f"⏱️ Timeout after {timeout}s")
287
+ raise HTTPException(504, f"⏱️ Timeout: обработка заняла больше {timeout} секунд")
288
 
289
  elapsed = time.time() - process_start
290
  logger.info(f"⏱️ Done in {elapsed:.1f}s, code={process.returncode}")
291
 
292
+ # 🔥 Фильтруем harmless warnings из stderr
293
  if stderr:
294
+ stderr_clean = '\n'.join(
295
+ l for l in stderr.split('\n')
296
+ if 'iCCP' not in l and 'libpng' not in l.lower()
297
+ ).strip()
298
+ if stderr_clean:
299
  logger.info(f"📋 STDERR: {stderr_clean[-400:]}")
300
 
301
+ # 🔥 Проверяем результат
302
  if process.returncode != 0:
303
  error_msg = stderr.strip()[-400:] if stderr else "Unknown error"
304
  logger.error(f"❌ FaceFusion failed: {error_msg}")
305
  raise RuntimeError(f"FaceFusion error: {error_msg}")
306
 
307
+ # 🔥 Ищем выходной файл
308
+ result_file = output_path if output_path.exists() else None
 
 
 
 
 
 
309
  if not result_file:
310
+ # Fallback: ищем любой output*.jpg
311
+ for f in Path(temp_dir).glob("output*.jpg"):
312
+ result_file = f
313
+ break
314
 
315
  if not result_file or not result_file.exists():
316
  raise RuntimeError("Output image not created")
317
 
318
+ # 🔥 Читаем и возвращаем результат
319
  image_data = result_file.read_bytes()
320
+
321
  logger.info(f"✅ Success: {len(image_data)/1024:.1f}KB in {elapsed:.1f}s")
322
 
323
  return Response(
324
  content=image_data,
325
+ media_type="image/jpeg", # 🔥 ВСЕГДА JPEG!
326
  headers={
327
  "X-Processing-Time": f"{elapsed:.1f}",
328
  "X-Swapper-Model": swapper_model,
329
+ "X-Enhancer-Model": enhancer_model or "none",
330
+ "X-Output-Format": "jpeg",
331
  "Cache-Control": "no-store"
332
  }
333
  )
 
338
  logger.exception(f"💥 Error: {e}")
339
  raise
340
  finally:
341
+ # 🔥 Очистка временных файлов
342
  if temp_dir and Path(temp_dir).exists():
343
+ try:
344
+ shutil.rmtree(temp_dir, ignore_errors=True)
345
+ logger.debug(f"🧹 Cleaned: {temp_dir}")
346
+ except Exception as e:
347
+ logger.warning(f"⚠️ Could not clean: {e}")
348
+
349
+ # ============================================
350
+ # 🔥 УПРОЩЁННЫЕ ЭНДПОИНТЫ
351
+ # ============================================
352
 
353
  @app.post("/swap/quick")
354
+ async def swap_quick(
355
+ source: UploadFile = File(..., description="Фото лица"),
356
+ target: UploadFile = File(..., description="Целевое изображение")
357
+ ):
358
+ """
359
+ ⚡ Быстрая замена лица (inswapper_128 без enhancer)
360
+ Время: ~30-60 секунд
361
+ Выход: JPEG
362
+ """
363
+ return await swap_face(
364
+ source=source,
365
+ target=target,
366
+ swapper_model="inswapper_128",
367
+ enhancer_model="none",
368
+ use_fallback=False
369
+ )
370
 
371
  @app.post("/swap/hq")
372
+ async def swap_hq(
373
+ source: UploadFile = File(..., description="Фото лица"),
374
+ target: UploadFile = File(..., description="Целевое изображение"),
375
+ with_enhancer: bool = Query(default=True, description="Включить улучшение лица GFPGAN")
376
+ ):
377
+ """
378
+ ⭐ Максимальное качество (ghost_3_256 + gfpgan_1.4)
379
+ Время: ~90-180 секунд
380
+ Выход: JPEG
381
+ """
382
+ return await swap_face(
383
+ source=source,
384
+ target=target,
385
+ swapper_model="ghost_3_256",
386
+ enhancer_model="gfpgan_1.4" if with_enhancer else "none",
387
+ enhance_blend=80,
388
+ use_fallback=True
389
+ )
390
+
391
+ # ============================================
392
+ # 🔥 ЗАПУСК
393
+ # ============================================
394
 
395
  if __name__ == "__main__":
396
  import uvicorn
397
+ uvicorn.run(
398
+ app,
399
+ host="0.0.0.0",
400
+ port=7860,
401
+ log_level="info",
402
+ timeout_keep_alive=900 # 15 минут keep-alive
403
+ )