服务: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 默认密钥已清空
262 lines
8.9 KiB
Python
262 lines
8.9 KiB
Python
"""
|
|
Batch JSON converter for omml2tex with key preservation.
|
|
|
|
Handles conversion of multiple equations in JSON format while preserving
|
|
unique identifiers. Supports both sequential and parallel processing.
|
|
"""
|
|
|
|
from typing import Dict, Any
|
|
import sys
|
|
|
|
from .compat import LegacyMathMLToTeXConverter
|
|
from .parsers.omml_parser import OmmlParser
|
|
|
|
|
|
class BatchJSONConverter:
|
|
"""
|
|
Batch converter for JSON format with key preservation.
|
|
|
|
Converts a dictionary of equations (key -> XML) to LaTeX while preserving
|
|
the original keys. Supports both OMML and MathML input formats with
|
|
automatic detection.
|
|
|
|
Example:
|
|
>>> converter = BatchJSONConverter(subject_mode="chemistry")
|
|
>>> equations = {"eq1": "<math><mi>x</mi></math>", "eq2": "..."}
|
|
>>> results = converter.convert_batch(equations)
|
|
>>> print(results["eq1"]) # LaTeX output
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
subject_mode: str = "math",
|
|
wrap_identifiers: bool = True,
|
|
prefer_xlongequal: bool = True,
|
|
universal_delimiter_scaling: bool = True,
|
|
verbose: bool = False
|
|
):
|
|
"""
|
|
Initialize batch converter.
|
|
|
|
Args:
|
|
subject_mode: Subject mode for conversion (math, chemistry, physics, statistics)
|
|
wrap_identifiers: Whether to wrap identifiers in \\mathrm{}
|
|
prefer_xlongequal: Whether to prefer \\xlongequal for overset equations
|
|
universal_delimiter_scaling: Whether to add \\left\\right to all delimiters
|
|
verbose: Enable verbose output
|
|
"""
|
|
self.subject_mode = subject_mode
|
|
self.wrap_identifiers = wrap_identifiers
|
|
self.prefer_xlongequal = prefer_xlongequal
|
|
self.universal_delimiter_scaling = universal_delimiter_scaling
|
|
self.verbose = verbose
|
|
|
|
# Initialize converter
|
|
self.converter = LegacyMathMLToTeXConverter(
|
|
wrap_identifiers_in_mathrm=wrap_identifiers,
|
|
prefer_xlongequal=prefer_xlongequal,
|
|
subject_mode=subject_mode,
|
|
universal_delimiter_scaling=universal_delimiter_scaling,
|
|
)
|
|
|
|
# OMML parser for OMML input
|
|
self.omml_parser = OmmlParser()
|
|
|
|
def detect_input_type(self, content: str) -> str:
|
|
"""
|
|
Detect if input is OMML or MathML.
|
|
|
|
Args:
|
|
content: XML content string
|
|
|
|
Returns:
|
|
"omml" or "mathml"
|
|
"""
|
|
content_stripped = content.strip()
|
|
|
|
# Check for OMML namespace
|
|
if 'm:oMath' in content_stripped or 'm:oMathPara' in content_stripped:
|
|
return "omml"
|
|
|
|
# Check for MathML namespace
|
|
if '<math' in content_stripped or 'xmlns="http://www.w3.org/1998/Math/MathML"' in content_stripped:
|
|
return "mathml"
|
|
|
|
# Default to mathml
|
|
return "mathml"
|
|
|
|
def convert_single(self, key: str, equation_xml: str) -> Dict[str, Any]:
|
|
"""
|
|
Convert a single equation while preserving the key.
|
|
|
|
Args:
|
|
key: Unique identifier for the equation
|
|
equation_xml: OMML or MathML XML string
|
|
|
|
Returns:
|
|
Dict with 'key', 'latex', 'success', and optional 'error'
|
|
"""
|
|
try:
|
|
# Detect input type
|
|
input_type = self.detect_input_type(equation_xml)
|
|
|
|
if self.verbose:
|
|
print(f" {key}: {input_type} -> ", end="", file=sys.stderr)
|
|
|
|
# Convert OMML to MathML if needed
|
|
if input_type == "omml":
|
|
mathml_list = self.omml_parser.parse(equation_xml)
|
|
# OmmlParser.parse() returns a list - take the first element
|
|
if not mathml_list:
|
|
raise ValueError("OMML parser returned empty result")
|
|
mathml = mathml_list[0]
|
|
else:
|
|
mathml = equation_xml
|
|
|
|
# Convert MathML to LaTeX
|
|
latex = self.converter.convert(mathml)
|
|
|
|
if self.verbose:
|
|
print(f"LaTeX ({len(latex)} chars)", file=sys.stderr)
|
|
|
|
return {
|
|
"key": key,
|
|
"latex": latex,
|
|
"success": True,
|
|
}
|
|
|
|
except Exception as e:
|
|
if self.verbose:
|
|
print(f"FAILED: {e}", file=sys.stderr)
|
|
|
|
return {
|
|
"key": key,
|
|
"latex": "",
|
|
"success": False,
|
|
"error": str(e),
|
|
}
|
|
|
|
def convert_batch(self, input_dict: Dict[str, str], quiet: bool = False) -> Dict[str, str]:
|
|
"""
|
|
Convert a batch of equations from JSON dict (sequential).
|
|
|
|
Args:
|
|
input_dict: Dict mapping unique keys to equation XML strings
|
|
quiet: Suppress progress messages
|
|
|
|
Returns:
|
|
Dict mapping same keys to LaTeX strings (failed conversions get empty string)
|
|
"""
|
|
if self.verbose and not quiet:
|
|
print(f"Converting {len(input_dict)} equations...", file=sys.stderr)
|
|
|
|
results = {}
|
|
successful = 0
|
|
failed = 0
|
|
|
|
for key, equation_xml in input_dict.items():
|
|
equation_xml = equation_xml.replace("JBYSIMJBY", "~")
|
|
result = self.convert_single(key, equation_xml)
|
|
|
|
# Store LaTeX string (or empty string if failed)
|
|
results[key] = result["latex"]
|
|
|
|
if result["success"]:
|
|
successful += 1
|
|
else:
|
|
failed += 1
|
|
if not self.verbose and not quiet:
|
|
print(
|
|
f"Warning: Failed to convert {key}: {result.get('error', 'Unknown error')}",
|
|
file=sys.stderr
|
|
)
|
|
|
|
if (self.verbose or failed > 0) and not quiet:
|
|
print(f"\nConversion complete:", file=sys.stderr)
|
|
print(f" Total: {len(input_dict)}", file=sys.stderr)
|
|
print(f" Successful: {successful}", file=sys.stderr)
|
|
print(f" Failed: {failed}", file=sys.stderr)
|
|
print(f" Success rate: {successful/len(input_dict)*100:.1f}%", file=sys.stderr)
|
|
|
|
return results
|
|
|
|
def convert_batch_parallel(
|
|
self,
|
|
input_dict: Dict[str, str],
|
|
workers: int = None,
|
|
quiet: bool = False
|
|
) -> Dict[str, str]:
|
|
"""
|
|
Convert batch using parallel processing (for large batches).
|
|
|
|
Args:
|
|
input_dict: Dict mapping unique keys to equation XML strings
|
|
workers: Number of worker processes (default: CPU count, max 8)
|
|
quiet: Suppress progress messages
|
|
|
|
Returns:
|
|
Dict mapping same keys to LaTeX strings
|
|
"""
|
|
from .parallel import ParallelConverter
|
|
|
|
if self.verbose and not quiet:
|
|
print(f"Converting {len(input_dict)} equations in parallel...", file=sys.stderr)
|
|
|
|
# Create parallel converter
|
|
parallel_converter = ParallelConverter(
|
|
workers=workers,
|
|
subject_mode=self.subject_mode,
|
|
wrap_identifiers=self.wrap_identifiers,
|
|
prefer_xlongequal=self.prefer_xlongequal,
|
|
show_progress=self.verbose and not quiet,
|
|
)
|
|
|
|
# Prepare input list (keys and equations)
|
|
keys = list(input_dict.keys())
|
|
equations = list(input_dict.values())
|
|
|
|
# Detect input types and convert OMML to MathML if needed
|
|
mathml_equations = []
|
|
for equation_xml in equations:
|
|
equation_xml = equation_xml.replace("JBYSIMJBY", "~")
|
|
input_type = self.detect_input_type(equation_xml)
|
|
if input_type == "omml":
|
|
mathml_list = self.omml_parser.parse(equation_xml)
|
|
# OmmlParser.parse() returns a list - take the first element
|
|
if not mathml_list:
|
|
raise ValueError("OMML parser returned empty result")
|
|
mathml = mathml_list[0]
|
|
else:
|
|
mathml = equation_xml
|
|
mathml_equations.append(mathml)
|
|
|
|
# Convert in parallel
|
|
conversion_results = parallel_converter.convert_batch(mathml_equations, quiet=quiet)
|
|
|
|
# Build result dict
|
|
results = {}
|
|
successful = 0
|
|
failed = 0
|
|
|
|
for key, result in zip(keys, conversion_results):
|
|
if result['success']:
|
|
results[key] = result['latex']
|
|
successful += 1
|
|
else:
|
|
results[key] = ""
|
|
failed += 1
|
|
if not self.verbose and not quiet:
|
|
print(
|
|
f"Warning: Failed to convert {key}: {result.get('error', 'Unknown error')}",
|
|
file=sys.stderr
|
|
)
|
|
|
|
if (self.verbose or failed > 0) and not quiet:
|
|
print(f"\nConversion complete:", file=sys.stderr)
|
|
print(f" Total: {len(input_dict)}", file=sys.stderr)
|
|
print(f" Successful: {successful}", file=sys.stderr)
|
|
print(f" Failed: {failed}", file=sys.stderr)
|
|
print(f" Success rate: {successful/len(input_dict)*100:.1f}%", file=sys.stderr)
|
|
|
|
return results
|