E2T4TWEWEWE / app.py
ssboost's picture
Update app.py
bfbe54d verified
import os
import gradio as gr
from dotenv import load_dotenv
import logging
import time
import uuid
import google.generativeai as genai
import random
# λ‘œκΉ… μ„€μ • - INFO 레벨둜 λ³€κ²½
logging.basicConfig(level=logging.INFO)
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 ν‚€λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.")
# λͺ¨λΈλ³„ ν”„λ‘¬ν”„νŠΈ μ •μ˜
GEMINI_PROMPTS = {
"λ§žμΆ€λ²• 검사기": """
# λͺ©μ : ν•œκ΅­μ–΄ 문법 ꡐ정 및 κ°œμ„ 
## μ§€μΉ¨
1. μ£Όμ–΄μ§„ ν…μŠ€νŠΈμ˜ 문법을 μ „λ¬Έμ μœΌλ‘œ κ²€ν† ν•˜κ³  κ΅μ •ν•˜μ„Έμš”.
2. 원본 ν…μŠ€νŠΈμ˜ μ˜λ―Έμ™€ μ˜λ„λ₯Ό μ² μ €νžˆ μœ μ§€ν•˜λ©΄μ„œ 문법적인 λΆ€λΆ„λ§Œ μˆ˜μ •ν•˜μ„Έμš”.
3. λ‹€μŒ μš”μ†Œλ“€μ„ ν•œκ΅­μ–΄ 문법 κ·œμΉ™μ— 맞게 μ •ν™•ν•˜κ²Œ μˆ˜μ •ν•˜μ„Έμš”:
- λ§žμΆ€λ²• 였λ₯˜
- 띄어쓰기 였λ₯˜
- λ¬Έμž₯ λΆ€ν˜Έ μ‚¬μš©
- 쑰사 μ‚¬μš©μ˜ μ •ν™•μ„±
- μ–΄λ―Έ ν™œμš©μ˜ μ •ν™•μ„±
- λ¬Έμž₯ μ„±λΆ„μ˜ ν˜Έμ‘ 관계
4. μ ˆλŒ€ 원본 ν…μŠ€νŠΈμ— μ—†λŠ” λ‚΄μš©μ„ μΆ”κ°€ν•˜κ±°λ‚˜ 의미λ₯Ό λ³€κ²½ν•˜μ§€ λ§ˆμ„Έμš”.
5. λΆ€κ°€ μ„€λͺ…μ΄λ‚˜ 메타 정보(예: "μˆ˜μ •λœ ν…μŠ€νŠΈ:", "κ°œμ„ λœ ν…μŠ€νŠΈ:" λ“±)λŠ” 좜λ ₯ν•˜μ§€ λ§ˆμ„Έμš”.
6. 였직 κ΅μ •λœ ν…μŠ€νŠΈλ§Œ 좜λ ₯ν•˜μ„Έμš”.
""",
"κΈ€ 닀듬기": """
# λͺ©μ : λ¬Έμž₯ ꡬ쑰와 ν‘œν˜„ κ°œμ„ 
## μ§€μΉ¨
1. λ¬Έμž₯ ꡬ쑰 μ΅œμ ν™”
- λΆˆν•„μš”ν•œ 문ꡬ와 쀑볡 ν‘œν˜„μ„ μ œκ±°ν•˜μ—¬ κ°„κ²°μ„± 확보
- μ£Όμ–΄-μ„œμˆ μ–΄ 관계λ₯Ό λͺ…ν™•νžˆ ν•˜μ—¬ 의미 전달λ ₯ κ°•ν™”
- κ³Όλ„ν•˜κ²Œ κΈ΄ λ¬Έμž₯은 적절히 λΆ„λ¦¬ν•˜μ—¬ 가독성 ν–₯상
- λ¬Έμž₯ κ°„μ˜ 논리적 μ—°κ²°μ„± 확보
2. 단락 ꡬ성 μ΅œμ ν™”
- 각 단락이 λͺ…ν™•ν•œ 핡심 주제λ₯Ό 가지도둝 μ‘°μ •
- 단락 κ°„ μžμ—°μŠ€λŸ¬μš΄ 흐름을 μœ„ν•œ μ—°κ²°μ–΄ μ‚¬μš©
- 단락 길이의 κ· ν˜• μœ μ§€
3. μ–΄νœ˜ 및 ν‘œν˜„ κ°œμ„ 
- λΆˆν•„μš”ν•œ μ™Έλž˜μ–΄λŠ” μ μ ˆν•œ ν•œκ΅­μ–΄λ‘œ λŒ€μ²΄
- μΆ”μƒμ μ΄κ±°λ‚˜ λͺ¨ν˜Έν•œ ν‘œν˜„μ€ ꡬ체적이고 λͺ…ν™•ν•œ μš©μ–΄λ‘œ λŒ€μ²΄
- μ€‘λ³΅λ˜λŠ” ν‘œν˜„ 제거둜 λ¬Έμž₯ νš¨μœ¨μ„± μ¦λŒ€
- κ΄€μš©μ  ν‘œν˜„κ³Ό μ μ ˆν•œ λΉ„μœ λ₯Ό ν™œμš©ν•˜μ—¬ ν‘œν˜„λ ₯ κ°•ν™”
4. 전달λ ₯ κ°•ν™”
- 핡심 λ©”μ‹œμ§€κ°€ 돋보이도둝 λ¬Έμž₯ ꡬ쑰 재배치
- 논리적 흐름과 일관성 확보
- λ…μžμ˜ 이해도λ₯Ό κ³ λ €ν•œ ν‘œν˜„ 선택
5. 일관성 μœ μ§€
- 문체와 ν†€μ˜ 일관성 μœ μ§€
- μ „λ¬Έ μš©μ–΄μ™€ ν‘œν˜„μ˜ 톡일성 확보
- μ‹œμ œμ˜ μΌκ΄€λœ μ‚¬μš©
## 좜λ ₯ ν˜•μ‹
- 원본 ν…μŠ€νŠΈμ™€ λ™μΌν•œ λ§νˆ¬μ™€ μ–΄μ‘°λ₯Ό μœ μ§€ν•˜λ˜, μœ„ 기쀀에 따라 κ°œμ„ λœ ν…μŠ€νŠΈλ§Œ 좜λ ₯
- λΆ€κ°€ μ„€λͺ…μ΄λ‚˜ 메타 μ •λ³΄λŠ” ν¬ν•¨ν•˜μ§€ μ•ŠμŒ
""",
"λͺ…μ–Έ μΈμš©ν•˜κΈ°": """
# λͺ©μ : λͺ…언을 ν™œμš©ν•œ 섀득λ ₯ μžˆλŠ” ν…μŠ€νŠΈ κ°œμ„ 
## μ§€μΉ¨
1. λ¬Έλ§₯ 뢄석
- ν…μŠ€νŠΈμ˜ 핡심 μ£Όμ œμ™€ μ˜λ„ νŒŒμ•…
- ν…μŠ€νŠΈμ˜ λΆ„μœ„κΈ°μ™€ 톀 뢄석
- λ…μžμΈ΅κ³Ό λͺ©μ  κ³ λ €
2. μ μ ˆν•œ λͺ…μ–Έ 선택
- ν…μŠ€νŠΈμ˜ μ£Όμ œμ™€ μ§μ ‘μ μœΌλ‘œ μ—°κ΄€λœ λͺ…μ–Έ 선택
- λ‹€μ–‘ν•œ λΆ„μ•Ό(μ² ν•™, λ¬Έν•™, 역사, κ³Όν•™ λ“±)의 λͺ…μ–Έ κ³ λ €
- 고전적인 λͺ…μ–ΈλΏλ§Œ μ•„λ‹ˆλΌ ν˜„λŒ€ 유λͺ…인(μ—°μ˜ˆμΈ, 기업인, μš΄λ™μ„ μˆ˜, μ •μΉ˜μΈ λ“±)의 인상적인 말도 적극 ν™œμš©
- ν•œκ΅­ 및 μ„Έκ³„μ˜ μ €λͺ…ν•œ 인물의 λͺ…μ–Έ ν™œμš©
- ν…μŠ€νŠΈμ˜ 톀과 λΆ„μœ„κΈ°μ— μ–΄μšΈλ¦¬λŠ” λͺ…μ–Έ 선택
- κ°€λŠ₯ν•˜λ©΄ μ‹œμ˜μ μ ˆν•˜κ³  μ΅œμ‹  νŠΈλ Œλ“œλ₯Ό λ°˜μ˜ν•œ ν˜„λŒ€μ  λͺ…μ–Έ μš°μ„  κ³ λ €
3. λͺ…μ–Έ 톡합
- ν…μŠ€νŠΈ λ‚΄ κ°€μž₯ 효과적인 μœ„μΉ˜μ— λͺ…μ–Έ 배치 (μ‹œμž‘, 쀑간, λ˜λŠ” κ²°λ‘ )
- λͺ…언을 μžμ—°μŠ€λŸ½κ²Œ λ„μž…ν•˜μ—¬ κΈ°μ‘΄ ν…μŠ€νŠΈμ™€ μ‘°ν™”λ‘­κ²Œ 톡합
- ν•„μš”μ‹œ λͺ…μ–Έμ˜ 좜처(인물 이름) 포함
- λ”± ν•œ 번만 λͺ…μ–Έ μΈμš©ν•˜κΈ°
- ν˜„λŒ€ 유λͺ…μΈμ˜ λͺ…μ–Έ μ‚¬μš© μ‹œ κ°„λ‹¨ν•œ λ§₯락 μ„€λͺ… κ³ λ €(ν•„μš”ν•œ 경우)
4. 전체 ν…μŠ€νŠΈ μ‘°μ •
- λͺ…μ–Έ λ„μž… ν›„ ν•„μš”μ‹œ μ „ν›„ λ¬Έμž₯ μ‘°μ •ν•˜μ—¬ μžμ—°μŠ€λŸ¬μš΄ 흐름 μœ μ§€
- 원본 ν…μŠ€νŠΈμ˜ 핡심 λ©”μ‹œμ§€μ™€ μ˜λ„ 보쑴
- 전체적인 일관성과 응집λ ₯ μœ μ§€
- λͺ…언이 ν…μŠ€νŠΈμ— 신선함과 κΆŒμœ„λ₯Ό λΆ€μ—¬ν•˜λŠ”μ§€ 확인
## 좜λ ₯ ν˜•μ‹
- λͺ…언이 μžμ—°μŠ€λŸ½κ²Œ ν†΅ν•©λœ κ°œμ„ λœ ν…μŠ€νŠΈλ§Œ 좜λ ₯
- λΆ€κ°€ μ„€λͺ…μ΄λ‚˜ 메타 정보 없이 μˆœμˆ˜ν•˜κ²Œ κ°œμ„ λœ ν…μŠ€νŠΈλ§Œ 제곡
""",
"λ§žμΆ€ν˜• λ³€ν™˜": """
# λͺ©μ : μ‚¬μš©μž μ •μ˜ 페λ₯΄μ†Œλ‚˜μ™€ λͺ©μ μ— λ§žλŠ” ν…μŠ€νŠΈ λ³€ν™˜
## μ§€μΉ¨
1. 페λ₯΄μ†Œλ‚˜ 뢄석 및 적용
- μ‚¬μš©μžκ°€ μ œκ³΅ν•œ 페λ₯΄μ†Œλ‚˜μ˜ νŠΉμ„± 뢄석 (말투, μ–΄νœ˜ 선택, λ¬Έμž₯ ꡬ쑰 λ“±)
- ν•΄λ‹Ή 페λ₯΄μ†Œλ‚˜κ°€ μ‹€μ œλ‘œ μž‘μ„±ν–ˆμ„ λ²•ν•œ μŠ€νƒ€μΌλ‘œ ν…μŠ€νŠΈ μž¬κ΅¬μ„±
- 페λ₯΄μ†Œλ‚˜μ˜ κ³ μœ ν•œ ν‘œν˜„ 방식과 관점 반영
2. λͺ©μ  뢄석 및 적용
- μ‚¬μš©μžκ°€ λͺ…μ‹œν•œ λͺ©μ μ— 맞게 ν…μŠ€νŠΈμ˜ μ „λ°˜μ μΈ λ°©ν–₯μ„± μ‘°μ •
- λͺ©μ μ— λ§žλŠ” ꡬ쑰, λ…Όμ‘°, 강쑰점 μ„€μ •
- λͺ©μ  달성에 ν•„μš”ν•œ μš”μ†Œ κ°•ν™” 및 λΆˆν•„μš”ν•œ μš”μ†Œ 제거
3. ν…μŠ€νŠΈ 맞좀 λ³€ν™˜
- 페λ₯΄μ†Œλ‚˜μ™€ λͺ©μ μ„ μ’…ν•©μ μœΌλ‘œ κ³ λ €ν•œ ν…μŠ€νŠΈ λ³€ν™˜
- 원본 ν…μŠ€νŠΈμ˜ 핡심 정보와 μ£Όμš” 논점 μœ μ§€
- μ–΄νœ˜, λ¬Έμž₯ ꡬ쑰, λΉ„μœ , μ˜ˆμ‹œ 등을 페λ₯΄μ†Œλ‚˜μ™€ λͺ©μ μ— 맞게 μ‘°μ •
- 전체적인 일관성과 μ§„μ •μ„± μœ μ§€
4. ν’ˆμ§ˆ 및 효과 μ΅œμ ν™”
- λͺ…ν™•μ„±κ³Ό 이해도 ν–₯상
- 섀득λ ₯κ³Ό ν˜Έμ†Œλ ₯ κ°•ν™”
- 독창성과 참신함 좔ꡬ
- λͺ©ν‘œ λ…μžμ—κ²Œ 효과적으둜 전달될 수 μžˆλŠ” ν˜•νƒœλ‘œ μ΅œμ ν™”
## 좜λ ₯ ν˜•μ‹
- 페λ₯΄μ†Œλ‚˜μ™€ λͺ©μ μ— 맞게 λ³€ν˜•λœ ν…μŠ€νŠΈλ§Œ 좜λ ₯
- λΆ€κ°€ μ„€λͺ…μ΄λ‚˜ 메타 정보 없이 μˆœμˆ˜ν•˜κ²Œ λ³€ν™˜λœ ν…μŠ€νŠΈλ§Œ 제곡
- μ‚¬μš©μžκ°€ λͺ…μ‹œν•œ λͺ©μ κ³Ό 페λ₯΄μ†Œλ‚˜μ˜ νŠΉμ„±μ΄ λͺ…ν™•νžˆ λ“œλŸ¬λ‚˜λŠ” κ²°κ³Όλ¬Ό 제곡
""",
}
def process_text(input_text, improvement_type, custom_purpose, persona, temperature, top_p):
"""Gemini λͺ¨λΈμš© ν…μŠ€νŠΈ 처리 ν•¨μˆ˜"""
try:
print("ν…μŠ€νŠΈ 처리 μ‹œμž‘")
request_id = str(uuid.uuid4())[:8]
timestamp_micro = int(time.time() * 1000000) % 1000
if improvement_type == "λ§žμΆ€ν˜• λ³€ν™˜":
purpose = f"λ‹€μŒ 페λ₯΄μ†Œλ‚˜λ₯Ό κ°€μ§€κ³  ν…μŠ€νŠΈλ₯Ό λ‹€λ“¬μœΌμ„Έμš”: {persona}\n\nλͺ©μ : {custom_purpose}"
else:
purpose = GEMINI_PROMPTS[improvement_type]
# μž‘λ™ν•˜λŠ” API ν‚€ κ°€μ Έμ˜€κΈ°
selected_api_key = get_working_api_key()
logger.info(f"API ν‚€ 선택 μ™„λ£Œ: {selected_api_key[:5]}...")
# μ„ νƒλœ API ν‚€λ‘œ Gemini ꡬ성
genai.configure(api_key=selected_api_key)
model = genai.GenerativeModel(
model_name="gemini-2.0-flash",
generation_config={
"temperature": temperature,
"top_p": top_p,
"max_output_tokens": 2000,
}
)
# μ‹œμŠ€ν…œ μ§€μ‹œμ‚¬ν•­μ„ 첫 번째 μ‚¬μš©μž λ©”μ‹œμ§€μ— 포함
prompt = f"REQ-{request_id}-{timestamp_micro}\n\nλ‹€μŒ ν…μŠ€νŠΈλ₯Ό λͺ©μ μ— 맞게 λ‹€λ“¬μ–΄μ£Όμ„Έμš”.\n\nν…μŠ€νŠΈ: {input_text}\n\nλͺ©μ : {purpose}"
response = model.generate_content(prompt)
print("ν…μŠ€νŠΈ 처리 μ™„λ£Œ")
return response.text
except Exception as e:
print("ν…μŠ€νŠΈ 처리 μ‹€νŒ¨")
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 create_interface():
# FontAwesome μ•„μ΄μ½˜ 포함
fontawesome = """
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer" />
"""
# 닀크λͺ¨λ“œ 적용 CSS μŠ€νƒ€μΌ
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;
/* κ°€μ΄λ“œ μ»¨ν…Œμ΄λ„ˆ */
--guide-bg: #FFF6F0;
--guide-border: rgba(255, 127, 0, 0.1);
}
/* 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);
/* κ°€μ΄λ“œ μ»¨ν…Œμ΄λ„ˆ */
--guide-bg: #2a2a2a;
--guide-border: rgba(255, 127, 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);
/* κ°€μ΄λ“œ μ»¨ν…Œμ΄λ„ˆ */
--guide-bg: #2a2a2a;
--guide-border: rgba(255, 127, 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;
transition: background-color 0.3s ease, color 0.3s ease;
}
footer {
visibility: hidden;
}
/* 5. Gradio μ»¨ν…Œμ΄λ„ˆ κ°•μ œ 적용 */
.gradio-container,
.gradio-container *,
.gr-app,
.gr-app *,
.gr-interface {
background-color: var(--background-color) !important;
color: var(--text-color) !important;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.header {
background: linear-gradient(135deg, #FB7F0D, #FF9A5B);
padding: 2rem;
border-radius: 15px;
margin-bottom: 20px;
box-shadow: var(--shadow);
text-align: center;
color: white;
}
.header h1 {
margin: 0;
font-size: 2.5rem;
font-weight: 700;
}
.header p {
margin: 10px 0 0;
font-size: 1.2rem;
opacity: 0.9;
}
/* 6. μΉ΄λ“œ 및 νŒ¨λ„ μŠ€νƒ€μΌ */
.card,
.gr-form,
.gr-box,
.gr-panel,
.custom-frame,
[class*="frame"],
[class*="panel"] {
background-color: var(--card-bg) !important;
border-radius: var(--border-radius);
padding: 20px;
margin: 10px 0;
box-shadow: var(--shadow);
border: 1px solid var(--border-color) !important;
color: var(--text-color) !important;
}
.button-primary {
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;
text-align: center;
font-weight: 600;
}
.button-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(251, 127, 13, 0.3);
}
.section-title {
display: flex;
align-items: center;
font-size: 20px;
font-weight: 700;
color: var(--text-color) !important;
margin-bottom: 15px;
padding-bottom: 8px;
border-bottom: 2px solid var(--primary-color);
}
.section-title i {
margin-right: 10px;
color: var(--primary-color);
}
.guide-container {
background-color: var(--guide-bg) !important;
border-radius: var(--border-radius);
padding: 1.5rem;
margin-bottom: 1.5rem;
border: 1px solid var(--guide-border) !important;
color: var(--text-color) !important;
}
.guide-title {
font-size: 1.3rem;
font-weight: 700;
color: var(--primary-color) !important;
margin-bottom: 1rem;
display: flex;
align-items: center;
}
.guide-title i {
margin-right: 0.8rem;
font-size: 1.3rem;
}
.guide-item {
display: flex;
margin-bottom: 0.8rem;
align-items: flex-start;
}
.guide-number {
background-color: var(--primary-color);
color: white;
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
margin-right: 10px;
flex-shrink: 0;
font-size: 14px;
}
.guide-text {
flex: 1;
line-height: 1.6;
color: var(--text-color) !important;
}
.feature-tag {
display: inline-block;
background-color: rgba(255, 127, 0, 0.1);
color: var(--primary-color);
padding: 3px 10px;
border-radius: 12px;
font-size: 14px;
font-weight: 600;
margin-right: 8px;
margin-bottom: 8px;
}
.input-label {
font-weight: 600;
margin-bottom: 8px;
color: var(--text-color) !important;
}
/* 7. μž…λ ₯ ν•„λ“œ μŠ€νƒ€μΌ */
input[type="text"],
input[type="number"],
input[type="email"],
input[type="password"],
textarea,
select,
.gr-input,
.gr-text-input,
.gr-textarea,
.gr-dropdown {
background-color: var(--input-bg) !important;
color: var(--text-color) !important;
border-color: var(--border-color) !important;
border-radius: var(--border-radius) !important;
border: 1px solid var(--border-color) !important;
padding: 12px !important;
transition: all 0.3s ease !important;
}
input[type="text"]:focus,
input[type="number"]:focus,
input[type="email"]:focus,
input[type="password"]:focus,
textarea:focus,
select:focus,
.gr-input:focus,
.gr-text-input: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. 라벨 및 ν…μŠ€νŠΈ μš”μ†Œ */
label,
.gr-label,
.gr-checkbox label,
.gr-radio label,
p, span, div {
color: var(--text-color) !important;
}
/* 9. ν…Œμ΄λΈ” μŠ€νƒ€μΌ */
table {
background-color: var(--card-bg) !important;
color: var(--text-color) !important;
border-color: var(--border-color) !important;
}
table th {
background-color: var(--primary-color) !important;
color: white !important;
border-color: var(--border-color) !important;
}
table td {
background-color: var(--card-bg) !important;
color: var(--text-color) !important;
border-color: var(--border-color) !important;
}
table tbody tr:nth-child(even) {
background-color: var(--table-even-bg) !important;
}
table tbody tr:hover {
background-color: var(--table-hover-bg) !important;
}
/* 10. μ²΄ν¬λ°•μŠ€ 및 λΌλ””μ˜€ λ²„νŠΌ */
input[type="checkbox"],
input[type="radio"] {
accent-color: var(--primary-color) !important;
}
/* 11. μŠ€ν¬λ‘€λ°” μŠ€νƒ€μΌ */
::-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);
}
/* 12. μ•„μ½”λ””μ–Έ 및 λ“œλ‘­λ‹€μš΄ */
details {
background-color: var(--card-bg) !important;
border-color: var(--border-color) !important;
color: var(--text-color) !important;
}
details summary {
background-color: var(--card-bg) !important;
color: var(--text-color) !important;
}
/* 13. 툴팁 및 νŒμ—… */
[data-tooltip]:hover::after,
.tooltip,
.popup {
background-color: var(--card-bg) !important;
color: var(--text-color) !important;
border-color: var(--border-color) !important;
box-shadow: var(--shadow-light) !important;
}
/* 14. λͺ¨λ‹¬ 및 μ˜€λ²„λ ˆμ΄ */
.modal,
.overlay,
[class*="modal"],
[class*="overlay"] {
background-color: var(--card-bg) !important;
color: var(--text-color) !important;
border-color: var(--border-color) !important;
}
/* 15. μΆ”κ°€ Gradio μ»΄ν¬λ„ŒνŠΈλ“€ */
.gr-block,
.gr-group,
.gr-row,
.gr-column {
background-color: var(--background-color) !important;
color: var(--text-color) !important;
}
/* 16. λ²„νŠΌμ€ κΈ°μ‘΄ μŠ€νƒ€μΌ μœ μ§€ (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;
}
/* 17. μ½”λ“œ 블둝 및 pre νƒœκ·Έ */
code,
pre,
.code-block {
background-color: var(--table-even-bg) !important;
color: var(--text-color) !important;
border-color: var(--border-color) !important;
}
/* 18. μ•Œλ¦Ό 및 λ©”μ‹œμ§€ */
.alert,
.message,
.notification,
[class*="alert"],
[class*="message"],
[class*="notification"] {
background-color: var(--card-bg) !important;
color: var(--text-color) !important;
border-color: var(--border-color) !important;
}
/* 19. μ „ν™˜ μ• λ‹ˆλ©”μ΄μ…˜ */
* {
transition: background-color 0.3s ease,
color 0.3s ease,
border-color 0.3s ease !important;
}
/* 20. μΆ”κ°€ Gradio μŠ€νƒ€μΌ 보완 */
.gr-sample-inputs {
border-radius: var(--border-radius) !important;
border: 1px solid var(--border-color) !important;
padding: 12px !important;
background-color: var(--input-bg) !important;
color: var(--text-color) !important;
}
/* 21. μŠ¬λΌμ΄λ” μŠ€νƒ€μΌ */
.gr-slider input[type="range"] {
accent-color: var(--primary-color) !important;
}
/* 22. λΌλ””μ˜€ λ²„νŠΌ κ·Έλ£Ή */
.gr-radio-group {
background-color: var(--card-bg) !important;
color: var(--text-color) !important;
border-color: var(--border-color) !important;
}
/* 23. μ²΄ν¬λ°•μŠ€ κ·Έλ£Ή */
.gr-checkbox-group {
background-color: var(--card-bg) !important;
color: var(--text-color) !important;
border-color: var(--border-color) !important;
}
"""
with gr.Blocks(css=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)
with gr.Row():
with gr.Column(scale=1):
# μ™Όμͺ½ μž…λ ₯ μ˜μ—­
with gr.Column(elem_classes="card"):
gr.HTML('<div class="section-title"><i class="fas fa-edit"></i> 원본 ν…μŠ€νŠΈ</div>')
input_text = gr.Textbox(
label="",
placeholder="여기에 κ°œμ„ ν•˜κ³  싢은 ν…μŠ€νŠΈλ₯Ό μž…λ ₯ν•˜μ„Έμš”...",
lines=15
)
with gr.Column(elem_classes="card"):
gr.HTML('<div class="section-title"><i class="fas fa-sliders"></i> λ³€ν™˜ μ„€μ •</div>')
improvement_type = gr.Radio(
choices=["λ§žμΆ€λ²• 검사기", "κΈ€ 닀듬기", "λͺ…μ–Έ μΈμš©ν•˜κΈ°", "λ§žμΆ€ν˜• λ³€ν™˜"],
label="λ³€ν™˜ μœ ν˜• 선택",
value="λ§žμΆ€λ²• 검사기"
)
# λ§žμΆ€ν˜• λ³€ν™˜ μ˜΅μ…˜λ“€
custom_purpose = gr.Textbox(
label="λ³€ν™˜ λͺ©μ ",
placeholder="ν…μŠ€νŠΈλ₯Ό μ–΄λ–€ λ°©ν–₯으둜 λ³€ν™˜ν•˜κ³  μ‹ΆμœΌμ‹ μ§€ μ„€λͺ…ν•΄μ£Όμ„Έμš”...",
lines=2,
visible=False
)
persona = gr.Textbox(
label="페λ₯΄μ†Œλ‚˜ μ„€μ •",
placeholder="μ–΄λ–€ μ—­ν• μ΄λ‚˜ 성격을 κ°€μ§„ κΈ€μ“°κΈ° μŠ€νƒ€μΌμ„ μ›ν•˜μ‹œλ‚˜μš”? (예: μœ λ¨ΈλŸ¬μŠ€ν•œ 개그맨, 전문적인 뢄석가)",
lines=2,
visible=False
)
with gr.Row(visible=False) as generation_settings:
temperature = gr.Slider(
minimum=0.0,
maximum=1.0,
value=0.7,
step=0.1,
label="μ°½μ˜μ„± μˆ˜μ€€",
info="λ†’μ„μˆ˜λ‘ 더 창의적인 κ²°κ³Όκ°€ μƒμ„±λ©λ‹ˆλ‹€"
)
top_p = gr.Slider(
minimum=0.0,
maximum=1.0,
value=0.9,
step=0.1,
label="λ‹€μ–‘μ„± μˆ˜μ€€",
info="λ†’μ„μˆ˜λ‘ 더 λ‹€μ–‘ν•œ ν‘œν˜„μ„ μ‚¬μš©ν•©λ‹ˆλ‹€"
)
submit_btn = gr.Button(
"✨ ν…μŠ€νŠΈ λ³€ν™˜ν•˜κΈ°",
elem_classes="button-primary"
)
with gr.Column(scale=1):
# 였λ₯Έμͺ½ 좜λ ₯ μ˜μ—­
with gr.Column(elem_classes="card"):
gr.HTML('<div class="section-title"><i class="fas fa-wand-magic-sparkles"></i> λ³€ν™˜λœ ν…μŠ€νŠΈ</div>')
output_text = gr.Textbox(
label="",
lines=22,
show_copy_button=True
)
def update_custom_inputs_visibility(choice):
is_custom = choice == "λ§žμΆ€ν˜• λ³€ν™˜"
return {
custom_purpose: gr.update(visible=is_custom),
persona: gr.update(visible=is_custom),
generation_settings: gr.update(visible=is_custom)
}
improvement_type.change(
fn=update_custom_inputs_visibility,
inputs=[improvement_type],
outputs=[custom_purpose, persona, generation_settings]
)
submit_btn.click(
fn=process_text,
inputs=[input_text, improvement_type, custom_purpose, persona, temperature, top_p],
outputs=output_text,
api_name="improve_text"
)
return demo
if __name__ == "__main__":
logger.info("μ• ν”Œλ¦¬μΌ€μ΄μ…˜ μ‹œμž‘")
demo = create_interface()
demo.queue()
demo.launch()