ngocdang83 commited on
Commit
ef7c8cd
·
verified ·
1 Parent(s): e692cf1

sync qt2 b453cb6

Browse files

Sync local qt2 master b453cb66acaef810322ef4f1f6fe844b7c600585 and rebuilt hachimimt-local.zip.

Files changed (5) hide show
  1. .gitignore +12 -5
  2. src/app.py +39 -24
  3. src/line_restore.py +11 -0
  4. src/progress_tracker.py +52 -19
  5. src/translator.py +71 -62
.gitignore CHANGED
@@ -1,5 +1,12 @@
1
- models/
2
- exports/
3
- __pycache__/
4
- *.pyc
5
- .hachimimt.pid
 
 
 
 
 
 
 
 
1
+ .hachimimt.pid
2
+ .env.txt
3
+ models/
4
+ exports/
5
+ feedback/
6
+ terminals/
7
+ __pycache__/
8
+ *.pyc
9
+ .env
10
+ .gitnexus
11
+ cship/
12
+ hachimimt-local.zip
src/app.py CHANGED
@@ -956,10 +956,16 @@ def render_progress_html(pct: float, message: str, running: bool) -> str:
956
  """
957
 
958
 
959
- def poll_progress_ui(active: bool) -> str:
 
 
 
 
 
 
960
  if not active:
961
  return gr.update()
962
- state = snapshot()
963
  return render_progress_html(state.pct, state.message, state.running)
964
 
965
 
@@ -1313,8 +1319,8 @@ def _build_results(
1313
  )
1314
 
1315
 
1316
- def _progress_stream_update(active: bool = True) -> tuple:
1317
- state = snapshot()
1318
  return (
1319
  render_progress_html(state.pct, state.message, state.running),
1320
  active,
@@ -1322,29 +1328,29 @@ def _progress_stream_update(active: bool = True) -> tuple:
1322
  )
1323
 
1324
 
1325
- def _progress_stream_final(results: tuple) -> tuple:
1326
- state = snapshot()
1327
  return (render_progress_html(state.pct, state.message, state.running), False, *results)
1328
 
1329
 
1330
- def prepare_progress_ui(message: str) -> tuple[str, bool]:
1331
- set_progress(0, message)
1332
  return render_progress_html(0, message, True), True
1333
 
1334
 
1335
- def prepare_text_progress_ui(source: str) -> tuple[str, bool]:
1336
  if not source.strip():
1337
  raise gr.Error("Nhập văn bản tiếng Trung cần dịch.")
1338
- return prepare_progress_ui("Đang chuẩn bị dịch văn bản...")
1339
 
1340
 
1341
- def prepare_file_progress_ui(file_obj) -> tuple[str, bool]:
1342
  if file_obj is None:
1343
  raise gr.Error("Chọn file .txt cần dịch.")
1344
  path = Path(file_obj)
1345
  if path.suffix.lower() != ".txt":
1346
  raise gr.Error("Chỉ hỗ trợ file .txt")
1347
- return prepare_progress_ui(f"Đang đọc file {path.name}...")
1348
 
1349
 
1350
  def _layout_lines(text: str) -> list[str]:
@@ -1499,6 +1505,7 @@ def _translate_run(
1499
  filename_stem: str,
1500
  summary_prefix: str,
1501
  source_kind: str,
 
1502
  ) -> Iterator[tuple]:
1503
  try:
1504
  source, space_cap_notice = cap_input_for_space(source)
@@ -1515,14 +1522,14 @@ def _translate_run(
1515
  else:
1516
  label = MODELS[model_key].label if model_key in MODELS else model_key
1517
  load_msg = f"Đang tải model {label} từ Hugging Face (lần đầu, vui lòng đợi)..."
1518
- set_progress(0, load_msg)
1519
- yield _progress_stream_update()
1520
 
1521
  resolve_batch_size(auto_batch, manual_batch)
1522
  status = ensure_model(model_key, backend, beam_size)
1523
 
1524
- set_progress(2, f"{normalize_msg} Đang chia chunk...")
1525
- yield _progress_stream_update()
1526
 
1527
  rows: list[tuple[int, str, str]] = []
1528
  full_text = ""
@@ -1551,9 +1558,9 @@ def _translate_run(
1551
 
1552
  pct = round(done / max(total, 1) * 100, 1)
1553
  detail = f"{message} ({pct}%)"
1554
- set_progress(pct, detail)
1555
  last_progress_update = now
1556
- yield _progress_stream_update()
1557
 
1558
  translate_seconds = time.perf_counter() - translate_start
1559
 
@@ -1612,7 +1619,10 @@ def _translate_run(
1612
  warning_note = f" Hậu kỳ lỗi, đã giữ bản dịch thô: {postprocess_warning}." if postprocess_warning else ""
1613
  summary = f"{summary_prefix} **{chunk_count}** chunk · {time_note}. {normalize_msg}{fallback_note}{route_note}{honorific_note}{pronoun_note}{warning_note}{space_cap_notice}"
1614
  status = f"{status} · {time_note}"
1615
- finish_progress(f"Hoàn tất — {chunk_count} chunk trong {duration} (100%)")
 
 
 
1616
 
1617
  feedback_context = _build_feedback_context(
1618
  rows=rows,
@@ -1626,11 +1636,12 @@ def _translate_run(
1626
  pronoun_v9=pronoun_harmonizer_v9,
1627
  )
1628
  yield _progress_stream_final(
1629
- _build_results(rows, full_text, status, summary, download_path, feedback_context)
 
1630
  )
1631
  except Exception as exc:
1632
- reset_progress(f"Lỗi: {_exception_message(exc)}")
1633
- yield _progress_stream_update(active=False)
1634
  raise
1635
 
1636
 
@@ -1646,6 +1657,7 @@ def translate_text_ui(
1646
  pronoun_harmonizer_v9: bool,
1647
  auto_batch: bool,
1648
  manual_batch: float,
 
1649
  ) -> Iterator[tuple]:
1650
  if not source.strip():
1651
  raise gr.Error("Nhập văn bản tiếng Trung cần dịch.")
@@ -1665,6 +1677,7 @@ def translate_text_ui(
1665
  filename_stem="hachimimt",
1666
  summary_prefix="Đã dịch",
1667
  source_kind="text",
 
1668
  )
1669
 
1670
 
@@ -1680,6 +1693,7 @@ def translate_file_ui(
1680
  pronoun_harmonizer_v9: bool,
1681
  auto_batch: bool,
1682
  manual_batch: float,
 
1683
  ) -> Iterator[tuple]:
1684
  if file_obj is None:
1685
  raise gr.Error("Chọn file .txt cần dịch.")
@@ -1707,6 +1721,7 @@ def translate_file_ui(
1707
  filename_stem=path.stem,
1708
  summary_prefix=f"Đã dịch từ `{path.name}` —",
1709
  source_kind="file",
 
1710
  )
1711
 
1712
 
@@ -1978,8 +1993,8 @@ mục `hachimimt`, chạy `setup.bat` (cài thư viện + tải model mặc đ
1978
 
1979
  - **GPU NVIDIA**: app mặc định chạy CPU; có nút **“Cài torch để bật GPU”** ngay trong
1980
  app (tải ~2–3 GB, một lần) → nhanh hơn nhiều lần với văn bản dài.
1981
- - Các model tự tải từ Hugging Face lần đầu, sau đó chạy **offline**.
1982
- - Bản local mở khoá: chọn 8 model, dịch file `.txt`, không giới hạn CPU như Space.""",
1983
  elem_classes=["info-card"],
