优化docx翻译

This commit is contained in:
xunbu
2025-10-18 23:56:56 +08:00
parent b31017bfb7
commit ce1ffddfa7

View File

@@ -15,9 +15,6 @@ from docx.text.paragraph import Paragraph
from docx.text.run import Run
from docx.table import _Cell, Table
# 注意根据最终方案lxml 不再需要,已移除
# from lxml import etree
from docutranslate.agents.segments_agent import SegmentsTranslateAgentConfig, SegmentsTranslateAgent
from docutranslate.ir.document import Document
from docutranslate.translator.ai_translator.base import AiTranslatorConfig, AiTranslator
@@ -30,12 +27,6 @@ def is_image_run(run: Run) -> bool:
return '<w:drawing' in xml or '<w:pict' in xml
# ==================== MODIFICATION START ====================
# 对 is_formatting_only_run 函数进行了修改
# 旧的实现无法识别仅包含颜色等 rPr 属性的空 Run导致其与后续文本 Run 错误合并。
# # 新的实现通过一个更简单的标准来判断:只要一个 Run 的文本内容为空,
# # 它就被认为是纯格式化的,从而解决了交叉引用文本消失的问题。
# ==========================================================
def is_formatting_only_run(run: Run) -> bool:
"""
检查一个 Run 是否仅用于格式化,不包含任何应被渲染的文本。
@@ -44,7 +35,19 @@ def is_formatting_only_run(run: Run) -> bool:
return run.text == ""
# ===================== MODIFICATION END =====================
def is_styled_whitespace_run(run: Run) -> bool:
"""
检查一个 Run 是否只包含空白字符,但应用了应保留的直接格式(如下划线)。
这些 Run 应被视为翻译段的边界并保持不变。
"""
# 如果 Run 不只包含空白,或者完全为空,则不符合条件
if not (run.text and run.text.isspace()):
return False
rPr = run.element.rPr
# 如果 rPr 元素存在且有任何子元素(例如 <w:u> 表示下划线),
# 这意味着样式被直接应用到了这个空白 Run 上。
return rPr is not None and len(list(rPr)) > 0
# ---------------- 配置类 ----------------
@@ -60,21 +63,19 @@ class DocxTranslator(AiTranslator):
一个基于高级结构化解析的 .docx 文件翻译器。
它能高精度保留样式,并正确处理正文、表格、页眉/脚、脚注/尾注、超链接和目录(TOC)等复杂元素。
[v5.3 - 语义切分修复版]
- 修复了 v5.2 方案中因过度切分格式变化文本(如 H₂O导致翻译上下文丢失的问题。
- 废弃了基于 <w:rPr> 比较的切分逻辑,转而采用更稳健的语义边界切分。
- 核心改动在处理完一个域Field的结束标记fldCharType="end")后强制刷新文本段,
既能正确分离引用标记(如[1])与后续文本,防止格式污染,又能保持化学式等
含格式变化的连续文本的完整
[v6.1 - 格式保留修复版]
- 修复了因合并 Run 导致下划线等格式在翻译后丢失的问题。
- 通过引入 is_styled_whitespace_run 检查,将仅包含空格但带有样式的 Run如下划线空格
视为与图片类似的不可翻译边界。
-可以防止这些关键的格式化 Run 被合并到文本段中或在应用翻译时被错误地删除,
从而确保了下划线、加粗空格等格式的完整保留
[v5.1 - 遍历修复版]
- 重构了核心遍历函数 _traverse_container使其能稳健处理所有类型的文本容器
包括页眉 (header)、页脚 (footer)、脚注 (footnote) 和尾注 (endnote)
[v5.0 - 增强版]
- 引入了智能域处理状态机,精确识别并跳过 PAGEREF (页码) 和 SEQ (序号) 等不应翻译的动态域内容
- 优化了文本切分逻辑,解决了目录(TOC)和图表目录(TOF)条目被错误拆分为“标题”和“页码”两部分的问题。
- 根除了因复杂域处理不当导致的目录项重复翻译问题,确保每个条目只被提取和翻译一次。
[v6.0 - 语义切分重构版]
- 核心思想重构:不再试图通过复杂的状态机去识别和“跳过”特定类型的域(如页码、序号)。
- 默认提取所有文本所有域的结果PAGEREF, SEQ 等)现在都会被提取出来进行处理
- 简化切分逻辑仅使用域的开始begin和结束end标记作为强制性的语义边界在此处切分文本段。
这确保了域结果(如交叉引用"[1]")与其前后的文本在语义上分离,同时又避免了因跳过逻辑导致的文本丢失风险。
- 提升鲁棒性:新逻辑对未知或复杂的域结构更具适应性,确保了文本提取的完整性
"""
IGNORED_TAGS = {
qn('w:proofErr'), qn('w:lastRenderedPageBreak'), qn('w:bookmarkStart'),
@@ -84,8 +85,6 @@ class DocxTranslator(AiTranslator):
RECURSIVE_CONTAINER_TAGS = {
qn('w:smartTag'), qn('w:sdtContent'), qn('w:hyperlink'),
}
# [v5.0] 定义不应翻译其结果的域指令
SKIPPABLE_FIELD_INSTRUCTIONS = {'PAGEREF', 'SEQ', 'PAGE', 'NUMPAGES', 'DATE', 'TIME', 'SECTION'}
def __init__(self, config: DocxTranslatorConfig):
super().__init__(config=config)
@@ -125,47 +124,28 @@ class DocxTranslator(AiTranslator):
self._process_element_children(child, elements, texts, state)
continue
# --- [v5.3] 智能域处理逻辑 ---
instr_text_element = child.find(qn('w:instrText')) if isinstance(child, CT_R) else None
if instr_text_element is not None:
instr_text = instr_text_element.text.strip()
if any(keyword in instr_text for keyword in self.SKIPPABLE_FIELD_INSTRUCTIONS):
state['is_in_skippable_field'] = True
continue
field_char_element = child.find(qn('w:fldChar')) if isinstance(child, CT_R) else (
child if child.tag == qn('w:fldChar') else None)
# --- [v6.0 Refactored] 简化的域处理逻辑 ---
field_char_element = child.find(qn('w:fldChar')) if isinstance(child, CT_R) else None
if field_char_element is not None:
fld_type = field_char_element.get(qn('w:fldCharType'))
if fld_type == 'begin':
if fld_type == 'begin' or fld_type == 'end':
flush_segment()
state['is_in_skippable_field'] = False
state['is_skipping_result'] = False
elif fld_type == 'separate':
if state.get('is_in_skippable_field'):
flush_segment()
state['is_skipping_result'] = True
elif fld_type == 'end':
# ===== [v5.3] 关键改动 =====
# 在域结束后强制刷新,确保域结果(如[1])和后面的文本分开。
# 这就是我们需要的语义边界。
flush_segment()
state['is_in_skippable_field'] = False
state['is_skipping_result'] = False
continue
if state.get('is_skipping_result'):
continue
# --- 域处理逻辑结束 ---
if isinstance(child, CT_R):
run = Run(child, None)
if is_image_run(run) or is_formatting_only_run(run):
# [v6.1 FIX] 将带样式的空白 Run如下划线视为与图片一样的边界
# 以防止其格式在翻译过程中丢失。
if is_image_run(run) or is_formatting_only_run(run) or is_styled_whitespace_run(run):
# 这些 Run 是边界,不应包含在可翻译段落中。
# 我们刷新任何之前的文本,然后这个 Run 本身被跳过,
# 从而在文档中保持原样。
flush_segment()
else:
# [v5.3] 移除了 v5.2 的 rPr 比较逻辑,允许 H₂O 合并
# 这是一个包含实际可翻译文本的 Run将其添加到当前段落。
state['current_runs'].append(run)
else:
# 如果子元素不是 Run它也充当一个边界。
flush_segment()
def _process_paragraph(self, para: Paragraph, elements: List[Dict[str, Any]], texts: List[str]):
@@ -173,10 +153,10 @@ class DocxTranslator(AiTranslator):
return
state = {
'current_runs': [],
'is_in_skippable_field': False,
'is_skipping_result': False
}
self._process_element_children(para._p, elements, texts, state)
# 刷新段落末尾任何剩余的 runs
current_runs = state['current_runs']
if current_runs:
full_text = "".join(r.text for r in current_runs)