File size: 10,137 Bytes
9089433
8577247
 
 
 
7a80146
9089433
 
8577247
 
 
 
 
 
 
 
 
9089433
 
8577247
 
 
f6b7ae2
 
 
 
 
 
 
8577247
 
 
9089433
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8577247
9089433
 
8577247
9089433
 
f6b7ae2
 
8577247
9089433
8577247
 
 
9089433
8577247
9089433
8577247
 
9089433
8577247
 
 
9089433
 
8577247
 
9089433
8577247
 
9089433
 
 
 
 
 
 
 
 
 
 
8577247
9089433
 
 
8577247
 
9089433
 
8577247
 
 
9089433
8577247
 
 
 
 
 
 
 
 
9089433
8577247
9089433
8577247
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9089433
 
 
 
 
8577247
9089433
8577247
9089433
 
 
3a03ca4
9089433
 
 
 
 
 
 
 
3d9d9e7
3a03ca4
8577247
 
 
 
 
 
 
 
 
 
9089433
 
 
8577247
 
 
 
 
 
 
 
 
9089433
f6b7ae2
 
 
 
 
 
 
9089433
 
 
 
8577247
 
9089433
 
 
 
8577247
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f6b7ae2
 
8577247
 
 
 
 
 
 
 
 
 
 
f6b7ae2
 
8577247
 
 
 
 
 
9089433
8577247
 
 
 
 
 
f6b7ae2
 
 
8577247
f6b7ae2
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
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)