Dmitry1313 commited on
Commit
60198ae
·
verified ·
1 Parent(s): fdf4b7c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +97 -111
app.py CHANGED
@@ -1,145 +1,131 @@
1
  import os
2
- import subprocess
 
3
  import tempfile
4
  import uuid
5
- import json
6
  import time
7
- import requests
8
- from fastapi import FastAPI, File, UploadFile, HTTPException
9
- from fastapi.responses import Response, JSONResponse
10
- import uvicorn
11
  import logging
 
 
 
 
12
 
 
13
  logging.basicConfig(level=logging.INFO)
14
  logger = logging.getLogger(__name__)
15
 
16
- app = FastAPI()
17
 
18
- # Запускаем ComfyUI в фоновом режиме
19
- comfy_process = None
 
20
 
21
- @app.on_event("startup")
22
- async def startup_event():
23
- global comfy_process
24
- logger.info("Starting ComfyUI...")
25
- comfy_process = subprocess.Popen(
26
- ["python", "/comfyui/main.py", "--listen", "0.0.0.0", "--port", "8188"],
27
- stdout=subprocess.PIPE,
28
- stderr=subprocess.PIPE
29
- )
30
- # Даём время на запуск
31
- time.sleep(10)
32
- logger.info("ComfyUI started")
33
 
34
- @app.on_event("shutdown")
35
- async def shutdown_event():
36
- if comfy_process:
37
- comfy_process.terminate()
 
 
 
38
 
39
- def load_workflow():
40
- """Загружает workflow из файла или создаёт базовый"""
41
- # Здесь можно загрузить ваш workflow.json
42
- # Пока используем простой workflow
43
- return {
44
- "3": {
45
- "class_type": "LoadImage",
46
- "inputs": {
47
- "image": "source.jpg"
48
- }
49
- },
50
- "4": {
51
- "class_type": "LoadImage",
52
- "inputs": {
53
- "image": "target.jpg"
54
- }
55
- },
56
- "5": {
57
- "class_type": "ReActorFaceSwap",
58
- "inputs": {
59
- "source_image": ["3", 0],
60
- "target_image": ["4", 0],
61
- "face_restorer": "gfpgan",
62
- "face_restorer_weight": 0.8,
63
- "swap_model": "inswapper_128.onnx",
64
- "detect_model": "yolov8n-face.pt",
65
- "save_original": False,
66
- "output_image": ["6", 0]
67
- }
68
- },
69
- "6": {
70
- "class_type": "SaveImage",
71
- "inputs": {
72
- "filename_prefix": "output",
73
- "images": ["5", 0]
74
- }
75
- }
76
- }
77
 
78
  @app.post("/swap")
79
- async def swap_face(
80
- source: UploadFile = File(...),
81
- target: UploadFile = File(...)
82
  ):
83
- temp_dir = tempfile.mkdtemp()
 
 
 
 
84
  try:
85
- # Сохраняем входные файлы
86
- source_path = os.path.join(temp_dir, "source.jpg")
87
- target_path = os.path.join(temp_dir, "target.jpg")
 
88
 
89
- with open(source_path, "wb") as f:
90
- f.write(await source.read())
91
  with open(target_path, "wb") as f:
92
  f.write(await target.read())
93
-
94
- # Копируем файлы в папку ComfyUI
95
- comfy_input_dir = "/comfyui/input"
96
- os.makedirs(comfy_input_dir, exist_ok=True)
97
 
98
- shutil.copy(source_path, os.path.join(comfy_input_dir, "source.jpg"))
99
- shutil.copy(target_path, os.path.join(comfy_input_dir, "target.jpg"))
100
-
101
- # Загружаем workflow
102
- workflow = load_workflow()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
 
104
- # Отправляем задачу в ComfyUI
105
- response = requests.post(
106
- "http://127.0.0.1:8188/prompt",
107
- json={"prompt": workflow}
108
- )
109
 
110
- if response.status_code != 200:
111
- raise HTTPException(500, f"ComfyUI error: {response.text}")
 
 
112
 
113
- prompt_id = response.json()["prompt_id"]
 
114
 
115
- # Ждём завершения
116
- while True:
117
- status = requests.get(f"http://127.0.0.1:8188/history/{prompt_id}")
118
- if status.status_code == 200 and status.json():
119
- history = status.json()
120
- if prompt_id in history:
121
- output_images = history[prompt_id]["outputs"]
122
- # Находим выходное изображение
123
- for node_id, node_output in output_images.items():
124
- if "images" in node_output:
125
- image_info = node_output["images"][0]
126
- image_path = os.path.join("/comfyui/output", image_info["filename"])
127
- if os.path.exists(image_path):
128
- with open(image_path, "rb") as f:
129
- image_data = f.read()
130
- return Response(content=image_data, media_type="image/jpeg")
131
- time.sleep(1)
132
-
133
  except Exception as e:
134
- logger.exception("Error")
135
  raise HTTPException(500, str(e))
 
136
  finally:
137
- import shutil
138
- shutil.rmtree(temp_dir, ignore_errors=True)
 
 
139
 
140
  @app.get("/health")
141
  async def health():
142
- return {"status": "ok"}
143
 
144
  if __name__ == "__main__":
145
  uvicorn.run(app, host="0.0.0.0", port=7860)
 
1
  import os
2
+ import torch
3
+ import uvicorn
4
  import tempfile
5
  import uuid
 
6
  import time
 
 
 
 
7
  import logging
8
+ from fastapi import FastAPI, File, UploadFile, HTTPException
9
+ from fastapi.responses import Response
10
+ from PIL import Image
11
+ from diffusers import QwenImageEditPlusPipeline
12
 