1984
  )
1985
 
 
956
  """
957
 
958
 
959
+ def _session_key(request: gr.Request | None) -> str | None:
960
+ """session_hash của phiên Gradio; None (không lấy được) rơi về key mặc định
961
+ trong progress_tracker — tức hành vi global cũ, không tệ hơn trước."""
962
+ return getattr(request, "session_hash", None) if request is not None else None
963
+
964
+
965
+ def poll_progress_ui(active: bool, request: gr.Request | None = None) -> str:
966
  if not active:
967
  return gr.update()
968
+ state = snapshot(session=_session_key(request))
969
  return render_progress_html(state.pct, state.message, state.running)
970
 
971
 
 
1319
  )
1320
 
1321
 
1322
+ def _progress_stream_update(active: bool = True, *, session: str | None = None) -> tuple:
1323
+ state = snapshot(session=session)
1324
  return (
1325
  render_progress_html(state.pct, state.message, state.running),
1326
  active,
 
1328
  )
1329
 
1330
 
1331
+ def _progress_stream_final(results: tuple, *, session: str | None = None) -> tuple:
1332
+ state = snapshot(session=session)
1333
  return (render_progress_html(state.pct, state.message, state.running), False, *results)
1334
 
1335
 
1336
+ def prepare_progress_ui(message: str, *, session: str | None = None) -> tuple[str, bool]:
1337
+ set_progress(0, message, session=session)
1338
  return render_progress_html(0, message, True), True
1339
 
1340
 
1341
+ def prepare_text_progress_ui(source: str, request: gr.Request | None = None) -> tuple[str, bool]:
1342
  if not source.strip():
1343
  raise gr.Error("Nhập văn bản tiếng Trung cần dịch.")
1344
+ return prepare_progress_ui("Đang chuẩn bị dịch văn bản...", session=_session_key(request))
1345
 
1346
 
1347
+ def prepare_file_progress_ui(file_obj, request: gr.Request | None = None) -> tuple[str, bool]:
1348
  if file_obj is None:
1349
  raise gr.Error("Chọn file .txt cần dịch.")
1350
  path = Path(file_obj)
1351
  if path.suffix.lower() != ".txt":
1352
  raise gr.Error("Chỉ hỗ trợ file .txt")
1353
+ return prepare_progress_ui(f"Đang đọc file {path.name}...", session=_session_key(request))
1354
 
1355
 
1356
  def _layout_lines(text: str) -> list[str]:
 
1505
  filename_stem: str,
1506
  summary_prefix: str,
1507
  source_kind: str,
1508
+ session: str | None = None,
1509
  ) -> Iterator[tuple]:
1510
  try:
1511
  source, space_cap_notice = cap_input_for_space(source)
 
1522
  else:
1523
  label = MODELS[model_key].label if model_key in MODELS else model_key
1524
  load_msg = f"Đang tải model {label} từ Hugging Face (lần đầu, vui lòng đợi)..."
1525
+ set_progress(0, load_msg, session=session)
1526
+ yield _progress_stream_update(session=session)
1527
 
1528
  resolve_batch_size(auto_batch, manual_batch)
1529
  status = ensure_model(model_key, backend, beam_size)
1530
 
1531
+ set_progress(2, f"{normalize_msg} Đang chia chunk...", session=session)
1532
+ yield _progress_stream_update(session=session)
1533
 
1534
  rows: list[tuple[int, str, str]] = []
1535
  full_text = ""
 
1558
 
1559
  pct = round(done / max(total, 1) * 100, 1)
1560
  detail = f"{message} ({pct}%)"
1561
+ set_progress(pct, detail, session=session)
1562
  last_progress_update = now
1563
+ yield _progress_stream_update(session=session)
1564
 
1565
  translate_seconds = time.perf_counter() - translate_start
1566
 
 
1619
  warning_note = f" Hậu kỳ lỗi, đã giữ bản dịch thô: {postprocess_warning}." if postprocess_warning else ""
1620
  summary = f"{summary_prefix} **{chunk_count}** chunk · {time_note}. {normalize_msg}{fallback_note}{route_note}{honorific_note}{pronoun_note}{warning_note}{space_cap_notice}"
1621
  status = f"{status} · {time_note}"
1622
+ finish_progress(
1623
+ f"Hoàn tất — {chunk_count} chunk trong {duration} (100%)",
1624
+ session=session,
1625
+ )
1626
 
1627
  feedback_context = _build_feedback_context(
1628
  rows=rows,
 
1636
  pronoun_v9=pronoun_harmonizer_v9,
1637
  )
1638
  yield _progress_stream_final(
1639
+ _build_results(rows, full_text, status, summary, download_path, feedback_context),
1640
+ session=session,
1641
  )
1642
  except Exception as exc:
1643
+ reset_progress(f"Lỗi: {_exception_message(exc)}", session=session)
1644
+ yield _progress_stream_update(active=False, session=session)
1645
  raise
1646
 
1647
 
 
1657
  pronoun_harmonizer_v9: bool,
1658
  auto_batch: bool,
1659
  manual_batch: float,
1660
+ request: gr.Request | None = None,
1661
  ) -> Iterator[tuple]:
1662
  if not source.strip():
1663
  raise gr.Error("Nhập văn bản tiếng Trung cần dịch.")
 
1677
  filename_stem="hachimimt",
1678
  summary_prefix="Đã dịch",
1679
  source_kind="text",
1680
+ session=_session_key(request),
1681
  )
1682
 
1683
 
 
1693
  pronoun_harmonizer_v9: bool,
1694
  auto_batch: bool,
1695
  manual_batch: float,
1696
+ request: gr.Request | None = None,
1697
  ) -> Iterator[tuple]:
1698
  if file_obj is None:
1699
  raise gr.Error("Chọn file .txt cần dịch.")
 
1721
  filename_stem=path.stem,
1722
  summary_prefix=f"Đã dịch từ `{path.name}` —",
1723
  source_kind="file",
1724
+ session=_session_key(request),
1725
  )
1726
 
1727
 
 
1993
 
1994
  - **GPU NVIDIA**: app mặc định chạy CPU; có nút **“Cài torch để bật GPU”** ngay trong
1995
  app (tải ~2–3 GB, một lần) → nhanh hơn nhiều lần với văn bản dài.
1996
+ - Các model tự tải từ Hugging Face lần đầu, sau đó chạy **offline**.
1997
+ - Bản local mở khoá: chọn 8 model, dịch file `.txt`, không giới hạn CPU như Space.""",
1998
  elem_classes=["info-card"],
