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:
283
scripts/omml_mathml2latex/strategies/physics_strategy.py
Normal file
283
scripts/omml_mathml2latex/strategies/physics_strategy.py
Normal file
@@ -0,0 +1,283 @@
|
||||
"""
|
||||
Physics strategy for physical equations with units and constants.
|
||||
|
||||
This strategy handles formatting for physics equations, including physical
|
||||
constants, SI units, vector notation, and standard physics symbols.
|
||||
"""
|
||||
|
||||
from lxml import etree as ET
|
||||
from typing import Set, Dict
|
||||
from ..core.interfaces import Strategy
|
||||
|
||||
# Import from existing data modules
|
||||
from ..data import UNICODE_TO_LATEX, MATH_FUNCTIONS, COMMON_UNITS, UNICODE_PASSTHROUGH
|
||||
|
||||
|
||||
class PhysicsStrategy(Strategy):
|
||||
"""
|
||||
Physical equation notation strategy.
|
||||
|
||||
This strategy provides formatting for physics content:
|
||||
- Physical constants → proper formatting (c, h, ħ, kB)
|
||||
- SI units → \\mathrm{} (upright)
|
||||
- Mathematical functions → \\function
|
||||
- Variables remain italic (LaTeX default)
|
||||
- Vector notation support
|
||||
"""
|
||||
|
||||
# Mathematical functions that get special LaTeX commands
|
||||
FUNCTIONS: Set[str] = MATH_FUNCTIONS
|
||||
|
||||
# Common physical constants (typically italic in physics)
|
||||
PHYSICAL_CONSTANTS: Set[str] = {
|
||||
'c', # speed of light
|
||||
'h', # Planck's constant
|
||||
'ℏ', # reduced Planck's constant (hbar)
|
||||
'G', # gravitational constant
|
||||
'e', # elementary charge
|
||||
'k', # Boltzmann constant (also kB)
|
||||
'kB', # Boltzmann constant
|
||||
'NA', # Avogadro's number
|
||||
'R', # gas constant
|
||||
'ε0', # permittivity of free space
|
||||
'μ0', # permeability of free space
|
||||
'g', # gravitational acceleration
|
||||
}
|
||||
|
||||
# SI units (upright formatting)
|
||||
UNITS: Set[str] = COMMON_UNITS
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the physics strategy."""
|
||||
pass
|
||||
|
||||
def format_identifier(self, text: str, node: ET.Element) -> str:
|
||||
"""
|
||||
Format an identifier (<mi> element) for physics.
|
||||
|
||||
Args:
|
||||
text: The identifier text content
|
||||
node: The <mi> element node
|
||||
|
||||
Returns:
|
||||
Formatted LaTeX string
|
||||
|
||||
Logic:
|
||||
1. Mathematical functions → \\function (e.g., "sin" → "\\sin")
|
||||
2. SI units → \\mathrm{unit} (e.g., "kg" → "\\mathrm{kg}")
|
||||
3. Physical constants → unchanged (remain italic)
|
||||
4. Single letters → unchanged (variables remain italic)
|
||||
5. Multi-letter non-units → check context
|
||||
|
||||
Examples:
|
||||
"sin" → "\\sin"
|
||||
"kg" → "\\mathrm{kg}"
|
||||
"m" → "m" (could be mass or meter, context-dependent)
|
||||
"c" → "c" (speed of light, italic)
|
||||
"force" → "force" (variable name, italic)
|
||||
"""
|
||||
# Check if it's a mathematical function
|
||||
if text in self.FUNCTIONS:
|
||||
return f"\\{text}"
|
||||
|
||||
# Check if it's an SI unit (upright)
|
||||
if text in self.UNITS:
|
||||
return f"\\mathrm{{{text}}}"
|
||||
|
||||
# UNICODE_PASSTHROUGH: Keep certain symbols as Unicode for MathJax compatibility
|
||||
if text in UNICODE_PASSTHROUGH:
|
||||
return text
|
||||
|
||||
# Check for Unicode symbols
|
||||
if text in UNICODE_TO_LATEX:
|
||||
return UNICODE_TO_LATEX[text]
|
||||
|
||||
# Special physics symbols
|
||||
if text == 'ℏ': # hbar
|
||||
return '\\hbar'
|
||||
elif text == 'ħ': # alternative hbar
|
||||
return '\\hbar'
|
||||
|
||||
# Physical constants remain italic (default LaTeX)
|
||||
if text in self.PHYSICAL_CONSTANTS:
|
||||
return text
|
||||
|
||||
# Handle multi-character strings
|
||||
if len(text) > 1:
|
||||
# Check if it contains Unicode (passthrough or convertible)
|
||||
has_unicode = any(char in UNICODE_TO_LATEX for char in text)
|
||||
has_passthrough = any(char in UNICODE_PASSTHROUGH for char in text)
|
||||
if has_unicode or has_passthrough:
|
||||
converted_parts = []
|
||||
for char in text:
|
||||
if char in UNICODE_PASSTHROUGH:
|
||||
converted_parts.append(char)
|
||||
elif char in UNICODE_TO_LATEX:
|
||||
converted_parts.append(UNICODE_TO_LATEX[char])
|
||||
elif char.isspace():
|
||||
converted_parts.append(' ')
|
||||
else:
|
||||
converted_parts.append(char)
|
||||
return ' '.join(converted_parts)
|
||||
|
||||
# Check if it looks like a unit (multiple known units combined)
|
||||
# e.g., "kg·m/s²" → should be upright
|
||||
# For now, let units be upright if explicitly in UNITS set
|
||||
# Otherwise, remain italic (variable names)
|
||||
|
||||
# Default: return as-is (variables remain italic in LaTeX)
|
||||
return text
|
||||
|
||||
def format_operator(self, text: str, node: ET.Element) -> str:
|
||||
"""
|
||||
Format an operator (<mo> element) for physics.
|
||||
|
||||
Args:
|
||||
text: The operator text content
|
||||
node: The <mo> element node
|
||||
|
||||
Returns:
|
||||
Formatted LaTeX string
|
||||
|
||||
Logic:
|
||||
1. Unicode symbols → LaTeX equivalents (via UNICODE_TO_LATEX)
|
||||
2. Standard operators → unchanged (+, -, *, /, =)
|
||||
3. Vector operators → proper notation (⃗, ×, ·)
|
||||
|
||||
Examples:
|
||||
"+" → "+"
|
||||
"×" → "\\times"
|
||||
"·" → "\\cdot"
|
||||
"∇" → "\\nabla"
|
||||
"∂" → "\\partial"
|
||||
"""
|
||||
# Handle < and > specifically (LaTeX convention)
|
||||
if text == "<":
|
||||
return "\\lt"
|
||||
if text == ">":
|
||||
return "\\gt"
|
||||
|
||||
# Vector cross product
|
||||
if text == '×':
|
||||
return '\\times'
|
||||
elif text == '·':
|
||||
return '\\cdot'
|
||||
|
||||
# UNICODE_PASSTHROUGH: Keep certain symbols as Unicode for MathJax compatibility
|
||||
if text in UNICODE_PASSTHROUGH:
|
||||
return text
|
||||
|
||||
# Check for Unicode symbol mapping
|
||||
if text in UNICODE_TO_LATEX:
|
||||
return UNICODE_TO_LATEX[text]
|
||||
|
||||
# Handle multi-character operators with Unicode
|
||||
if len(text) > 1:
|
||||
has_unicode = any(char in UNICODE_TO_LATEX for char in text)
|
||||
has_passthrough = any(char in UNICODE_PASSTHROUGH for char in text)
|
||||
if has_unicode or has_passthrough:
|
||||
converted_parts = []
|
||||
for char in text:
|
||||
if char in UNICODE_PASSTHROUGH:
|
||||
converted_parts.append(char)
|
||||
elif char in UNICODE_TO_LATEX:
|
||||
converted_parts.append(UNICODE_TO_LATEX[char])
|
||||
else:
|
||||
converted_parts.append(char)
|
||||
return ' '.join(converted_parts)
|
||||
|
||||
# Default: return as-is
|
||||
return text
|
||||
|
||||
def format_number(self, text: str, node: ET.Element) -> str:
|
||||
"""
|
||||
Format a number (<mn> element) for physics.
|
||||
|
||||
Args:
|
||||
text: The number text content
|
||||
node: The <mn> element node
|
||||
|
||||
Returns:
|
||||
Formatted LaTeX string
|
||||
|
||||
Logic:
|
||||
Numbers are returned as-is in physics mode.
|
||||
|
||||
Examples:
|
||||
"9.8" → "9.8" (gravitational acceleration)
|
||||
"3.0×10⁸" → "3.0×10⁸" (speed of light)
|
||||
"""
|
||||
# Numbers are returned as-is
|
||||
return text
|
||||
|
||||
def detect_context(self, equation_text: str) -> bool:
|
||||
"""
|
||||
Detect if an equation is physics content.
|
||||
|
||||
Args:
|
||||
equation_text: The full equation text
|
||||
|
||||
Returns:
|
||||
True if likely physics, False otherwise
|
||||
|
||||
Detection indicators:
|
||||
- SI units (kg, m, s, N, J, etc.)
|
||||
- Physical constants (c, h, G, etc.)
|
||||
- Vector operators (∇, ∂, ×, ·)
|
||||
- Common physics notation
|
||||
|
||||
Examples:
|
||||
"F = ma" → True (if 'a' is acceleration)
|
||||
"E = mc²" → True (energy-mass equivalence)
|
||||
"∇×B = μ₀J" → True (Maxwell's equation)
|
||||
"sin(x) + cos(x)" → False
|
||||
"T = C" → False (just math variables, not Tesla/Coulomb)
|
||||
"""
|
||||
# CRITICAL: Check if we're in a mathematical context first
|
||||
# If equation contains math functions like sin, cos, tan, log, etc.,
|
||||
# it's likely a pure math equation, not physics
|
||||
# This prevents false positives like "C = 2πr" being detected as physics
|
||||
if any(func in equation_text for func in ['sin', 'cos', 'tan', 'cot', 'sec', 'csc',
|
||||
'arcsin', 'arccos', 'arctan',
|
||||
'sinh', 'cosh', 'tanh',
|
||||
'log', 'ln', 'lg', 'exp',
|
||||
'lim', 'max', 'min', 'sup', 'inf']):
|
||||
return False
|
||||
|
||||
# CRITICAL: Filter out single-letter units that are too ambiguous
|
||||
# 'C', 'T', 'P', 'A', 'K', 'S', 'H', 'N', 'V', 'F', 'W', 'J' can all be variables!
|
||||
# Only check for multi-character units or units in clear physics context
|
||||
ambiguous_single_letters = {'C', 'T', 'P', 'A', 'K', 'S', 'H', 'N', 'V', 'F', 'W', 'J', 'L', 'M', 'R', 'G'}
|
||||
|
||||
# Check for UNAMBIGUOUS multi-character units
|
||||
unambiguous_units = {u for u in self.UNITS if len(u) > 1 or u not in ambiguous_single_letters}
|
||||
unit_count = sum(1 for unit in unambiguous_units if unit in equation_text)
|
||||
if unit_count >= 1:
|
||||
return True
|
||||
|
||||
# Check for physical constants (these are less ambiguous)
|
||||
const_count = sum(1 for const in self.PHYSICAL_CONSTANTS if const in equation_text)
|
||||
if const_count >= 1:
|
||||
return True
|
||||
|
||||
# Check for vector operators
|
||||
if any(op in equation_text for op in ['∇', '∂', '×', '·', '⃗']):
|
||||
return True
|
||||
|
||||
# Check for common physics patterns (units with superscripts: m/s², kg·m, etc.)
|
||||
import re
|
||||
if re.search(r'(kg|m|s|N|J|W|Pa|V|A|K|mol|cd)[²³⁴]', equation_text):
|
||||
return True
|
||||
if re.search(r'(m|kg|s|N|J)/s', equation_text):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Get the strategy name."""
|
||||
return "physics"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""String representation for debugging."""
|
||||
return f"PhysicsStrategy(units={len(self.UNITS)}, constants={len(self.PHYSICAL_CONSTANTS)})"
|
||||
Reference in New Issue
Block a user