#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 = """ """ 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('
1๋‹จ๊ณ„: AI ์นดํ”ผ ์ƒ์„ฑ
') 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('
2๋‹จ๊ณ„: ์ด๋ฏธ์ง€์ƒ์„ฑ
') 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('
์ƒ์„ฑ๋œ ์ด๋ฏธ์ง€
') # ๐ŸŽฏ ํ•ต์‹ฌ: 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)