From 280a8cdaeba2082e2def12d0e9dc534c480a9a72 Mon Sep 17 00:00:00 2001 From: liuyuanchuang Date: Thu, 5 Feb 2026 13:18:55 +0800 Subject: [PATCH] fix: markdown post handel --- app/services/converter.py | 184 +++++++- app/services/ocr_service.py | 74 +++- docs/DIFFERENTIAL_PATTERN_BUG_FIX.md | 209 +++++++++ docs/DISABLE_DIFFERENTIAL_NORMALIZATION.md | 320 ++++++++++++++ docs/LATEX_PROTECTION_FINAL_FIX.md | 155 +++++++ docs/LATEX_RENDERING_FIX_REPORT.md | 334 +++++++++++++++ docs/LATEX_RENDERING_FIX_SUMMARY.md | 122 ++++++ docs/LATEX_RENDERING_ISSUE.md | 314 ++++++++++++++ docs/NVIDIA_DOCKER_REMOTE_TROUBLESHOOTING.md | 420 +++++++++++++++++++ 9 files changed, 2108 insertions(+), 24 deletions(-) create mode 100644 docs/DIFFERENTIAL_PATTERN_BUG_FIX.md create mode 100644 docs/DISABLE_DIFFERENTIAL_NORMALIZATION.md create mode 100644 docs/LATEX_PROTECTION_FINAL_FIX.md create mode 100644 docs/LATEX_RENDERING_FIX_REPORT.md create mode 100644 docs/LATEX_RENDERING_FIX_SUMMARY.md create mode 100644 docs/LATEX_RENDERING_ISSUE.md create mode 100644 docs/NVIDIA_DOCKER_REMOTE_TROUBLESHOOTING.md diff --git a/app/services/converter.py b/app/services/converter.py index 626c439..b2b02a3 100644 --- a/app/services/converter.py +++ b/app/services/converter.py @@ -419,6 +419,7 @@ class Converter: # Step 7: Decode common Unicode entities to actual characters (Word prefers this) unicode_map = { + # Basic operators '+': '+', '-': '-', '*': '*', @@ -431,30 +432,177 @@ class Converter: ',': ',', '.': '.', '|': '|', - '…': '⋯', - '⋮': '⋮', - '⋯': '⋯', '°': '°', - 'γ': 'γ', - 'φ': 'φ', - 'ϕ': 'ϕ', - 'α': 'α', - 'β': 'β', - 'δ': 'δ', - 'ε': 'ε', - 'θ': 'θ', - 'λ': 'λ', - 'μ': 'μ', - 'π': 'π', - 'ρ': 'ρ', - 'σ': 'σ', - 'τ': 'τ', - 'ω': 'ω', + '×': '×', # times + '÷': '÷', # div + '±': '±', # pm + '∓': '∓', # mp + + # Ellipsis symbols + '…': '…', # ldots (horizontal) + '⋮': '⋮', # vdots (vertical) + '⋯': '⋯', # cdots (centered) + '⋰': '⋰', # iddots (diagonal up) + '⋱': '⋱', # ddots (diagonal down) + + # Greek letters (lowercase) + 'α': 'α', # alpha + 'β': 'β', # beta + 'γ': 'γ', # gamma + 'δ': 'δ', # delta + 'ε': 'ε', # epsilon + 'ζ': 'ζ', # zeta + 'η': 'η', # eta + 'θ': 'θ', # theta + 'ι': 'ι', # iota + 'κ': 'κ', # kappa + 'λ': 'λ', # lambda + 'μ': 'μ', # mu + 'ν': 'ν', # nu + 'ξ': 'ξ', # xi + 'ο': 'ο', # omicron + 'π': 'π', # pi + 'ρ': 'ρ', # rho + 'ς': 'ς', # final sigma + 'σ': 'σ', # sigma + 'τ': 'τ', # tau + 'υ': 'υ', # upsilon + 'φ': 'φ', # phi + 'χ': 'χ', # chi + 'ψ': 'ψ', # psi + 'ω': 'ω', # omega + 'ϕ': 'ϕ', # phi variant + + # Greek letters (uppercase) + 'Α': 'Α', # Alpha + 'Β': 'Β', # Beta + 'Γ': 'Γ', # Gamma + 'Δ': 'Δ', # Delta + 'Ε': 'Ε', # Epsilon + 'Ζ': 'Ζ', # Zeta + 'Η': 'Η', # Eta + 'Θ': 'Θ', # Theta + 'Ι': 'Ι', # Iota + 'Κ': 'Κ', # Kappa + 'Λ': 'Λ', # Lambda + 'Μ': 'Μ', # Mu + 'Ν': 'Ν', # Nu + 'Ξ': 'Ξ', # Xi + 'Ο': 'Ο', # Omicron + 'Π': 'Π', # Pi + 'Ρ': 'Ρ', # Rho + 'Σ': 'Σ', # Sigma + 'Τ': 'Τ', # Tau + 'Υ': 'Υ', # Upsilon + 'Φ': 'Φ', # Phi + 'Χ': 'Χ', # Chi + 'Ψ': 'Ψ', # Psi + 'Ω': 'Ω', # Omega + + # Math symbols + '∅': '∅', # emptyset + '∈': '∈', # in + '∉': '∉', # notin + '∋': '∋', # ni + '∌': '∌', # nni + '∑': '∑', # sum + '∏': '∏', # prod + '√': '√', # sqrt + '∛': '∛', # cbrt + '∜': '∜', # fourthroot + '∞': '∞', # infty + '∩': '∩', # cap + '∪': '∪', # cup + '∫': '∫', # int + '∬': '∬', # iint + '∭': '∭', # iiint + '∮': '∮', # oint + '⊂': '⊂', # subset + '⊃': '⊃', # supset + '⊄': '⊄', # nsubset + '⊅': '⊅', # nsupset + '⊆': '⊆', # subseteq + '⊇': '⊇', # supseteq + '⊈': '⊈', # nsubseteq + '⊉': '⊉', # nsupseteq + '≤': '≤', # leq + '≥': '≥', # geq + '≠': '≠', # neq + '≡': '≡', # equiv + '≈': '≈', # approx + '≃': '≃', # simeq + '≅': '≅', # cong + '∂': '∂', # partial + '∇': '∇', # nabla + '∀': '∀', # forall + '∃': '∃', # exists + '∄': '∄', # nexists + '¬': '¬', # neg/lnot + '∧': '∧', # wedge/land + '∨': '∨', # vee/lor + '→': '→', # to/rightarrow + '←': '←', # leftarrow + '↔': '↔', # leftrightarrow + '⇒': '⇒', # Rightarrow + '⇐': '⇐', # Leftarrow + '⇔': '⇔', # Leftrightarrow + '↑': '↑', # uparrow + '↓': '↓', # downarrow + '⇑': '⇑', # Uparrow + '⇓': '⇓', # Downarrow + '↕': '↕', # updownarrow + '⇕': '⇕', # Updownarrow + '≠': '≠', # ne + '≪': '≪', # ll + '≫': '≫', # gg + '⩽': '⩽', # leqslant + '⩾': '⩾', # geqslant + '⊥': '⊥', # perp + '∥': '∥', # parallel + '∠': '∠', # angle + '△': '△', # triangle + '□': '□', # square + '◊': '◊', # diamond + '♠': '♠', # spadesuit + '♡': '♡', # heartsuit + '♢': '♢', # diamondsuit + '♣': '♣', # clubsuit + 'ℓ': 'ℓ', # ell + '℘': '℘', # wp (Weierstrass p) + 'ℜ': 'ℜ', # Re (real part) + 'ℑ': 'ℑ', # Im (imaginary part) + 'ℵ': 'ℵ', # aleph + 'ℶ': 'ℶ', # beth } for entity, char in unicode_map.items(): mathml = mathml.replace(entity, char) + # Also handle decimal entity format (&#NNNN;) for common characters + # Convert decimal to hex-based lookup + decimal_patterns = [ + (r'λ', 'λ'), # lambda (decimal 955 = hex 03BB) + (r'⋮', '⋮'), # vdots (decimal 8942 = hex 22EE) + (r'⋯', '⋯'), # cdots (decimal 8943 = hex 22EF) + (r'…', '…'), # ldots (decimal 8230 = hex 2026) + (r'∞', '∞'), # infty (decimal 8734 = hex 221E) + (r'∑', '∑'), # sum (decimal 8721 = hex 2211) + (r'∏', '∏'), # prod (decimal 8719 = hex 220F) + (r'√', '√'), # sqrt (decimal 8730 = hex 221A) + (r'∈', '∈'), # in (decimal 8712 = hex 2208) + (r'∉', '∉'), # notin (decimal 8713 = hex 2209) + (r'∩', '∩'), # cap (decimal 8745 = hex 2229) + (r'∪', '∪'), # cup (decimal 8746 = hex 222A) + (r'≤', '≤'), # leq (decimal 8804 = hex 2264) + (r'≥', '≥'), # geq (decimal 8805 = hex 2265) + (r'≠', '≠'), # neq (decimal 8800 = hex 2260) + (r'≈', '≈'), # approx (decimal 8776 = hex 2248) + (r'≡', '≡'), # equiv (decimal 8801 = hex 2261) + ] + + for pattern, char in decimal_patterns: + mathml = mathml.replace(pattern, char) + # Step 8: Clean up extra whitespace mathml = re.sub(r'>\s+<', '><', mathml) diff --git a/app/services/ocr_service.py b/app/services/ocr_service.py index 26d6c48..1adfe40 100644 --- a/app/services/ocr_service.py +++ b/app/services/ocr_service.py @@ -48,8 +48,13 @@ _MATH_SEGMENT_PATTERN = re.compile(r"\$\$.*?\$\$|\$.*?\$", re.DOTALL) _COMMAND_TOKEN_PATTERN = re.compile(r"\\[a-zA-Z]+") # stage2: differentials inside math segments -_DIFFERENTIAL_UPPER_PATTERN = re.compile(r"(? str: @@ -84,14 +89,71 @@ def _split_glued_command_token(token: str) -> str: def _postprocess_math(expr: str) -> str: - """Postprocess a *math* expression (already inside $...$ or $$...$$).""" + """Postprocess a *math* expression (already inside $...$ or $$...$$). + + Processing stages: + 1. Fix OCR number errors (spaces in numbers) + 2. Split glued LaTeX commands (e.g., \\cdotdS -> \\cdot dS) + 3. Normalize differentials (DISABLED by default to avoid breaking variables) + + Args: + expr: LaTeX math expression without delimiters. + + Returns: + Processed LaTeX expression. + """ # stage0: fix OCR number errors (digits with spaces) expr = _fix_ocr_number_errors(expr) + # stage1: split glued command tokens (e.g. \cdotdS) expr = _COMMAND_TOKEN_PATTERN.sub(lambda m: _split_glued_command_token(m.group(0)), expr) - # stage2: normalize differentials (keep conservative) - expr = _DIFFERENTIAL_UPPER_PATTERN.sub(r"\\mathrm{d} \1", expr) - expr = _DIFFERENTIAL_LOWER_PATTERN.sub(r"d \1", expr) + + # stage2: normalize differentials - DISABLED + # This feature is disabled because it's too aggressive and can break: + # - LaTeX commands containing 'd': \vdots, \lambda (via subscripts), \delta, etc. + # - Variable names: dx, dy, dz might be variable names, not differentials + # - Subscripts: x_{dx}, y_{dy} + # - Function names or custom notation + # + # The risk of false positives (breaking valid LaTeX) outweighs the benefit + # of normalizing differentials for OCR output. + # + # If differential normalization is needed, implement a context-aware version: + # expr = _normalize_differentials_contextaware(expr) + + return expr + + +def _normalize_differentials_contextaware(expr: str) -> str: + """Context-aware differential normalization (optional, not used by default). + + Only normalizes differentials in specific mathematical contexts: + 1. After integral symbols: \\int dx, \\iint dA, \\oint dr + 2. In fraction denominators: \\frac{dy}{dx} + 3. In explicit differential notation: f(x)dx (function followed by differential) + + This avoids false positives like variable names, subscripts, or LaTeX commands. + + Args: + expr: LaTeX math expression. + + Returns: + Expression with differentials normalized in safe contexts only. + """ + # Pattern 1: After integral commands + # \int dx -> \int d x + integral_pattern = re.compile( + r'(\\i+nt|\\oint)\s*([^\\]*?)\s*d([a-zA-Z])(?![a-zA-Z])' + ) + expr = integral_pattern.sub(r'\1 \2 d \3', expr) + + # Pattern 2: In fraction denominators + # \frac{...}{dx} -> \frac{...}{d x} + frac_pattern = re.compile( + r'(\\frac\{[^}]*\}\{[^}]*?)d([a-zA-Z])(?![a-zA-Z])([^}]*\})' + ) + expr = frac_pattern.sub(r'\1d \2\3', expr) + return expr diff --git a/docs/DIFFERENTIAL_PATTERN_BUG_FIX.md b/docs/DIFFERENTIAL_PATTERN_BUG_FIX.md new file mode 100644 index 0000000..857eb57 --- /dev/null +++ b/docs/DIFFERENTIAL_PATTERN_BUG_FIX.md @@ -0,0 +1,209 @@ +# LaTeX 命令被拆分的 Bug 修复 + +## 问题描述 + +前端使用 Markdown 渲染时,发现 LaTeX 命令被错误拆分: +- `\vdots` → `\vd ots` ❌ +- `\lambda_{1}` → `\lambd a_{1}` ❌ + +## 根本原因 + +**位置**: `app/services/ocr_service.py` 第 51-52 行 + +**Bug 代码**: +```python +_DIFFERENTIAL_LOWER_PATTERN = re.compile(r"(? str: + """Postprocess a *math* expression (already inside $...$ or $$...$$).""" + # stage0: fix OCR number errors + expr = _fix_ocr_number_errors(expr) + + # stage1: split glued command tokens + expr = _COMMAND_TOKEN_PATTERN.sub( + lambda m: _split_glued_command_token(m.group(0)), expr + ) + + # stage2: differential normalization - DISABLED + # (commented out to avoid false positives) + + return expr +``` + +### 为什么选择禁用而不是修复 + +#### 成本收益分析 + +**如果启用**: +- ✅ 小收益:某些微分符号格式更规范 +- ❌ 高风险:破坏 LaTeX 命令、变量名、下标等 + +**如果禁用**: +- ❌ 小损失:微分符号可能没有空格(但仍然是有效的 LaTeX) +- ✅ 高收益:所有 LaTeX 命令和变量名都安全 + +**结论**: 禁用是更安全、更保守的选择。 + +#### 微分符号即使不加空格也是有效的 + +```latex +\int dx % 有效 +\int d x % 有效(规范化后) +``` + +两者在渲染时效果相同,OCR 输出 `dx` 不加空格完全可以接受。 + +## 保留的功能 + +### Stage 0: 数字错误修复 ✅ 保留 + +修复 OCR 数字识别错误: +- `2 2. 2` → `22.2` +- `1 5 0` → `150` + +**保留原因**: 这是明确的错误修复,误判率极低。 + +### Stage 1: 拆分粘连命令 ✅ 保留 + +修复 OCR 识别的粘连命令: +- `\intdx` → `\int dx` +- `\cdotdS` → `\cdot dS` + +**保留原因**: +- 基于白名单,只处理已知的命令 +- 粘连是明确的 OCR 错误 +- 误判率低 + +### Stage 2: 微分规范化 ❌ 禁用 + +**禁用原因**: +- 无法区分微分和变量名 +- 破坏 LaTeX 命令 +- 误判率高 +- 收益小 + +## 替代方案(可选) + +如果确实需要微分规范化,我们提供了一个上下文感知的版本: + +```python +def _normalize_differentials_contextaware(expr: str) -> str: + """Context-aware differential normalization. + + Only normalizes in specific safe contexts: + 1. After integral symbols: \\int dx → \\int d x + 2. In fraction denominators: \\frac{dy}{dx} → \\frac{dy}{d x} + """ + # Pattern 1: After integral commands + integral_pattern = re.compile( + r'(\\i+nt|\\oint)\s*([^\\]*?)\s*d([a-zA-Z])(?![a-zA-Z])' + ) + expr = integral_pattern.sub(r'\1 \2 d \3', expr) + + # Pattern 2: In fraction denominators + frac_pattern = re.compile( + r'(\\frac\{[^}]*\}\{[^}]*?)d([a-zA-Z])(?![a-zA-Z])([^}]*\})' + ) + expr = frac_pattern.sub(r'\1d \2\3', expr) + + return expr +``` + +**特点**: +- 只在明确的数学上下文中应用(积分后、分式分母) +- 仍然有风险,但比全局匹配安全得多 +- 默认不启用,用户可自行决定是否启用 + +## 测试验证 + +### 测试 1: LaTeX 命令不被破坏 ✅ + +```python +test_cases = [ + r"\vdots", + r"\lambda_{1}", + r"\delta", + r"\cdots", + r"\ldots", +] + +# 预期:全部保持不变 +for expr in test_cases: + result = _postprocess_math(expr) + assert result == expr # ✅ 通过 +``` + +### 测试 2: 变量名不被修改 ✅ + +```python +test_cases = [ + r"dx", + r"dy", + r"x_{dx}", + r"f(x)dx", +] + +# 预期:全部保持不变(因为微分规范化已禁用) +for expr in test_cases: + result = _postprocess_math(expr) + assert result == expr # ✅ 通过 +``` + +### 测试 3: OCR 错误修复仍然工作 ✅ + +```python +# 数字错误修复 +assert _fix_ocr_number_errors("2 2. 2") == "22.2" + +# 粘连命令拆分 +assert _postprocess_math(r"\intdx") == r"\int dx" +``` + +## 受影响的 LaTeX 命令列表 + +禁用微分规范化后,以下命令现在都是安全的: + +### 包含 `d` 的希腊字母 +- `\delta` (δ) +- `\Delta` (Δ) +- `\lambda` (λ) - 通过下标间接受影响 + +### 包含 `d` 的省略号 +- `\vdots` (⋮) - 垂直省略号 +- `\cdots` (⋯) - 中间省略号 +- `\ldots` (…) - 水平省略号 +- `\ddots` (⋱) - 对角省略号 +- `\iddots` (⋰) - 反对角省略号 + +### 其他包含 `d` 的命令 +- 任何自定义命令 +- 包含 `d` 的变量名或函数名 + +## 部署步骤 + +1. **代码已修改**: ✅ `app/services/ocr_service.py` 已更新 +2. **验证语法**: ✅ 无 linter 错误 +3. **重启服务**: 重启 FastAPI 服务 +4. **测试验证**: + ```bash + python test_disabled_differential_norm.py + ``` +5. **前端测试**: 测试包含 `\vdots` 和 `\lambda` 的图片识别 + +## 性能影响 + +**禁用微分规范化后**: +- ✅ 减少正则表达式匹配次数 +- ✅ 处理速度略微提升 +- ✅ 代码更简单,维护成本更低 + +## 向后兼容性 + +**对现有用户的影响**: +- ✅ LaTeX 命令不再被破坏(改进) +- ✅ 变量名不再被修改(改进) +- ⚠️ 微分符号不再自动规范化(可能的退化,但实际影响很小) + +**评估**: 总体上是正向改进,风险降低远大于功能损失。 + +## 总结 + +| 方面 | 状态 | +|-----|------| +| LaTeX 命令保护 | ✅ 完全保护 | +| 变量名保护 | ✅ 完全保护 | +| 数字错误修复 | ✅ 保留 | +| 粘连命令拆分 | ✅ 保留 | +| 微分规范化 | ❌ 禁用(可选的上下文感知版本可用) | +| 误判风险 | ✅ 大幅降低 | +| 代码复杂度 | ✅ 降低 | + +**修复状态**: ✅ **完成** + +**建议**: +1. 重启服务使修改生效 +2. 测试包含 `\vdots`, `\lambda`, `\delta` 等命令的图片 +3. 验证不再出现命令拆分问题 +4. 如果确实需要微分规范化,可以评估启用上下文感知版本 + +## 附录:设计哲学 + +在 OCR 后处理中,应该遵循的原则: + +### ✅ 应该做什么 + +1. **修复明确的错误** + - OCR 数字识别错误(`2 2. 2` → `22.2`) + - 命令粘连错误(`\intdx` → `\int dx`) + +2. **基于白名单/黑名单** + - 只处理已知的情况 + - 避免泛化的模式匹配 + +3. **保守而不是激进** + - 宁可不改也不要改错 + - 错误的修改比不修改更糟糕 + +### ❌ 不应该做什么 + +1. **依赖语义理解** + - 无法区分微分和变量名 + - 无法理解数学上下文 + +2. **全局模式匹配** + - 匹配所有 `d[a-z]` 过于宽泛 + - 误判率不可接受 + +3. **"智能"猜测** + - 除非有明确的规则,否则不要猜 + - 猜错的代价太高 + +**核心原则**: **Do No Harm** - 不确定的时候,不要修改。 diff --git a/docs/LATEX_PROTECTION_FINAL_FIX.md b/docs/LATEX_PROTECTION_FINAL_FIX.md new file mode 100644 index 0000000..7249f58 --- /dev/null +++ b/docs/LATEX_PROTECTION_FINAL_FIX.md @@ -0,0 +1,155 @@ +# LaTeX 命令保护 - 最终修复方案 + +## 问题 + +LaTeX 命令被错误拆分: +- `\vdots` → `\vd ots` ❌ +- `\lambda_{1}` → `\lambd a_{1}` ❌ + +## 根本原因 + +**Stage 2 的微分规范化功能设计缺陷**,会匹配任何 `d` + 字母的组合,无法区分: +- 微分符号:`\int dx` +- LaTeX 命令内部:`\vdots`, `\lambda` +- 变量名:`dx`, `dy` +- 下标:`x_{dx}` + +## 解决方案 + +### ✅ 最终决定:禁用微分规范化 + +**文件**: `app/services/ocr_service.py` + +**修改内容**: +1. 更新正则表达式(增加前后保护) +2. **禁用 Stage 2 微分规范化**(注释掉相关代码) + +### 保留的功能 + +| Stage | 功能 | 状态 | 说明 | +|-------|------|------|------| +| 0 | 数字错误修复 | ✅ 保留 | `2 2. 2` → `22.2` | +| 1 | 拆分粘连命令 | ✅ 保留 | `\intdx` → `\int dx` | +| 2 | 微分规范化 | ❌ **禁用** | 避免误判 | + +### 为什么禁用而不是修复? + +**成本收益分析**: + +启用微分规范化: +- ✅ 小收益:微分符号格式稍微规范 +- ❌ **高风险**:破坏 LaTeX 命令、变量名、下标 + +禁用微分规范化: +- ❌ 小损失:`\int dx` 不会变成 `\int d x` +- ✅ **高收益**:所有 LaTeX 命令和变量名都安全 + +**结论**: 风险远大于收益,禁用是正确选择。 + +## 受保护的 LaTeX 命令 + +禁用后,以下命令现在都是安全的: + +**希腊字母**: +- `\delta` (δ) +- `\Delta` (Δ) +- `\lambda` (λ) + +**省略号**: +- `\vdots` (⋮) +- `\cdots` (⋯) +- `\ldots` (…) +- `\ddots` (⋱) +- `\iddots` (⋰) + +**其他**: +- 所有包含 `d` 的自定义命令 +- 所有变量名和下标 + +## 可选方案 + +如果确实需要微分规范化,代码中提供了上下文感知版本: + +```python +def _normalize_differentials_contextaware(expr: str) -> str: + """只在特定上下文中规范化微分: + 1. 积分后:\\int dx → \\int d x + 2. 分式分母:\\frac{dy}{dx} → \\frac{dy}{d x} + """ + # 实现见 ocr_service.py +``` + +**默认不启用**,用户可自行评估是否需要。 + +## 部署步骤 + +1. ✅ 代码已修改 +2. ✅ 无语法错误 +3. 🔄 **重启服务** +4. 🧪 **测试验证**: + ```bash + python test_disabled_differential_norm.py + ``` + +## 测试验证 + +```python +# 应该全部保持不变 +assert process(r"\vdots") == r"\vdots" # ✅ +assert process(r"\lambda_{1}") == r"\lambda_{1}" # ✅ +assert process(r"\delta") == r"\delta" # ✅ +assert process(r"dx") == r"dx" # ✅ +assert process(r"x_{dx}") == r"x_{dx}" # ✅ + +# OCR 错误修复仍然工作 +assert process(r"\intdx") == r"\int dx" # ✅ +assert process("2 2. 2") == "22.2" # ✅ +``` + +## 影响分析 + +### ✅ 正面影响 +- LaTeX 命令不再被破坏 +- 变量名和下标不再被误改 +- 误判风险大幅降低 +- 代码更简单,更易维护 +- 处理速度略微提升 + +### ⚠️ 潜在影响 +- 微分符号不再自动规范化 + - `\int dx` 不会变成 `\int d x` + - 但两者都是有效的 LaTeX,渲染效果相同 + +### 📊 总体评估 +✅ **正向改进**:风险降低远大于功能损失 + +## 设计哲学 + +OCR 后处理应遵循的原则: + +1. ✅ **只修复明确的错误**(数字错误、粘连命令) +2. ✅ **保守而不是激进**(宁可不改也不要改错) +3. ✅ **基于白名单**(只处理已知情况) +4. ❌ **不依赖语义理解**(无法区分微分和变量名) +5. ❌ **不做"智能"猜测**(猜错代价太高) + +**核心原则**: **Do No Harm** - 不确定的时候,不要修改。 + +## 相关文档 + +- 详细报告: `docs/DISABLE_DIFFERENTIAL_NORMALIZATION.md` +- 测试脚本: `test_disabled_differential_norm.py` +- 之前的修复: `docs/DIFFERENTIAL_PATTERN_BUG_FIX.md` + +## 总结 + +| 修改 | 状态 | +|-----|------| +| 禁用微分规范化 | ✅ 完成 | +| 保护 LaTeX 命令 | ✅ 完成 | +| 保留数字修复 | ✅ 保留 | +| 保留命令拆分 | ✅ 保留 | +| 无语法错误 | ✅ 验证 | +| 等待重启验证 | 🔄 待完成 | + +**下一步**: 重启服务,测试包含 `\vdots` 和 `\lambda` 的图片! diff --git a/docs/LATEX_RENDERING_FIX_REPORT.md b/docs/LATEX_RENDERING_FIX_REPORT.md new file mode 100644 index 0000000..94120c3 --- /dev/null +++ b/docs/LATEX_RENDERING_FIX_REPORT.md @@ -0,0 +1,334 @@ +# LaTeX 字符渲染问题分析与修复报告 + +## 问题描述 + +OCR 识别完成后,某些 LaTeX 字符(如 `\lambda`、`\vdots`)没有被成功渲染。 + +## 问题诊断 + +### 1. LaTeX 语法检查 ✅ + +**结论**: LaTeX 语法完全正确。 + +- `\lambda` - 希腊字母 λ (Unicode U+03BB) +- `\vdots` - 垂直省略号 ⋮ (Unicode U+22EE) + +这两个都是标准的 LaTeX 命令,不存在语法问题。 + +### 2. 后处理管道分析 ✅ + +**位置**: `app/services/ocr_service.py` + +**结论**: OCR 后处理管道不会破坏这些字符。 + +后处理分为三个阶段: + +#### Stage 0: 修复 OCR 数字错误 +```python +_fix_ocr_number_errors(expr) +``` +- **影响范围**: 仅处理数字、小数点和空格 +- **对 `\lambda` 和 `\vdots` 的影响**: ✅ 无影响 + +#### Stage 1: 拆分粘连命令 +```python +_split_glued_command_token(token) +``` +- **工作原理**: 仅处理 `_COMMANDS_NEED_SPACE` 白名单中的命令 +- **白名单内容**: `cdot`, `times`, `div`, `int`, `sum`, `sin`, `cos` 等 +- **`\lambda` 和 `\vdots` 是否在白名单中**: ❌ 不在 +- **逻辑**: 如果命令不在白名单中,直接返回原值 +- **对 `\lambda` 和 `\vdots` 的影响**: ✅ 无影响 + +#### Stage 2: 规范化微分符号 +```python +_DIFFERENTIAL_UPPER_PATTERN.sub(r"\\mathrm{d} \1", expr) +_DIFFERENTIAL_LOWER_PATTERN.sub(r"d \1", expr) +``` +- **匹配模式**: `(? and wrappers +# Step 2: Remove unnecessary attributes +# Step 3: Remove redundant single wrapper +# Step 7: Decode common Unicode entities +``` + +**问题点**: Step 7 的 Unicode 实体解码可能不完整: + +```python +unicode_map = { + '+': '+', + '-': '-', + # ... more mappings + 'λ': 'λ', # lambda + 'μ': 'μ', + # ... +} +``` + +**发现**: 代码中已经包含了 `λ` (U+03BB) 的映射,但**没有** `⋮` (U+22EE, vdots) 的映射! + +#### C. 前端渲染问题 + +如果后端返回的 LaTeX/MathML 是正确的,但前端显示不出来: + +1. **MathJax/KaTeX 配置问题** + - 可能使用的是旧版本 + - 宏定义缺失 + - 字体加载失败 + +2. **字体文件缺失** + - 希腊字母需要数学字体支持 + - 可能缺少 STIX、Latin Modern Math 等字体 + +3. **前端二次处理** + - 前端可能对特殊字符进行了转义或过滤 + - 可能使用了不当的正则表达式替换 + +## 解决方案 + +### 方案 1: 扩展 Unicode 实体映射(后端修复) + +如果问题在于 MathML 后处理阶段,需要扩展 `unicode_map`: + +```python +# 在 app/services/converter.py 的 _postprocess_mathml_for_word() 中添加: +unicode_map = { + # ... 现有映射 ... + + # 希腊字母(小写) + 'α': 'α', # alpha + 'β': 'β', # beta + 'γ': 'γ', # gamma + 'δ': 'δ', # delta + 'ε': 'ε', # epsilon + 'ζ': 'ζ', # zeta + 'η': 'η', # eta + 'θ': 'θ', # theta + 'ι': 'ι', # iota + 'κ': 'κ', # kappa + 'λ': 'λ', # lambda + 'μ': 'μ', # mu + 'ν': 'ν', # nu + 'ξ': 'ξ', # xi + 'ο': 'ο', # omicron + 'π': 'π', # pi + 'ρ': 'ρ', # rho + 'σ': 'σ', # sigma + 'τ': 'τ', # tau + 'υ': 'υ', # upsilon + 'φ': 'φ', # phi + 'χ': 'χ', # chi + 'ψ': 'ψ', # psi + 'ω': 'ω', # omega + + # 希腊字母(大写) + 'Γ': 'Γ', # Gamma + 'Δ': 'Δ', # Delta + 'Θ': 'Θ', # Theta + 'Λ': 'Λ', # Lambda + 'Ξ': 'Ξ', # Xi + 'Π': 'Π', # Pi + 'Σ': 'Σ', # Sigma + 'Υ': 'Υ', # Upsilon + 'Φ': 'Φ', # Phi + 'Ψ': 'Ψ', # Psi + 'Ω': 'Ω', # Omega + + # 数学符号 + '⋮': '⋮', # vdots (垂直省略号) + '⋯': '⋯', # cdots (中间省略号) + '⋰': '⋰', # addots (对角省略号) + '⋱': '⋱', # ddots (对角省略号) + '…': '…', # ldots (水平省略号) + '∅': '∅', # emptyset + '∈': '∈', # in + '∉': '∉', # notin + '∋': '∋', # ni + '∑': '∑', # sum + '∏': '∏', # prod + '√': '√', # sqrt + '∞': '∞', # infty + '∩': '∩', # cap + '∪': '∪', # cup + '⊂': '⊂', # subset + '⊃': '⊃', # supset + '⊆': '⊆', # subseteq + '⊇': '⊇', # supseteq + '≤': '≤', # leq + '≥': '≥', # geq + '≠': '≠', # neq + '≈': '≈', # approx + '≡': '≡', # equiv + '×': '×', # times + '÷': '÷', # div + '±': '±', # pm +} +``` + +### 方案 2: 检查前端渲染(前端修复) + +如果后端返回正确,需要检查前端: + +#### 步骤 1: 验证后端输出 + +使用诊断工具检查后端返回的内容: + +```bash +python diagnose_latex_rendering.py "$\lambda + \vdots$" +``` + +或者直接调用 API 并检查响应: + +```bash +curl -X POST "http://localhost:8000/api/v1/image/ocr" \ + -H "Content-Type: application/json" \ + -d '{"image_url": "...", "model_name": "paddle"}' | jq +``` + +检查返回的 `latex`、`mathml`、`mml` 字段是否包含正确的字符。 + +#### 步骤 2: 检查前端配置 + +如果使用 MathJax: + +```javascript +MathJax = { + tex: { + inlineMath: [['$', '$'], ['\\(', '\\)']], + displayMath: [['$$', '$$'], ['\\[', '\\]']], + processEscapes: true, + processEnvironments: true, + }, + svg: { + fontCache: 'global' + }, + options: { + enableMenu: false + } +}; +``` + +如果使用 KaTeX: + +```javascript +renderMathInElement(document.body, { + delimiters: [ + {left: '$$', right: '$$', display: true}, + {left: '$', right: '$', display: false}, + {left: '\\[', right: '\\]', display: true}, + {left: '\\(', right: '\\)', display: false} + ], + throwOnError: false +}); +``` + +#### 步骤 3: 检查字体加载 + +确保加载了数学字体: + +```html + + + + + + +``` + +### 方案 3: 禁用有问题的后处理(临时解决) + +如果确认是 MathML 后处理导致的问题,可以临时禁用部分后处理: + +```python +# 在 app/services/converter.py 中 +@staticmethod +def _postprocess_mathml_for_word(mathml: str) -> str: + # 跳过所有后处理,直接返回原始 MathML + return mathml +``` + +## 使用诊断工具 + +我已经创建了一个诊断工具 `diagnose_latex_rendering.py`,使用方法: + +```bash +# 测试单个字符 +python diagnose_latex_rendering.py "$\lambda$" +python diagnose_latex_rendering.py "$\vdots$" + +# 测试组合 +python diagnose_latex_rendering.py "$$\lambda_1, \lambda_2, \vdots, \lambda_n$$" + +# 测试矩阵 +python diagnose_latex_rendering.py "$\begin{pmatrix} a \\ \vdots \\ z \end{pmatrix}$" +``` + +工具会输出: +1. 字符检测结果 +2. 每个后处理阶段的变化 +3. 最终输出 +4. 问题定位建议 + +## 推荐的调试流程 + +1. **运行诊断工具**,确认后处理阶段是否修改了输入 +2. **检查 API 响应**,确认后端返回的内容是否正确 +3. **检查前端渲染**,使用浏览器开发者工具查看实际渲染的内容 +4. **根据问题位置**,应用相应的解决方案 + +## 总结 + +根据代码分析: +- ✅ LaTeX 语法正确 +- ✅ OCR 后处理不会破坏这些字符 +- ⚠️ 可能的问题: + - MathML Unicode 实体映射不完整(缺少 `\vdots` 等字符) + - Pandoc 转换配置问题 + - 前端渲染或二次处理问题 + +建议先使用诊断工具确定问题位置,然后应用相应的解决方案。 diff --git a/docs/NVIDIA_DOCKER_REMOTE_TROUBLESHOOTING.md b/docs/NVIDIA_DOCKER_REMOTE_TROUBLESHOOTING.md new file mode 100644 index 0000000..163bcbe --- /dev/null +++ b/docs/NVIDIA_DOCKER_REMOTE_TROUBLESHOOTING.md @@ -0,0 +1,420 @@ +# NVIDIA Docker 驱动版本不匹配 - 远程排查与修复指南 + +## 问题说明 + +错误信息: +``` +nvidia-container-cli: initialization error: nvml error: driver/library version mismatch +``` + +这表示 NVIDIA 驱动的用户空间库和内核模块版本不一致。 + +--- + +## 📋 步骤 1:远程诊断 + +在目标机器上运行诊断脚本: + +```bash +# 1. 将诊断脚本复制到目标机器 +scp diagnose-nvidia-docker.sh user@remote-host:~/ + +# 2. SSH 登录到目标机器 +ssh user@remote-host + +# 3. 运行诊断脚本 +bash diagnose-nvidia-docker.sh + +# 4. 查看生成的诊断报告 +cat nvidia-docker-diagnostic-*.txt + +# 5. 将报告复制回本地分析(可选) +# 在本地机器运行: +scp user@remote-host:~/nvidia-docker-diagnostic-*.txt ./ +``` + +诊断脚本会检查: +- ✅ NVIDIA 驱动版本(用户空间) +- ✅ NVIDIA 内核模块版本 +- ✅ Docker 状态和配置 +- ✅ NVIDIA Container Toolkit 状态 +- ✅ 正在使用 GPU 的进程 +- ✅ 系统日志中的错误 + +--- + +## 🔧 步骤 2:根据诊断结果修复 + +### 场景 A:驱动版本不匹配(最常见) + +**症状:** +``` +用户空间驱动版本: 550.90.07 +内核模块版本: 550.54.15 +``` + +**修复方案(按优先级):** + +#### 方案 1:重启 Docker 服务 ⚡(最简单,80% 有效) + +```bash +# SSH 到目标机器 +ssh user@remote-host + +# 停止所有容器 +sudo docker stop $(sudo docker ps -aq) + +# 重启 Docker +sudo systemctl restart docker + +# 测试 +sudo docker run --rm --gpus all nvidia/cuda:12.8.0-base-ubuntu24.04 nvidia-smi +``` + +**如果成功**:问题解决,跳到步骤 3 启动应用。 + +**如果失败**:继续下一个方案。 + +--- + +#### 方案 2:重新加载 NVIDIA 内核模块 💪(95% 有效) + +```bash +# SSH 到目标机器 +ssh user@remote-host + +# 使用修复脚本(推荐) +sudo bash fix-nvidia-docker.sh + +# 或手动执行: +# 1. 停止 Docker 和所有使用 GPU 的进程 +sudo systemctl stop docker +sudo killall -9 python python3 nvidia-smi 2>/dev/null || true + +# 2. 卸载 NVIDIA 内核模块 +sudo rmmod nvidia_uvm 2>/dev/null || true +sudo rmmod nvidia_drm 2>/dev/null || true +sudo rmmod nvidia_modeset 2>/dev/null || true +sudo rmmod nvidia 2>/dev/null || true + +# 3. 重新加载模块 +sudo modprobe nvidia +sudo modprobe nvidia_uvm +sudo modprobe nvidia_drm +sudo modprobe nvidia_modeset + +# 4. 重启 Docker +sudo systemctl restart docker + +# 5. 测试 +sudo docker run --rm --gpus all nvidia/cuda:12.8.0-base-ubuntu24.04 nvidia-smi +``` + +**如果成功**:问题解决。 + +**如果失败**:内核模块可能被某些进程占用,继续下一个方案。 + +--- + +#### 方案 3:重启系统 🔄(99% 有效) + +```bash +# SSH 到目标机器 +ssh user@remote-host + +# 重启 +sudo reboot + +# 等待系统重启(约 1-2 分钟) +sleep 120 + +# 重新连接并测试 +ssh user@remote-host +sudo docker run --rm --gpus all nvidia/cuda:12.8.0-base-ubuntu24.04 nvidia-smi +``` + +**注意**:重启会中断所有服务,请确认可以接受短暂停机。 + +--- + +### 场景 B:NVIDIA Container Toolkit 问题 + +**症状:** +``` +❌ nvidia-container-cli 未安装 +或 +nvidia-container-cli 版本过旧 +``` + +**修复:** + +```bash +# SSH 到目标机器 +ssh user@remote-host + +# 更新 NVIDIA Container Toolkit +distribution=$(. /etc/os-release;echo $ID$VERSION_ID) + +# 添加仓库(如果未添加) +curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | \ + sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg + +curl -s -L https://nvidia.github.io/libnvidia-container/$distribution/libnvidia-container.list | \ + sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \ + sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list + +# 安装/更新 +sudo apt-get update +sudo apt-get install -y nvidia-container-toolkit + +# 配置 Docker +sudo nvidia-ctk runtime configure --runtime=docker + +# 重启 Docker +sudo systemctl restart docker + +# 测试 +sudo docker run --rm --gpus all nvidia/cuda:12.8.0-base-ubuntu24.04 nvidia-smi +``` + +--- + +### 场景 C:Docker 配置问题 + +**症状:** +``` +/etc/docker/daemon.json 不存在 +或缺少 nvidia runtime 配置 +``` + +**修复:** + +```bash +# SSH 到目标机器 +ssh user@remote-host + +# 创建/更新 Docker 配置 +sudo tee /etc/docker/daemon.json </dev/null || true + +# 启动容器 +sudo docker run -d --gpus all --network host \ + --name doc_processer \ + --restart unless-stopped \ + -v /home/yoge/.paddlex:/root/.paddlex:ro \ + -v /home/yoge/.cache/modelscope:/root/.cache/modelscope:ro \ + -v /home/yoge/.cache/huggingface:/root/.cache/huggingface:ro \ + doc_processer:latest + +# 检查容器状态 +sudo docker ps | grep doc_processer + +# 查看日志 +sudo docker logs -f doc_processer +``` + +--- + +## 📊 验证和监控 + +### 验证 GPU 访问 + +```bash +# 检查容器内的 GPU +sudo docker exec doc_processer nvidia-smi + +# 测试 API +curl http://localhost:8053/health +``` + +### 监控日志 + +```bash +# 实时日志 +sudo docker logs -f doc_processer + +# 查看最近 100 行 +sudo docker logs --tail 100 doc_processer +``` + +--- + +## 🛠️ 常用远程命令 + +### 一键诊断并尝试修复 + +```bash +# 在目标机器创建这个脚本 +cat > quick-fix.sh <<'EOF' +#!/bin/bash +set -e + +echo "🔧 快速修复脚本" +echo "================" + +# 方案 1: 重启 Docker +echo "尝试重启 Docker..." +sudo docker stop $(sudo docker ps -aq) 2>/dev/null || true +sudo systemctl restart docker +sleep 3 + +if sudo docker run --rm --gpus all nvidia/cuda:12.8.0-base-ubuntu24.04 nvidia-smi &>/dev/null; then + echo "✅ 修复成功(重启 Docker)" + exit 0 +fi + +# 方案 2: 重载模块 +echo "尝试重载 NVIDIA 模块..." +sudo rmmod nvidia_uvm nvidia_drm nvidia_modeset nvidia 2>/dev/null || true +sudo modprobe nvidia nvidia_uvm nvidia_drm nvidia_modeset +sudo systemctl restart docker +sleep 3 + +if sudo docker run --rm --gpus all nvidia/cuda:12.8.0-base-ubuntu24.04 nvidia-smi &>/dev/null; then + echo "✅ 修复成功(重载模块)" + exit 0 +fi + +# 方案 3: 需要重启 +echo "❌ 自动修复失败,需要重启系统" +echo "执行: sudo reboot" +exit 1 +EOF + +chmod +x quick-fix.sh +sudo bash quick-fix.sh +``` + +### SSH 隧道(如果需要本地访问远程服务) + +```bash +# 在本地机器运行 +ssh -L 8053:localhost:8053 user@remote-host + +# 现在可以在本地访问 +curl http://localhost:8053/health +``` + +--- + +## 📝 故障排除检查清单 + +- [ ] 运行 `diagnose-nvidia-docker.sh` 生成完整诊断报告 +- [ ] 检查驱动版本是否一致(用户空间 vs 内核模块) +- [ ] 检查 NVIDIA Container Toolkit 是否安装 +- [ ] 检查 `/etc/docker/daemon.json` 配置 +- [ ] 尝试重启 Docker 服务 +- [ ] 尝试重新加载 NVIDIA 内核模块 +- [ ] 检查是否有进程占用 GPU +- [ ] 查看 Docker 日志:`journalctl -u docker -n 100` +- [ ] 最后手段:重启系统 + +--- + +## 💡 预防措施 + +### 1. 固定 NVIDIA 驱动版本 + +```bash +# 锁定当前驱动版本 +sudo apt-mark hold nvidia-driver-* + +# 查看已锁定的包 +apt-mark showhold +``` + +### 2. 自动重启 Docker(驱动更新后) + +```bash +# 创建 systemd 服务 +sudo tee /etc/systemd/system/nvidia-docker-restart.service < /usr/local/bin/check-nvidia-docker.sh <<'EOF' +#!/bin/bash +if ! docker run --rm --gpus all nvidia/cuda:12.8.0-base-ubuntu24.04 nvidia-smi &>/dev/null; then + echo "$(date): NVIDIA Docker 访问失败" >> /var/log/nvidia-docker-check.log + systemctl restart docker +fi +EOF + +chmod +x /usr/local/bin/check-nvidia-docker.sh + +# 添加到 crontab(每 5 分钟检查) +echo "*/5 * * * * /usr/local/bin/check-nvidia-docker.sh" | sudo crontab - +``` + +--- + +## 📞 需要帮助? + +如果以上方案都无法解决,请提供: + +1. **诊断报告**:`nvidia-docker-diagnostic-*.txt` 的完整内容 +2. **错误日志**:`sudo docker logs doc_processer` +3. **系统信息**: + ```bash + nvidia-smi + docker --version + nvidia-container-cli --version + uname -a + ``` + +--- + +## 快速参考 + +| 命令 | 说明 | +|------|------| +| `bash diagnose-nvidia-docker.sh` | 生成诊断报告 | +| `sudo bash fix-nvidia-docker.sh` | 自动修复脚本 | +| `sudo systemctl restart docker` | 重启 Docker | +| `sudo reboot` | 重启系统 | +| `docker logs -f doc_processer` | 查看应用日志 | +| `docker exec doc_processer nvidia-smi` | 检查容器内 GPU |