add a retranslate chinese function

This commit is contained in:
2026-10-06 14:47:22 +08:00
parent 4fd0fd36a4
commit b28f809c46
11 changed files with 448 additions and 9 deletions

View File

@@ -83,6 +83,7 @@ class AITutor:
course_title: str = "",
existing: dict[int, str] | None = None,
persist=None,
terms: list[tuple[str, str]] | None = None,
) -> dict[int, str]:
if not self.available:
raise AITutorError("AI translation is not configured.")
@@ -93,6 +94,7 @@ class AITutor:
if not ordered:
raise AITutorError("No sentence text was provided for translation.")
glossary = _normalize_translation_terms(terms)
translations = {
index: text.strip()
for index, text in (existing or {}).items()
@@ -123,6 +125,7 @@ class AITutor:
all_sentences=ordered,
existing=translations,
course_title=course_title,
terms=glossary,
)
if workers == 1:
@@ -153,6 +156,7 @@ class AITutor:
all_sentences=ordered,
existing=translations,
course_title=course_title,
terms=glossary,
force_split=len(batch) > 1,
)
)
@@ -160,6 +164,12 @@ class AITutor:
missing = [index for index, _ in pending if not translations.get(index, "").strip()]
if missing:
raise AITutorError("AI translation did not cover every sentence.")
if glossary:
source_by_index = {index: text for index, text in ordered}
translations = {
index: _apply_translation_terms(source_by_index.get(index, ""), zh_text, glossary)
for index, zh_text in translations.items()
}
return translations
def _translate_batch_resilient(
@@ -169,6 +179,7 @@ class AITutor:
all_sentences: list[tuple[int, str]],
existing: dict[int, str],
course_title: str,
terms: list[tuple[str, str]] | None = None,
force_split: bool = False,
) -> dict[int, str]:
if not batch:
@@ -179,6 +190,7 @@ class AITutor:
all_sentences=all_sentences,
existing=existing,
course_title=course_title,
terms=terms,
)
last_error: Exception | None = None
@@ -190,6 +202,7 @@ class AITutor:
all_sentences=all_sentences,
existing=existing,
course_title=course_title,
terms=terms,
)
except AITutorError as exc:
last_error = exc
@@ -206,6 +219,7 @@ class AITutor:
all_sentences=all_sentences,
existing=existing,
course_title=course_title,
terms=terms,
)
if last_error is not None:
raise last_error
@@ -218,6 +232,7 @@ class AITutor:
all_sentences: list[tuple[int, str]],
existing: dict[int, str],
course_title: str,
terms: list[tuple[str, str]] | None = None,
) -> dict[int, str]:
mid = max(1, len(batch) // 2)
merged: dict[int, str] = {}
@@ -228,6 +243,7 @@ class AITutor:
all_sentences=all_sentences,
existing=existing,
course_title=course_title,
terms=terms,
)
)
return merged
@@ -239,29 +255,36 @@ class AITutor:
all_sentences: list[tuple[int, str]],
existing: dict[int, str],
course_title: str,
terms: list[tuple[str, str]] | None = None,
) -> dict[int, str]:
context = course_title.strip() or "口语训练课程"
existing = dict(existing)
before, after = _neighbor_context(batch, all_sentences)
expected_indexes = [index for index, _ in batch]
glossary = _normalize_translation_terms(terms)
system_prompt = (
"你是专业的影视字幕翻译。请结合前后文语境,把指定编号的外语句子翻译成自然、口语化的简体中文。"
"必须一句对一句,不能合并、拆分、跳过或改写编号。"
"只输出 JSON 对象,键是句子编号,值是对应中文,不要输出解释或 markdown。"
)
if glossary:
system_prompt += "必须严格使用用户指定的专有名词译法,禁止自行音译成其他写法。"
user_parts = [
f"课程:{context}",
f"本次需要翻译 {len(batch)} 句,编号必须与输入完全一致。",
]
glossary = [
if glossary:
user_parts.append("专有名词译法(必须严格遵守,原文出现时译文必须使用指定中文):")
user_parts.extend(f"{source} => {zh}" for source, zh in glossary)
previous = [
(index, source, existing[index])
for index, source in before
if existing.get(index, "").strip()
]
if glossary:
if previous:
user_parts.append("已有译文(供用词一致,不要重复输出):")
user_parts.extend(
f"[{index}] {source} => {zh}" for index, source, zh in glossary[-6:]
f"[{index}] {source} => {zh}" for index, source, zh in previous[-6:]
)
elif before:
user_parts.append("前文(只供理解语境,不要翻译):")
@@ -284,7 +307,11 @@ class AITutor:
missing = [index for index in expected_indexes if not parsed.get(index, "").strip()]
if missing:
raise AITutorError("AI translation returned an unexpected response.")
return {index: parsed[index] for index in expected_indexes}
source_by_index = {index: text for index, text in batch}
return {
index: _apply_translation_terms(source_by_index.get(index, ""), parsed[index], glossary)
for index in expected_indexes
}
def _complete(
self,
@@ -512,3 +539,43 @@ def _translations_from_json(data: object, expected: list[int]) -> dict[int, str]
if len(values) == len(expected):
return values
return None
def _normalize_translation_terms(terms: list[tuple[str, str]] | None) -> list[tuple[str, str]]:
cleaned: list[tuple[str, str]] = []
seen: dict[str, int] = {}
for item in terms or []:
if not isinstance(item, (tuple, list)) or len(item) < 2:
continue
source = str(item[0] or "").strip()[:120]
zh = str(item[1] or "").strip()[:120]
if not source or not zh:
continue
key = source.casefold()
if key in seen:
cleaned[seen[key]] = (source, zh)
continue
seen[key] = len(cleaned)
cleaned.append((source, zh))
if len(cleaned) >= 50:
break
return cleaned
def _term_pattern(source: str) -> re.Pattern[str]:
escaped = re.escape(source)
if re.fullmatch(r"[A-Za-z][A-Za-z0-9'’.\- ]*", source):
return re.compile(rf"(?<![A-Za-z0-9]){escaped}(?![A-Za-z0-9])", re.IGNORECASE)
return re.compile(escaped)
def _apply_translation_terms(
source_text: str,
zh_text: str,
terms: list[tuple[str, str]] | None,
) -> str:
del source_text
result = zh_text
for source, zh in _normalize_translation_terms(terms):
result = _term_pattern(source).sub(zh, result)
return result