LaTeXSnipper Mobile — 项目维护指南
代码规范
- JS 使用 ES Module (
import/export) - CSS 使用
src/styles/分模块管理(base/ocr/editor/handwriting/mobile) - HTML 标签内联事件用
pointerdown而不是click(WebView 兼容) - 所有用户可见文本使用
data-i18n属性 +t()函数,禁止硬编码中文 - 语言包在
src/core/lang/统一管理,新增文本只需加键值对 - 新增功能归到所属模块,不要跨模块散落
- 修改
public/下文件后需重新npm run build - 提交时不添加
Co-Authored-By署名行 - 新增 OCR 模式:创建
src/ocr/pipelines/<name>.js→ 在pipelines/manifest.json加一行声明即可,无需改 recognition.js 或 registry - 新增功能模块:在模块内导出
bindEvents()→ 在app.js注册,无需改 main.js
一、项目架构
LaTeXSnipper_mobile/
├── index.html # 单页面 SPA,4 个 Tab 页面
├── public/
│ ├── vendor/ # 内置库
│ │ ├── katex.min.js # KaTeX 公式渲染 (265KB)
│ │ ├── katex.min.css # KaTeX CSS + fonts/ 字体
│ │ ├── mathlive/ # MathLive 编辑器
│ │ └── pdf.min.js # PDF.js
│ ├── models/ # 模型目录(doc-ori ONNX + tokenizer/keys fallback)
│ ├── sw.js # Service Worker
│ └── manifest.json # PWA 清单
├── src/
│ ├── main.js # 入口(17 行):await bootstrap → await createApp → await start
│ ├── constants.js # 全局常量
│ ├── update-checker.js # GitHub Releases 自动更新检查
│ ├── core/ # 基础设施
│ │ ├── bootstrap.js # 平台初始化(Theme/SW/Tab/PWA)
│ │ ├── app.js # 模块加载/事件注册/业务逻辑
│ │ ├── event-registry.js # EventRegistry(registerBinding/bindAll)
│ │ ├── logger.js # 日志收集(localStorage + Java 桥接 + DOM 事件)
│ │ ├── i18n.js # 国际化引擎
│ │ └── lang/ # 语言文件(zh-CN/zh-TW/en/ja/ko)
│ ├── ocr/ # OCR 管线
│ │ ├── pipeline.js # OcrPipeline 基类(Metadata: id/name/icon/requiredModels)
│ │ ├── pipeline-registry.js # 注册表(Manifest 自发现 + Lazy 加载 + checkPipelineModels)
│ │ ├── ocr-result.js # OcrResult/OcrBlock 数据模型
│ │ ├── ocr-native.js # Android Native Bridge 封装
│ │ ├── recognition.js # 识别协调器(PDF/外部API/Pipeline调度)
│ │ └── pipelines/ # 可插拔 Pipeline(lazy chunk)
│ │ ├── manifest.json # Pipeline 声明(自动发现注册)
│ │ ├── formula.js # 公式识别
│ │ ├── text.js # 文字识别
│ │ └── mixed.js # 混合识别
│ ├── model/ # 模型管理
│ │ ├── model-manager.js # 清单解析、CRUD、下载、导入、变体合并
│ │ ├── model-analyzer.js # ONNX protobuf 解析器
│ │ ├── model-import.js # ZIP/单文件导入 UI
│ │ ├── model-settings.js # 设置页模型管理 UI
│ │ └── package-builder.js # 模型包创建器
│ ├── camera/ # 全屏相机:拍照/框选/套索/四角把手/旋转
│ ├── handwriting/ # Canvas 手写板 + 导出
│ ├── editor/ # MathLive 编辑器 + 虚拟键盘 + KaTeX 预览
│ ├── export/ # 导出模块
│ │ ├── pandoc-export.js # 统一导出系统(下拉菜单 + 9 种格式)
│ │ ├── latex-generator.js # OcrResult → LaTeX
│ │ ├── markdown-generator.js # OcrResult → Markdown
│ │ └── share.js # 分享功能(Capacitor → 下载降级)
│ ├── history/ # IndexedDB 存储(idb 封装)
│ ├── settings/ # 设置页面逻辑
│ ├── ui/ # UI 组件
│ │ ├── ui.js # 状态栏/进度条/拖放/模式切换
│ │ ├── result.js # 结果显示/KaTeX预览/复制/分享/PDF分页/导出
│ │ ├── splash.js # 启动加载进度
│ │ ├── custom-select.js # 自定义下拉选择器
│ │ ├── status.js # 状态栏(带图标)
│ │ ├── theme.js # 日/夜主题切换
│ │ ├── polish.js # AI 整理(DeepSeek API)
│ │ ├── welcome-dialog.js # 首次启动欢迎弹窗
│ │ └── dom-refs.js # DOM 元素引用共享
│ └── styles/ # CSS 样式模块
│ ├── base.css # CSS 变量、布局、导航、自定义下拉
│ ├── ocr.css # 识别页面 + 导出下拉菜单样式
│ ├── editor.css # MathLive + KaTeX 预览 + 符号工具栏
│ ├── handwriting.css # 手写板
│ ├── history.css # 历史记录滑动
│ └── mobile.css # 移动端适配
├── android/ # Capacitor Android 项目
│ └── app/src/main/java/com/latexsnipper/app/
│ ├── MainActivity.java # 入口 + NativeOcrBridge 注入
│ └── ocr/ # Java ONNX OCR 引擎
│ ├── NativeOcrBridge.java # @JavascriptInterface 桥接
│ ├── OnnxRunner.java # ONNX Runtime 会话管理
│ ├── OcrEngine.java # 主编排器(formula/text/mixed)
│ ├── DetPreProcess.java # 公式检测预处理
│ ├── FormulaDetPostProcess.java # YOLOv8 后处理
│ ├── FormulaRecPreProcess.java # TrOCR 预处理
│ ├── FormulaRecPostProcess.java # 束搜索解码
│ ├── FormulaLineSplitter.java # 多行公式行分割
│ ├── TextDetProcessor.java # DBNet 轮廓追踪
│ ├── TextRecPreProcess.java # CRNN 预处理
│ ├── TextRecPostProcess.java # CTC 解码
│ ├── DocOriPreProcess.java # 方向检测
│ ├── ModelConfig.java # config.json 解析 + 模型文件发现
│ └── ImagePreProcess.java # 图像增强
├── test/ # 测试套件(4 套 785+ 项)
│ ├── test_behavior_consistency.js # 行为一致性(112 项)
│ ├── test_integration.js # 集成测试(221 项)
│ ├── test_user_workflows.js # 用户工作流(154 项)
│ ├── test_e2e.js # E2E 全量(303 项)
│ └── test_*.py # OCR 模型测试(Python)
├── scripts/
│ ├── package-models.js # 模型打包脚本(生成 per-category + 完整 ZIP)
│ └── quantize.py # 模型量化
├── vite.config.js # Vite 8 配置(wasm + top-level-await 原生支持)
├── SECURITY.md # 安全政策
├── capacitor.config.json # Capacitor 配置
└── .github/workflows/
├── build-apk.yml # Android APK 构建(workflow_dispatch)
├── build-ios.yml # iOS 模拟器构建
├── package-models.yml # 模型打包 + 上传 GitHub Releases
└── security-scan.yml # 安全扫描
二、Tab 页面结构
| Tab | ID | 功能 |
|-----|-----|------|
| 识别 | #page-ocr | 图片/PDF/拍照/手写识别,模式选择(公式/文本/混合) |
| 编辑器 | #page-editor | MathLive 所见即所得编辑,KaTeX 预览,虚拟键盘,符号工具栏,导出 |
| 历史 | #page-history | IndexedDB 列表,收藏筛选,滑动删除/分享/复制,点击填入编辑器 |
| 设置 | #page-settings | 识别引擎选择、加速模式、外部 API 配置、预设、皮肤、语言、AI 整理配置、开发者模式、更新检查 |
三、识别引擎架构
Android 端使用纯 Java ONNX Runtime 管线,桌面端 Python mathcraft-ocr 实现对标。
公式识别 (formula mode)
图片 → FormulaDetPreProcess (768×768 letterbox)
→ 公式检测 (YOLOv8) → 结果区域 → 每个区域:
→ FormulaRecPreProcess (短边384+中心裁剪) → TrOCR 编码器(DeiT) → 束搜索解码(beam=3)
→ LaTeX 修复 → 输出
文字识别 (text mode)
图片 → TextDetPreProcess (最长边960, stride32对齐)
→ DBNet 推理 → Moore-Neighbor 轮廓追踪 → unclip → box_thresh=0.5
→ 每个文本框 → TextRecPreProcess (BGR 48×320) → CRNN 推理 → CTC 解码
→ 输出文本
混合模式 (mixed mode)
图片 → 公式检测 (YOLOv8) + 文字检测 (DBNet)
→ splitTextBoxAroundFormulas (按公式 x 范围 + y 重叠分割)
→ 公式段 → crop 使用 formulaDet 框坐标 → 公式行分割/单行识别
→ 文字段 → crop 使用 textDet 框坐标 → 直接 CRNN 识别
→ 独立显示公式加入 → overlap check 去重(使用正确坐标避免重复)
→ 行分组(union box y-overlap≥0.45)
→ 版面输出(inline 用 $…$,display 用 $$\n…\n$$)
桥接通信
JS → window.NativeOcr.recognizeFormula(base64) → NativeOcrBridge (后台线程)
→ OcrEngine → ONNX Runtime Android
→ 结果 JSON → JS 轮询 getResult(key) 获取
- 识别异步:Java 后台线程执行,JS 每 200ms 轮询
- 结果 JSON 含
text/latex/confidence/timeMs/regions(混合模式)
四、ONNX 模型清单(按需下载,doc-ori 内置)
模型通过 ZIP 包下载导入,doc-ori 方向检测模型内置 APK(6.5 MB)。
ZIP 包格式对齐 HuggingFace ONNX + PaddleOCR 规范,每个包含 config.json。
| 类别 | 默认 variant ID | 模型文件 | 分发方式 |
|------|----------------|----------|----------|
| formula-det | yolov8-mfd | mathcraft-mfd.onnx | 下载 ZIP |
| formula-rec | trocr-deit | encoder_model.onnx + decoder_model.onnx + tokenizer.json | 下载 ZIP |
| text-det | ppocrv5-mobile | ppocrv5_mobile_det.onnx | 下载 ZIP |
| text-rec | ppocrv5-mobile | ppocrv5_mobile_rec.onnx + ppocrv5_keys.txt | 下载 ZIP |
| doc-ori | pplcnet-doc-ori | pplcnet_doc_ori.onnx | 内置 APK |
ZIP 包结构
{category}/{variantId}/
model.onnx (或 encoder_model.onnx + decoder_model.onnx) — ONNX 模型权重
config.json — 模型自描述(类型/输入/输出/预处理/后处理)
tokenizer.json / ppocrv5_keys.txt — 解码器字典文件
ModelConfig.java — config.json 解析
ModelConfig.load(modelDir) // 从模型目录读取 config.json
ModelConfig.findModelFile(dir) // 发现 ONNX 文件(model.onnx → *.onnx)
ModelConfig.findEncoderFile(dir) // 发现编码器 ONNX(encoder.onnx → encoder_model.onnx)
ModelConfig.findDecoderFile(dir) // 发现解码器 ONNX
ModelConfig.findTokenizerFile(dir) // 发现字典文件(tokenizer.json → ppocr_keys.txt)
注意:已知模型加载使用硬编码文件名(避免多 ONNX 目录误选),findModelFile 仅供第三方模型发现使用。
模型管理系统
JS 端:
model-manager.js — 清单解析、CRUD、下载、导入、变体合并
model-analyzer.js — ONNX protobuf 解析,自动推断类别
model-import.js — ZIP/单文件导入 UI
model-settings.js — 设置页模型管理(源/变体/下载/删除)
package-builder.js — 应用内模型包创建器
Java 端:
ModelManager.java — 文件路径、活跃变体(SharedPreferences)、安装状态
OnnxRunner.java — 动态加载(文件系统优先 → 资产回退 → null)
NativeOcrBridge.java — getModelStatus() 返回各模型可用状态
存储路径
- JS:
localStorage(sources/active/installed/manifests/download_progress) + Capacitor Filesystem (DATA/models/{category}/{variantId}/) - Java:
SharedPreferences "ModelManagerPrefs"+ctx.getFilesDir()/models/{category}/{variantId}/
下载系统(镜像 + 断点续传 + SHA256 校验)
manifest.mirrors[] — 多下载源,主源失败自动切换
manifest.checksums{} — {filename: sha256hex},下载后校验完整性
downloadVariant() — 镜像 fallback → Range 断点续传 → SHA256 校验 → importFromZip
localStorage — ls_download_progress 持久化下载进度,支持应用重启恢复
- 镜像 URL 格式:
https://mirror/https://github.com/original-path - 默认镜像:
gh.zwy.one、gh.xxooo.cf - 断点续传:HTTP
Range: bytes=N-header,服务器不支持时自动重新下载 - SHA256:Web Crypto API
crypto.subtle.digest('SHA-256'),不匹配则拒绝导入
打包脚本
node scripts/package-models.js --output dist-models
# 生成: dist-models/latexsnipper-{category}.zip + model-manifest.json
# ONNX 源文件在 model-sources/(不被 Vite 清理)
# 每个 ZIP 包含 config.json(自动从 CATEGORY_MAP 生成)
模型目录结构
model-sources/ ← 打包源文件(.gitignore,不在 git 中)
mathcraft-formula-det/
mathcraft-formula-rec/
mathcraft-text-det/
mathcraft-text-rec/
mathcraft-doc-ori/
public/models/ ← 仅含内置 APK 的文件
mathcraft-doc-ori/pplcnet_doc_ori.onnx ← 内置方向检测(6.5 MB)
mathcraft-formula-rec/tokenizer.json ← 公式 tokenizer fallback
mathcraft-text-rec/ppocrv5_keys.txt ← 文字 CTC 字典 fallback
五、关键参数
| 参数 | 值 | 说明 | |------|-----|------| | det 置信度阈值 | 0.25 | 匹配桌面端 | | det NMS IoU | 0.45 | 匹配桌面端 | | rec max_tokens | 512 | 匹配桌面端 | | det thresh | 0.3 | RapidOCR 默认 | | box_thresh | 0.5 | RapidOCR 默认 | | unclip_ratio | 1.6 | RapidOCR 默认 | | min_text_score | 0.45 | 文字置信度过滤 | | largeHeap | true | AndroidManifest.xml |
六、欢迎弹窗与首次启动
首次启动 → checkFirstLaunch() → 欢迎弹窗
→ "立即下载" → refreshManifests() → 逐个下载 4 个模型(弹窗内进度条)
→ "使用外部 API" → 切换引擎到外部 API
→ "稍后设置" → 跳过
- 欢迎弹窗自动下载所有模型,弹窗内显示每个模型的下载进度
- 下载失败可重试,不会自动关闭弹窗
POST_NOTIFICATIONS权限在 AndroidManifest.xml 声明,Java 端运行时检查
七、Pandoc WASM 按需下载
pandoc.wasm(58 MB)不内置 APK,用户在设置页手动下载。
设置页 → "下载 Pandoc WASM" → downloadPandocWasm()
→ IndexedDB 缓存(避免 base64 OOM)
→ 首次编译 WASM 显示加载弹窗
→ 后续导出直接使用缓存实例
- 下载源:GitHub Release + gh.zwy.one + gh.xxooo.cf 镜像
- 缓存:IndexedDB(原生二进制,无 base64 开销)
- 导出菜单:pandoc 不可用时仅显示 PNG/SVG/Typst
- AndroidManifest.xml 声明
POST_NOTIFICATIONS权限
六、多语言系统
用户切换语言 → setLang(code) → 加载语言包 → translateDOM() 批量更新
└→ onLangChange 回调(更新动态文本)
- 静态 HTML:
data-i18n/data-i18n-html/data-i18n-title - 动态 JS:
import { t } from './core/i18n.js' - 新增语言:复制 zh-CN.js → 翻译 → 在 LANG_MAP 注册 → 加 HTML 选项
- 所有用户可见文本必须通过 i18n 系统,禁止硬编码
现有语言
| 语言 | 文件 |
|------|------|
| 简体中文 | src/core/lang/zh-CN.js |
| 繁体中文 | src/core/lang/zh-TW.js |
| 英文 | src/core/lang/en.js |
| 日文 | src/core/lang/ja.js |
| 韩文 | src/core/lang/ko.js |
七、导出系统
导出下拉菜单位于 OCR 结果卡和编辑器底部,共 9 种格式:
| 格式 | 转换引擎 | 说明 |
|------|---------|------|
| PNG | KaTeX → SVG → Canvas | 高清公式图片 |
| SVG | KaTeX → SVG | 矢量公式图片 |
| LaTeX | Pandoc WASM | .tex 格式 |
| MathML | Pandoc WASM | 数学标记语言 |
| Markdown | Pandoc WASM | markdown+tex_math_dollars |
| HTML | Pandoc WASM | 网页 |
| Typst | 纯 JS 转换器 | 符号表 + 结构转换,不依赖 Pandoc |
| Word | Pandoc WASM | .docx 格式 |
| Plain Text | Pandoc WASM | 纯文本 |
Typst 转换器(pandoc-export.js):
- 200+ LaTeX→Typst 符号映射(希腊字母、运算符、箭头、关系符、函数名)
- 结构转换:
\frac、\sqrt、\binom、\begin{cases}、矩阵环境、\text、\underline、\hat/\vec等 - 混合内容分段:
$...$/$$...$$解析,只转换公式段,文本段保留 - 预处理:
\textcolor、\cfrac、\sideset、\varnothing、#?等修复
八、构建与部署
npm install # 安装依赖
npm run dev # Vite 开发服务器(:5174)
npm run build # 构建到 dist/
Android
npx cap sync android # 同步到 Android
cd android && ./gradlew assembleDebug # 编译 debug APK
iOS(需要 macOS + Xcode)
# 推荐:构建后自动打开 Xcode,选签名后点 Run
bash scripts/build-ios.sh
# 仅模拟器
bash scripts/build-ios.sh --simulator
# 真机 IPA
bash scripts/build-ios.sh --device
免费 Apple ID 即可签名(不需要 $99 开发者账号),限制:每 7 天重新签名,最多 3 个 app。
测试
# Node.js 测试(无需 conda)
node test/test_pandoc_export.js # Pandoc + Typst 导出
node test/test_katex.js # KaTeX 渲染
node test/test_integration.js # 项目结构检查
node test/test_e2e.js # 全量 E2E
# 全部测试(含 OCR 模型)
conda activate ppocr_finetune
bash test/run_tests.sh
注意事项
- 模型按需下载 — ONNX 模型不再内置 APK(~220MB),通过设置页下载 ZIP 包导入,或使用外部 API
- 模型加载优雅失败 — 缺失模型不崩溃,
loadModelData返回 null,createSession返回 null,OcrEngine 跳过并记录 - 外部 API 独立 — 选择外部 API 模式时不加载本地模型,
initModels()直接跳过 - 图片解码 —
is.available()在 APK 压缩资产中返回压缩后大小,必须用ByteArrayOutputStream分段读取 - 文件分享 — Capacitor Share 传 base64 文件在某些 Android 版本失败时,直接触发下载而非弹系统分享
- MathLive 自定义元素 —
<mathlive-field>在部分 WebView 中不注册,改用new MathfieldElement()创建 - 虚拟键盘策略 — 三态切换:
manual(关闭) →manual+toggleVirtualKeyboard(MathLive 键盘) →sandboxed(系统键盘) - 相机按钮 — 必须用
pointerdown+stopPropagation,click在 WebView 中不可靠 - COOP/COEP 头 — Capacitor 和 Vite 中已配置
- iOS 构建 — 需要 Apple Developer($99/年),CI 只能验证模拟器编译
- 大图拍照 — >500KB 自动压缩到最长边 1920px
- KaTeX 替换 MathJax — 公式渲染使用 KaTeX HTML 渲染,轻量快速
- Typst 不经过 Pandoc WASM — Typst 导出使用纯 JS 符号映射 + 结构转换器
Next.js App Router Expert
Development
A skill that turns Claude into a Next.js App Router expert.
README Generator
Development
Creates professional and comprehensive README.md files for your projects.
API Documentation Writer
Development
Generates comprehensive API documentation in OpenAPI/Swagger format.