Files
Quentin 3271fee5cc 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 默认密钥已清空
2026-09-29 09:43:13 +08:00

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