import os import tempfile import logging import re import time import json from PIL import Image import gradio as gr from google import genai from google.genai import types import google.generativeai as genai_generative from dotenv import load_dotenv from db_examples import product_background_examples load_dotenv() # ------------------- 로깅 설정 ------------------- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) # ------------------- 배경 디렉토리 설정 ------------------- BACKGROUNDS_DIR = "./background" if not os.path.exists(BACKGROUNDS_DIR): os.makedirs(BACKGROUNDS_DIR) logger.info(f"배경 디렉토리를 생성했습니다: {BACKGROUNDS_DIR}") # ------------------- API 키 순환 시스템 ------------------- API_KEYS = [] # API 키 목록 current_key_index = 0 # 현재 사용 중인 키 인덱스 def initialize_api_keys(): """API 키 목록을 초기화하는 함수""" global API_KEYS # 환경 변수에서 API 키 가져오기 key1 = os.environ.get("GEMINI_API_KEY_1", "") key2 = os.environ.get("GEMINI_API_KEY_2", "") key3 = os.environ.get("GEMINI_API_KEY_3", "") # 빈 문자열이 아닌 키만 추가 if key1: API_KEYS.append(key1) if key2: API_KEYS.append(key2) if key3: API_KEYS.append(key3) # 기존 GEMINI_API_KEY가 있으면 추가 default_key = os.environ.get("GEMINI_API_KEY", "") if default_key and default_key not in API_KEYS: API_KEYS.append(default_key) logger.info(f"API 키 {len(API_KEYS)}개가 로드되었습니다.") def get_next_api_key(): """다음 API 키를 가져오는 함수""" global current_key_index if not API_KEYS: return None # 현재 키 가져오기 api_key = API_KEYS[current_key_index] # 다음 키 인덱스로 업데이트 current_key_index = (current_key_index + 1) % len(API_KEYS) return api_key # ------------------- 전역 변수 설정 ------------------- SIMPLE_BACKGROUNDS = {} STUDIO_BACKGROUNDS = {} NATURE_BACKGROUNDS = {} INDOOR_BACKGROUNDS = {} SPECIAL_BACKGROUNDS = {} # 특수 배경 추가 IMAGE_CACHE = {} # 이미지 캐시 추가 # 커스텀 CSS 스타일 - 기존 스타일 유지 및 예시 탭용 스타일 추가 custom_css = """ :root { --primary-color: #FB7F0D; --secondary-color: #ff9a8b; --accent-color: #FF6B6B; --background-color: #FFF3E9; --card-bg: #ffffff; --text-color: #334155; --border-radius: 18px; --shadow: 0 8px 30px rgba(251, 127, 13, 0.08); } /* 예시 갤러리 스타일 */ .example-gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 20px; padding: 20px; } .example-item { cursor: pointer; border: 1px solid rgba(0, 0, 0, 0.1); border-radius: var(--border-radius); overflow: hidden; transition: all 0.3s ease; background: white; } .example-item:hover { box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1); transform: translateY(-2px); } .example-item img { width: 100%; height: 250px; object-fit: cover; } .example-label { padding: 10px; text-align: center; font-weight: bold; background: rgba(251, 127, 13, 0.1); } .example-detail-view { margin-bottom: 30px; } .example-params { background: white; padding: 20px; border-radius: var(--border-radius); box-shadow: var(--shadow); } .example-params p { margin: 10px 0; font-size: 16px; } .example-params strong { color: var(--primary-color); } /* 선택된 예시 하이라이트 */ .example-item.selected { border: 3px solid var(--primary-color); } /* ── 탭 내부 패널 배경 제거 ── */ .gr-tabs-panel { background-color: var(--background-color) !important; box-shadow: none !important; } .gr-tabs-panel::before, .gr-tabs-panel::after { display: none !important; content: none !important; } /* ── 그룹 래퍼 배경 완전 제거 ── */ .custom-section-group, .gr-block.gr-group { background-color: var(--background-color) !important; box-shadow: none !important; } .custom-section-group::before, .custom-section-group::after, .gr-block.gr-group::before, .gr-block.gr-group::after { display: none !important; content: none !important; } /* 그룹 컨테이너 배경을 아이보리로, 그림자 제거 */ .custom-section-group { background-color: var(--background-color) !important; box-shadow: none !important; } /* 상단·하단에 그려지는 회색 캡(둥근 모서리) 제거 */ .custom-section-group::before, .custom-section-group::after { display: none !important; content: none !important; } body { font-family: 'Pretendard', 'Noto Sans KR', -apple-system, BlinkMacSystemFont, sans-serif; background-color: var(--background-color); color: var(--text-color); line-height: 1.6; margin: 0; padding: 0; } .gradio-container { width: 100%; /* 전체 너비 100% 고정 */ margin: 0 auto; padding: 20px; background-color: var(--background-color); } /* 콘텐츠 박스 (프레임) 스타일 */ .custom-frame { background-color: var(--card-bg); border: 1px solid rgba(0, 0, 0, 0.04); border-radius: var(--border-radius); padding: 20px; margin: 10px 0; box-shadow: var(--shadow); } /* 섹션 그룹 스타일 - 회색 배경 완전 제거 */ .custom-section-group { margin-top: 20px; padding: 0; border: none; border-radius: 0; background-color: var(--background-color); /* 회색 → 아이보리(전체 배경색) */ box-shadow: none !important; /* 혹시 남아있는 그림자도 같이 제거 */ } /* 버튼 스타일 - 글자 크기 18px */ .custom-button { border-radius: 30px !important; background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)) !important; color: white !important; font-size: 18px !important; padding: 10px 20px !important; border: none; box-shadow: 0 4px 8px rgba(251, 127, 13, 0.25); transition: transform 0.3s ease; } .custom-button:hover { transform: translateY(-2px); box-shadow: 0 6px 12px rgba(251, 127, 13, 0.3); } /* 제목 스타일 (모든 항목명이 동일하게 custom-title 클래스로) */ .custom-title { font-size: 28px; font-weight: bold; margin-bottom: 10px; color: var(--text-color); border-bottom: 2px solid var(--primary-color); padding-bottom: 5px; } /* 이미지 컨테이너 */ .image-container { border-radius: var(--border-radius); overflow: hidden; border: 1px solid rgba(0, 0, 0, 0.08); transition: all 0.3s ease; background-color: white; aspect-ratio: 1 / 1; /* 정사각형 비율 강제 */ } .image-container:hover { box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1); } .image-container img { width: 100%; height: 100%; object-fit: contain; /* 이미지 비율 유지하면서 컨테이너에 맞춤 */ } /* 입력 필드 스타일 */ .gr-input, .gr-text-input, .gr-sample-inputs { border-radius: var(--border-radius) !important; border: 1px solid #dddddd !important; padding: 12px !important; box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05) !important; transition: all 0.3s ease !important; } .gr-input:focus, .gr-text-input:focus { border-color: var(--primary-color) !important; outline: none !important; box-shadow: 0 0 0 2px rgba(251, 127, 13, 0.2) !important; } /* 메인 컨텐츠 스크롤바 */ ::-webkit-scrollbar { width: 8px; height: 8px; } ::-webkit-scrollbar-track { background: rgba(0, 0, 0, 0.05); border-radius: 10px; } ::-webkit-scrollbar-thumb { background: var(--primary-color); border-radius: 10px; } /* 애니메이션 스타일 */ @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } .fade-in { animation: fadeIn 0.5s ease-out; } /* 반응형 */ @media (max-width: 768px) { .button-grid { grid-template-columns: repeat(2, 1fr); } } /* 섹션 제목 스타일 - 참조코드 스타일과 동일하게 적용 */ .section-title { display: flex; align-items: center; font-size: 20px; font-weight: 700; color: #333333; margin-bottom: 10px; padding-bottom: 5px; border-bottom: 2px solid #FB7F0D; font-family: 'Pretendard', 'Noto Sans KR', -apple-system, BlinkMacSystemFont, sans-serif; } .section-title img { margin-right: 10px; width: 24px; height: 24px; } """ # FontAwesome 아이콘 포함 fontawesome_link = """ """ # 제목과 사용 가이드 HTML 제거 header_html = "" guide_html = "" # ------------------- 배경 JSON 파일 로드 함수 ------------------- def load_background_json(filename): """배경 JSON 파일 로드 함수""" file_path = os.path.join(BACKGROUNDS_DIR, filename) try: with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) logger.info(f"{filename} 파일을 성공적으로 로드했습니다. {len(data)} 항목 포함.") return data except FileNotFoundError: logger.info(f"{filename} 파일이 없습니다.") return {} except Exception as e: logger.warning(f"{filename} 파일 로드 중 오류 발생: {str(e)}.") return {} # ------------------- 배경 옵션 초기화 함수 ------------------- def initialize_backgrounds(): """모든 배경 옵션 초기화 함수""" global SIMPLE_BACKGROUNDS, STUDIO_BACKGROUNDS, NATURE_BACKGROUNDS, INDOOR_BACKGROUNDS global SPECIAL_BACKGROUNDS logger.info(f"Backgrounds 디렉토리 경로: {BACKGROUNDS_DIR}") logger.info(f"디렉토리 내 파일 목록: {os.listdir(BACKGROUNDS_DIR)}") SIMPLE_BACKGROUNDS = load_background_json("simple_backgrounds.json") STUDIO_BACKGROUNDS = load_background_json("studio_backgrounds.json") NATURE_BACKGROUNDS = load_background_json("nature_backgrounds.json") INDOOR_BACKGROUNDS = load_background_json("indoor_backgrounds.json") SPECIAL_BACKGROUNDS = load_background_json("special_backgrounds.json") # 기본값 설정 (파일이 없거나 비어있는 경우) if not SIMPLE_BACKGROUNDS: SIMPLE_BACKGROUNDS = {"클래식 화이트": "clean white background with soft even lighting"} if not STUDIO_BACKGROUNDS: STUDIO_BACKGROUNDS = {"미니멀 플랫레이": "minimalist flat lay with clean white background"} if not NATURE_BACKGROUNDS: NATURE_BACKGROUNDS = {"열대 해변": "tropical beach with crystal clear water"} if not INDOOR_BACKGROUNDS: INDOOR_BACKGROUNDS = {"미니멀 스칸디나비안 거실": "minimalist Scandinavian living room"} if not SPECIAL_BACKGROUNDS: SPECIAL_BACKGROUNDS = {"네온 라이트": "neon light background with vibrant glowing elements"} logger.info("모든 배경 옵션 초기화 완료") # 배경 드롭다운 초기화를 위한 함수 def initialize_dropdowns(): """드롭다운 메뉴 초기화 함수""" simple_choices = list(SIMPLE_BACKGROUNDS.keys()) studio_choices = list(STUDIO_BACKGROUNDS.keys()) nature_choices = list(NATURE_BACKGROUNDS.keys()) indoor_choices = list(INDOOR_BACKGROUNDS.keys()) special_choices = list(SPECIAL_BACKGROUNDS.keys()) return { "simple": simple_choices, "studio": studio_choices, "nature": nature_choices, "indoor": indoor_choices, "special": special_choices, } # ------------------- 기본 유틸리티 함수 ------------------- def get_api_key(user_input_key=None): """API 키를 가져오는 함수 (순환 시스템 적용)""" return get_next_api_key() def save_binary_file(file_name, data): with open(file_name, "wb") as f: f.write(data) def translate_prompt_to_english(prompt): if not re.search("[가-힣]", prompt): return prompt prompt = prompt.replace("#1", "IMAGE_TAG_ONE") try: api_key = get_api_key() if not api_key: logger.error("Gemini API 키가 설정되지 않았습니다.") prompt = prompt.replace("IMAGE_TAG_ONE", "#1") return prompt client = genai.Client(api_key=api_key) translation_prompt = f""" Translate the following Korean text to English: {prompt} IMPORTANT: The token IMAGE_TAG_ONE is a special tag and must be preserved exactly as is in your translation. Do not translate this token. """ logger.info(f"Translation prompt: {translation_prompt}") response = client.models.generate_content( model="gemini-2.0-flash", contents=[translation_prompt], config=types.GenerateContentConfig( response_modalities=['Text'], temperature=0.2, top_p=0.95, top_k=40, max_output_tokens=512 ) ) translated_text = "" for part in response.candidates[0].content.parts: if hasattr(part, 'text') and part.text: translated_text += part.text if translated_text.strip(): translated_text = translated_text.replace("IMAGE_TAG_ONE", "#1") logger.info(f"Translated text: {translated_text.strip()}") return translated_text.strip() else: logger.warning("번역 결과가 없습니다. 원본 프롬프트 사용") prompt = prompt.replace("IMAGE_TAG_ONE", "#1") return prompt except Exception as e: logger.exception("번역 중 오류 발생:") prompt = prompt.replace("IMAGE_TAG_ONE", "#1") return prompt def preprocess_prompt(prompt, image1): has_img1 = image1 is not None if "#1" in prompt and not has_img1: prompt = prompt.replace("#1", "첫 번째 이미지(없음)") else: prompt = prompt.replace("#1", "첫 번째 이미지") prompt += " 이미지를 생성해주세요. 이미지에 텍스트나 글자를 포함하지 마세요." return prompt # ------------------- 이미지 생성 함수 ------------------- def generate_with_images(prompt, images, variation_index=0): try: api_key = get_api_key() if not api_key: return None, "API 키가 설정되지 않았습니다. 환경 변수에 GEMINI_API_KEY_1, GEMINI_API_KEY_2, GEMINI_API_KEY_3 중 하나 이상을 설정해주세요." client = genai.Client(api_key=api_key) logger.info(f"Gemini API 요청 시작 - 프롬프트: {prompt}, 변형 인덱스: {variation_index}") variation_suffixes = [ " Create this as a professional studio product shot with precise focus on the product details. Do not add any text, watermarks, or labels to the image.", " Create this as a high-contrast artistic studio shot with dramatic lighting and shadows. Do not add any text, watermarks, or labels to the image.", " Create this as a soft-lit elegantly styled product shot with complementary elements. Do not add any text, watermarks, or labels to the image.", " Create this as a high-definition product photography with perfect color accuracy and detail preservation. Do not add any text, watermarks, or labels to the image." ] if variation_index < len(variation_suffixes): prompt = prompt + variation_suffixes[variation_index] else: prompt = prompt + " Create as high-end commercial product photography. Do not add any text, watermarks, or labels to the image." contents = [prompt] for idx, img in enumerate(images, 1): if img is not None: contents.append(img) logger.info(f"이미지 #{idx} 추가됨") response = client.models.generate_content( model="gemini-2.0-flash-exp-image-generation", contents=contents, config=types.GenerateContentConfig( response_modalities=['Text', 'Image'], temperature=1.05, top_p=0.97, top_k=50, max_output_tokens=10240 ) ) with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp: temp_path = tmp.name result_text = "" image_found = False for part in response.candidates[0].content.parts: if hasattr(part, 'text') and part.text: result_text += part.text logger.info(f"응답 텍스트: {part.text}") elif hasattr(part, 'inline_data') and part.inline_data: save_binary_file(temp_path, part.inline_data.data) image_found = True logger.info("응답에서 이미지 추출 성공") if not image_found: return None, f"API에서 이미지를 생성하지 못했습니다." result_img = Image.open(temp_path) if result_img.mode == "RGBA": result_img = result_img.convert("RGB") result_img.save(temp_path, format="JPEG", quality=95) return temp_path, f"이미지가 성공적으로 생성되었습니다." except Exception as e: logger.exception("이미지 생성 중 오류 발생:") return None, f"오류 발생: {str(e)}" def process_images_with_prompt(image1, prompt, variation_index=0, max_retries=3): retry_count = 0 last_error = None while retry_count < max_retries: try: images = [image1] valid_images = [img for img in images if img is not None] if not valid_images: return None, "이미지를 업로드해주세요.", "" final_prompt = prompt.strip() result_img, status = generate_with_images(final_prompt, valid_images, variation_index) # 상태 정보에서 프롬프트 정보 제거 if result_img is not None: if "이미지가 성공적으로 생성되었습니다" in status: status = "이미지가 성공적으로 생성되었습니다." 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 filter_prompt_only(prompt): """Gemini의 설명 및 불필요한 메시지를 제거하고 실제 프롬프트만 추출하는 함수""" code_block_pattern = r"```\s*(.*?)```" code_match = re.search(code_block_pattern, prompt, re.DOTALL) if code_match: return code_match.group(1).strip() if "--ar 1:1" in prompt: lines = prompt.split('\n') prompt_lines = [] in_prompt = False for line in lines: if (not in_prompt and ("product" in line.lower() or "magazine" in line.lower() or "commercial" in line.lower() or "photography" in line.lower())): in_prompt = True prompt_lines.append(line) elif in_prompt: if "explanation" in line.lower() or "let me know" in line.lower(): break prompt_lines.append(line) if prompt_lines: return '\n'.join(prompt_lines).strip() return prompt.strip() def get_selected_background_info(bg_type, simple, studio, nature, indoor, special): """선택된 배경 정보를 가져오는 함수""" if bg_type == "심플 배경": return { "category": "심플 배경", "name": simple, "english": SIMPLE_BACKGROUNDS.get(simple, "white background") } elif bg_type == "스튜디오 배경": return { "category": "스튜디오 배경", "name": studio, "english": STUDIO_BACKGROUNDS.get(studio, "product photography studio") } elif bg_type == "자연 환경": return { "category": "자연 환경", "name": nature, "english": NATURE_BACKGROUNDS.get(nature, "natural environment") } elif bg_type == "실내 환경": return { "category": "실내 환경", "name": indoor, "english": INDOOR_BACKGROUNDS.get(indoor, "indoor environment") } elif bg_type == "특수배경": return { "category": "특수배경", "name": special, "english": SPECIAL_BACKGROUNDS.get(special, "special background") } else: return { "category": "기본 배경", "name": "화이트 배경", "english": "white background" } def generate_enhanced_system_instruction(): """향상된 시스템 인스트럭션 생성 함수""" return """당신은 상품 이미지의 배경을 변경하기 위한 최고 품질의 프롬프트를 생성하는 전문가입니다. 사용자가 제공하는 상품명, 배경 유형, 추가 요청사항을 바탕으로 미드저니(Midjourney)에 사용할 수 있는 상세하고 전문적인 프롬프트를 영어로 생성해주세요. 다음 가이드라인을 반드시 준수해야 합니다: 1. 상품을 "#1"로 지정하여 참조합니다. (예: "skincare tube (#1)") 2. *** 매우 중요: 상품의 원래 특성(디자인, 색상, 형태, 로고, 패키지 등)은 어떤 상황에서도 절대 변경하지 않습니다. *** 3. *** 상품의 본질적 특성을 유지하되, 상품에 포커스를 맞춰 모든 세부 사항이 선명하게 드러나도록 하며, 8K 해상도(8K resolution), 오버샤프닝 없는 초고화질(ultra high definition without oversharpening)로 렌더링되어야 합니다. *** 4. 이미지 비율은 정확히 1:1(정사각형) 형식으로 지정합니다. 프롬프트에 "square format", "1:1 ratio" 또는 "aspect ratio 1:1"을 명시적으로 포함합니다. 5. 상품은 반드시 정사각형 구도의 정중앙에 배치되어야 하며, 적절한 크기로 표현하여 디테일이 완벽하게 보이도록 합니다. 6. 상품을 이미지의 주요 초점으로 부각시키고, 상품의 비율이 전체 이미지에서 60-70% 이상 차지하도록 합니다. 7. 조명 설명을 매우 구체적으로 해주세요. 예: "soft directional lighting from left side", "dramatic rim lighting", "diffused natural light through windows" 8. 배경의 재질과 질감을 상세히 설명해주세요. 예: "polished marble surface", "rustic wooden table with visible grain", "matte concrete wall with subtle texture" 9. 프롬프트에 다음 요소들을 명시적으로 포함하되, 사용 맥락에 적절하게 변형하세요: - "award-winning product photography" - "magazine-worthy commercial product shot" - "professional advertising imagery with perfect exposure" - "studio lighting with color-accurate rendering" - "8K ultra high definition product showcase" - "commercial product photography with precise detail rendering" - "ultra high definition" - "crystal clear details" 10. 사용자가 제공한 구체적인 배경과 추가 요청사항을 프롬프트에 정확히 반영하고 확장합니다. 11. 프롬프트 끝에 미드저니 파라미터 "--ar 1:1 --s 750 --q 2 --v 5.2" 파라미터를 추가하여 미드저니에서 고품질 정사각형 비율을 강제합니다. 12. 매우 중요: 프롬프트 외에 다른 설명이나 메타 텍스트를 포함하지 마세요. 오직 프롬프트 자체만 제공하세요. """ def generate_prompt_with_gemini(product_name, background_info, additional_info=""): """향상된 프롬프트 생성 함수""" api_key = get_api_key() if not api_key: return "Gemini API 키가 설정되지 않았습니다." try: genai_generative.configure(api_key=api_key) prompt_request = f""" 상품명: {product_name} 배경 유형: {background_info.get('english', 'studio')} 배경 카테고리: {background_info.get('category', '')} 배경 이름: {background_info.get('name', '')} 추가 요청사항: {additional_info} 중요 요구사항: 1. 상품(#1)이 이미지 구도에서 중심적인 위치를 차지하며 적절한 크기(이미지의 60-70%)로 표현되도록 프롬프트를 생성해주세요. 2. 이미지는 정확히 1:1 비율(정사각형)이어야 합니다. 3. 상품의 디자인, 색상, 형태, 로고 등 본질적 특성은 절대 수정하지 마세요. 4. 구체적인 조명 기법을 상세히 명시해주세요: - 정확한 조명 위치 (예: "45-degree key light from upper left") - 조명 품질 (예: "soft diffused light", "hard directional light") - 조명 강도와 색온도 (예: "warm tungsten key light with cool blue fill") - 반사와 그림자 처리 방식 (예: "controlled specular highlights with soft shadow transitions") 5. 상품을 더 돋보이게 하는 보조 요소(props)를 자연스럽게 활용하되, 상품이 항상 주인공이어야 합니다. 6. 배경 재질과 표면 질감을 구체적으로 설명하고, 상품과의 상호작용 방식을 명시해주세요. 7. 색상 구성(color palette, color harmonies)을 명확히 해주세요. 8. 고급스러운 상업 광고 품질의 이미지가 되도록 프롬프트를 작성해주세요. 9. 프롬프트 끝에 미드저니 파라미터 "--ar 1:1 --s 750 --q 2 --v 5.2"를 추가해주세요. 한국어 입력 내용을 전문적인 영어로 번역하여 반영해주세요. """ model = genai_generative.GenerativeModel( 'gemini-2.0-flash', system_instruction=generate_enhanced_system_instruction() ) response = model.generate_content( prompt_request, generation_config=genai_generative.types.GenerationConfig( temperature=0.8, top_p=0.97, top_k=64, max_output_tokens=1600, ) ) response_text = response.text.strip() if "--ar 1:1" not in response_text: response_text = response_text.rstrip(".") + ". --ar 1:1 --s 750 --q 2 --v 5.2" return response_text except Exception as e: return f"프롬프트 생성 중 오류가 발생했습니다: {str(e)}" # ------------------- 단일 이미지 생성 함수 ------------------- def generate_product_image(image, bg_type, simple, studio, nature, indoor, special, product_name, additional_info): if image is None: return None, "이미지를 업로드해주세요.", "이미지를 업로드 후 프롬프트를 생성해주세요." product_name = product_name.strip() or "제품" background_info = get_selected_background_info(bg_type, simple, studio, nature, indoor, special) generated_prompt = generate_prompt_with_gemini(product_name, background_info, additional_info) if "Gemini API 키가 설정되지 않았습니다" in generated_prompt: warning_msg = ( "[Gemini API 키 누락]\n" "API 키 설정 방법:\n" "1. 환경 변수: export GEMINI_API_KEY_1=\"your-api-key-1\"\n" "2. 환경 변수: export GEMINI_API_KEY_2=\"your-api-key-2\"\n" "3. 환경 변수: export GEMINI_API_KEY_3=\"your-api-key-3\"\n" "키 발급: https://aistudio.google.com/apikey" ) return None, warning_msg, warning_msg final_prompt = filter_prompt_only(generated_prompt) result_image, status, _ = process_images_with_prompt(image, final_prompt, 0) return result_image, status, final_prompt # ------------------- 4장 이미지 생성 함수 ------------------- def generate_product_images(image, bg_type, simple, studio, nature, indoor, special, product_name, additional_info): if image is None: return None, None, None, None, "이미지를 업로드해주세요.", "이미지를 업로드 후 프롬프트를 생성해주세요." product_name = product_name.strip() or "제품" background_info = get_selected_background_info(bg_type, simple, studio, nature, indoor, special) generated_prompt = generate_prompt_with_gemini(product_name, background_info, additional_info) if "Gemini API 키가 설정되지 않았습니다" in generated_prompt: warning_msg = ( "[Gemini API 키 누락]\n" "API 키 설정 방법:\n" "1. 환경 변수: export GEMINI_API_KEY_1=\"your-api-key-1\"\n" "2. 환경 변수: export GEMINI_API_KEY_2=\"your-api-key-2\"\n" "3. 환경 변수: export GEMINI_API_KEY_3=\"your-api-key-3\"\n" "키 발급: https://aistudio.google.com/apikey" ) return None, None, None, None, warning_msg, warning_msg final_prompt = filter_prompt_only(generated_prompt) images_list = [] statuses = [] for i in range(4): result_img, status, _ = process_images_with_prompt(image, final_prompt, variation_index=i) images_list.append(result_img) statuses.append(f"이미지 #{i+1}: {status}") time.sleep(1) combined_status = "\n".join(statuses) return images_list[0], images_list[1], images_list[2], images_list[3], combined_status, final_prompt # ------------------- 예시 탭을 위한 함수 ------------------- def load_image_cached(image_path): """이미지를 캐시하여 로드하는 함수""" global IMAGE_CACHE if image_path not in IMAGE_CACHE: try: img = Image.open(image_path) # 큰 이미지는 미리 리사이즈하여 캐시 if max(img.size) > 1000: ratio = 1000 / max(img.size) new_size = (int(img.size[0] * ratio), int(img.size[1] * ratio)) img = img.resize(new_size, Image.Resampling.LANCZOS) IMAGE_CACHE[image_path] = img except Exception as e: logger.error(f"이미지 로드 실패: {image_path}, 에러: {e}") return None return IMAGE_CACHE[image_path] def preload_example_images(): """예시 이미지들을 미리 로드하는 함수""" for example in product_background_examples: load_image_cached(example[0]) # 입력 이미지 load_image_cached(example[5]) # 결과 이미지 def load_example(evt: gr.SelectData): """선택된 예시의 정보를 로드하는 함수""" selected_example = product_background_examples[evt.index] return ( selected_example[0], # 입력 이미지 selected_example[1], # 배경 유형 selected_example[2], # 배경 선택 selected_example[3], # 상품명 selected_example[4], # 추가 요청사항 selected_example[5] # 결과 이미지 ) # ------------------- Gradio 인터페이스 구성 ------------------- def create_app(): dropdown_options = initialize_dropdowns() 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) # 제목과 사용 가이드 제거 # gr.HTML(header_html) # gr.HTML(guide_html) with gr.Tabs(): # 첫 번째 탭: 이미지 생성 with gr.TabItem("이미지 생성"): with gr.Row(): with gr.Column(scale=1): # API 키 입력 섹션 제거 # 이미지 업로드 및 설정 섹션 with gr.Column(elem_classes="custom-frame"): gr.HTML('