4jdjl3hi / app.py
ssboost's picture
Update app.py
f7d1828 verified
raw
history blame
51.8 kB
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 = """
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer" />
"""
# ์ œ๋ชฉ๊ณผ ์‚ฌ์šฉ ๊ฐ€์ด๋“œ 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('<div class="section-title"><img src="https://cdn-icons-png.flaticon.com/512/3097/3097412.png"> ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ๋ฐ ์„ค์ •</div>')
product_name = gr.Textbox(
label="์ƒํ’ˆ๋ช… (ํ•œ๊ตญ์–ด ์ž…๋ ฅ)",
placeholder="์˜ˆ: ์Šคํ‚จ์ผ€์–ด ํŠœ๋ธŒ, ์Šค๋งˆํŠธ์›Œ์น˜, ํ–ฅ์ˆ˜, ์šด๋™ํ™” ๋“ฑ",
interactive=True
)
image_input = gr.Image(
label="์ƒํ’ˆ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ",
type="pil",
elem_classes="image-container"
)
# ๋ฐฐ๊ฒฝ ์˜ต์…˜ ์„น์…˜
with gr.Column(elem_classes="custom-frame"):
gr.HTML('<div class="section-title"><img src="https://cdn-icons-png.flaticon.com/512/4297/4297825.png"> ๋ฐฐ๊ฒฝ ์˜ต์…˜</div>')
background_type = gr.Radio(
choices=["์‹ฌํ”Œ ๋ฐฐ๊ฒฝ", "์ŠคํŠœ๋””์˜ค ๋ฐฐ๊ฒฝ", "์ž์—ฐ ํ™˜๊ฒฝ", "์‹ค๋‚ด ํ™˜๊ฒฝ", "ํŠน์ˆ˜๋ฐฐ๊ฒฝ"],
label="๋ฐฐ๊ฒฝ ์œ ํ˜•",
value="์‹ฌํ”Œ ๋ฐฐ๊ฒฝ"
)
# ๋“œ๋กญ๋‹ค์šด ์ปดํฌ๋„ŒํŠธ๋“ค
simple_dropdown = gr.Dropdown(
choices=dropdown_options["simple"],
value=dropdown_options["simple"][0] if dropdown_options["simple"] else None,
label="์‹ฌํ”Œ ๋ฐฐ๊ฒฝ ์„ ํƒ",
visible=True,
interactive=True
)
studio_dropdown = gr.Dropdown(
choices=dropdown_options["studio"],
value=dropdown_options["studio"][0] if dropdown_options["studio"] else None,
label="์ŠคํŠœ๋””์˜ค ๋ฐฐ๊ฒฝ ์„ ํƒ",
visible=False,
interactive=True
)
nature_dropdown = gr.Dropdown(
choices=dropdown_options["nature"],
value=dropdown_options["nature"][0] if dropdown_options["nature"] else None,
label="์ž์—ฐ ํ™˜๊ฒฝ ์„ ํƒ",
visible=False,
interactive=True
)
indoor_dropdown = gr.Dropdown(
choices=dropdown_options["indoor"],
value=dropdown_options["indoor"][0] if dropdown_options["indoor"] else None,
label="์‹ค๋‚ด ํ™˜๊ฒฝ ์„ ํƒ",
visible=False,
interactive=True
)
special_dropdown = gr.Dropdown(
choices=dropdown_options["special"],
value=dropdown_options["special"][0] if dropdown_options["special"] else None,
label="ํŠน์ˆ˜๋ฐฐ๊ฒฝ ์„ ํƒ",
visible=False,
interactive=True
)
additional_info = gr.Textbox(
label="์ถ”๊ฐ€ ์š”์ฒญ์‚ฌํ•ญ (์„ ํƒ์‚ฌํ•ญ)",
placeholder="์˜ˆ: ๊ณ ๊ธ‰์Šค๋Ÿฌ์šด ๋А๋‚Œ, ๋ฐ์€ ์กฐ๋ช…, ์ž์—ฐ์Šค๋Ÿฌ์šด ๋ณด์กฐ ๊ฐ์ฒด ๋“ฑ",
lines=3,
interactive=True
)
# ๋“œ๋กญ๋‹ค์šด ๋ณ€๊ฒฝ ํ•จ์ˆ˜
def update_dropdowns(bg_type):
return {
simple_dropdown: gr.update(visible=(bg_type == "์‹ฌํ”Œ ๋ฐฐ๊ฒฝ")),
studio_dropdown: gr.update(visible=(bg_type == "์ŠคํŠœ๋””์˜ค ๋ฐฐ๊ฒฝ")),
nature_dropdown: gr.update(visible=(bg_type == "์ž์—ฐ ํ™˜๊ฒฝ")),
indoor_dropdown: gr.update(visible=(bg_type == "์‹ค๋‚ด ํ™˜๊ฒฝ")),
special_dropdown: gr.update(visible=(bg_type == "ํŠน์ˆ˜๋ฐฐ๊ฒฝ"))
}
background_type.change(
fn=update_dropdowns,
inputs=[background_type],
outputs=[simple_dropdown, studio_dropdown, nature_dropdown, indoor_dropdown, special_dropdown]
)
# ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์„น์…˜
with gr.Column(elem_classes="custom-frame"):
gr.HTML('<div class="section-title"><img src="https://cdn-icons-png.flaticon.com/512/1022/1022293.png"> ์ด๋ฏธ์ง€ ์ƒ์„ฑ</div>')
with gr.Row():
single_btn = gr.Button("ํ”„๋กฌํ”„ํŠธ ๋ฐ ๋‹จ์ผ ์ด๋ฏธ์ง€ ์ƒ์„ฑ", elem_classes="custom-button")
multi_btn = gr.Button("ํ”„๋กฌํ”„ํŠธ ๋ฐ 4์žฅ ์ด๋ฏธ์ง€ ์ƒ์„ฑ", elem_classes="custom-button")
with gr.Column(scale=1):
# ์ƒ์„ฑ๋œ ์ด๋ฏธ์ง€ ์„น์…˜
with gr.Column(elem_classes="custom-frame"):
gr.HTML('<div class="section-title"><img src="https://cdn-icons-png.flaticon.com/512/1375/1375106.png"> ์ƒ์„ฑ๋œ ์ด๋ฏธ์ง€</div>')
with gr.Row():
with gr.Column():
image_output1 = gr.Image(label="์ด๋ฏธ์ง€ #1", elem_classes="image-container", height=400, width=400)
image_output3 = gr.Image(label="์ด๋ฏธ์ง€ #3", elem_classes="image-container", height=400, width=400)
with gr.Column():
image_output2 = gr.Image(label="์ด๋ฏธ์ง€ #2", elem_classes="image-container", height=400, width=400)
image_output4 = gr.Image(label="์ด๋ฏธ์ง€ #4", elem_classes="image-container", height=400, width=400)
# ๊ฒฐ๊ณผ ์ •๋ณด ์„น์…˜
with gr.Column(elem_classes="custom-frame"):
gr.HTML('<div class="section-title"><img src="https://cdn-icons-png.flaticon.com/512/3153/3153376.png"> ๊ฒฐ๊ณผ ์ •๋ณด</div>')
status_output = gr.Textbox(label="๊ฒฐ๊ณผ ์ •๋ณด", lines=3)
# ๋‘ ๋ฒˆ์งธ ํƒญ: ์˜ˆ์‹œ ๊ฒฐ๊ณผ ๋ณด๊ธฐ
with gr.TabItem("์˜ˆ์‹œ ๊ฒฐ๊ณผ ๋ณด๊ธฐ"):
# ์ƒํ’ˆ ๋ฐฐ๊ฒฝ ์ด๋ฏธ์ง€ ์˜ˆ์‹œ ๊ฐค๋Ÿฌ๋ฆฌ ์„น์…˜
with gr.Column(elem_classes="custom-frame"):
gr.HTML('<div class="section-title"><img src="https://cdn-icons-png.flaticon.com/512/681/681443.png"> ์ƒํ’ˆ ๋ฐฐ๊ฒฝ ์ด๋ฏธ์ง€ ์˜ˆ์‹œ ๊ฐค๋Ÿฌ๋ฆฌ</div>')
# ์ƒ๋‹จ ๋ฉ”์ธ ๋ทฐ ์˜์—ญ
with gr.Row():
example_input_image = gr.Image(
label="์ž…๋ ฅ ์ด๋ฏธ์ง€",
height=400,
width=400,
elem_classes="image-container",
show_label=True,
show_download_button=True,
container=True,
scale=1
)
with gr.Column(elem_classes="example-params"):
example_bg_type = gr.Textbox(label="๋ฐฐ๊ฒฝ ์œ ํ˜•", interactive=False)
example_bg_option = gr.Textbox(label="๋ฐฐ๊ฒฝ ์„ ํƒ", interactive=False)
example_product_name = gr.Textbox(label="์ƒํ’ˆ๋ช…", interactive=False)
example_additional_info = gr.Textbox(label="์ถ”๊ฐ€ ์š”์ฒญ์‚ฌํ•ญ", interactive=False)
example_output_image = gr.Image(
label="๊ฒฐ๊ณผ ์ด๋ฏธ์ง€",
height=400,
width=400,
elem_classes="image-container",
show_label=True,
show_download_button=True,
container=True,
scale=1
)
# ๋ฐฐ๊ฒฝ ์œ ํ˜•๋ณ„ ๊ฐค๋Ÿฌ๋ฆฌ ์„น์…˜
with gr.Column(elem_classes="custom-frame"):
gr.HTML('<div class="section-title"><img src="https://cdn-icons-png.flaticon.com/512/3068/3068327.png"> ๋ฐฐ๊ฒฝ ์œ ํ˜•๋ณ„ ์˜ˆ์‹œ</div>')
# ์˜ˆ์‹œ๋“ค์„ ๋ฐฐ๊ฒฝ ์œ ํ˜•๋ณ„๋กœ ๊ทธ๋ฃนํ™”
examples_by_type = {}
for example in product_background_examples:
bg_type = example[1] # ๋ฐฐ๊ฒฝ ์œ ํ˜•
if bg_type not in examples_by_type:
examples_by_type[bg_type] = []
examples_by_type[bg_type].append(example)
# ๋ฐฐ๊ฒฝ ์œ ํ˜•๋ณ„ ๊ฐค๋Ÿฌ๋ฆฌ ์„น์…˜ ์ƒ์„ฑ
for bg_type, examples in examples_by_type.items():
gr.Markdown(f"### {bg_type}")
# 10๊ฐœ์”ฉ ํ•œ ์ค„๋กœ ํ‘œ์‹œ
for i in range(0, len(examples), 10):
with gr.Row():
for j in range(10):
if i + j < len(examples):
example = examples[i + j]
with gr.Column(scale=1, min_width=100):
# ๊ฒฐ๊ณผ ์ด๋ฏธ์ง€๋งŒ ํ‘œ์‹œ
display_img = gr.Image(
value=example[5],
label=None, # ๋ ˆ์ด๋ธ” ์ œ๊ฑฐ
elem_classes="example-item",
height=120, # ํฌ๊ธฐ ์ถ•์†Œ
width=120, # ํฌ๊ธฐ ์ถ•์†Œ
show_label=False, # ๋ ˆ์ด๋ธ” ์ˆจ๊ธฐ๊ธฐ
show_download_button=False, # ๋‹ค์šด๋กœ๋“œ ๋ฒ„ํŠผ ์ˆจ๊ธฐ๊ธฐ
show_share_button=False, # ๊ณต์œ  ๋ฒ„ํŠผ ์ˆจ๊ธฐ๊ธฐ
container=False # ์ปจํ…Œ์ด๋„ˆ ํ…Œ๋‘๋ฆฌ ์ œ๊ฑฐ
)
def make_example_handler(ex):
def handler():
# ๋กœ๋”ฉ ์ƒํƒœ ํ‘œ์‹œ๋ฅผ ์œ„ํ•œ ์ค‘๊ฐ„ ์—…๋ฐ์ดํŠธ
yield (
gr.update(value=None), # ์ž„์‹œ๋กœ ์ด๋ฏธ์ง€ ๋น„์šฐ๊ธฐ
gr.update(value="๋กœ๋”ฉ ์ค‘..."),
gr.update(value="๋กœ๋”ฉ ์ค‘..."),
gr.update(value="๋กœ๋”ฉ ์ค‘..."),
gr.update(value="๋กœ๋”ฉ ์ค‘..."),
gr.update(value=None)
)
# ์‹ค์ œ ๋ฐ์ดํ„ฐ ๋กœ๋“œ
yield (
ex[0], # ์ž…๋ ฅ ์ด๋ฏธ์ง€
ex[1], # ๋ฐฐ๊ฒฝ ์œ ํ˜•
ex[2], # ๋ฐฐ๊ฒฝ ์„ ํƒ
ex[3], # ์ƒํ’ˆ๋ช…
ex[4] if ex[4] else "(์—†์Œ)", # ์ถ”๊ฐ€ ์š”์ฒญ์‚ฌํ•ญ
ex[5] # ๊ฒฐ๊ณผ ์ด๋ฏธ์ง€
)
return handler
display_img.select(
fn=make_example_handler(example),
outputs=[
example_input_image,
example_bg_type,
example_bg_option,
example_product_name,
example_additional_info,
example_output_image
],
queue=True # ์š”์ฒญ์„ ํ์— ๋„ฃ์–ด ์ˆœ์ฐจ์ ์œผ๋กœ ์ฒ˜๋ฆฌ
)
else:
# ๋นˆ ๊ณต๊ฐ„ ์œ ์ง€
with gr.Column(scale=1, min_width=100):
pass
# ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ ์ฒซ ๋ฒˆ์งธ ์˜ˆ์‹œ ์ž๋™ ํ‘œ์‹œ
def load_first_example():
if product_background_examples:
first_example = product_background_examples[0]
return (
first_example[0], # ์ž…๋ ฅ ์ด๋ฏธ์ง€
first_example[1], # ๋ฐฐ๊ฒฝ ์œ ํ˜•
first_example[2], # ๋ฐฐ๊ฒฝ ์„ ํƒ
first_example[3], # ์ƒํ’ˆ๋ช…
first_example[4] if first_example[4] else "(์—†์Œ)", # ์ถ”๊ฐ€ ์š”์ฒญ์‚ฌํ•ญ
first_example[5] # ๊ฒฐ๊ณผ ์ด๋ฏธ์ง€
)
return None, "", "", "", "", None
demo.load(
fn=load_first_example,
outputs=[
example_input_image,
example_bg_type,
example_bg_option,
example_product_name,
example_additional_info,
example_output_image
]
)
# ๋‹จ์ผ ์ด๋ฏธ์ง€ ์ƒ์„ฑ ํ•จ์ˆ˜ (API ํ‚ค ํŒŒ๋ผ๋ฏธํ„ฐ ์ œ๊ฑฐ)
def modified_single_image_gen(image, bg_type, simple, studio, nature, indoor, special, product_name, additional_info):
actual_api_key = get_api_key()
if actual_api_key:
os.environ["GEMINI_API_KEY"] = actual_api_key # ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์—…๋ฐ์ดํŠธ
else:
return None, None, None, None, "API ํ‚ค๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ํ™˜๊ฒฝ ๋ณ€์ˆ˜์— GEMINI_API_KEY_1, GEMINI_API_KEY_2, GEMINI_API_KEY_3 ์ค‘ ํ•˜๋‚˜ ์ด์ƒ์„ ์„ค์ •ํ•ด์ฃผ์„ธ์š”."
result_img, status, _ = generate_product_image(image, bg_type, simple, studio, nature, indoor, special, product_name, additional_info)
return result_img, None, None, None, status
# ๋‹จ์ผ ์ด๋ฏธ์ง€ ์ƒ์„ฑ ๋ฒ„ํŠผ ์ด๋ฒคํŠธ
single_btn.click(
fn=modified_single_image_gen,
inputs=[image_input, background_type, simple_dropdown, studio_dropdown, nature_dropdown, indoor_dropdown, special_dropdown, product_name, additional_info],
outputs=[image_output1, image_output2, image_output3, image_output4, status_output]
)
# 4์žฅ ์ด๋ฏธ์ง€ ์ƒ์„ฑ ํ•จ์ˆ˜ (API ํ‚ค ํŒŒ๋ผ๋ฏธํ„ฐ ์ œ๊ฑฐ)
def modified_multi_image_gen(image, bg_type, simple, studio, nature, indoor, special, product_name, additional_info):
actual_api_key = get_api_key()
if actual_api_key:
os.environ["GEMINI_API_KEY"] = actual_api_key # ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์—…๋ฐ์ดํŠธ
else:
return None, None, None, None, "API ํ‚ค๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ํ™˜๊ฒฝ ๋ณ€์ˆ˜์— GEMINI_API_KEY_1, GEMINI_API_KEY_2, GEMINI_API_KEY_3 ์ค‘ ํ•˜๋‚˜ ์ด์ƒ์„ ์„ค์ •ํ•ด์ฃผ์„ธ์š”."
img1, img2, img3, img4, combined_status, _ = generate_product_images(image, bg_type, simple, studio, nature, indoor, special, product_name, additional_info)
# ์ƒํƒœ ์ •๋ณด์—์„œ ํ”„๋กฌํ”„ํŠธ ์ •๋ณด ์ œ๊ฑฐ
cleaned_statuses = []
for status_line in combined_status.split('\n'):
if "์ด๋ฏธ์ง€ #" in status_line:
if "์ด๋ฏธ์ง€๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค" in status_line:
cleaned_statuses.append(status_line.split(":")[0] + ": ์ด๋ฏธ์ง€๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.")
else:
cleaned_statuses.append(status_line)
cleaned_combined_status = "\n".join(cleaned_statuses)
return img1, img2, img3, img4, cleaned_combined_status
# 4์žฅ ์ด๋ฏธ์ง€ ์ƒ์„ฑ ๋ฒ„ํŠผ ์ด๋ฒคํŠธ
multi_btn.click(
fn=modified_multi_image_gen,
inputs=[image_input, background_type, simple_dropdown, studio_dropdown, nature_dropdown, indoor_dropdown, special_dropdown, product_name, additional_info],
outputs=[image_output1, image_output2, image_output3, image_output4, status_output]
)
return demo
# ------------------- ๋ฉ”์ธ ์‹คํ–‰ ํ•จ์ˆ˜ -------------------
if __name__ == "__main__":
initialize_backgrounds()
initialize_api_keys() # API ํ‚ค ์ดˆ๊ธฐํ™” ํ•จ์ˆ˜ ํ˜ธ์ถœ ์ถ”๊ฐ€
preload_example_images() # ์˜ˆ์‹œ ์ด๋ฏธ์ง€ ๋ฏธ๋ฆฌ ๋กœ๋“œ
app = create_app()
app.queue(max_size=10) # ์š”์ฒญ์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๋„๋ก ํ ์„ค์ •
app.launch(share=False, inbrowser=True, width="100%")