PixelFree / app.py
minotrey's picture
Fix Hugging Face Spaces compatibility
ad395bd
raw
history blame
44.6 kB
"""
PixelFree - AI Image Processing Service
HuggingFace Spaces용 Gradio 애플리케이션
Polygom API를 활용한 이미지 처리 서비스
Features:
- 배경 제거 (Background Removal)
- 배경 생성 (Background Generation)
- 얼굴 교체 (Face Swap)
Version: 3.0.0
Version 3: Simplified Multi-Mode Workflow
Author: PixelFree Team
"""
import gradio as gr
import requests
import base64
import io
import logging
import tempfile
from PIL import Image
from typing import Optional, Tuple
from datetime import datetime
from collections import OrderedDict
import time
import os
# ===== Configuration =====
API_BASE_URL = os.environ.get("API_BASE_URL")
API_ENDPOINTS = {
"background_removal": os.environ.get("BACKGROUND_REMOVAL_ENDPOINT"),
"background_generation": os.environ.get("BACKGROUND_GENERATION_ENDPOINT"),
"face_swap": os.environ.get("FACE_SWAP_ENDPOINT"),
"video_generation": os.environ.get("VIDEO_GEN"),
}
VIDEO_PROMPT = os.environ.get("VIDEO_PROMPT", "환하게 웃으며 눈을 깜빡인다.")
# 허용된 사용자 목록 (Hugging Face Secrets에서 가져오기)
ALLOWED_USERS = os.environ.get("ALLOWED_USERS", "").split(",")
ALLOWED_USERS = [user.strip() for user in ALLOWED_USERS if user.strip()]
IMAGE_HEIGHT = 500
GALLERY_COLUMNS = 3
GALLERY_ROWS = 3
logging.basicConfig(level=logging.WARNING)
logger = logging.getLogger(__name__)
IMAGE_CACHE_LIMIT = 24
image_cache: "OrderedDict[str, Image.Image]" = OrderedDict()
def cache_image(path: str, image: Image.Image) -> None:
"""Add image to global cache with simple LRU eviction."""
if path in image_cache:
image_cache.move_to_end(path)
image_cache[path] = image
while len(image_cache) > IMAGE_CACHE_LIMIT:
image_cache.popitem(last=False)
output_history = []
def ensure_png_image(image: Optional[Image.Image]) -> Optional[Image.Image]:
"""Ensure provided PIL image is stored as a PNG image (returns copy)."""
if image is None:
return None
working_image = image
if working_image.mode not in ("RGB", "RGBA"):
working_image = working_image.convert("RGBA")
buffer = io.BytesIO()
# Convert to PNG while preserving alpha when present
target_mode = "RGBA" if working_image.mode == "RGBA" else "RGB"
working_image.convert(target_mode).save(buffer, format="PNG")
buffer.seek(0)
with Image.open(buffer) as png_image:
return png_image.convert("RGBA").copy()
def make_thumbnail_image(image: Optional[Image.Image], size: Tuple[int, int] = (256, 256), *, fill_color=(255, 255, 255, 0)) -> Optional[Image.Image]:
"""Create a thumbnail copy of the provided image for gallery previews."""
base_image = ensure_png_image(image)
if base_image is None:
return None
preview = base_image.copy()
try:
preview.thumbnail(size, Image.Resampling.LANCZOS)
except AttributeError:
preview.thumbnail(size)
canvas = Image.new("RGBA", size, fill_color)
offset = ((size[0] - preview.width) // 2, (size[1] - preview.height) // 2)
canvas.paste(preview, offset, preview if preview.mode == "RGBA" else None)
return canvas
def add_to_history(image: Optional[Image.Image], workspace: str) -> Optional[str]:
"""Append processed image to history gallery with descriptive PNG name."""
if image is None:
return None
png_image = ensure_png_image(image)
if png_image is None:
return None
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
prefix_map = {
"bg_remove": "background_removed",
"bg_gen": "background_generated",
"face_swap": "face_swap",
"video": "video_generated",
}
prefix = prefix_map.get(workspace, "result")
file_name = f"{prefix}_{timestamp}.png"
output_history.append((png_image, file_name))
if len(output_history) > 20:
del output_history[:-20]
return file_name
def get_history_items(limit: Optional[int] = None):
"""Return history items (image, filename) with newest first."""
if limit is None:
data = output_history[:]
elif limit <= 0:
return []
else:
data = output_history[-limit:]
return list(reversed(data))
class ImageProcessor:
"""Wrapper around PixelFree backend APIs."""
def __init__(self) -> None:
self.session = requests.Session()
adapter = requests.adapters.HTTPAdapter(
pool_connections=10,
pool_maxsize=20,
max_retries=3,
)
self.session.mount("http://", adapter)
self.session.mount("https://", adapter)
self.session.headers.update({"accept": "application/json"})
def image_to_bytes(self, image: Image.Image, format: str = "PNG", preserve_alpha: bool = False) -> bytes:
buffer = io.BytesIO()
img = image
if not preserve_alpha and img.mode in ("RGBA", "P"):
img = img.convert("RGB")
elif preserve_alpha and img.mode != "RGBA":
img = img.convert("RGBA")
img.save(buffer, format=format)
buffer.seek(0)
return buffer.getvalue()
def base64_to_image(self, base64_str: str) -> Optional[Image.Image]:
try:
if base64_str.startswith("http"):
logger.error("Refusing to download image from URL: %s", base64_str)
return None
if "," in base64_str:
base64_str = base64_str.split(",", 1)[1]
img_data = base64.b64decode(base64_str)
return Image.open(io.BytesIO(img_data))
except Exception as exc:
logger.error("Failed to decode image: %s", exc)
return None
def call_api(self, endpoint: Optional[str], files: dict, data: dict) -> Optional[dict]:
if not endpoint or not API_BASE_URL:
logger.error("API endpoint or base URL missing")
return None
url = f"{API_BASE_URL}{endpoint}"
try:
response = self.session.post(url, files=files, data=data, timeout=300)
logger.info("API response %s: %s", endpoint, response.status_code)
response.raise_for_status()
return response.json()
except Exception as exc:
logger.error("API call failed (%s): %s", endpoint, exc)
return None
def remove_background(self, image: Image.Image) -> Tuple[Optional[Image.Image], str]:
start_time = time.time()
files = {
"file": ("input.png", self.image_to_bytes(image, preserve_alpha=True), "image/png")
}
result = self.call_api(API_ENDPOINTS.get("background_removal"), files, {})
total = time.time() - start_time
if result and result.get("success") and result.get("data"):
b64 = result["data"].get("result_base64")
image_obj = self.base64_to_image(b64) if b64 else None
if image_obj:
return image_obj, f"✅ 배경 제거 완료! (처리 시간: {total:.1f}초)"
return None, f"❌ 배경 제거 실패 (시도 시간: {total:.1f}초)"
def generate_background(
self,
image: Image.Image,
bg_image: Image.Image,
hq_mode: bool = False,
) -> Tuple[Optional[Image.Image], str]:
start_time = time.time()
files = {
"image": ("input.png", self.image_to_bytes(image, preserve_alpha=True), "image/png"),
"bg_image": ("background.png", self.image_to_bytes(bg_image), "image/png"),
}
data = {
"jobId": f"job_{int(time.time() * 1000)}",
"hq_mode": "true" if hq_mode else "false",
}
result = self.call_api(API_ENDPOINTS.get("background_generation"), files, data)
total = time.time() - start_time
mode_text = "고품질" if hq_mode else "일반"
if result and result.get("success") and result.get("data"):
payload = result["data"]
if payload.get("imageData"):
image_obj = self.base64_to_image(payload["imageData"])
if image_obj:
return image_obj, f"✅ 배경 생성 완료! (처리 시간: {total:.1f}초, 모드: {mode_text})"
if payload.get("rawDataUrl"):
logger.error("Ignoring rawDataUrl field for security reasons.")
return None, f"❌ 배경 생성 실패 (시도 시간: {total:.1f}초, 모드: {mode_text})"
def swap_face(self, source: Image.Image, target: Image.Image) -> Tuple[Optional[Image.Image], str]:
start_time = time.time()
files = {
"image": ("image.png", self.image_to_bytes(source), "image/png"),
"targetFaceImage": ("targetFace.png", self.image_to_bytes(target), "image/png"),
}
data = {
"jobId": f"face_swap_{int(time.time() * 1000)}"
}
result = self.call_api(API_ENDPOINTS.get("face_swap"), files, data)
total = time.time() - start_time
if result and result.get("success") and result.get("data"):
payload = result["data"]
if payload.get("imageData"):
image_obj = self.base64_to_image(payload["imageData"])
if image_obj:
api_time = payload.get("processingTime", 0)
return image_obj, f"✅ 얼굴 교체 완료! (전체: {total:.1f}초, API: {api_time:.1f}초)"
if payload.get("rawDataUrl"):
logger.error("Ignoring rawDataUrl field for security reasons.")
return None, f"❌ 얼굴 교체 실패 (시도 시간: {total:.1f}초)"
def generate_video(self, image: Optional[Image.Image]) -> Tuple[Optional[str], str]:
if image is None:
return None, "입력 이미지가 필요합니다."
start_time = time.time()
files = {
"image": ("image.png", self.image_to_bytes(image, preserve_alpha=True), "image/png"),
}
data = {
"jobId": f"video_{int(time.time() * 1000)}",
"prompt": VIDEO_PROMPT,
"duration": "5",
}
result = self.call_api(API_ENDPOINTS.get("video_generation"), files, data)
total = time.time() - start_time
if result and result.get("success") and result.get("data"):
payload = result["data"]
video_base64 = payload.get("videoData")
if video_base64:
if "," in video_base64:
video_base64 = video_base64.split(",", 1)[1]
try:
video_bytes = base64.b64decode(video_base64)
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") as temp_video:
temp_video.write(video_bytes)
temp_path = temp_video.name
except Exception as exc:
logger.error("Failed to decode video: %s", exc)
else:
api_time = payload.get("processingTime", 0)
return temp_path, f"✅ 영상 생성 완료! (전체: {total:.1f}초, API: {api_time:.1f}초)"
if payload.get("rawDataUrl"):
logger.error("Ignoring rawDataUrl field for security reasons.")
return None, f"❌ 영상 생성 실패 (시도 시간: {total:.1f}초)"
processor = ImageProcessor()
# 인증 함수
def auth_fn(username: str, password: str) -> bool:
"""
사용자 인증 함수
- username이 허용된 사용자 목록에 있고
- username과 password가 같으면 인증 성공
"""
return username == password and username in ALLOWED_USERS
# 배경 이미지 로드
def load_background_images():
"""배경 이미지를 assets 폴더에서 로드 (Lazy loading)"""
backgrounds = {
"indoor": [],
"outdoor": [],
"indoor_paths": [],
"outdoor_paths": [],
"indoor_pil": [],
"outdoor_pil": []
}
cwd = os.getcwd()
# Indoor 이미지 경로 저장
indoor_path = os.path.join(cwd, "assets", "indoor")
if os.path.exists(indoor_path):
files = sorted([f for f in os.listdir(indoor_path) if f.endswith('.png')])
for img_file in files:
img_path = os.path.join(indoor_path, img_file)
try:
backgrounds["indoor_paths"].append(img_path)
# 갤러리용 썸네일만 생성
with Image.open(img_path) as img:
img = img.convert('RGBA' if 'A' in img.getbands() else 'RGB')
thumb = img.copy()
thumb.thumbnail((240, 240), Image.Resampling.LANCZOS)
backgrounds["indoor"].append(thumb)
backgrounds["indoor_pil"].append(img_path) # 경로 저장
except Exception:
pass
# Outdoor 이미지 로드
outdoor_path = os.path.join(cwd, "assets", "outdoor")
if os.path.exists(outdoor_path):
files = sorted([f for f in os.listdir(outdoor_path) if f.endswith('.png')])
for img_file in files:
img_path = os.path.join(outdoor_path, img_file)
try:
backgrounds["outdoor_paths"].append(img_path)
with Image.open(img_path) as img:
img = img.convert('RGBA' if 'A' in img.getbands() else 'RGB')
thumb = img.copy()
thumb.thumbnail((240, 240), Image.Resampling.LANCZOS)
backgrounds["outdoor"].append(thumb)
backgrounds["outdoor_pil"].append(img_path)
except Exception:
pass
# 이미지가 없는 경우 더미 이미지 생성
if len(backgrounds['indoor']) == 0:
dummy = Image.new('RGB', (240, 240), color=(200, 200, 200))
backgrounds["indoor"].append(dummy)
backgrounds["indoor_pil"].append(None)
if len(backgrounds['outdoor']) == 0:
dummy = Image.new('RGB', (240, 240), color=(150, 200, 150))
backgrounds["outdoor"].append(dummy)
backgrounds["outdoor_pil"].append(None)
return backgrounds
bg_images = load_background_images()
# 얼굴 이미지 로드
def load_face_images():
"""얼굴 이미지들을 로드 - PIL 이미지로 직접 로드하여 갤러리에 전달"""
faces = {
"man_pil": [],
"woman_pil": [],
"all_pil": [],
"man_paths": [],
"woman_paths": [],
"all_paths": []
}
cwd = os.getcwd()
# Man 이미지 로드
man_path = os.path.join(cwd, "assets", "faces", "man")
if os.path.exists(man_path):
files = sorted([f for f in os.listdir(man_path) if f.endswith(('.png', '.jpg', '.jpeg'))])
for img_file in files:
img_path = os.path.join(man_path, img_file)
try:
with Image.open(img_path) as img:
img = img.convert('RGBA' if 'A' in img.getbands() else 'RGB')
thumb = img.copy()
thumb.thumbnail((240, 240), Image.Resampling.LANCZOS)
faces["man_pil"].append(thumb)
faces["man_paths"].append(img_path)
except Exception:
pass
# Woman 이미지 로드
woman_path = os.path.join(cwd, "assets", "faces", "woman")
if os.path.exists(woman_path):
files = sorted([f for f in os.listdir(woman_path) if f.endswith(('.png', '.jpg', '.jpeg'))])
for img_file in files:
img_path = os.path.join(woman_path, img_file)
try:
with Image.open(img_path) as img:
img = img.convert('RGBA' if 'A' in img.getbands() else 'RGB')
thumb = img.copy()
thumb.thumbnail((240, 240), Image.Resampling.LANCZOS)
faces["woman_pil"].append(thumb)
faces["woman_paths"].append(img_path)
except Exception:
pass
# 이미지가 없는 경우 더미 이미지 생성
if len(faces['man_pil']) == 0:
for _ in range(3):
dummy = Image.new('RGB', (240, 240), color=(200, 200, 250))
faces["man_pil"].append(dummy)
faces["man_paths"].append(None)
if len(faces['woman_pil']) == 0:
for _ in range(3):
dummy = Image.new('RGB', (240, 240), color=(250, 200, 200))
faces["woman_pil"].append(dummy)
faces["woman_paths"].append(None)
# all 리스트 재구성
faces["all_pil"] = faces["man_pil"] + faces["woman_pil"]
faces["all_paths"] = faces["man_paths"] + faces["woman_paths"]
return faces
face_images = load_face_images()
logger.info(f"Face images loaded - Total: {len(face_images.get('all_pil', []))}, Man: {len(face_images.get('man_pil', []))}, Woman: {len(face_images.get('woman_pil', []))}")
# 이미지를 실제로 로드하는 헬퍼 함수
def load_full_image(path_or_image):
"""경로에서 전체 크기 이미지를 로드 (캐싱 포함)"""
if isinstance(path_or_image, str):
cached = image_cache.get(path_or_image)
if cached is not None:
image_cache.move_to_end(path_or_image)
return cached
try:
with Image.open(path_or_image) as img:
mode = 'RGBA' if 'A' in img.getbands() else 'RGB'
loaded = img.convert(mode).copy()
except Exception as exc:
logger.error(f"Failed to load image {path_or_image}: {exc}")
return None
cache_image(path_or_image, loaded)
return loaded
else:
return path_or_image
def create_app():
"""PixelFree Gradio UI (v3.0.0)."""
# CSS for history gallery image display
custom_css = """
#history-gallery img {
object-fit: contain !important;
height: 100% !important;
width: 100% !important;
}
#history-gallery .thumbnail-item {
height: 150px !important;
}
#history-gallery .grid-wrap {
gap: 10px !important;
}
"""
with gr.Blocks(title="PixelFree v3.0.0", theme=gr.themes.Soft(), css=custom_css) as demo:
# 메인 헤더
gr.Markdown("# 🎨 PixelFree Studio")
# 입력 섹션
gr.Markdown("## 📸 입력 이미지")
input_state = gr.State(None)
bg_removed_state = gr.State(None)
selected_bg_state = gr.State(None)
selected_face_state = gr.State(None)
last_output_state = gr.State(None)
input_image = gr.Image(label="입력 이미지", type="pil", height=540)
# 결과 섹션
gr.Markdown("## ✨ 결과 이미지")
output_image = gr.Image(label="처리된 이미지", type="pil", height=500, interactive=False)
download_btn = gr.DownloadButton(
"📥 사진 다운로드",
value=None,
interactive=False
)
# 비디오 생성 섹션
gr.Markdown("## 🎬 AI 비디오 생성")
with gr.Row():
video_output = gr.Video(
label="생성된 영상",
height=360,
interactive=False,
autoplay=True,
)
with gr.Column():
gr.Markdown("최근 이미지 결과를 기반으로 짧은 클립을 생성합니다.")
generate_video_btn = gr.Button(
"🎬 영상 생성하기",
variant="primary",
size="lg",
interactive=False,
)
# 배경 제거 섹션
gr.Markdown("## 🎯 AI 배경 제거")
remove_bg_btn = gr.Button("🚀 배경 제거 실행", variant="primary", size="lg", interactive=False)
# AI 생성 기능 섹션
gr.Markdown("## 🎨 AI 생성 기능")
with gr.Row():
with gr.Column():
gr.Markdown("### 🏞️ 배경 생성")
with gr.Tabs():
with gr.Tab("🏠 실내"):
indoor_gallery = gr.Gallery(
value=bg_images["indoor"],
columns=4,
show_label=False, interactive=False,
allow_preview=False,
object_fit="contain"
)
with gr.Tab("🌳 아웃도어"):
outdoor_gallery = gr.Gallery(
value=bg_images["outdoor"],
columns=4,
rows=4, show_label=False, interactive=False,
allow_preview=False,
object_fit="contain"
)
hq_checkbox = gr.Checkbox(label="✨ 고품질 모드", value=False)
generate_bg_btn = gr.Button("🎨 배경 생성하기", variant="secondary", interactive=False, size="lg")
with gr.Column():
gr.Markdown("### 👤 얼굴 교체")
with gr.Tabs():
with gr.Tab("👨 남성"):
man_gallery = gr.Gallery(
value=face_images["man_pil"],
columns=4,
rows=2, show_label=False, interactive=False,
allow_preview=False,
object_fit="contain"
)
with gr.Tab("👩 여성"):
woman_gallery = gr.Gallery(
value=face_images["woman_pil"],
columns=4,
rows=2, show_label=False, interactive=False,
allow_preview=False,
object_fit="contain"
)
swap_face_btn = gr.Button("🔄 얼굴 교체 적용", variant="secondary", interactive=False, size="lg")
# Collapsible history section with image grid
with gr.Accordion("📜 작업 히스토리", open=False):
# History gallery with preview enabled
history_gallery = gr.Gallery(
value=[img for img, _ in get_history_items(20)],
label=None,
show_label=False,
columns=5,
rows=4,
height=500,
object_fit="contain",
allow_preview=True,
elem_id="history-gallery",
interactive=False
)
def make_download_update(image: Optional[Image.Image], prefix: str = "pixelfree_output"):
if image is None:
return gr.update(value=None, interactive=False)
filename = f"{prefix}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png"
temp_path = f"/tmp/{filename}"
image.save(temp_path, "PNG")
return gr.update(value=temp_path, interactive=True)
def history_update():
"""Update history gallery with original images."""
items = get_history_items(20)
images = [img for img, _ in items]
return gr.update(value=images)
def disable_all_buttons():
disabled = gr.update(interactive=False)
return disabled, disabled, disabled, disabled
def compute_button_states(
input_img: Optional[Image.Image],
last_output: Optional[Image.Image],
bg_removed: Optional[Image.Image],
selected_bg: Optional[Image.Image],
selected_face: Optional[Image.Image],
):
remove_ready = gr.update(interactive=input_img is not None)
video_ready = gr.update(interactive=input_img is not None)
bg_ready = gr.update(interactive=(bg_removed is not None and selected_bg is not None))
swap_ready = gr.update(
interactive=(selected_face is not None and ((last_output is not None) or (input_img is not None)))
)
return remove_ready, video_ready, bg_ready, swap_ready
def on_upload(new_image: Optional[Image.Image]):
if new_image is None:
gr.Warning("입력 이미지를 업로드해주세요.")
remove_ready, video_ready, bg_ready, swap_ready = disable_all_buttons()
return (
None,
None,
None,
None,
None,
gr.update(value=None),
make_download_update(None),
gr.update(value=None),
remove_ready,
video_ready,
bg_ready,
swap_ready,
history_update(),
)
gr.Info("입력 이미지를 불러왔습니다.")
remove_ready, video_ready, bg_ready, swap_ready = compute_button_states(
new_image,
None,
None,
None,
None,
)
return (
new_image,
None,
None,
None,
None,
gr.update(value=None),
make_download_update(None),
gr.update(value=None),
remove_ready,
video_ready,
bg_ready,
swap_ready,
history_update(),
)
def handle_remove_background(
input_img: Optional[Image.Image],
current_bg_removed: Optional[Image.Image],
selected_bg: Optional[Image.Image],
current_output: Optional[Image.Image],
selected_face: Optional[Image.Image],
):
if input_img is None:
gr.Warning("입력 이미지를 먼저 업로드해주세요.")
remove_ready, video_ready, bg_ready, swap_ready = compute_button_states(
input_img,
current_output,
current_bg_removed,
selected_bg,
selected_face,
)
yield (
current_bg_removed,
current_output,
gr.update(),
make_download_update(current_output),
bg_ready,
swap_ready,
remove_ready,
video_ready,
history_update(),
)
return
remove_disabled, video_disabled, bg_disabled, swap_disabled = disable_all_buttons()
yield (
current_bg_removed,
current_output,
gr.update(),
make_download_update(current_output),
bg_disabled,
swap_disabled,
remove_disabled,
video_disabled,
history_update(),
)
removed, message = processor.remove_background(input_img)
if removed is None:
gr.Warning(message)
remove_ready, video_ready, bg_ready, swap_ready = compute_button_states(
input_img,
current_output,
current_bg_removed,
selected_bg,
selected_face,
)
yield (
current_bg_removed,
current_output,
gr.update(value=current_output),
make_download_update(current_output),
bg_ready,
swap_ready,
remove_ready,
video_ready,
history_update(),
)
return
gr.Info(message)
add_to_history(removed, "bg_remove")
remove_ready, video_ready, bg_ready, swap_ready = compute_button_states(
input_img,
removed,
removed,
selected_bg,
selected_face,
)
yield (
removed,
removed,
gr.update(value=removed),
make_download_update(removed, "background_removed"),
bg_ready,
swap_ready,
remove_ready,
video_ready,
history_update(),
)
def on_generate_video(
last_output: Optional[Image.Image],
input_img: Optional[Image.Image],
current_bg_removed: Optional[Image.Image],
selected_bg: Optional[Image.Image],
selected_face: Optional[Image.Image],
):
base_image = last_output or input_img
if base_image is None:
gr.Warning("입력 이미지를 먼저 업로드해주세요.")
remove_ready, video_ready, bg_ready, swap_ready = compute_button_states(
input_img,
last_output,
current_bg_removed,
selected_bg,
selected_face,
)
yield (
gr.update(value=None),
history_update(),
remove_ready,
video_ready,
bg_ready,
swap_ready,
)
return
remove_disabled, video_disabled, bg_disabled, swap_disabled = disable_all_buttons()
yield (
gr.update(),
history_update(),
remove_disabled,
video_disabled,
bg_disabled,
swap_disabled,
)
video_path, message = processor.generate_video(base_image)
if video_path is None:
gr.Warning(message)
remove_ready, video_ready, bg_ready, swap_ready = compute_button_states(
input_img,
last_output,
current_bg_removed,
selected_bg,
selected_face,
)
yield (
gr.update(value=None),
history_update(),
remove_ready,
video_ready,
bg_ready,
swap_ready,
)
return
gr.Info(message)
add_to_history(base_image, "video")
remove_ready, video_ready, bg_ready, swap_ready = compute_button_states(
input_img,
last_output,
current_bg_removed,
selected_bg,
selected_face,
)
yield (
gr.update(value=video_path),
history_update(),
remove_ready,
video_ready,
bg_ready,
swap_ready,
)
def load_background_image(category: str, index: int) -> Optional[Image.Image]:
key = f"{category}_pil"
paths = bg_images.get(key, [])
if 0 <= index < len(paths):
path = paths[index]
return load_full_image(path)
return None
def on_select_background(
evt: gr.SelectData,
category: str,
bg_removed: Optional[Image.Image],
):
index = getattr(evt, "index", None)
if index is None:
gr.Warning("선택 정보를 확인할 수 없습니다.")
return None, gr.update(interactive=False)
image = load_background_image(category, index)
if image is None:
gr.Warning("선택한 배경 이미지를 불러오지 못했습니다.")
return None, gr.update(interactive=False)
gr.Info("배경 이미지를 선택했습니다.")
ready = gr.update(interactive=bg_removed is not None)
return image, ready
def on_generate_background(
last_output: Optional[Image.Image],
selected_bg: Optional[Image.Image],
hq_mode: bool,
input_img: Optional[Image.Image],
current_bg_removed: Optional[Image.Image],
selected_face: Optional[Image.Image],
):
if last_output is None:
gr.Warning("먼저 배경 제거를 실행해주세요.")
remove_ready, video_ready, bg_ready, swap_ready = compute_button_states(
input_img,
last_output,
current_bg_removed,
selected_bg,
selected_face,
)
yield (
last_output,
gr.update(value=last_output),
make_download_update(last_output),
history_update(),
remove_ready,
video_ready,
bg_ready,
swap_ready,
)
return
if selected_bg is None:
gr.Warning("사용할 배경 이미지를 선택해주세요.")
remove_ready, video_ready, bg_ready, swap_ready = compute_button_states(
input_img,
last_output,
current_bg_removed,
selected_bg,
selected_face,
)
yield (
last_output,
gr.update(value=last_output),
make_download_update(last_output),
history_update(),
remove_ready,
video_ready,
bg_ready,
swap_ready,
)
return
remove_disabled, video_disabled, bg_disabled, swap_disabled = disable_all_buttons()
yield (
last_output,
gr.update(value=last_output),
make_download_update(last_output),
history_update(),
remove_disabled,
video_disabled,
bg_disabled,
swap_disabled,
)
generated, message = processor.generate_background(last_output, selected_bg, hq_mode)
if generated is None:
gr.Warning(message)
remove_ready, video_ready, bg_ready, swap_ready = compute_button_states(
input_img,
last_output,
current_bg_removed,
selected_bg,
selected_face,
)
yield (
last_output,
gr.update(value=last_output),
make_download_update(last_output),
history_update(),
remove_ready,
video_ready,
bg_ready,
swap_ready,
)
return
gr.Info(message)
add_to_history(generated, "bg_gen")
remove_ready, video_ready, bg_ready, swap_ready = compute_button_states(
input_img,
generated,
current_bg_removed,
selected_bg,
selected_face,
)
yield (
generated,
gr.update(value=generated),
make_download_update(generated, "background_generated"),
history_update(),
remove_ready,
video_ready,
bg_ready,
swap_ready,
)
def load_face_image(category: str, index: int) -> Optional[Image.Image]:
key = f"{category}_paths"
paths = face_images.get(key, [])
if 0 <= index < len(paths):
path = paths[index]
return load_full_image(path)
return None
def on_select_face(
evt: gr.SelectData,
category: str,
last_output: Optional[Image.Image],
input_img: Optional[Image.Image],
):
base_image = last_output or input_img
index = getattr(evt, "index", None)
if index is None:
gr.Warning("선택 정보를 확인할 수 없습니다.")
return None, gr.update(interactive=False)
image = load_face_image(category, index)
if image is None:
gr.Warning("선택한 얼굴 이미지를 불러오지 못했습니다.")
return None, gr.update(interactive=False)
gr.Info("얼굴 이미지를 선택했습니다.")
ready = gr.update(interactive=base_image is not None)
return image, ready
def on_swap_face(
last_output: Optional[Image.Image],
input_img: Optional[Image.Image],
selected_face: Optional[Image.Image],
current_bg_removed: Optional[Image.Image],
selected_bg: Optional[Image.Image],
):
base_image = last_output or input_img
if base_image is None:
gr.Warning("입력 이미지를 먼저 업로드해주세요.")
remove_ready, video_ready, bg_ready, swap_ready = compute_button_states(
input_img,
last_output,
current_bg_removed,
selected_bg,
selected_face,
)
yield (
last_output,
gr.update(),
make_download_update(last_output),
history_update(),
remove_ready,
video_ready,
bg_ready,
swap_ready,
)
return
if selected_face is None:
gr.Warning("사용할 얼굴 이미지를 선택해주세요.")
remove_ready, video_ready, bg_ready, swap_ready = compute_button_states(
input_img,
last_output,
current_bg_removed,
selected_bg,
selected_face,
)
yield (
last_output,
gr.update(value=base_image),
make_download_update(last_output),
history_update(),
remove_ready,
video_ready,
bg_ready,
swap_ready,
)
return
remove_disabled, video_disabled, bg_disabled, swap_disabled = disable_all_buttons()
yield (
last_output,
gr.update(value=base_image),
make_download_update(last_output),
history_update(),
remove_disabled,
video_disabled,
bg_disabled,
swap_disabled,
)
swapped, message = processor.swap_face(base_image, selected_face)
if swapped is None:
gr.Warning(message)
remove_ready, video_ready, bg_ready, swap_ready = compute_button_states(
input_img,
last_output,
current_bg_removed,
selected_bg,
selected_face,
)
yield (
last_output,
gr.update(value=base_image),
make_download_update(last_output),
history_update(),
remove_ready,
video_ready,
bg_ready,
swap_ready,
)
return
gr.Info(message)
add_to_history(swapped, "face_swap")
remove_ready, video_ready, bg_ready, swap_ready = compute_button_states(
input_img,
swapped,
current_bg_removed,
selected_bg,
selected_face,
)
yield (
swapped,
gr.update(value=swapped),
make_download_update(swapped, "face_swap"),
history_update(),
remove_ready,
video_ready,
bg_ready,
swap_ready,
)
input_image.change(
on_upload,
inputs=[input_image],
outputs=[
input_state,
bg_removed_state,
selected_bg_state,
selected_face_state,
last_output_state,
output_image,
download_btn,
video_output,
remove_bg_btn,
generate_video_btn,
generate_bg_btn,
swap_face_btn,
history_gallery,
],
)
remove_bg_btn.click(
handle_remove_background,
inputs=[
input_state,
bg_removed_state,
selected_bg_state,
last_output_state,
selected_face_state,
],
outputs=[
bg_removed_state,
last_output_state,
output_image,
download_btn,
generate_bg_btn,
swap_face_btn,
remove_bg_btn,
generate_video_btn,
history_gallery,
],
)
generate_video_btn.click(
on_generate_video,
inputs=[
last_output_state,
input_state,
bg_removed_state,
selected_bg_state,
selected_face_state,
],
outputs=[
video_output,
history_gallery,
remove_bg_btn,
generate_video_btn,
generate_bg_btn,
swap_face_btn,
],
)
indoor_gallery.select(
on_select_background,
inputs=[gr.State("indoor"), bg_removed_state],
outputs=[selected_bg_state, generate_bg_btn],
)
outdoor_gallery.select(
on_select_background,
inputs=[gr.State("outdoor"), bg_removed_state],
outputs=[selected_bg_state, generate_bg_btn],
)
generate_bg_btn.click(
on_generate_background,
inputs=[
last_output_state,
selected_bg_state,
hq_checkbox,
input_state,
bg_removed_state,
selected_face_state,
],
outputs=[
last_output_state,
output_image,
download_btn,
history_gallery,
remove_bg_btn,
generate_video_btn,
generate_bg_btn,
swap_face_btn,
],
)
man_gallery.select(
on_select_face,
inputs=[gr.State("man"), last_output_state, input_state],
outputs=[selected_face_state, swap_face_btn],
)
woman_gallery.select(
on_select_face,
inputs=[gr.State("woman"), last_output_state, input_state],
outputs=[selected_face_state, swap_face_btn],
)
swap_face_btn.click(
on_swap_face,
inputs=[
last_output_state,
input_state,
selected_face_state,
bg_removed_state,
selected_bg_state,
],
outputs=[
last_output_state,
output_image,
download_btn,
history_gallery,
remove_bg_btn,
generate_video_btn,
generate_bg_btn,
swap_face_btn,
],
)
return demo
if __name__ == "__main__":
app = create_app()
app.launch(
share=True,
show_error=True,
auth=auth_fn,
ssr_mode=False
)