修复DOCX表格漏译并增加残留校验

This commit is contained in:
Leon committed 2026-09-24 18:15:59 +08:00
1 parent 9778981191
commit e0a48122ba
2 files changed
+247 -6

No files matched your search

+181
View File
@@ -1,3 +1,4 @@
import asyncio
from io import BytesIO
import unittest
from zipfile import ZipFile
@@ -266,10 +267,190 @@ def _build_fixture_docx():
return _docx_bytes(document)
def _build_signature_table_docx():
document = docx.Document()
table = document.add_table(rows=2, cols=11)
table.cell(0, 0).text = "日期"
paragraph = table.cell(1, 0).paragraphs[0]
paragraph.add_run("姓名&签名")
paragraph.add_run(" ")
return _docx_bytes(document)
class ScriptedAgent:
def __init__(self, responses):
self.responses = iter(responses)
self.calls = []
def send_segments(self, segments, chunk_size):
self.calls.append(list(segments))
return next(self.responses)
async def send_segments_async(self, segments, chunk_size):
return self.send_segments(segments, chunk_size)
class DocxTranslatorTest(unittest.TestCase):
def _translator(self, skip_translate=True):
return DocxTranslator(DocxTranslatorConfig(skip_translate=skip_translate))
def _quality_translator(self, responses, **options):
translator = DocxTranslator(DocxTranslatorConfig(
api_key="dummy", base_url="http://127.0.0.1", model_id="dummy",
**options,
))
translator.translate_agent = ScriptedAgent(responses)
return translator
def test_only_incomplete_segments_are_retried(self):
for use_async in (False, True):
for incomplete in ("姓名&签名 ", "Nama & 签名", "", None):
with self.subTest(use_async=use_async, incomplete=incomplete):
translator = self._quality_translator(
[["Tanggal", incomplete], ["Nama & Tanda Tangan"]],
to_lang="Bahasa Indonesia",
)
document = Document(content=_build_signature_table_docx(), suffix=".docx")
if use_async:
asyncio.run(translator.translate_async(document))
else:
translator.translate(document)
self.assertEqual(translator.translate_agent.calls, [
["日期", "姓名&签名 "], ["姓名&签名 "],
])
result = docx.Document(BytesIO(document.content))
self.assertEqual(result.tables[0].cell(1, 0).text, "Nama & Tanda Tangan")
def test_incomplete_translation_fails_after_two_repairs_without_changing_input(self):
for use_async in (False, True):
with self.subTest(use_async=use_async):
translator = self._quality_translator(
[["Tanggal", "姓名&签名"], ["Nama & 签名"], ["Nama & 签名"]],
to_lang="Indonesian",
)
source = _build_signature_table_docx()
document = Document(content=source, suffix=".docx")
with self.assertRaisesRegex(ValueError, "已定向重试 2 次"):
if use_async:
asyncio.run(translator.translate_async(document))
else:
translator.translate(document)
self.assertEqual(document.content, source)
self.assertEqual(len(translator.translate_agent.calls), 3)
def test_second_repair_can_complete_translation(self):
translator = self._quality_translator(
[["Tanggal", "姓名&签名"], ["Nama & 签名"], ["Nama & Tanda Tangan"]],
to_lang="印尼语",
)
document = Document(content=_build_signature_table_docx(), suffix=".docx")
translator.translate(document)
self.assertEqual(len(translator.translate_agent.calls), 3)
self.assertIn("Nama & Tanda Tangan", "".join(_text_values(document.content)))
def test_translation_quality_respects_target_language_and_glossary(self):
for language in ("English", "en-US", "英文", "Indonesian", "id", "印尼语"):
with self.subTest(language=language):
translator = self._quality_translator([], to_lang=language)
self.assertEqual(translator._incomplete_translation_indices(
["姓名&签名"], ["Nama & 签名"]
), [0])
for language in ("简体中文", "繁体中文", "Japanese"):
with self.subTest(language=language):
translator = self._quality_translator([], to_lang=language)
self.assertEqual(translator._incomplete_translation_indices(
["姓名&签名"], ["姓名&签名"]
), [])
translator = self._quality_translator(
[], to_lang="English", glossary_dict={"华宝": "华宝"},
)
self.assertEqual(translator._incomplete_translation_indices(
["华宝签名"], ["华宝 signature"]
), [])
self.assertEqual(translator._incomplete_translation_indices(
["华宝签名"], ["华宝签名"]
), [0])
def test_wrong_translation_count_is_not_silently_truncated(self):
for use_async in (False, True):
for responses in ([["Tanggal"]], [["Tanggal", "姓名&签名"], []]):
with self.subTest(use_async=use_async, responses=responses):
translator = self._quality_translator(responses, to_lang="Indonesian")
source = _build_signature_table_docx()
document = Document(content=source, suffix=".docx")
with self.assertRaisesRegex(ValueError, "译文数量与原文不一致"):
if use_async:
asyncio.run(translator.translate_async(document))
else:
translator.translate(document)
self.assertEqual(document.content, source)
def test_signature_label_after_empty_cells_is_always_extracted(self):
input_bytes = _build_signature_table_docx()
translator = self._translator()
for iteration in range(50):
with self.subTest(iteration=iteration):
_, _, originals = translator._pre_translate(
Document(content=input_bytes, suffix=".docx")
)
self.assertEqual(originals, ["日期", "姓名&签名 "])
def test_empty_and_merged_cells_do_not_hide_later_labels(self):
document = docx.Document()
table = document.add_table(rows=5, cols=11)
table.cell(0, 0).merge(table.cell(0, 1)).text = "日期"
table.cell(1, 0).merge(table.cell(2, 0)).text = "姓名&签名"
table.cell(4, 0).text = "姓名&签名"
table.cell(4, 10).text = "审批<通过>"
input_bytes = _docx_bytes(document)
for iteration in range(20):
with self.subTest(iteration=iteration):
_, _, originals = self._translator()._pre_translate(
Document(content=input_bytes, suffix=".docx")
)
self.assertEqual(
originals, ["日期", "姓名&签名", "姓名&签名", "审批<通过>"]
)
def test_signature_label_round_trip_sync_and_async(self):
class FakeAgent:
def send_segments(self, segments, chunk_size):
translations = {
"日期": "Tanggal",
"姓名&签名 ": "Nama & Tanda Tangan ",
}
return [translations[segment] for segment in segments]
async def send_segments_async(self, segments, chunk_size):
return self.send_segments(segments, chunk_size)
input_bytes = _build_signature_table_docx()
for use_async in (False, True):
for mode in ("replace", "append", "prepend"):
with self.subTest(use_async=use_async, mode=mode):
translator = DocxTranslator(DocxTranslatorConfig(
skip_translate=False, insert_mode=mode,
api_key="dummy", base_url="http://127.0.0.1",
model_id="dummy", to_lang="Indonesian",
))
translator.translate_agent = FakeAgent()
document = Document(content=input_bytes, suffix=".docx")
if use_async:
asyncio.run(translator.translate_async(document))
else:
translator.translate(document)
result = docx.Document(BytesIO(document.content))
self.assertEqual(len(result.tables), 1)
self.assertEqual(len(result.tables[0].rows), 2)
self.assertEqual(len(result.tables[0].columns), 11)
self.assertIn("Tanggal", result.tables[0].cell(0, 0).text)
label = result.tables[0].cell(1, 0).text
self.assertIn("Nama & Tanda Tangan", label)
self.assertEqual("姓名&签名" in label, mode != "replace")
for row in result.tables[0].rows:
self.assertTrue(all(not cell.text for cell in row.cells[1:]))
def test_pre_translate_extracts_docx_boundary_text(self):
translator = self._translator()
_, _, originals = translator._pre_translate(Document(content=_build_fixture_docx(), suffix=".docx"))