Files
DoLuEdu-WordFileParse/scripts/omml_mathml2latex/processing/docx_processor.py
Quentin 3271fee5cc init: WordFileParse docx→HTML 解析服务(含 Word EQ 域公式修复)
服务:Spring Boot 3.2 + docx4j 8.3.9,POST /api/v3/server_office_file/file/parse
      接受 .docx,返回 HTML(公式 → LaTeX,图片 → RustFS 外链)

公式支持三类来源:
- OMML(Word 2007+ 公式)      → Python omml_mathml2latex
- MathType OLE 对象            → Ruby mathtype2mathml → MathML → LaTeX
- Word EQ 域(老教辅主力)      → EqToLatexUtil(本次新增)

本次改动:
1. 新增 java-svc/.../core/EqToLatexUtil.java:
   移植自 jby backend-services office-core,EQ 语法 → LaTeX
   (\f→\dfrac、\r→\sqrt、\i→\int/\sum/\prod、\s→^{}/_{}、\b→\left\right、
     \a→array、\x→\boxed/\overline、\o\al 堆叠、圈码、化学符号、极限、连等式)
   品牌标记 JBYU* → WPU*,与采集端一致
2. CtoMathFinder:接上 EQ 分支(原为 not-ported 桩),域代码经
   EqToLatexUtil.convert 转 LaTeX 写入 eqLatexMap(WPEQNUM 占位符)
3. WordParseUtil.dealPlaceholder:EQ 的 LaTeX 同样加 \( \) 包裹
   (原先因 WPEQNUM 例外不加包裹,前端/MathJax 不渲染)

修前症状:教辅 Word 里分式/方程组/上下标整段丢失(如 {−1/3, 1} 变成 {-,1})
修后实测:46 份教辅 2908 处 EQ 域全部还原,公式总数 2954 处、零未包裹残留

安全:application.yml(含 RustFS 密钥)不入库,提供 application.yml.example;
      OssService 的 @Value 默认密钥已清空
2026-09-29 09:43:13 +08:00

157 lines
5.6 KiB
Python

"""
DOCX file processing and end-to-end conversion.
This module provides the DocxProcessor class which orchestrates the complete
conversion pipeline from DOCX to LaTeX, including OMML extraction, MathML
transformation, and LaTeX conversion.
"""
import logging
import time
from typing import List
from .result import ConversionResult
from .omml_extractor import OMMLExtractor
logger = logging.getLogger(__name__)
class DocxProcessor:
"""Process DOCX files and convert all OMML equations to LaTeX.
This class coordinates:
- OMML extraction from DOCX (via OMMLExtractor)
- OMML → MathML transformation (via omml_to_mathml function)
- MathML → LaTeX conversion (via MathMLToTeXConverter)
Example:
>>> from omml2tex.compat import LegacyMathMLToTeXConverter
>>> processor = DocxProcessor(
... xsl_path='OMML2MML.xsl',
... converter=MathMLToTeXConverter()
... )
>>> results = processor.process_docx('document.docx')
>>> for result in results:
... if result.success:
... print(result.latex)
"""
def __init__(self, xsl_path: str, converter, subject_mode: str = "auto"):
"""Initialize DOCX processor.
Args:
xsl_path: Path to OMML2MML.xsl transformation file
converter: MathMLToTeXConverter instance
subject_mode: Subject mode for conversion ('auto', 'math', 'chemistry', 'physics')
"""
self.extractor = OMMLExtractor()
self.xsl_path = xsl_path
self.converter = converter
self.subject_mode = subject_mode
def process_docx(self, docx_path: str, omml_to_mathml_func=None) -> List[ConversionResult]:
"""Process a DOCX file and convert all equations.
Args:
docx_path: Path to DOCX file
omml_to_mathml_func: Function to convert OMML to MathML (for dependency injection)
Returns:
List of ConversionResult objects
Example:
>>> results = processor.process_docx('test.docx')
>>> successful = [r for r in results if r.success]
>>> print(f"{len(successful)}/{len(results)} equations converted")
"""
results = []
try:
# Extract OMML equations
equations = self.extractor.extract_from_docx(docx_path)
logger.info(f"Found {len(equations)} equations in {docx_path}")
for eq_data in equations:
result = ConversionResult()
result.omml = eq_data['cleaned_omml']
result.context = eq_data['context']
result.position = eq_data['position']
result.source_file = eq_data['source_file']
result.display_mode = eq_data.get('display_mode', False)
try:
start_time = time.time()
# Convert OMML to MathML
if omml_to_mathml_func is None:
# Import here to avoid circular dependency
from ..core.xslt_transformer import omml_to_mathml
omml_to_mathml_func = omml_to_mathml
mathml_xml = omml_to_mathml_func(eq_data['cleaned_omml'], self.xsl_path)
result.mathml = mathml_xml
# Convert MathML to LaTeX
latex = self.converter.convert(mathml_xml)
result.latex = latex
result.processing_time = time.time() - start_time
result.success = True
except Exception as e:
result.error = str(e)
result.success = False
logger.error(f"Failed to convert equation {result.position}: {e}")
results.append(result)
except Exception as e:
logger.error(f"Failed to process DOCX file: {e}")
# Return empty results with error
result = ConversionResult()
result.error = f"DOCX processing failed: {e}"
results.append(result)
return results
def detect_subject_mode(self, results: List[ConversionResult]) -> str:
"""Auto-detect subject mode based on content.
Analyzes context text from equations to determine if document
is primarily chemistry, physics, or general math.
Args:
results: List of conversion results with context
Returns:
Detected subject mode: 'chemistry', 'physics', or 'math'
Example:
>>> results = processor.process_docx('chem_doc.docx')
>>> mode = processor.detect_subject_mode(results)
>>> print(f"Detected mode: {mode}")
"""
chemistry_indicators = 0
physics_indicators = 0
total_context = ""
for result in results:
if result.success and result.context:
total_context += " " + result.context.lower()
# Simple heuristic detection
chemistry_words = ['reaction', 'equation', 'chemical', 'compound', 'element', 'molecule', 'ion', 'acid', 'base']
physics_words = ['force', 'energy', 'velocity', 'acceleration', 'wave', 'field', 'quantum', 'particle']
for word in chemistry_words:
chemistry_indicators += total_context.count(word)
for word in physics_words:
physics_indicators += total_context.count(word)
if chemistry_indicators > physics_indicators and chemistry_indicators > 2:
return "chemistry"
elif physics_indicators > chemistry_indicators and physics_indicators > 2:
return "physics"
else:
return "math"