1999
  )
2000
 
src/line_restore.py CHANGED
@@ -164,6 +164,12 @@ def restore_line_breaks_by_dp(
164
 
165
  n = len(nonblank)
166
  m = len(target_units)
 
 
 
 
 
 
167
  source_chars = [max(len(line), 1) for line in nonblank]
168
  target_chars = [max(len(unit), 1) for unit in target_units]
169
  total_source = max(sum(source_chars), 1)
@@ -426,6 +432,11 @@ def assemble_paragraph_output(
426
  if not line_indices:
427
  continue
428
 
 
 
 
 
 
429
  if all(line_index in fallback_line_translations for line_index in line_indices):
430
  for line_index in line_indices:
431
  line_parts[line_index].append(fallback_line_translations[line_index].strip())
 
164
 
165
  n = len(nonblank)
166
  m = len(target_units)
167
+ # DP là O(n·m²) — chỉ rẻ ở quy mô chunk (CT2 plan-path: n ≤ ~12). Đường
168
+ # legacy (plan=None, backend PyTorch) có thể đưa cả tài liệu vào đây:
169
+ # 400 dòng/1200 câu đo được ~26s, 2000 dòng ~50 phút. Vượt ngân sách →
170
+ # trả None để caller dùng sentence-proportional (tuyến tính).
171
+ if n * m * m > 100_000_000:
172
+ return None, "dp-too-large"
173
  source_chars = [max(len(line), 1) for line in nonblank]
174
  target_chars = [max(len(unit), 1) for unit in target_units]
175
  total_source = max(sum(source_chars), 1)
 
432
  if not line_indices:
433
  continue
434
 
435
+ # Invariant với translator: fallback được chọn theo NGUYÊN CHUNK
436
+ # (paragraph_chunk_fallback_indices trả chunk-index, mọi line của
437
+ # chunk đó đều được dịch lại) nên all() ở đây nhận trọn chunk hoặc
438
+ # không gì cả. Nếu đổi sang fallback theo dòng lẻ, phải chuyển sang
439
+ # xử lý per-line, không thì các dòng partial bị bỏ qua âm thầm.
440
  if all(line_index in fallback_line_translations for line_index in line_indices):
441
  for line_index in line_indices:
442
  line_parts[line_index].append(fallback_line_translations[line_index].strip())
src/progress_tracker.py CHANGED
@@ -1,8 +1,15 @@
1
- """Shared progress state — polled by UI timer during long translation."""
 
 
 
 
 
 
2
 
3
  from __future__ import annotations
4
 
5
  import threading
 
6
  from dataclasses import dataclass
7
 
8
 
@@ -13,35 +20,61 @@ class ProgressState:
13
  running: bool = False
14
 
15
 
 
 
 
16
  _lock = threading.Lock()
17
- _state = ProgressState()
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
 
20
- def set_progress(pct: float, message: str, *, running: bool = True) -> None:
 
 
 
 
 
 
21
  with _lock:
22
- _state.pct = max(0.0, min(100.0, float(pct)))
23
- _state.message = message
24
- _state.running = running
 
25
 
26
 
27
- def finish_progress(message: str) -> None:
28
  with _lock:
29
- _state.pct = 100.0
30
- _state.message = message
31
- _state.running = False
 
32
 
33
 
34
- def reset_progress(message: str = "Sẵn sàng.") -> None:
35
  with _lock:
36
- _state.pct = 0.0
37
- _state.message = message
38
- _state.running = False
 
39
 
40
 
41
- def snapshot() -> ProgressState:
42
  with _lock:
 
43
  return ProgressState(
44
- pct=_state.pct,
45
- message=_state.message,
46
- running=_state.running,
47
- )
 
1
+ """Shared progress state — polled by UI timer during long translation.
2
+
3
+ Per-session: mỗi phiên Gradio (tab/trình duyệt, key theo request.session_hash)
4
+ có ProgressState riêng — nhiều người dùng đồng thời (HF Space) không đè thanh
5
+ tiến trình của nhau. Handler không lấy được session (None) rơi về key mặc định,
6
+ tức hành vi global cũ — an toàn cho single-user local và mọi đường gọi cũ.
7
+ """
8
 
9
  from __future__ import annotations
10
 
11
  import threading
12
+ from collections import OrderedDict
13
  from dataclasses import dataclass
14
 
15
 
 
20
  running: bool = False
21
 
22
 
23
+ _DEFAULT_KEY = "__default__"
24
+ _MAX_SESSIONS = 256 # LRU cap — Space chạy dài ngày không tích state vô hạn
25
+
26
  _lock = threading.Lock()
27
+ _states: OrderedDict[str, ProgressState] = OrderedDict()
28
+
29
+
30
+ def _state_for(session: str | None) -> ProgressState:
31
+ """Lấy state của session, tạo mới nếu chưa có. Phải gọi TRONG _lock."""
32
+ key = session or _DEFAULT_KEY
33
+ state = _states.get(key)
34
+ if state is None:
35
+ state = ProgressState()
36
+ _states[key] = state
37
+ _states.move_to_end(key)
38
+ while len(_states) > _MAX_SESSIONS:
39
+ _states.popitem(last=False)
40
+ return state
41
 
42
 
43
+ def set_progress(
44
+ pct: float,
45
+ message: str,
46
+ *,
47
+ running: bool = True,
48
+ session: str | None = None,
49
+ ) -> None:
50
  with _lock:
51
+ state = _state_for(session)
52
+ state.pct = max(0.0, min(100.0, float(pct)))
53
+ state.message = message
54
+ state.running = running
55
 
56
 
57
+ def finish_progress(message: str, *, session: str | None = None) -> None:
58
  with _lock:
59
+ state = _state_for(session)
60
+ state.pct = 100.0
61
+ state.message = message
62
+ state.running = False
63
 
64
 
65
+ def reset_progress(message: str = "Sẵn sàng.", *, session: str | None = None) -> None:
66
  with _lock:
67
+ state = _state_for(session)
68
+ state.pct = 0.0
69
+ state.message = message
70
+ state.running = False
71
 
72
 
73
+ def snapshot(session: str | None = None) -> ProgressState:
74
  with _lock:
75
+ state = _state_for(session)
76
  return ProgressState(
77
+ pct=state.pct,
78
+ message=state.message,
79
+ running=state.running,
80
+ )
src/translator.py CHANGED
@@ -75,10 +75,10 @@ class ModelConfig:
75
 
76
 
77
  MODELS: dict[str, ModelConfig] = {
78
- "HachimiMT-60": ModelConfig(
79
- label="HachimiMT-60",
80
- model_id="ngocdang83/HachimiMT-60-zh-vi",
81
- use_marian_class=True,
82
  generate_kwargs={
83
  "max_new_tokens": 300,
84
  # BỎ no_repeat_ngram_size=2 (đo 2026-06-26, entity-drift probe): cùng
@@ -95,30 +95,30 @@ MODELS: dict[str, ModelConfig] = {
95
  # cap = 1 chunk; chỉ dòng rất dài bị char-split nhỏ hơn, tự ghép lại 1 dòng).
96
  ct2_max_input_tokens=160,
97
  ct2_max_output_tokens=300,
98
- default_beam=2,
99
- ct2_size_mb=57,
100
- ),
101
- "HachimiMT-60-QT": ModelConfig(
102
- label="HachimiMT-60-QT",
103
- model_id="ngocdang83/HachimiMT-60-QT",
104
- use_marian_class=True,
105
- generate_kwargs={
106
- "max_new_tokens": 300,
107
- # QT-register variant: same 60M Marian family as HachimiMT-60, but
108
- # targets are normalized toward ta/nguoi/han/nang. Keep the 60M
109
- # decode profile and DO NOT add no_repeat_ngram_size: 711d70e showed
110
- # no_repeat causes duplicate-name entity drift. repetition_penalty
111
- # was isolated as safe there.
112
- "repetition_penalty": 1.2,
113
- },
114
- ct2_max_input_tokens=160,
115
- ct2_max_output_tokens=300,
116
- default_beam=2,
117
- ct2_size_mb=58,
118
- ),
119
- "HachimiMT-30": ModelConfig(
120
- label="HachimiMT-30",
121
- model_id="ngocdang83/HachimiMT-30-zh-vi",
122
  use_marian_class=False,
123
  generate_kwargs={
124
  "max_length": 512,
@@ -157,11 +157,11 @@ MODELS: dict[str, ModelConfig] = {
157
  ct2_size_mb=58,
158
  ct2_subdir="ct2-int8", # repo này dùng tên thư mục CT2 khác
159
  ),
160
- "MoxhiMT-30": ModelConfig(
161
- label="MoxhiMT-30",
162
- model_id="DanVP/MoxhiMT-30",
163
- use_marian_class=True,
164
- generate_kwargs={
165
  "max_new_tokens": 300,
166
  # BỎ no_repeat_ngram_size (đo 2026-06-26, entity-drift probe 72 câu):
167
  # =2 CẤM CỨNG tên 2-token lặp (vd [Mặc][Hoạ]) → decoder buộc viết lại
@@ -178,32 +178,32 @@ MODELS: dict[str, ModelConfig] = {
178
  # source cap short enough to split long entity-heavy paragraphs.
179
  ct2_max_input_tokens=160,
180
  ct2_max_output_tokens=512,
181
- default_beam=2,
182
- ct2_size_mb=38,
183
- ),
184
- "MoxhiMT-30-QT": ModelConfig(
185
- label="MoxhiMT-30-QT",
186
- model_id="DanVP/MoxhiMT-30-QT",
187
- use_marian_class=True,
188
- generate_kwargs={
189
- "max_new_tokens": 300,
190
- # QT-register variant: same 37M Marian family as MoxhiMT-30, but
191
- # targets are normalized toward ta/nguoi/han/nang. Keep the short
192
- # 30M input cap and DO NOT add no_repeat_ngram_size: 711d70e showed
193
- # no_repeat causes duplicate-name entity drift. repetition_penalty
194
- # was isolated as safe there. Default beam stays 1 because this model
195
- # is meant as a simple stable-pronoun option.
196
- "repetition_penalty": 1.2,
197
- },
198
- ct2_max_input_tokens=160,
199
- ct2_max_output_tokens=512,
200
- default_beam=1,
201
- ct2_size_mb=38,
202
- ),
203
- "HirashibaMT-Medium": ModelConfig(
204
- label="HirashibaMT-Medium",
205
- model_id="Moleys/hirashiba-mt-medium",
206
- use_marian_class=True,
207
  generate_kwargs={
208
  "max_new_tokens": 256,
209
  },
@@ -671,12 +671,21 @@ def model_local_dir(config: ModelConfig) -> Path:
671
 
672
 
673
  def _ct2_ready(path: Path, ct2_subdir: str = DEFAULT_CT2_SUBDIR) -> bool:
674
- ct2_path = path / ct2_subdir
675
- return ct2_path.is_dir() and any(ct2_path.iterdir())
 
 
 
 
 
676
 
677
 
678
  def _pytorch_ready(path: Path) -> bool:
679
- return any(path.glob("*.safetensors")) or any(path.glob("pytorch_model*.bin"))
 
 
 
 
680
 
681
 
682
  def _tokenizer_ready(path: Path) -> bool:
 
75
 
76
 
77
  MODELS: dict[str, ModelConfig] = {
78
+ "HachimiMT-60": ModelConfig(
79
+ label="HachimiMT-60",
80
+ model_id="ngocdang83/HachimiMT-60-zh-vi",
81
+ use_marian_class=True,
82
  generate_kwargs={
83
  "max_new_tokens": 300,
84
  # BỎ no_repeat_ngram_size=2 (đo 2026-06-26, entity-drift probe): cùng
 
95
  # cap = 1 chunk; chỉ dòng rất dài bị char-split nhỏ hơn, tự ghép lại 1 dòng).
96
  ct2_max_input_tokens=160,
97
  ct2_max_output_tokens=300,
98
+ default_beam=2,
99
+ ct2_size_mb=57,
100
+ ),
101
+ "HachimiMT-60-QT": ModelConfig(
102
+ label="HachimiMT-60-QT",
103
+ model_id="ngocdang83/HachimiMT-60-QT",
104
+ use_marian_class=True,
105
+ generate_kwargs={
106
+ "max_new_tokens": 300,
107
+ # QT-register variant: same 60M Marian family as HachimiMT-60, but
108
+ # targets are normalized toward ta/nguoi/han/nang. Keep the 60M
109
+ # decode profile and DO NOT add no_repeat_ngram_size: 711d70e showed
110
+ # no_repeat causes duplicate-name entity drift. repetition_penalty
111
+ # was isolated as safe there.
112
+ "repetition_penalty": 1.2,
113
+ },
114
+ ct2_max_input_tokens=160,
115
+ ct2_max_output_tokens=300,
116
+ default_beam=2,
117
+ ct2_size_mb=58,
118
+ ),
119
+ "HachimiMT-30": ModelConfig(
120
+ label="HachimiMT-30",
121
+ model_id="ngocdang83/HachimiMT-30-zh-vi",
122
  use_marian_class=False,
123
  generate_kwargs={
124
  "max_length": 512,
 
157
  ct2_size_mb=58,
158
  ct2_subdir="ct2-int8", # repo này dùng tên thư mục CT2 khác
159
  ),
160
+ "MoxhiMT-30": ModelConfig(
161
+ label="MoxhiMT-30",
162
+ model_id="DanVP/MoxhiMT-30",
163
+ use_marian_class=True,
164
+ generate_kwargs={
165
  "max_new_tokens": 300,
166
  # BỎ no_repeat_ngram_size (đo 2026-06-26, entity-drift probe 72 câu):
167
  # =2 CẤM CỨNG tên 2-token lặp (vd [Mặc][Hoạ]) → decoder buộc viết lại
 
178
  # source cap short enough to split long entity-heavy paragraphs.
179
  ct2_max_input_tokens=160,
180
  ct2_max_output_tokens=512,
181
+ default_beam=2,
182
+ ct2_size_mb=38,
183
+ ),
184
+ "MoxhiMT-30-QT": ModelConfig(
185
+ label="MoxhiMT-30-QT",
186
+ model_id="DanVP/MoxhiMT-30-QT",
187
+ use_marian_class=True,
188
+ generate_kwargs={
189
+ "max_new_tokens": 300,
190
+ # QT-register variant: same 37M Marian family as MoxhiMT-30, but
191
+ # targets are normalized toward ta/nguoi/han/nang. Keep the short
192
+ # 30M input cap and DO NOT add no_repeat_ngram_size: 711d70e showed
193
+ # no_repeat causes duplicate-name entity drift. repetition_penalty
194
+ # was isolated as safe there. Default beam stays 1 because this model
195
+ # is meant as a simple stable-pronoun option.
196
+ "repetition_penalty": 1.2,
197
+ },
198
+ ct2_max_input_tokens=160,
199
+ ct2_max_output_tokens=512,
200
+ default_beam=1,
201
+ ct2_size_mb=38,
202
+ ),
203
+ "HirashibaMT-Medium": ModelConfig(
204
+ label="HirashibaMT-Medium",
205
+ model_id="Moleys/hirashiba-mt-medium",
206
+ use_marian_class=True,
207
  generate_kwargs={
208
  "max_new_tokens": 256,
209
  },
 
671
 
672
 
673
  def _ct2_ready(path: Path, ct2_subdir: str = DEFAULT_CT2_SUBDIR) -> bool:
674
+ # model.bin >0 byte, không phải "thư mục có file bất kỳ": download dở
675
+ # (mất mạng/Ctrl-C) để lại file phụ → ready giả → CT2 crash khó hiểu.
676
+ model_bin = path / ct2_subdir / "model.bin"
677
+ try:
678
+ return model_bin.is_file() and model_bin.stat().st_size > 0
679
+ except OSError:
680
+ return False
681
 
682
 
683
  def _pytorch_ready(path: Path) -> bool:
684
+ try:
685
+ weight_files = [*path.glob("*.safetensors"), *path.glob("pytorch_model*.bin")]
686
+ return any(f.is_file() and f.stat().st_size > 0 for f in weight_files)
687
+ except OSError:
688
+ return False
689
 
690
 
691
  def _tokenizer_ready(path: Path) -> bool: