服务: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 默认密钥已清空
157 lines
5.6 KiB
Python
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"
|