Spaces:
Running
Running
sync qt2 b453cb6
Browse filesSync local qt2 master b453cb66acaef810322ef4f1f6fe844b7c600585 and rebuilt hachimimt-local.zip.
- .gitignore +12 -5
- src/app.py +39 -24
- src/line_restore.py +11 -0
- src/progress_tracker.py +52 -19
- src/translator.py +71 -62
.gitignore
CHANGED
|
@@ -1,5 +1,12 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
|
| 20 |
-
def set_progress(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
with _lock:
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
|
|
|
| 25 |
|
| 26 |
|
| 27 |
-
def finish_progress(message: str) -> None:
|
| 28 |
with _lock:
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
|
|
|
| 32 |
|
| 33 |
|
| 34 |
-
def reset_progress(message: str = "Sẵn sàng.") -> None:
|
| 35 |
with _lock:
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
|
|
|
| 39 |
|
| 40 |
|
| 41 |
-
def snapshot() -> ProgressState:
|
| 42 |
with _lock:
|
|
|
|
| 43 |
return ProgressState(
|
| 44 |
-
pct=
|
| 45 |
-
message=
|
| 46 |
-
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 |
-
|
| 675 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 676 |
|
| 677 |
|
| 678 |
def _pytorch_ready(path: Path) -> bool:
|
| 679 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|