Spaces:
Sleeping
Sleeping
| """ | |
| 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 | |
| ) | |