Files
DoLuEdu-WordFileParse/scripts/omml_mathml2latex/utils/chemistry_detection.py
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

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