import os import tempfile from PIL import Image, ImageEnhance, ImageFilter import gradio as gr import logging import re import time import cv2 import numpy as np from io import BytesIO from datetime import datetime, timedelta import random import requests import base64 from dotenv import load_dotenv load_dotenv() # 로깅 설정 logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) # ------------------- API 엔드포인트 설정 ------------------- def get_api_endpoint(): """환경변수에서 API 엔드포인트를 가져오는 함수""" endpoint = os.getenv('API_ENDPOINT') if not endpoint: logger.error("API_ENDPOINT 환경변수가 설정되지 않았습니다.") return None return endpoint def encode_image_to_base64(image): """PIL 이미지를 base64로 인코딩""" if image is None: return None buffered = BytesIO() image.save(buffered, format="PNG") img_str = base64.b64encode(buffered.getvalue()).decode() return img_str def decode_base64_to_image(base64_str): """base64 문자열을 PIL 이미지로 디코딩""" if not base64_str: return None image_data = base64.b64decode(base64_str) image = Image.open(BytesIO(image_data)) return image def call_api_endpoint(endpoint_url, data): """API 엔드포인트 호출 함수""" try: response = requests.post(endpoint_url, json=data, timeout=300) response.raise_for_status() return response.json() except requests.exceptions.RequestException as e: logger.error(f"API 호출 중 오류 발생: {str(e)}") return {"error": f"API 호출 실패: {str(e)}"} # ========== 이미지 생성기 관련 함수 ========== def translate_prompt_to_english(prompt): """프롬프트 번역 - API 엔드포인트로 전달""" endpoint_url = get_api_endpoint() if not endpoint_url: return prompt data = { "function": "translate_prompt", "prompt": prompt } try: result = call_api_endpoint(f"{endpoint_url}/translate", data) if "error" in result: logger.warning(f"번역 API 오류: {result['error']}") return prompt return result.get("translated_prompt", prompt) except Exception as e: logger.warning(f"번역 중 오류 발생: {str(e)}") return prompt def preprocess_prompt(prompt, image1, image2, image3): """프롬프트 전처리 - 기존 로직 유지""" has_img1 = image1 is not None has_img2 = image2 is not None has_img3 = image3 is not None if "#1" in prompt and not has_img1: prompt = prompt.replace("#1", "첫 번째 이미지(없음)") else: prompt = prompt.replace("#1", "첫 번째 이미지") if "#2" in prompt and not has_img2: prompt = prompt.replace("#2", "두 번째 이미지(없음)") else: prompt = prompt.replace("#2", "두 번째 이미지") if "#3" in prompt and not has_img3: prompt = prompt.replace("#3", "세 번째 이미지(없음)") else: prompt = prompt.replace("#3", "세 번째 이미지") if "1. 이미지 변경" in prompt: desc_match = re.search(r'#1을 "(.*?)"으로 바꿔라', prompt) if desc_match: description = desc_match.group(1) prompt = f"첫 번째 이미지를 {description}으로 변경해주세요. 원본 이미지의 주요 내용은 유지하되 새로운 스타일과 분위기로 재해석해주세요." else: prompt = "첫 번째 이미지를 창의적으로 변형해주세요. 더 생생하고 예술적인 버전으로 만들어주세요." elif "2. 글자지우기" in prompt: text_match = re.search(r'#1에서 "(.*?)"를 지워라', prompt) if text_match: text_to_remove = text_match.group(1) prompt = f"첫 번째 이미지에서 '{text_to_remove}' 텍스트를 찾아 자연스럽게 제거해주세요. 텍스트가 있던 부분을 배경과 조화롭게 채워주세요." else: prompt = "첫 번째 이미지에서 모든 텍스트를 찾아 자연스럽게 제거해주세요. 깔끔한 이미지로 만들어주세요." elif "4. 옷바꾸기" in prompt: prompt = "첫 번째 이미지의 인물 의상을 두 번째 이미지의 의상으로 변경해주세요. 의상의 스타일과 색상은 두 번째 이미지를 따르되, 신체 비율과 포즈는 첫 번째 이미지를 유지해주세요." elif "5. 배경바꾸기" in prompt: prompt = "첫 번째 이미지의 배경을 두 번째 이미지의 배경으로 변경해주세요. 첫 번째 이미지의 주요 피사체는 유지하고, 두 번째 이미지의 배경과 조화롭게 합성해주세요." elif "6. 이미지 합성(상품포함)" in prompt: prompt = "첫 번째 이미지와 두 번째 이미지(또는 세 번째 이미지)를 자연스럽게 합성해주세요. 모든 이미지의 주요 요소를 포함하고, 특히 상품이 돋보이도록 조화롭게 통합해주세요." prompt += " 이미지를 생성해주세요. 이미지에 텍스트나 글자를 포함하지 마세요." return prompt def generate_with_images(prompt, images, variation_index=0): """API 엔드포인트로 이미지 생성 요청""" endpoint_url = get_api_endpoint() if not endpoint_url: return None, "API 엔드포인트가 설정되지 않았습니다." # 이미지들을 base64로 인코딩 encoded_images = [] for img in images: if img is not None: encoded_img = encode_image_to_base64(img) encoded_images.append(encoded_img) else: encoded_images.append(None) data = { "function": "generate_with_images", "prompt": prompt, "images": encoded_images, "variation_index": variation_index } try: result = call_api_endpoint(f"{endpoint_url}/generate", data) if "error" in result: return None, result["error"] # base64 이미지를 PIL 이미지로 변환 if "image" in result: generated_image = decode_base64_to_image(result["image"]) return generated_image, result.get("status", "이미지가 성공적으로 생성되었습니다.") else: return None, "API에서 이미지를 생성하지 못했습니다." except Exception as e: logger.error(f"이미지 생성 API 호출 중 오류: {str(e)}") return None, f"이미지 생성 중 오류 발생: {str(e)}" def process_images_with_prompt(image1, image2, image3, prompt, variation_index=0, max_retries=3): """이미지 처리 메인 함수""" retry_count = 0 last_error = None while retry_count < max_retries: try: images = [image1, image2, image3] valid_images = [img for img in images if img is not None] if not valid_images: if prompt and prompt.strip(): logger.info("텍스트 프롬프트만으로 이미지 생성 시도") try: result_img, status = generate_with_images(prompt, [], variation_index) if result_img is not None: return result_img, status, prompt except: pass return None, "이미지를 업로드하거나 텍스트 프롬프트를 입력해주세요.", "" if prompt and prompt.strip(): processed_prompt = preprocess_prompt(prompt, image1, image2, image3) if re.search("[가-힣]", processed_prompt): final_prompt = translate_prompt_to_english(processed_prompt) else: final_prompt = processed_prompt else: if len(valid_images) == 1: final_prompt = "Please creatively transform this image into a more vivid and artistic version. Do not include any text or watermarks in the generated image." logger.info("Default prompt generated for single image") elif len(valid_images) == 2: final_prompt = "Please seamlessly composite these two images, integrating their key elements harmoniously into a single image. Do not include any text or watermarks in the generated image." logger.info("Default prompt generated for two images") else: final_prompt = "Please creatively composite these three images, combining their main elements into a cohesive and natural scene. Do not include any text or watermarks in the generated image." logger.info("Default prompt generated for three images") result_img, status = generate_with_images(final_prompt, valid_images, variation_index) if result_img is not None: return result_img, status, final_prompt else: last_error = status retry_count += 1 logger.warning(f"이미지 생성 실패, 재시도 {retry_count}/{max_retries}: {status}") time.sleep(1) except Exception as e: last_error = str(e) retry_count += 1 logger.exception(f"이미지 처리 중 오류 발생, 재시도 {retry_count}/{max_retries}:") time.sleep(1) return None, f"최대 재시도 횟수({max_retries}회) 초과 후 실패: {last_error}", prompt def generate_multiple_images(image1, image2, image3, prompt, progress=gr.Progress()): """다중 이미지 생성 함수""" results = [] statuses = [] prompts = [] num_images = 2 max_retries = 3 progress(0, desc="이미지 생성 준비 중...") for i in range(num_images): progress((i / num_images), desc=f"{i+1}/{num_images} 이미지 생성 중...") result_img, status, final_prompt = process_images_with_prompt(image1, image2, image3, prompt, i, max_retries) if result_img is not None: results.append(result_img) statuses.append(f"이미지 #{i+1}: {status}") prompts.append(f"이미지 #{i+1}: {final_prompt}") else: results.append(None) statuses.append(f"이미지 #{i+1} 생성 실패: {status}") prompts.append(f"이미지 #{i+1}: {final_prompt}") time.sleep(1) progress(1.0, desc="이미지 생성 완료!") while len(results) < 2: results.append(None) combined_status = "\n".join(statuses) combined_prompts = "\n".join(prompts) return results[0], results[1], combined_status, combined_prompts # 다크모드 자동 적용 커스텀 CSS 스타일 custom_css = """ /* ============================================ 다크모드 자동 변경 템플릿 CSS ============================================ */ /* 1. CSS 변수 정의 (라이트모드 - 기본값) */ :root { /* 메인 컬러 */ --primary-color: #FB7F0D; --secondary-color: #ff9a8b; --accent-color: #FF6B6B; /* 배경 컬러 */ --background-color: #FFFFFF; --card-bg: #ffffff; --input-bg: #ffffff; /* 텍스트 컬러 */ --text-color: #334155; --text-secondary: #64748b; /* 보더 및 구분선 */ --border-color: #dddddd; --border-light: #e5e5e5; /* 테이블 컬러 */ --table-even-bg: #f3f3f3; --table-hover-bg: #f0f0f0; /* 그림자 */ --shadow: 0 8px 30px rgba(251, 127, 13, 0.08); --shadow-light: 0 2px 4px rgba(0, 0, 0, 0.1); /* 기타 */ --border-radius: 18px; } /* 2. 다크모드 색상 변수 (자동 감지) */ @media (prefers-color-scheme: dark) { :root { /* 배경 컬러 */ --background-color: #1a1a1a; --card-bg: #2d2d2d; --input-bg: #2d2d2d; /* 텍스트 컬러 */ --text-color: #e5e5e5; --text-secondary: #a1a1aa; /* 보더 및 구분선 */ --border-color: #404040; --border-light: #525252; /* 테이블 컬러 */ --table-even-bg: #333333; --table-hover-bg: #404040; /* 그림자 */ --shadow: 0 8px 30px rgba(0, 0, 0, 0.3); --shadow-light: 0 2px 4px rgba(0, 0, 0, 0.2); } } /* 3. 수동 다크모드 클래스 (Gradio 토글용) */ [data-theme="dark"], .dark, .gr-theme-dark { /* 배경 컬러 */ --background-color: #1a1a1a; --card-bg: #2d2d2d; --input-bg: #2d2d2d; /* 텍스트 컬러 */ --text-color: #e5e5e5; --text-secondary: #a1a1aa; /* 보더 및 구분선 */ --border-color: #404040; --border-light: #525252; /* 테이블 컬러 */ --table-even-bg: #333333; --table-hover-bg: #404040; /* 그림자 */ --shadow: 0 8px 30px rgba(0, 0, 0, 0.3); --shadow-light: 0 2px 4px rgba(0, 0, 0, 0.2); } /* 4. 기본 요소 다크모드 적용 */ body { font-family: 'Pretendard', 'Noto Sans KR', -apple-system, BlinkMacSystemFont, sans-serif; background-color: var(--background-color) !important; color: var(--text-color) !important; line-height: 1.6; transition: background-color 0.3s ease, color 0.3s ease; } /* 5. Gradio 컨테이너 강제 적용 */ .gradio-container, .gradio-container *, .gr-app, .gr-app *, .gr-interface { background-color: var(--background-color) !important; color: var(--text-color) !important; } /* Gradio 컨테이너 오버라이드 */ .gradio-container { max-width: 100% !important; width: 100% !important; margin: 0 auto !important; padding: 0 !important; background-color: var(--background-color) !important; box-sizing: border-box !important; } /* 추가: 내부 컨테이너도 100% 너비로 설정 */ .contain { max-width: 100% !important; width: 100% !important; } /* 추가: 각 행(Row)도 100% 너비로 설정 */ .gr-padded { padding: 0 !important; width: 100% !important; max-width: 100% !important; } /* 6. 카드 및 패널 스타일 */ .gr-group, .gr-form, .gr-box, .gr-panel, .custom-frame, [class*="frame"], [class*="card"], [class*="panel"] { background-color: var(--card-bg) !important; border-radius: var(--border-radius) !important; box-shadow: var(--shadow) !important; padding: 1.5rem !important; margin-bottom: 1.5rem !important; border: 1px solid var(--border-color) !important; transition: transform 0.3s ease, background-color 0.3s ease; color: var(--text-color) !important; } .gr-group:hover { transform: translateY(-5px); } /* 7. 입력 필드 스타일 */ input[type="text"], input[type="number"], input[type="email"], input[type="password"], textarea, select, .gr-input, .gr-text-input, .gr-textarea, .gr-dropdown { background-color: var(--input-bg) !important; color: var(--text-color) !important; border: 1px solid var(--border-color) !important; border-radius: var(--border-radius) !important; padding: 12px !important; box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05) !important; transition: all 0.3s ease !important; } input[type="text"]:focus, input[type="number"]:focus, input[type="email"]:focus, input[type="password"]:focus, textarea:focus, select:focus, .gr-input:focus, .gr-text-input:focus, .gr-textarea:focus, .gr-dropdown:focus { border-color: var(--primary-color) !important; outline: none !important; box-shadow: 0 0 0 2px rgba(251, 127, 13, 0.2) !important; } /* 8. 라벨 및 텍스트 요소 */ label, .gr-label, .gr-checkbox label, .gr-radio label, p, span, div { color: var(--text-color) !important; } /* 9. 섹션 제목 */ .section-title { font-size: 22px !important; font-weight: 700 !important; color: var(--text-color) !important; margin-bottom: 1rem !important; padding-bottom: 0.5rem !important; border-bottom: 2px solid var(--primary-color) !important; display: flex; align-items: center; } .section-title span { color: var(--primary-color); } /* 10. 버튼 스타일링 */ .custom-button { background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)) !important; color: white !important; font-weight: 600 !important; border: none !important; border-radius: 30px !important; padding: 12px 24px !important; box-shadow: 0 4px 8px rgba(251, 127, 13, 0.25) !important; transition: all 0.3s ease !important; text-transform: none !important; display: flex !important; align-items: center !important; justify-content: center !important; } .custom-button:hover { transform: translateY(-2px) !important; box-shadow: 0 6px 12px rgba(251, 127, 13, 0.3) !important; } .custom-button.primary { background: linear-gradient(135deg, var(--accent-color), #ff9a8b) !important; } button:not([class*="custom"]):not([class*="primary"]):not([class*="secondary"]) { background-color: var(--card-bg) !important; color: var(--text-color) !important; border: 1px solid var(--border-color) !important; border-radius: var(--border-radius) !important; transition: all 0.3s ease !important; } /* 11. 이미지 컨테이너 */ .image-container { border-radius: var(--border-radius); overflow: hidden; border: 1px solid var(--border-color); transition: all 0.3s ease; background-color: var(--card-bg); } .image-container:hover { box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1); } /* 12. 테이블 스타일 */ table { background-color: var(--card-bg) !important; color: var(--text-color) !important; border-color: var(--border-color) !important; } table th { background-color: var(--primary-color) !important; color: white !important; border-color: var(--border-color) !important; } table td { background-color: var(--card-bg) !important; color: var(--text-color) !important; border-color: var(--border-color) !important; } table tbody tr:nth-child(even) { background-color: var(--table-even-bg) !important; } table tbody tr:hover { background-color: var(--table-hover-bg) !important; } /* 13. 체크박스 및 라디오 버튼 */ input[type="checkbox"], input[type="radio"] { accent-color: var(--primary-color) !important; } /* 14. 버튼 그룹 */ .button-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 0.8rem; margin-bottom: 1.2rem; } /* 15. 스크롤바 스타일 */ ::-webkit-scrollbar-thumb:hover { background: var(--secondary-color); } /* 16. 아코디언 및 드롭다운 */ details { background-color: var(--card-bg) !important; border-color: var(--border-color) !important; color: var(--text-color) !important; } details summary { background-color: var(--card-bg) !important; color: var(--text-color) !important; } /* 17. 툴팁 및 팝업 */ [data-tooltip]:hover::after, .tooltip, .popup { background-color: var(--card-bg) !important; color: var(--text-color) !important; border-color: var(--border-color) !important; box-shadow: var(--shadow-light) !important; } /* 18. 모달 및 오버레이 */ .modal, .overlay, [class*="modal"], [class*="overlay"] { background-color: var(--card-bg) !important; color: var(--text-color) !important; border-color: var(--border-color) !important; } /* 19. 추가 Gradio 컴포넌트들 */ .gr-block, .gr-group, .gr-row, .gr-column { background-color: var(--background-color) !important; color: var(--text-color) !important; } /* 20. 코드 블록 및 pre 태그 */ code, pre, .code-block { background-color: var(--table-even-bg) !important; color: var(--text-color) !important; border-color: var(--border-color) !important; } /* 21. 알림 및 메시지 */ .alert, .message, .notification, [class*="alert"], [class*="message"], [class*="notification"] { background-color: var(--card-bg) !important; color: var(--text-color) !important; border-color: var(--border-color) !important; } /* 22. 애니메이션 스타일 */ @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } .fade-in { animation: fadeIn 0.5s ease-out; } /* 23. Examples 섹션 스타일 */ .examples-section { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1.5rem; margin-top: 1rem; } .example-item { background-color: var(--card-bg); border-radius: var(--border-radius); overflow: hidden; box-shadow: var(--shadow); transition: transform 0.3s ease, background-color 0.3s ease; } .example-item:hover { transform: translateY(-5px); } /* 24. 전환 애니메이션 */ * { transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease !important; } /* 25. 반응형 */ @media (max-width: 768px) { .button-grid { grid-template-columns: repeat(2, 1fr); } .examples-section { grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); } } """ # FontAwesome 아이콘 포함 fontawesome_link = """ """ # 제목과 사용 가이드 제거 header_html = "" image_generator_guide_html = "" # UI 구성 with gr.Blocks(css=custom_css, theme=gr.themes.Default( primary_hue="orange", secondary_hue="orange", font=[gr.themes.GoogleFont("Noto Sans KR"), "ui-sans-serif", "system-ui"] )) as demo: gr.HTML(fontawesome_link) # 탭 제거하고 이미지 생성기 내용을 직접 배치 with gr.Row(equal_height=True): with gr.Column(scale=1): # ======== 이미지 업로드 및 설정 섹션 ======== with gr.Group(): gr.HTML('