img_3 / app.py
Kims12's picture
Update app.py
fe6aa7a verified
raw
history blame
27.9 kB
import gradio as gr
import google.generativeai as genai
from PIL import Image
import os
import json
import tempfile
import re
import time
import logging
# λ‘œκΉ… μ„€μ •
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# Gemini API ν‚€ μ„€μ • (ν™˜κ²½ λ³€μˆ˜μ—μ„œ κ°€μ Έμ˜€κ±°λ‚˜ 직접 μž…λ ₯)
GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY", "")
# Gemini API μ΄ˆκΈ°ν™”
genai.configure(api_key=GEMINI_API_KEY)
# λ°°κ²½ JSON 파일 경둜 μ„€μ • - μƒλŒ€ 경둜 μ‚¬μš©
BACKGROUNDS_DIR = "./background"
# 디버깅을 μœ„ν•œ 정보 좜λ ₯
print(f"ν˜„μž¬ μž‘μ—… 디렉토리: {os.getcwd()}")
print(f"μ‚¬μš© 쀑인 λ°°κ²½ 디렉토리 경둜: {BACKGROUNDS_DIR}")
# JSON 파일이 μ‘΄μž¬ν•˜μ§€ μ•Šμ„ 경우 디렉토리 생성
if not os.path.exists(BACKGROUNDS_DIR):
os.makedirs(BACKGROUNDS_DIR)
print(f"λ°°κ²½ 디렉토리λ₯Ό μƒμ„±ν–ˆμŠ΅λ‹ˆλ‹€: {BACKGROUNDS_DIR}")
else:
print(f"λ°°κ²½ 디렉토리가 이미 μ‘΄μž¬ν•©λ‹ˆλ‹€: {BACKGROUNDS_DIR}")
try:
for file in os.listdir(BACKGROUNDS_DIR):
print(f"발견된 파일: {file}")
except Exception as e:
print(f"디렉토리 λ‚΄μš©μ„ λ‚˜μ—΄ν•˜λŠ” 쀑 였λ₯˜ λ°œμƒ: {str(e)}")
# JSON 파일 λ‘œλ“œ ν•¨μˆ˜
def load_background_json(filename):
file_path = os.path.join(BACKGROUNDS_DIR, filename)
try:
with open(file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
print(f"{filename} νŒŒμΌμ„ μ„±κ³΅μ μœΌλ‘œ λ‘œλ“œν–ˆμŠ΅λ‹ˆλ‹€. {len(data)} ν•­λͺ© 포함.")
return data
except FileNotFoundError:
print(f"κ²½κ³ : {filename} νŒŒμΌμ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€. 기본값을 μ‚¬μš©ν•©λ‹ˆλ‹€.")
return {}
except json.JSONDecodeError:
print(f"κ²½κ³ : {filename} 파일의 JSON ν˜•μ‹μ΄ μ˜¬λ°”λ₯΄μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. 기본값을 μ‚¬μš©ν•©λ‹ˆλ‹€.")
return {}
except Exception as e:
print(f"κ²½κ³ : {filename} 파일 λ‘œλ“œ 쀑 였λ₯˜ λ°œμƒ: {str(e)}. 기본값을 μ‚¬μš©ν•©λ‹ˆλ‹€.")
return {}
# λ°°κ²½ 데이터 λ‘œλ“œ
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")
ABSTRACT_BACKGROUNDS = load_background_json("abstract_backgrounds.json")
# 배경이 λ‘œλ“œλ˜μ§€ μ•Šμ€ 경우 κΈ°λ³Έκ°’ μ„€μ •
if not SIMPLE_BACKGROUNDS:
SIMPLE_BACKGROUNDS = {"ν™”μ΄νŠΈ λ°°κ²½": "white background"}
if not STUDIO_BACKGROUNDS:
STUDIO_BACKGROUNDS = {"μ œν’ˆ 사진 μŠ€νŠœλ””μ˜€": "product photography studio"}
if not NATURE_BACKGROUNDS:
NATURE_BACKGROUNDS = {"μ—΄λŒ€ ν•΄λ³€": "tropical beach"}
if not INDOOR_BACKGROUNDS:
INDOOR_BACKGROUNDS = {"λͺ¨λ˜ 리빙룸": "modern living room"}
if not ABSTRACT_BACKGROUNDS:
ABSTRACT_BACKGROUNDS = {"λ„€μ˜¨ μ‘°λͺ…": "neon lights"}
def generate_system_instruction():
return """당신은 μƒν’ˆ μ΄λ―Έμ§€μ˜ 배경을 λ³€κ²½ν•˜κΈ° μœ„ν•œ κ³ ν’ˆμ§ˆ ν”„λ‘¬ν”„νŠΈλ₯Ό μƒμ„±ν•˜λŠ” μ „λ¬Έκ°€μž…λ‹ˆλ‹€.
μ‚¬μš©μžκ°€ μ œκ³΅ν•˜λŠ” μƒν’ˆλͺ…, λ°°κ²½ μœ ν˜•, μΆ”κ°€ μš”μ²­μ‚¬ν•­μ„ λ°”νƒ•μœΌλ‘œ λ―Έλ“œμ €λ‹ˆ(Midjourney)에 μ‚¬μš©ν•  수 μžˆλŠ”
μƒμ„Έν•˜κ³  전문적인 ν”„λ‘¬ν”„νŠΈλ₯Ό μ˜μ–΄λ‘œ μƒμ„±ν•΄μ£Όμ„Έμš”.
λ‹€μŒ κ°€μ΄λ“œλΌμΈμ„ λ°˜λ“œμ‹œ 따라야 ν•©λ‹ˆλ‹€:
1. μƒν’ˆμ„ "#1"둜 μ§€μ •ν•˜μ—¬ μ°Έμ‘°ν•©λ‹ˆλ‹€. (예: "skincare tube (#1)")
2. *** 맀우 μ€‘μš”: μƒν’ˆμ˜ μ›λž˜ νŠΉμ„±(λ””μžμΈ, 색상, ν˜•νƒœ, 둜고, νŒ¨ν‚€μ§€ λ“±)은 μ–΄λ–€ μƒν™©μ—μ„œλ„ μ ˆλŒ€ λ³€κ²½ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. ***
3. *** μƒν’ˆμ˜ 본질적 νŠΉμ„±μ€ μœ μ§€ν•˜λ˜, μžμ—°μŠ€λŸ¬μš΄ ν™˜κ²½ 톡합을 μœ„ν•œ μ‘°λͺ…κ³Ό κ·Έλ¦ΌμžλŠ” ν—ˆμš©ν•©λ‹ˆλ‹€: ***
- μƒν’ˆ 자체의 색상, λ””μžμΈ, ν˜•νƒœ, ν…μŠ€μ²˜λŠ” μ ˆλŒ€ μˆ˜μ •ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.
- ν™˜κ²½κ³Ό μžμ—°μŠ€λŸ½κ²Œ μ–΄μšΈλ¦¬λŠ” 그림자, μ£Όλ³€ μ‘°λͺ… νš¨κ³ΌλŠ” ν—ˆμš©λ©λ‹ˆλ‹€.
- μƒν’ˆμ— 물방울, 응좕, 금, 은과 같은 μΆ”κ°€ μš”μ†Œλ‚˜ 물리적 νš¨κ³ΌλŠ” μ μš©ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.
- ν™˜κ²½μ— μ–΄μšΈλ¦¬λŠ” μžμ—°μŠ€λŸ¬μš΄ λΉ› λ°˜μ‚¬, μ£Όλ³€ μ‘°λͺ…, κ·Έλ¦ΌμžλŠ” 사싀적 톡합감을 μœ„ν•΄ μ μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
4. 이미지 λΉ„μœ¨μ€ μ •ν™•νžˆ 1:1(μ •μ‚¬κ°ν˜•) ν˜•μ‹μœΌλ‘œ μ§€μ •ν•©λ‹ˆλ‹€. ν”„λ‘¬ν”„νŠΈμ— "square format", "1:1 ratio" λ˜λŠ” "aspect ratio 1:1"을 λͺ…μ‹œμ μœΌλ‘œ ν¬ν•¨ν•©λ‹ˆλ‹€.
5. μƒν’ˆμ€ λ°˜λ“œμ‹œ μ •μ‚¬κ°ν˜• κ΅¬λ„μ˜ 정쀑앙에 λ°°μΉ˜λ˜μ–΄μ•Ό ν•©λ‹ˆλ‹€.
6. μƒν’ˆμ„ μ΄λ―Έμ§€μ˜ μ£Όμš” 초점으둜 λΆ€κ°μ‹œν‚€κ³ , μƒν’ˆμ˜ λΉ„μœ¨μ΄ 전체 μ΄λ―Έμ§€μ—μ„œ 크게 μ°¨μ§€ν•˜λ„λ‘ ν•©λ‹ˆλ‹€.
7. μƒν’ˆ 이미지 컷아웃(#1)의 κΈ°λ³Έ ν˜•νƒœμ™€ 색상은 μœ μ§€ν•˜λ©΄μ„œ, μ„ νƒν•œ ν™˜κ²½μ— μžμ—°μŠ€λŸ½κ²Œ ν†΅ν•©λ˜λ„λ‘ ν•©λ‹ˆλ‹€.
8. κ³ κΈ‰μŠ€λŸ¬μš΄ 상업적 이미지λ₯Ό μœ„ν•œ λ‹€μŒ ν™˜κ²½ μš”μ†Œλ“€μ„ ν¬ν•¨ν•˜μ„Έμš”:
- μƒν’ˆκ³Ό μ–΄μšΈλ¦¬λŠ” μ£Όλ³€ ν™˜κ²½/λ°°κ²½ μš”μ†Œλ₯Ό μΆ”κ°€ν•©λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄, ν™”μž₯ν’ˆ 주변에 κ½ƒμ΄λ‚˜ ν—ˆλΈŒ, 음료 μ œν’ˆ μ˜†μ— 과일, μ „μžμ œν’ˆ κ·Όμ²˜μ— ν˜„λŒ€μ  μ†Œν’ˆ λ“±.
- ν™˜κ²½μ˜ μ‘°λͺ… 효과(λ¦Ό 라이트, 백라이트, μ†Œν”„νŠΈλ°•μŠ€ λ“±)λ₯Ό μ„€λͺ…ν•©λ‹ˆλ‹€.
- μƒν’ˆμ΄ ν™˜κ²½μ— μžμ—°μŠ€λŸ½κ²Œ μ‘΄μž¬ν•˜λŠ” κ²ƒμ²˜λŸΌ 보이도둝 μ μ ˆν•œ κ·Έλ¦Όμžμ™€ λΉ› ν‘œν˜„μ„ ν¬ν•¨ν•©λ‹ˆλ‹€.
- μƒν’ˆμ˜ μš©λ„λ‚˜ μž₯점을 κ°„μ ‘μ μœΌλ‘œ μ•”μ‹œν•˜λŠ” λ°°κ²½ μš”μ†Œλ₯Ό ν¬ν•¨ν•©λ‹ˆλ‹€.
- ν”„λ‘œνŽ˜μ…”λ„ν•œ 상업 사진 효과(선택적 피사계 심도, μ†Œν”„νŠΈ 포컀슀, μŠ€νŠœλ””μ˜€ μ‘°λͺ… λ“±)λ₯Ό λͺ…μ‹œν•©λ‹ˆλ‹€.
9. ν”„λ‘¬ν”„νŠΈμ— λ‹€μŒ μš”μ†Œλ“€μ„ λͺ…μ‹œμ μœΌλ‘œ ν¬ν•¨ν•˜μ„Έμš”:
- "highly detailed commercial photography"
- "award-winning product photography"
- "professional advertising imagery"
- "studio quality"
- "magazine advertisement quality"
10. λ°°κ²½ ν™˜κ²½ μš”μ†Œλ₯Ό μƒν’ˆ μΉ΄ν…Œκ³ λ¦¬μ— 맞게 μ„ νƒν•©λ‹ˆλ‹€:
- μŠ€ν‚¨μΌ€μ–΄ μ œν’ˆ: κΉ¨λ—ν•œ μš•μ‹€ μ„ λ°˜, μš°μ•„ν•œ ν™”μž₯λŒ€, 슀파 같은 ν™˜κ²½ λ“±
- 음료 μ œν’ˆ: μ„Έλ ¨λœ ν…Œμ΄λΈ”, νŒŒν‹° ν™˜κ²½, μ•Όμ™Έ 피크닉 μž₯λ©΄ λ“±
- μ „μž μ œν’ˆ: μ„Έλ ¨λœ μž‘μ—… 곡간, ν˜„λŒ€μ μΈ κ±°μ‹€, λ―Έλ‹ˆλ©€ν•œ 책상 λ“±
- νŒ¨μ…˜/의λ₯˜: μ„Έλ ¨λœ μ‡Όλ£Έ, λ„μ‹œ 거리, μ—˜λ ˆκ°•μŠ€ν•œ λΌμ΄ν”„μŠ€νƒ€μΌ ν™˜κ²½ λ“±
- μ‹ν’ˆ μ œν’ˆ: κΉ”λ”ν•œ μ£Όλ°©, 식탁, μš”λ¦¬ ν™˜κ²½ λ“±
11. μ‚¬μš©μžκ°€ μ œκ³΅ν•œ ꡬ체적인 λ°°κ²½κ³Ό μΆ”κ°€ μš”μ²­μ‚¬ν•­μ„ μ •ν™•νžˆ λ°˜μ˜ν•©λ‹ˆλ‹€.
12. ν”„λ‘¬ν”„νŠΈλŠ” λ―Έλ“œμ €λ‹ˆ AI에 μ΅œμ ν™”λ˜μ–΄μ•Ό ν•©λ‹ˆλ‹€.
13. ν”„λ‘¬ν”„νŠΈ 끝에 "--ar 1:1 --s 750 --q 2" νŒŒλΌλ―Έν„°λ₯Ό μΆ”κ°€ν•˜μ—¬ λ―Έλ“œμ €λ‹ˆμ—μ„œ κ³ ν’ˆμ§ˆ μ •μ‚¬κ°ν˜• λΉ„μœ¨μ„ κ°•μ œν•©λ‹ˆλ‹€.
좜λ ₯ ν˜•μ‹μ€ μ˜μ–΄λ‘œ 된 단일 λ‹¨λ½μ˜ μƒμ„Έν•œ ν”„λ‘¬ν”„νŠΈμ—¬μ•Ό ν•˜λ©°, 끝에 λ―Έλ“œμ €λ‹ˆ νŒŒλΌλ―Έν„°κ°€ ν¬ν•¨λ˜μ–΄μ•Ό ν•©λ‹ˆλ‹€.
"""
def generate_prompt_with_gemini(product_name, background_info, additional_info=""):
if not GEMINI_API_KEY:
return "Gemini API ν‚€κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. ν™˜κ²½ λ³€μˆ˜ GEMINI_API_KEYλ₯Ό μ„€μ •ν•˜κ±°λ‚˜ μ½”λ“œμ— 직접 μž…λ ₯ν•˜μ„Έμš”."
try:
prompt_request = f"""
μƒν’ˆλͺ…: {product_name}
λ°°κ²½ μœ ν˜•: {background_info.get('english', 'studio')}
λ°°κ²½ μΉ΄ν…Œκ³ λ¦¬: {background_info.get('category', '')}
λ°°κ²½ 이름: {background_info.get('name', '')}
μΆ”κ°€ μš”μ²­μ‚¬ν•­: {additional_info}
μ€‘μš” μš”κ΅¬μ‚¬ν•­:
1. μƒν’ˆμ΄ 크게 λΆ€κ°λ˜κ³  μ΄λ―Έμ§€μ—μ„œ 쀑심적인 μœ„μΉ˜λ₯Ό μ°¨μ§€ν•˜λ„λ‘ ν”„λ‘¬ν”„νŠΈλ₯Ό μƒμ„±ν•΄μ£Όμ„Έμš”.
2. μ΄λ―Έμ§€λŠ” μ •ν™•νžˆ 1:1 λΉ„μœ¨(μ •μ‚¬κ°ν˜•)이어야 ν•©λ‹ˆλ‹€.
3. μƒν’ˆμ€ μ •μ‚¬κ°ν˜• ν”„λ ˆμž„μ˜ 정쀑앙에 μœ„μΉ˜ν•΄μ•Ό ν•©λ‹ˆλ‹€.
4. μƒν’ˆμ˜ λ””μžμΈ, 색상, ν˜•νƒœ, 둜고 λ“± 본질적 νŠΉμ„±μ€ μ ˆλŒ€ μˆ˜μ •ν•˜μ§€ λ§ˆμ„Έμš”.
5. ν™˜κ²½κ³Όμ˜ μžμ—°μŠ€λŸ¬μš΄ 톡합을 μœ„ν•œ μ‘°λͺ… νš¨κ³Όμ™€ κ·Έλ¦ΌμžλŠ” ν¬ν•¨ν•΄μ£Όμ„Έμš”.
6. μƒν’ˆμ„ 더 λ‹λ³΄μ΄κ²Œ ν•˜λŠ” λ°°κ²½ ν™˜κ²½μ„ μ„€λͺ…ν•΄μ£Όμ„Έμš”.
7. κ³ κΈ‰μŠ€λŸ¬μš΄ 상업 κ΄‘κ³  ν’ˆμ§ˆμ˜ 이미지가 λ˜λ„λ‘ ν™˜κ²½ μ„€λͺ…을 ν•΄μ£Όμ„Έμš”.
8. ν”„λ‘¬ν”„νŠΈ 끝에 λ―Έλ“œμ €λ‹ˆ νŒŒλΌλ―Έν„° "--ar 1:1 --s 750 --q 2"λ₯Ό μΆ”κ°€ν•΄μ£Όμ„Έμš”.
ν•œκ΅­μ–΄ μž…λ ₯ λ‚΄μš©μ„ μ˜μ–΄λ‘œ 적절히 λ²ˆμ—­ν•˜μ—¬ λ°˜μ˜ν•΄μ£Όμ„Έμš”.
"""
model = genai.GenerativeModel(
'gemini-2.0-flash',
system_instruction=generate_system_instruction()
)
response = model.generate_content(
prompt_request,
generation_config=genai.GenerationConfig(
temperature=0.7,
top_p=0.95,
top_k=64,
max_output_tokens=1024,
)
)
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"
return response_text
except Exception as e:
return f"ν”„λ‘¬ν”„νŠΈ 생성 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: {str(e)}"
# 이미지 생성에 ν•„μš”ν•œ ν•¨μˆ˜λ“€
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")
prompt = prompt.replace("#2", "IMAGE_TAG_TWO")
prompt = prompt.replace("#3", "IMAGE_TAG_THREE")
try:
if not GEMINI_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
model = genai.GenerativeModel('gemini-2.0-flash')
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 = model.generate_content(
translation_prompt,
generation_config=genai.GenerationConfig(
temperature=0.2,
top_p=0.95,
top_k=40,
max_output_tokens=512
)
)
translated_text = response.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):
try:
if not GEMINI_API_KEY:
return None, "API ν‚€κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. ν™˜κ²½λ³€μˆ˜λ₯Ό ν™•μΈν•΄μ£Όμ„Έμš”."
model = genai.GenerativeModel('gemini-2.0-flash-exp-image-generation')
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 = model.generate_content(
contents=contents,
generation_config=genai.GenerationConfig(
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
if hasattr(response, 'candidates') and response.candidates:
candidate = response.candidates[0]
if hasattr(candidate, 'content') and candidate.content:
for part in candidate.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):
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:
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 = 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:
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) < 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
# μ»€μŠ€ν…€ CSS μŠ€νƒ€μΌ
custom_css = """
:root {
--primary-color: #5561e9;
--secondary-color: #6c8aff;
--accent-color: #ff6b6b;
--background-color: #f0f5ff;
--card-bg: #ffffff;
--text-color: #334155;
--border-radius: 18px;
--shadow: 0 8px 30px rgba(0, 0, 0, 0.08);
}
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;
}
/* Gradio μ»¨ν…Œμ΄λ„ˆ μ˜€λ²„λΌμ΄λ“œ */
.gradio-container {
max-width: 100% !important;
margin: 0 auto !important;
padding: 0 !important;
background-color: var(--background-color) !important;
}
/* 상단 헀더 */
.app-header {
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
color: white;
padding: 2rem;
border-radius: var(--border-radius);
margin-bottom: 1.5rem;
box-shadow: var(--shadow);
text-align: center;
}
.app-header h1 {
margin: 0;
font-size: 2.5rem;
font-weight: 700;
letter-spacing: -0.5px;
}
.app-header p {
margin: 0.75rem 0 0;
font-size: 1.1rem;
opacity: 0.9;
}
/* νŒ¨λ„ μŠ€νƒ€μΌλ§ */
.panel {
background-color: var(--card-bg);
border-radius: var(--border-radius);
box-shadow: var(--shadow);
padding: 1.5rem;
margin-bottom: 1.5rem;
border: 1px solid rgba(0, 0, 0, 0.04);
transition: transform 0.3s ease;
}
.panel:hover {
transform: translateY(-5px);
}
/* μ„Ήμ…˜ 제λͺ© */
.section-title {
font-size: 1.5rem;
font-weight: 700;
color: var(--primary-color);
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid var(--secondary-color);
display: flex;
align-items: center;
}
.section-title i {
margin-right: 0.5rem;
font-size: 1.4rem;
}
/* λ²„νŠΌ μŠ€νƒ€μΌλ§ */
.custom-button {
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
color: white !important;
border: none !important;
border-radius: calc(var(--border-radius) - 5px) !important;
padding: 0.8rem 1.2rem !important;
font-weight: 600 !important;
cursor: pointer !important;
transition: all 0.3s ease !important;
box-shadow: 0 4px 6px rgba(50, 50, 93, 0.11), 0 1px 3px rgba(0, 0, 0, 0.08);
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 7px 14px rgba(50, 50, 93, 0.1), 0 3px 6px rgba(0, 0, 0, 0.08) !important;
}
.custom-button.primary {
background: linear-gradient(135deg, var(--accent-color), #ff9a8b) !important;
}
.custom-button i {
margin-right: 0.5rem;
}
/* 이미지 μ»¨ν…Œμ΄λ„ˆ */
.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;
}
.image-container:hover {
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
}
/* νƒ­ μ»¨ν…Œμ΄λ„ˆ */
.custom-tabs {
background-color: transparent !important;
border: none !important;
margin-bottom: 1rem;
}
.custom-tabs button {
background-color: rgba(255, 255, 255, 0.7) !important;
border: none !important;
border-radius: var(--border-radius) var(--border-radius) 0 0 !important;
padding: 0.8rem 1.5rem !important;
font-weight: 600 !important;
color: var(--text-color) !important;
transition: all 0.3s ease !important;
}
.custom-tabs button[aria-selected="true"] {
background-color: var(--primary-color) !important;
color: white !important;
}
/* μž…λ ₯ ν•„λ“œ */
.custom-input {
border-radius: calc(var(--border-radius) - 5px) !important;
border: 1px solid rgba(0, 0, 0, 0.1) !important;
padding: 0.8rem 1rem !important;
transition: all 0.3s ease !important;
}
.custom-input:focus {
border-color: var(--primary-color) !important;
box-shadow: 0 0 0 2px rgba(85, 97, 233, 0.2) !important;
}
/* μ‚¬μš©μž 맀뉴얼 */
.user-manual {
background-color: white;
padding: 2rem;
border-radius: var(--border-radius);
box-shadow: var(--shadow);
margin-top: 2rem;
}
.manual-title {
font-size: 1.8rem;
font-weight: 700;
color: var(--primary-color);
margin-bottom: 1.5rem;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
}
.manual-title i {
margin-right: 0.5rem;
font-size: 1.8rem;
}
.manual-section {
margin-bottom: 1.5rem;
padding: 1.2rem;
background-color: #f8faff;
border-radius: calc(var(--border-radius) - 5px);
}
.manual-section-title {
font-size: 1.3rem;
font-weight: 700;
margin-bottom: 1rem;
color: var(--primary-color);
display: flex;
align-items: center;
}
.manual-section-title i {
margin-right: 0.5rem;
font-size: 1.2rem;
}
.manual-text {
font-size: 1rem;
line-height: 1.7;
}
.manual-text strong {
color: var(--accent-color);
}
.tip-box {
background-color: rgba(255, 107, 107, 0.1);
border-left: 3px solid var(--accent-color);
padding: 1rem 1.2rem;
margin: 1rem 0;
border-radius: 8px;
}
/* λ²„νŠΌ κ·Έλ£Ή */
.button-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 0.8rem;
margin-bottom: 1.2rem;
}
/* λ‘œλ”© μ• λ‹ˆλ©”μ΄μ…˜ */
.progress-container {
background-color: rgba(255, 255, 255, 0.9);
border-radius: var(--border-radius);
padding: 2rem;
box-shadow: var(--shadow);
text-align: center;
}
.progress-bar {
height: 8px;
border-radius: 4px;
background: linear-gradient(90deg, var(--primary-color), var(--secondary-color));
margin: 1rem 0;
}
/* μ˜ˆμ‹œ κ·Έλ¦¬λ“œ */
.examples-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 1rem;
margin: 1.5rem 0;
}
.example-card {
background: white;
border-radius: var(--border-radius);
overflow: hidden;
box-shadow: var(--shadow);
transition: transform 0.3s ease;
}
.example-card:hover {
transform: translateY(-5px);
}
/* λ°˜μ‘ν˜• */
@media (max-width: 768px) {
.button-grid {
grid-template-columns: repeat(2, 1fr);
}
}
/* 전체 μΈν„°νŽ˜μ΄μŠ€ λ‘₯κ·Ό λͺ¨μ„œλ¦¬ */
.block, .prose, .gr-prose, .gr-form, .gr-panel {
border-radius: var(--border-radius) !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(--secondary-color);
border-radius: 10px;
}