import os import tempfile from PIL import Image import gradio as gr import logging import re from io import BytesIO import time from google import genai from google.genai import types # 환경변수 로드 from dotenv import load_dotenv load_dotenv() # 로깅 설정 logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) def save_binary_file(file_name, data): with open(file_name, "wb") as f: f.write(data) def translate_prompt_to_english(prompt): """ 입력된 프롬프트에 한글이 포함되어 있으면 Gemini‑2.0‑flash 모델을 사용하여 영어로 번역합니다. 한글이 없으면 원본 프롬프트를 그대로 반환합니다. 중요: #1, #2, #3 태그는 번역 전후에 반드시 보존되어야 합니다. """ if not re.search("[가-힣]", prompt): return prompt # #1, #2, #3 태그를 임시 토큰으로 대체하여 보존 prompt = prompt.replace("#1", "IMAGE_TAG_ONE") prompt = prompt.replace("#2", "IMAGE_TAG_TWO") prompt = prompt.replace("#3", "IMAGE_TAG_THREE") try: api_key = os.environ.get("GEMINI_API_KEY") if not api_key: logger.error("Gemini API 키가 설정되지 않았습니다.") # 임시 토큰을 원래 태그로 복원 prompt = prompt.replace("IMAGE_TAG_ONE", "#1") prompt = prompt.replace("IMAGE_TAG_TWO", "#2") prompt = prompt.replace("IMAGE_TAG_THREE", "#3") return prompt client = genai.Client(api_key=api_key) translation_prompt = f""" Translate the following Korean text to English: {prompt} IMPORTANT: The tokens IMAGE_TAG_ONE, IMAGE_TAG_TWO, and IMAGE_TAG_THREE are special tags and must be preserved exactly as is in your translation. Do not translate these tokens. """ 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") translated_text = translated_text.replace("IMAGE_TAG_TWO", "#2") translated_text = translated_text.replace("IMAGE_TAG_THREE", "#3") logger.info(f"Translated text: {translated_text.strip()}") return translated_text.strip() else: logger.warning("번역 결과가 없습니다. 원본 프롬프트 사용") # 임시 토큰을 원래 태그로 복원 prompt = prompt.replace("IMAGE_TAG_ONE", "#1") prompt = prompt.replace("IMAGE_TAG_TWO", "#2") prompt = prompt.replace("IMAGE_TAG_THREE", "#3") return prompt except Exception as e: logger.exception("번역 중 오류 발생:") # 임시 토큰을 원래 태그로 복원 prompt = prompt.replace("IMAGE_TAG_ONE", "#1") prompt = prompt.replace("IMAGE_TAG_TWO", "#2") prompt = prompt.replace("IMAGE_TAG_THREE", "#3") 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 호출을 통해 이미지를 생성하고 결과 이미지를 반환합니다. variation_index로 다양한 변화를 줍니다. """ try: api_key = os.environ.get("GEMINI_API_KEY") if not api_key: return None, "API 키가 설정되지 않았습니다. 환경변수를 확인해주세요." client = genai.Client(api_key=api_key) logger.info(f"Gemini API 요청 시작 - 프롬프트: {prompt}, 변형 인덱스: {variation_index}") # 변형 인덱스에 따라 프롬프트에 약간의 변화 추가 variation_suffixes = [ " Create this as the first variation. Do not add any text, watermarks, or labels to the image.", " Create this as the second variation with more vivid colors. Do not add any text, watermarks, or labels to the image.", " Create this as the third variation with a more creative style. Do not add any text, watermarks, or labels to the image.", " Create this as the fourth variation with enhanced details. 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 + " 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, top_p=0.95, top_k=40, max_output_tokens=8192 ) ) with tempfile.NamedTemporaryFile(suffix=".png", 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_text}" result_img = Image.open(temp_path) if result_img.mode == "RGBA": result_img = result_img.convert("RGB") return result_img, f"이미지가 성공적으로 생성되었습니다. {result_text}" except Exception as e: logger.exception("이미지 생성 중 오류 발생:") return None, f"오류 발생: {str(e)}" def process_images_with_prompt(image1, image2, image3, prompt, variation_index=0, max_retries=3): """ 3개의 이미지와 프롬프트를 처리하여 최종 영어 프롬프트(final_prompt)를 생성한 후, API를 호출하여 결과 이미지를 반환합니다. 에러 발생 시 최대 max_retries 횟수만큼 재시도합니다. """ 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: 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: # API 호출은 성공했지만 이미지 생성 실패 last_error = status retry_count += 1 logger.warning(f"이미지 생성 실패, 재시도 {retry_count}/{max_retries}: {status}") time.sleep(1) # 1초 대기 후 재시도 except Exception as e: last_error = str(e) retry_count += 1 logger.exception(f"이미지 처리 중 오류 발생, 재시도 {retry_count}/{max_retries}:") time.sleep(1) # 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 = 4 # 생성할 이미지 수 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: # 에러가 발생한 경우에도 결과 목록에 None을 추가 results.append(None) statuses.append(f"이미지 #{i+1} 생성 실패: {status}") prompts.append(f"이미지 #{i+1}: {final_prompt}") # API 호출 사이에 약간의 간격을 두어 속도 제한 방지 time.sleep(1) progress(1.0, desc="이미지 생성 완료!") # 결과가 없는 경우 빈 슬롯 채우기 while len(results) < 4: results.append(None) # 상태 메시지와 프롬프트 정보 결합 combined_status = "\n".join(statuses) combined_prompts = "\n".join(prompts) return results[0], results[1], results[2], results[3], combined_status, combined_prompts # Gradio 인터페이스 with gr.Blocks() as demo: gr.HTML( """
이미지를 업로드하고 "이미지 생성" 버튼을 클릭하면 차례로 4장의 이미지가 생성됩니다.
모든 이미지는 균일한 크기로 표시됩니다.