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 默认密钥已清空
This commit is contained in:
156
scripts/omml_mathml2latex/processing/docx_processor.py
Normal file
156
scripts/omml_mathml2latex/processing/docx_processor.py
Normal file
@@ -0,0 +1,156 @@
|
||||
"""
|
||||
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"
|
||||
Reference in New Issue
Block a user