minotrey commited on
Commit
d4df633
·
1 Parent(s): 554f2ea

Add user authentication with Hugging Face Secrets

Browse files

- Implement auth_fn to restrict access to allowed users only
- Add ALLOWED_USERS environment variable support
- Users authenticate with username/password (both same value)
- Update app.py with authentication integration
- Add Git LFS support for binary files

This view is limited to 50 files because it contains too many changes.   See raw diff
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ *.png filter=lfs diff=lfs merge=lfs -text
37
+ *.mp4 filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # Git LFS tracks all binary files
2
+ # See .gitattributes for tracked file types
LICENSE ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ POLYGOM PROPRIETARY SOFTWARE LICENSE
2
+
3
+ Copyright (c) 2025 Polygom. All Rights Reserved.
4
+
5
+ This software and associated documentation files (the "Software") are the
6
+ proprietary property of Polygom and are protected by copyright law.
7
+
8
+ NO PERMISSION IS GRANTED TO:
9
+ - Use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software
11
+ - Use the Software for any commercial or non-commercial purpose
12
+ - Reverse engineer, decompile, or disassemble the Software
13
+
14
+ This Software contains proprietary AI technology developed by Polygom.
15
+ Any unauthorized use, reproduction, or distribution is strictly prohibited
16
+ and may result in severe civil and criminal penalties.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED. IN NO EVENT SHALL POLYGOM BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE.
21
+
22
+ For licensing inquiries, please contact Polygom directly.
23
+
24
+ ---
25
+
26
+ POLYGOM 독점 소프트웨어 라이선스
27
+
28
+ Copyright (c) 2025 Polygom. 모든 권리 보유.
29
+
30
+ 이 소프트웨어와 관련 문서 파일("소프트웨어")은 Polygom의 독점 자산이며
31
+ 저작권법으로 보호됩니다.
32
+
33
+ 다음 행위는 허가되지 않습니다:
34
+ - 소프트웨어의 사용, 복사, 수정, 병합, 게시, 배포, 재라이선스, 판매
35
+ - 상업적 또는 비상업적 목적으로의 소프트웨어 사용
36
+ - 소프트웨어의 역공학, 디컴파일, 역어셈블
37
+
38
+ 이 소프트웨어는 Polygom이 개발한 독점 AI 기술을 포함하고 있습니다.
39
+ 무단 사용, 복제 또는 배포는 엄격히 금지되며 심각한 민형사상 처벌을
40
+ 받을 수 있습니다.
41
+
42
+ 소프트웨어는 "있는 그대로" 제공되며, 어떠한 명시적 또는 묵시적 보증도
43
+ 없습니다. Polygom은 소프트웨어와 관련하여 발생하는 어떠한 청구, 손해
44
+ 또는 기타 책임에 대해서도 책임지지 않습니다.
45
+
46
+ 라이선스 문의는 Polygom에 직접 연락하시기 바랍니다.
README.md CHANGED
@@ -1,13 +1,51 @@
1
  ---
2
- title: PixelFree
3
- emoji: 💻
4
- colorFrom: blue
5
- colorTo: purple
6
  sdk: gradio
7
- sdk_version: 5.49.0
8
  app_file: app.py
9
  pinned: false
10
- short_description: AI Image Processing
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: PixelFree - AI Image Processing Platform
3
+ emoji: 🎨
4
+ colorFrom: purple
5
+ colorTo: pink
6
  sdk: gradio
7
+ sdk_version: 5.45.0
8
  app_file: app.py
9
  pinned: false
 
10
  ---
11
 
