import os import cv2 import numpy as np from PIL import Image import urllib.request import gradio as gr import insightface from insightface.app import FaceAnalysis from gfpgan import GFPGANer # --- Загрузка моделей при первом старте --- def download_file(url: str, filename: str): if not os.path.exists(filename): print(f"Скачиваю {filename} из {url}...") urllib.request.urlretrieve(url, filename) print(f"{filename} скачан.") # Пути к моделям INSWAPPER_URL = "https://huggingface.co/ezioruan/inswapper_128.onnx/resolve/main/inswapper_128.onnx" INSWAPPER_PATH = "inswapper_128.onnx" GFPGAN_URL = "https://github.com/TencentARC/GFPGAN/releases/download/v1.3.0/GFPGANv1.3.pth" GFPGAN_PATH = "GFPGANv1.3.pth" download_file(INSWAPPER_URL, INSWAPPER_PATH) download_file(GFPGAN_URL, GFPGAN_PATH) assert os.path.exists(INSWAPPER_PATH), "inswapper_128.onnx не найден." assert os.path.exists(GFPGAN_PATH), "GFPGANv1.3.pth не найден." # --- Инициализация моделей --- try: import torch has_gpu = torch.cuda.is_available() except Exception: has_gpu = False ctx_id = 0 if has_gpu else -1 # 0 = GPU, -1 = CPU print("GPU доступен" if has_gpu else "Работаем на CPU") providers = ['CPUExecutionProvider'] # Только CPU для бесплатного тарифа app_face = FaceAnalysis(name="buffalo_l", providers=providers) app_face.prepare(ctx_id=ctx_id, det_size=(640, 640)) swapper = insightface.model_zoo.get_model(INSWAPPER_PATH, providers=providers) face_enhancer = GFPGANer( model_path=GFPGAN_PATH, upscale=1, arch="clean", channel_multiplier=2, bg_upsampler=None, ) MAX_PREVIEWS = 8 # Максимум маленьких превью # --- Тексты для локализации --- TEXTS = { "ru": { "lang_radio_label": "Язык / Language", "title_md": "# FaceSwap Pro\nСлева — фото **донора**, справа — фото, где нужно заменить лица.", "step1_title": "### 1. Фото донора", "step1_input_label": "Загрузите фото донора (может быть несколько лиц)", "step1_donor_choice_label": "Выберите лицо-донора", "step2_title": "### 2. Фото, которое меняем", "step2_input_label": "Загрузите изображение для замены лиц", "step2_target_choices_label": "Выберите лица для замены (если ничего не выбрано — меняем все)", "step3_title": "### 3. Настройки и экспорт", "use_enh_label": "Улучшить качество результата (GFPGAN)", "eta_initial": "Оценка времени появится после обнаружения лиц.", "fmt_label": "Формат файла для скачивания", "run_btn": "Запустить замену", "download_btn": "Скачать результат", "before_after_label": "До / После", "eta_fmt_sec": "Оценочное время обработки: ~{sec} сек.", "eta_fmt_min": "Оценочное время обработки: ~{min} мин {sec} сек.", "msg_need_images": "Загрузите оба изображения, чтобы начать обработку.", "msg_no_donor_faces": "На фото донора не найдено ни одного лица.", "msg_no_target_faces": "На целевом фото не найдено ни одного лица.", "msg_prep": "Подготовка к обработке...", "msg_swap_step": "Замена лица {i} из {n}", "msg_enh": "Улучшение качества (GFPGAN)...", "msg_done": "Готово. Обработано лиц: {n}.", "progress_done": "Готово!", "donor_option": "Донор {i}", "target_option": "Лицо {i}", }, "en": { "lang_radio_label": "Language / Язык", "title_md": "# FaceSwap Pro\nLeft — **donor photo**, right — photo where faces will be replaced.", "step1_title": "### 1. Donor photo", "step1_input_label": "Upload donor photo (can contain multiple faces)", "step1_donor_choice_label": "Choose donor face", "step2_title": "### 2. Target photo", "step2_input_label": "Upload image where faces will be replaced", "step2_target_choices_label": "Choose faces to replace (if none selected — replace all)", "step3_title": "### 3. Settings & Export", "use_enh_label": "Enhance result quality (GFPGAN)", "eta_initial": "Time estimate will appear after detecting faces.", "fmt_label": "Download file format", "run_btn": "Start swapping", "download_btn": "Download result", "before_after_label": "Before / After", "eta_fmt_sec": "Estimated processing time: ~{sec} sec.", "eta_fmt_min": "Estimated processing time: ~{min} min {sec} sec.", "msg_need_images": "Please upload both images to start.", "msg_no_donor_faces": "No faces detected on donor image.", "msg_no_target_faces": "No faces detected on target image.", "msg_prep": "Preparing for processing...", "msg_swap_step": "Replacing face {i} of {n}", "msg_enh": "Enhancing quality (GFPGAN)...", "msg_done": "Done. Faces processed: {n}.", "progress_done": "Done!", "donor_option": "Donor {i}", "target_option": "Face {i}", }, } def T(lang: str, key: str, **kwargs) -> str: s = TEXTS[lang][key] return s.format(**kwargs) # --- Основная логика обработки --- def detect_faces_generic(img): if img is None: return [], [] bgr = cv2.cvtColor(img, cv2.COLOR_RGB2BGR) faces = app_face.get(bgr) faces = sorted(faces, key=lambda f: f.bbox[0]) previews = [] for f in faces: x1, y1, x2, y2 = map(int, f.bbox) h, w = bgr.shape[:2] x1 = max(0, x1); y1 = max(0, y1) x2 = min(w, x2); y2 = min(h, y2) crop = bgr[y1:y2, x1:x2] previews.append(cv2.cvtColor(crop, cv2.COLOR_BGR2RGB)) return previews, faces def update_donor_faces(img, lang: str): previews, faces = detect_faces_generic(img) updates = [] for i in range(MAX_PREVIEWS): if i < len(previews): updates.append(gr.update(value=previews[i], visible=True)) else: updates.append(gr.update(value=None, visible=False)) labels = [T(lang, "donor_option", i=i + 1) for i in range(len(previews))] default = labels[0] if labels else None radio_update = gr.update(choices=labels, value=default) return updates + [radio_update, faces] def update_target_faces(img, use_enhancer: bool, lang: str): previews, faces = detect_faces_generic(img) updates = [] for i in range(MAX_PREVIEWS): if i < len(previews): updates.append(gr.update(value=previews[i], visible=True)) else: updates.append(gr.update(value=None, visible=False)) labels = [T(lang, "target_option", i=i + 1) for i in range(len(previews))] default = labels if labels else [] checkbox_update = gr.update(choices=labels, value=default) eta_text = estimate_time(lang, len(faces), use_enhancer) return updates + [checkbox_update, faces, eta_text] def estimate_time(lang: str, num_faces: int, use_enhancer: bool) -> str: if num_faces <= 0: return TEXTS[lang]["eta_initial"] base_per_face = 4 if has_gpu else 10 extra_per_face = 6 if use_enhancer else 0 total = num_faces * (base_per_face + extra_per_face) if total < 60: return T(lang, "eta_fmt_sec", sec=int(total)) m = int(total // 60) s = int(total % 60) return T(lang, "eta_fmt_min", min=m, sec=s) # --- Интерфейс Gradio --- custom_css = """ .gradio-container { max-width: 1200px; margin: 0 auto; } .step-card { background: radial-gradient(circle at top left, #0b1220, #020617); border-radius: 18px; padding: 18px 20px; } .face-thumb img { width: 100%; height: 100%; object-fit: cover; border-radius: 8px; } .progress-ring { width: 120px; height: 120px; border-radius: 50%; display: flex; align-items: center; justify-content: center; } .progress-ring-text { font-size: 14px; color: #e5e7eb; text-align: center; } """ premium_theme = gr.themes.Default( primary_hue="violet", neutral_hue="slate", ).set( body_text_size="16px", button_large_padding="12px 22px", ) with gr.Blocks(title="FaceSwap Pro", css=custom_css) as demo: lang_state = gr.State("ru") lang_radio = gr.Radio(choices=["RU", "EN"], value="RU", label="Язык / Language") title_md = gr.Markdown(TEXTS["ru"]["title_md"]) with gr.Row(): with gr.Column(): donor_img = gr.Image(label="Фото донора", type="numpy") donor_previews = [gr.Image(visible=False, interactive=False, width=64, height=64) for _ in range(MAX_PREVIEWS)] donor_choice = gr.Radio(label="Выберите лицо-донора", choices=[]) with gr.Column(): target_img = gr.Image(label="Целевое фото", type="numpy") target_previews = [gr.Image(visible=False, interactive=False, width=64, height=64) for _ in range(MAX_PREVIEWS)] target_choices = gr.CheckboxGroup(label="Выберите лица для замены", choices=[]) with gr.Row(): use_enh = gr.Checkbox(label="Улучшить качество результата (GFPGAN)", value=True) fmt = gr.Dropdown(label="Формат файла для скачивания", choices=["png", "jpeg", "webp"], value="png") run_btn = gr.Button("Запустить замену", variant="primary") download_btn = gr.DownloadButton("Скачать результат") result_img = gr.Image(label="Результат", interactive=False) status_md = gr.Markdown("") before_after = gr.Gallery(label="До / После", columns=2) def switch_language(choice): lang = "ru" if choice == "RU" else "en" t = TEXTS[lang] return ( lang, gr.update(value=t["title_md"]), gr.update(label=t["step1_input_label"]), gr.update(label=t["step1_donor_choice_label"]), gr.update(label=t["step2_input_label"]), gr.update(label=t["step2_target_choices_label"]), gr.update(label=t["use_enh_label"]), gr.update(label=t["fmt_label"]), gr.update(value=t["run_btn"]), gr.update(value=t["download_btn"]), ) lang_radio.change( fn=switch_language, inputs=lang_radio, outputs=[ lang_state, title_md, donor_img, donor_choice, target_img, target_choices, use_enh, fmt, run_btn, download_btn, ], ) donor_img.change( fn=update_donor_faces, inputs=[donor_img, lang_state], outputs=donor_previews + [donor_choice], ) target_img.change( fn=update_target_faces, inputs=[target_img, use_enh, lang_state], outputs=target_previews + [target_choices], ) demo.launch( share=False, ssr_mode=False, theme=premium_theme, css=custom_css )