优化docx翻译v4.1
This commit is contained in:
@@ -21,6 +21,22 @@ from docutranslate.translator.ai_translator.base import AiTranslatorConfig, AiTr
|
||||
|
||||
|
||||
# ---------------- 辅助函数 ----------------
|
||||
|
||||
# [v6.2] 定义一组具有显著视觉效果的格式标签。
|
||||
# 我们只在 Run 包含这些格式时才将其视为空白格式边界。
|
||||
# 这避免了因字体、字号等微小变化导致的过度文本切分。
|
||||
SIGNIFICANT_STYLES = frozenset([
|
||||
qn('w:u'), # 下划线
|
||||
qn('w:strike'), # 删除线
|
||||
qn('w:dstrike'), # 双删除线
|
||||
qn('w:shd'), # 底纹/背景色
|
||||
qn('w:highlight'), # 荧光笔高亮
|
||||
qn('w:bdr'), # 边框
|
||||
qn('w:effectLst'), # 文本效果 (如发光、阴影)
|
||||
qn('w:em'), # 强调标记 (着重号)
|
||||
])
|
||||
|
||||
|
||||
def is_image_run(run: Run) -> bool:
|
||||
"""检查一个 Run 是否包含图片。"""
|
||||
xml = getattr(run.element, 'xml', '')
|
||||
@@ -37,17 +53,18 @@ def is_formatting_only_run(run: Run) -> bool:
|
||||
|
||||
def is_styled_whitespace_run(run: Run) -> bool:
|
||||
"""
|
||||
检查一个 Run 是否只包含空白字符,但应用了应保留的直接格式(如下划线)。
|
||||
[v6.2] 检查一个 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
|
||||
if rPr is None:
|
||||
return False
|
||||
|
||||
# 仅当 Run 的属性中包含我们定义的“显著”样式之一时,才返回 True。
|
||||
return any(child.tag in SIGNIFICANT_STYLES for child in rPr)
|
||||
|
||||
|
||||
# ---------------- 配置类 ----------------
|
||||
@@ -63,19 +80,19 @@ class DocxTranslator(AiTranslator):
|
||||
一个基于高级结构化解析的 .docx 文件翻译器。
|
||||
它能高精度保留样式,并正确处理正文、表格、页眉/脚、脚注/尾注、超链接和目录(TOC)等复杂元素。
|
||||
|
||||
[v6.2 - 精确格式保留]
|
||||
- 根据用户反馈,精确定义了构成“重要格式”的样式范围。
|
||||
- 现在仅在空白 Run 包含下划线、背景色、边框等显著视觉样式时才将其视为边界。
|
||||
- 忽略了加粗、倾斜、字体/字号变化等对空格本身无视觉效果或不重要的样式,
|
||||
避免了因此对翻译句子造成不必要的切分,提升了翻译质量。
|
||||
|
||||
[v6.1 - 格式保留修复版]
|
||||
- 修复了因合并 Run 导致下划线等格式在翻译后丢失的问题。
|
||||
- 通过引入 is_styled_whitespace_run 检查,将仅包含空格但带有样式的 Run(如下划线空格)
|
||||
视为与图片类似的不可翻译边界。
|
||||
- 这可以防止这些关键的格式化 Run 被合并到文本段中或在应用翻译时被错误地删除,
|
||||
从而确保了下划线、加粗空格等格式的完整保留。
|
||||
|
||||
[v6.0 - 语义切分重构版]
|
||||
- 核心思想重构:不再试图通过复杂的状态机去识别和“跳过”特定类型的域(如页码、序号)。
|
||||
- 默认提取所有文本:所有域的结果(PAGEREF, SEQ 等)现在都会被提取出来进行处理。
|
||||
- 简化切分逻辑:仅使用域的开始(begin)和结束(end)标记作为强制性的语义边界,在此处切分文本段。
|
||||
这确保了域结果(如交叉引用"[1]")与其前后的文本在语义上分离,同时又避免了因跳过逻辑导致的文本丢失风险。
|
||||
- 提升鲁棒性:新逻辑对未知或复杂的域结构更具适应性,确保了文本提取的完整性。
|
||||
- 重构核心逻辑,不再跳过域结果,而是将其作为语义边界来切分文本,增强了鲁棒性。
|
||||
"""
|
||||
IGNORED_TAGS = {
|
||||
qn('w:proofErr'), qn('w:lastRenderedPageBreak'), qn('w:bookmarkStart'),
|
||||
@@ -124,7 +141,6 @@ class DocxTranslator(AiTranslator):
|
||||
self._process_element_children(child, elements, texts, state)
|
||||
continue
|
||||
|
||||
# --- [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'))
|
||||
@@ -134,29 +150,23 @@ class DocxTranslator(AiTranslator):
|
||||
|
||||
if isinstance(child, CT_R):
|
||||
run = Run(child, None)
|
||||
# [v6.1 FIX] 将带样式的空白 Run(如下划线)视为与图片一样的边界,
|
||||
# 以防止其格式在翻译过程中丢失。
|
||||
# [v6.2] 使用更精确的检查来识别作为边界的 Run。
|
||||
if is_image_run(run) or is_formatting_only_run(run) or is_styled_whitespace_run(run):
|
||||
# 这些 Run 是边界,不应包含在可翻译段落中。
|
||||
# 我们刷新任何之前的文本,然后这个 Run 本身被跳过,
|
||||
# 从而在文档中保持原样。
|
||||
flush_segment()
|
||||
else:
|
||||
# 这是一个包含实际可翻译文本的 Run,将其添加到当前段落。
|
||||
state['current_runs'].append(run)
|
||||
else:
|
||||
# 如果子元素不是 Run,它也充当一个边界。
|
||||
flush_segment()
|
||||
|
||||
def _process_paragraph(self, para: Paragraph, elements: List[Dict[str, Any]], texts: List[str]):
|
||||
if not para.text.strip():
|
||||
return
|
||||
# [v6.2] 此处无需检查 para.text.strip(),因为一个段落可能只包含一个带样式的空白 Run,
|
||||
# 这种 Run 我们需要保留,而 .text.strip() 会将其视为空。
|
||||
# 具体的文本提取逻辑在 _process_element_children 中处理。
|
||||
state = {
|
||||
'current_runs': [],
|
||||
}
|
||||
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)
|
||||
@@ -189,36 +199,26 @@ class DocxTranslator(AiTranslator):
|
||||
return
|
||||
|
||||
parent_element = None
|
||||
# 首先获取包含内容的顶层 XML 元素
|
||||
if isinstance(container, (DocumentObject, Part)):
|
||||
# 对于 Document 和 Part 对象 (如 FootnotesPart),使用 .element
|
||||
parent_element = container.element.body if hasattr(container.element, 'body') else container.element
|
||||
elif isinstance(container, (_Cell, _Header, _Footer)):
|
||||
# 对于内部块容器,使用 ._element
|
||||
parent_element = container._element
|
||||
else:
|
||||
# 如果遇到未知类型,记录警告并返回
|
||||
self.logger.warning(f"跳过未知类型的容器: {type(container)}")
|
||||
return
|
||||
|
||||
# 检查是否是需要特殊处理的嵌套结构 (如脚注/尾注)
|
||||
if parent_element is not None and parent_element.tag in [qn('w:footnotes'), qn('w:endnotes')]:
|
||||
# 遍历每个 <w:footnote> 或 <w:endnote> 元素
|
||||
for note_element in parent_element:
|
||||
# 在每个注释元素内部处理其包含的段落和表格
|
||||
self._process_body_elements(note_element, container, elements, texts)
|
||||
elif parent_element is not None:
|
||||
# 对于其他所有容器 (body, tc, hdr, ftr),直接处理其子元素
|
||||
self._process_body_elements(parent_element, container, elements, texts)
|
||||
|
||||
def _pre_translate(self, document: Document) -> Tuple[DocumentObject, List[Dict[str, Any]], List[str]]:
|
||||
doc = docx.Document(BytesIO(document.content))
|
||||
elements, texts = [], []
|
||||
|
||||
# 1. 处理主文档内容
|
||||
self._traverse_container(doc, elements, texts)
|
||||
|
||||
# 2. 处理所有节的页眉和页脚
|
||||
for section in doc.sections:
|
||||
self._traverse_container(section.header, elements, texts)
|
||||
self._traverse_container(section.first_page_header, elements, texts)
|
||||
@@ -227,7 +227,6 @@ class DocxTranslator(AiTranslator):
|
||||
self._traverse_container(section.first_page_footer, elements, texts)
|
||||
self._traverse_container(section.even_page_footer, elements, texts)
|
||||
|
||||
# 3. 处理脚注和尾注
|
||||
if hasattr(doc.part, 'footnotes_part') and doc.part.footnotes_part is not None:
|
||||
self._traverse_container(doc.part.footnotes_part, elements, texts)
|
||||
if hasattr(doc.part, 'endnotes_part') and doc.part.endnotes_part is not None:
|
||||
@@ -242,9 +241,7 @@ class DocxTranslator(AiTranslator):
|
||||
|
||||
first_real_run_index = -1
|
||||
for i, run in enumerate(runs):
|
||||
# 确保run仍然在文档树中
|
||||
if run.element.getparent() is not None:
|
||||
# 找到第一个可以写入文本的run
|
||||
run.text = final_text
|
||||
first_real_run_index = i
|
||||
break
|
||||
@@ -253,7 +250,6 @@ class DocxTranslator(AiTranslator):
|
||||
self.logger.warning(f"无法应用翻译 '{final_text}',因为找不到有效的run。")
|
||||
return
|
||||
|
||||
# 从第一个有效的run之后开始,删除所有多余的run
|
||||
for i in range(first_real_run_index + 1, len(runs)):
|
||||
run = runs[i]
|
||||
parent_element = run.element.getparent()
|
||||
@@ -261,7 +257,6 @@ class DocxTranslator(AiTranslator):
|
||||
try:
|
||||
parent_element.remove(run.element)
|
||||
except ValueError:
|
||||
# 如果元素已经被其他操作移除,这里会抛出ValueError,可以安全地忽略
|
||||
self.logger.debug(f"尝试删除一个不存在的run元素。这通常是安全的。")
|
||||
pass
|
||||
|
||||
|
||||
Reference in New Issue
Block a user