mpdj4sd / app.py
ssboost's picture
Update app.py
bc17107 verified
#app.py
import gradio as gr
import os
import tempfile
from datetime import datetime
from PIL import Image
import random
# ๐Ÿ”‘ ํ—ˆ๊น…ํŽ˜์ด์Šค Secrets์—์„œ API ํ‚ค๋“ค ๊ฐ€์ ธ์™€์„œ ๋žœ๋ค ์„ ํƒ
def get_random_gemini_api_key():
"""ํ—ˆ๊น…ํŽ˜์ด์Šค Secrets์—์„œ API ํ‚ค ๋ฆฌ์ŠคํŠธ๋ฅผ ๊ฐ€์ ธ์™€ ๋žœ๋ค ์„ ํƒ (๋””๋ฒ„๊น… ๊ฐ•ํ™”)"""
try:
print("๐Ÿ” API ํ‚ค ๋กœ๋”ฉ ์‹œ์ž‘...")
# Secrets์—์„œ API ํ‚ค ๋ฆฌ์ŠคํŠธ ๊ฐ€์ ธ์˜ค๊ธฐ (์ฝค๋งˆ๋กœ ๊ตฌ๋ถ„๋œ ๋ฌธ์ž์—ด)
api_keys_string = os.environ.get('GEMINI_API_KEYS')
print(f"๐Ÿ” GEMINI_API_KEYS ์›๋ณธ: {api_keys_string[:50] if api_keys_string else 'None'}...")
if api_keys_string:
# ์ฝค๋งˆ๋กœ ๋ถ„๋ฆฌํ•˜๊ณ  ๊ณต๋ฐฑ ์ œ๊ฑฐ
api_keys = [key.strip() for key in api_keys_string.split(',') if key.strip()]
print(f"๐Ÿ” ํŒŒ์‹ฑ๋œ ํ‚ค ๊ฐœ์ˆ˜: {len(api_keys)}")
for i, key in enumerate(api_keys):
print(f" ํ‚ค {i+1}: {key[:8]}***{key[-4:] if len(key) > 12 else '***'} (๊ธธ์ด: {len(key)})")
if api_keys:
# ๋žœ๋คํ•˜๊ฒŒ ํ•˜๋‚˜ ์„ ํƒ
selected_key = random.choice(api_keys)
print(f"๐ŸŽฒ ๋žœ๋ค API ํ‚ค ์„ ํƒ: {selected_key[:8]}***{selected_key[-4:]}")
return selected_key
else:
print("โŒ ํŒŒ์‹ฑ๋œ ํ‚ค๊ฐ€ ์—†์Œ")
else:
print("โŒ GEMINI_API_KEYS ํ™˜๊ฒฝ๋ณ€์ˆ˜๊ฐ€ ์—†์Œ")
# ํด๋ฐฑ: ๋‹จ์ผ ํ‚ค
fallback_key = os.environ.get('GEMINI_API_KEY')
if fallback_key:
print(f"๐Ÿ”‘ ํด๋ฐฑ API ํ‚ค ์‚ฌ์šฉ: {fallback_key[:8]}***{fallback_key[-4:]}")
return fallback_key
print("โŒ ๋ชจ๋“  API ํ‚ค๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค")
return None
except Exception as e:
print(f"โŒ API ํ‚ค ์„ ํƒ ์‹คํŒจ: {e}")
return os.environ.get('GEMINI_API_KEY')
# ํ—ˆ๊น…ํŽ˜์ด์Šค Secrets์—์„œ ๋ชจ๋“ˆ ์ฝ”๋“œ ๋ถˆ๋Ÿฌ์™€์„œ ์‹คํ–‰
def load_module_from_env(env_var_name):
"""ํ™˜๊ฒฝ๋ณ€์ˆ˜์—์„œ ์ฝ”๋“œ๋ฅผ ๋ถˆ๋Ÿฌ์™€์„œ ๋ชจ๋“ˆ๋กœ ์‹คํ–‰"""
try:
code = os.environ.get(env_var_name)
if not code:
raise ImportError(f"Environment variable '{env_var_name}' not found")
# ๋ชจ๋“ˆ namespace ์ƒ์„ฑํ•˜๊ณ  ์ฝ”๋“œ ์‹คํ–‰
module_globals = {}
exec(code, module_globals)
print(f"โœ… {env_var_name} ๋ชจ๋“ˆ ๋กœ๋“œ ์„ฑ๊ณต")
return module_globals
except Exception as e:
print(f"โŒ {env_var_name} ๋ชจ๋“ˆ ๋กœ๋“œ ์‹คํŒจ: {e}")
return None
# ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ ๋ชจ๋“ˆ ์ž„ํฌํŠธ
try:
image_processor_module = load_module_from_env('IMAGE_PROCESSOR_CODE')
if image_processor_module:
create_uhp_image = image_processor_module['create_uhp_image']
save_image_to_downloads = image_processor_module['save_image_to_downloads']
print("โœ… image_processor ๋ชจ๋“ˆ ์ž„ํฌํŠธ ์„ฑ๊ณต")
else:
raise ImportError("IMAGE_PROCESSOR_CODE not found")
except ImportError as e:
print(f"โŒ image_processor ์ž„ํฌํŠธ ์˜ค๋ฅ˜: {e}")
def create_uhp_image(*args, **kwargs):
return None, "์ถ”์ฒœ๋ฐฐ๊ฒฝ", "#FFFFFF", "#000000", "#000000", 100, 55, {}
def save_image_to_downloads(*args, **kwargs):
return None, "image_processor ๋ชจ๋“ˆ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."
# ์นดํ”ผ ์ƒ์„ฑ ๋ชจ๋“ˆ ์ž„ํฌํŠธ
try:
copy_generator_module = load_module_from_env('COPY_GENERATOR_CODE')
if copy_generator_module:
generate_copy_suggestions = copy_generator_module['generate_copy_suggestions']
print("โœ… copy_generator ๋ชจ๋“ˆ ์ž„ํฌํŠธ ์„ฑ๊ณต")
else:
raise ImportError("COPY_GENERATOR_CODE not found")
except ImportError as e:
print(f"โš ๏ธ copy_generator ์ž„ํฌํŠธ ์˜ค๋ฅ˜: {e}")
def generate_copy_suggestions(keyword, api_key, selected_type):
return {
"error": "copy_generator ๋ชจ๋“ˆ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์นดํ”ผ ์ƒ์„ฑ ๊ธฐ๋Šฅ์ด ๋น„ํ™œ์„ฑํ™”๋ฉ๋‹ˆ๋‹ค."
}, ""
# ์ปค์Šคํ…€ CSS ์Šคํƒ€์ผ (๋‹คํฌ๋ชจ๋“œ ์ ์šฉ ๋ฒ„์ „)
custom_css = """
/* ============================================
๋‹คํฌ๋ชจ๋“œ ์ž๋™ ๋ณ€๊ฒฝ ํ…œํ”Œ๋ฆฟ CSS
============================================ */
/* 1. CSS ๋ณ€์ˆ˜ ์ •์˜ (๋ผ์ดํŠธ๋ชจ๋“œ - ๊ธฐ๋ณธ๊ฐ’) */
:root {
/* ๋ฉ”์ธ ์ปฌ๋Ÿฌ */
--primary-color: #FB7F0D;
--secondary-color: #ff9a8b;
--accent-color: #FF6B6B;
/* ๋ฐฐ๊ฒฝ ์ปฌ๋Ÿฌ */
--background-color: #FFFFFF;
--card-bg: #ffffff;
--input-bg: #ffffff;
/* ํ…์ŠคํŠธ ์ปฌ๋Ÿฌ */
--text-color: #334155;
--text-secondary: #64748b;
/* ๋ณด๋” ๋ฐ ๊ตฌ๋ถ„์„  */
--border-color: #dddddd;
--border-light: #e5e5e5;
/* ํ…Œ์ด๋ธ” ์ปฌ๋Ÿฌ */
--table-even-bg: #f3f3f3;
--table-hover-bg: #f0f0f0;
/* ๊ทธ๋ฆผ์ž */
--shadow: 0 8px 30px rgba(251, 127, 13, 0.08);
--shadow-light: 0 2px 4px rgba(0, 0, 0, 0.1);
/* ๊ธฐํƒ€ */
--border-radius: 18px;
}
/* 2. ๋‹คํฌ๋ชจ๋“œ ์ƒ‰์ƒ ๋ณ€์ˆ˜ (์ž๋™ ๊ฐ์ง€) */
@media (prefers-color-scheme: dark) {
:root {
/* ๋ฐฐ๊ฒฝ ์ปฌ๋Ÿฌ */
--background-color: #1a1a1a;
--card-bg: #2d2d2d;
--input-bg: #2d2d2d;
/* ํ…์ŠคํŠธ ์ปฌ๋Ÿฌ */
--text-color: #e5e5e5;
--text-secondary: #a1a1aa;
/* ๋ณด๋” ๋ฐ ๊ตฌ๋ถ„์„  */
--border-color: #404040;
--border-light: #525252;
/* ํ…Œ์ด๋ธ” ์ปฌ๋Ÿฌ */
--table-even-bg: #333333;
--table-hover-bg: #404040;
/* ๊ทธ๋ฆผ์ž */
--shadow: 0 8px 30px rgba(0, 0, 0, 0.3);
--shadow-light: 0 2px 4px rgba(0, 0, 0, 0.2);
}
}
/* 3. ์ˆ˜๋™ ๋‹คํฌ๋ชจ๋“œ ํด๋ž˜์Šค (Gradio ํ† ๊ธ€์šฉ) */
[data-theme="dark"],
.dark,
.gr-theme-dark {
/* ๋ฐฐ๊ฒฝ ์ปฌ๋Ÿฌ */
--background-color: #1a1a1a;
--card-bg: #2d2d2d;
--input-bg: #2d2d2d;
/* ํ…์ŠคํŠธ ์ปฌ๋Ÿฌ */
--text-color: #e5e5e5;
--text-secondary: #a1a1aa;
/* ๋ณด๋” ๋ฐ ๊ตฌ๋ถ„์„  */
--border-color: #404040;
--border-light: #525252;
/* ํ…Œ์ด๋ธ” ์ปฌ๋Ÿฌ */
--table-even-bg: #333333;
--table-hover-bg: #404040;
/* ๊ทธ๋ฆผ์ž */
--shadow: 0 8px 30px rgba(0, 0, 0, 0.3);
--shadow-light: 0 2px 4px rgba(0, 0, 0, 0.2);
}
/* 4. ๊ธฐ๋ณธ ์š”์†Œ ๋‹คํฌ๋ชจ๋“œ ์ ์šฉ */
body {
font-family: 'Pretendard', 'Noto Sans KR', -apple-system, BlinkMacSystemFont, sans-serif;
background-color: var(--background-color) !important;
color: var(--text-color) !important;
line-height: 1.6;
margin: 0;
padding: 0;
font-size: 16px;
transition: background-color 0.3s ease, color 0.3s ease;
}
/* 5. Gradio ์ปจํ…Œ์ด๋„ˆ ๊ฐ•์ œ ์ ์šฉ */
.gradio-container,
.gradio-container *,
.gr-app,
.gr-app *,
.gr-interface {
background-color: var(--background-color) !important;
color: var(--text-color) !important;
}
/* ํ‘ธํ„ฐ ์ˆจ๊น€ ์„ค์ • ์ถ”๊ฐ€ */
footer {
visibility: hidden;
}
.gradio-container {
width: 100%;
margin: 0 auto;
padding: 20px;
background-color: var(--background-color);
}
/* โ”€โ”€ ๊ทธ๋ฃน ๋ž˜ํผ ๋ฐฐ๊ฒฝ ์™„์ „ ์ œ๊ฑฐ โ”€โ”€ */
.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;
}
/* 6. ์นด๋“œ ๋ฐ ํŒจ๋„ ์Šคํƒ€์ผ */
.custom-frame,
.gr-form,
.gr-box,
.gr-panel,
[class*="frame"],
[class*="card"],
[class*="panel"] {
background-color: var(--card-bg) !important;
border: 1px solid var(--border-color) !important;
border-radius: var(--border-radius);
padding: 20px;
margin: 10px 0;
box-shadow: var(--shadow) !important;
color: var(--text-color) !important;
}
/* ์„น์…˜ ๊ทธ๋ฃน ์Šคํƒ€์ผ - ํšŒ์ƒ‰ ๋ฐฐ๊ฒฝ ์™„์ „ ์ œ๊ฑฐ */
.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: 2px dashed var(--border-color);
transition: all 0.3s ease;
background-color: var(--card-bg);
}
/* ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ์˜์—ญ ๊ฐœ์„  */
.gradio-container .gr-image {
border: 2px dashed var(--border-color) !important;
border-radius: var(--border-radius) !important;
background-color: var(--card-bg) !important;
transition: all 0.3s ease !important;
}
.gradio-container .gr-image:hover {
border-color: var(--primary-color) !important;
box-shadow: 0 4px 12px rgba(251, 127, 13, 0.15) !important;
}
/* ์—…๋กœ๋“œ ์˜์—ญ ๋‚ด๋ถ€ ํ…์ŠคํŠธ */
.gradio-container .gr-image .upload-container,
.gradio-container .gr-image [data-testid="upload-container"] {
background-color: var(--card-bg) !important;
color: var(--text-color) !important;
border: none !important;
}
/* ์—…๋กœ๋“œ ์˜์—ญ ๋“œ๋ž˜๊ทธ ์•ˆ๋‚ด ํ…์ŠคํŠธ */
.gradio-container .gr-image .upload-container p,
.gradio-container .gr-image [data-testid="upload-container"] p {
color: var(--text-color) !important;
font-size: 14px !important;
}
/* ์—…๋กœ๋“œ ๋ฒ„ํŠผ ์Šคํƒ€์ผ ๊ฐœ์„  */
.gradio-container .gr-image .upload-container button,
.gradio-container .gr-image [data-testid="upload-container"] button {
background-color: var(--primary-color) !important;
color: white !important;
border: none !important;
padding: 8px 16px !important;
border-radius: 8px !important;
font-size: 14px !important;
cursor: pointer !important;
transition: all 0.3s ease !important;
}
.gradio-container .gr-image .upload-container button:hover,
.gradio-container .gr-image [data-testid="upload-container"] button:hover {
background-color: var(--secondary-color) !important;
transform: translateY(-1px) !important;
}
/* ์—…๋กœ๋“œ ์˜์—ญ ์•„์ด์ฝ˜ */
.gradio-container .gr-image .upload-container svg,
.gradio-container .gr-image [data-testid="upload-container"] svg {
color: var(--primary-color) !important;
width: 32px !important;
height: 32px !important;
}
/* ์ด๋ฏธ์ง€๊ฐ€ ์—…๋กœ๋“œ๋œ ํ›„ ํ‘œ์‹œ ์˜์—ญ */
.gradio-container .gr-image img {
background-color: var(--card-bg) !important;
border-radius: var(--border-radius) !important;
}
/* ์ด๋ฏธ์ง€ ์ œ๊ฑฐ ๋ฒ„ํŠผ */
.gradio-container .gr-image .image-container button,
.gradio-container .gr-image [data-testid="image"] button {
background-color: rgba(255, 255, 255, 0.9) !important;
color: #333 !important;
border: none !important;
border-radius: 50% !important;
width: 28px !important;
height: 28px !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
cursor: pointer !important;
transition: all 0.3s ease !important;
}
.gradio-container .gr-image .image-container button:hover,
.gradio-container .gr-image [data-testid="image"] button:hover {
background-color: rgba(255, 255, 255, 1) !important;
transform: scale(1.1) !important;
}
/* ์—…๋กœ๋“œ ์ด๋ฏธ์ง€ ์ปจํ…Œ์ด๋„ˆ (600x600) */
.upload-image-container {
width: 600px !important;
height: 600px !important;
min-width: 600px !important;
min-height: 600px !important;
max-width: 600px !important;
max-height: 600px !important;
}
/* ์ถœ๋ ฅ ์ด๋ฏธ์ง€ ์ปจํ…Œ์ด๋„ˆ (700x600) */
.output-image-container {
width: 700px !important;
height: 600px !important;
min-width: 700px !important;
min-height: 600px !important;
max-width: 700px !important;
max-height: 600px !important;
}
.image-container:hover {
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
}
.image-container img {
width: 100% !important;
height: 100% !important;
object-fit: contain !important;
}
/* Gradio ์—…๋กœ๋“œ ์ด๋ฏธ์ง€ ์ปดํฌ๋„ŒํŠธ ํฌ๊ธฐ ๊ณ ์ • (600x600) */
.gradio-container .gr-image.upload-image {
width: 600px !important;
height: 600px !important;
min-width: 600px !important;
min-height: 600px !important;
max-width: 600px !important;
max-height: 600px !important;
}
/* Gradio ์ถœ๋ ฅ ์ด๋ฏธ์ง€ ์ปดํฌ๋„ŒํŠธ ํฌ๊ธฐ ๊ณ ์ • (700x600) */
.gradio-container .gr-image.output-image {
width: 700px !important;
height: 600px !important;
min-width: 700px !important;
min-height: 600px !important;
max-width: 700px !important;
max-height: 600px !important;
}
/* ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ์˜์—ญ ํฌ๊ธฐ ๊ณ ์ • */
.gradio-container .gr-image.upload-image > div {
width: 600px !important;
height: 600px !important;
min-width: 600px !important;
min-height: 600px !important;
max-width: 600px !important;
max-height: 600px !important;
}
/* ์ด๋ฏธ์ง€ ์ถœ๋ ฅ ์˜์—ญ ํฌ๊ธฐ ๊ณ ์ • */
.gradio-container .gr-image.output-image > div {
width: 700px !important;
height: 600px !important;
min-width: 700px !important;
min-height: 600px !important;
max-width: 700px !important;
max-height: 600px !important;
}
/* ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ๋“œ๋ž˜๊ทธ ์˜์—ญ ํฌ๊ธฐ ๊ณ ์ • */
.gradio-container .gr-image.upload-image .image-container,
.gradio-container .gr-image.upload-image [data-testid="image"],
.gradio-container .gr-image.upload-image .upload-container {
width: 600px !important;
height: 600px !important;
min-width: 600px !important;
min-height: 600px !important;
max-width: 600px !important;
max-height: 600px !important;
}
/* ์ด๋ฏธ์ง€ ์ถœ๋ ฅ ๋“œ๋ž˜๊ทธ ์˜์—ญ ํฌ๊ธฐ ๊ณ ์ • */
.gradio-container .gr-image.output-image .image-container,
.gradio-container .gr-image.output-image [data-testid="image"],
.gradio-container .gr-image.output-image .upload-container {
width: 700px !important;
height: 600px !important;
min-width: 700px !important;
min-height: 600px !important;
max-width: 700px !important;
max-height: 600px !important;
}
/* 7. ์ž…๋ ฅ ํ•„๋“œ ์Šคํƒ€์ผ */
.gr-input, .gr-text-input, .gr-sample-inputs,
input[type="text"],
input[type="number"],
input[type="email"],
input[type="password"],
textarea,
select,
.gr-textarea,
.gr-dropdown {
border-radius: var(--border-radius) !important;
border: 1px solid var(--border-color) !important;
padding: 14px !important;
font-size: 15px !important;
background-color: var(--input-bg) !important;
color: var(--text-color) !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,
input[type="text"]:focus,
input[type="number"]:focus,
input[type="email"]:focus,
input[type="password"]:focus,
textarea:focus,
select:focus,
.gr-textarea:focus,
.gr-dropdown:focus {
border-color: var(--primary-color) !important;
outline: none !important;
box-shadow: 0 0 0 2px rgba(251, 127, 13, 0.2) !important;
}
/* 8. ๋ผ๋ฒจ ๋ฐ ํ…์ŠคํŠธ ์š”์†Œ */
.gradio-container label,
label,
.gr-label,
.gr-checkbox label,
.gr-radio label,
p, span, div {
color: var(--text-color) !important;
font-size: 16px !important;
font-weight: 600 !important;
margin-bottom: 8px !important;
}
/* ๋“œ๋กญ๋‹ค์šด ๋ฐ ๋ผ๋””์˜ค ๋ฒ„ํŠผ ํฐํŠธ ํฌ๊ธฐ */
.gr-radio label, .gr-dropdown label, .gr-checkbox label {
font-size: 15px !important;
}
/* ๋ผ๋””์˜ค ๋ฒ„ํŠผ ์„ ํƒ์ง€ ๋ณผ๋“œ ์ฒ˜๋ฆฌ ์ œ๊ฑฐ */
.gr-radio .gr-radio-option label,
.gr-radio input[type="radio"] + label,
.gr-radio .gr-form label {
font-weight: normal !important;
font-size: 15px !important;
}
/* ๋ผ๋””์˜ค ๋ฒ„ํŠผ ๊ทธ๋ฃน ๋‚ด ๋ชจ๋“  ๋ผ๋ฒจ ์ผ๋ฐ˜ ํฐํŠธ๋กœ ์„ค์ • */
.gr-radio fieldset label {
font-weight: normal !important;
}
/* ๋งˆํฌ๋‹ค์šด ํ…์ŠคํŠธ ํฌ๊ธฐ ์ฆ๊ฐ€ */
.gradio-container .gr-markdown {
font-size: 15px !important;
line-height: 1.6 !important;
color: var(--text-color) !important;
}
/* ํ…์ŠคํŠธ๋ฐ•์Šค ๋‚ด์šฉ ํฐํŠธ ํฌ๊ธฐ */
.gr-textbox textarea, .gr-textbox input {
font-size: 15px !important;
background-color: var(--input-bg) !important;
color: var(--text-color) !important;
}
/* ์•„์ฝ”๋””์–ธ ์ œ๋ชฉ ํฐํŠธ ํฌ๊ธฐ */
.gr-accordion summary {
font-size: 17px !important;
font-weight: 600 !important;
background-color: var(--card-bg) !important;
color: var(--text-color) !important;
}
/* ๋ฉ”์ธ ์ปจํ…์ธ  ์Šคํฌ๋กค๋ฐ” */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--card-bg);
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background: var(--primary-color);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--secondary-color);
}
/* ์• ๋‹ˆ๋ฉ”์ด์…˜ ์Šคํƒ€์ผ */
@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: 24px;
font-weight: 700;
color: var(--text-color);
margin-bottom: 15px;
padding-bottom: 8px;
border-bottom: 2px solid var(--primary-color);
font-family: 'Pretendard', 'Noto Sans KR', -apple-system, BlinkMacSystemFont, sans-serif;
}
.section-title img {
margin-right: 12px;
width: 28px;
height: 28px;
/* ๋‹คํฌ๋ชจ๋“œ์—์„œ ์•„์ด์ฝ˜ ํ•„ํ„ฐ ์ ์šฉ */
filter: brightness(0) saturate(100%) invert(27%) sepia(51%) saturate(2878%) hue-rotate(346deg) brightness(104%) contrast(97%);
}
/* ๋ผ์ดํŠธ๋ชจ๋“œ์—์„œ๋Š” ์›๋ž˜ ์•„์ด์ฝ˜ ์ƒ‰์ƒ ์œ ์ง€ */
@media (prefers-color-scheme: light) {
.section-title img {
filter: none;
}
}
/* ์ˆ˜๋™ ๋‹คํฌ๋ชจ๋“œ ํด๋ž˜์Šค์—์„œ๋„ ์•„์ด์ฝ˜ ์ƒ‰์ƒ ์ ์šฉ */
[data-theme="dark"] .section-title img,
.dark .section-title img,
.gr-theme-dark .section-title img {
filter: brightness(0) saturate(100%) invert(27%) sepia(51%) saturate(2878%) hue-rotate(346deg) brightness(104%) contrast(97%);
}
/* 10. ์•„์ฝ”๋””์–ธ ๋ฐ ๋“œ๋กญ๋‹ค์šด - ์ˆ˜๋™์„ค์ • ์˜์—ญ ํšŒ์ƒ‰ ๋ฐฐ๊ฒฝ ์ œ๊ฑฐ */
details,
.gr-accordion,
.gr-accordion details {
background-color: var(--background-color) !important;
border: 1px solid var(--border-color) !important;
color: var(--text-color) !important;
border-radius: var(--border-radius) !important;
margin: 10px 0 !important;
}
details summary,
.gr-accordion summary,
.gr-accordion details summary {
background-color: var(--card-bg) !important;
color: var(--text-color) !important;
padding: 12px 16px !important;
border-radius: var(--border-radius) !important;
cursor: pointer !important;
border: none !important;
font-weight: 600 !important;
transition: all 0.3s ease !important;
}
details summary:hover,
.gr-accordion summary:hover,
.gr-accordion details summary:hover {
background-color: var(--table-hover-bg) !important;
}
/* ์•„์ฝ”๋””์–ธ ๋‚ด๋ถ€ ์ฝ˜ํ…์ธ  */
details[open],
.gr-accordion[open],
.gr-accordion details[open] {
background-color: var(--background-color) !important;
}
details[open] > *:not(summary),
.gr-accordion[open] > *:not(summary),
.gr-accordion details[open] > *:not(summary) {
background-color: var(--background-color) !important;
color: var(--text-color) !important;
padding: 16px !important;
border-top: 1px solid var(--border-color) !important;
}
/* ๊ทธ๋ฃน ๋‚ด๋ถ€ ์Šคํƒ€์ผ - ์ˆ˜๋™์„ค์ • ๋‚ด๋ถ€ ๊ทธ๋ฃน๋“ค */
.gr-group,
details .gr-group,
.gr-accordion .gr-group {
background-color: var(--background-color) !important;
border: none !important;
padding: 12px 0 !important;
margin: 8px 0 !important;
border-radius: var(--border-radius) !important;
}
/* ๊ทธ๋ฃน ๋‚ด๋ถ€ ์ œ๋ชฉ */
.gr-group .gr-markdown h3,
.gr-group h3 {
color: var(--text-color) !important;
font-size: 16px !important;
font-weight: 600 !important;
margin-bottom: 12px !important;
padding-bottom: 6px !important;
border-bottom: 1px solid var(--border-color) !important;
}
/* 11. ์ถ”๊ฐ€ Gradio ์ปดํฌ๋„ŒํŠธ๋“ค */
.gr-block,
.gr-group,
.gr-row,
.gr-column {
background-color: var(--background-color) !important;
color: var(--text-color) !important;
}
/* 12. ๋ฒ„ํŠผ์€ ๊ธฐ์กด ์Šคํƒ€์ผ ์œ ์ง€ (primary-color ์‚ฌ์šฉ) */
button:not([class*="custom"]):not([class*="primary"]):not([class*="secondary"]) {
background-color: var(--card-bg) !important;
color: var(--text-color) !important;
border-color: var(--border-color) !important;
}
/* 14. ์ „ํ™˜ ์• ๋‹ˆ๋ฉ”์ด์…˜ */
* {
transition: background-color 0.3s ease,
color 0.3s ease,
border-color 0.3s ease !important;
}
"""
# 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" />
"""
def create_download_filename(keyword):
"""ํ•œ๊ตญ์‹œ๊ฐ„ ๊ธฐ์ค€์œผ๋กœ ๋‹ค์šด๋กœ๋“œ ํŒŒ์ผ๋ช… ์ƒ์„ฑ"""
from datetime import datetime, timezone, timedelta
# ํ•œ๊ตญ ์‹œ๊ฐ„๋Œ€ ์„ค์ • (UTC+9)
kst = timezone(timedelta(hours=9))
now = datetime.now(kst)
# ํŒŒ์ผ๋ช…์— ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋Š” ๋ฌธ์ž ์ œ๊ฑฐ
import re
safe_keyword = re.sub(r'[<>:"/\\|?*]', '_', keyword) if keyword else "์ƒํ’ˆ"
safe_keyword = safe_keyword[:30] # ๊ธธ์ด ์ œํ•œ 30์ž๋กœ ๋ณ€๊ฒฝ
# YYMMDD_์‹œ๊ฐ„๋ถ„ ํ˜•์‹
time_str = now.strftime("%y%m%d_%H%M")
filename = f"{safe_keyword}_{time_str}.jpg"
return filename
def prepare_download_file(image, keyword):
"""๋‹ค์šด๋กœ๋“œ์šฉ ์ž„์‹œ ํŒŒ์ผ ์ƒ์„ฑ (์˜ฌ๋ฐ”๋ฅธ ํŒŒ์ผ๋ช… ํฌํ•จ)"""
if image is None:
return None
try:
# ํ•œ๊ตญ์‹œ๊ฐ„ ๊ธฐ์ค€ ํŒŒ์ผ๋ช… ์ƒ์„ฑ
filename = create_download_filename(keyword)
# ์ž„์‹œ ๋””๋ ‰ํ† ๋ฆฌ์— ์›ํ•˜๋Š” ํŒŒ์ผ๋ช…์œผ๋กœ ์ €์žฅ
import tempfile
import os
# ์ž„์‹œ ๋””๋ ‰ํ† ๋ฆฌ ์ƒ์„ฑ
temp_dir = tempfile.mkdtemp()
file_path = os.path.join(temp_dir, filename)
# PIL Image๋ฅผ RGB๋กœ ๋ณ€ํ™˜ ํ›„ ์ €์žฅ
if isinstance(image, Image.Image):
# RGBA๋ฅผ RGB๋กœ ๋ณ€ํ™˜
if image.mode == 'RGBA':
background = Image.new('RGB', image.size, (255, 255, 255))
background.paste(image, mask=image.split()[-1])
image_to_save = background
else:
image_to_save = image.convert('RGB')
else:
image_to_save = image
# JPG๋กœ ์ €์žฅ (์›ํ•˜๋Š” ํŒŒ์ผ๋ช…์œผ๋กœ)
image_to_save.save(file_path, 'JPEG', quality=95)
print(f"โœ… ๋‹ค์šด๋กœ๋“œ ํŒŒ์ผ ์ค€๋น„: {filename}")
print(f"๐Ÿ“ ํŒŒ์ผ ๊ฒฝ๋กœ: {file_path}")
return file_path
except Exception as e:
print(f"โŒ ๋‹ค์šด๋กœ๋“œ ํŒŒ์ผ ์ค€๋น„ ์‹คํŒจ: {e}")
return None
def main():
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"]
),
title="UHP ์ด๋ฏธ์ง€์ƒ์„ฑ๊ธฐ"
) as app:
# FontAwesome ๋งํฌ ์ถ”๊ฐ€
gr.HTML(fontawesome_link)
# ์ƒํƒœ ๊ด€๋ฆฌ
copy_suggestions_state = gr.State({})
current_keyword_state = gr.State("")
with gr.Row():
# ์™ผ์ชฝ: 1๋‹จ๊ณ„ AI ์นดํ”ผ ์ƒ์„ฑ
with gr.Column(scale=1):
# AI ์นดํ”ผ ์ƒ์„ฑ ์„น์…˜
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"> 1๋‹จ๊ณ„: AI ์นดํ”ผ ์ƒ์„ฑ</div>')
product_keyword = gr.Textbox(
label="์ƒํ’ˆ ํ‚ค์›Œ๋“œ",
placeholder="์˜ˆ: ๋ฐ”๋‚˜๋ฐ”์žŽ ์ถ”์ถœ๋ฌผ, ํ”„๋ฆฌ๋ฏธ์—„ ํ…€๋ธ”๋Ÿฌ, ์ฒœ์—ฐ ์Šคํ‚จ์ผ€์–ด"
)
copy_type_selection = gr.Radio(
choices=[
"์žฅ์ ์š”์•ฝํ˜•", "๋ฌธ์ œ์ œ์‹œํ˜•", "์‚ฌํšŒ์ ์ฆ๊ฑฐํ˜•", "๊ธด๊ธ‰์„ฑ์œ ๋„ํ˜•",
"๊ฐ€๊ฒฉ๊ฒฝ์Ÿ๋ ฅํ˜•", "๋งค์ธ๋ณ€ํ™”ํ˜•", "์ถฉ๋™๊ตฌ๋งค์œ ๋„ํ˜•", "๊ณตํฌ์†Œ๊ตฌํ˜•"
],
label="๐Ÿ“‹ 1. ์นดํ”ผ ํƒ€์ž… ์„ ํƒ",
value="์žฅ์ ์š”์•ฝํ˜•",
visible=True
)
copy_type_description = gr.Markdown(
"**์žฅ์ ์š”์•ฝํ˜•**: ์ œํ’ˆ์˜ ์žฅ์ ์„ ํ•œ๋ˆˆ์— ๊ฐ•์กฐ - ํ•ต์‹ฌ ๊ธฐ๋Šฅ๊ณผ ํ˜œํƒ์„ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ์š”์•ฝํ•˜์—ฌ ์ œ์‹œ",
visible=True
)
generate_copy_btn = gr.Button("๋ฉ”์ธ์นดํ”ผ ์ƒ์„ฑ", elem_classes="custom-button")
# ์นดํ”ผ ์ถœ๋ ฅ ์˜์—ญ
copy1_display = gr.Textbox(label="์ถ”์ฒœ1", interactive=False, show_label=True)
copy2_display = gr.Textbox(label="์ถ”์ฒœ2", interactive=False, show_label=True)
copy3_display = gr.Textbox(label="์ถ”์ฒœ3", interactive=False, show_label=True)
copy4_display = gr.Textbox(label="์ถ”์ฒœ4", interactive=False, show_label=True)
copy5_display = gr.Textbox(label="์ถ”์ฒœ5", interactive=False, show_label=True)
copy_selection = gr.Radio(
choices=["์ถ”์ฒœ 1", "์ถ”์ฒœ 2", "์ถ”์ฒœ 3", "์ถ”์ฒœ 4", "์ถ”์ฒœ 5"],
label="๐Ÿ“ 2. ์นดํ”ผ ์„ ํƒ",
value="์ถ”์ฒœ 1",
visible=True
)
with gr.Row():
main_text = gr.Textbox(
label="๋ฉ”์ธ์นดํ”ผ",
placeholder="์œ„์—์„œ ์นดํ”ผ๋ฅผ ์„ ํƒํ•˜๋ฉด ์ž๋™์œผ๋กœ ์ž…๋ ฅ๋ฉ๋‹ˆ๋‹ค",
interactive=True
)
sub_text = gr.Textbox(
label="์„œ๋ธŒ์นดํ”ผ",
placeholder="์œ„์—์„œ ์นดํ”ผ๋ฅผ ์„ ํƒํ•˜๋ฉด ์ž๋™์œผ๋กœ ์ž…๋ ฅ๋ฉ๋‹ˆ๋‹ค",
interactive=True
)
# ์˜ˆ์‹œ
gr.Examples(
examples=[
["ํ†ต๊ตฝ์Šฌ๋ฆฌํผ"], ["๋ฐ”๋‚˜๋ฐ”์žŽ ์ถ”์ถœ๋ฌผ"], ["ํ”„๋ฆฌ๋ฏธ์—„ ํ…€๋ธ”๋Ÿฌ"],
["์ฒœ์—ฐ ์Šคํ‚จ์ผ€์–ด"], ["์œ ๊ธฐ๋† ์›๋‘"], ["๋ฌด์„  ์ด์–ดํฐ"]
],
inputs=[product_keyword]
)
# ์˜ค๋ฅธ์ชฝ: 2๋‹จ๊ณ„ ์ด๋ฏธ์ง€์ƒ์„ฑ
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/4297/4297825.png"> 2๋‹จ๊ณ„: ์ด๋ฏธ์ง€์ƒ์„ฑ</div>')
input_image = gr.Image(
type="filepath",
label="์ƒํ’ˆ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ",
elem_classes="image-container upload-image-container"
)
color_mode = gr.Radio(
choices=["์ถ”์ฒœ๋ฐฐ๊ฒฝ", "ํฐ์ƒ‰๋ฐฐ๊ฒฝ", "์ˆ˜๋™ ์„ค์ •๋ฐฐ๊ฒฝ"],
value="์ถ”์ฒœ๋ฐฐ๊ฒฝ",
label="๋ฐฐ๊ฒฝ์ƒ‰ ์„ค์ •"
)
generate_image_btn = gr.Button("์ด๋ฏธ์ง€์ƒ์„ฑ", elem_classes="custom-button")
# ์ˆ˜๋™์„ค์ • ์•„์ฝ”๋””์–ธ
with gr.Accordion("์ˆ˜๋™์„ค์ •", open=False):
with gr.Group():
gr.Markdown("### ํฐํŠธ ์„ ํƒ")
with gr.Row():
main_font_choice = gr.Dropdown(
choices=[
"ํ”„๋ฆฌํ…๋‹ค๋“œ-Bold", "์—์Šค์ฝ”์–ด๋“œ๋ฆผ-Bold", "๋…ธํ† ์‚ฐ์Šค-Bold",
"๋ฐฐ๋‹ฌ์˜๋ฏผ์กฑ ๋„ํ˜„์ฒด", "๋ฐฐ๋‹ฌ์˜๋ฏผ์กฑ ์ฃผ์•„์ฒด", "์—ฌ๊ธฐ์–ด๋•Œ ์ž˜๋‚œ์ฒด",
"์˜จ๊ธ€์žŽ ์ฝ˜์ฝ˜์ฒด", "์˜จ๊ธ€์žŽ ๋ฐ•๋‹คํ˜„์ฒด", "์ด์‚ฌ๋งŒ๋ฃจ-Bold",
"์นดํŽ˜24 ์•„๋„ค๋ชจ๋„ค-Bold", "์–ด๊ทธ๋กœ์ฒด-Bold", "SFํ•จ๋ฐ•๋ˆˆ", "ํŽ˜์ดํผ๋กœ์ง€-Bold"
],
value="ํ”„๋ฆฌํ…๋‹ค๋“œ-Bold",
label="๋ฉ”์ธ ์นดํ”ผ ํฐํŠธ"
)
sub_font_choice = gr.Dropdown(
choices=[
"ํ”„๋ฆฌํ…๋‹ค๋“œ-Regular", "์—์Šค์ฝ”์–ด๋“œ๋ฆผ-Regular", "๋…ธํ† ์‚ฐ์Šค-Regular",
"๋ฐฐ๋‹ฌ์˜๋ฏผ์กฑ ๋„ํ˜„์ฒด", "๋ฐฐ๋‹ฌ์˜๋ฏผ์กฑ ์ฃผ์•„์ฒด", "์—ฌ๊ธฐ์–ด๋•Œ ์ž˜๋‚œ์ฒด",
"์˜จ๊ธ€์žŽ ์ฝ˜์ฝ˜์ฒด", "์˜จ๊ธ€์žŽ ๋ฐ•๋‹คํ˜„์ฒด", "์ด์‚ฌ๋งŒ๋ฃจ-Light",
"์นดํŽ˜24 ์•„๋„ค๋ชจ๋„ค-Regular", "์–ด๊ทธ๋กœ์ฒด-Light", "SFํ•จ๋ฐ•๋ˆˆ", "ํŽ˜์ดํผ๋กœ์ง€-Regular"
],
value="ํ”„๋ฆฌํ…๋‹ค๋“œ-Regular",
label="์„œ๋ธŒ ์นดํ”ผ ํฐํŠธ"
)
with gr.Group():
gr.Markdown("### ์ˆ˜๋™ ์ƒ‰์ƒ ์„ค์ •")
with gr.Row():
manual_bg_color = gr.ColorPicker(label="๋ฐฐ๊ฒฝ์ƒ‰", value="#FFFFFF", interactive=True)
with gr.Row():
manual_main_text_color = gr.ColorPicker(label="๋ฉ”์ธ ํ…์ŠคํŠธ์ƒ‰", value="#000000", interactive=True)
manual_sub_text_color = gr.ColorPicker(label="์„œ๋ธŒ ํ…์ŠคํŠธ์ƒ‰", value="#000000", interactive=True)
with gr.Group():
gr.Markdown("### ์ˆ˜๋™ ํฐํŠธ ํฌ๊ธฐ ์„ค์ •")
with gr.Row():
manual_main_font_size = gr.Slider(
minimum=20, maximum=200, value=100, step=5,
label="๋ฉ”์ธ ํฐํŠธ ํฌ๊ธฐ (px)", interactive=True
)
manual_sub_font_size = gr.Slider(
minimum=15, maximum=120, value=55, step=5,
label="์„œ๋ธŒ ํฐํŠธ ํฌ๊ธฐ (px)", interactive=True
)
with gr.Group():
gr.Markdown("### ๐Ÿ“ ์—ฌ๋ฐฑ ๋ฐ ๊ฐ„๊ฒฉ ์กฐ์ •")
with gr.Row():
top_bottom_margin = gr.Slider(
minimum=50, maximum=800, value=450, step=10,
label="์ƒํ•˜ ์—ฌ๋ฐฑ (px)",
info="์—ฌ๋ฐฑ-์นดํ”ผ-์—ฌ๋ฐฑ์˜ ์—ฌ๋ฐฑ ํฌ๊ธฐ",
interactive=True
)
with gr.Row():
text_gap = gr.Slider(
minimum=5, maximum=100, value=30, step=5,
label="๋ฉ”์ธโ†”์„œ๋ธŒ ๊ฐ„๊ฒฉ (px)",
info="๋ฉ”์ธ์นดํ”ผ์™€ ์„œ๋ธŒ์นดํ”ผ ์‚ฌ์ด ๊ฐ„๊ฒฉ",
interactive=True
)
# ํ˜„์žฌ ์ ์šฉ๋œ ์—ฌ๋ฐฑ ์ •๋ณด ํ‘œ์‹œ
margin_info = gr.Markdown(
"๐Ÿ’ก **ํ˜„์žฌ ์—ฌ๋ฐฑ ์ •๋ณด:** ์ด๋ฏธ์ง€ ์ƒ์„ฑ ํ›„ ์‹ค์ œ ์ ์šฉ๋œ ์ˆ˜์น˜๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.",
visible=True
)
# ์ƒ์„ฑ๋œ ์ด๋ฏธ์ง€ ์„น์…˜
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>')
# ๐ŸŽฏ ํ•ต์‹ฌ: Gradio ๊ธฐ๋ณธ ์ด๋ฏธ์ง€ ์ถœ๋ ฅ (๋‹ค์šด๋กœ๋“œ ๋ฒ„ํŠผ ์ž๋™ ํฌํ•จ)
output_image = gr.Image(
label="์ƒ์„ฑ๋œ ์ด๋ฏธ์ง€",
show_download_button=True, # ๋‹ค์šด๋กœ๋“œ ๋ฒ„ํŠผ ํ‘œ์‹œ
show_share_button=False, # ๊ณต์œ  ๋ฒ„ํŠผ ์ˆจ๊น€
interactive=False,
type="pil", # PIL Image ํƒ€์ž…์œผ๋กœ ์„ค์ •
elem_classes="image-container output-image-container"
)
# ๐ŸŽฏ ํ•ต์‹ฌ: Gradio ๊ธฐ๋ณธ ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ ์ปดํฌ๋„ŒํŠธ
download_file = gr.File(
label="๐Ÿ“ฅ ์ด๋ฏธ์ง€ ๋‹ค์šด๋กœ๋“œ",
visible=True,
interactive=False
)
# ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ๋“ค
def handle_copy_generation(keyword, selected_type):
"""์นดํ”ผ ์ƒ์„ฑ ์ฒ˜๋ฆฌ (๋žœ๋ค API ํ‚ค ์‚ฌ์šฉ)"""
# ๐ŸŽฒ ๋žœ๋ค API ํ‚ค ์„ ํƒ
api_key = get_random_gemini_api_key()
print(f"๐Ÿ”‘ ์นดํ”ผ ์ƒ์„ฑ์šฉ API ํ‚ค: {api_key[:8] if api_key else 'None'}***")
if not keyword.strip():
return ("โš ๏ธ ์ƒํ’ˆ ํ‚ค์›Œ๋“œ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.", {}, "", "", "", "", "", "", "", keyword.strip())
if not api_key:
return ("โš ๏ธ API ํ‚ค๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.", {}, "", "", "", "", "", "", "", keyword.strip())
if not selected_type:
return ("โš ๏ธ ์นดํ”ผ ํƒ€์ž…์„ ๋จผ์ € ์„ ํƒํ•ด์ฃผ์„ธ์š”.", {}, "", "", "", "", "", "", "", keyword.strip())
print(f"๐Ÿš€ ์นดํ”ผ ์ƒ์„ฑ ํ”„๋กœ์„ธ์Šค ์‹œ์ž‘: {keyword} - {selected_type}")
suggestions, analysis = generate_copy_suggestions(keyword, api_key, selected_type)
print(f"๐Ÿ” AI ์‘๋‹ต ํƒ€์ž…: {type(suggestions)}")
print(f"๐Ÿ” AI ์‘๋‹ต ํ‚ค๋“ค: {list(suggestions.keys()) if isinstance(suggestions, dict) else 'Not dict'}")
if "error" not in suggestions:
print("โœ… ์นดํ”ผ ์ƒ์„ฑ ์„ฑ๊ณต!")
# ์„ ํƒ๋œ ํƒ€์ž…์˜ ์นดํ”ผ ๋ฆฌ์ŠคํŠธ ๊ฐ€์ ธ์˜ค๊ธฐ
copy_list = suggestions.get(selected_type, [])
print(f"๐Ÿ“‹ {selected_type} ์นดํ”ผ ๊ฐœ์ˆ˜: {len(copy_list)}")
# ๊ฐ ์นดํ”ผ ์•„์ดํ…œ ๊ฒ€์ฆ
for i, item in enumerate(copy_list):
if isinstance(item, dict):
main = item.get("main", "")
sub = item.get("sub", "")
print(f" ์ถ”์ฒœ{i+1}: ๋ฉ”์ธ='{main}', ์„œ๋ธŒ='{sub}'")
else:
print(f" ์ถ”์ฒœ{i+1}: ์ž˜๋ชป๋œ ํ˜•์‹ {type(item)}")
# ํ‘œ์‹œ์šฉ ์นดํ”ผ ๋ฌธ์ž์—ด ์ƒ์„ฑ (์ถ”์ฒœ1~5 ํ…์ŠคํŠธ๋ฐ•์Šค์šฉ)
copy_values = []
for i in range(5):
if i < len(copy_list) and isinstance(copy_list[i], dict):
main = copy_list[i].get("main", "")
sub = copy_list[i].get("sub", "")
combined = f"{main} / {sub}"
copy_values.append(combined)
print(f"๐Ÿ“ ์ถ”์ฒœ{i+1} ํ‘œ์‹œ: {combined}")
else:
copy_values.append("")
print(f"๐Ÿ“ ์ถ”์ฒœ{i+1} ํ‘œ์‹œ: (๋น„์–ด์žˆ์Œ)")
# ์ฒซ ๋ฒˆ์งธ ์นดํ”ผ๋ฅผ ๋ฉ”์ธ/์„œ๋ธŒ ํ…์ŠคํŠธ๋ฐ•์Šค์— ์ž๋™ ์ž…๋ ฅ
first_main = ""
first_sub = ""
if copy_list and isinstance(copy_list[0], dict):
first_main = copy_list[0].get("main", "")
first_sub = copy_list[0].get("sub", "")
print(f"๐ŸŽฏ ์ž๋™ ์„ ํƒ: ๋ฉ”์ธ='{first_main}', ์„œ๋ธŒ='{first_sub}'")
return (f"โœ… {selected_type} ์นดํ”ผ ์ƒ์„ฑ ์™„๋ฃŒ!", suggestions, *copy_values, first_main, first_sub, keyword.strip())
else:
print(f"โŒ ์นดํ”ผ ์ƒ์„ฑ ์‹คํŒจ: {suggestions['error']}")
error_msg = f"โŒ ์˜ค๋ฅ˜: {suggestions['error']}"
return (error_msg, {}, "", "", "", "", "", "", "", keyword.strip())
def update_copy_type_description(selected_type):
"""์นดํ”ผ ํƒ€์ž… ์„ ํƒ์‹œ ์„ค๋ช… ์—…๋ฐ์ดํŠธ"""
descriptions = {
"์žฅ์ ์š”์•ฝํ˜•": "**์žฅ์ ์š”์•ฝํ˜•**: ์ œํ’ˆ์˜ ์žฅ์ ์„ ํ•œ๋ˆˆ์— ๊ฐ•์กฐ - ํ•ต์‹ฌ ๊ธฐ๋Šฅ๊ณผ ํ˜œํƒ์„ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ์š”์•ฝํ•˜์—ฌ ์ œ์‹œ",
"๋ฌธ์ œ์ œ์‹œํ˜•": "**๋ฌธ์ œ์ œ์‹œํ˜•**: ๋ฌธ์ œ๋ฅผ ์ œ์‹œ ํ›„ ํ•ด๊ฒฐ์ฑ… ์ œ์•ˆ - ๊ณ ๊ฐ์˜ ๋ถˆํŽธํ•จ์„ ๋จผ์ € ์–ธ๊ธ‰ํ•˜๊ณ  ํ•ด๊ฒฐ๋ฐฉ์•ˆ ์ œ์‹œ",
"์‚ฌํšŒ์ ์ฆ๊ฑฐํ˜•": "**์‚ฌํšŒ์ ์ฆ๊ฑฐํ˜•**: ์‹ ๋ขฐ์™€ ์ธ๊ธฐ๋ฅผ ๊ฐ•์กฐ - ํƒ€์ธ์˜ ์‚ฌ์šฉํ›„๊ธฐ์™€ ๊ฒ€์ฆ๋œ ์‹ค์ ์œผ๋กœ ์‹ ๋ขฐ์„ฑ ์–ดํ•„",
"๊ธด๊ธ‰์„ฑ์œ ๋„ํ˜•": "**๊ธด๊ธ‰์„ฑ์œ ๋„ํ˜•**: ์ฆ‰์‹œ ๊ตฌ๋งค๋ฅผ ์œ ๋„ - ํ•œ์ •์ˆ˜๋Ÿ‰, ์‹œ๊ฐ„์ œํ•œ ๋“ฑ์œผ๋กœ ๊ธด๊ธ‰๊ฐ ์กฐ์„ฑ",
"๊ฐ€๊ฒฉ๊ฒฝ์Ÿ๋ ฅํ˜•": "**๊ฐ€๊ฒฉ๊ฒฝ์Ÿ๋ ฅํ˜•**: ํ•ฉ๋ฆฌ์  ๊ฐ€๊ฒฉ์„ ๊ฐ•์กฐํ•ด ์„ฑ์ทจ - ๊ฐ€์„ฑ๋น„, ํ• ์ธํ˜œํƒ ๋“ฑ ๊ฒฝ์ œ์  ์ด๋“ ๊ฐ•์กฐ",
"๋งค์ธ๋ณ€ํ™”ํ˜•": "**๋งค์ธ๋ณ€ํ™”ํ˜•**: ๊ฐ•ํ•œ ๋น„๊ต๋ฅผ ์‹œ์„  ์ง‘์ค‘ - ์‚ฌ์šฉ ์ „ํ›„ ๋ณ€ํ™”๋‚˜ ๊ทน์ ์ธ ๊ฐœ์„  ํšจ๊ณผ ๋ถ€๊ฐ",
"์ถฉ๋™๊ตฌ๋งค์œ ๋„ํ˜•": "**์ถฉ๋™๊ตฌ๋งค์œ ๋„ํ˜•**: ํŠน์ • ๊ตฌ๋งค์š•๊ตฌ๋ฅผ ์ž๊ทน - ํŠน๋ณ„ํ•จ๊ณผ ํ”„๋ฆฌ๋ฏธ์—„ ๊ฐ€์น˜๋กœ ์†Œ์œ ์š• ์ž๊ทน",
"๊ณตํฌ์†Œ๊ตฌํ˜•": "**๊ณตํฌ์†Œ๊ตฌํ˜•**: ๋ถˆ์•ˆ ์œ„ํ—˜์„ ๊ฐ•์กฐํ•ด ๊ฐ์ • ์œ ๋ฐœ - ๋†“์น˜๋ฉด ํ›„ํšŒํ•  ๊ธฐํšŒ๋‚˜ ์œ„ํ—˜์„ฑ์„ ๊ฒฝ๊ณ "
}
if selected_type and selected_type in descriptions:
return descriptions[selected_type]
else:
return "### ์นดํ”ผ ํƒ€์ž…์„ ์„ ํƒํ•˜๋ฉด ์„ค๋ช…์ด ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค."
def handle_copy_selection(suggestions, selected_type, selected_copy):
"""์นดํ”ผ ์„ ํƒ์‹œ ๋ฉ”์ธ/์„œ๋ธŒ ํ…์ŠคํŠธ๋ฐ•์Šค ์—…๋ฐ์ดํŠธ (๊ฐœ์„ ๋œ ๋ฒ„์ „)"""
print(f"๐Ÿ”˜ ๋ผ๋””์˜ค ์„ ํƒ: {selected_copy}, ํƒ€์ž…: {selected_type}")
print(f"๐Ÿ” suggestions ์ƒํƒœ: {type(suggestions)}, ํ‚ค: {list(suggestions.keys()) if suggestions else 'None'}")
if not suggestions or "error" in suggestions:
print("โŒ suggestions ๋ฐ์ดํ„ฐ๊ฐ€ ์—†๊ฑฐ๋‚˜ ์—๋Ÿฌ ์ƒํƒœ")
return "", ""
if not selected_type or not selected_copy:
print("โŒ ํƒ€์ž…์ด๋‚˜ ์„ ํƒ๋œ ์นดํ”ผ๊ฐ€ ์—†์Œ")
return "", ""
if selected_type not in suggestions:
print(f"โŒ {selected_type} ํƒ€์ž…์„ suggestions์—์„œ ์ฐพ์„ ์ˆ˜ ์—†์Œ")
print(f"๐Ÿ“‹ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ํƒ€์ž…๋“ค: {list(suggestions.keys())}")
return "", ""
copy_list = suggestions[selected_type]
if not isinstance(copy_list, list):
print(f"โŒ {selected_type} ๋ฐ์ดํ„ฐ๊ฐ€ ๋ฆฌ์ŠคํŠธ๊ฐ€ ์•„๋‹˜: {type(copy_list)}")
return "", ""
print(f"๐Ÿ“ {selected_type} ์นดํ”ผ ๋ฆฌ์ŠคํŠธ: {len(copy_list)}๊ฐœ")
# ๋ผ๋””์˜ค ๋ฒ„ํŠผ ์„ ํƒ๊ฐ’์—์„œ ๋ฒˆํ˜ธ ์ถ”์ถœ (๋” ์•ˆ์ „ํ•œ ๋ฐฉ๋ฒ•)
try:
if "์ถ”์ฒœ 1" in selected_copy:
option_number = 0
elif "์ถ”์ฒœ 2" in selected_copy:
option_number = 1
elif "์ถ”์ฒœ 3" in selected_copy:
option_number = 2
elif "์ถ”์ฒœ 4" in selected_copy:
option_number = 3
elif "์ถ”์ฒœ 5" in selected_copy:
option_number = 4
else:
# ๋ฐฑ์—… ๋ฐฉ๋ฒ•: ์ˆซ์ž ์ถ”์ถœ
import re
numbers = re.findall(r'\d+', selected_copy)
option_number = int(numbers[0]) - 1 if numbers else 0
print(f"๐Ÿ”ข ์ถ”์ถœ๋œ ์˜ต์…˜ ๋ฒˆํ˜ธ: {option_number}")
except Exception as e:
print(f"โŒ ์˜ต์…˜ ๋ฒˆํ˜ธ ์ถ”์ถœ ์‹คํŒจ: {e}, ๊ธฐ๋ณธ๊ฐ’ 0 ์‚ฌ์šฉ")
option_number = 0
# ๋ฒ”์œ„ ์ฒดํฌ
if 0 <= option_number < len(copy_list):
selected_copy_item = copy_list[option_number]
if not isinstance(selected_copy_item, dict):
print(f"โŒ ์„ ํƒ๋œ ์นดํ”ผ ์•„์ดํ…œ์ด ๋”•์…”๋„ˆ๋ฆฌ๊ฐ€ ์•„๋‹˜: {type(selected_copy_item)}")
return "", ""
main_text = selected_copy_item.get("main", "")
sub_text = selected_copy_item.get("sub", "")
print(f"โœ… ์„ ํƒ๋œ ์นดํ”ผ - ๋ฉ”์ธ: '{main_text}', ์„œ๋ธŒ: '{sub_text}'")
# ๋นˆ ๋ฌธ์ž์—ด ์ฒดํฌ
if not main_text and not sub_text:
print("โš ๏ธ ๋ฉ”์ธ์นดํ”ผ์™€ ์„œ๋ธŒ์นดํ”ผ๊ฐ€ ๋ชจ๋‘ ๋น„์–ด์žˆ์Œ")
return main_text, sub_text
else:
print(f"โŒ ์ž˜๋ชป๋œ ์˜ต์…˜ ๋ฒˆํ˜ธ: {option_number} (๋ฒ”์œ„: 0~{len(copy_list)-1})")
return "", ""
def handle_image_generation(input_image, main_text, sub_text, color_mode,
main_font_choice, sub_font_choice, manual_bg_color, manual_main_text_color,
manual_sub_text_color, manual_main_font_size, manual_sub_font_size,
top_bottom_margin, text_gap, current_keyword):
"""์ด๋ฏธ์ง€ ์ƒ์„ฑ ์ฒ˜๋ฆฌ (์—ฌ๋ฐฑ ์กฐ์ • ๊ธฐ๋Šฅ ํฌํ•จ)"""
# ๐ŸŽฒ ๋žœ๋ค API ํ‚ค ์„ ํƒ
api_key = get_random_gemini_api_key()
print(f"๐Ÿ”‘ ์ด๋ฏธ์ง€ ์ƒ์„ฑ์šฉ API ํ‚ค: {api_key[:8] if api_key else 'None'}***")
# ๐ŸŽฏ NEW: ์—ฌ๋ฐฑ ์„ค์ •์„ create_uhp_image ํ•จ์ˆ˜์— ์ „๋‹ฌ
image_result, color_mode_result, new_bg_color, new_main_text_color, new_sub_text_color, new_main_font_size, new_sub_font_size, applied_margin_info = create_uhp_image(
input_image, main_text, sub_text, color_mode,
main_font_choice, sub_font_choice, manual_bg_color, manual_main_text_color, manual_sub_text_color,
manual_main_font_size, manual_sub_font_size, api_key,
top_bottom_margin, text_gap # ์ƒˆ๋กœ์šด ์—ฌ๋ฐฑ ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€
)
# ๐ŸŽฏ ํ•ต์‹ฌ: ๋‹ค์šด๋กœ๋“œ ํŒŒ์ผ ์ค€๋น„
download_file_path = None
if image_result is not None:
download_file_path = prepare_download_file(image_result, current_keyword)
# ๐ŸŽฏ NEW: ์ ์šฉ๋œ ์—ฌ๋ฐฑ ์ •๋ณด ์ƒ์„ฑ
margin_info_text = f"""๐Ÿ“ **์ ์šฉ๋œ ์—ฌ๋ฐฑ ์ •๋ณด:**
โ€ข ์ƒํ•˜ ์—ฌ๋ฐฑ: {applied_margin_info.get('top_bottom_margin', 0)}px
โ€ข ๋ฉ”์ธโ†”์„œ๋ธŒ ๊ฐ„๊ฒฉ: {applied_margin_info.get('text_gap', 0)}px
โ€ข ์ด๋ฏธ์ง€ ํฌ๊ธฐ: {applied_margin_info.get('canvas_width', 0)} ร— {applied_margin_info.get('canvas_height', 0)}px
โ€ข ์›๋ณธ ํฌ๊ธฐ: {applied_margin_info.get('original_width', 0)} ร— {applied_margin_info.get('original_height', 0)}px"""
return (image_result, color_mode_result, new_bg_color, new_main_text_color, new_sub_text_color,
new_main_font_size, new_sub_font_size, download_file_path, margin_info_text)
# ์ด๋ฒคํŠธ ์—ฐ๊ฒฐ
copy_type_selection.change(
fn=update_copy_type_description,
inputs=[copy_type_selection],
outputs=[copy_type_description]
)
generate_copy_btn.click(
fn=handle_copy_generation,
inputs=[product_keyword, copy_type_selection],
outputs=[copy_type_description, copy_suggestions_state,
copy1_display, copy2_display, copy3_display, copy4_display, copy5_display,
main_text, sub_text, current_keyword_state]
)
copy_selection.change(
fn=handle_copy_selection,
inputs=[copy_suggestions_state, copy_type_selection, copy_selection],
outputs=[main_text, sub_text]
)
generate_image_btn.click(
fn=handle_image_generation,
inputs=[input_image, main_text, sub_text, color_mode,
main_font_choice, sub_font_choice, manual_bg_color, manual_main_text_color, manual_sub_text_color,
manual_main_font_size, manual_sub_font_size, top_bottom_margin, text_gap, current_keyword_state],
outputs=[output_image, color_mode, manual_bg_color, manual_main_text_color, manual_sub_text_color,
manual_main_font_size, manual_sub_font_size, download_file, margin_info]
)
return app
if __name__ == "__main__":
app = main()
app.launch(share=False, inbrowser=True)