Spaces:
Build error
Build error
Upload 4 files
Browse files- .gitattributes +13 -0
- app.py +270 -0
- packages.txt +2 -0
- requirements.txt +11 -0
.gitattributes
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
app.py
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import cv2
|
| 3 |
+
import numpy as np
|
| 4 |
+
from PIL import Image
|
| 5 |
+
import urllib.request
|
| 6 |
+
import gradio as gr
|
| 7 |
+
import insightface
|
| 8 |
+
from insightface.app import FaceAnalysis
|
| 9 |
+
from gfpgan import GFPGANer
|
| 10 |
+
|
| 11 |
+
# --- Загрузка моделей при первом старте ---
|
| 12 |
+
def download_file(url: str, filename: str):
|
| 13 |
+
if not os.path.exists(filename):
|
| 14 |
+
print(f"Скачиваю {filename} из {url}...")
|
| 15 |
+
urllib.request.urlretrieve(url, filename)
|
| 16 |
+
print(f"{filename} скачан.")
|
| 17 |
+
|
| 18 |
+
# Пути к моделям
|
| 19 |
+
INSWAPPER_URL = "https://huggingface.co/ezioruan/inswapper_128.onnx/resolve/main/inswapper_128.onnx"
|
| 20 |
+
INSWAPPER_PATH = "inswapper_128.onnx"
|
| 21 |
+
GFPGAN_URL = "https://github.com/TencentARC/GFPGAN/releases/download/v1.3.0/GFPGANv1.3.pth"
|
| 22 |
+
GFPGAN_PATH = "GFPGANv1.3.pth"
|
| 23 |
+
|
| 24 |
+
download_file(INSWAPPER_URL, INSWAPPER_PATH)
|
| 25 |
+
download_file(GFPGAN_URL, GFPGAN_PATH)
|
| 26 |
+
|
| 27 |
+
assert os.path.exists(INSWAPPER_PATH), "inswapper_128.onnx не найден."
|
| 28 |
+
assert os.path.exists(GFPGAN_PATH), "GFPGANv1.3.pth не найден."
|
| 29 |
+
|
| 30 |
+
# --- Инициализация моделей ---
|
| 31 |
+
try:
|
| 32 |
+
import torch
|
| 33 |
+
has_gpu = torch.cuda.is_available()
|
| 34 |
+
except Exception:
|
| 35 |
+
has_gpu = False
|
| 36 |
+
|
| 37 |
+
ctx_id = 0 if has_gpu else -1 # 0 = GPU, -1 = CPU
|
| 38 |
+
print("GPU доступен" if has_gpu else "Работаем на CPU")
|
| 39 |
+
|
| 40 |
+
providers = ['CPUExecutionProvider'] # Только CPU для бесплатного тарифа
|
| 41 |
+
|
| 42 |
+
app_face = FaceAnalysis(name="buffalo_l", providers=providers)
|
| 43 |
+
app_face.prepare(ctx_id=ctx_id, det_size=(640, 640))
|
| 44 |
+
|
| 45 |
+
swapper = insightface.model_zoo.get_model(INSWAPPER_PATH, providers=providers)
|
| 46 |
+
face_enhancer = GFPGANer(
|
| 47 |
+
model_path=GFPGAN_PATH,
|
| 48 |
+
upscale=1,
|
| 49 |
+
arch="clean",
|
| 50 |
+
channel_multiplier=2,
|
| 51 |
+
bg_upsampler=None,
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
MAX_PREVIEWS = 8 # Максимум маленьких превью
|
| 55 |
+
|
| 56 |
+
# --- Тексты для локализации ---
|
| 57 |
+
TEXTS = {
|
| 58 |
+
"ru": {
|
| 59 |
+
"lang_radio_label": "Язык / Language",
|
| 60 |
+
"title_md": "# FaceSwap Pro\nСлева — фото **донора**, справа — фото, где нужно заменить лица.",
|
| 61 |
+
"step1_title": "### 1. Фото донора",
|
| 62 |
+
"step1_input_label": "Загрузите фото донора (может быть несколько лиц)",
|
| 63 |
+
"step1_donor_choice_label": "Выберите лицо-донора",
|
| 64 |
+
"step2_title": "### 2. Фото, которое меняем",
|
| 65 |
+
"step2_input_label": "Загрузите изображение для замены лиц",
|
| 66 |
+
"step2_target_choices_label": "Выберите лица для замены (если ничего не выбрано — меняем все)",
|
| 67 |
+
"step3_title": "### 3. Настройки и экспорт",
|
| 68 |
+
"use_enh_label": "Улучшить качество результата (GFPGAN)",
|
| 69 |
+
"eta_initial": "Оценка времени появится после обнаружения лиц.",
|
| 70 |
+
"fmt_label": "Формат файла для скачивания",
|
| 71 |
+
"run_btn": "Запустить замену",
|
| 72 |
+
"download_btn": "Скачать результат",
|
| 73 |
+
"before_after_label": "До / После",
|
| 74 |
+
"eta_fmt_sec": "Оценочное время обработки: ~{sec} сек.",
|
| 75 |
+
"eta_fmt_min": "Оценочное время обработки: ~{min} мин {sec} сек.",
|
| 76 |
+
"msg_need_images": "Загрузите оба изображения, чтобы начать обработку.",
|
| 77 |
+
"msg_no_donor_faces": "На фото донора не найдено ни одного лица.",
|
| 78 |
+
"msg_no_target_faces": "На целевом фото не найдено ни одного лица.",
|
| 79 |
+
"msg_prep": "Подготовка к обработке...",
|
| 80 |
+
"msg_swap_step": "Замена лица {i} из {n}",
|
| 81 |
+
"msg_enh": "Улучшение качества (GFPGAN)...",
|
| 82 |
+
"msg_done": "Готово. Обработано лиц: {n}.",
|
| 83 |
+
"progress_done": "Готово!",
|
| 84 |
+
"donor_option": "Донор {i}",
|
| 85 |
+
"target_option": "Лицо {i}",
|
| 86 |
+
},
|
| 87 |
+
"en": {
|
| 88 |
+
"lang_radio_label": "Language / Язык",
|
| 89 |
+
"title_md": "# FaceSwap Pro\nLeft — **donor photo**, right — photo where faces will be replaced.",
|
| 90 |
+
"step1_title": "### 1. Donor photo",
|
| 91 |
+
"step1_input_label": "Upload donor photo (can contain multiple faces)",
|
| 92 |
+
"step1_donor_choice_label": "Choose donor face",
|
| 93 |
+
"step2_title": "### 2. Target photo",
|
| 94 |
+
"step2_input_label": "Upload image where faces will be replaced",
|
| 95 |
+
"step2_target_choices_label": "Choose faces to replace (if none selected — replace all)",
|
| 96 |
+
"step3_title": "### 3. Settings & Export",
|
| 97 |
+
"use_enh_label": "Enhance result quality (GFPGAN)",
|
| 98 |
+
"eta_initial": "Time estimate will appear after detecting faces.",
|
| 99 |
+
"fmt_label": "Download file format",
|
| 100 |
+
"run_btn": "Start swapping",
|
| 101 |
+
"download_btn": "Download result",
|
| 102 |
+
"before_after_label": "Before / After",
|
| 103 |
+
"eta_fmt_sec": "Estimated processing time: ~{sec} sec.",
|
| 104 |
+
"eta_fmt_min": "Estimated processing time: ~{min} min {sec} sec.",
|
| 105 |
+
"msg_need_images": "Please upload both images to start.",
|
| 106 |
+
"msg_no_donor_faces": "No faces detected on donor image.",
|
| 107 |
+
"msg_no_target_faces": "No faces detected on target image.",
|
| 108 |
+
"msg_prep": "Preparing for processing...",
|
| 109 |
+
"msg_swap_step": "Replacing face {i} of {n}",
|
| 110 |
+
"msg_enh": "Enhancing quality (GFPGAN)...",
|
| 111 |
+
"msg_done": "Done. Faces processed: {n}.",
|
| 112 |
+
"progress_done": "Done!",
|
| 113 |
+
"donor_option": "Donor {i}",
|
| 114 |
+
"target_option": "Face {i}",
|
| 115 |
+
},
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
def T(lang: str, key: str, **kwargs) -> str:
|
| 119 |
+
s = TEXTS[lang][key]
|
| 120 |
+
return s.format(**kwargs)
|
| 121 |
+
|
| 122 |
+
# --- Основная логика обработки ---
|
| 123 |
+
def detect_faces_generic(img):
|
| 124 |
+
if img is None:
|
| 125 |
+
return [], []
|
| 126 |
+
bgr = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
|
| 127 |
+
faces = app_face.get(bgr)
|
| 128 |
+
faces = sorted(faces, key=lambda f: f.bbox[0])
|
| 129 |
+
previews = []
|
| 130 |
+
for f in faces:
|
| 131 |
+
x1, y1, x2, y2 = map(int, f.bbox)
|
| 132 |
+
h, w = bgr.shape[:2]
|
| 133 |
+
x1 = max(0, x1); y1 = max(0, y1)
|
| 134 |
+
x2 = min(w, x2); y2 = min(h, y2)
|
| 135 |
+
crop = bgr[y1:y2, x1:x2]
|
| 136 |
+
previews.append(cv2.cvtColor(crop, cv2.COLOR_BGR2RGB))
|
| 137 |
+
return previews, faces
|
| 138 |
+
|
| 139 |
+
def update_donor_faces(img, lang: str):
|
| 140 |
+
previews, faces = detect_faces_generic(img)
|
| 141 |
+
updates = []
|
| 142 |
+
for i in range(MAX_PREVIEWS):
|
| 143 |
+
if i < len(previews):
|
| 144 |
+
updates.append(gr.update(value=previews[i], visible=True))
|
| 145 |
+
else:
|
| 146 |
+
updates.append(gr.update(value=None, visible=False))
|
| 147 |
+
labels = [T(lang, "donor_option", i=i + 1) for i in range(len(previews))]
|
| 148 |
+
default = labels[0] if labels else None
|
| 149 |
+
radio_update = gr.update(choices=labels, value=default)
|
| 150 |
+
return updates + [radio_update, faces]
|
| 151 |
+
|
| 152 |
+
def update_target_faces(img, use_enhancer: bool, lang: str):
|
| 153 |
+
previews, faces = detect_faces_generic(img)
|
| 154 |
+
updates = []
|
| 155 |
+
for i in range(MAX_PREVIEWS):
|
| 156 |
+
if i < len(previews):
|
| 157 |
+
updates.append(gr.update(value=previews[i], visible=True))
|
| 158 |
+
else:
|
| 159 |
+
updates.append(gr.update(value=None, visible=False))
|
| 160 |
+
labels = [T(lang, "target_option", i=i + 1) for i in range(len(previews))]
|
| 161 |
+
default = labels if labels else []
|
| 162 |
+
checkbox_update = gr.update(choices=labels, value=default)
|
| 163 |
+
eta_text = estimate_time(lang, len(faces), use_enhancer)
|
| 164 |
+
return updates + [checkbox_update, faces, eta_text]
|
| 165 |
+
|
| 166 |
+
def estimate_time(lang: str, num_faces: int, use_enhancer: bool) -> str:
|
| 167 |
+
if num_faces <= 0:
|
| 168 |
+
return TEXTS[lang]["eta_initial"]
|
| 169 |
+
base_per_face = 4 if has_gpu else 10
|
| 170 |
+
extra_per_face = 6 if use_enhancer else 0
|
| 171 |
+
total = num_faces * (base_per_face + extra_per_face)
|
| 172 |
+
if total < 60:
|
| 173 |
+
return T(lang, "eta_fmt_sec", sec=int(total))
|
| 174 |
+
m = int(total // 60)
|
| 175 |
+
s = int(total % 60)
|
| 176 |
+
return T(lang, "eta_fmt_min", min=m, sec=s)
|
| 177 |
+
|
| 178 |
+
# --- Интерфейс Gradio ---
|
| 179 |
+
custom_css = """
|
| 180 |
+
.gradio-container { max-width: 1200px; margin: 0 auto; }
|
| 181 |
+
.step-card { background: radial-gradient(circle at top left, #0b1220, #020617); border-radius: 18px; padding: 18px 20px; }
|
| 182 |
+
.face-thumb img { width: 100%; height: 100%; object-fit: cover; border-radius: 8px; }
|
| 183 |
+
.progress-ring { width: 120px; height: 120px; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
|
| 184 |
+
.progress-ring-text { font-size: 14px; color: #e5e7eb; text-align: center; }
|
| 185 |
+
"""
|
| 186 |
+
|
| 187 |
+
premium_theme = gr.themes.Default(
|
| 188 |
+
primary_hue="violet",
|
| 189 |
+
neutral_hue="slate",
|
| 190 |
+
).set(
|
| 191 |
+
body_text_size="16px",
|
| 192 |
+
button_large_padding="12px 22px",
|
| 193 |
+
)
|
| 194 |
+
|
| 195 |
+
with gr.Blocks(title="FaceSwap Pro", css=custom_css) as demo:
|
| 196 |
+
lang_state = gr.State("ru")
|
| 197 |
+
lang_radio = gr.Radio(choices=["RU", "EN"], value="RU", label="Язык / Language")
|
| 198 |
+
title_md = gr.Markdown(TEXTS["ru"]["title_md"])
|
| 199 |
+
|
| 200 |
+
with gr.Row():
|
| 201 |
+
with gr.Column():
|
| 202 |
+
donor_img = gr.Image(label="Фото донора", type="numpy")
|
| 203 |
+
donor_previews = [gr.Image(visible=False, interactive=False, width=64, height=64) for _ in range(MAX_PREVIEWS)]
|
| 204 |
+
donor_choice = gr.Radio(label="Выберите лицо-донора", choices=[])
|
| 205 |
+
with gr.Column():
|
| 206 |
+
target_img = gr.Image(label="Целевое фото", type="numpy")
|
| 207 |
+
target_previews = [gr.Image(visible=False, interactive=False, width=64, height=64) for _ in range(MAX_PREVIEWS)]
|
| 208 |
+
target_choices = gr.CheckboxGroup(label="Выберите лица для замены", choices=[])
|
| 209 |
+
|
| 210 |
+
with gr.Row():
|
| 211 |
+
use_enh = gr.Checkbox(label="Улучшить качество результата (GFPGAN)", value=True)
|
| 212 |
+
fmt = gr.Dropdown(label="Формат файла для скачивания", choices=["png", "jpeg", "webp"], value="png")
|
| 213 |
+
run_btn = gr.Button("Запустить замену", variant="primary")
|
| 214 |
+
download_btn = gr.DownloadButton("Скачать результат")
|
| 215 |
+
|
| 216 |
+
result_img = gr.Image(label="Результат", interactive=False)
|
| 217 |
+
status_md = gr.Markdown("")
|
| 218 |
+
before_after = gr.Gallery(label="До / После", columns=2)
|
| 219 |
+
|
| 220 |
+
def switch_language(choice):
|
| 221 |
+
lang = "ru" if choice == "RU" else "en"
|
| 222 |
+
t = TEXTS[lang]
|
| 223 |
+
return (
|
| 224 |
+
lang,
|
| 225 |
+
gr.update(value=t["title_md"]),
|
| 226 |
+
gr.update(label=t["step1_input_label"]),
|
| 227 |
+
gr.update(label=t["step1_donor_choice_label"]),
|
| 228 |
+
gr.update(label=t["step2_input_label"]),
|
| 229 |
+
gr.update(label=t["step2_target_choices_label"]),
|
| 230 |
+
gr.update(label=t["use_enh_label"]),
|
| 231 |
+
gr.update(label=t["fmt_label"]),
|
| 232 |
+
gr.update(value=t["run_btn"]),
|
| 233 |
+
gr.update(value=t["download_btn"]),
|
| 234 |
+
)
|
| 235 |
+
|
| 236 |
+
lang_radio.change(
|
| 237 |
+
fn=switch_language,
|
| 238 |
+
inputs=lang_radio,
|
| 239 |
+
outputs=[
|
| 240 |
+
lang_state,
|
| 241 |
+
title_md,
|
| 242 |
+
donor_img,
|
| 243 |
+
donor_choice,
|
| 244 |
+
target_img,
|
| 245 |
+
target_choices,
|
| 246 |
+
use_enh,
|
| 247 |
+
fmt,
|
| 248 |
+
run_btn,
|
| 249 |
+
download_btn,
|
| 250 |
+
],
|
| 251 |
+
)
|
| 252 |
+
|
| 253 |
+
donor_img.change(
|
| 254 |
+
fn=update_donor_faces,
|
| 255 |
+
inputs=[donor_img, lang_state],
|
| 256 |
+
outputs=donor_previews + [donor_choice],
|
| 257 |
+
)
|
| 258 |
+
|
| 259 |
+
target_img.change(
|
| 260 |
+
fn=update_target_faces,
|
| 261 |
+
inputs=[target_img, use_enh, lang_state],
|
| 262 |
+
outputs=target_previews + [target_choices],
|
| 263 |
+
)
|
| 264 |
+
|
| 265 |
+
demo.launch(
|
| 266 |
+
share=False,
|
| 267 |
+
ssr_mode=False,
|
| 268 |
+
theme=premium_theme,
|
| 269 |
+
css=custom_css
|
| 270 |
+
)
|
packages.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
libglib2.0-0
|
| 2 |
+
libgl1
|
requirements.txt
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
torch==2.4.1
|
| 2 |
+
torchvision==0.19.1
|
| 3 |
+
gradio==4.36.1
|
| 4 |
+
insightface==0.7.3
|
| 5 |
+
onnxruntime==1.16.3
|
| 6 |
+
opencv-python-headless==4.8.1.78
|
| 7 |
+
gfpgan==1.3.8
|
| 8 |
+
basicsr==1.3.4.2
|
| 9 |
+
facexlib==0.3.0
|
| 10 |
+
Pillow==10.3.0
|
| 11 |
+
huggingface_hub==0.23.0
|