服务: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 默认密钥已清空
260 lines
9.3 KiB
Python
260 lines
9.3 KiB
Python
"""
|
|
Chemistry equation detection utilities.
|
|
|
|
This module provides functions to detect whether an equation is chemistry-related
|
|
based on strong indicators like multi-char elements, charges, and reaction arrows.
|
|
"""
|
|
|
|
from lxml import etree as ET
|
|
from typing import Optional, Tuple
|
|
from ..data.chemistry_data import (
|
|
MULTI_CHAR_ELEMENTS,
|
|
ALL_CHEMICAL_ELEMENTS,
|
|
CHEMICAL_GROUPS,
|
|
CHINESE_CHEMICAL_TERMS,
|
|
)
|
|
|
|
|
|
def detect_chemistry_equation(root: ET.Element) -> bool:
|
|
"""
|
|
Detect if equation is chemistry-related using STRONG indicators only.
|
|
|
|
DISABLED: Subject detection is obsolete. We rely on mathvariant="normal" only.
|
|
Chemistry formulas are properly formatted when MathML includes mathvariant="normal".
|
|
|
|
Args:
|
|
root: The root MathML element (<math>)
|
|
|
|
Returns:
|
|
False - always (chemistry detection disabled)
|
|
"""
|
|
# DISABLED: Always return False - no heuristic chemistry detection
|
|
return False
|
|
|
|
# OLD CODE (kept for reference):
|
|
# Strong indicators:
|
|
# 1. Multi-char chemical elements in single <mi> tag (Cu, Al, Fe, Mn, etc.)
|
|
# 2. Chemical groups (OH, SO, NO, CO, NH)
|
|
# 3. Element + numeric subscript (H₂O pattern)
|
|
# 4. Electric charges in superscript (^{2+}, ^{2-})
|
|
# 5. Chemical arrows (↑, ↓)
|
|
# 6. Chinese chemical terms (高温, 电解, etc.)
|
|
#
|
|
# for node in root.iter():
|
|
# # Remove namespace if present
|
|
# tag = node.tag.split('}')[-1] if '}' in node.tag else node.tag
|
|
# text = (node.text or '').strip()
|
|
#
|
|
# # Strong indicator 1: Multi-char element in single <mi>
|
|
# if tag == 'mi' and text in MULTI_CHAR_ELEMENTS:
|
|
# return True
|
|
#
|
|
# # Strong indicator 2: Chemical group
|
|
# if tag == 'mtext' and text in CHEMICAL_GROUPS:
|
|
# return True
|
|
#
|
|
# # Strong indicator 3: Element + numeric subscript (H₂O, SO₄ pattern)
|
|
# if tag == 'msub' and len(node) >= 2:
|
|
# base_node = node[0]
|
|
# sub_node = node[1]
|
|
# base_text = (base_node.text or '').strip()
|
|
# sub_text = (sub_node.text or '').strip()
|
|
#
|
|
# # Element with numeric subscript
|
|
# if base_text in ALL_CHEMICAL_ELEMENTS and sub_text.isdigit():
|
|
# return True
|
|
#
|
|
# # Strong indicator 4: Electric charge superscript (Cu^{2+}, Mn^{2-})
|
|
# if tag == 'msup' and len(node) >= 2:
|
|
# sup_node = node[1]
|
|
# sup_text = (sup_node.text or '').strip()
|
|
#
|
|
# # Check for charge notation (contains + or -)
|
|
# # Pattern: "2+", "+", "2-", "-", "3+", etc.
|
|
# if sup_text and ('+' in sup_text or '-' in sup_text):
|
|
# # Additional check: ensure it's likely a charge, not math
|
|
# # Charges are typically: digit* followed by +/-
|
|
# # Math would be expressions like "x+1", "y-2"
|
|
# # Simple heuristic: if it's short and has +/- at end, it's likely a charge
|
|
# if len(sup_text) <= 3 and (sup_text.endswith('+') or sup_text.endswith('-')):
|
|
# return True
|
|
# # Also check for format: "2 +" or "2 -" (with space)
|
|
# parts = sup_text.split()
|
|
# if len(parts) == 2 and parts[0].isdigit() and parts[1] in ('+', '-'):
|
|
# return True
|
|
#
|
|
# # Strong indicator 5: Chemical arrows (precipitate, gas)
|
|
# if tag == 'mo' and text in ('↑', '↓'):
|
|
# return True
|
|
#
|
|
# # Strong indicator 6: Chinese chemical terms
|
|
# if tag in ('mtext', 'mi') and text in CHINESE_CHEMICAL_TERMS:
|
|
# return True
|
|
#
|
|
# return False
|
|
|
|
|
|
def calculate_chemistry_probability(root: ET.Element, subject_mode: Optional[str] = None) -> float:
|
|
"""
|
|
Calculate chemistry probability score based on multiple indicators.
|
|
|
|
Scoring weights:
|
|
- Ion charge present (^{2+}, ^{3-}): +1.0
|
|
- Multi-char element in single <mi>: +0.8
|
|
- Nearby multi-char element: +0.8
|
|
- Reaction arrow (→, ⇌, ⇄): +0.6
|
|
- Chemical subscripts (H_2, O_2): +0.4
|
|
- Chinese chemical term: +0.3
|
|
- User set --subject chemistry: +0.5
|
|
|
|
Thresholds:
|
|
- Chemistry mode: ≥0.8 (merge fragmented elements)
|
|
- Auto-mode: ≥1.5 (only VERY strong indicators)
|
|
|
|
Args:
|
|
root: The root MathML element (<math>)
|
|
subject_mode: Optional subject mode ('chemistry', 'physics', etc.)
|
|
|
|
Returns:
|
|
Probability score (0.0-3.0+)
|
|
|
|
Examples:
|
|
>>> # M n^{2+} with MnO nearby: 1.0 + 0.8 + 0.8 = 2.6 (strong)
|
|
>>> calculate_chemistry_probability(math_root) # 2.6
|
|
|
|
>>> # Just "M n" with no context: 0.0 (weak)
|
|
>>> calculate_chemistry_probability(math_root) # 0.0
|
|
|
|
>>> # Chemistry mode boost: M n → 0.5 (mode) + context
|
|
>>> calculate_chemistry_probability(math_root, 'chemistry') # 0.5+
|
|
"""
|
|
# DISABLED: Always return 0.0 - no chemistry detection
|
|
return 0.0
|
|
|
|
# OLD CODE (kept for reference):
|
|
# score = 0.0
|
|
# # Mode boost: user explicitly set --subject chemistry
|
|
# if subject_mode == 'chemistry':
|
|
# score += 0.5
|
|
#
|
|
# # Counters for various indicators
|
|
# has_ion_charge = False
|
|
# multi_char_element_count = 0
|
|
# has_reaction_arrow = False
|
|
# chemical_subscript_count = 0
|
|
# has_chinese_term = False
|
|
#
|
|
# for node in root.iter():
|
|
# # Remove namespace if present
|
|
# tag = node.tag.split('}')[-1] if '}' in node.tag else node.tag
|
|
# text = (node.text or '').strip()
|
|
#
|
|
# # Indicator 1: Multi-char element in single <mi> (+0.8 each, max 2)
|
|
# if tag == 'mi' and text in MULTI_CHAR_ELEMENTS:
|
|
# multi_char_element_count += 1
|
|
#
|
|
# # Indicator 2: Chemical group (+0.8)
|
|
# if tag == 'mtext' and text in CHEMICAL_GROUPS:
|
|
# multi_char_element_count += 1
|
|
#
|
|
# # Indicator 3: Element + numeric subscript (H₂O pattern) (+0.4 each, max 3)
|
|
# if tag == 'msub' and len(node) >= 2:
|
|
# base_node = node[0]
|
|
# sub_node = node[1]
|
|
# base_text = (base_node.text or '').strip()
|
|
# sub_text = (sub_node.text or '').strip()
|
|
#
|
|
# if base_text in ALL_CHEMICAL_ELEMENTS and sub_text.isdigit():
|
|
# chemical_subscript_count += 1
|
|
#
|
|
# # Indicator 4: Ion charge superscript (+1.0, strongest indicator)
|
|
# if tag == 'msup' and len(node) >= 2:
|
|
# sup_node = node[1]
|
|
# sup_text = (sup_node.text or '').strip()
|
|
#
|
|
# # Check for charge notation
|
|
# if sup_text and ('+' in sup_text or '-' in sup_text):
|
|
# if len(sup_text) <= 3 and (sup_text.endswith('+') or sup_text.endswith('-')):
|
|
# has_ion_charge = True
|
|
# # Also check for format: "2 +" or "2 -"
|
|
# parts = sup_text.split()
|
|
# if len(parts) == 2 and parts[0].isdigit() and parts[1] in ('+', '-'):
|
|
# has_ion_charge = True
|
|
#
|
|
# # Indicator 5: Reaction arrows (+0.6)
|
|
# if tag == 'mo':
|
|
# # Chemical arrows (precipitate, gas)
|
|
# if text in ('↑', '↓'):
|
|
# has_reaction_arrow = True
|
|
# # Reaction arrows (→, ⇌, ⇄, etc.)
|
|
# if text in ('→', '←', '⇌', '⇄', '⟶', '⟵', '⇀', '↽'):
|
|
# has_reaction_arrow = True
|
|
#
|
|
# # Indicator 6: Chinese chemical terms (+0.3)
|
|
# if tag in ('mtext', 'mi') and text in CHINESE_CHEMICAL_TERMS:
|
|
# has_chinese_term = True
|
|
#
|
|
# # Calculate total score
|
|
# if has_ion_charge:
|
|
# score += 1.0
|
|
#
|
|
# # Cap multi-char elements at 2 (prevent over-scoring)
|
|
# score += min(multi_char_element_count, 2) * 0.8
|
|
#
|
|
# if has_reaction_arrow:
|
|
# score += 0.6
|
|
#
|
|
# # Cap chemical subscripts at 3
|
|
# score += min(chemical_subscript_count, 3) * 0.4
|
|
#
|
|
# if has_chinese_term:
|
|
# score += 0.3
|
|
#
|
|
# return score
|
|
|
|
|
|
def detect_chemistry_equation_with_probability(root: ET.Element) -> Tuple[bool, float]:
|
|
"""
|
|
Detect chemistry equation and return both boolean and probability score.
|
|
|
|
DISABLED: Subject detection is obsolete.
|
|
|
|
Args:
|
|
root: The root MathML element (<math>)
|
|
|
|
Returns:
|
|
Tuple of (False, 0.0) - always (chemistry detection disabled)
|
|
"""
|
|
# DISABLED: Always return (False, 0.0)
|
|
return False, 0.0
|
|
|
|
# OLD CODE (kept for reference):
|
|
# probability = calculate_chemistry_probability(root)
|
|
# is_chemistry = probability >= 1.0 # Strong indicator threshold
|
|
# return is_chemistry, probability
|
|
|
|
|
|
def is_chemistry_variable(text: str, in_chemistry_context: bool) -> bool:
|
|
"""
|
|
Determine if text is a chemistry variable (should stay italic).
|
|
|
|
DISABLED: Chemistry detection is obsolete.
|
|
|
|
Args:
|
|
text: The identifier text
|
|
in_chemistry_context: Whether we're in a chemistry equation (ignored)
|
|
|
|
Returns:
|
|
False - always (not used anymore)
|
|
"""
|
|
# DISABLED: Always return False - chemistry detection disabled
|
|
return False
|
|
|
|
# OLD CODE (kept for reference):
|
|
# from ..data.chemistry_data import CHEMISTRY_VARIABLES
|
|
# # In chemistry context, check if it's a known variable
|
|
# if in_chemistry_context:
|
|
# return text in CHEMISTRY_VARIABLES
|
|
# # Not in chemistry context, not a chemistry variable
|
|
# return False
|