Dmitry1313 commited on
Commit
99e2cf8
·
verified ·
1 Parent(s): 33ae872

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +214 -246
app.py CHANGED
@@ -1,301 +1,269 @@
1
- # 🤖 HuggingFace FaceFusion API
2
- # Реальное API с максимальным качеством замены лиц (CPU)
3
  import io
4
  import os
5
  import logging
6
  import shutil
7
  from pathlib import Path
8
- import numpy as np
9
- from PIL import Image, ImageEnhance, ImageFilter
10
  import uvicorn
11
  from fastapi import FastAPI, File, UploadFile, Form, HTTPException
12
  from fastapi.responses import Response, JSONResponse
13
-
14
- # Попытка импорта FaceFusion с учётом возможных изменений в API
15
- FACEFUSION_AVAILABLE = False
16
- facefusion_import_error = None
17
-
18
- try:
19
- # Пытаемся импортировать основные модули
20
- from facefusion.face_analyser import get_many_faces
21
- from facefusion.typing import Frame
22
-
23
- # Проверяем различные варианты импорта процессоров
24
- try:
25
- # Новый стиль (классы)
26
- from facefusion.processors.frame.modules.face_swapper import FaceSwapper
27
- from facefusion.processors.frame.modules.face_enhancer import FaceEnhancer
28
- USE_CLASS_STYLE = True
29
- except ImportError:
30
- # Старый стиль (функции)
31
- try:
32
- from facefusion.processors.frame.modules.face_swapper import swap_face
33
- from facefusion.processors.frame.modules.face_enhancer import enhance_face
34
- USE_CLASS_STYLE = False
35
- except ImportError:
36
- # Падаем, если ничего не подошло
37
- raise ImportError("Cannot import face_swapper or face_enhancer modules")
38
-
39
- FACEFUSION_AVAILABLE = True
40
- print("✅ FaceFusion loaded successfully")
41
- except ImportError as e:
42
- facefusion_import_error = str(e)
43
- print(f"⚠️ FaceFusion not installed or incompatible: {e}")
44
 
45
  # Настройка логирования
46
  logging.basicConfig(level=logging.INFO)
47
  logger = logging.getLogger(__name__)
48
 
49
- app = FastAPI(title="FaceFusion API", description="API для замены лиц с максимальным качеством", version="2.0.0")
50
 
51
- TEMP_DIR = Path("/tmp/facefusion")
52
  TEMP_DIR.mkdir(exist_ok=True)
 
 
53
 
54
- # ----- Вспомогательные функции -----
55
- def enhance_image_quality(image_bytes: bytes) -> bytes:
56
- """Дополнительное улучшение качества (постобработка)"""
57
- try:
58
- img = Image.open(io.BytesIO(image_bytes)).convert("RGB")
59
- enhancer = ImageEnhance.Sharpness(img)
60
- img = enhancer.enhance(1.2)
61
- enhancer = ImageEnhance.Contrast(img)
62
- img = enhancer.enhance(1.1)
63
- img = img.filter(ImageFilter.UnsharpMask(radius=1, percent=120, threshold=3))
64
- output = io.BytesIO()
65
- img.save(output, format="JPEG", quality=98, optimize=True)
66
- return output.getvalue()
67
- except Exception as e:
68
- logger.error(f"❌ Image enhancement error: {e}")
69
- return image_bytes
70
 
71
- def save_upload_file(upload_file: UploadFile) -> Path:
72
- """Сохраняет загруженный файл во временную папку"""
 
 
 
 
 
 
 
73
  try:
74
- contents = upload_file.file.read()
75
- unique_name = f"{os.urandom(8).hex()}_{upload_file.filename}"
76
- file_path = TEMP_DIR / unique_name
77
- with open(file_path, "wb") as f:
78
- f.write(contents)
79
- return file_path
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  except Exception as e:
81
- logger.error(f"❌ File save error: {e}")
82
- raise HTTPException(status_code=500, detail="Failed to save file")
83
-
84
- def read_image_numpy(path: Path) -> np.ndarray:
85
- """Читает изображение как numpy array (RGB)"""
86
- img = Image.open(path).convert("RGB")
87
- return np.array(img)
88
-
89
- def write_numpy_image(arr: np.ndarray, quality: int = 98) -> bytes:
90
- """Конвертирует numpy array в байты JPEG"""
91
- img = Image.fromarray(arr.astype(np.uint8))
92
- output = io.BytesIO()
93
- img.save(output, format="JPEG", quality=quality, optimize=True)
94
- return output.getvalue()
95
-
96
- # ----- Основные функции обработки через FaceFusion (адаптер) -----
97
- def process_face_swap(source_path: Path, target_path: Path,
98
- swapper_model: str = "inswapper_128",
99
- enhancer_model: str = None) -> np.ndarray:
100
- """Замена лица с максимальным качеством"""
101
- if not FACEFUSION_AVAILABLE:
102
- logger.warning("FaceFusion not available, returning target with basic enhancements")
103
- return read_image_numpy(target_path)
104
-
105
- # Загружаем изображения
106
- target_frame = read_image_numpy(target_path)
107
- source_frame = read_image_numpy(source_path)
108
 
109
- # Детектируем лица
110
- source_faces = get_many_faces(source_frame)
111
- if not source_faces:
112
- raise HTTPException(status_code=400, detail="No face found in source image")
113
- source_face = source_faces[0]
114
-
115
- target_faces = get_many_faces(target_frame)
116
- if not target_faces:
117
- raise HTTPException(status_code=400, detail="No face found in target image")
118
-
119
- result_frame = target_frame.copy()
120
-
121
- if USE_CLASS_STYLE:
122
- # Используем классы FaceSwapper / FaceEnhancer
123
- swapper = FaceSwapper(swapper_model)
124
- for target_face in target_faces:
125
- result_frame = swapper.process_frame(result_frame, source_face, target_face)
126
- if enhancer_model:
127
- enhancer = FaceEnhancer(enhancer_model)
128
- result_frame = enhancer.process_frame(result_frame)
129
- else:
130
- # Старый стиль (функции)
131
- for target_face in target_faces:
132
- result_frame = swap_face(result_frame, source_face, target_face, model=swapper_model)
133
- if enhancer_model:
134
- result_frame = enhance_face(result_frame, model=enhancer_model)
135
-
136
- return result_frame
137
-
138
- def process_face_enhance(image_path: Path, enhancer_model: str = "gfpgan_1.4") -> np.ndarray:
139
- """Улучшение лица"""
140
- if not FACEFUSION_AVAILABLE:
141
- return read_image_numpy(image_path)
142
- frame = read_image_numpy(image_path)
143
- if USE_CLASS_STYLE:
144
- enhancer = FaceEnhancer(enhancer_model)
145
- return enhancer.process_frame(frame)
146
- else:
147
- return enhance_face(frame, model=enhancer_model)
148
-
149
- def process_face_analyse(image_path: Path) -> dict:
150
- """Анализ лиц"""
151
- if not FACEFUSION_AVAILABLE:
152
- # Заглушка
153
- img = Image.open(image_path)
154
- w, h = img.size
155
- return {
156
- "faces_detected": 1,
157
- "face_regions": [{"x": int(w*0.3), "y": int(h*0.1), "width": int(w*0.4), "height": int(h*0.4), "confidence": 0.95}],
158
- "image_size": {"width": w, "height": h}
159
- }
160
- frame = read_image_numpy(image_path)
161
- faces = get_many_faces(frame)
162
- regions = []
163
- for face in faces:
164
- # Проверяем, какой формат у bounding box
165
- if hasattr(face, 'bbox'):
166
- bbox = face.bbox
167
- elif hasattr(face, 'detection'):
168
- bbox = face.detection
169
- else:
170
- # Пытаемся получить через стандартный способ
171
- bbox = face.bbox if hasattr(face, 'bbox') else [0,0,0,0]
172
- # Уверены, что bbox - список из 4 чисел
173
- if len(bbox) == 4:
174
- x1, y1, x2, y2 = map(int, bbox)
175
- regions.append({
176
- "x": x1,
177
- "y": y1,
178
- "width": x2 - x1,
179
- "height": y2 - y1,
180
- "confidence": getattr(face, 'det_score', 0.95)
181
- })
182
- h, w = frame.shape[:2]
183
- return {
184
- "faces_detected": len(faces),
185
- "face_regions": regions,
186
- "image_size": {"width": w, "height": h}
187
- }
188
 
