修复DOCX表格漏译并增加残留校验
This commit is contained in:
1 parent
9778981191
commit
e0a48122ba
2 files changed
+247
-6
No files matched your search
@@ -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"))
|
||||
|
||||
Reference in new issue
Block a user