Update app.py
Browse files
app.py
CHANGED
@@ -8,13 +8,12 @@ import traceback
|
|
8 |
import requests
|
9 |
from PIL import Image
|
10 |
import gradio as gr
|
11 |
-
from
|
12 |
-
|
|
|
|
|
13 |
|
14 |
-
#
|
15 |
-
load_dotenv()
|
16 |
-
|
17 |
-
# 로깅 설정 (민감한 정보 제외)
|
18 |
logging.basicConfig(
|
19 |
level=logging.INFO,
|
20 |
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
@@ -25,164 +24,71 @@ logging.basicConfig(
|
|
25 |
)
|
26 |
logger = logging.getLogger("image-enhancer-app")
|
27 |
|
28 |
-
#
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
if not backgrounds_code:
|
47 |
-
logger.warning("BACKGROUNDS_DATA environment variable is empty")
|
48 |
-
return {}, {}, {}, {}, {}, {}, {}
|
49 |
|
50 |
try:
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
local_vars.get("STUDIO_BACKGROUNDS", {}),
|
58 |
-
local_vars.get("NATURE_BACKGROUNDS", {}),
|
59 |
-
local_vars.get("INDOOR_BACKGROUNDS", {}),
|
60 |
-
local_vars.get("SPECIAL_BACKGROUNDS", {}),
|
61 |
-
local_vars.get("JEWELRY_BACKGROUNDS", {}),
|
62 |
-
local_vars.get("SPECIAL_EFFECTS_BACKGROUNDS", {})
|
63 |
-
)
|
64 |
-
except Exception as e:
|
65 |
-
logger.error(f"Failed to parse BACKGROUNDS_DATA: {e}")
|
66 |
-
return {}, {}, {}, {}, {}, {}, {}
|
67 |
-
|
68 |
-
# 환경변수에서 배경 딕셔너리들 로드
|
69 |
-
(SIMPLE_BACKGROUNDS, STUDIO_BACKGROUNDS, NATURE_BACKGROUNDS,
|
70 |
-
INDOOR_BACKGROUNDS, SPECIAL_BACKGROUNDS, JEWELRY_BACKGROUNDS,
|
71 |
-
SPECIAL_EFFECTS_BACKGROUNDS) = load_backgrounds_from_env()
|
72 |
|
73 |
-
#
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
"파스텔 그라데이션": "pastel gradient background",
|
83 |
-
"스카이블루 파스텔": "sky blue pastel",
|
84 |
-
"버터옐로우 파스텔": "butter yellow pastel",
|
85 |
-
"블루 원색": "pure blue background",
|
86 |
-
"레드 원색": "pure red background"
|
87 |
-
}
|
88 |
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
"소프트 핑크 벚꽃": "soft pink cherry blossom",
|
95 |
-
"파스텔 옐로우 꽃봉오리": "pastel yellow buds",
|
96 |
-
"아이보리 로즈 심플": "ivory rose simple",
|
97 |
-
"심플 화이트 작은 화분": "simple white small pot",
|
98 |
-
"퍼플 장난스러운 심비로운 분위기": "purple playful mood",
|
99 |
-
"코지 데스크 플랜트": "cozy desk plant",
|
100 |
-
"연핑크 튤립 작은테이블": "light pink tulip small table",
|
101 |
-
"오크우드 튤립 샤워 푸프": "oak wood tulip shower puff",
|
102 |
-
"크림 코튼": "cream cotton",
|
103 |
-
"브라운 거친 페이퍼": "brown rough paper"
|
104 |
-
}
|
105 |
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
"눈내리는 산악 설원": "snowy mountain field",
|
111 |
-
"열대해변": "tropical beach",
|
112 |
-
"일출 직전의 바위산": "rocky mountain before sunrise",
|
113 |
-
"초근접 봄꽃 들판": "close-up spring flower field",
|
114 |
-
"물가 바위": "waterside rocks",
|
115 |
-
"작은 돌이 깔린 얕은 물가": "shallow water with small stones",
|
116 |
-
"나무 테이블 숲속 계곡": "wooden table forest valley",
|
117 |
-
"안개 낀 숲": "foggy forest",
|
118 |
-
"일몰 바다": "sunset sea",
|
119 |
-
"별이 빛나는 캠핑": "starry camping"
|
120 |
-
}
|
121 |
|
122 |
-
|
123 |
-
|
124 |
-
"기본 책상": "basic desk",
|
125 |
-
"빛이 비치는 책상": "desk with light",
|
126 |
-
"빛이 비치는 거실": "living room with light",
|
127 |
-
"스튜디어 거실": "studio living room",
|
128 |
-
"화분이 있는 거실": "living room with plants",
|
129 |
-
"전체적인 거실모습": "overall living room view",
|
130 |
-
"포인트 거실": "point living room",
|
131 |
-
"중앙 거실": "central living room",
|
132 |
-
"서랍테이블": "drawer table",
|
133 |
-
"침실 탁자위": "bedroom table top",
|
134 |
-
"원목 테이블 블록": "wood table block",
|
135 |
-
"테이블 심플": "simple table",
|
136 |
-
"분위기 있는 침실": "atmospheric bedroom",
|
137 |
-
"나무테이블위": "on wooden table",
|
138 |
-
"책위 거실테이블": "living room table with books",
|
139 |
-
"유리테이블위": "on glass table",
|
140 |
-
"침실 옆 작은탁자": "small bedside table",
|
141 |
-
"분위기 있는 원형 테이블": "atmospheric round table",
|
142 |
-
"포커스 탁자": "focus table",
|
143 |
-
"투톤 벽면": "two-tone wall",
|
144 |
-
"아늑한 테이블위 액세서리": "cozy table accessories",
|
145 |
-
"아늑한 테이블 보 위": "on cozy tablecloth",
|
146 |
-
"유리 바닥": "glass floor",
|
147 |
-
"빛치는 식탁위": "dining table with light",
|
148 |
-
"나늑한 식탁위 작업대": "cozy dining workspace"
|
149 |
-
}
|
150 |
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
|
156 |
-
"
|
157 |
-
|
158 |
-
"
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
JEWELRY_BACKGROUNDS = {
|
164 |
-
"화이트 미러 스팟 라이트": "white mirror spotlight",
|
165 |
-
"그레이 그라데이션 미러": "gray gradient mirror",
|
166 |
-
"네이비 벨벳": "navy velvet",
|
167 |
-
"블랙 미러 시네마틱": "black mirror cinematic",
|
168 |
-
"화이트 마블 프리미엄": "white marble premium",
|
169 |
-
"파스텔 블루 큐브 플랫폼": "pastel blue cube platform",
|
170 |
-
"내추럴 그라스": "natural grass",
|
171 |
-
"소프트 ���이직 패브릭": "soft basic fabric",
|
172 |
-
"마이크로텍스쳐 프리미엄": "micro texture premium"
|
173 |
-
}
|
174 |
-
|
175 |
-
if not SPECIAL_EFFECTS_BACKGROUNDS:
|
176 |
-
SPECIAL_EFFECTS_BACKGROUNDS = {
|
177 |
-
"블루블랙 큰 물방울 효과": "blue black water drop effect",
|
178 |
-
"크리스탈 버블 물속 장면": "crystal bubble underwater scene",
|
179 |
-
"잔 물결 수면 위 장면": "gentle ripple water surface scene",
|
180 |
-
"컬러 스모크 효과": "color smoke effect",
|
181 |
-
"자옥한 안개 효과": "dense fog effect",
|
182 |
-
"가습기 수중기 효과": "humidifier mist effect"
|
183 |
-
}
|
184 |
|
185 |
-
# 임시 파일 저장 함수
|
186 |
def save_uploaded_file(uploaded_file, suffix='.png'):
|
187 |
try:
|
188 |
logger.info(f"Processing uploaded file: {type(uploaded_file)}")
|
@@ -224,7 +130,406 @@ def save_uploaded_file(uploaded_file, suffix='.png'):
|
|
224 |
logger.error(traceback.format_exc())
|
225 |
return None
|
226 |
|
227 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
228 |
def create_gradio_interface():
|
229 |
try:
|
230 |
logger.info("Creating Gradio interface")
|
@@ -310,29 +615,22 @@ def create_gradio_interface():
|
|
310 |
interactive=True
|
311 |
)
|
312 |
|
313 |
-
# 드롭다운 변경 함수
|
314 |
def update_dropdowns(bg_type):
|
315 |
-
|
316 |
-
|
317 |
-
|
318 |
-
|
319 |
-
|
320 |
-
|
321 |
-
|
322 |
-
|
323 |
-
|
324 |
-
nature_dropdown: gr.update(visible=(bg_type == "자연 환경")),
|
325 |
-
indoor_dropdown: gr.update(visible=(bg_type == "실내 환경")),
|
326 |
-
special_dropdown: gr.update(visible=(bg_type == "특수배경")),
|
327 |
-
jewelry_dropdown: gr.update(visible=(bg_type == "주얼리")),
|
328 |
-
special_effects_dropdown: gr.update(visible=(bg_type == "특수효과"))
|
329 |
-
}
|
330 |
|
331 |
background_type.change(
|
332 |
fn=update_dropdowns,
|
333 |
inputs=[background_type],
|
334 |
-
outputs=[simple_dropdown, studio_dropdown, nature_dropdown, indoor_dropdown, special_dropdown, jewelry_dropdown, special_effects_dropdown]
|
335 |
-
api_name="update_dropdowns"
|
336 |
)
|
337 |
|
338 |
# 요청사항 입력
|
@@ -347,7 +645,7 @@ def create_gradio_interface():
|
|
347 |
label="품질 레벨",
|
348 |
choices=["gpt", "flux"],
|
349 |
value="flux",
|
350 |
-
info="GPT: GPT 모델 (고품질),
|
351 |
)
|
352 |
|
353 |
aspect_ratio = gr.Dropdown(
|
@@ -393,190 +691,118 @@ def create_gradio_interface():
|
|
393 |
info = gr.Textbox(label="처리 정보", interactive=False)
|
394 |
error = gr.Textbox(label="오류 메시지", interactive=False, visible=True)
|
395 |
|
396 |
-
# 프롬프트만 생성하는 함수 (
|
397 |
def generate_prompt_with_password_check(password, bg_type, simple, studio, nature, indoor, special, jewelry, special_effects, request_text, aspect_ratio):
|
398 |
-
|
399 |
-
|
400 |
-
|
401 |
-
|
402 |
-
|
403 |
-
|
404 |
-
|
405 |
-
|
406 |
-
|
407 |
-
|
408 |
-
|
409 |
-
|
410 |
-
|
411 |
-
|
412 |
-
|
413 |
-
|
414 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
415 |
|
416 |
-
# 비밀번호 확인 함수
|
417 |
def check_password(password, *args):
|
418 |
-
|
419 |
-
|
420 |
-
|
421 |
-
|
422 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
423 |
|
424 |
-
#
|
425 |
-
|
426 |
-
|
427 |
-
|
|
|
|
|
|
|
|
|
428 |
|
429 |
-
|
430 |
-
|
431 |
-
|
432 |
-
|
433 |
-
|
434 |
-
|
435 |
-
|
436 |
-
|
437 |
-
|
438 |
-
|
439 |
-
param_7=special,
|
440 |
-
param_8=jewelry,
|
441 |
-
param_9=special_effects,
|
442 |
-
param_10=request_text,
|
443 |
-
param_11=quality_level,
|
444 |
-
param_12=aspect_ratio,
|
445 |
-
param_13=output_format,
|
446 |
-
param_14=enable_enhancement,
|
447 |
-
api_name="/check_password"
|
448 |
-
)
|
449 |
-
|
450 |
-
# API 응답 처리 - 딕셔너리를 원본처럼 PIL Image로 변환
|
451 |
-
if result and len(result) >= 7:
|
452 |
-
original_output, original_download, enhanced_output, enhanced_download, prompt_output, info, error = result
|
453 |
-
|
454 |
-
# 디버깅: API 응답 구조 로깅
|
455 |
-
logger.info(f"Original output type: {type(original_output)}")
|
456 |
-
logger.info(f"Enhanced output type: {type(enhanced_output)}")
|
457 |
-
if original_output:
|
458 |
-
logger.info(f"Original output first item: {type(original_output[0]) if original_output else 'Empty'}")
|
459 |
-
if original_output and isinstance(original_output[0], dict):
|
460 |
-
logger.info(f"Original output keys: {list(original_output[0].keys())}")
|
461 |
-
|
462 |
-
# Gallery 데이터를 PIL Image 리스트로 변환
|
463 |
-
def convert_gallery_to_images(gallery_data):
|
464 |
-
if not gallery_data:
|
465 |
-
return []
|
466 |
-
|
467 |
-
images = []
|
468 |
-
logger.info(f"Converting gallery data, length: {len(gallery_data)}")
|
469 |
-
|
470 |
-
for i, item in enumerate(gallery_data):
|
471 |
-
try:
|
472 |
-
logger.info(f"Processing gallery item {i}: {type(item)}")
|
473 |
-
|
474 |
-
img_url = None
|
475 |
-
if isinstance(item, dict):
|
476 |
-
# 여러 가지 가능한 구조 시도
|
477 |
-
if 'image' in item:
|
478 |
-
if isinstance(item['image'], dict):
|
479 |
-
img_url = item['image'].get('url') or item['image'].get('path')
|
480 |
-
else:
|
481 |
-
img_url = item['image']
|
482 |
-
elif 'url' in item:
|
483 |
-
img_url = item['url']
|
484 |
-
elif 'path' in item:
|
485 |
-
img_url = item['path']
|
486 |
-
|
487 |
-
logger.info(f"Extracted URL from dict: {img_url}")
|
488 |
-
elif isinstance(item, str):
|
489 |
-
# 직접 URL인 경우
|
490 |
-
img_url = item
|
491 |
-
logger.info(f"Direct URL: {img_url}")
|
492 |
-
|
493 |
-
if img_url:
|
494 |
-
# URL이 상대 경로인 경우 절대 경로로 변환
|
495 |
-
if img_url.startswith('/'):
|
496 |
-
img_url = f"https://happydoggg-49493h.hf.space/gradio_api/file={img_url}"
|
497 |
-
elif not img_url.startswith('http'):
|
498 |
-
img_url = f"https://happydoggg-49493h.hf.space/gradio_api/file={img_url}"
|
499 |
-
|
500 |
-
logger.info(f"Final URL: {img_url}")
|
501 |
-
|
502 |
-
# URL에서 이미지 다운로드하여 PIL Image로 변환
|
503 |
-
response = requests.get(img_url)
|
504 |
-
if response.status_code == 200:
|
505 |
-
pil_image = Image.open(io.BytesIO(response.content))
|
506 |
-
images.append(pil_image)
|
507 |
-
logger.info(f"Successfully converted image {i}")
|
508 |
-
else:
|
509 |
-
logger.warning(f"Failed to download image from {img_url}, status: {response.status_code}")
|
510 |
-
else:
|
511 |
-
logger.warning(f"No URL found in item {i}: {item}")
|
512 |
-
|
513 |
-
except Exception as e:
|
514 |
-
logger.error(f"Failed to convert gallery item {i}: {e}")
|
515 |
-
logger.error(f"Item content: {item}")
|
516 |
-
continue
|
517 |
-
|
518 |
-
logger.info(f"Converted {len(images)} images from gallery data")
|
519 |
-
return images
|
520 |
-
|
521 |
-
# Gallery 데이터 변환
|
522 |
-
converted_original = convert_gallery_to_images(original_output)
|
523 |
-
converted_enhanced = convert_gallery_to_images(enhanced_output)
|
524 |
-
|
525 |
-
# 변환된 이미지가 없는 경우, 원본 데이터를 그대로 반환 시도
|
526 |
-
if not converted_original and original_output:
|
527 |
-
logger.info("No converted original images, trying to use original data directly")
|
528 |
-
converted_original = original_output
|
529 |
-
|
530 |
-
if not converted_enhanced and enhanced_output:
|
531 |
-
logger.info("No converted enhanced images, trying to use enhanced data directly")
|
532 |
-
converted_enhanced = enhanced_output
|
533 |
-
|
534 |
-
# 다운로드 파일 처리 - 원본 그대로 전달
|
535 |
-
original_file = None
|
536 |
-
enhanced_file = None
|
537 |
-
|
538 |
-
# 원본 이미지 파일 생성
|
539 |
-
if converted_original:
|
540 |
-
try:
|
541 |
-
original_file_path = f"original_image.{output_format}"
|
542 |
-
converted_original[0].save(original_file_path, format=output_format.upper())
|
543 |
-
original_file = original_file_path
|
544 |
-
except Exception as e:
|
545 |
-
logger.error(f"Error saving original image: {e}")
|
546 |
-
|
547 |
-
# 개선된 이미지 파일 생성
|
548 |
-
if converted_enhanced:
|
549 |
-
try:
|
550 |
-
enhanced_file_path = f"enhanced_image.{output_format}"
|
551 |
-
converted_enhanced[0].save(enhanced_file_path, format=output_format.upper())
|
552 |
-
enhanced_file = enhanced_file_path
|
553 |
-
except Exception as e:
|
554 |
-
logger.error(f"Error saving enhanced image: {e}")
|
555 |
-
|
556 |
-
return (
|
557 |
-
converted_original, # PIL Image 리스트
|
558 |
-
original_file, # 다운로드 파일 경로
|
559 |
-
converted_enhanced, # PIL Image 리스트
|
560 |
-
enhanced_file, # 다운로드 파일 경로
|
561 |
-
prompt_output, # 프롬프트 텍스트
|
562 |
-
info, # 처리 정보
|
563 |
-
error # 오류 메시지
|
564 |
-
)
|
565 |
-
else:
|
566 |
-
return ([], None, [], None, "", "", "API 응답이 올바르지 않습니다.")
|
567 |
-
|
568 |
-
finally:
|
569 |
-
# 임시 파일 정리
|
570 |
-
if os.path.exists(temp_path):
|
571 |
-
try:
|
572 |
-
os.remove(temp_path)
|
573 |
-
except:
|
574 |
-
pass
|
575 |
-
|
576 |
-
except Exception as e:
|
577 |
-
logger.error(f"Error in check_password: {e}")
|
578 |
-
logger.error(f"Full traceback: {traceback.format_exc()}")
|
579 |
-
return ([], None, [], None, "", "", f"오류 발생: {str(e)}")
|
580 |
|
581 |
# 프롬프트 생성 버튼 클릭 이벤트
|
582 |
generate_prompt_btn.click(
|
@@ -588,8 +814,7 @@ def create_gradio_interface():
|
|
588 |
jewelry_dropdown, special_effects_dropdown,
|
589 |
request_text, aspect_ratio
|
590 |
],
|
591 |
-
outputs=[prompt_output]
|
592 |
-
api_name="generate_prompt_with_password_check"
|
593 |
)
|
594 |
|
595 |
# 편집 버튼 클릭 이벤트
|
@@ -606,8 +831,7 @@ def create_gradio_interface():
|
|
606 |
original_output, original_download,
|
607 |
enhanced_output, enhanced_download,
|
608 |
prompt_output, info, error
|
609 |
-
]
|
610 |
-
api_name="check_password"
|
611 |
)
|
612 |
|
613 |
logger.info("Gradio interface created successfully")
|
|
|
8 |
import requests
|
9 |
from PIL import Image
|
10 |
import gradio as gr
|
11 |
+
from openai import OpenAI
|
12 |
+
import replicate
|
13 |
+
from google import genai
|
14 |
+
from google.genai import types
|
15 |
|
16 |
+
# 로깅 설정
|
|
|
|
|
|
|
17 |
logging.basicConfig(
|
18 |
level=logging.INFO,
|
19 |
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
|
24 |
)
|
25 |
logger = logging.getLogger("image-enhancer-app")
|
26 |
|
27 |
+
# 환경변수로부터 배경 설정 로드
|
28 |
+
def load_backgrounds():
|
29 |
+
"""환경변수 BACKGROUNDS_CONFIG로부터 배경 설정을 로드합니다."""
|
30 |
+
import json
|
31 |
+
|
32 |
+
backgrounds_json = os.environ.get("BACKGROUNDS_CONFIG", "")
|
33 |
+
|
34 |
+
if not backgrounds_json:
|
35 |
+
logger.warning("BACKGROUNDS_CONFIG 환경변수가 설정되지 않았습니다. 기본값을 사용합니다.")
|
36 |
+
return {
|
37 |
+
"SIMPLE_BACKGROUNDS": {"기본 화이트": "clean white background"},
|
38 |
+
"STUDIO_BACKGROUNDS": {"기본 스튜디오": "studio background"},
|
39 |
+
"NATURE_BACKGROUNDS": {"기본 자연": "nature background"},
|
40 |
+
"INDOOR_BACKGROUNDS": {"기본 실내": "indoor background"},
|
41 |
+
"SPECIAL_BACKGROUNDS": {"기본 특수": "special background"},
|
42 |
+
"JEWELRY_BACKGROUNDS": {"기본 주얼리": "jewelry background"},
|
43 |
+
"SPECIAL_EFFECTS_BACKGROUNDS": {"기본 효과": "special effects background"}
|
44 |
+
}
|
|
|
|
|
|
|
45 |
|
46 |
try:
|
47 |
+
backgrounds_data = json.loads(backgrounds_json)
|
48 |
+
logger.info("환경변수로부터 배경 설정을 성공적으로 로드했습니다.")
|
49 |
+
return backgrounds_data
|
50 |
+
except json.JSONDecodeError as e:
|
51 |
+
logger.error(f"배경 설정 JSON 파싱 오류: {e}")
|
52 |
+
return {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
53 |
|
54 |
+
# 배경 설정 로드
|
55 |
+
backgrounds_data = load_backgrounds()
|
56 |
+
SIMPLE_BACKGROUNDS = backgrounds_data.get("SIMPLE_BACKGROUNDS", {})
|
57 |
+
STUDIO_BACKGROUNDS = backgrounds_data.get("STUDIO_BACKGROUNDS", {})
|
58 |
+
NATURE_BACKGROUNDS = backgrounds_data.get("NATURE_BACKGROUNDS", {})
|
59 |
+
INDOOR_BACKGROUNDS = backgrounds_data.get("INDOOR_BACKGROUNDS", {})
|
60 |
+
SPECIAL_BACKGROUNDS = backgrounds_data.get("SPECIAL_BACKGROUNDS", {})
|
61 |
+
JEWELRY_BACKGROUNDS = backgrounds_data.get("JEWELRY_BACKGROUNDS", {})
|
62 |
+
SPECIAL_EFFECTS_BACKGROUNDS = backgrounds_data.get("SPECIAL_EFFECTS_BACKGROUNDS", {})
|
|
|
|
|
|
|
|
|
|
|
|
|
63 |
|
64 |
+
# 환경변수로부터 모델 설정 로드
|
65 |
+
IMAGE_EDIT_MODEL_GPT = os.environ.get("IMAGE_EDIT_MODEL_GPT", "gpt-image-1")
|
66 |
+
IMAGE_EDIT_MODEL_FLUX = os.environ.get("IMAGE_EDIT_MODEL_FLUX", "black-forest-labs/flux-kontext-pro")
|
67 |
+
TRANSLATION_MODEL = os.environ.get("TRANSLATION_MODEL", "gemini-2.0-flash")
|
68 |
+
ENHANCEMENT_MODEL = os.environ.get("ENHANCEMENT_MODEL", "philz1337x/clarity-upscaler:dfad41707589d68ecdccd1dfa600d55a208f9310748e44bfe35b4a6291453d5e")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
69 |
|
70 |
+
logger.info(f"이미지 편집 모델 (GPT): {IMAGE_EDIT_MODEL_GPT}")
|
71 |
+
logger.info(f"이미지 편집 모델 (Flux): {IMAGE_EDIT_MODEL_FLUX}")
|
72 |
+
logger.info(f"번역 모델: {TRANSLATION_MODEL}")
|
73 |
+
logger.info(f"화질 개선 모델: {ENHANCEMENT_MODEL}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
74 |
|
75 |
+
# API 클라이언트 초기화 (안전하게)
|
76 |
+
openai_client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY", ""))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
77 |
|
78 |
+
# Gemini 클라이언트 초기화 - API 키가 있을 때만
|
79 |
+
gemini_api_key = os.environ.get("GEMINI_API_KEY")
|
80 |
+
if gemini_api_key and gemini_api_key.strip():
|
81 |
+
try:
|
82 |
+
gemini_client = genai.Client(api_key=gemini_api_key)
|
83 |
+
logger.info("Gemini client initialized successfully")
|
84 |
+
except Exception as e:
|
85 |
+
logger.error(f"Failed to initialize Gemini client: {e}")
|
86 |
+
gemini_client = None
|
87 |
+
else:
|
88 |
+
logger.warning("GEMINI_API_KEY not found or empty, Gemini client not initialized")
|
89 |
+
gemini_client = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
90 |
|
91 |
+
# 임시 파일 저장 함수
|
92 |
def save_uploaded_file(uploaded_file, suffix='.png'):
|
93 |
try:
|
94 |
logger.info(f"Processing uploaded file: {type(uploaded_file)}")
|
|
|
130 |
logger.error(traceback.format_exc())
|
131 |
return None
|
132 |
|
133 |
+
# 텍스트 번역 함수 (한국어 → 영어) - 환경변수 모델 사용
|
134 |
+
def translate_to_english(text):
|
135 |
+
"""한국어 텍스트를 영어로 번역 (환경변수 모델 사용)"""
|
136 |
+
try:
|
137 |
+
if not text or not text.strip():
|
138 |
+
return ""
|
139 |
+
|
140 |
+
# Gemini 클라이언트가 초기화되었는지 확인
|
141 |
+
if gemini_client is None:
|
142 |
+
logger.warning("Gemini client not available, returning original text")
|
143 |
+
return text
|
144 |
+
|
145 |
+
# 환경변수에서 설정된 번역 모델 사용
|
146 |
+
try:
|
147 |
+
response = gemini_client.models.generate_content(
|
148 |
+
model=TRANSLATION_MODEL,
|
149 |
+
config=types.GenerateContentConfig(
|
150 |
+
system_instruction="You are a professional translator. Translate the given Korean text to English. Keep the translation natural and contextually appropriate for image generation prompts. If the text is already in English, return it as is. Only return the translated text without any additional explanation.",
|
151 |
+
max_output_tokens=200,
|
152 |
+
temperature=0.1
|
153 |
+
),
|
154 |
+
contents=[f"Translate this to English: {text}"]
|
155 |
+
)
|
156 |
+
|
157 |
+
translated = response.text.strip()
|
158 |
+
logger.info(f"Translated '{text}' to '{translated}' using {TRANSLATION_MODEL}")
|
159 |
+
return translated
|
160 |
+
|
161 |
+
except Exception as e:
|
162 |
+
logger.error(f"Translation error with {TRANSLATION_MODEL}: {e}")
|
163 |
+
logger.warning("Translation failed, returning original text")
|
164 |
+
return text
|
165 |
+
|
166 |
+
except Exception as e:
|
167 |
+
logger.error(f"Translation error: {e}")
|
168 |
+
return text
|
169 |
+
|
170 |
+
# 프롬프트 생성 함수 (종횡비와 요청사항 통합)
|
171 |
+
def generate_prompt(background_type, background_name, user_request, aspect_ratio="1:1"):
|
172 |
+
# 기본 고정 프롬프트 (종횡비 정보 포함) - 영어로 변경
|
173 |
+
fixed_prompt = f"""
|
174 |
+
## Fixed Prompt (Required)
|
175 |
+
[Aspect Ratio: {aspect_ratio}]
|
176 |
+
[Foreground: all uploaded product images, preserve their original proportions and clarity]
|
177 |
+
[Preserve originals: keep the same random seed; maintain exact shape and aspect ratio; no vertical or horizontal scaling; do not alter any existing logos or text]
|
178 |
+
[Product sizing: ensure product images maintain at least 50% of their height relative to the background]
|
179 |
+
[Composition: products must be naturally composited with the background, maintain proper shadows aligned with lighting]
|
180 |
+
[Product placement: if products already exist in the background prompt, follow their exact arrangement and positioning]
|
181 |
+
"""
|
182 |
+
|
183 |
+
# 배경 프롬프트 선택
|
184 |
+
if background_type == "심플 배경":
|
185 |
+
background_prompt = SIMPLE_BACKGROUNDS.get(background_name, "")
|
186 |
+
elif background_type == "스튜디오 배경":
|
187 |
+
background_prompt = STUDIO_BACKGROUNDS.get(background_name, "")
|
188 |
+
elif background_type == "자연 환경":
|
189 |
+
background_prompt = NATURE_BACKGROUNDS.get(background_name, "")
|
190 |
+
elif background_type == "실내 환경":
|
191 |
+
background_prompt = INDOOR_BACKGROUNDS.get(background_name, "")
|
192 |
+
elif background_type == "특수배경":
|
193 |
+
background_prompt = SPECIAL_BACKGROUNDS.get(background_name, "")
|
194 |
+
elif background_type == "주얼리":
|
195 |
+
background_prompt = JEWELRY_BACKGROUNDS.get(background_name, "")
|
196 |
+
elif background_type == "특수효과":
|
197 |
+
background_prompt = SPECIAL_EFFECTS_BACKGROUNDS.get(background_name, "")
|
198 |
+
else:
|
199 |
+
background_prompt = "clean white background with soft even lighting"
|
200 |
+
|
201 |
+
# 사용자 요청사항 처리
|
202 |
+
if user_request and user_request.strip():
|
203 |
+
# 한국어 요청사항을 영어로 번역
|
204 |
+
translated_request = translate_to_english(user_request)
|
205 |
+
|
206 |
+
# 번역된 요청사항을 배경 프롬프트에 통합
|
207 |
+
integrated_background = f"{background_prompt} Additionally, incorporate the following elements naturally into the scene: {translated_request}. Ensure these elements blend harmoniously with the existing background while maintaining the overall aesthetic and lighting."
|
208 |
+
|
209 |
+
# 요청 프롬프트 섹션 (번역된 내용 사용)
|
210 |
+
request_prompt = f"""
|
211 |
+
## Request Prompt
|
212 |
+
{translated_request}
|
213 |
+
"""
|
214 |
+
|
215 |
+
# 배경 프롬프트 섹션
|
216 |
+
background_section = f"""
|
217 |
+
## Background Prompt (Background Settings)
|
218 |
+
{integrated_background}
|
219 |
+
"""
|
220 |
+
else:
|
221 |
+
# 요청사항이 없는 경우
|
222 |
+
request_prompt = f"""
|
223 |
+
## Request Prompt
|
224 |
+
No specific request
|
225 |
+
"""
|
226 |
+
|
227 |
+
# 요청사항이 없는 경우 기본 배경만 사용
|
228 |
+
background_section = f"""
|
229 |
+
## Background Prompt (Background Settings)
|
230 |
+
{background_prompt}
|
231 |
+
"""
|
232 |
+
|
233 |
+
# 최종 프롬프트 조합
|
234 |
+
final_prompt = fixed_prompt + request_prompt + background_section
|
235 |
+
|
236 |
+
return final_prompt
|
237 |
+
|
238 |
+
# 이미지 편집 및 화질 개선 함수
|
239 |
+
def edit_and_enhance_image(
|
240 |
+
prompt,
|
241 |
+
image,
|
242 |
+
quality_level="gpt",
|
243 |
+
aspect_ratio="1:1",
|
244 |
+
output_format="jpg",
|
245 |
+
enable_enhancement=True,
|
246 |
+
enhancement_level=2
|
247 |
+
):
|
248 |
+
try:
|
249 |
+
logger.info(f"Editing image with prompt: '{prompt[:50]}...' (truncated)")
|
250 |
+
logger.info(f"Parameters: quality_level={quality_level}, aspect_ratio={aspect_ratio}, output_format={output_format}")
|
251 |
+
logger.info(f"Enhancement requested: {enable_enhancement}, level: {enhancement_level}")
|
252 |
+
|
253 |
+
if image is None:
|
254 |
+
logger.error("No image provided")
|
255 |
+
return None, None, None, "이미지를 업로드해야 합니다."
|
256 |
+
|
257 |
+
# 이미지 처리
|
258 |
+
processed_image = None
|
259 |
+
temp_paths = [] # 나중에 정리할 경로 추적
|
260 |
+
|
261 |
+
img_path = save_uploaded_file(image)
|
262 |
+
if img_path:
|
263 |
+
logger.info(f"Saved image to temp path: {img_path}")
|
264 |
+
processed_image = open(img_path, "rb")
|
265 |
+
temp_paths.append(img_path)
|
266 |
+
else:
|
267 |
+
logger.error("Failed to save image")
|
268 |
+
return None, None, None, "이미지 처리에 실패했습니다. 다른 이미지를 업로드해 보세요."
|
269 |
+
|
270 |
+
# 모델 선택에 따른 처리
|
271 |
+
edited_images = []
|
272 |
+
usage_info = ""
|
273 |
+
error_msg = None
|
274 |
+
|
275 |
+
try:
|
276 |
+
if quality_level == "gpt":
|
277 |
+
# GPT 모델 사용
|
278 |
+
if not openai_client.api_key:
|
279 |
+
logger.error("OpenAI API key is not set")
|
280 |
+
return None, None, None, "OpenAI API 키가 설정되지 않았습니다. API 키를 설정해주세요."
|
281 |
+
|
282 |
+
# 종횡비를 크기로 변환
|
283 |
+
size_mapping = {
|
284 |
+
"1:1": "1024x1024",
|
285 |
+
"3:2": "1536x1024",
|
286 |
+
"2:3": "1024x1536"
|
287 |
+
}
|
288 |
+
size = size_mapping.get(aspect_ratio, "1024x1024")
|
289 |
+
|
290 |
+
params = {
|
291 |
+
"prompt": prompt,
|
292 |
+
"model": IMAGE_EDIT_MODEL_GPT,
|
293 |
+
"n": 1,
|
294 |
+
"size": size,
|
295 |
+
"image": processed_image
|
296 |
+
}
|
297 |
+
|
298 |
+
logger.info(f"Calling OpenAI API for image editing with model: {IMAGE_EDIT_MODEL_GPT}")
|
299 |
+
response = openai_client.images.edit(**params)
|
300 |
+
logger.info("OpenAI API call successful")
|
301 |
+
|
302 |
+
# 결과 처리
|
303 |
+
for i, data in enumerate(response.data):
|
304 |
+
logger.info(f"Processing result image {i+1}/{len(response.data)}")
|
305 |
+
|
306 |
+
if hasattr(data, 'b64_json') and data.b64_json:
|
307 |
+
image_data = base64.b64decode(data.b64_json)
|
308 |
+
image = Image.open(io.BytesIO(image_data))
|
309 |
+
elif hasattr(data, 'url') and data.url:
|
310 |
+
response_url = requests.get(data.url)
|
311 |
+
image = Image.open(io.BytesIO(response_url.content))
|
312 |
+
else:
|
313 |
+
logger.warning(f"No image data found in response item {i+1}")
|
314 |
+
continue
|
315 |
+
|
316 |
+
# 이미지 형식 변환
|
317 |
+
if output_format.lower() != "png" and image.mode == "RGBA":
|
318 |
+
background = Image.new("RGB", image.size, (255, 255, 255))
|
319 |
+
background.paste(image, mask=image.split()[3])
|
320 |
+
image = background
|
321 |
+
|
322 |
+
edited_images.append(image)
|
323 |
+
|
324 |
+
usage_info = f"이미지 편집 완료 ({IMAGE_EDIT_MODEL_GPT} 모델 사용)"
|
325 |
+
|
326 |
+
else: # quality_level == "flux"
|
327 |
+
# Flux 모델 사용 (항상 기본 화질개선 1회 적용)
|
328 |
+
if not os.environ.get("REPLICATE_API_TOKEN"):
|
329 |
+
logger.error("Replicate API token is not set")
|
330 |
+
return None, None, None, "Replicate API 토큰이 설정되지 않았습니다. API 토큰을 설정해주세요."
|
331 |
+
|
332 |
+
logger.info(f"Using Flux model for image editing: {IMAGE_EDIT_MODEL_FLUX}")
|
333 |
+
|
334 |
+
# Flux 모델로 이미지 생성
|
335 |
+
output = replicate.run(
|
336 |
+
IMAGE_EDIT_MODEL_FLUX,
|
337 |
+
input={
|
338 |
+
"prompt": prompt,
|
339 |
+
"input_image": processed_image,
|
340 |
+
"output_format": output_format.lower(),
|
341 |
+
"aspect_ratio": aspect_ratio,
|
342 |
+
"safety_tolerance": 2
|
343 |
+
}
|
344 |
+
)
|
345 |
+
|
346 |
+
logger.info(f"Flux API response received")
|
347 |
+
|
348 |
+
# Flux API 응답 처리
|
349 |
+
flux_image = None
|
350 |
+
if output:
|
351 |
+
# output이 바이트 스트림인 경우
|
352 |
+
if hasattr(output, 'read'):
|
353 |
+
image_data = output.read()
|
354 |
+
flux_image = Image.open(io.BytesIO(image_data))
|
355 |
+
# output이 URL인 경우
|
356 |
+
elif isinstance(output, str) and output.startswith('http'):
|
357 |
+
response_url = requests.get(output)
|
358 |
+
flux_image = Image.open(io.BytesIO(response_url.content))
|
359 |
+
# output이 바이너리 데이터인 경우
|
360 |
+
else:
|
361 |
+
flux_image = Image.open(io.BytesIO(output))
|
362 |
+
|
363 |
+
# 이미지 형식 변환
|
364 |
+
if output_format.lower() != "png" and flux_image.mode == "RGBA":
|
365 |
+
background = Image.new("RGB", flux_image.size, (255, 255, 255))
|
366 |
+
background.paste(flux_image, mask=flux_image.split()[3])
|
367 |
+
flux_image = background
|
368 |
+
|
369 |
+
# Flux 모델은 항상 첫 번째 화질 개선을 자동 적용
|
370 |
+
try:
|
371 |
+
logger.info(f"Applying automatic first enhancement for Flux model using: {ENHANCEMENT_MODEL}")
|
372 |
+
|
373 |
+
# 임시 파일로 저장
|
374 |
+
temp_flux_path = tempfile.mktemp(suffix='.png')
|
375 |
+
flux_image.save(temp_flux_path)
|
376 |
+
temp_paths.append(temp_flux_path)
|
377 |
+
|
378 |
+
# 첫 번째 화질 향상 (Flux 모델 기본 적용)
|
379 |
+
first_enhanced_output = replicate.run(
|
380 |
+
ENHANCEMENT_MODEL,
|
381 |
+
input={
|
382 |
+
"image": open(temp_flux_path, "rb"),
|
383 |
+
"scale_factor": 2,
|
384 |
+
"resemblance": 0.8,
|
385 |
+
"creativity": 0.2,
|
386 |
+
"output_format": output_format.lower(),
|
387 |
+
"prompt": prompt,
|
388 |
+
"negative_prompt": "(worst quality, low quality, normal quality:2)"
|
389 |
+
}
|
390 |
+
)
|
391 |
+
|
392 |
+
if first_enhanced_output and isinstance(first_enhanced_output, list) and len(first_enhanced_output) > 0:
|
393 |
+
first_enhanced_url = first_enhanced_output[0]
|
394 |
+
first_enhanced_response = requests.get(first_enhanced_url)
|
395 |
+
|
396 |
+
if first_enhanced_response.status_code == 200:
|
397 |
+
first_enhanced_image = Image.open(io.BytesIO(first_enhanced_response.content))
|
398 |
+
|
399 |
+
# 이미지 형식 변환
|
400 |
+
if output_format.lower() != "png" and first_enhanced_image.mode == "RGBA":
|
401 |
+
background = Image.new("RGB", first_enhanced_image.size, (255, 255, 255))
|
402 |
+
background.paste(first_enhanced_image, mask=first_enhanced_image.split()[3])
|
403 |
+
first_enhanced_image = background
|
404 |
+
|
405 |
+
edited_images.append(first_enhanced_image)
|
406 |
+
usage_info = f"이미지 편집 완료 ({IMAGE_EDIT_MODEL_FLUX} + 기본 화질개선 적용)"
|
407 |
+
logger.info("First enhancement completed for Flux model")
|
408 |
+
else:
|
409 |
+
# 첫 번째 화질개선 실패 시 원본 사용
|
410 |
+
edited_images.append(flux_image)
|
411 |
+
usage_info = f"이미지 편집 완료 ({IMAGE_EDIT_MODEL_FLUX} 사용, 기본 화질개선 실패)"
|
412 |
+
else:
|
413 |
+
# 첫 번째 화질개선 실패 시 원본 사용
|
414 |
+
edited_images.append(flux_image)
|
415 |
+
usage_info = f"이미지 편집 완료 ({IMAGE_EDIT_MODEL_FLUX} 사용, 기본 화질개선 실패)"
|
416 |
+
|
417 |
+
except Exception as e:
|
418 |
+
logger.error(f"Error in first enhancement for Flux: {e}")
|
419 |
+
# 첫 번째 화질개선 실패 시 원본 사용
|
420 |
+
edited_images.append(flux_image)
|
421 |
+
usage_info = f"이미지 편집 완료 ({IMAGE_EDIT_MODEL_FLUX} 사용, 기본 화질개선 오류: {str(e)})"
|
422 |
+
|
423 |
+
else:
|
424 |
+
logger.error("No output from Flux API")
|
425 |
+
error_msg = "Flux API에서 응답을 받지 못했습니다."
|
426 |
+
|
427 |
+
except Exception as e:
|
428 |
+
if quality_level == "gpt":
|
429 |
+
logger.error(f"OpenAI API call error: {e}")
|
430 |
+
error_msg = f"OpenAI API 호출 오류: {str(e)}"
|
431 |
+
else:
|
432 |
+
logger.error(f"Flux API call error: {e}")
|
433 |
+
error_msg = f"Flux API 호출 오류: {str(e)}"
|
434 |
+
|
435 |
+
finally:
|
436 |
+
# 임시 파일 정리
|
437 |
+
if processed_image and hasattr(processed_image, 'close'):
|
438 |
+
processed_image.close()
|
439 |
+
|
440 |
+
# 화질 향상 처리 (GPT 모델은 일반적인 화질개선, Flux 모델은 2차 화질개선)
|
441 |
+
enhanced_image = None
|
442 |
+
temp_image_path = None
|
443 |
+
|
444 |
+
if enable_enhancement and edited_images and not error_msg:
|
445 |
+
try:
|
446 |
+
if quality_level == "gpt":
|
447 |
+
# GPT 모델: 일반적인 화질 개선
|
448 |
+
logger.info(f"Enhancing GPT image with {ENHANCEMENT_MODEL}, enhancement level: {enhancement_level}")
|
449 |
+
enhancement_info = "화질 개선"
|
450 |
+
else:
|
451 |
+
# Flux 모델: 2차 화질 개선 (이미 1차는 적용됨)
|
452 |
+
logger.info(f"Applying second enhancement for Flux image with {ENHANCEMENT_MODEL}, enhancement level: {enhancement_level}")
|
453 |
+
enhancement_info = "2차 화질 개선"
|
454 |
+
|
455 |
+
if not os.environ.get("REPLICATE_API_TOKEN"):
|
456 |
+
logger.error("Replicate API token is not set")
|
457 |
+
usage_info += f" | {enhancement_info} 실패: Replicate API 토큰이 설정되지 않았습니다."
|
458 |
+
else:
|
459 |
+
# 임시 파일로 저장
|
460 |
+
temp_image_path = tempfile.mktemp(suffix='.png')
|
461 |
+
edited_images[0].save(temp_image_path)
|
462 |
+
temp_paths.append(temp_image_path)
|
463 |
+
|
464 |
+
# Replicate API로 화질 향상
|
465 |
+
output = replicate.run(
|
466 |
+
ENHANCEMENT_MODEL,
|
467 |
+
input={
|
468 |
+
"image": open(temp_image_path, "rb"),
|
469 |
+
"scale_factor": enhancement_level,
|
470 |
+
"resemblance": 0.8,
|
471 |
+
"creativity": 0.2,
|
472 |
+
"output_format": output_format.lower(),
|
473 |
+
"prompt": prompt,
|
474 |
+
"negative_prompt": "(worst quality, low quality, normal quality:2)"
|
475 |
+
}
|
476 |
+
)
|
477 |
+
|
478 |
+
logger.info(f"Enhancement API response: {output}")
|
479 |
+
|
480 |
+
if output and isinstance(output, list) and len(output) > 0:
|
481 |
+
enhanced_url = output[0]
|
482 |
+
enhanced_response = requests.get(enhanced_url)
|
483 |
+
if enhanced_response.status_code == 200:
|
484 |
+
enhanced_image = Image.open(io.BytesIO(enhanced_response.content))
|
485 |
+
|
486 |
+
if output_format.lower() != "png" and enhanced_image.mode == "RGBA":
|
487 |
+
background = Image.new("RGB", enhanced_image.size, (255, 255, 255))
|
488 |
+
background.paste(enhanced_image, mask=enhanced_image.split()[3])
|
489 |
+
enhanced_image = background
|
490 |
+
|
491 |
+
if quality_level == "gpt":
|
492 |
+
usage_info += f" | {enhancement_info} 완료: {ENHANCEMENT_MODEL} 사용"
|
493 |
+
else:
|
494 |
+
usage_info += f" | {enhancement_info} 완료: 총 2회 화질개선 적용"
|
495 |
+
else:
|
496 |
+
usage_info += f" | {enhancement_info} 실패: 이미지 다운로드 오류"
|
497 |
+
else:
|
498 |
+
usage_info += f" | {enhancement_info} 실패: Enhancement API 응답 없음"
|
499 |
+
|
500 |
+
except Exception as e:
|
501 |
+
logger.error(f"Error enhancing image: {e}")
|
502 |
+
if quality_level == "gpt":
|
503 |
+
usage_info += f" | 화질 개선 실패: {str(e)}"
|
504 |
+
else:
|
505 |
+
usage_info += f" | 2차 화질 개선 실패: {str(e)}"
|
506 |
+
|
507 |
+
# 임시 파일 정리
|
508 |
+
for path in temp_paths:
|
509 |
+
if os.path.exists(path):
|
510 |
+
try:
|
511 |
+
os.remove(path)
|
512 |
+
logger.info(f"Removed temp file: {path}")
|
513 |
+
except Exception as e:
|
514 |
+
logger.error(f"Error removing temp file {path}: {e}")
|
515 |
+
|
516 |
+
# 결과 반환
|
517 |
+
if error_msg:
|
518 |
+
return None, None, None, error_msg
|
519 |
+
elif edited_images:
|
520 |
+
if enable_enhancement and enhanced_image:
|
521 |
+
return edited_images, [enhanced_image], usage_info, None
|
522 |
+
else:
|
523 |
+
return edited_images, None, usage_info, None
|
524 |
+
else:
|
525 |
+
return None, None, None, "이미지 편집에 실패했습니다."
|
526 |
+
|
527 |
+
except Exception as e:
|
528 |
+
logger.error(f"Error in edit_and_enhance_image function: {e}")
|
529 |
+
logger.error(traceback.format_exc())
|
530 |
+
return None, None, None, f"에러 발생: {str(e)}\n\n{traceback.format_exc()}"
|
531 |
+
|
532 |
+
# Gradio 인터페이스 구성
|
533 |
def create_gradio_interface():
|
534 |
try:
|
535 |
logger.info("Creating Gradio interface")
|
|
|
615 |
interactive=True
|
616 |
)
|
617 |
|
618 |
+
# 드롭다운 변경 함수
|
619 |
def update_dropdowns(bg_type):
|
620 |
+
return {
|
621 |
+
simple_dropdown: gr.update(visible=(bg_type == "심플 배경")),
|
622 |
+
studio_dropdown: gr.update(visible=(bg_type == "스튜디오 배경")),
|
623 |
+
nature_dropdown: gr.update(visible=(bg_type == "자연 환경")),
|
624 |
+
indoor_dropdown: gr.update(visible=(bg_type == "실내 환경")),
|
625 |
+
special_dropdown: gr.update(visible=(bg_type == "특수배경")),
|
626 |
+
jewelry_dropdown: gr.update(visible=(bg_type == "주얼리")),
|
627 |
+
special_effects_dropdown: gr.update(visible=(bg_type == "특수효과"))
|
628 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
629 |
|
630 |
background_type.change(
|
631 |
fn=update_dropdowns,
|
632 |
inputs=[background_type],
|
633 |
+
outputs=[simple_dropdown, studio_dropdown, nature_dropdown, indoor_dropdown, special_dropdown, jewelry_dropdown, special_effects_dropdown]
|
|
|
634 |
)
|
635 |
|
636 |
# 요청사항 입력
|
|
|
645 |
label="품질 레벨",
|
646 |
choices=["gpt", "flux"],
|
647 |
value="flux",
|
648 |
+
info="GPT: GPT 모델 (고품질), Flux: Flux 모델 (빠른 처리 + 기본 화질개선)"
|
649 |
)
|
650 |
|
651 |
aspect_ratio = gr.Dropdown(
|
|
|
691 |
info = gr.Textbox(label="처리 정보", interactive=False)
|
692 |
error = gr.Textbox(label="오류 메시지", interactive=False, visible=True)
|
693 |
|
694 |
+
# 프롬프트만 생성하는 함수 (비밀번호 체크 포함)
|
695 |
def generate_prompt_with_password_check(password, bg_type, simple, studio, nature, indoor, special, jewelry, special_effects, request_text, aspect_ratio):
|
696 |
+
# 비밀번호 확인
|
697 |
+
if password != "1089":
|
698 |
+
return "비밀번호가 틀렸습니다. 올바른 비밀번호를 입력해주세요."
|
699 |
+
|
700 |
+
# 배경 선택
|
701 |
+
background_name = ""
|
702 |
+
if bg_type == "심플 배경":
|
703 |
+
background_name = simple
|
704 |
+
elif bg_type == "스튜디오 배경":
|
705 |
+
background_name = studio
|
706 |
+
elif bg_type == "자연 환경":
|
707 |
+
background_name = nature
|
708 |
+
elif bg_type == "실내 환경":
|
709 |
+
background_name = indoor
|
710 |
+
elif bg_type == "특수배경":
|
711 |
+
background_name = special
|
712 |
+
elif bg_type == "주얼리":
|
713 |
+
background_name = jewelry
|
714 |
+
elif bg_type == "특수효과":
|
715 |
+
background_name = special_effects
|
716 |
+
|
717 |
+
# 프롬프트 생성 (종횡비 포함)
|
718 |
+
prompt = generate_prompt(bg_type, background_name, request_text, aspect_ratio)
|
719 |
+
return prompt
|
720 |
|
721 |
+
# 비밀번호 확인 함수
|
722 |
def check_password(password, *args):
|
723 |
+
if password != "1089":
|
724 |
+
return (
|
725 |
+
[], # original_output
|
726 |
+
None, # original_download
|
727 |
+
[], # enhanced_output
|
728 |
+
None, # enhanced_download
|
729 |
+
"", # prompt_output
|
730 |
+
"", # info
|
731 |
+
"비밀번호가 틀렸습니다. 올바른 비밀번호를 입력해주세요." # error
|
732 |
+
)
|
733 |
+
|
734 |
+
# 이미지 편집 요청 처리
|
735 |
+
image, bg_type, simple, studio, nature, indoor, special, jewelry, special_effects, request_text, quality_level, aspect_ratio, output_format, enable_enhancement = args
|
736 |
+
|
737 |
+
# 배경 선택
|
738 |
+
background_name = ""
|
739 |
+
if bg_type == "심플 배경":
|
740 |
+
background_name = simple
|
741 |
+
elif bg_type == "스튜디오 배경":
|
742 |
+
background_name = studio
|
743 |
+
elif bg_type == "자연 환경":
|
744 |
+
background_name = nature
|
745 |
+
elif bg_type == "실내 환경":
|
746 |
+
background_name = indoor
|
747 |
+
elif bg_type == "특수배경":
|
748 |
+
background_name = special
|
749 |
+
elif bg_type == "주얼리":
|
750 |
+
background_name = jewelry
|
751 |
+
elif bg_type == "특수효과":
|
752 |
+
background_name = special_effects
|
753 |
+
|
754 |
+
# 프롬프트 생성
|
755 |
+
prompt = generate_prompt(bg_type, background_name, request_text, aspect_ratio)
|
756 |
+
|
757 |
+
# 이미지 편집 및 화질 개선 실행
|
758 |
+
original_images, enhanced_images, usage_info, error_msg = edit_and_enhance_image(
|
759 |
+
prompt, image, quality_level, aspect_ratio, output_format, enable_enhancement, 2
|
760 |
+
)
|
761 |
+
|
762 |
+
# 이미지 저장 및 다운로드 파일 준비
|
763 |
+
original_path = None
|
764 |
+
enhanced_path = None
|
765 |
+
|
766 |
+
if error_msg:
|
767 |
+
logger.error(f"Error returned from edit_and_enhance_image: {error_msg}")
|
768 |
+
return (
|
769 |
+
[], # original_output
|
770 |
+
None, # original_download
|
771 |
+
[], # enhanced_output
|
772 |
+
None, # enhanced_download
|
773 |
+
prompt, # prompt_output
|
774 |
+
"", # info
|
775 |
+
error_msg # error
|
776 |
+
)
|
777 |
+
else:
|
778 |
+
# 원본 편집 이미지 저장
|
779 |
+
if original_images and len(original_images) > 0:
|
780 |
+
try:
|
781 |
+
original_path = f"original_image.{output_format}"
|
782 |
+
original_images[0].save(original_path)
|
783 |
+
logger.info(f"Saved original image to {original_path}")
|
784 |
+
except Exception as e:
|
785 |
+
logger.error(f"Error saving original image: {e}")
|
786 |
|
787 |
+
# 화질 개선 이미지 저장
|
788 |
+
if enhanced_images and len(enhanced_images) > 0:
|
789 |
+
try:
|
790 |
+
enhanced_path = f"enhanced_image.{output_format}"
|
791 |
+
enhanced_images[0].save(enhanced_path)
|
792 |
+
logger.info(f"Saved enhanced image to {enhanced_path}")
|
793 |
+
except Exception as e:
|
794 |
+
logger.error(f"Error saving enhanced image: {e}")
|
795 |
|
796 |
+
# 결과 반환
|
797 |
+
return (
|
798 |
+
original_images if original_images else [], # original_output
|
799 |
+
original_path, # original_download
|
800 |
+
enhanced_images if enhanced_images else [], # enhanced_output
|
801 |
+
enhanced_path, # enhanced_download
|
802 |
+
prompt, # prompt_output
|
803 |
+
usage_info, # info
|
804 |
+
"" # error (빈 문자열로 설정)
|
805 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
806 |
|
807 |
# 프롬프트 생성 버튼 클릭 이벤트
|
808 |
generate_prompt_btn.click(
|
|
|
814 |
jewelry_dropdown, special_effects_dropdown,
|
815 |
request_text, aspect_ratio
|
816 |
],
|
817 |
+
outputs=[prompt_output]
|
|
|
818 |
)
|
819 |
|
820 |
# 편집 버튼 클릭 이벤트
|
|
|
831 |
original_output, original_download,
|
832 |
enhanced_output, enhanced_download,
|
833 |
prompt_output, info, error
|
834 |
+
]
|
|
|
835 |
)
|
836 |
|
837 |
logger.info("Gradio interface created successfully")
|