Spaces:
Runtime error
Runtime error
| import os | |
| import time | |
| from collections import defaultdict, deque | |
| from typing import Tuple | |
| import gradio as gr | |
| from huggingface_hub import InferenceClient | |
| # ========================= | |
| # CONFIG & AUTH | |
| # ========================= | |
| # Lấy token từ biến môi trường (khuyên dùng: add tại Settings → Repository secrets) | |
| # Nếu cần hardcode để test nhanh (KHÔNG an toàn repo public): | |
| # HF_TOKEN = "hf_XXXXXXXXXXXXXXXXXXXXXXXX" # <--- DÁN TOKEN Ở ĐÂY (không khuyến nghị) | |
| HF_TOKEN = os.getenv("HF_TOKEN", None) | |
| # Model mặc định cho UI (có thể đổi qua dropdown) | |
| DEFAULT_MODEL = os.getenv("HYMT_MODEL", "tencent/Hunyuan-MT-7B-fp8") | |
| # Model cố định cho API | |
| FIXED_MODEL = "tencent/Hunyuan-MT-7B-fp8" | |
| # Giới hạn đồng thời theo event (thay cho concurrency_count cũ) | |
| UI_CONCURRENCY_LIMIT = int(os.getenv("UI_CONCURRENCY_LIMIT", "2")) # nút Dịch | |
| API_CONCURRENCY_LIMIT = int(os.getenv("API_CONCURRENCY_LIMIT", "2")) # endpoint API | |
| # Số worker tổng (tuỳ chọn): ảnh hưởng thread pool của server | |
| LAUNCH_MAX_THREADS = int(os.getenv("LAUNCH_MAX_THREADS", "40")) | |
| # ========================= | |
| # NGÔN NGỮ & PROMPT | |
| # ========================= | |
| LANGS = [ | |
| ("Chinese (简体中文)", "zh"), | |
| ("Traditional Chinese (繁體中文)", "zh-Hant"), | |
| ("Cantonese (粤语)", "yue"), | |
| ("English (English)", "en"), | |
| ("Vietnamese (Tiếng Việt)", "vi"), | |
| ("Japanese (日本語)", "ja"), | |
| ("Korean (한국어)", "ko"), | |
| ("Thai (ไทย)", "th"), | |
| ("French (Français)", "fr"), | |
| ("Spanish (Español)", "es"), | |
| ("Portuguese (Português)", "pt"), | |
| ("Italian (Italiano)", "it"), | |
| ("German (Deutsch)", "de"), | |
| ("Russian (Русский)", "ru"), | |
| ("Arabic (العربية)", "ar"), | |
| ("Turkish (Türkçe)", "tr"), | |
| ("Indonesian (Bahasa Indonesia)", "id"), | |
| ("Malay (Bahasa Melayu)", "ms"), | |
| ("Filipino (Filipino)", "tl"), | |
| ("Hindi (हिन्दी)", "hi"), | |
| ("Polish (Polski)", "pl"), | |
| ("Czech (Čeština)", "cs"), | |
| ("Dutch (Nederlands)", "nl"), | |
| ("Khmer (ភាសាខ្មែរ)", "km"), | |
| ("Burmese (မြန်မာ)", "my"), | |
| ("Persian (فارسی)", "fa"), | |
| ("Gujarati (ગુજરાતી)", "gu"), | |
| ("Urdu (اردو)", "ur"), | |
| ("Telugu (తెలుగు)", "te"), | |
| ("Marathi (मराठी)", "mr"), | |
| ("Hebrew (עברית)", "he"), | |
| ("Bengali (বাংলা)", "bn"), | |
| ("Tamil (தமிழ்)", "ta"), | |
| ("Ukrainian (Українська)", "uk"), | |
| ("Tibetan (བོད་ཡིག)", "bo"), | |
| ("Kazakh (Қазақша)", "kk"), | |
| ("Mongolian (Монгол)", "mn"), | |
| ("Uyghur (ئۇيغۇرچە)", "ug"), | |
| ] | |
| LABEL2CODE = {label: code for label, code in LANGS} | |
| ZH_CODES = {"zh", "zh-Hant", "yue"} | |
| def build_prompt(src_lang: str, tgt_lang: str, text: str) -> str: | |
| """ | |
| Prompt template: | |
| - Nếu có tiếng Trung (zh/zh-Hant/yue) ở nguồn hoặc đích -> template tiếng Trung | |
| - Nếu không -> template tiếng Anh | |
| """ | |
| txt = (text or "").strip() | |
| if not txt: | |
| return "" | |
| if src_lang in ZH_CODES or tgt_lang in ZH_CODES: | |
| return f"把下面的文本翻译成{tgt_lang},不要额外解释。\n\n{txt}" | |
| else: | |
| return f"Translate the following segment into {tgt_lang}, without additional explanation.\n\n{txt}" | |
| # ========================= | |
| # INFERENCE HELPER | |
| # ========================= | |
| def call_hf_inference(model: str, prompt: str) -> str: | |
| """ | |
| Gọi Hugging Face Serverless Inference API (text-generation). | |
| Không cần GPU, phù hợp Space free. | |
| """ | |
| if not prompt: | |
| return "" | |
| client = InferenceClient(token=HF_TOKEN) | |
| try: | |
| out = client.text_generation( | |
| model=model, | |
| prompt=prompt, | |
| max_new_tokens=512, | |
| temperature=0.7, | |
| top_p=0.6, | |
| repetition_penalty=1.05, | |
| stream=False, | |
| ) | |
| return (out or "").strip() | |
| except Exception as e: | |
| return f"[Lỗi] Không thể gọi Inference API: {e}" | |
| def translate(text: str, src_code: str, tgt_code: str, model_choice: str) -> str: | |
| if not text or not text.strip(): | |
| return "Vui lòng nhập nội dung cần dịch." | |
| if not src_code or not tgt_code: | |
| return "Thiếu mã ngôn ngữ nguồn/đích." | |
| if src_code == tgt_code: | |
| return text.strip() | |
| prompt = build_prompt(src_code, tgt_code, text) | |
| return call_hf_inference(model_choice, prompt) | |
| # ========================= | |
| # RATE LIMIT THEO IP (IN-MEMORY) | |
| # ========================= | |
| RATE_WINDOW_SEC = int(os.getenv("RATE_WINDOW_SEC", "60")) # ví dụ: 60 giây | |
| RATE_MAX_REQ = int(os.getenv("RATE_MAX_REQ", "10")) # ví dụ: 10 request / IP / 60s | |
| _ip_buckets: dict[str, deque] = defaultdict(deque) | |
| def _rate_limited(request: gr.Request) -> Tuple[bool, str]: | |
| """ | |
| Trả (ok, msg). ok=False nếu vượt ngưỡng. | |
| Lưu ý: in-memory -> reset khi container restart. | |
| """ | |
| try: | |
| ip = (request.client.host if request and request.client else "unknown") or "unknown" | |
| except Exception: | |
| ip = "unknown" | |
| now = time.time() | |
| dq = _ip_buckets[ip] | |
| # Bỏ các dấu thời gian ngoài cửa sổ | |
| while dq and (now - dq[0] > RATE_WINDOW_SEC): | |
| dq.popleft() | |
| if len(dq) >= RATE_MAX_REQ: | |
| wait_sec = max(1, int(RATE_WINDOW_SEC - (now - dq[0]))) | |
| return False, f"Bạn đã vượt giới hạn {RATE_MAX_REQ} yêu cầu / {RATE_WINDOW_SEC}s. Hãy thử lại sau ~{wait_sec}s." | |
| dq.append(now) | |
| return True, "" | |
| # ========================= | |
| # UI (GRADIO BLOCKS) | |
| # ========================= | |
| def build_ui() -> gr.Blocks: | |
| with gr.Blocks(title="Hunyuan-MT Translation (HF Inference API)", fill_height=True) as demo: | |
| gr.Markdown( | |
| """ | |
| # Tencent Hunyuan-MT (Serverless) | |
| Chạy trên **Hugging Face Space (CPU free)** bằng **Serverless Inference API**. | |
| - Chọn mô hình `tencent/Hunyuan-MT-7B-fp8` (khuyến nghị) hoặc `tencent/Hunyuan-MT-7B`. | |
| - Chọn ngôn ngữ nguồn/đích rồi bấm **Dịch**. | |
| > Mẹo: vào *Settings → Repository secrets* thêm `HF_TOKEN` để tăng hạn mức. | |
| """ | |
| ) | |
| with gr.Row(): | |
| model_choice = gr.Dropdown( | |
| choices=[ | |
| "tencent/Hunyuan-MT-7B-fp8", | |
| "tencent/Hunyuan-MT-7B", | |
| ], | |
| value=DEFAULT_MODEL, | |
| label="Model (Serverless)" | |
| ) | |
| with gr.Row(): | |
| src = gr.Dropdown( | |
| choices=[l for l, _ in LANGS], | |
| value="English (English)", | |
| label="Nguồn" | |
| ) | |
| tgt = gr.Dropdown( | |
| choices=[l for l, _ in LANGS], | |
| value="Vietnamese (Tiếng Việt)", | |
| label="Đích" | |
| ) | |
| inp = gr.Textbox(label="Nội dung cần dịch", lines=8, placeholder="Nhập văn bản…") | |
| out = gr.Textbox(label="Kết quả", lines=8) | |
| btn = gr.Button("Dịch", variant="primary") | |
| def _on_translate(text, src_label, tgt_label, model_id, request: gr.Request = None): | |
| ok, msg = _rate_limited(request) | |
| if not ok: | |
| return msg | |
| src_code = LABEL2CODE[src_label] | |
| tgt_code = LABEL2CODE[tgt_label] | |
| return translate(text, src_code, tgt_code, model_id) | |
| # ✅ Đặt concurrency_limit ngay trên event listener (chuẩn mới) | |
| btn.click( | |
| _on_translate, | |
| [inp, src, tgt, model_choice], | |
| [out], | |
| concurrency_limit=UI_CONCURRENCY_LIMIT | |
| ) | |
| gr.Markdown( | |
| """ | |
| #### Lưu ý | |
| - Demo gọi **Serverless Inference API** nên tốc độ phụ thuộc hạn mức. | |
| - Cần throughput cao hơn? Hãy cân nhắc GPU hoặc tự triển khai TGI/vLLM. | |
| """ | |
| ) | |
| return demo | |
| # ========================= | |
| # API CỐ ĐỊNH (MODEL KHÓA) | |
| # ========================= | |
| def api_translate_fixed(text: str, src_code: str, tgt_code: str, request: gr.Request = None) -> str: | |
| """ | |
| API cho website: nhận mã ngôn ngữ (vd: 'en', 'vi', 'zh', ...), | |
| dùng model cố định Hunyuan-MT-7B-fp8. | |
| """ | |
| ok, msg = _rate_limited(request) | |
| if not ok: | |
| return msg | |
| if not text or not text.strip(): | |
| return "" | |
| if not src_code or not tgt_code: | |
| return "Thiếu mã ngôn ngữ nguồn/đích." | |
| if src_code == tgt_code: | |
| return text.strip() | |
| prompt = build_prompt(src_code, tgt_code, text) | |
| return call_hf_inference(FIXED_MODEL, prompt) | |
| def build_api_interface() -> gr.Interface: | |
| """ | |
| Interface riêng để có endpoint REST. | |
| - /run/api_translate_fixed (nếu platform hỗ trợ) | |
| - Hoặc /run/predict với fn_index tương ứng | |
| """ | |
| return gr.Interface( | |
| fn=api_translate_fixed, | |
| inputs=[ | |
| gr.Textbox(label="text"), | |
| gr.Textbox(label="src_code"), | |
| gr.Textbox(label="tgt_code"), | |
| ], | |
| outputs=gr.Textbox(label="translation"), | |
| title="Hunyuan-MT Fixed API", | |
| description="POST JSON tới endpoint để nhận bản dịch. Model cố định: tencent/Hunyuan-MT-7B-fp8.", | |
| concurrency_limit=API_CONCURRENCY_LIMIT, # ✅ giới hạn đồng thời cho API | |
| ) | |
| # ========================= | |
| # MAIN | |
| # ========================= | |
| if __name__ == "__main__": | |
| ui_app = build_ui() | |
| api_iface = build_api_interface() | |
| # Gộp UI + API vào cùng server | |
| demo = gr.TabbedInterface([ui_app, api_iface], tab_names=["App", "API"]) | |
| # Hàng chờ: KHÔNG dùng concurrency_count nữa! | |
| QUEUE_MAX = int(os.getenv("GRADIO_QUEUE_MAX", "20")) # số job có thể chờ | |
| demo = demo.queue(max_size=QUEUE_MAX, status_update_rate=2) | |
| # Bật REST API; (tuỳ chọn) khống chế thread tổng bằng max_threads | |
| demo.launch(enable_api=True, max_threads=LAUNCH_MAX_THREADS) |