import random import threading import logging import sys import os # 로그 억제 설정 logging.getLogger().setLevel(logging.ERROR) # stderr 리다이렉션으로 gRPC 에러 숨기기 class SuppressStderr: def __enter__(self): self._original_stderr = sys.stderr sys.stderr = open(os.devnull, 'w') return self def __exit__(self, exc_type, exc_val, exc_tb): sys.stderr.close() sys.stderr = self._original_stderr # Google Generative AI 임포트 시 에러 억제 with SuppressStderr(): import google.generativeai as genai import gradio as gr from dotenv import load_dotenv import time import uuid # 로깅 설정 - INFO 레벨로 변경 logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # 환경 변수 로드 (dotenv는 이제 불필요하지만 호환성을 위해 유지) # load_dotenv() # 주석 처리됨 # Gemini API 키 설정 - 환경변수에서 GEMINI_API_CONFIGS = [...] 구문 그대로 실행 def load_api_configs_from_env(): env_configs = os.getenv('GEMINI_API_CONFIGS') if env_configs: try: # GEMINI_API_CONFIGS = [...] 구문을 그대로 실행 exec(env_configs, globals()) return globals().get('GEMINI_API_CONFIGS', []) except Exception as e: print(f"⚠️ GEMINI_API_CONFIGS 환경변수 실행 오류: {e}") return [] return [] # 환경변수에서 로드하거나 기본값 사용 loaded_configs = load_api_configs_from_env() GEMINI_API_CONFIGS = loaded_configs if loaded_configs else [ "", "", "", "", "", "", "", "", # 필요한 만큼 API 키를 추가하세요 ] # 현재 사용 중인 API 키 인덱스 current_gemini_api_index = 0 gemini_lock = threading.Lock() # Gemini 모델 캐시 _gemini_models = {} def initialize_gemini_api_configs(): """Gemini API 설정을 초기화하고 랜덤하게 정렬""" global GEMINI_API_CONFIGS # 유효한 키만 필터링 valid_keys = [] for key in GEMINI_API_CONFIGS: if key and key.strip() and len(key.strip()) > 30 and not key.startswith("AIzaSyExample"): valid_keys.append(key.strip()) if valid_keys: GEMINI_API_CONFIGS = valid_keys # API 키를 랜덤하게 섞기 random.shuffle(GEMINI_API_CONFIGS) print(f"✅ Gemini API 설정 초기화 완료: {len(GEMINI_API_CONFIGS)}개 키 준비") # 키 정보 출력 (보안상 일부만 표시) for i, key in enumerate(GEMINI_API_CONFIGS): print(f" 키 {i+1}: {key[:8]}***{key[-4:]}") else: print("⚠️ 유효한 Gemini API 키를 찾을 수 없습니다.") print("💡 GEMINI_API_CONFIGS 환경변수를 설정해주세요:") print(" 예시: GEMINI_API_CONFIGS=key1,key2,key3,key4") def get_next_gemini_api_key(): """순차적으로 다음 Gemini API 키를 반환 (스레드 안전)""" global current_gemini_api_index if not GEMINI_API_CONFIGS: print("❌ 설정된 Gemini API 키가 없습니다.") return None with gemini_lock: api_key = GEMINI_API_CONFIGS[current_gemini_api_index] current_gemini_api_index = (current_gemini_api_index + 1) % len(GEMINI_API_CONFIGS) print(f"🔑 Gemini API 키 선택: {api_key[:8]}***{api_key[-4:]} (키 {current_gemini_api_index}/{len(GEMINI_API_CONFIGS)})") return api_key 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(GEMINI_API_CONFIGS) if GEMINI_API_CONFIGS else 5 for attempt in range(max_attempts): try: api_key = get_next_gemini_api_key() if api_key and test_api_key(api_key): logger.info(f"작동하는 API 키를 찾았습니다. (시도 {attempt + 1}회)") return api_key else: 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)}") return f"오류 발생: {str(e)}" def create_interface(): # FontAwesome 아이콘 포함 fontawesome = """ """ # 다크모드 적용 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('