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:
171
scripts/omml_mathml2latex/core/pipeline.py
Normal file
171
scripts/omml_mathml2latex/core/pipeline.py
Normal file
@@ -0,0 +1,171 @@
|
||||
"""
|
||||
Pipeline orchestrator for the conversion process.
|
||||
|
||||
This module implements the pipeline pattern, coordinating the execution
|
||||
of multiple stages in sequence to convert inputs to LaTeX.
|
||||
"""
|
||||
|
||||
import time
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
from .interfaces import Stage, ConversionContext
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConversionPipeline:
|
||||
"""
|
||||
Orchestrates conversion through multiple stages.
|
||||
|
||||
The pipeline executes stages in sequence, passing the context from
|
||||
one stage to the next. Each stage can read from and write to the
|
||||
shared context object.
|
||||
|
||||
Example:
|
||||
>>> pipeline = (ConversionPipeline()
|
||||
... .add_stage(InputStage())
|
||||
... .add_stage(ValidationStage())
|
||||
... .add_stage(ConversionStage())
|
||||
... .add_stage(OutputStage()))
|
||||
>>> context = ConversionContext(input_type='mathml', mathml='<math>...</math>')
|
||||
>>> result = pipeline.process(context)
|
||||
"""
|
||||
|
||||
def __init__(self, enable_validation: bool = True, enable_timing: bool = True):
|
||||
"""
|
||||
Initialize the pipeline.
|
||||
|
||||
Args:
|
||||
enable_validation: Whether to validate stage preconditions before execution
|
||||
enable_timing: Whether to record stage execution times
|
||||
"""
|
||||
self.stages: List[Stage] = []
|
||||
self.enable_validation = enable_validation
|
||||
self.enable_timing = enable_timing
|
||||
|
||||
def add_stage(self, stage: Stage) -> 'ConversionPipeline':
|
||||
"""
|
||||
Add a stage to the pipeline (fluent interface).
|
||||
|
||||
Args:
|
||||
stage: The stage to add to the pipeline
|
||||
|
||||
Returns:
|
||||
Self for method chaining
|
||||
|
||||
Raises:
|
||||
TypeError: If stage is not an instance of Stage
|
||||
"""
|
||||
if not isinstance(stage, Stage):
|
||||
raise TypeError(f"Stage must be instance of Stage, got {type(stage)}")
|
||||
|
||||
self.stages.append(stage)
|
||||
logger.debug(f"Added stage: {stage.name}")
|
||||
return self
|
||||
|
||||
def process(self, context: ConversionContext) -> ConversionContext:
|
||||
"""
|
||||
Execute all stages in order.
|
||||
|
||||
Args:
|
||||
context: The initial conversion context
|
||||
|
||||
Returns:
|
||||
Updated conversion context after all stages
|
||||
|
||||
Raises:
|
||||
ValueError: If any stage validation fails
|
||||
RuntimeError: If any stage processing fails
|
||||
"""
|
||||
if not self.stages:
|
||||
raise RuntimeError("Pipeline has no stages configured")
|
||||
|
||||
logger.info(f"Starting pipeline with {len(self.stages)} stages")
|
||||
pipeline_start_time = time.time()
|
||||
|
||||
try:
|
||||
for stage in self.stages:
|
||||
context = self._execute_stage(stage, context)
|
||||
|
||||
# Record total pipeline time
|
||||
pipeline_end_time = time.time()
|
||||
context.total_time = pipeline_end_time - pipeline_start_time
|
||||
|
||||
logger.info(
|
||||
f"Pipeline completed successfully in {context.total_time:.4f}s "
|
||||
f"({len(self.stages)} stages)"
|
||||
)
|
||||
return context
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Pipeline failed: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
def _execute_stage(self, stage: Stage, context: ConversionContext) -> ConversionContext:
|
||||
"""
|
||||
Execute a single stage with validation and timing.
|
||||
|
||||
Args:
|
||||
stage: The stage to execute
|
||||
context: The current context
|
||||
|
||||
Returns:
|
||||
Updated context after stage execution
|
||||
|
||||
Raises:
|
||||
ValueError: If stage validation fails
|
||||
RuntimeError: If stage execution fails
|
||||
"""
|
||||
logger.debug(f"Executing stage: {stage.name}")
|
||||
|
||||
# Validate preconditions if enabled
|
||||
if self.enable_validation:
|
||||
if not stage.validate_input(context):
|
||||
error_msg = f"Stage '{stage.name}' validation failed"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg)
|
||||
|
||||
# Execute stage with timing
|
||||
if self.enable_timing:
|
||||
stage_start_time = time.time()
|
||||
context = stage.process(context)
|
||||
stage_end_time = time.time()
|
||||
stage_time = stage_end_time - stage_start_time
|
||||
context.record_stage_time(stage.name, stage_time)
|
||||
logger.debug(f"Stage '{stage.name}' completed in {stage_time:.4f}s")
|
||||
else:
|
||||
context = stage.process(context)
|
||||
|
||||
return context
|
||||
|
||||
def clear(self) -> 'ConversionPipeline':
|
||||
"""
|
||||
Remove all stages from the pipeline.
|
||||
|
||||
Returns:
|
||||
Self for method chaining
|
||||
"""
|
||||
self.stages.clear()
|
||||
logger.debug("Pipeline cleared")
|
||||
return self
|
||||
|
||||
def get_stages(self) -> List[Stage]:
|
||||
"""Get a copy of the stages list."""
|
||||
return self.stages.copy()
|
||||
|
||||
def get_stage_count(self) -> int:
|
||||
"""Get the number of stages in the pipeline."""
|
||||
return len(self.stages)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""String representation for debugging."""
|
||||
stage_names = [stage.name for stage in self.stages]
|
||||
return f"ConversionPipeline(stages={stage_names})"
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Human-readable representation."""
|
||||
if not self.stages:
|
||||
return "ConversionPipeline (empty)"
|
||||
|
||||
stage_list = "\n ".join([f"{i+1}. {stage.name}" for i, stage in enumerate(self.stages)])
|
||||
return f"ConversionPipeline ({len(self.stages)} stages):\n {stage_list}"
|
||||
Reference in New Issue
Block a user