189
- # ----- Эндпоинты -----
190
- @app.get("/")
191
- async def root():
192
- return {"service": "FaceFusion API", "status": "running", "version": "2.0.0"}
 
 
 
 
193
 
194
- @app.get("/health")
195
- async def health():
196
- return {"status": "ok", "facefusion_loaded": FACEFUSION_AVAILABLE}
 
 
 
 
197
 
198
  @app.post("/swap")
199
  async def swap_face(
200
- target: UploadFile = File(...),
201
- source: UploadFile = File(...),
202
- face_enhancer: str = Form("true"),
203
- face_swapper_model: str = Form("inswapper_128"),
204
- face_enhancer_model: str = Form("gfpgan_1.4")
205
  ):
206
- """Замена лица – возвращает изображение"""
 
 
 
207
  target_path = source_path = None
 
208
  try:
 
209
  target_path = save_upload_file(target)
210
  source_path = save_upload_file(source)
211
- logger.info(f"🔄 Processing swap: {target.filename} <- {source.filename}")
212
-
213
- result_arr = process_face_swap(
214
- source_path, target_path,
215
- swapper_model=face_swapper_model,
216
- enhancer_model=face_enhancer_model if face_enhancer.lower() == "true" else None
217
- )
218
- result_bytes = write_numpy_image(result_arr, quality=98)
219
- # Финальное улучшение
220
- enhanced_bytes = enhance_image_quality(result_bytes)
221
-
222
- logger.info(f"✅ Swap completed, size: {len(enhanced_bytes)} bytes")
223
- return Response(content=enhanced_bytes, media_type="image/jpeg")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
  except Exception as e:
225
  logger.error(f"❌ Swap error: {e}")
226
  raise HTTPException(status_code=500, detail=str(e))
227
  finally:
 
228
  for p in (target_path, source_path):
229
  if p and p.exists():
230
  p.unlink()
231
 
232
- @app.post("/enhance")
233
- async def enhance_face(
234
- image: UploadFile = File(...),
235
- enhancer_model: str = Form("gfpgan_1.4")
 
 
236
  ):
237
- """Улучшение лица возвращает изображение"""
238
- img_path = None
 
239
  try:
240
- img_path = save_upload_file(image)
241
- logger.info(f"🔧 Enhancing: {image.filename}")
242
-
243
- result_arr = process_face_enhance(img_path, enhancer_model)
244
- result_bytes = write_numpy_image(result_arr, quality=98)
245
- enhanced_bytes = enhance_image_quality(result_bytes)
246
-
247
- logger.info(f"✅ Enhancement completed, size: {len(enhanced_bytes)} bytes")
248
- return Response(content=enhanced_bytes, media_type="image/jpeg")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
  except Exception as e:
250
- logger.error(f"❌ Enhance error: {e}")
251
  raise HTTPException(status_code=500, detail=str(e))
252
  finally:
253
- if img_path and img_path.exists():
254
- img_path.unlink()
 
255
 
256
- @app.post("/analyse")
257
- async def analyse_face(image: UploadFile = File(...)):
258
- """Анализ лиц – возвращает JSON"""
259
- img_path = None
260
- try:
261
- img_path = save_upload_file(image)
262
- analysis = process_face_analyse(img_path)
263
- return JSONResponse(content={"status": "success", "analysis": analysis})
264
- except Exception as e:
265
- logger.error(f"❌ Analyse error: {e}")
266
- raise HTTPException(status_code=500, detail=str(e))
267
- finally:
268
- if img_path and img_path.exists():
269
- img_path.unlink()
270
 
271
  @app.get("/models")
