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 = """ """ 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('