PixelFree / app.py
minotrey's picture
Implement custom Gradio State-based login system
e9dd3c0
"""
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:
# ์„ธ์…˜ ์ƒํƒœ ๊ด€๋ฆฌ
session_user = gr.State(None)
# ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€
with gr.Column(visible=True, elem_id="login-page") as login_page:
gr.Markdown("# ๐Ÿ” PixelFree ๋กœ๊ทธ์ธ")
gr.Markdown("ํ—ˆ๊ฐ€๋œ ์‚ฌ์šฉ์ž๋งŒ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.")
username_input = gr.Textbox(
label="์‚ฌ์šฉ์ž๋ช…",
placeholder="username์„ ์ž…๋ ฅํ•˜์„ธ์š”",
max_lines=1
)
password_input = gr.Textbox(
label="๋น„๋ฐ€๋ฒˆํ˜ธ",
placeholder="password๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”",
type="password",
max_lines=1
)
login_btn = gr.Button("๐Ÿ”“ ๋กœ๊ทธ์ธ", variant="primary", size="lg")
login_msg = gr.Markdown("")
# ๋ฉ”์ธ ํŽ˜์ด์ง€ (์ดˆ๊ธฐ์—๋Š” ์ˆจ๊น€)
with gr.Column(visible=False, elem_id="main-page") as main_page:
# ๋ฉ”์ธ ํ—ค๋”
with gr.Row():
gr.Markdown("# ๐ŸŽจ PixelFree Studio")
logout_btn = gr.Button("๐Ÿšช ๋กœ๊ทธ์•„์›ƒ", variant="secondary", size="sm")
# ์ž…๋ ฅ ์„น์…˜
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,
],
)
# ๋กœ๊ทธ์ธ/๋กœ๊ทธ์•„์›ƒ ํ•ธ๋“ค๋Ÿฌ
def handle_login(username, password):
"""๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ"""
if username in ALLOWED_USERS and username == password:
return (
gr.update(visible=False), # ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ ์ˆจ๊น€
gr.update(visible=True), # ๋ฉ”์ธ ํŽ˜์ด์ง€ ํ‘œ์‹œ
username, # ์„ธ์…˜์— ์‚ฌ์šฉ์ž ์ €์žฅ
gr.update(value="โœ… ๋กœ๊ทธ์ธ ์„ฑ๊ณต!")
)
return (
gr.update(visible=True), # ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ ์œ ์ง€
gr.update(visible=False), # ๋ฉ”์ธ ํŽ˜์ด์ง€ ์ˆจ๊น€
None, # ์„ธ์…˜ ์ดˆ๊ธฐํ™”
gr.update(value="โŒ ๋กœ๊ทธ์ธ ์‹คํŒจ: ์‚ฌ์šฉ์ž๋ช… ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.")
)
def handle_logout():
"""๋กœ๊ทธ์•„์›ƒ ์ฒ˜๋ฆฌ"""
return (
gr.update(visible=True), # ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ ํ‘œ์‹œ
gr.update(visible=False), # ๋ฉ”์ธ ํŽ˜์ด์ง€ ์ˆจ๊น€
None, # ์„ธ์…˜ ์ดˆ๊ธฐํ™”
gr.update(value="") # ๋ฉ”์‹œ์ง€ ์ดˆ๊ธฐํ™”
)
# ๋กœ๊ทธ์ธ/๋กœ๊ทธ์•„์›ƒ ์ด๋ฒคํŠธ ์—ฐ๊ฒฐ
login_btn.click(
handle_login,
inputs=[username_input, password_input],
outputs=[login_page, main_page, session_user, login_msg]
)
logout_btn.click(
handle_logout,
outputs=[login_page, main_page, session_user, login_msg]
)
return demo
if __name__ == "__main__":
app = create_app()
app.launch(
share=True,
show_error=True,
ssr_mode=False
)