G378DFE4AS / app.py
ssboost's picture
Update app.py
606f0ee verified
Raw History Blame Contribute Delete
28.9 kB
import os
import gradio as gr
import time
import logging
import random
import uuid
from datetime import datetime
from langdetect import detect, DetectorFactory
import google.generativeai as genai
from dotenv import load_dotenv
DetectorFactory.seed = 0
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋กœ๋“œ
load_dotenv()
# ํ™˜๊ฒฝ๋ณ€์ˆ˜์—์„œ API ํ‚ค ์„ค์ • ๋กœ๋“œ
def get_gemini_api_configs():
"""ํ™˜๊ฒฝ๋ณ€์ˆ˜์—์„œ Gemini API ํ‚ค ์„ค์ •์„ ๋กœ๋“œ"""
api_configs_str = os.getenv('GEMINI_API_CONFIGS', '')
if not api_configs_str:
logger.error("GEMINI_API_CONFIGS ํ™˜๊ฒฝ๋ณ€์ˆ˜๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.")
return []
try:
# ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๊ฐ’์„ exec๋กœ ์‹คํ–‰ํ•˜์—ฌ ์„ค์ • ๋กœ๋“œ
local_vars = {}
exec(api_configs_str, {}, local_vars)
return local_vars.get('API_KEYS_LIST', [])
except Exception as e:
logger.error(f"ํ™˜๊ฒฝ๋ณ€์ˆ˜ ํŒŒ์‹ฑ ์˜ค๋ฅ˜: {e}")
return []
# API ํ‚ค ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ ์ „์—ญ ๋ณ€์ˆ˜
api_key_manager = {
'keys': [],
'current_index': -1,
'failed_keys': set(), # ์‹คํŒจํ•œ ํ‚ค๋“ค์„ ์ถ”์ 
'is_initialized': False
}
# API ํ‚ค ๋กœ๋“œ ํ•จ์ˆ˜ (๊ฐœ์„ ๋œ ๋ฒ„์ „)
def load_api_keys():
"""ํ™˜๊ฒฝ๋ณ€์ˆ˜์—์„œ API ํ‚ค๋ฅผ ๋กœ๋“œํ•ฉ๋‹ˆ๋‹ค."""
# ํ™˜๊ฒฝ๋ณ€์ˆ˜์—์„œ API ํ‚ค ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ
api_keys_from_env = get_gemini_api_configs()
# ๋นˆ ํ‚ค๋‚˜ ํ”Œ๋ ˆ์ด์Šคํ™€๋” ์ œ๊ฑฐ
api_keys = [
key.strip() for key in api_keys_from_env
if key and key.strip() and not key.startswith("YOUR_") and not key.startswith("your_")
]
# ์ค‘๋ณต ์ œ๊ฑฐ
api_keys = list(dict.fromkeys(api_keys))
if not api_keys:
logger.error("API ํ‚ค๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. GEMINI_API_CONFIGS ํ™˜๊ฒฝ๋ณ€์ˆ˜์— ์‹ค์ œ API ํ‚ค๋ฅผ ์ถ”๊ฐ€ํ•˜์„ธ์š”.")
raise ValueError("API ํ‚ค๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. GEMINI_API_CONFIGS ํ™˜๊ฒฝ๋ณ€์ˆ˜์— ์‹ค์ œ API ํ‚ค๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ์„ธ์š”.")
logger.info(f"์ด {len(api_keys)}๊ฐœ์˜ API ํ‚ค๊ฐ€ ๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.")
return api_keys
def initialize_api_keys():
"""API ํ‚ค ๊ด€๋ฆฌ์ž๋ฅผ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค."""
global api_key_manager
if api_key_manager['is_initialized']:
return
try:
api_key_manager['keys'] = load_api_keys()
api_key_manager['is_initialized'] = True
logger.info("API ํ‚ค ๊ด€๋ฆฌ์ž ์ดˆ๊ธฐํ™” ์™„๋ฃŒ")
except Exception as e:
logger.error(f"API ํ‚ค ์ดˆ๊ธฐํ™” ์‹คํŒจ: {str(e)}")
raise e
def get_next_api_key():
"""๋‹ค์Œ ์‚ฌ์šฉํ•  API ํ‚ค๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์‹คํŒจํ•œ ํ‚ค๋Š” ๊ฑด๋„ˆ๋œ๋‹ˆ๋‹ค."""
global api_key_manager
# ์ดˆ๊ธฐํ™” ํ™•์ธ
if not api_key_manager['is_initialized']:
initialize_api_keys()
available_keys = [
key for i, key in enumerate(api_key_manager['keys'])
if i not in api_key_manager['failed_keys']
]
if not available_keys:
# ๋ชจ๋“  ํ‚ค๊ฐ€ ์‹คํŒจํ–ˆ์œผ๋ฉด ์‹คํŒจ ๋ชฉ๋ก์„ ์ดˆ๊ธฐํ™”ํ•˜๊ณ  ๋‹ค์‹œ ์‹œ๋„
logger.warning("๋ชจ๋“  API ํ‚ค๊ฐ€ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ์‹คํŒจ ๋ชฉ๋ก์„ ์ดˆ๊ธฐํ™”ํ•˜๊ณ  ๋‹ค์‹œ ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค.")
api_key_manager['failed_keys'].clear()
available_keys = api_key_manager['keys']
# ์ฒซ ๋ฒˆ์งธ ์‚ฌ์šฉ์€ ๋žœ๋ค์œผ๋กœ ์„ ํƒ
if api_key_manager['current_index'] == -1:
available_indices = [
i for i, key in enumerate(api_key_manager['keys'])
if i not in api_key_manager['failed_keys']
]
api_key_manager['current_index'] = random.choice(available_indices)
logger.info(f"์ฒซ ๋ฒˆ์งธ API ํ‚ค ์„ ํƒ: ๋žœ๋ค ์ธ๋ฑ์Šค {api_key_manager['current_index'] + 1}")
else:
# ์ดํ›„ ์‚ฌ์šฉ์€ ์ˆœ์ฐจ์ ์œผ๋กœ ๋‹ค์Œ ํ‚ค ์„ ํƒ (์‹คํŒจํ•œ ํ‚ค๋Š” ๊ฑด๋„ˆ๋œ€)
original_index = api_key_manager['current_index']
for _ in range(len(api_key_manager['keys'])):
api_key_manager['current_index'] = (api_key_manager['current_index'] + 1) % len(api_key_manager['keys'])
if api_key_manager['current_index'] not in api_key_manager['failed_keys']:
break
if api_key_manager['current_index'] in api_key_manager['failed_keys']:
# ๋ชจ๋“  ํ‚ค๋ฅผ ์‹œ๋„ํ–ˆ์ง€๋งŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ํ‚ค๊ฐ€ ์—†์Œ
logger.warning("์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ API ํ‚ค๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ์‹คํŒจ ๋ชฉ๋ก์„ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค.")
api_key_manager['failed_keys'].clear()
api_key_manager['current_index'] = 0
logger.info(f"๋‹ค์Œ API ํ‚ค ์„ ํƒ: ์ธ๋ฑ์Šค {api_key_manager['current_index'] + 1}")
return api_key_manager['keys'][api_key_manager['current_index']]
def mark_api_key_failed(api_key):
"""API ํ‚ค๋ฅผ ์‹คํŒจ ๋ชฉ๋ก์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค."""
global api_key_manager
try:
key_index = api_key_manager['keys'].index(api_key)
api_key_manager['failed_keys'].add(key_index)
logger.warning(f"API ํ‚ค ์ธ๋ฑ์Šค {key_index + 1}๋ฅผ ์‹คํŒจ ๋ชฉ๋ก์— ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.")
except ValueError:
logger.error("์‹คํŒจํ•œ API ํ‚ค๋ฅผ ๋ชฉ๋ก์—์„œ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
def test_api_key(api_key):
"""API ํ‚ค๊ฐ€ ์œ ํšจํ•œ์ง€ ํ…Œ์ŠคํŠธํ•ฉ๋‹ˆ๋‹ค."""
try:
genai.configure(api_key=api_key)
model = genai.GenerativeModel(model_name="gemini-2.0-flash")
# ๊ฐ„๋‹จํ•œ ํ…Œ์ŠคํŠธ ์š”์ฒญ
response = model.generate_content("Test", generation_config={
"max_output_tokens": 10,
"temperature": 0.1,
})
if response and response.text:
return True
return False
except Exception as e:
logger.error(f"API ํ‚ค ํ…Œ์ŠคํŠธ ์‹คํŒจ: {str(e)}")
return False
def get_working_api_key():
"""์ž‘๋™ํ•˜๋Š” API ํ‚ค๋ฅผ ์ฐพ์•„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค."""
max_attempts = len(api_key_manager['keys']) if api_key_manager['is_initialized'] else 5
for attempt in range(max_attempts):
try:
api_key = get_next_api_key()
if test_api_key(api_key):
logger.info(f"์ž‘๋™ํ•˜๋Š” API ํ‚ค๋ฅผ ์ฐพ์•˜์Šต๋‹ˆ๋‹ค. (์‹œ๋„ {attempt + 1}ํšŒ)")
return api_key
else:
mark_api_key_failed(api_key)
logger.warning(f"API ํ‚ค ํ…Œ์ŠคํŠธ ์‹คํŒจ. ๋‹ค์Œ ํ‚ค๋กœ ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค. (์‹œ๋„ {attempt + 1}ํšŒ)")
except Exception as e:
logger.error(f"API ํ‚ค ๊ฐ€์ ธ์˜ค๊ธฐ ์‹คํŒจ: {str(e)}")
raise Exception("์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ API ํ‚ค๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
# ํ•œ๊ตญ์–ด ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์กฐ๊ฑด (ํ•œ-์˜ ๋ฒˆ์—ญ์šฉ)
KO_EN_CONDITIONS = """
์ถ”๊ฐ€ ์กฐ๊ฑด (ํ•œ๊ตญ์–ด ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ํ•˜๊ธฐ):
- ๋ฒˆ์—ญ์ฒด๊ฐ€ ์•„๋‹Œ ์ž์—ฐ์Šค๋Ÿฝ๊ณ  ๋งค๋„๋Ÿฌ์šด ํ•œ๊ตญ์–ด๋ฅผ ์ตœ์šฐ์„ ์ ์œผ๋กœ ์ž‘์„ฑ
- ์•„๋ž˜ ์กฐ๊ฑด์„ ๋”ฐ๋ผ์ฃผ์„ธ์š”:
1. ๋ฌธ๋ฒ•์  ์ •ํ™•์„ฑ
- ํ”ผ๋™๋ฌธ๋ณด๋‹ค ๋Šฅ๋™๋ฌธ ์šฐ์„ 
- ๋Œ€๋ช…์‚ฌ ์ตœ์†Œํ™”
- ๋ช…์‚ฌํ˜•๋ณด๋‹ค ๋™์‚ฌ/ํ˜•์šฉ์‚ฌ ์šฐ์„ 
- ํ˜„์žฌ์ง„ํ–‰ํ˜•๋ณด๋‹ค ๋‹จ์ˆœํ˜„์žฌ๋‚˜ ์™„๋ฃŒํ˜• ์šฐ์„ 
- ์ฃผ์–ด-๋ชฉ์ ์–ด-์„œ์ˆ ์–ด ์ˆœ์„œ ์œ ์ง€
- "~์—ˆ์–ด์š”" ํ‘œํ˜„ ์ง€์–‘
2. ์ฃผ์ œ ๋ฌธ๋งฅ์— ๋งž๋Š” ์ž์—ฐ์Šค๋Ÿฌ์šด ํ๋ฆ„
3. ์ฃผ์ œ์™€ ์ƒํ™ฉ์— ์–ด์šธ๋ฆฌ๋Š” ์–ดํœ˜ ์„ ํƒ
4. ๋ฌธํ™”์  ์ ํ•ฉ์„ฑ ๊ณ ๋ ค
5. ์ •์„œ์  ๋‰˜์•™์Šค ๋ฐ˜์˜ (๊ณต๊ฐ ์ด๋„๋Š” ํ‘œํ˜„)
6. ์ง์—ญ/์˜์—ญ ๊ท ํ˜• ์œ ์ง€
7. ์ฃผ์ œ ํ‚ค์›Œ๋“œ ๋ฐ˜๋ณต ์–ธ๊ธ‰ ๊ธˆ์ง€
8. AI๊ฐ€ ์“ด ๊ธ€์ฒ˜๋Ÿผ ๋ณด์ด์ง€ ์•Š๋„๋ก ์ฃผ์˜
"""
# ์‹œ์Šคํ…œ ๋ฉ”์‹œ์ง€ (ํ•œ-์ค‘ ๋ฒˆ์—ญ์šฉ)
KO_ZH_SYSTEM_MESSAGE = """๋‹น์‹ ์€ ํ•œ๊ตญ์–ด-์ค‘๊ตญ์–ด ์–‘๋ฐฉํ–ฅ ๋ฒˆ์—ญ ์–ด์‹œ์Šคํ„ดํŠธ์ž…๋‹ˆ๋‹ค.
์•„๋ž˜ ์กฐ๊ฑด์„ ์ถฉ์‹คํžˆ ๋”ฐ๋ฅด์„ธ์š”.
[๋ชฉ์  ๋ฐ ์ƒํ™ฉ]
- ์‚ฌ์šฉ์ž ๋ชฉ์ : 1688 ์†Œ์‹ฑ, ๋„๋งค, ์œ ํ†ต ๊ด€๋ จ๋œ ์ƒํ™ฉ์—์„œ ์–ธ์–ด ์žฅ๋ฒฝ ์—†์ด ์›ํ™œํ•œ ์†Œํ†ต์„ ๋•๋Š”๋‹ค.
- ์‚ฌ์šฉ์ž ์ž…๋ ฅ์ด ํ•œ๊ตญ์–ด์ผ ๊ฒฝ์šฐ ์ž์—ฐ์Šค๋Ÿฝ๊ณ  ๋งฅ๋ฝ์— ๋งž๊ฒŒ ์ค‘๊ตญ์–ด๋กœ ๋ฒˆ์—ญํ•œ๋‹ค.
- ์‚ฌ์šฉ์ž ์ž…๋ ฅ์ด ์ค‘๊ตญ์–ด์ผ ๊ฒฝ์šฐ ์ž์—ฐ์Šค๋Ÿฝ๊ณ  ๋งฅ๋ฝ์— ๋งž๊ฒŒ ํ•œ๊ตญ์–ด๋กœ ๋ฒˆ์—ญํ•œ๋‹ค.
[ํ•œ๊ตญ์–ด ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์กฐ๊ฑด์ •๋ฆฌ]
1. ๋ฌธ๋ฒ•์  ์ •ํ™•์„ฑ
- ํ”ผ๋™๋ฌธ ๋Œ€์‹  ๋Šฅ๋™๋ฌธ
- ๋Œ€๋ช…์‚ฌ ์ตœ์†Œํ™”
- ๋ช…์‚ฌํ˜•๋ณด๋‹ค ๋™์‚ฌ ๋ฐ ํ˜•์šฉ์‚ฌ ์šฐ์„ 
- ๋‹จ์ˆœํ˜„์žฌํ˜•์ด๋‚˜ ์™„๋ฃŒํ˜• ์šฐ์„ 
- ๋ฌธ์žฅ ๊ตฌ์กฐ๋Š” ์ฃผ์–ด-๋ชฉ์ ์–ด-๋™์‚ฌ
- "~์—ˆ์–ด์š”" ํ˜•ํƒœ ์‚ฌ์šฉ ์ง€์–‘
2. ์ฃผ์ œ ๋งฅ๋ฝ์— ๋งž๋Š” ์ž์—ฐ์Šค๋Ÿฌ์šด ํ‘œํ˜„
3. ์ ์ ˆํ•œ ์–ดํœ˜ ์„ ํƒ(์†Œ์‹ฑ, ๋„๋งค, ์œ ํ†ต ์ƒํ™ฉ ๊ณ ๋ ค)
4. ๋ฌธํ™”์ , ์ƒํ™ฉ์  ์ ํ•ฉ์„ฑ
5. ์ •์„œ์  ๋‰˜์•™์Šค ์กฐํ™”
6. ์ง์—ญ๊ณผ ์˜์—ญ์˜ ๊ท ํ˜•
7. ์ฃผ์ œ ํ‚ค์›Œ๋“œ ๋ฐ˜๋ณต ๊ธˆ์ง€
8. ์ƒ์„ฑ AI ํ‹ฐ ๋‚˜์ง€ ์•Š๋„๋ก ์ฃผ์˜
[์ถ”๊ฐ€์‚ฌํ•ญ]
- ๋ฒˆ์—ญ ์‹œ ์–ด์ƒ‰ํ•œ ๋ฒˆ์—ญ์ฒด๋ฅผ ํ”ผํ•˜๊ณ  ์ž์—ฐ์Šค๋Ÿฌ์šด ํ‘œํ˜„์„ ์‚ฌ์šฉํ•œ๋‹ค.
- ์ถœ๋ ฅ์€ ๋ฒˆ์—ญ๋œ ๊ฒฐ๊ณผ๋ฌธ๋งŒ ์ œ์‹œํ•œ๋‹ค.
"""
# 1์ฐจ ์นดํ…Œ๊ณ ๋ฆฌ ๋ฐ 2์ฐจ ์งˆ๋ฌธ ๋ชฉ๋ก ์ •์˜
categories = {
"๊ธฐ๋ณธ ์ธ์‚ฌ ๋ฐ ๊ฑฐ๋ž˜ ์‹œ์ž‘": [
"์•ˆ๋…•ํ•˜์„ธ์š”, ๊ท€์‚ฌ์˜ ์ œํ’ˆ์ด ๋งˆ์Œ์— ๋“ญ๋‹ˆ๋‹ค. ์ž์„ธํžˆ ์„ค๋ช… ๋ถ€ํƒ๋“œ๋ฆฝ๋‹ˆ๋‹ค.",
"์ œํ’ˆ์ด ํ˜„์žฌ ์žฌ๊ณ ๊ฐ€ ์žˆ๋‚˜์š”?",
"๋„๋งค ๊ฑฐ๋ž˜๋ฅผ ํ•˜๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค. ๊ฐ€๋Šฅํ•œ๊ฐ€์š”?",
"์ƒˆ๋กœ์šด ๊ณ ๊ฐ์ธ๋ฐ ๊ฑฐ๋ž˜๋ฅผ ์‹œ์ž‘ํ•  ์ˆ˜ ์žˆ์„๊นŒ์š”?",
"ํ˜น์‹œ ํ•œ๊ตญ ๊ณ ๊ฐ๋“ค๊ณผ๋„ ๊ฑฐ๋ž˜๋ฅผ ํ•˜๊ณ  ๊ณ„์‹ ๊ฐ€์š”?",
"์ œํ’ˆ์— ๋Œ€ํ•œ ์ž์„ธํ•œ ์„ค๋ช…์„œ๋‚˜ ๋ธŒ๋กœ์Šˆ์–ด๊ฐ€ ์žˆ๋‚˜์š”?",
"์ œํ’ˆ์ด ์ธ๊ธฐ ์ƒํ’ˆ์ธ๊ฐ€์š”? ํŒ๋งค ๋ฐ์ดํ„ฐ๋ฅผ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ๋‚˜์š”?",
"ํŒ๋งค ์ง€์—ญ์ด๋‚˜ ์ œํ•œ์ด ์žˆ๋‚˜์š”?",
"์ด ์ œํ’ˆ์˜ ํ˜„์žฌ ์‹œ์žฅ ํŠธ๋ Œ๋“œ๋Š” ์–ด๋–ค๊ฐ€์š”?",
"ํŒ๋งค ๊ธฐ๋ก์ด ์žˆ๋Š” ์ œํ’ˆ์ธ์ง€ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‚˜์š”?"
],
"์ œํ’ˆ ์ƒ์„ธ ๋ฐ ํ’ˆ์งˆ ๊ด€๋ จ ์งˆ๋ฌธ": [
"์ด ์ œํ’ˆ์˜ ์ฃผ์š” ๊ธฐ๋Šฅ์€ ๋ฌด์—‡์ธ๊ฐ€์š”?",
"ํ’ˆ์งˆ ํ…Œ์ŠคํŠธ๋ฅผ ํ†ต๊ณผํ•œ ์ œํ’ˆ์ธ๊ฐ€์š”?",
"์ œํ’ˆ์˜ ์›์žฌ๋ฃŒ๋Š” ๋ฌด์—‡์ธ๊ฐ€์š”?",
"์›์‚ฐ์ง€๋Š” ์–ด๋””์ธ๊ฐ€์š”?",
"์ œํ’ˆ์€ ์‚ฌ์šฉํ•˜๊ธฐ์— ์•ˆ์ „ํ•œ๊ฐ€์š”?",
"์นœํ™˜๊ฒฝ ์ธ์ฆ์„ ๋ฐ›์€ ์ œํ’ˆ์ธ๊ฐ€์š”?",
"์ œํ’ˆ ์‚ฌ์šฉ ๊ธฐ๊ฐ„(๋‚ด๊ตฌ์„ฑ)์€ ์–ด๋А ์ •๋„์ธ๊ฐ€์š”?",
"๋ณด์ฆ ๊ธฐ๊ฐ„์€ ์–ด๋–ป๊ฒŒ ๋˜๋‚˜์š”?",
"์‚ฌ์šฉ ์ค‘ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ์–ด๋–ป๊ฒŒ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•˜๋‚˜์š”?",
"์‚ฌ์šฉ ์„ค๋ช…์„œ๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ๋‚˜์š”?"
],
"๊ฐ€๊ฒฉ ๋ฐ ๊ฒฐ์ œ ๊ด€๋ จ ์งˆ๋ฌธ": [
"๋„๋งค ๊ฐ€๊ฒฉ์€ ์–ผ๋งˆ์ธ๊ฐ€์š”?",
"๋Œ€๋Ÿ‰ ๊ตฌ๋งค ์‹œ ํ• ์ธ์ด ๊ฐ€๋Šฅํ•œ๊ฐ€์š”?",
"์ฒซ ๊ฑฐ๋ž˜ ๊ณ ๊ฐ์„ ์œ„ํ•œ ํŠน๋ณ„ ํ˜œํƒ์ด ์žˆ๋‚˜์š”?",
"๊ฒฐ์ œ ์กฐ๊ฑด์„ ์•Œ๋ ค์ฃผ์„ธ์š”.",
"๋ถ€๋ถ„ ๊ฒฐ์ œ ํ›„ ์ž”๊ธˆ ๊ฒฐ์ œ๊ฐ€ ๊ฐ€๋Šฅํ•œ๊ฐ€์š”?",
"๋Œ€๋Ÿ‰ ์ฃผ๋ฌธ ์‹œ ์ดˆ๊ธฐ ๋ณด์ฆ๊ธˆ์„ ์š”๊ตฌํ•˜์‹œ๋‚˜์š”?",
"๋ฐฐ์†ก๋ฃŒ ํฌํ•จ ๊ฐ€๊ฒฉ์ธ๊ฐ€์š”?",
"์ถ”๊ฐ€ ๋น„์šฉ์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‚˜์š”?",
"ํ• ์ธ ์ฟ ํฐ์ด๋‚˜ ํ”„๋กœ๋ชจ์…˜ ์ฝ”๋“œ๋Š” ์—†๋‚˜์š”?",
"์ •๊ธฐ ๊ตฌ๋งค ๊ณ ๊ฐ์„ ์œ„ํ•œ ํ• ์ธ ์ •์ฑ…์ด ์žˆ๋‚˜์š”?"
],
"๋ฐฐ์†ก ๋ฐ ๋ฌผ๋ฅ˜ ๊ด€๋ จ ์งˆ๋ฌธ": [
"๋ฐฐ์†ก์€ ์–ด๋А ํƒ๋ฐฐ์‚ฌ๋ฅผ ํ†ตํ•ด ์ง„ํ–‰๋˜๋‚˜์š”?",
"์˜ˆ์ƒ ๋ฐฐ์†ก ๊ธฐ๊ฐ„์€ ์–ผ๋งˆ๋‚˜ ๋˜๋‚˜์š”?",
"ํ•œ๊ตญ๊นŒ์ง€ ๋ฐฐ์†ก์ด ๊ฐ€๋Šฅํ•œ๊ฐ€์š”?",
"๊ตญ์ œ ๋ฐฐ์†ก๋ฃŒ๋Š” ์–ด๋–ป๊ฒŒ ๊ณ„์‚ฐ๋˜๋‚˜์š”?",
"๋Œ€๋Ÿ‰ ์ฃผ๋ฌธ ์‹œ ๋ฐฐ์†ก๋น„ ํ• ์ธ ํ˜œํƒ์ด ์žˆ๋‚˜์š”?",
"๋ฐฐ์†ก ์ค‘ ํŒŒ์†์ด ๋ฐœ์ƒํ•˜๋ฉด ์–ด๋–ป๊ฒŒ ์ฒ˜๋ฆฌํ•˜๋‚˜์š”?",
"์ฃผ๋ฌธ ํ›„ ๋ช‡ ์ผ ๋‚ด์— ๋ฐฐ์†ก์ด ์‹œ์ž‘๋˜๋‚˜์š”?",
"๋ฐฐ์†ก ์ถ”์  ๋ฒˆํ˜ธ๋ฅผ ์ œ๊ณตํ•˜์‹œ๋‚˜์š”?",
"์ฃผ๋ฌธ ์ƒํƒœ๋ฅผ ์–ด๋–ป๊ฒŒ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‚˜์š”?",
"๋ฐฐ์†ก๋ฃŒ๋ฅผ ์ง์ ‘ ๋ถ€๋‹ดํ•ด์•ผ ํ•˜๋‚˜์š”?"
]
}
def guess_lang(text: str) -> str:
text = text.strip()
if not text:
return ""
try:
lang = detect(text)
if lang in ['ko', 'en', 'zh-cn', 'zh-tw']:
if lang.startswith('zh'):
return 'zh'
return lang
else:
# langdetect ๊ฒฐ๊ณผ๊ฐ€ ko, en, zh๊ฐ€ ์•„๋‹ ๊ฒฝ์šฐ ํœด๋ฆฌ์Šคํ‹ฑ ์ ์šฉ
if all('a' <= c.lower() <= 'z' for c in text if c.isalpha()):
return 'en'
# ํ•œ๊ธ€ ์Œ์ ˆ ๋ฒ”์œ„ ํŒ๋‹จ
elif any('\uac00' <= c <= '\ud7a3' for c in text):
return 'ko'
# ์ค‘๊ตญ์–ด ๋ฒ”์œ„ ํŒ๋‹จ
elif any('\u4e00' <= c <= '\u9fff' for c in text):
return 'zh'
else:
# ๋””ํดํŠธ ์˜์–ด ์ฒ˜๋ฆฌ
return 'en'
except:
# langdetect ์‹คํŒจ ์‹œ ํœด๋ฆฌ์Šคํ‹ฑ ์ ์šฉ
if all('a' <= c.lower() <= 'z' for c in text if c.isalpha()):
return 'en'
elif any('\uac00' <= c <= '\ud7a3' for c in text):
return 'ko'
elif any('\u4e00' <= c <= '\u9fff' for c in text):
return 'zh'
else:
return 'en'
def validate_input(text: str, target_langs: list) -> bool:
lang = guess_lang(text)
logger.info(f"๊ฐ์ง€๋œ(์ถ”์ •) ์–ธ์–ด: {lang}")
return lang in target_langs
def translate_ko_en(input_text: str):
logger.info("ํ•œ-์˜ ๋ฒˆ์—ญ ์‹œ์ž‘")
if not validate_input(input_text, ['ko', 'en']):
return "์œ ํšจํ•˜์ง€ ์•Š์€ ์ž…๋ ฅ์ž…๋‹ˆ๋‹ค. ํ•œ๊ตญ์–ด ๋˜๋Š” ์˜์–ด ํ…์ŠคํŠธ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”."
try:
detected_lang = guess_lang(input_text)
logger.info(f"๊ฐ์ง€๋œ(์ถ”์ •) ์–ธ์–ด: {detected_lang}")
if detected_lang == 'ko':
direction = "Korean to English"
input_lang = "Korean"
output_lang = "English"
else:
direction = "English to Korean"
input_lang = "English"
output_lang = "Korean"
logger.info(f"๋ฒˆ์—ญ ๋ฐฉํ–ฅ: {direction}")
current_time = int(time.time() * 1000)
random.seed(current_time)
temperature = random.uniform(0.4, 0.85)
top_p = random.uniform(0.9, 0.98)
request_id = str(uuid.uuid4())[:8]
timestamp_micro = int(time.time() * 1000000) % 1000
current_hour = datetime.now().hour
time_context = f"Consider the current time is {current_hour}:00."
# ํ•œ๊ตญ์–ด ์ถœ๋ ฅ ์‹œ ์ž์—ฐ์Šค๋Ÿฌ์šด ํ•œ๊ตญ์–ด ์กฐ๊ฑด ์ถ”๊ฐ€
korean_conditions = ""
if output_lang == "Korean":
korean_conditions = KO_EN_CONDITIONS
system_prompt = "You are a helpful translator."
prompt = f"""
Translate the following {input_lang} text into {output_lang}:
{input_text}
Rules:
1. Output ONLY the {output_lang} translation without additional labels or formatting.
2. Preserve the original meaning and intent.
3. No explanations or meta-commentary.
4. No formatting beyond basic text.
5. {time_context}
[Seed: {current_time}]
{korean_conditions}
"""
logger.debug(f"ํ”„๋กฌํ”„ํŠธ:\n{prompt}")
# ์ž‘๋™ํ•˜๋Š” API ํ‚ค ๊ฐ€์ ธ์˜ค๊ธฐ
api_key = get_working_api_key()
genai.configure(api_key=api_key)
# Gemini ๋ชจ๋ธ ์„ค์ •
generation_config = {
"max_output_tokens": 200,
"temperature": temperature,
"top_p": top_p,
}
# Gemini ๋ชจ๋ธ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ
model = genai.GenerativeModel(
model_name="gemini-2.0-flash",
generation_config=generation_config,
)
# ๋ฒˆ์—ญ ์‹คํ–‰
full_prompt = f"{system_prompt}\n\n{prompt}"
response = model.generate_content(full_prompt)
translated = response.text.strip()
logger.info("Gemini ๋ฒˆ์—ญ ์™„๋ฃŒ")
logger.debug(f"๋ฒˆ์—ญ ๊ฒฐ๊ณผ:\n{translated}")
return translated
except Exception as e:
logger.error(f"๋ฒˆ์—ญ ์ค‘ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}")
# API ํ‚ค ๋ฌธ์ œ์ธ ๊ฒฝ์šฐ ํ•ด๋‹น ํ‚ค๋ฅผ ์‹คํŒจ ๋ชฉ๋ก์— ์ถ”๊ฐ€
if "api" in str(e).lower() or "key" in str(e).lower() or "quota" in str(e).lower():
try:
current_key = api_key_manager['keys'][api_key_manager['current_index']]
mark_api_key_failed(current_key)
logger.info("API ํ‚ค ๋ฌธ์ œ๋กœ ์ธํ•œ ์˜ค๋ฅ˜. ๋‹ค์Œ ํ‚ค๋กœ ์žฌ์‹œ๋„ํ•˜์„ธ์š”.")
except:
pass
return f"๋ฒˆ์—ญ ์ค‘ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}"
def translate_ko_zh(input_text: str):
logger.info("ํ•œ-์ค‘ ๋ฒˆ์—ญ ์‹œ์ž‘")
if not validate_input(input_text, ['ko', 'zh']):
return "์œ ํšจํ•˜์ง€ ์•Š์€ ์ž…๋ ฅ์ž…๋‹ˆ๋‹ค. ํ•œ๊ตญ์–ด ๋˜๋Š” ์ค‘๊ตญ์–ด ํ…์ŠคํŠธ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”."
try:
detected_lang = guess_lang(input_text)
logger.info(f"๊ฐ์ง€๋œ(์ถ”์ •) ์–ธ์–ด: {detected_lang}")
if detected_lang == 'ko':
direction = "Korean to Chinese"
input_lang = "Korean"
output_lang = "Chinese"
else:
direction = "Chinese to Korean"
input_lang = "Chinese"
output_lang = "Korean"
logger.info(f"๋ฒˆ์—ญ ๋ฐฉํ–ฅ: {direction}")
current_time = int(time.time() * 1000)
random.seed(current_time)
temperature = random.uniform(0.5, 0.8)
top_p = random.uniform(0.9, 0.98)
# ์ž‘๋™ํ•˜๋Š” API ํ‚ค ๊ฐ€์ ธ์˜ค๊ธฐ
api_key = get_working_api_key()
genai.configure(api_key=api_key)
# Gemini ๋ชจ๋ธ ์„ค์ •
generation_config = {
"max_output_tokens": 1024,
"temperature": temperature,
"top_p": top_p,
}
# Gemini ๋ชจ๋ธ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ
model = genai.GenerativeModel(
model_name="gemini-2.0-flash",
generation_config=generation_config,
)
# ํ”„๋กฌํ”„ํŠธ ์ค€๋น„
prompt = f"{KO_ZH_SYSTEM_MESSAGE}\n\n{input_text}"
# ๋ฒˆ์—ญ ์‹คํ–‰
response = model.generate_content(prompt)
translated = response.text.strip()
logger.info("Gemini ๋ฒˆ์—ญ ์™„๋ฃŒ")
logger.debug(f"๋ฒˆ์—ญ ๊ฒฐ๊ณผ:\n{translated}")
return translated
except Exception as e:
logger.error(f"๋ฒˆ์—ญ ์ค‘ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}")
# API ํ‚ค ๋ฌธ์ œ์ธ ๊ฒฝ์šฐ ํ•ด๋‹น ํ‚ค๋ฅผ ์‹คํŒจ ๋ชฉ๋ก์— ์ถ”๊ฐ€
if "api" in str(e).lower() or "key" in str(e).lower() or "quota" in str(e).lower():
try:
current_key = api_key_manager['keys'][api_key_manager['current_index']]
mark_api_key_failed(current_key)
logger.info("API ํ‚ค ๋ฌธ์ œ๋กœ ์ธํ•œ ์˜ค๋ฅ˜. ๋‹ค์Œ ํ‚ค๋กœ ์žฌ์‹œ๋„ํ•˜์„ธ์š”.")
except:
pass
return f"๋ฒˆ์—ญ ์ค‘ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}"
def update_subcategories(category):
if category in categories:
return gr.update(choices=categories[category], value=None)
else:
return gr.update(choices=[], value=None)
def set_input_text(selected_text):
return selected_text
# ์ปค์Šคํ…€ 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);
}
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%;
margin: 0 auto;
padding: 20px;
background-color: var(--background-color);
}
.custom-header {
background: #FF7F00;
padding: 2rem;
border-radius: 15px;
margin-bottom: 20px;
box-shadow: var(--shadow);
text-align: center;
}
.custom-header h1 {
margin: 0;
font-size: 2.5rem;
font-weight: 700;
color: black;
}
.custom-header p {
margin: 10px 0 0;
font-size: 1.2rem;
color: black;
}
.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;
}
.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 {
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;
}
.image-container:hover {
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
}
.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;
}
.guide-container {
background-color: var(--card-bg);
border-radius: var(--border-radius);
box-shadow: var(--shadow);
padding: 1.5rem;
margin-bottom: 1.5rem;
border: 1px solid rgba(0, 0, 0, 0.04);
}
.guide-title {
font-size: 1.5rem;
font-weight: 700;
color: var(--primary-color);
margin-bottom: 1.5rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid var(--primary-color);
display: flex;
align-items: center;
}
.guide-title i {
margin-right: 0.8rem;
font-size: 1.5rem;
}
.guide-item {
display: flex;
margin-bottom: 1rem;
align-items: flex-start;
}
.guide-number {
background-color: var(--primary-color);
color: white;
width: 25px;
height: 25px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
margin-right: 10px;
flex-shrink: 0;
}
.guide-text {
flex: 1;
line-height: 1.6;
}
.guide-text a {
color: var(--primary-color);
text-decoration: underline;
font-weight: 600;
}
.guide-text a:hover {
color: var(--accent-color);
}
.guide-highlight {
background-color: rgba(251, 127, 13, 0.1);
padding: 2px 5px;
border-radius: 4px;
font-weight: 500;
}
"""
# 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_interface():
with gr.Blocks(css=custom_css, theme=gr.themes.Soft(
primary_hue=gr.themes.Color(
c50="#FFF7ED",
c100="#FFEDD5",
c200="#FED7AA",
c300="#FDBA74",
c400="#FB923C",
c500="#F97316",
c600="#EA580C",
c700="#C2410C",
c800="#9A3412",
c900="#7C2D12",
c950="#431407",
),
secondary_hue="zinc",
neutral_hue="zinc",
font=("Pretendard", "sans-serif")
)) as demo:
gr.HTML(fontawesome_link)
with gr.Tabs() as tabs:
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/3097/3097412.png"> ์นดํ…Œ๊ณ ๋ฆฌ ์„ ํƒ</div>')
with gr.Row():
category_dropdown = gr.Dropdown(
label="1์ฐจ ์นดํ…Œ๊ณ ๋ฆฌ ์„ ํƒ",
choices=list(categories.keys()),
value=None
)
subcategory_dropdown = gr.Dropdown(
label="2์ฐจ ์งˆ๋ฌธ ์„ ํƒ",
choices=[],
value=None
)
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>')
with gr.Row():
with gr.Column(scale=1):
kz_input_box = gr.Textbox(
label="์ž…๋ ฅ (ํ•œ๊ตญ์–ด ๋˜๋Š” ์ค‘๊ตญ์–ด)",
lines=8,
placeholder="๋ฒˆ์—ญํ•  ํ…์ŠคํŠธ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”..."
)
with gr.Column(scale=1):
kz_output_box = gr.Textbox(
label="๋ฒˆ์—ญ ๊ฒฐ๊ณผ",
lines=8
)
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>')
kz_translate_button = gr.Button("๋ฒˆ์—ญํ•˜๊ธฐ", elem_classes="custom-button")
# 1์ฐจ ์„ ํƒ ์‹œ 2์ฐจ ํ•ญ๋ชฉ ์—…๋ฐ์ดํŠธ
category_dropdown.change(
fn=update_subcategories,
inputs=category_dropdown,
outputs=subcategory_dropdown
)
# 2์ฐจ ์„ ํƒ ์‹œ ์ž…๋ ฅ๋ž€์— ํ•ด๋‹น ๊ฐ’ ์ž๋™ ์ž…๋ ฅ
subcategory_dropdown.change(
fn=set_input_text,
inputs=subcategory_dropdown,
outputs=kz_input_box
)
# ๋ฒˆ์—ญ ํ•จ์ˆ˜
kz_translate_button.click(
fn=translate_ko_zh,
inputs=kz_input_box,
outputs=kz_output_box
)
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/4297/4297825.png"> ๋ฒˆ์—ญ</div>')
with gr.Row():
with gr.Column(scale=1):
ke_input_box = gr.Textbox(
label="์ž…๋ ฅ ํ…์ŠคํŠธ (ํ•œ๊ตญ์–ด ๋˜๋Š” ์˜์–ด)",
placeholder="๋ฒˆ์—ญํ•  ํ…์ŠคํŠธ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”...",
lines=8
)
with gr.Column(scale=1):
ke_output_box = gr.Textbox(
label="๋ฒˆ์—ญ ๊ฒฐ๊ณผ",
lines=8,
interactive=False
)
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>')
ke_translate_button = gr.Button("๋ฒˆ์—ญํ•˜๊ธฐ", elem_classes="custom-button")
# ๋ฒˆ์—ญ ํ•จ์ˆ˜
ke_translate_button.click(
fn=translate_ko_en,
inputs=ke_input_box,
outputs=ke_output_box
)
return demo
if __name__ == "__main__":
demo = create_interface()
demo.launch()