12
+ # PixelFree - AI 이미지 처리 플랫폼
13
+
14
+ <p align="center">
15
+ <img src="https://img.shields.io/badge/Python-3.9+-blue.svg" alt="Python">
16
+ <img src="https://img.shields.io/badge/Gradio-4.0+-orange.svg" alt="Gradio">
17
+ </p>
18
+
19
+ ## 🎯 소개
20
+
21
+ PixelFree는 **Polygom**의 자체 AI 기술을 활용한 이미지 처리 플랫폼입니다. 배경 제거, 배경 생성, 얼굴 교체 등의 고급 기능을 원클릭으로 제공합니다.
22
+
23
+ ## 📋 주요 기능
24
+
25
+ ### 1. 배경 제거
26
+ - 입력 이미지에서 배경을 자동으로 제거
27
+ - 투명한 PNG 형식으로 결과 반환
28
+
29
+ ### 2. 배경 생성
30
+ - **2단계 프로세스**:
31
+ 1. 자동 배경 제거
32
+ 2. 새로운 배경 생성 및 합성
33
+ - 배경 선택 옵션:
34
+ - Indoor (실내 배경)
35
+ - Outdoor (실외 배경)
36
+ - Custom (사용자 업로드)
37
+
38
+ ### 3. 얼굴 교체
39
+ - 참조 얼굴 선택 옵션:
40
+ - 남자
41
+ - 여자
42
+ - 전체
43
+ - Custom (사용자 업로드)
44
+
45
+ ⚠️ **참고**: 이 API는 Polygom의 독점 기술이며, 별도의 라이선스 계약이 필요할 수 있습니다.
46
+
47
+ 자세한 내용은 [LICENSE](./LICENSE) 파일을 참조하세요.
48
+
49
+ ---
50
+
51
+ **Powered by Polygom**
app.py ADDED
@@ -0,0 +1,1219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PixelFree - AI Image Processing Service
3
+ HuggingFace Spaces용 Gradio 애플리케이션
4
+ Polygom API를 활용한 이미지 처리 서비스
5
+
6
+ Features:
7
+ - 배경 제거 (Background Removal)
8
+ - 배경 생성 (Background Generation)
9
+ - 얼굴 교체 (Face Swap)
10
+
11
+ Version: 3.0.0
12
+ Version 3: Simplified Multi-Mode Workflow
13
+ Author: PixelFree Team
14
+ """
15
+
16
+ import gradio as gr
17
+ import requests
18
+ import base64
19
+ import io
20
+ import logging
21
+ import tempfile
22
+ from PIL import Image
23
+ from typing import Optional, Tuple
24
+ from datetime import datetime
25
+ from collections import OrderedDict
26
+ import time
27
+ import os
28
+
29
+ # ===== Configuration =====
30
+ API_BASE_URL = os.environ.get("API_BASE_URL")
31
+ API_ENDPOINTS = {
32
+ "background_removal": os.environ.get("BACKGROUND_REMOVAL_ENDPOINT"),
33
+ "background_generation": os.environ.get("BACKGROUND_GENERATION_ENDPOINT"),
34
+ "face_swap": os.environ.get("FACE_SWAP_ENDPOINT"),
35
+ "video_generation": os.environ.get("VIDEO_GEN"),
36
+ }
37
+ VIDEO_PROMPT = os.environ.get("VIDEO_PROMPT", "환하게 웃으며 눈을 깜빡인다.")
38
+
39
+ # 허용된 사용자 목록 (Hugging Face Secrets에서 가져오기)
40
+ ALLOWED_USERS = os.environ.get("ALLOWED_USERS", "").split(",")
41
+ ALLOWED_USERS = [user.strip() for user in ALLOWED_USERS if user.strip()]
42
+
43
+ IMAGE_HEIGHT = 500
44
+ GALLERY_COLUMNS = 3
45
+ GALLERY_ROWS = 3
46
+ logging.basicConfig(level=logging.WARNING)
47
+ logger = logging.getLogger(__name__)
48
+
49
+ IMAGE_CACHE_LIMIT = 24
50
+ image_cache: "OrderedDict[str, Image.Image]" = OrderedDict()
51
+
52
+
53
+ def cache_image(path: str, image: Image.Image) -> None:
54
+ """Add image to global cache with simple LRU eviction."""
55
+ if path in image_cache:
56
+ image_cache.move_to_end(path)
57
+ image_cache[path] = image
58
+ while len(image_cache) > IMAGE_CACHE_LIMIT:
59
+ image_cache.popitem(last=False)
60
+
61
+ output_history = []
62
+
63
+
64
+ def ensure_png_image(image: Optional[Image.Image]) -> Optional[Image.Image]:
65
+ """Ensure provided PIL image is stored as a PNG image (returns copy)."""
66
+ if image is None:
67
+ return None
68
+
69
+ working_image = image
70
+ if working_image.mode not in ("RGB", "RGBA"):
71
+ working_image = working_image.convert("RGBA")
72
+
73
+ buffer = io.BytesIO()
74
+ # Convert to PNG while preserving alpha when present
75
+ target_mode = "RGBA" if working_image.mode == "RGBA" else "RGB"
76
+ working_image.convert(target_mode).save(buffer, format="PNG")
77
+ buffer.seek(0)
78
+ with Image.open(buffer) as png_image:
79
+ return png_image.convert("RGBA").copy()
80
+
81
+
82
+ def make_thumbnail_image(image: Optional[Image.Image], size: Tuple[int, int] = (256, 256), *, fill_color=(255, 255, 255, 0)) -> Optional[Image.Image]:
83
+ """Create a thumbnail copy of the provided image for gallery previews."""
84
+ base_image = ensure_png_image(image)
85
+ if base_image is None:
86
+ return None
87
+
88
+ preview = base_image.copy()
89
+ try:
90
+ preview.thumbnail(size, Image.Resampling.LANCZOS)
91
+ except AttributeError:
92
+ preview.thumbnail(size)
93
+ canvas = Image.new("RGBA", size, fill_color)
94
+ offset = ((size[0] - preview.width) // 2, (size[1] - preview.height) // 2)
95
+ canvas.paste(preview, offset, preview if preview.mode == "RGBA" else None)
96
+ return canvas
97
+
98
+
99
+ def add_to_history(image: Optional[Image.Image], workspace: str) -> Optional[str]:
100
+ """Append processed image to history gallery with descriptive PNG name."""
101
+ if image is None:
102
+ return None
103
+
104
+ png_image = ensure_png_image(image)
105
+ if png_image is None:
106
+ return None
107
+
108
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
109
+ prefix_map = {
110
+ "bg_remove": "background_removed",
111
+ "bg_gen": "background_generated",
112
+ "face_swap": "face_swap",
113
+ "video": "video_generated",
114
+ }
115
+ prefix = prefix_map.get(workspace, "result")
116
+ file_name = f"{prefix}_{timestamp}.png"
117
+
118
+ output_history.append((png_image, file_name))
119
+ if len(output_history) > 20:
120
+ del output_history[:-20]
121
+
122
+ return file_name
123
+
124
+
125
+ def get_history_items(limit: Optional[int] = None):
126
+ """Return history items (image, filename) with newest first."""
127
+ if limit is None:
128
+ data = output_history[:]
129
+ elif limit <= 0:
130
+ return []
131
+ else:
132
+ data = output_history[-limit:]
133
+ return list(reversed(data))
134
+
135
+
136
+
137
+ class ImageProcessor:
138
+ """Wrapper around PixelFree backend APIs."""
139
+
140
+ def __init__(self) -> None:
141
+ self.session = requests.Session()
142
+ adapter = requests.adapters.HTTPAdapter(
143
+ pool_connections=10,
144
+ pool_maxsize=20,
145
+ max_retries=3,
146
+ )
147
+ self.session.mount("http://", adapter)
148
+ self.session.mount("https://", adapter)
149
+ self.session.headers.update({"accept": "application/json"})
150
+
151
+ def image_to_bytes(self, image: Image.Image, format: str = "PNG", preserve_alpha: bool = False) -> bytes:
152
+ buffer = io.BytesIO()
153
+ img = image
154
+ if not preserve_alpha and img.mode in ("RGBA", "P"):
155
+ img = img.convert("RGB")
156
+ elif preserve_alpha and img.mode != "RGBA":
157
+ img = img.convert("RGBA")
158
+ img.save(buffer, format=format)
159
+ buffer.seek(0)
160
+ return buffer.getvalue()
161
+
162
+ def base64_to_image(self, base64_str: str) -> Optional[Image.Image]:
163
+ try:
164
+ if base64_str.startswith("http"):
165
+ logger.error("Refusing to download image from URL: %s", base64_str)
166
+ return None
167
+ if "," in base64_str:
168
+ base64_str = base64_str.split(",", 1)[1]
169
+ img_data = base64.b64decode(base64_str)
170
+ return Image.open(io.BytesIO(img_data))
171
+ except Exception as exc:
172
+ logger.error("Failed to decode image: %s", exc)
173
+ return None
174
+
175
+ def call_api(self, endpoint: Optional[str], files: dict, data: dict) -> Optional[dict]:
176
+ if not endpoint or not API_BASE_URL:
177
+ logger.error("API endpoint or base URL missing")
178
+ return None
179
+ url = f"{API_BASE_URL}{endpoint}"
180
+ try:
181
+ response = self.session.post(url, files=files, data=data, timeout=300)
182
+ logger.info("API response %s: %s", endpoint, response.status_code)
183
+ response.raise_for_status()
184
+ return response.json()
185
+ except Exception as exc:
186
+ logger.error("API call failed (%s): %s", endpoint, exc)
187
+ return None
188
+
189
+ def remove_background(self, image: Image.Image) -> Tuple[Optional[Image.Image], str]:
190
+ start_time = time.time()
191
+ files = {
192
+ "file": ("input.png", self.image_to_bytes(image, preserve_alpha=True), "image/png")
193
+ }
194
+ result = self.call_api(API_ENDPOINTS.get("background_removal"), files, {})
195
+ total = time.time() - start_time
196
+ if result and result.get("success") and result.get("data"):
197
+ b64 = result["data"].get("result_base64")
198
+ image_obj = self.base64_to_image(b64) if b64 else None
199
+ if image_obj:
200
+ return image_obj, f"✅ 배경 제거 완료! (처리 시간: {total:.1f}초)"
201
+ return None, f"❌ 배경 제거 실패 (시도 시간: {total:.1f}초)"
202
+
203
+ def generate_background(
204
+ self,
205
+ image: Image.Image,
206
+ bg_image: Image.Image,
207
+ hq_mode: bool = False,
208
+ ) -> Tuple[Optional[Image.Image], str]:
209
+ start_time = time.time()
210
+ files = {
211
+ "image": ("input.png", self.image_to_bytes(image, preserve_alpha=True), "image/png"),
212
+ "bg_image": ("background.png", self.image_to_bytes(bg_image), "image/png"),
213
+ }
214
+ data = {
215
+ "jobId": f"job_{int(time.time() * 1000)}",
216
+ "hq_mode": "true" if hq_mode else "false",
217
+ }
218
+ result = self.call_api(API_ENDPOINTS.get("background_generation"), files, data)
219
+ total = time.time() - start_time
220
+ mode_text = "고품질" if hq_mode else "일반"
221
+ if result and result.get("success") and result.get("data"):
222
+ payload = result["data"]
223
+ if payload.get("imageData"):
224
+ image_obj = self.base64_to_image(payload["imageData"])
225
+ if image_obj:
226
+ return image_obj, f"✅ 배경 생성 완료! (처리 시간: {total:.1f}초, 모드: {mode_text})"
227
+ if payload.get("rawDataUrl"):
228
+ logger.error("Ignoring rawDataUrl field for security reasons.")
229
+ return None, f"❌ 배경 생성 실패 (시도 시간: {total:.1f}초, 모드: {mode_text})"
230
+
231
+ def swap_face(self, source: Image.Image, target: Image.Image) -> Tuple[Optional[Image.Image], str]:
232
+ start_time = time.time()
233
+ files = {
234
+ "image": ("image.png", self.image_to_bytes(source), "image/png"),
235
+ "targetFaceImage": ("targetFace.png", self.image_to_bytes(target), "image/png"),
236
+ }
237
+ data = {
238
+ "jobId": f"face_swap_{int(time.time() * 1000)}"
239
+ }
240
+ result = self.call_api(API_ENDPOINTS.get("face_swap"), files, data)
241
+ total = time.time() - start_time
242
+ if result and result.get("success") and result.get("data"):
243
+ payload = result["data"]
244
+ if payload.get("imageData"):
245
+ image_obj = self.base64_to_image(payload["imageData"])
246
+ if image_obj:
247
+ api_time = payload.get("processingTime", 0)
248
+ return image_obj, f"✅ 얼굴 교체 완료! (전체: {total:.1f}초, API: {api_time:.1f}초)"
249
+ if payload.get("rawDataUrl"):
250
+ logger.error("Ignoring rawDataUrl field for security reasons.")
251
+ return None, f"❌ 얼굴 교체 실패 (시도 시간: {total:.1f}초)"
252
+
253
+ def generate_video(self, image: Optional[Image.Image]) -> Tuple[Optional[str], str]:
254
+ if image is None:
255
+ return None, "입력 이미지가 필요합니다."
256
+
257
+ start_time = time.time()
258
+ files = {
259
+ "image": ("image.png", self.image_to_bytes(image, preserve_alpha=True), "image/png"),
260
+ }
261
+ data = {
262
+ "jobId": f"video_{int(time.time() * 1000)}",
263
+ "prompt": VIDEO_PROMPT,
264
+ "duration": "5",
265
+ }
266
+ result = self.call_api(API_ENDPOINTS.get("video_generation"), files, data)
267
+ total = time.time() - start_time
268
+ if result and result.get("success") and result.get("data"):
269
+ payload = result["data"]
270
+ video_base64 = payload.get("videoData")
271
+ if video_base64:
272
+ if "," in video_base64:
273
+ video_base64 = video_base64.split(",", 1)[1]
274
+ try:
275
+ video_bytes = base64.b64decode(video_base64)
276
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") as temp_video:
277
+ temp_video.write(video_bytes)
278
+ temp_path = temp_video.name
279
+ except Exception as exc:
280
+ logger.error("Failed to decode video: %s", exc)
281
+ else:
282
+ api_time = payload.get("processingTime", 0)
283
+ return temp_path, f"✅ 영상 생성 완료! (전체: {total:.1f}초, API: {api_time:.1f}초)"
284
+ if payload.get("rawDataUrl"):
285
+ logger.error("Ignoring rawDataUrl field for security reasons.")
286
+ return None, f"❌ 영상 생성 실패 (시도 시간: {total:.1f}초)"
287
+
288
+ processor = ImageProcessor()
289
+
290
+ # 인증 함수
291
+ def auth_fn(username: str, password: str) -> bool:
292
+ """
293
+ 사용자 인증 함수
294
+ - username이 허용된 사용자 목록에 있고
295
+ - username과 password가 같으면 인증 성공
296
+ """
297
+ return username == password and username in ALLOWED_USERS
298
+
299
+ # 배경 이미지 로드
300
+ def load_background_images():
301
+ """배경 이미지를 assets 폴더에서 로드 (Lazy loading)"""
302
+ backgrounds = {
303
+ "indoor": [],
304
+ "outdoor": [],
305
+ "indoor_paths": [],
306
+ "outdoor_paths": [],
307
+ "indoor_pil": [],
308
+ "outdoor_pil": []
309
+ }
310
+
311
+ cwd = os.getcwd()
312
+
313
+ # Indoor 이미지 경로 저장
314
+ indoor_path = os.path.join(cwd, "assets", "indoor")
315
+ if os.path.exists(indoor_path):
316
+ files = sorted([f for f in os.listdir(indoor_path) if f.endswith('.png')])
317
+ for img_file in files:
318
+ img_path = os.path.join(indoor_path, img_file)
319
+ try:
320
+ backgrounds["indoor_paths"].append(img_path)
321
+ # 갤러리용 썸네일만 생성
322
+ with Image.open(img_path) as img:
323
+ img = img.convert('RGBA' if 'A' in img.getbands() else 'RGB')
324
+ thumb = img.copy()
325
+ thumb.thumbnail((240, 240), Image.Resampling.LANCZOS)
326
+ backgrounds["indoor"].append(thumb)
327
+ backgrounds["indoor_pil"].append(img_path) # 경로 저장
328
+ except Exception:
329
+ pass
330
+
331
+ # Outdoor 이미지 로드
332
+ outdoor_path = os.path.join(cwd, "assets", "outdoor")
333
+ if os.path.exists(outdoor_path):
334
+ files = sorted([f for f in os.listdir(outdoor_path) if f.endswith('.png')])
335
+ for img_file in files:
336
+ img_path = os.path.join(outdoor_path, img_file)
337
+ try:
338
+ backgrounds["outdoor_paths"].append(img_path)
339
+ with Image.open(img_path) as img:
340
+ img = img.convert('RGBA' if 'A' in img.getbands() else 'RGB')
341
+ thumb = img.copy()
342
+ thumb.thumbnail((240, 240), Image.Resampling.LANCZOS)
343
+ backgrounds["outdoor"].append(thumb)
344
+ backgrounds["outdoor_pil"].append(img_path)
345
+ except Exception:
346
+ pass
347
+
348
+ # 이미지가 없는 경우 더미 이미지 생성
349
+ if len(backgrounds['indoor']) == 0:
350
+ dummy = Image.new('RGB', (240, 240), color=(200, 200, 200))
351
+ backgrounds["indoor"].append(dummy)
352
+ backgrounds["indoor_pil"].append(None)
353
+
354
+ if len(backgrounds['outdoor']) == 0:
355
+ dummy = Image.new('RGB', (240, 240), color=(150, 200, 150))
356
+ backgrounds["outdoor"].append(dummy)
357
+ backgrounds["outdoor_pil"].append(None)
358
+
359
+ return backgrounds
360
+
361
+ bg_images = load_background_images()
362
+
363
+ # 얼굴 이미지 로드
364
+ def load_face_images():
365
+ """얼굴 이미지들을 로드 - PIL 이미지로 직접 로드하여 갤러리에 전달"""
366
+ faces = {
367
+ "man_pil": [],
368
+ "woman_pil": [],
369
+ "all_pil": [],
370
+ "man_paths": [],
371
+ "woman_paths": [],
372
+ "all_paths": []
373
+ }
374
+
375
+ cwd = os.getcwd()
376
+
377
+ # Man 이미지 로드
378
+ man_path = os.path.join(cwd, "assets", "faces", "man")
379
+ if os.path.exists(man_path):
380
+ files = sorted([f for f in os.listdir(man_path) if f.endswith(('.png', '.jpg', '.jpeg'))])
381
+ for img_file in files:
382
+ img_path = os.path.join(man_path, img_file)
383
+ try:
384
+ with Image.open(img_path) as img:
385
+ img = img.convert('RGBA' if 'A' in img.getbands() else 'RGB')
386
+ thumb = img.copy()
387
+ thumb.thumbnail((240, 240), Image.Resampling.LANCZOS)
388
+ faces["man_pil"].append(thumb)
389
+ faces["man_paths"].append(img_path)
390
+ except Exception:
391
+ pass
392
+
393
+ # Woman 이미지 로드
394
+ woman_path = os.path.join(cwd, "assets", "faces", "woman")
395
+ if os.path.exists(woman_path):
396
+ files = sorted([f for f in os.listdir(woman_path) if f.endswith(('.png', '.jpg', '.jpeg'))])
397
+ for img_file in files:
398
+ img_path = os.path.join(woman_path, img_file)
399
+ try:
400
+ with Image.open(img_path) as img:
401
+ img = img.convert('RGBA' if 'A' in img.getbands() else 'RGB')
402
+ thumb = img.copy()
403
+ thumb.thumbnail((240, 240), Image.Resampling.LANCZOS)
404
+ faces["woman_pil"].append(thumb)
405
+ faces["woman_paths"].append(img_path)
406
+ except Exception:
407
+ pass
408
+
409
+ # 이미지가 없는 경우 더미 이미지 생성
410
+ if len(faces['man_pil']) == 0:
411
+ for _ in range(3):
412
+ dummy = Image.new('RGB', (240, 240), color=(200, 200, 250))
413
+ faces["man_pil"].append(dummy)
414
+ faces["man_paths"].append(None)
415
+
416
+ if len(faces['woman_pil']) == 0:
417
+ for _ in range(3):
418
+ dummy = Image.new('RGB', (240, 240), color=(250, 200, 200))
419
+ faces["woman_pil"].append(dummy)
420
+ faces["woman_paths"].append(None)
421
+
422
+ # all 리스트 재구성
423
+ faces["all_pil"] = faces["man_pil"] + faces["woman_pil"]
424
+ faces["all_paths"] = faces["man_paths"] + faces["woman_paths"]
425
+
426
+ return faces
427
+
428
+ face_images = load_face_images()
429
+ 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', []))}")
430
+
431
+ # 이미지를 실제로 로드하는 헬퍼 함수
432
+ def load_full_image(path_or_image):
433
+ """경로에서 전체 크기 이미지를 로드 (캐싱 포함)"""
434
+ if isinstance(path_or_image, str):
435
+ cached = image_cache.get(path_or_image)
436
+ if cached is not None:
437
+ image_cache.move_to_end(path_or_image)
438
+ return cached
439
+
440
+ try:
441
+ with Image.open(path_or_image) as img:
442
+ mode = 'RGBA' if 'A' in img.getbands() else 'RGB'
443
+ loaded = img.convert(mode).copy()
444
+ except Exception as exc:
445
+ logger.error(f"Failed to load image {path_or_image}: {exc}")
446
+ return None
447
+
448
+ cache_image(path_or_image, loaded)
449
+ return loaded
450
+ else:
451
+ return path_or_image
452
+
453
+
454
+ def create_app():
455
+ """PixelFree Gradio UI (v3.0.0)."""
456
+
457
+ # CSS for history gallery image display
458
+ custom_css = """
459
+ #history-gallery img {
460
+ object-fit: contain !important;
461
+ height: 100% !important;
462
+ width: 100% !important;
463
+ }
464
+
465
+ #history-gallery .thumbnail-item {
466
+ height: 150px !important;
467
+ }
468
+
469
+ #history-gallery .grid-wrap {
470
+ gap: 10px !important;
471
+ }
472
+ """
473
+
474
+ with gr.Blocks(title="PixelFree v3.0.0", theme=gr.themes.Soft(), css=custom_css) as demo:
475
+
476
+ # 메인 헤더
477
+ gr.Markdown("# 🎨 PixelFree Studio")
478
+
479
+ # 입력 섹션
480
+ gr.Markdown("## 📸 입력 이미지")
481
+
482
+ input_state = gr.State(None)
483
+ bg_removed_state = gr.State(None)
484
+ selected_bg_state = gr.State(None)
485
+ selected_face_state = gr.State(None)
486
+ last_output_state = gr.State(None)
487
+
488
+ input_image = gr.Image(label="입력 이미지", type="pil", height=540)
489
+
490
+ # 결과 섹션
491
+ gr.Markdown("## ✨ 결과 이미지")
492
+ output_image = gr.Image(label="처리된 이미지", type="pil", height=500, interactive=False)
493
+ download_btn = gr.DownloadButton(
494
+ "📥 사진 다운로드",
495
+ value=None,
496
+ interactive=False
497
+ )
498
+
499
+ # 비디오 생성 섹션
500
+ gr.Markdown("## 🎬 AI 비디오 생성")
501
+ with gr.Row():
502
+ video_output = gr.Video(
503
+ label="생성된 영상",
504
+ height=360,
505
+ interactive=False,
506
+ autoplay=True,
507
+ )
508
+ with gr.Column():
509
+ gr.Markdown("최근 이미지 결과를 기반으로 짧은 클립을 생성합니다.")
510
+ generate_video_btn = gr.Button(
511
+ "🎬 영상 생성하기",
512
+ variant="primary",
513
+ size="lg",
514
+ interactive=False,
515
+ )
516
+
517
+ # 배경 제거 섹션
518
+ gr.Markdown("## 🎯 AI 배경 제거")
519
+ remove_bg_btn = gr.Button("🚀 배경 제거 실행", variant="primary", size="lg", interactive=False)
520
+
521
+ # AI 생성 기능 섹션
522
+ gr.Markdown("## 🎨 AI 생성 기능")
523
+ with gr.Row():
524
+ with gr.Column():
525
+ gr.Markdown("### 🏞️ 배경 생성")
526
+ with gr.Tabs():
527
+ with gr.Tab("🏠 실내"):
528
+ indoor_gallery = gr.Gallery(
529
+ value=bg_images["indoor"],
530
+ columns=4,
531
+ show_label=False, interactive=False,
532
+ allow_preview=False,
533
+ object_fit="contain"
534
+ )
535
+ with gr.Tab("🌳 아웃도어"):
536
+ outdoor_gallery = gr.Gallery(
537
+ value=bg_images["outdoor"],
538
+ columns=4,
539
+ rows=4, show_label=False, interactive=False,
540
+ allow_preview=False,
541
+ object_fit="contain"
542
+ )
543
+ hq_checkbox = gr.Checkbox(label="✨ 고품질 모드", value=False)
544
+ generate_bg_btn = gr.Button("🎨 배경 생성하기", variant="secondary", interactive=False, size="lg")
545
+ with gr.Column():
546
+ gr.Markdown("### 👤 얼굴 교체")
547
+ with gr.Tabs():
548
+ with gr.Tab("👨 남성"):
549
+ man_gallery = gr.Gallery(
550
+ value=face_images["man_pil"],
551
+ columns=4,
552
+ rows=2, show_label=False, interactive=False,
553
+ allow_preview=False,
554
+ object_fit="contain"
555
+ )
556
+ with gr.Tab("👩 여성"):
557
+ woman_gallery = gr.Gallery(
558
+ value=face_images["woman_pil"],
559
+ columns=4,
560
+ rows=2, show_label=False, interactive=False,
561
+ allow_preview=False,
562
+ object_fit="contain"
563
+ )
564
+ swap_face_btn = gr.Button("🔄 얼굴 교체 적용", variant="secondary", interactive=False, size="lg")
565
+ # Collapsible history section with image grid
566
+ with gr.Accordion("📜 작업 히스토리", open=False):
567
+ # History gallery with preview enabled
568
+ history_gallery = gr.Gallery(
569
+ value=[img for img, _ in get_history_items(20)],
570
+ label=None,
571
+ show_label=False,
572
+ columns=5,
573
+ rows=4,
574
+ height=500,
575
+ object_fit="contain",
576
+ allow_preview=True,
577
+ elem_id="history-gallery",
578
+ interactive=False
579
+ )
580
+
581
+ def make_download_update(image: Optional[Image.Image], prefix: str = "pixelfree_output"):
582
+ if image is None:
583
+ return gr.update(value=None, interactive=False)
584
+ filename = f"{prefix}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png"
585
+ temp_path = f"/tmp/{filename}"
586
+ image.save(temp_path, "PNG")
587
+ return gr.update(value=temp_path, interactive=True)
588
+
589
+ def history_update():
590
+ """Update history gallery with original images."""
591
+ items = get_history_items(20)
592
+ images = [img for img, _ in items]
593
+ return gr.update(value=images)
594
+
595
+ def disable_all_buttons():
596
+ disabled = gr.update(interactive=False)
597
+ return disabled, disabled, disabled, disabled
598
+
599
+ def compute_button_states(
600
+ input_img: Optional[Image.Image],
601
+ last_output: Optional[Image.Image],
602
+ bg_removed: Optional[Image.Image],
603
+ selected_bg: Optional[Image.Image],
604
+ selected_face: Optional[Image.Image],
605
+ ):
606
+ remove_ready = gr.update(interactive=input_img is not None)
607
+ video_ready = gr.update(interactive=input_img is not None)
608
+ bg_ready = gr.update(interactive=(bg_removed is not None and selected_bg is not None))
609
+ swap_ready = gr.update(
610
+ interactive=(selected_face is not None and ((last_output is not None) or (input_img is not None)))
611
+ )
612
+ return remove_ready, video_ready, bg_ready, swap_ready
613
+
614
+
615
+ def on_upload(new_image: Optional[Image.Image]):
616
+ if new_image is None:
617
+ gr.Warning("입력 이미지를 업로드해주세요.")
618
+ remove_ready, video_ready, bg_ready, swap_ready = disable_all_buttons()
619
+ return (
620
+ None,
621
+ None,
622
+ None,
623
+ None,
624
+ None,
625
+ gr.update(value=None),
626
+ make_download_update(None),
627
+ gr.update(value=None),
628
+ remove_ready,
629
+ video_ready,
630
+ bg_ready,
631
+ swap_ready,
632
+ history_update(),
633
+ )
634
+
635
+ gr.Info("입력 이미지를 불러왔습니다.")
636
+ remove_ready, video_ready, bg_ready, swap_ready = compute_button_states(
637
+ new_image,
638
+ None,
639
+ None,
640
+ None,
641
+ None,
642
+ )
643
+ return (
644
+ new_image,
645
+ None,
646
+ None,
647
+ None,
648
+ None,
649
+ gr.update(value=None),
650
+ make_download_update(None),
651
+ gr.update(value=None),
652
+ remove_ready,
653
+ video_ready,
654
+ bg_ready,
655
+ swap_ready,
656
+ history_update(),
657
+ )
658
+
659
+ def handle_remove_background(
660
+ input_img: Optional[Image.Image],
661
+ current_bg_removed: Optional[Image.Image],
662
+ selected_bg: Optional[Image.Image],
663
+ current_output: Optional[Image.Image],
664
+ selected_face: Optional[Image.Image],
665
+ ):
666
+ if input_img is None:
667
+ gr.Warning("입력 이미지를 먼저 업로드해주세요.")
668
+ remove_ready, video_ready, bg_ready, swap_ready = compute_button_states(
669
+ input_img,
670
+ current_output,
671
+ current_bg_removed,
672
+ selected_bg,
673
+ selected_face,
674
+ )
675
+ yield (
676
+ current_bg_removed,
677
+ current_output,
678
+ gr.update(),
679
+ make_download_update(current_output),
680
+ bg_ready,
681
+ swap_ready,
682
+ remove_ready,
683
+ video_ready,
684
+ history_update(),
685
+ )
686
+ return
687
+
688
+ remove_disabled, video_disabled, bg_disabled, swap_disabled = disable_all_buttons()
689
+ yield (
690
+ current_bg_removed,
691
+ current_output,
692
+ gr.update(),
693
+ make_download_update(current_output),
694
+ bg_disabled,
695
+ swap_disabled,
696
+ remove_disabled,
697
+ video_disabled,
698
+ history_update(),
699
+ )
700
+
701
+ removed, message = processor.remove_background(input_img)
702
+ if removed is None:
703
+ gr.Warning(message)
704
+ remove_ready, video_ready, bg_ready, swap_ready = compute_button_states(
705
+ input_img,
706
+ current_output,
707
+ current_bg_removed,
708
+ selected_bg,
709
+ selected_face,
710
+ )
711
+ yield (
712
+ current_bg_removed,
713
+ current_output,
714
+ gr.update(value=current_output),
715
+ make_download_update(current_output),
716
+ bg_ready,
717
+ swap_ready,
718
+ remove_ready,
719
+ video_ready,
720
+ history_update(),
721
+ )
722
+ return
723
+
724
+ gr.Info(message)
725
+ add_to_history(removed, "bg_remove")
726
+ remove_ready, video_ready, bg_ready, swap_ready = compute_button_states(
727
+ input_img,
728
+ removed,
729
+ removed,
730
+ selected_bg,
731
+ selected_face,
732
+ )
733
+ yield (
734
+ removed,
735
+ removed,
736
+ gr.update(value=removed),
737
+ make_download_update(removed, "background_removed"),
738
+ bg_ready,
739
+ swap_ready,
740
+ remove_ready,
741
+ video_ready,
742
+ history_update(),
743
+ )
744
+
745
+ def on_generate_video(
746
+ last_output: Optional[Image.Image],
747
+ input_img: Optional[Image.Image],
748
+ current_bg_removed: Optional[Image.Image],
749
+ selected_bg: Optional[Image.Image],
750
+ selected_face: Optional[Image.Image],
751
+ ):
752
+ base_image = last_output or input_img
753
+ if base_image is None:
754
+ gr.Warning("입력 이미지를 먼저 업로드해주세요.")
755
+ remove_ready, video_ready, bg_ready, swap_ready = compute_button_states(
756
+ input_img,
757
+ last_output,
758
+ current_bg_removed,
759
+ selected_bg,
760
+ selected_face,
761
+ )
762
+ yield (
763
+ gr.update(value=None),
764
+ history_update(),
765
+ remove_ready,
766
+ video_ready,
767
+ bg_ready,
768
+ swap_ready,
769
+ )
770
+ return
771
+
772
+ remove_disabled, video_disabled, bg_disabled, swap_disabled = disable_all_buttons()
773
+ yield (
774
+ gr.update(),
775
+ history_update(),
776
+ remove_disabled,
777
+ video_disabled,
778
+ bg_disabled,
779
+ swap_disabled,
780
+ )
781
+
782
+ video_path, message = processor.generate_video(base_image)
783
+ if video_path is None:
784
+ gr.Warning(message)
785
+ remove_ready, video_ready, bg_ready, swap_ready = compute_button_states(
786
+ input_img,
787
+ last_output,
788
+ current_bg_removed,
789
+ selected_bg,
790
+ selected_face,
791
+ )
792
+ yield (
793
+ gr.update(value=None),
794
+ history_update(),
795
+ remove_ready,
796
+ video_ready,
797
+ bg_ready,
798
+ swap_ready,
799
+ )
800
+ return
801
+
802
+ gr.Info(message)
803
+ add_to_history(base_image, "video")
804
+ remove_ready, video_ready, bg_ready, swap_ready = compute_button_states(
805
+ input_img,
806
+ last_output,
807
+ current_bg_removed,
808
+ selected_bg,
809
+ selected_face,
810
+ )
811
+ yield (
812
+ gr.update(value=video_path),
813
+ history_update(),
814
+ remove_ready,
815
+ video_ready,
816
+ bg_ready,
817
+ swap_ready,
818
+ )
819
+
820
+ def load_background_image(category: str, index: int) -> Optional[Image.Image]:
821
+ key = f"{category}_pil"
822
+ paths = bg_images.get(key, [])
823
+ if 0 <= index < len(paths):
824
+ path = paths[index]
825
+ return load_full_image(path)
826
+ return None
827
+
828
+ def on_select_background(
829
+ evt: gr.SelectData,
830
+ category: str,
831
+ bg_removed: Optional[Image.Image],
832
+ ):
833
+ index = getattr(evt, "index", None)
834
+ if index is None:
835
+ gr.Warning("선택 정보를 확인할 수 없습니다.")
836
+ return None, gr.update(interactive=False)
837
+ image = load_background_image(category, index)
838
+ if image is None:
839
+ gr.Warning("선택한 배경 이미지를 불러오지 못했습니다.")
840
+ return None, gr.update(interactive=False)
841
+ gr.Info("배경 이미지를 선택했습니다.")
842
+ ready = gr.update(interactive=bg_removed is not None)
843
+ return image, ready
844
+
845
+ def on_generate_background(
846
+ last_output: Optional[Image.Image],
847
+ selected_bg: Optional[Image.Image],
848
+ hq_mode: bool,
849
+ input_img: Optional[Image.Image],
850
+ current_bg_removed: Optional[Image.Image],
851
+ selected_face: Optional[Image.Image],
852
+ ):
853
+ if last_output is None:
854
+ gr.Warning("먼저 배경 제거를 실행해주세요.")
855
+ remove_ready, video_ready, bg_ready, swap_ready = compute_button_states(
856
+ input_img,
857
+ last_output,
858
+ current_bg_removed,
859
+ selected_bg,
860
+ selected_face,
861
+ )
862
+ yield (
863
+ last_output,
864
+ gr.update(value=last_output),
865
+ make_download_update(last_output),
866
+ history_update(),
867
+ remove_ready,
868
+ video_ready,
869
+ bg_ready,
870
+ swap_ready,
871
+ )
872
+ return
873
+
874
+ if selected_bg is None:
875
+ gr.Warning("사용할 배경 이미지를 선택해주세요.")
876
+ remove_ready, video_ready, bg_ready, swap_ready = compute_button_states(
877
+ input_img,
878
+ last_output,
879
+ current_bg_removed,
880
+ selected_bg,
881
+ selected_face,
882
+ )
883
+ yield (
884
+ last_output,
885
+ gr.update(value=last_output),
886
+ make_download_update(last_output),
887
+ history_update(),
888
+ remove_ready,
889
+ video_ready,
890
+ bg_ready,
891
+ swap_ready,
892
+ )
893
+ return
894
+
895
+ remove_disabled, video_disabled, bg_disabled, swap_disabled = disable_all_buttons()
896
+ yield (
897
+ last_output,
898
+ gr.update(value=last_output),
899
+ make_download_update(last_output),
900
+ history_update(),
901
+ remove_disabled,
902
+ video_disabled,
903
+ bg_disabled,
904
+ swap_disabled,
905
+ )
906
+
907
+ generated, message = processor.generate_background(last_output, selected_bg, hq_mode)
908
+ if generated is None:
909
+ gr.Warning(message)
910
+ remove_ready, video_ready, bg_ready, swap_ready = compute_button_states(
911
+ input_img,
912
+ last_output,
913
+ current_bg_removed,
914
+ selected_bg,
915
+ selected_face,
916
+ )
917
+ yield (
918
+ last_output,
919
+ gr.update(value=last_output),
920
+ make_download_update(last_output),
921
+ history_update(),
922
+ remove_ready,
923
+ video_ready,
924
+ bg_ready,
925
+ swap_ready,
926
+ )
927
+ return
928
+
929
+ gr.Info(message)
930
+ add_to_history(generated, "bg_gen")
931
+ remove_ready, video_ready, bg_ready, swap_ready = compute_button_states(
932
+ input_img,
933
+ generated,
934
+ current_bg_removed,
935
+ selected_bg,
936
+ selected_face,
937
+ )
938
+ yield (
939
+ generated,
940
+ gr.update(value=generated),
941
+ make_download_update(generated, "background_generated"),
942
+ history_update(),
943
+ remove_ready,
944
+ video_ready,
945
+ bg_ready,
946
+ swap_ready,
947
+ )
948
+
949
+ def load_face_image(category: str, index: int) -> Optional[Image.Image]:
950
+ key = f"{category}_paths"
951
+ paths = face_images.get(key, [])
952
+ if 0 <= index < len(paths):
953
+ path = paths[index]
954
+ return load_full_image(path)
955
+ return None
956
+
957
+ def on_select_face(
958
+ evt: gr.SelectData,
959
+ category: str,
960
+ last_output: Optional[Image.Image],
961
+ input_img: Optional[Image.Image],
962
+ ):
963
+ base_image = last_output or input_img
964
+ index = getattr(evt, "index", None)
965
+ if index is None:
966
+ gr.Warning("선택 정보를 확인할 수 없습니다.")
967
+ return None, gr.update(interactive=False)
968
+ image = load_face_image(category, index)
969
+ if image is None:
970
+ gr.Warning("선택한 얼굴 이미지를 불러오지 못했습니다.")
971
+ return None, gr.update(interactive=False)
972
+ gr.Info("얼굴 이미지를 선택했습니다.")
973
+ ready = gr.update(interactive=base_image is not None)
974
+ return image, ready
975
+
976
+ def on_swap_face(
977
+ last_output: Optional[Image.Image],
978
+ input_img: Optional[Image.Image],
979
+ selected_face: Optional[Image.Image],
980
+ current_bg_removed: Optional[Image.Image],
981
+ selected_bg: Optional[Image.Image],
982
+ ):
983
+ base_image = last_output or input_img
984
+ if base_image is None:
985
+ gr.Warning("입력 이미지를 먼저 업로드해주세요.")
986
+ remove_ready, video_ready, bg_ready, swap_ready = compute_button_states(
987
+ input_img,
988
+ last_output,
989
+ current_bg_removed,
990
+ selected_bg,
991
+ selected_face,
992
+ )
993
+ yield (
994
+ last_output,
995
+ gr.update(),
996
+ make_download_update(last_output),
997
+ history_update(),
998
+ remove_ready,
999
+ video_ready,
1000
+ bg_ready,
1001
+ swap_ready,
1002
+ )
1003
+ return
1004
+
1005
+ if selected_face is None:
1006
+ gr.Warning("사용할 얼굴 이미지를 선택해주세요.")
1007
+ remove_ready, video_ready, bg_ready, swap_ready = compute_button_states(
1008
+ input_img,
1009
+ last_output,
1010
+ current_bg_removed,
1011
+ selected_bg,
1012
+ selected_face,
1013
+ )
1014
+ yield (
1015
+ last_output,
1016
+ gr.update(value=base_image),
1017
+ make_download_update(last_output),
1018
+ history_update(),
1019
+ remove_ready,
1020
+ video_ready,
1021
+ bg_ready,
1022
+ swap_ready,
1023
+ )
1024
+ return
1025
+
1026
+ remove_disabled, video_disabled, bg_disabled, swap_disabled = disable_all_buttons()
1027
+ yield (
1028
+ last_output,
1029
+ gr.update(value=base_image),
1030
+ make_download_update(last_output),
1031
+ history_update(),
1032
+ remove_disabled,
1033
+ video_disabled,
1034
+ bg_disabled,
1035
+ swap_disabled,
1036
+ )
1037
+
1038
+ swapped, message = processor.swap_face(base_image, selected_face)
1039
+ if swapped is None:
1040
+ gr.Warning(message)
1041
+ remove_ready, video_ready, bg_ready, swap_ready = compute_button_states(
1042
+ input_img,
1043
+ last_output,
1044
+ current_bg_removed,
1045
+ selected_bg,
1046
+ selected_face,
1047
+ )
1048
+ yield (
1049
+ last_output,
1050
+ gr.update(value=base_image),
1051
+ make_download_update(last_output),
1052
+ history_update(),
1053
+ remove_ready,
1054
+ video_ready,
1055
+ bg_ready,
1056
+ swap_ready,
1057
+ )
1058
+ return
1059
+
1060
+ gr.Info(message)
1061
+ add_to_history(swapped, "face_swap")
1062
+ remove_ready, video_ready, bg_ready, swap_ready = compute_button_states(
1063
+ input_img,
1064
+ swapped,
1065
+ current_bg_removed,
1066
+ selected_bg,
1067
+ selected_face,
1068
+ )
1069
+ yield (
1070
+ swapped,
1071
+ gr.update(value=swapped),
1072
+ make_download_update(swapped, "face_swap"),
1073
+ history_update(),
1074
+ remove_ready,
1075
+ video_ready,
1076
+ bg_ready,
1077
+ swap_ready,
1078
+ )
1079
+
1080
+ input_image.change(
1081
+ on_upload,
1082
+ inputs=[input_image],
1083
+ outputs=[
1084
+ input_state,
1085
+ bg_removed_state,
1086
+ selected_bg_state,
1087
+ selected_face_state,
1088
+ last_output_state,
1089
+ output_image,
1090
+ download_btn,
1091
+ video_output,
1092
+ remove_bg_btn,
1093
+ generate_video_btn,
1094
+ generate_bg_btn,
1095
+ swap_face_btn,
1096
+ history_gallery,
1097
+ ],
1098
+ )
1099
+
1100
+ remove_bg_btn.click(
1101
+ handle_remove_background,
1102
+ inputs=[
1103
+ input_state,
1104
+ bg_removed_state,
1105
+ selected_bg_state,
1106
+ last_output_state,
1107
+ selected_face_state,
1108
+ ],
1109
+ outputs=[
1110
+ bg_removed_state,
1111
+ last_output_state,
1112
+ output_image,
1113
+ download_btn,
1114
+ generate_bg_btn,
1115
+ swap_face_btn,
1116
+ remove_bg_btn,
1117
+ generate_video_btn,
1118
+ history_gallery,
1119
+ ],
1120
+ )
1121
+
1122
+ generate_video_btn.click(
1123
+ on_generate_video,
1124
+ inputs=[
1125
+ last_output_state,
1126
+ input_state,
1127
+ bg_removed_state,
1128
+ selected_bg_state,
1129
+ selected_face_state,
1130
+ ],
1131
+ outputs=[
1132
+ video_output,
1133
+ history_gallery,
1134
+ remove_bg_btn,
1135
+ generate_video_btn,
1136
+ generate_bg_btn,
1137
+ swap_face_btn,
1138
+ ],
1139
+ )
1140
+
1141
+ indoor_gallery.select(
1142
+ on_select_background,
1143
+ inputs=[gr.State("indoor"), bg_removed_state],
1144
+ outputs=[selected_bg_state, generate_bg_btn],
1145
+ )
1146
+
1147
+ outdoor_gallery.select(
1148
+ on_select_background,
1149
+ inputs=[gr.State("outdoor"), bg_removed_state],
1150
+ outputs=[selected_bg_state, generate_bg_btn],
1151
+ )
1152
+
1153
+ generate_bg_btn.click(
1154
+ on_generate_background,
1155
+ inputs=[
1156
+ last_output_state,
1157
+ selected_bg_state,
1158
+ hq_checkbox,
1159
+ input_state,
1160
+ bg_removed_state,
1161
+ selected_face_state,
1162
+ ],
1163
+ outputs=[
1164
+ last_output_state,
1165
+ output_image,
1166
+ download_btn,
1167
+ history_gallery,
1168
+ remove_bg_btn,
1169
+ generate_video_btn,
1170
+ generate_bg_btn,
1171
+ swap_face_btn,
1172
+ ],
1173
+ )
1174
+
1175
+ man_gallery.select(
1176
+ on_select_face,
1177
+ inputs=[gr.State("man"), last_output_state, input_state],
1178
+ outputs=[selected_face_state, swap_face_btn],
1179
+ )
1180
+
1181
+ woman_gallery.select(
1182
+ on_select_face,
1183
+ inputs=[gr.State("woman"), last_output_state, input_state],
1184
+ outputs=[selected_face_state, swap_face_btn],
1185
+ )
1186
+
1187
+ swap_face_btn.click(
1188
+ on_swap_face,
1189
+ inputs=[
1190
+ last_output_state,
1191
+ input_state,
1192
+ selected_face_state,
1193
+ bg_removed_state,
1194
+ selected_bg_state,
1195
+ ],
1196
+ outputs=[
1197
+ last_output_state,
1198
+ output_image,
1199
+ download_btn,
1200
+ history_gallery,
1201
+ remove_bg_btn,
1202
+ generate_video_btn,
1203
+ generate_bg_btn,
1204
+ swap_face_btn,
1205
+ ],
1206
+ )
1207
+
1208
+
1209
+ return demo
1210
+
1211
+ if __name__ == "__main__":
1212
+ app = create_app()
1213
+ app.launch(
1214
+ server_name="0.0.0.0",
1215
+ server_port=7860,
1216
+ share=False,
1217
+ show_error=True,
1218
+ auth=auth_fn
1219
+ )
assets/.DS_Store ADDED
Binary file (6.15 kB). View file
 
assets/POLYGOM_VIDEO_ver2_original.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7d042e834dac52034113fd56d3e9d950bfb3a85c1386dccfd84d6b3e88c80957
3
+ size 14848608
assets/faces/.DS_Store ADDED
Binary file (6.15 kB). View file
 
assets/faces/man/001.png ADDED

Git LFS Details

  • SHA256: 33cec6e6305c5361a2d82338d71e0f68497c522eaf642cedb1a684b8585a652d
  • Pointer size: 131 Bytes
  • Size of remote file: 611 kB
assets/faces/man/002.png ADDED

Git LFS Details

  • SHA256: f1da5a08e127140e40b2ae6dc379690c56996632b75bc9cb5aef4b422e77e73c
  • Pointer size: 132 Bytes
  • Size of remote file: 1.11 MB
assets/faces/man/003.png ADDED

Git LFS Details

  • SHA256: efa2386f3bb1380f01fd5956db70fecd6118ac1f7a223975a190dbe91c0582fc
  • Pointer size: 132 Bytes
  • Size of remote file: 1.33 MB
assets/faces/man/004.png ADDED

Git LFS Details

  • SHA256: 6cc05b4094c08e0c78a2c9404193c890e9192dc0185b508803ab6d041817427d
  • Pointer size: 132 Bytes
  • Size of remote file: 1.3 MB
assets/faces/man/005.png ADDED

Git LFS Details

  • SHA256: 52b74f683dbd867094a3d61d4defd34e731619cad190fde9d97aad01a5d461e0
  • Pointer size: 132 Bytes
  • Size of remote file: 1.31 MB
assets/faces/man/006.png ADDED

Git LFS Details

  • SHA256: e09a5409705d48d4fbaa37981ff3ac4aff98b4ae2efe501b88df7b5894472eea
  • Pointer size: 132 Bytes
  • Size of remote file: 1.32 MB
assets/faces/man/007.png ADDED

Git LFS Details

  • SHA256: 00c008c2334712baaa40bbfd17b324884322a153b8844716f78952aa23facdee
  • Pointer size: 132 Bytes
  • Size of remote file: 1.07 MB
assets/faces/man/008.png ADDED

Git LFS Details

  • SHA256: 2cf8ee93858c559ff8ac1e9f20d1beae4fcef2f9f6b62141edefdf20616d7516
  • Pointer size: 132 Bytes
  • Size of remote file: 1.71 MB
assets/faces/woman/001.png ADDED

Git LFS Details

  • SHA256: 4ba2de2df8f5ab9cb741a857bf2b2eaee10e34067fd81e89009e81ccff12b011
  • Pointer size: 132 Bytes
  • Size of remote file: 1.17 MB
assets/faces/woman/002.png ADDED

Git LFS Details

  • SHA256: 14ce72d69eb7b16dbf9d09167fe80f02747eee08199039c4eaf9e8695d9e7481
  • Pointer size: 132 Bytes
  • Size of remote file: 1.12 MB
assets/faces/woman/003.png ADDED

Git LFS Details

  • SHA256: 4b704f984433b5da3e61d73d840432c204792828ec9e611f98d27ef06ae772ce
  • Pointer size: 132 Bytes
  • Size of remote file: 1.56 MB
assets/faces/woman/004.png ADDED

Git LFS Details

  • SHA256: e30ed1a55089a8e0b1a91471858edfff47cce2a2696bcfed2bec1ba1223ce8a6
  • Pointer size: 132 Bytes
  • Size of remote file: 1.09 MB
assets/faces/woman/005.png ADDED

Git LFS Details

  • SHA256: 3bc6782310cfc93bf5ba905e8a50de93418535f994e7f4df0935b028bc348aa4
  • Pointer size: 132 Bytes
  • Size of remote file: 1.17 MB
assets/faces/woman/006.png ADDED

Git LFS Details

  • SHA256: 5765c0da746e4b00ed357fc6158aad4880dcc8935e29e589e4b131b5b2cdb840
  • Pointer size: 132 Bytes
  • Size of remote file: 1.41 MB
assets/faces/woman/007.png ADDED

Git LFS Details

  • SHA256: 4f870c70f38f8d8387821adcda60ee922c144163e34444ae313cf7b812807945
  • Pointer size: 132 Bytes
  • Size of remote file: 1.59 MB
assets/faces/woman/008.png ADDED

Git LFS Details

  • SHA256: f13c4cbb89ff372eff01bc589930c8f0919c6543691c0f5819cc6b64581940cc
  • Pointer size: 132 Bytes
  • Size of remote file: 1.68 MB
assets/indoor/001.png ADDED

Git LFS Details

  • SHA256: e86be7c59d59a1e11e4fc0df8271944c6ef8cc9384069f3db245edd857ef11a2
  • Pointer size: 131 Bytes
  • Size of remote file: 747 kB
assets/indoor/003.png ADDED

Git LFS Details

  • SHA256: 6dab739162520e710ec74e1a848e02d4dc33a76808190496320de1bd99bc0e61
  • Pointer size: 131 Bytes
  • Size of remote file: 839 kB
assets/indoor/004.png ADDED

Git LFS Details

  • SHA256: 06bdd47916183ab371d324a5786967f4d75b320e80e4ad2c814e303ed7867b94
  • Pointer size: 132 Bytes
  • Size of remote file: 2.99 MB
assets/indoor/005.png ADDED

Git LFS Details

  • SHA256: a8d984cee48f2f12af6bbc5248fee7d4cd209071a5deb4dc32cf25a00bb0605a
  • Pointer size: 132 Bytes
  • Size of remote file: 2.01 MB
assets/indoor/006.png ADDED

Git LFS Details

  • SHA256: cabbfe0546a772325f536cb38be24628fb9eded81bd7fe33d8e705cfbb488ed5
  • Pointer size: 132 Bytes
  • Size of remote file: 1.84 MB
assets/indoor/007.png ADDED

Git LFS Details

  • SHA256: 644ffe8138c36261afa0d49d3e596992b4a7372d6ee76236902e887d2e14adb7
  • Pointer size: 131 Bytes
  • Size of remote file: 608 kB
assets/indoor/008.png ADDED

Git LFS Details

  • SHA256: 125536abd0eae586cd66604e56fef8932e482ae7d40fd6d1e5defbf4bde922fd
  • Pointer size: 131 Bytes
  • Size of remote file: 605 kB
assets/indoor/009.png ADDED

Git LFS Details

  • SHA256: 31c10cc449cd49e420acf46dbbd3b89d2ed62986ac521420521a18ad2c7c42ad
  • Pointer size: 132 Bytes
  • Size of remote file: 2.6 MB
assets/indoor/010.png ADDED

Git LFS Details

  • SHA256: 11ce561a723ff4c73ff1efbc1eddbabdb27e7743f5d7a2848f5492471ec94dd4
  • Pointer size: 131 Bytes
  • Size of remote file: 812 kB
assets/indoor/011.png ADDED

Git LFS Details

  • SHA256: d0491a348dfb9221028f087e20df8107b7c3b9229ad8c07e9c7befaa4b6f60a1
  • Pointer size: 132 Bytes
  • Size of remote file: 4.48 MB
assets/indoor/012.png ADDED

Git LFS Details

  • SHA256: a2d556e9a3934b6e89ac987624ebefc58e639ff60af58bcf05bde5956861dd8e
  • Pointer size: 131 Bytes
  • Size of remote file: 996 kB
assets/outdoor/001.png ADDED

Git LFS Details

  • SHA256: 97d7a17e2ac12a8bb1c03b6da93e946b35f43f829fa0220420006416ce121071
  • Pointer size: 132 Bytes
  • Size of remote file: 1.1 MB
assets/outdoor/002.png ADDED

Git LFS Details

  • SHA256: be0ec40dcb88bdf714274213671656e9bdad8f02782eff5c9cdc00b6f2a0c7e0
  • Pointer size: 132 Bytes
  • Size of remote file: 1.19 MB
assets/outdoor/003.png ADDED

Git LFS Details

  • SHA256: 2636192cbb8bf83d5679306e7a41693a09c5d9abb2916cd9c8585da057cf3024
  • Pointer size: 131 Bytes
  • Size of remote file: 480 kB
assets/outdoor/004.png ADDED

Git LFS Details

  • SHA256: 133c92ee439d10cee08187cbe10dd82d8b96c725a38224473cb2988f559eca6d
  • Pointer size: 132 Bytes
  • Size of remote file: 6.04 MB
assets/outdoor/005.png ADDED

Git LFS Details

  • SHA256: 4a3fa1a5c3d343069141772fb11713931d519b7d1622a8abc6cd0ee2f31a6e5a
  • Pointer size: 132 Bytes
  • Size of remote file: 1.55 MB
assets/outdoor/006.png ADDED

Git LFS Details

  • SHA256: 2bfc4fc06db1d570c0e40ca438d1d2b13d0b5b292ae36d247f71439bf232ca4e
  • Pointer size: 132 Bytes
  • Size of remote file: 1.42 MB
assets/outdoor/007.png ADDED

Git LFS Details

  • SHA256: 77fd0afe2bc58b51a8d90932653ab79fa57e4515ea8ee46e8e99ccc0c999392b
  • Pointer size: 132 Bytes
  • Size of remote file: 1.56 MB
assets/outdoor/008.png ADDED

Git LFS Details

  • SHA256: 15e09295e6e45926d00ef0daf690b210bc903c74727063650a5f8916028f1d69
  • Pointer size: 132 Bytes
  • Size of remote file: 1.74 MB
assets/outdoor/009.png ADDED

Git LFS Details

  • SHA256: 2de3552341d13faccfbccf0e1e2810f54bc197cf8c62db9a003a5f5b9f89e84d
  • Pointer size: 132 Bytes
  • Size of remote file: 1.25 MB
assets/outdoor/010.png ADDED

Git LFS Details

  • SHA256: 2964dbea528fdc10b044e51754bce6ac155a9e9edf83e05f9e403e90a8cc6adb
  • Pointer size: 132 Bytes
  • Size of remote file: 1.3 MB
assets/outdoor/011.png ADDED

Git LFS Details

  • SHA256: 5bb7ff6156c13a1620384108bb410e14391c5f6c54f64aa984162fb8e040c09e
  • Pointer size: 132 Bytes
  • Size of remote file: 1.41 MB
assets/outdoor/012.png ADDED

Git LFS Details

  • SHA256: 53f66cabade7f37b3f7ed5223a2149fd616b70ea11d0acee3d20a9d3441d4ffd
  • Pointer size: 132 Bytes
  • Size of remote file: 1.01 MB
assets/outdoor/013.png ADDED

Git LFS Details

  • SHA256: 1c0136bf396eba40cfb7ab00ae3ad6377703bad1f7d249120d3b7e5a80bf1d93
  • Pointer size: 132 Bytes
  • Size of remote file: 1.33 MB
assets/outdoor/014.png ADDED

Git LFS Details

  • SHA256: fee389d93c8903f70526acdfd41f4fc7edd301765fb1247fa42876882d32fccd
  • Pointer size: 132 Bytes
  • Size of remote file: 1.1 MB
assets/outdoor/015.png ADDED

Git LFS Details

  • SHA256: fba97edd4f55735a58c4e8a0de49b87453c70d011e0fc59558e31fda39197782
  • Pointer size: 132 Bytes
  • Size of remote file: 6.35 MB