13
+ # Настройка логирования
14
  logging.basicConfig(level=logging.INFO)
15
  logger = logging.getLogger(__name__)
16
 
17
+ app = FastAPI(title="Head Swap API (Qwen + BFS LoRA)")
18
 
19
+ # Глобальные переменные для пайплайна
20
+ pipe = None
21
+ device = "cpu" # Работаем на CPU
22
 
23
+ # Константы для генерации
24
+ NUM_INFERENCE_STEPS = 40
25
+ TRUE_GUIDANCE_SCALE = 4.0
26
+ NEGATIVE_PROMPT = " " # Пустой негативный промпт (как в оригинале)
 
 
 
 
 
 
 
 
27
 
28
+ # Фиксированный промпт для замены головы (следуя рекомендациям BFS V3)
29
+ # Picture 1 – целевое изображение (тело), Picture 2 – изображение лица (источник)
30
+ HEAD_SWAP_PROMPT = (
31
+ "head_swap: start with Picture 1 as the base image, keeping its lighting, environment, and background. "
32
+ "remove the head from Picture 1 completely and replace it with the head from Picture 2, "
33
+ "ensuring a seamless and natural blend. maintain the facial identity, expression, and features from Picture 2."
34
+ )
35
 
36
+ @app.on_event("startup")
37
+ async def load_model():
38
+ global pipe
39
+ logger.info("Loading Qwen-Image-Edit-2511 model...")
40
+ # Загружаем пайплайн с float32 для CPU
41
+ pipe = QwenImageEditPlusPipeline.from_pretrained(
42
+ "Qwen/Qwen-Image-Edit-2511",
43
+ torch_dtype=torch.float32,
44
+ safety_checker=None # отключаем safety checker для скорости
45
+ )
46
+ pipe = pipe.to(device)
47
+
48
+ # Загружаем LoRA BFS Head V3
49
+ lora_path = "/app/bfs_head_v3_qwen_image_edit_2509.safetensors"
50
+ if os.path.exists(lora_path):
51
+ logger.info("Loading BFS LoRA weights...")
52
+ pipe.load_lora_weights(lora_path, adapter_name="bfs")
53
+ pipe.set_adapter("bfs")
54
+ else:
55
+ logger.warning("LoRA file not found, proceeding without it.")
56
+
57
+ logger.info("Model ready.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
 
59
  @app.post("/swap")
60
+ async def swap_head(
61
+ target: UploadFile = File(..., description="Target image (body)"),
62
+ source: UploadFile = File(..., description="Source image (face)")
63
  ):
64
+ """
65
+ Заменяет голову на целевом изображении (target) лицом из source.
66
+ Порядок важен: target – тело, source – лицо (BFS V3 инвертированный порядок).
67
+ """
68
+ temp_dir = None
69
  try:
70
+ # Сохраняем загруженные файлы во временную директорию
71
+ temp_dir = tempfile.mkdtemp()
72
+ target_path = os.path.join(temp_dir, f"target_{uuid.uuid4().hex}.jpg")
73
+ source_path = os.path.join(temp_dir, f"source_{uuid.uuid4().hex}.jpg")
74
 
 
 
75
  with open(target_path, "wb") as f:
76
  f.write(await target.read())
77
+ with open(source_path, "wb") as f:
78
+ f.write(await source.read())
 
 
79
 
80
+ # Открываем изображения как PIL
81
+ target_img = Image.open(target_path).convert("RGB")
82
+ source_img = Image.open(source_path).convert("RGB")
83
+
84
+ # Пайплайн ожидает список изображений: [target, source] (в таком порядке)
85
+ input_images = [target_img, source_img]
86
+
87
+ logger.info("Starting generation...")
88
+ start_time = time.time()
89
+
90
+ # Генерация
91
+ result_images = pipe(
92
+ image=input_images,
93
+ prompt=HEAD_SWAP_PROMPT,
94
+ negative_prompt=NEGATIVE_PROMPT,
95
+ num_inference_steps=NUM_INFERENCE_STEPS,
96
+ true_cfg_scale=TRUE_GUIDANCE_SCALE,
97
+ generator=torch.Generator(device=device).manual_seed(42), # фиксированный seed для воспроизводимости
98
+ ).images
99
+
100
+ elapsed = time.time() - start_time
101
+ logger.info(f"Generation took {elapsed:.2f} seconds")
102
 
103
+ if not result_images:
104
+ raise HTTPException(500, "No image generated")
 
 
 
105
 
106
+ # Сохраняем результат во временный файл и возвращаем
107
+ result_img = result_images[0]
108
+ output_path = os.path.join(temp_dir, f"output_{uuid.uuid4().hex}.jpg")
109
+ result_img.save(output_path, format="JPEG")
110
 
111
+ with open(output_path, "rb") as f:
112
+ image_data = f.read()
113
 
114
+ return Response(content=image_data, media_type="image/jpeg")
115
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  except Exception as e:
117
+ logger.exception("Error during head swap")
118
  raise HTTPException(500, str(e))
119
+
120
  finally:
121
+ # Очистка временных файлов
122
+ if temp_dir and os.path.exists(temp_dir):
123
+ import shutil
124
+ shutil.rmtree(temp_dir, ignore_errors=True)
125
 
126
  @app.get("/health")
127
  async def health():
128
+ return {"status": "ok", "device": device, "model": "Qwen-Image-Edit-2511 with BFS LoRA"}
129
 
130
  if __name__ == "__main__":
131
  uvicorn.run(app, host="0.0.0.0", port=7860)