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