add a retranslate chinese function
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user