diff --git a/.gitattributes b/.gitattributes
index a6344aac8c09253b3b630fb776ae94478aa0275b..e2d9729aae670eb88273b99ec1e813b08ea59162 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
*.zip filter=lfs diff=lfs merge=lfs -text
*.zst filter=lfs diff=lfs merge=lfs -text
*tfevents* filter=lfs diff=lfs merge=lfs -text
+*.png filter=lfs diff=lfs merge=lfs -text
+*.mp4 filter=lfs diff=lfs merge=lfs -text
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..715cf8f27f20188facb12525b5573efe83956e88
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+# Git LFS tracks all binary files
+# See .gitattributes for tracked file types
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..8af2f2399ad120faacc12fb33d166112b65496f3
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,46 @@
+POLYGOM PROPRIETARY SOFTWARE LICENSE
+
+Copyright (c) 2025 Polygom. All Rights Reserved.
+
+This software and associated documentation files (the "Software") are the
+proprietary property of Polygom and are protected by copyright law.
+
+NO PERMISSION IS GRANTED TO:
+- Use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software
+- Use the Software for any commercial or non-commercial purpose
+- Reverse engineer, decompile, or disassemble the Software
+
+This Software contains proprietary AI technology developed by Polygom.
+Any unauthorized use, reproduction, or distribution is strictly prohibited
+and may result in severe civil and criminal penalties.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED. IN NO EVENT SHALL POLYGOM BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE.
+
+For licensing inquiries, please contact Polygom directly.
+
+---
+
+POLYGOM 독점 소프트웨어 라이선스
+
+Copyright (c) 2025 Polygom. 모든 권리 보유.
+
+이 소프트웨어와 관련 문서 파일("소프트웨어")은 Polygom의 독점 자산이며
+저작권법으로 보호됩니다.
+
+다음 행위는 허가되지 않습니다:
+- 소프트웨어의 사용, 복사, 수정, 병합, 게시, 배포, 재라이선스, 판매
+- 상업적 또는 비상업적 목적으로의 소프트웨어 사용
+- 소프트웨어의 역공학, 디컴파일, 역어셈블
+
+이 소프트웨어는 Polygom이 개발한 독점 AI 기술을 포함하고 있습니다.
+무단 사용, 복제 또는 배포는 엄격히 금지되며 심각한 민형사상 처벌을
+받을 수 있습니다.
+
+소프트웨어는 "있는 그대로" 제공되며, 어떠한 명시적 또는 묵시적 보증도
+없습니다. Polygom은 소프트웨어와 관련하여 발생하는 어떠한 청구, 손해
+또는 기타 책임에 대해서도 책임지지 않습니다.
+
+라이선스 문의는 Polygom에 직접 연락하시기 바랍니다.
\ No newline at end of file
diff --git a/README.md b/README.md
index 2e9aab659697eb258a81453ea8a5e92b4da09743..6e11090e4d661ac3939268b0b2eb7c842501c3a5 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,51 @@
---
-title: PixelFree
-emoji: 💻
-colorFrom: blue
-colorTo: purple
+title: PixelFree - AI Image Processing Platform
+emoji: 🎨
+colorFrom: purple
+colorTo: pink
sdk: gradio
-sdk_version: 5.49.0
+sdk_version: 5.45.0
app_file: app.py
pinned: false
-short_description: AI Image Processing
---
-Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
+# PixelFree - AI 이미지 처리 플랫폼
+
+
+
+
+
+
+## 🎯 소개
+
+PixelFree는 **Polygom**의 자체 AI 기술을 활용한 이미지 처리 플랫폼입니다. 배경 제거, 배경 생성, 얼굴 교체 등의 고급 기능을 원클릭으로 제공합니다.
+
+## 📋 주요 기능
+
+### 1. 배경 제거
+- 입력 이미지에서 배경을 자동으로 제거
+- 투명한 PNG 형식으로 결과 반환
+
+### 2. 배경 생성
+- **2단계 프로세스**:
+ 1. 자동 배경 제거
+ 2. 새로운 배경 생성 및 합성
+- 배경 선택 옵션:
+ - Indoor (실내 배경)
+ - Outdoor (실외 배경)
+ - Custom (사용자 업로드)
+
+### 3. 얼굴 교체
+- 참조 얼굴 선택 옵션:
+ - 남자
+ - 여자
+ - 전체
+ - Custom (사용자 업로드)
+
+⚠️ **참고**: 이 API는 Polygom의 독점 기술이며, 별도의 라이선스 계약이 필요할 수 있습니다.
+
+자세한 내용은 [LICENSE](./LICENSE) 파일을 참조하세요.
+
+---
+
+**Powered by Polygom**
\ No newline at end of file
diff --git a/app.py b/app.py
new file mode 100644
index 0000000000000000000000000000000000000000..190e235deb51768682abc59e0efc5a398129401c
--- /dev/null
+++ b/app.py
@@ -0,0 +1,1219 @@
+"""
+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(
+ server_name="0.0.0.0",
+ server_port=7860,
+ share=False,
+ show_error=True,
+ auth=auth_fn
+ )
diff --git a/assets/.DS_Store b/assets/.DS_Store
new file mode 100644
index 0000000000000000000000000000000000000000..50c9f712bc6f431580c3da911f13152186ed07f9
Binary files /dev/null and b/assets/.DS_Store differ
diff --git a/assets/POLYGOM_VIDEO_ver2_original.mp4 b/assets/POLYGOM_VIDEO_ver2_original.mp4
new file mode 100644
index 0000000000000000000000000000000000000000..ec6bb083c68bb0bf03f051f1edc17f8f009c52dc
--- /dev/null
+++ b/assets/POLYGOM_VIDEO_ver2_original.mp4
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7d042e834dac52034113fd56d3e9d950bfb3a85c1386dccfd84d6b3e88c80957
+size 14848608
diff --git a/assets/faces/.DS_Store b/assets/faces/.DS_Store
new file mode 100644
index 0000000000000000000000000000000000000000..c1c123f113de0e79c8ee73666c69fd1a973121df
Binary files /dev/null and b/assets/faces/.DS_Store differ
diff --git a/assets/faces/man/001.png b/assets/faces/man/001.png
new file mode 100644
index 0000000000000000000000000000000000000000..16592afc61fd9fa03a39225d7a9d76f246166d5c
--- /dev/null
+++ b/assets/faces/man/001.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:33cec6e6305c5361a2d82338d71e0f68497c522eaf642cedb1a684b8585a652d
+size 611034
diff --git a/assets/faces/man/002.png b/assets/faces/man/002.png
new file mode 100644
index 0000000000000000000000000000000000000000..bc07f737d15069bf1057babc39fb7d3a48318bf9
--- /dev/null
+++ b/assets/faces/man/002.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f1da5a08e127140e40b2ae6dc379690c56996632b75bc9cb5aef4b422e77e73c
+size 1110727
diff --git a/assets/faces/man/003.png b/assets/faces/man/003.png
new file mode 100644
index 0000000000000000000000000000000000000000..b9477ebb90e94198be879a7b2d40adb39ab5a348
--- /dev/null
+++ b/assets/faces/man/003.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:efa2386f3bb1380f01fd5956db70fecd6118ac1f7a223975a190dbe91c0582fc
+size 1330906
diff --git a/assets/faces/man/004.png b/assets/faces/man/004.png
new file mode 100644
index 0000000000000000000000000000000000000000..f9bd7f1b959e5ff19b6cce9c3106fd910781b985
--- /dev/null
+++ b/assets/faces/man/004.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6cc05b4094c08e0c78a2c9404193c890e9192dc0185b508803ab6d041817427d
+size 1295404
diff --git a/assets/faces/man/005.png b/assets/faces/man/005.png
new file mode 100644
index 0000000000000000000000000000000000000000..62c63baff85b23ad392e533c5925df1248e76474
--- /dev/null
+++ b/assets/faces/man/005.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:52b74f683dbd867094a3d61d4defd34e731619cad190fde9d97aad01a5d461e0
+size 1306470
diff --git a/assets/faces/man/006.png b/assets/faces/man/006.png
new file mode 100644
index 0000000000000000000000000000000000000000..4f2e5c8c0e6b3fc7b35179e50cbf6c620e1b5e81
--- /dev/null
+++ b/assets/faces/man/006.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e09a5409705d48d4fbaa37981ff3ac4aff98b4ae2efe501b88df7b5894472eea
+size 1320689
diff --git a/assets/faces/man/007.png b/assets/faces/man/007.png
new file mode 100644
index 0000000000000000000000000000000000000000..3f084b4c23afbe149154d234509b92e4a645d698
--- /dev/null
+++ b/assets/faces/man/007.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:00c008c2334712baaa40bbfd17b324884322a153b8844716f78952aa23facdee
+size 1072793
diff --git a/assets/faces/man/008.png b/assets/faces/man/008.png
new file mode 100644
index 0000000000000000000000000000000000000000..5d0fcc26826c7ceaf2af2a42da81178ce4ff89d5
--- /dev/null
+++ b/assets/faces/man/008.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2cf8ee93858c559ff8ac1e9f20d1beae4fcef2f9f6b62141edefdf20616d7516
+size 1711648
diff --git a/assets/faces/woman/001.png b/assets/faces/woman/001.png
new file mode 100644
index 0000000000000000000000000000000000000000..d27b5439689d8537b45a028a95c619bf7099f440
--- /dev/null
+++ b/assets/faces/woman/001.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4ba2de2df8f5ab9cb741a857bf2b2eaee10e34067fd81e89009e81ccff12b011
+size 1166268
diff --git a/assets/faces/woman/002.png b/assets/faces/woman/002.png
new file mode 100644
index 0000000000000000000000000000000000000000..c597099f0bab301313b88c70316b9e6d84edda98
--- /dev/null
+++ b/assets/faces/woman/002.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:14ce72d69eb7b16dbf9d09167fe80f02747eee08199039c4eaf9e8695d9e7481
+size 1121384
diff --git a/assets/faces/woman/003.png b/assets/faces/woman/003.png
new file mode 100644
index 0000000000000000000000000000000000000000..7ce3387dd175da0235332a9248ba5dd91dcb0c7c
--- /dev/null
+++ b/assets/faces/woman/003.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4b704f984433b5da3e61d73d840432c204792828ec9e611f98d27ef06ae772ce
+size 1557225
diff --git a/assets/faces/woman/004.png b/assets/faces/woman/004.png
new file mode 100644
index 0000000000000000000000000000000000000000..be1bc3d8fee50cb6ffbfda41581efaeb0199d1a3
--- /dev/null
+++ b/assets/faces/woman/004.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e30ed1a55089a8e0b1a91471858edfff47cce2a2696bcfed2bec1ba1223ce8a6
+size 1085956
diff --git a/assets/faces/woman/005.png b/assets/faces/woman/005.png
new file mode 100644
index 0000000000000000000000000000000000000000..d7bc5b8e0077173097c373d92b9ee3ccbe7aa665
--- /dev/null
+++ b/assets/faces/woman/005.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3bc6782310cfc93bf5ba905e8a50de93418535f994e7f4df0935b028bc348aa4
+size 1174661
diff --git a/assets/faces/woman/006.png b/assets/faces/woman/006.png
new file mode 100644
index 0000000000000000000000000000000000000000..bf31e8961259a5574bbabceb8ac956d734370c84
--- /dev/null
+++ b/assets/faces/woman/006.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5765c0da746e4b00ed357fc6158aad4880dcc8935e29e589e4b131b5b2cdb840
+size 1410455
diff --git a/assets/faces/woman/007.png b/assets/faces/woman/007.png
new file mode 100644
index 0000000000000000000000000000000000000000..4a8d5f5b3959067baf5f1248b917ada777de9f00
--- /dev/null
+++ b/assets/faces/woman/007.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4f870c70f38f8d8387821adcda60ee922c144163e34444ae313cf7b812807945
+size 1593121
diff --git a/assets/faces/woman/008.png b/assets/faces/woman/008.png
new file mode 100644
index 0000000000000000000000000000000000000000..ade50d9bbd25dfeb3155aa80bec799fc1beaea89
--- /dev/null
+++ b/assets/faces/woman/008.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f13c4cbb89ff372eff01bc589930c8f0919c6543691c0f5819cc6b64581940cc
+size 1683328
diff --git a/assets/indoor/001.png b/assets/indoor/001.png
new file mode 100644
index 0000000000000000000000000000000000000000..4b0ded551955da64a5f80ba8dee5dfe3f34a102f
--- /dev/null
+++ b/assets/indoor/001.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e86be7c59d59a1e11e4fc0df8271944c6ef8cc9384069f3db245edd857ef11a2
+size 746759
diff --git a/assets/indoor/003.png b/assets/indoor/003.png
new file mode 100644
index 0000000000000000000000000000000000000000..dc63b751817e59fed0d44935f6c6b45b0b113497
--- /dev/null
+++ b/assets/indoor/003.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6dab739162520e710ec74e1a848e02d4dc33a76808190496320de1bd99bc0e61
+size 838981
diff --git a/assets/indoor/004.png b/assets/indoor/004.png
new file mode 100644
index 0000000000000000000000000000000000000000..b4670f97106c089cf4af560475da2d443672a3b6
--- /dev/null
+++ b/assets/indoor/004.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:06bdd47916183ab371d324a5786967f4d75b320e80e4ad2c814e303ed7867b94
+size 2993568
diff --git a/assets/indoor/005.png b/assets/indoor/005.png
new file mode 100644
index 0000000000000000000000000000000000000000..52e25c3b439d61c7b6428822535c4d91d88aa0fb
--- /dev/null
+++ b/assets/indoor/005.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a8d984cee48f2f12af6bbc5248fee7d4cd209071a5deb4dc32cf25a00bb0605a
+size 2010555
diff --git a/assets/indoor/006.png b/assets/indoor/006.png
new file mode 100644
index 0000000000000000000000000000000000000000..1f4578901e345d9b3e744b8ad2ff63ad57ded4a0
--- /dev/null
+++ b/assets/indoor/006.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cabbfe0546a772325f536cb38be24628fb9eded81bd7fe33d8e705cfbb488ed5
+size 1837065
diff --git a/assets/indoor/007.png b/assets/indoor/007.png
new file mode 100644
index 0000000000000000000000000000000000000000..72e9e0be99d939c8cc1e94d3b79d3204353fd8b4
--- /dev/null
+++ b/assets/indoor/007.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:644ffe8138c36261afa0d49d3e596992b4a7372d6ee76236902e887d2e14adb7
+size 608348
diff --git a/assets/indoor/008.png b/assets/indoor/008.png
new file mode 100644
index 0000000000000000000000000000000000000000..7100112a10f3a96eceb2026e2c86a540e5889d82
--- /dev/null
+++ b/assets/indoor/008.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:125536abd0eae586cd66604e56fef8932e482ae7d40fd6d1e5defbf4bde922fd
+size 605223
diff --git a/assets/indoor/009.png b/assets/indoor/009.png
new file mode 100644
index 0000000000000000000000000000000000000000..635a33283fc71450cd42f37015fcb9fee25d9153
--- /dev/null
+++ b/assets/indoor/009.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:31c10cc449cd49e420acf46dbbd3b89d2ed62986ac521420521a18ad2c7c42ad
+size 2595931
diff --git a/assets/indoor/010.png b/assets/indoor/010.png
new file mode 100644
index 0000000000000000000000000000000000000000..b392d6d00ba69f82d9e3fb90390c0214308af4eb
--- /dev/null
+++ b/assets/indoor/010.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:11ce561a723ff4c73ff1efbc1eddbabdb27e7743f5d7a2848f5492471ec94dd4
+size 811846
diff --git a/assets/indoor/011.png b/assets/indoor/011.png
new file mode 100644
index 0000000000000000000000000000000000000000..6ee54597836955715987e521c2dc1a4fa98a9820
--- /dev/null
+++ b/assets/indoor/011.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d0491a348dfb9221028f087e20df8107b7c3b9229ad8c07e9c7befaa4b6f60a1
+size 4475668
diff --git a/assets/indoor/012.png b/assets/indoor/012.png
new file mode 100644
index 0000000000000000000000000000000000000000..c4a0c5303c4f18ab4fe7fcdcd5f54e7adf074cb6
--- /dev/null
+++ b/assets/indoor/012.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a2d556e9a3934b6e89ac987624ebefc58e639ff60af58bcf05bde5956861dd8e
+size 996212
diff --git a/assets/outdoor/001.png b/assets/outdoor/001.png
new file mode 100644
index 0000000000000000000000000000000000000000..879f4a2d206532f1755559a778cf94c24ace6c14
--- /dev/null
+++ b/assets/outdoor/001.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:97d7a17e2ac12a8bb1c03b6da93e946b35f43f829fa0220420006416ce121071
+size 1102947
diff --git a/assets/outdoor/002.png b/assets/outdoor/002.png
new file mode 100644
index 0000000000000000000000000000000000000000..a9625ebbefc60e1e4ad4de6cc0d9b35d37551da8
--- /dev/null
+++ b/assets/outdoor/002.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:be0ec40dcb88bdf714274213671656e9bdad8f02782eff5c9cdc00b6f2a0c7e0
+size 1185045
diff --git a/assets/outdoor/003.png b/assets/outdoor/003.png
new file mode 100644
index 0000000000000000000000000000000000000000..abba4aeab16e87e76d020bb0c7e4d5ef13e34121
--- /dev/null
+++ b/assets/outdoor/003.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2636192cbb8bf83d5679306e7a41693a09c5d9abb2916cd9c8585da057cf3024
+size 480306
diff --git a/assets/outdoor/004.png b/assets/outdoor/004.png
new file mode 100644
index 0000000000000000000000000000000000000000..27cb1094c18e39401dee528c3940738016d889eb
--- /dev/null
+++ b/assets/outdoor/004.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:133c92ee439d10cee08187cbe10dd82d8b96c725a38224473cb2988f559eca6d
+size 6038945
diff --git a/assets/outdoor/005.png b/assets/outdoor/005.png
new file mode 100644
index 0000000000000000000000000000000000000000..c8a63d0e791543ca7c4a41b2dfe87c969abf86cc
--- /dev/null
+++ b/assets/outdoor/005.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4a3fa1a5c3d343069141772fb11713931d519b7d1622a8abc6cd0ee2f31a6e5a
+size 1548042
diff --git a/assets/outdoor/006.png b/assets/outdoor/006.png
new file mode 100644
index 0000000000000000000000000000000000000000..f54c0c4eb1284db9ec8ddb205b8a4df3766b0a0d
--- /dev/null
+++ b/assets/outdoor/006.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2bfc4fc06db1d570c0e40ca438d1d2b13d0b5b292ae36d247f71439bf232ca4e
+size 1424646
diff --git a/assets/outdoor/007.png b/assets/outdoor/007.png
new file mode 100644
index 0000000000000000000000000000000000000000..363133eaebc3b9fab40a1f4b82a35d4e34d6e24b
--- /dev/null
+++ b/assets/outdoor/007.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:77fd0afe2bc58b51a8d90932653ab79fa57e4515ea8ee46e8e99ccc0c999392b
+size 1561655
diff --git a/assets/outdoor/008.png b/assets/outdoor/008.png
new file mode 100644
index 0000000000000000000000000000000000000000..4b7fafbbbac064b54dd0a49ca2f2195eb9e36f56
--- /dev/null
+++ b/assets/outdoor/008.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:15e09295e6e45926d00ef0daf690b210bc903c74727063650a5f8916028f1d69
+size 1737899
diff --git a/assets/outdoor/009.png b/assets/outdoor/009.png
new file mode 100644
index 0000000000000000000000000000000000000000..0169eeb66cc4a89ac030377e52ade976c20ab385
--- /dev/null
+++ b/assets/outdoor/009.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2de3552341d13faccfbccf0e1e2810f54bc197cf8c62db9a003a5f5b9f89e84d
+size 1253017
diff --git a/assets/outdoor/010.png b/assets/outdoor/010.png
new file mode 100644
index 0000000000000000000000000000000000000000..4cd86aeb6378efec9f37b67b04256c53d234a6e8
--- /dev/null
+++ b/assets/outdoor/010.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2964dbea528fdc10b044e51754bce6ac155a9e9edf83e05f9e403e90a8cc6adb
+size 1303435
diff --git a/assets/outdoor/011.png b/assets/outdoor/011.png
new file mode 100644
index 0000000000000000000000000000000000000000..9e0d666e894dcd9a28126b3d548e56be66398e8d
--- /dev/null
+++ b/assets/outdoor/011.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5bb7ff6156c13a1620384108bb410e14391c5f6c54f64aa984162fb8e040c09e
+size 1413134
diff --git a/assets/outdoor/012.png b/assets/outdoor/012.png
new file mode 100644
index 0000000000000000000000000000000000000000..9086b7658c511470ba1e831094c9b714667d3826
--- /dev/null
+++ b/assets/outdoor/012.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:53f66cabade7f37b3f7ed5223a2149fd616b70ea11d0acee3d20a9d3441d4ffd
+size 1013801
diff --git a/assets/outdoor/013.png b/assets/outdoor/013.png
new file mode 100644
index 0000000000000000000000000000000000000000..2f3cd1985682e1f27ccbc3d9e6cc0c846de6794c
--- /dev/null
+++ b/assets/outdoor/013.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1c0136bf396eba40cfb7ab00ae3ad6377703bad1f7d249120d3b7e5a80bf1d93
+size 1325034
diff --git a/assets/outdoor/014.png b/assets/outdoor/014.png
new file mode 100644
index 0000000000000000000000000000000000000000..fb4cc24e1d0c18db540179e0a0e694f6224abc4d
--- /dev/null
+++ b/assets/outdoor/014.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:fee389d93c8903f70526acdfd41f4fc7edd301765fb1247fa42876882d32fccd
+size 1103513
diff --git a/assets/outdoor/015.png b/assets/outdoor/015.png
new file mode 100644
index 0000000000000000000000000000000000000000..a87cd953b307370e2b65be1611a5a3999efdeccd
--- /dev/null
+++ b/assets/outdoor/015.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:fba97edd4f55735a58c4e8a0de49b87453c70d011e0fc59558e31fda39197782
+size 6354729
diff --git a/assets/outdoor/016.png b/assets/outdoor/016.png
new file mode 100644
index 0000000000000000000000000000000000000000..eb2c287dab94ad73c788491a4e10a86ce29aa807
--- /dev/null
+++ b/assets/outdoor/016.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:000051055b8b10761a194808c9106f11bec18d35c5c1b27bbd86bb999162086d
+size 1596132
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..178333be418e7fc100b8318ee43ea070c38c6890
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,4 @@
+gradio==5.45.0
+Pillow>=10.0.0
+numpy>=1.24.0
+requests>=2.31.0
\ No newline at end of file