272
- async def get_available_models():
273
- """Список доступных моделей"""
274
  return JSONResponse(content={
275
- "face_swappers": [
276
- {"name": "inswapper_128", "description": "Fast and accurate face swapper"},
277
- {"name": "simswap", "description": "High quality face swapper"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278
  ],
279
- "face_enhancers": [
280
- {"name": "gfpgan_1.4", "description": "Face restoration model"},
281
- {"name": "codeformer", "description": "Face enhancement model"}
282
- ],
283
- "face_detectors": [
284
- {"name": "retinaface", "description": "Accurate face detector"},
285
- {"name": "mtcnn", "description": "Multi-task face detector"}
286
- ]
287
  })
288
 
289
- @app.on_event("startup")
290
- async def startup_event():
291
- logger.info("🚀 FaceFusion API starting...")
292
- if FACEFUSION_AVAILABLE:
293
- logger.info("✅ FaceFusion loaded successfully")
294
- else:
295
- logger.warning(f"⚠️ FaceFusion not available, using mock mode. Error: {facefusion_import_error}")
296
-
297
  @app.on_event("shutdown")
298
  async def shutdown_event():
 
299
  logger.info("🛑 Shutting down...")
300
  shutil.rmtree(TEMP_DIR, ignore_errors=True)
301
 
 
1
+ # 🤖 HuggingFace BFS Face Swap API (CPU Optimized)
 
2
  import io
3
  import os
4
  import logging
5
  import shutil
6
  from pathlib import Path
7
+ import torch
8
+ from PIL import Image
9
  import uvicorn
10
  from fastapi import FastAPI, File, UploadFile, Form, HTTPException
11
  from fastapi.responses import Response, JSONResponse
12
+ from diffusers import QwenImageEditPlusPipeline
13
+ from huggingface_hub import snapshot_download
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
  # Настройка логирования
16
  logging.basicConfig(level=logging.INFO)
17
  logger = logging.getLogger(__name__)
18
 
19
+ app = FastAPI(title="BFS Face Swap API", version="3.0.0")
20
 
21
+ TEMP_DIR = Path("/tmp/bfs")
22
  TEMP_DIR.mkdir(exist_ok=True)
23
+ MODEL_CACHE = Path("/app/model_cache")
24
+ MODEL_CACHE.mkdir(exist_ok=True)
25
 
26
+ # Глобальные переменные
27
+ pipe = None
28
+ lora_loaded = False
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
+ # Конфигурация модели
31
+ BASE_MODEL = "Qwen/Qwen-Image-Edit-2511" # или 2509
32
+ LORA_REPO = "Alissonerdx/BFS-Best-Face-Swap"
33
+ LORA_FILE = "bfs_head_v5_2511_merged_version_rank_16_fp16.safetensors" # Рекомендованная версия [citation:2][citation:4]
34
+
35
+ def load_pipeline():
36
+ """Загрузка квантованной модели с CPU offload"""
37
+ global pipe, lora_loaded
38
+
39
  try:
40
+ logger.info("🔄 Loading 4-bit quantized model (first load takes 10-15 minutes)...")
41
+
42
+ # Используем 4-bit версию для экономии RAM
43
+ pipe = QwenImageEditPlusPipeline.from_pretrained(
44
+ "toandev/Qwen-Image-Edit-2511-4bit", # 4-bit quantized version
45
+ torch_dtype=torch.bfloat16,
46
+ low_cpu_mem_usage=True,
47
+ cache_dir=MODEL_CACHE
48
+ )
49
+
50
+ # КРИТИЧЕСКИ ВАЖНО для CPU: включаем offload
51
+ pipe.enable_model_cpu_offload()
52
+
53
+ # Загружаем BFS LoRA веса
54
+ logger.info("🔄 Loading BFS LoRA weights...")
55
+
56
+ # Скачиваем LoRA если ещё нет
57
+ lora_path = MODEL_CACHE / LORA_FILE
58
+ if not lora_path.exists():
59
+ snapshot_download(
60
+ repo_id=LORA_REPO,
61
+ allow_patterns=[LORA_FILE],
62
+ local_dir=MODEL_CACHE
63
+ )
64
+
65
+ pipe.load_lora_weights(str(lora_path))
66
+ lora_loaded = True
67
+
68
+ logger.info("✅ Model and LoRA loaded successfully")
69
+ return True
70
+
71
  except Exception as e:
72
+ logger.error(f"❌ Failed to load model: {e}")
73
+ return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
 
75
+ def save_upload_file(upload_file: UploadFile) -> Path:
76
+ """Сохраняет загруженный файл"""
77
+ contents = upload_file.file.read()
78
+ unique_name = f"{os.urandom(8).hex()}_{upload_file.filename}"
79
+ file_path = TEMP_DIR / unique_name
80
+ with open(file_path, "wb") as f:
81
+ f.write(contents)
82
+ return file_path
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
 
84
+ def optimize_image(image: Image.Image, max_size=1024) -> Image.Image:
85
+ """Оптимизация размера для ускорения"""
86
+ w, h = image.size
87
+ if max(w, h) > max_size:
88
+ scale = max_size / max(w, h)
89
+ new_w, new_h = int(w * scale), int(h * scale)
90
+ return image.resize((new_w, new_h), Image.Resampling.LANCZOS)
91
+ return image
92
 
93
+ @app.on_event("startup")
94
+ async def startup_event():
95
+ """Загрузка модели при старте"""
96
+ logger.info("🚀 BFS Face Swap API starting...")
97
+ success = load_pipeline()
98
+ if not success:
99
+ logger.warning("⚠️ Model failed to load, API will return fallback responses")
100
 
101
  @app.post("/swap")
102
  async def swap_face(
103
+ target: UploadFile = File(...), # BODY (тело) - ПЕРВОЕ!
104
+ source: UploadFile = File(...), # FACE (лицо) - ВТОРОЕ!
105
+ num_steps: int = Form(20), # Можно уменьшить до 10-15 для скорости
106
+ guidance_scale: float = Form(1.0)
 
107
  ):
108
+ """
109
+ Замена лица с BFS Head V5
110
+ ВАЖНО: порядок файлов - сначала тело (target), потом лицо (source) [citation:2][citation:4]
111
+ """
112
  target_path = source_path = None
113
+
114
  try:
115
+ # Сохраняем файлы
116
  target_path = save_upload_file(target)
117
  source_path = save_upload_file(source)
118
+
119
+ logger.info(f"🔄 Processing BFS V5 swap: {target.filename} (body) <- {source.filename} (face)")
120
+
121
+ # Загружаем и оптимизируем изображения
122
+ body_img = optimize_image(Image.open(target_path).convert("RGB"))
123
+ face_img = optimize_image(Image.open(source_path).convert("RGB"))
124
+
125
+ if pipe is not None and lora_loaded:
126
+ # Промпт для Head V5 из официальной документации [citation:2][citation:4]
127
+ prompt = """head_swap: start with Picture 1 as the base image, keeping its lighting, environment, and background. remove the head from Picture 1 completely and replace it with the head from Picture 2, strictly preserving the hair, eye color, and nose structure of Picture 2. copy the eye direction, head rotation, and micro-expressions from Picture 1. high quality, sharp details, 4k"""
128
+
129
+ # Инвертированный порядок: [body, face] [citation:2]
130
+ inputs = {
131
+ "image": [body_img, face_img],
132
+ "prompt": prompt,
133
+ "generator": torch.manual_seed(42),
134
+ "true_cfg_scale": 4.0,
135
+ "negative_prompt": "blurry, low quality, distorted face, bad anatomy, unnatural lighting",
136
+ "num_inference_steps": num_steps,
137
+ "guidance_scale": guidance_scale,
138
+ }
139
+
140
+ logger.info("🔄 Running inference (this takes 1-3 minutes on CPU)...")
141
+ with torch.inference_mode():
142
+ output = pipe(**inputs)
143
+ result_img = output.images[0]
144
+ else:
145
+ # Fallback: если модель не загрузилась
146
+ logger.warning("Using fallback mode - returning body image")
147
+ result_img = body_img
148
+
149
+ # Сохраняем результат с высоким качеством
150
+ result_bytes = io.BytesIO()
151
+ result_img.save(result_bytes, format="JPEG", quality=98, optimize=True)
152
+
153
+ logger.info(f"✅ Swap completed: {len(result_bytes.getvalue())} bytes")
154
+ return Response(content=result_bytes.getvalue(), media_type="image/jpeg")
155
+
156
  except Exception as e:
157
  logger.error(f"❌ Swap error: {e}")
158
  raise HTTPException(status_code=500, detail=str(e))
159
  finally:
160
+ # Очистка временных файлов
161
  for p in (target_path, source_path):
162
  if p and p.exists():
163
  p.unlink()
164
 
165
+ @app.post("/swap-with-prompt")
166
+ async def swap_face_with_prompt(
167
+ target: UploadFile = File(...), # BODY
168
+ source: UploadFile = File(...), # FACE
169
+ custom_prompt: str = Form(...), # Кастомный промпт
170
+ num_steps: int = Form(20)
171
  ):
172
+ """Замена лица с кастомным промптом"""
173
+ target_path = source_path = None
174
+
175
  try:
176
+ target_path = save_upload_file(target)
177
+ source_path = save_upload_file(source)
178
+
179
+ body_img = optimize_image(Image.open(target_path).convert("RGB"))
180
+ face_img = optimize_image(Image.open(source_path).convert("RGB"))
181
+
182
+ if pipe is not None and lora_loaded:
183
+ # Добавляем требования качества к кастомному промпту
184
+ enhanced_prompt = f"{custom_prompt}. high quality, sharp details, 4k, photorealistic"
185
+
186
+ inputs = {
187
+ "image": [body_img, face_img],
188
+ "prompt": enhanced_prompt,
189
+ "generator": torch.manual_seed(42),
190
+ "true_cfg_scale": 4.0,
191
+ "negative_prompt": "blurry, low quality, distorted",
192
+ "num_inference_steps": num_steps,
193
+ "guidance_scale": 1.0,
194
+ }
195
+
196
+ with torch.inference_mode():
197
+ output = pipe(**inputs)
198
+ result_img = output.images[0]
199
+ else:
200
+ result_img = body_img
201
+
202
+ result_bytes = io.BytesIO()
203
+ result_img.save(result_bytes, format="JPEG", quality=98)
204
+
205
+ return Response(content=result_bytes.getvalue(), media_type="image/jpeg")
206
+
207
  except Exception as e:
208
+ logger.error(f"❌ Swap error: {e}")
209
  raise HTTPException(status_code=500, detail=str(e))
210
  finally:
211
+ for p in (target_path, source_path):
212
+ if p and p.exists():
213
+ p.unlink()
214
 
215
+ @app.get("/health")
216
+ async def health():
217
+ """Проверка статуса"""
218
+ return {
219
+ "status": "ok",
220
+ "model_loaded": pipe is not None,
221
+ "lora_loaded": lora_loaded,
222
+ "base_model": BASE_MODEL,
223
+ "lora_file": LORA_FILE
224
+ }
 
 
 
 
225
 
226
  @app.get("/models")
227
+ async def get_models():
228
+ """Информация о доступных версиях BFS [citation:2]"""
229
  return JSONResponse(content={
230
+ "base_model": BASE_MODEL,
231
+ "lora_repo": LORA_REPO,
232
+ "current_lora": LORA_FILE,
233
+ "available_versions": [
234
+ {
235
+ "version": "Face V1",
236
+ "file": "bfs_face_v1_qwen_image_edit_2509.safetensors",
237
+ "order": "Face then Body",
238
+ "description": "Swaps only face, preserves hair"
239
+ },
240
+ {
241
+ "version": "Head V1",
242
+ "file": "bfs_head_v1_qwen_image_edit_2509.safetensors",
243
+ "order": "Face then Body",
244
+ "description": "Full head swap"
245
+ },
246
+ {
247
+ "version": "Head V3 (Recommended for 2509)",
248
+ "file": "bfs_head_v3_qwen_image_edit_2509.safetensors",
249
+ "order": "Body then Face",
250
+ "description": "Most stable for 2509"
251
+ },
252
+ {
253
+ "version": "Head V5 (Recommended for 2511)",
254
+ "file": "bfs_head_v5_2511_merged_version_rank_16_fp16.safetensors",
255
+ "order": "Body then Face",
256
+ "description": "Latest, best expression transfer"
257
+ }
258
  ],
259
+ "prompts": {
260
+ "head_v5": "head_swap: start with Picture 1 as the base image, keeping its lighting, environment, and background. remove the head from Picture 1 completely and replace it with the head from Picture 2, strictly preserving the hair, eye color, and nose structure of Picture 2. copy the eye direction, head rotation, and micro-expressions from Picture 1. high quality, sharp details, 4k"
261
+ }
 
 
 
 
 
262
  })
263
 
 
 
 
 
 
 
 
 
264
  @app.on_event("shutdown")
265
  async def shutdown_event():
266
+ """Очистка при остановке"""
267
  logger.info("🛑 Shutting down...")
268
  shutil.rmtree(TEMP_DIR, ignore_errors=True)
269