# miocr **Repository Path**: fmscole/miocr ## Basic Information - **Project Name**: miocr - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-10-02 - **Last Updated**: 2026-11-05 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # miocr — 试卷 PDF → Word 一键 OCR 管线 本地试卷数字化流水线。当前主力为 **OXR 管线**(`oxr_batch.py`): [OXR-1.0](https://github.com/SoMarkAI/OXR)(SoMark,1.3B,olmOCR-Bench / OmniDocBench / DocParsingBench 三榜第一)本地解析 → Markdown → pandoc 两段式 → Word 原生公式(OMML)的 docx。 另有基于 [Xiaomi-OCR-0](https://github.com/SeerRay-Lab/Xiaomi-OCR-0) 的旧管线(`run_pipeline.py`),见文末「旧管线」章节备用。 ## OXR 管线用法(WSL 内运行) ```bash # 传 Windows 路径(自动转 /mnt/...;忘了加引号也能按目录名还原) python3 oxr_batch.py "D:\BaiduSyncdisk\进才中学\2026年秋季\ocr" # 或直接传 WSL 路径 python3 oxr_batch.py /mnt/d/BaiduSyncdisk/进才中学/2026年秋季/ocr ``` - **递归**扫描目录下所有 PDF 和图片(含子文件夹) - 每份 `xxx.pdf` → 同目录生成 `xxx.docx`(公式为 Word 原生 OMML,表格带框线,图片按真实 DPI 尺寸嵌入) - 同名 docx/doc 已存在则**跳过**,随时中断随时重跑 - 三个 OXR 服务没在跑则**自动拉起、跑完自动关掉**(显存不残留;本来就在跑的服务不会被动) - 每个文件处理完立即清理服务端中间产物,不占磁盘 `books_pipeline.py` 是四本《数学精编》的专用管线(保留 md/json/图片中间产物到 `work/books/`,供检测-修复循环用);`detect_bare.py` / `scan_all.py` 是配套的裸公式检测。 ## OXR 环境搭建(WSL2 Ubuntu-24.04,已实测 RTX 5090 Laptop 24GB) 官方要求 Linux + NVIDIA 显卡(支持 BF16)+ 24GB 显存,**WSL2 即可满足**。 ```bash # 0. 系统依赖 sudo apt install pandoc # 3.1.3 实测可用,公式修复验证也用它 # 1. OXR 代码 git clone https://github.com/SoMarkAI/OXR.git ~/OXR # 2. 模型权重(含 layout_analysis/last.pth 版面模型) pip install modelscope modelscope download --model SoMark/OXR-1.0 --local_dir ~/models/OXR-1.0 # 3. 三个虚拟环境(依赖互相冲突,官方也是分开部署的) python3 -m venv ~/.venvs/vllm ~/.venvs/vllm/bin/pip install vllm # 实测 0.25.1 + torch 2.11 python3 -m venv ~/.venvs/layout ~/.venvs/layout/bin/pip install torch torchvision \ -r ~/OXR/services/layout_analyze/requirements.txt python3 -m venv ~/.venvs/api ~/.venvs/api/bin/pip install ~/OXR # somark-oxr 包,提供 oxr CLI # 4. WSL2 必需的两个环境变量(启动 vLLM 时) export VLLM_WSL2_ENABLE_PIN_MEMORY=1 # WSL2 默认禁锁页内存,vLLM 需要显式打开 # triton 编译需要 Python 头文件;若系统没有 python3-dev, # 把 python3.12 头文件解压到 ~/pyhdr 后: export CPATH=$HOME/pyhdr/usr/include/python3.12:$HOME/pyhdr/usr/include ``` ### `~/OXR/config.yaml` 两处必改 | 配置 | 默认 | 改为 | 原因 | |---|---|---|---| | `layout_analyze_model.score_threshold` | 0.5 | **0.3** | 默认阈值会漏检复杂公式区域,导致嵌套分式整段丢失 | | `pipeline.full_overlap_dedupe` | false | **true** | 不修会出现重复文本块 | ### 手动启停服务(一般不用,`oxr_batch.py` 会自动管) ```bash # ① vLLM 模型服务 :8080(启动脚本会先装模型自带的 oxr-vllm-plugin) cd ~/OXR && VLLM_WSL2_ENABLE_PIN_MEMORY=1 \ CPATH=$HOME/pyhdr/usr/include/python3.12:$HOME/pyhdr/usr/include \ MODEL_PATH=~/models/OXR-1.0 PATH=~/.venvs/vllm/bin:$PATH \ bash services/oxr/start_vllm.sh --gpu-memory-utilization 0.70 --max-num-seqs 16 # ② 版面分析服务 :8081 cd ~/OXR && MODEL_PATH=~/models/OXR-1.0/layout_analysis/last.pth DFINE_DEVICE=cuda \ ~/.venvs/layout/bin/python -m uvicorn dfine_la.api:app \ --app-dir services/layout_analyze --host 127.0.0.1 --port 8081 --workers 1 # ③ 解析 API :8000 cd ~/OXR && ~/.venvs/api/bin/oxr serve --config config.yaml --host 0.0.0.0 --port 8000 --workers 1 ``` > ⚠️ 停服务时 `pkill -f 'oxr serve'` 这类写法会匹配到发起命令的 shell 自身导致自杀 > (退出码 15),要写成 `pkill -f '[o]xr serve'`(括号写法,见「坑」#9)。 ### md → docx 转换层(`oxr_batch.py` 内置,无需额外安装) - 两段式 pandoc:`md → html(--mathml) → docx`,表格成原生 `w:tbl` 且自动补框线,公式成 OMML - `-fancy_lists` 修选择题选项阶梯错位;图片靠 `stamp_png_dpi` 写入 pHYs 元数据保证不被放大 - `repair_math()` 三层公式修复:断裂环境合并 → 确定性规则(HTML 转义/全角标点/`\textcircled`/命令粘连/连续下划线/悬空 `\hline`)→ **错误驱动修复**(按 pandoc 报错逐个修,每步重新验证,修不好保留原文)。四本《数学精编》1005 页实测:13000+ 公式,裸公式残留 **0** --- ## 旧管线(Xiaomi-OCR-0,备用) ### 用法 Windows 侧和 WSL 里都可以跑,脚本会自动判断自己在哪一侧(`sys.platform`), 并自动把 Windows 路径转成 Linux 路径。 ```bash # ① WSL 里跑(conda activate miner 之后,推荐) python run_pipeline.py -i /mnt/d/BaiduSyncdisk/进才中学/2026年秋季/ocr # ② WSL 里传 Windows 路径也可以(自动转 /mnt/d/...) python run_pipeline.py -i "D:\BaiduSyncdisk\进才中学\2026年秋季\ocr" # ③ Windows 侧跑(PowerShell / cmd) python run_pipeline.py -i "D:\BaiduSyncdisk\进才中学\2026年秋季\ocr" ``` > ⚠️ **在 bash 里传 Windows 路径必须加引号**。不加引号时反斜杠会被 shell 当转义符吃掉, > `D:\BaiduSyncdisk\进才中学` 会变成 `D:BaiduSyncdisk进才中学`。 > 脚本对这种情形会按目录名做贪心还原并打印 `[warn]`,但**加引号才是可靠做法**。 - **递归**扫描目录下所有 PDF(含子文件夹),逐份处理 - 每份 `xxx.pdf` → 同目录生成 `xxx.docx`(公式为 Word 原生 OMML,页间分页符) - 同名 docx 已存在则**跳过**,随时中断随时重跑,不重复识别 - 单份完成后自动清理中间产物;`-k` 保留调试 ### `--serve`:服务自管(推荐) 不加参数时脚本只做健康检查,检测不到 vLLM 就直接退出(要求你先手动起服务)。 加上 `--serve` 后,脚本会**自己拉起、自己收掉**: ```bash # Windows 侧一行搞定:没服务就起,跑完自动关,显存不残留 python run_pipeline.py -i "D:\BaiduSyncdisk\进才中学\2026年秋季\ocr" --serve ``` 行为细节: | 场景 | 行为 | | --------------- | ------------------------------------------------------------------------------------------- | | 服务已在跑 | 健康检查通过(要求 `/v1/models` 里模型名对得上),**原样沿用、退出时不动它** | | 服务没在跑 | 在 WSL `miner` 环境里 `setsid nohup` 后台拉起 vLLM,日志 `/tmp/vllm_xocr.log`、PID `/tmp/vllm_xocr.pid` | | 等待就绪 | 最长 **900s(15 分钟)**,每 30s 打印一次进度;首跑含约 1.75GB 权重下载 | | 进程早退 | 每 5s 探活,发现进程已死**立刻报错并打印日志末尾 20 行**(不必等满 15 分钟) | | 等待超时 | 报错时附带日志末尾 + 排查提示(显存占用 / HF 缓存完整性 / 手跑 `serve_xocr.sh`) | | 正常退出 | `finally` 里按 PID 文件关掉自启的服务,回收进程组,释放显存 | | **Ctrl-C / 异常** | 同上——**绝不把 vLLM 留成孤儿** | **两个设计要点**(改这块代码时别踩反): - **关闭只认 PID 文件,不做「杀掉所有监听 8000 的进程」兜底。** PID 文件是 `start_server()` 写的,代表「这是我们起的」。用端口兜底会**误杀用户自己启动的服务** (那是用户明确说「我在跑,别管」的语义)。 - **清理逻辑写在 `start_server()` 内部(`except BaseException`),而不是只靠 `main()` 的 `finally`。** `except Exception` **抓不到 `KeyboardInterrupt`**;而 `KeyboardInterrupt` 会在 `started_by_us = start_server()` 这行**赋值完成之前**就抛出去,导致 `finally` 里 `started_by_us` 仍是 `False`、服务变孤儿(实测泄漏 18,868 MiB)。现在 `start_server()` 内部就地收尸、只有真正就绪才返回 `True`,`main()` 只管关「就绪后」的服务。 **WSL 保活会话**:`--serve` 会额外挂一个常驻 `wsl.exe`(`sleep 86400`)会话,从拉服务 钉到收尾,防止 WSL2 的 `vmIdleTimeout` 把「用 `setsid` 分离出去的长任务」连同整个 VM 一起 回收(见「坑」#15)。这是**硬保证**;`.wslconfig` 里的 `vmIdleTimeout=-1` 是软保证 (用户级配置可能被重置/换机)。退出时保活会话一并回收。 ### 架构 ``` run_pipeline.py(编排,Windows 侧 或 WSL 内均可) │ Windows 侧:wsl -d Ubuntu-24.04 -- bash -lc ... │ WSL 内:直接 bash -lc ...(不嵌套) │ ├─(--serve)服务自管:健康检查 → 拉起/沿用 → 退出时收掉 │ + 常驻 wsl.exe 保活会话(防 vmIdleTimeout 回收 VM) ▼ WSL2 Ubuntu-24.04(conda miner): pdf2imgs.py PDF → PNG 页面(200 DPI,pypdfium2) pipeline.layout_detect_paddlex PP-DocLayoutV3 版面检测(PaddleX) pipeline.twostage_img2md Xiaomi-OCR-0 区域识别(vLLM @ 127.0.0.1:8000) pandoc 分页 md → docx(WSL 优先,本地便携版兜底) ``` 中间产物:项目根目录下的 `.tmp_ocr\`(页面 PNG、jsonl、分页 md;完成后自动清理)。 ### 环境搭建(WSL2 Ubuntu-24.04) 本机实际环境:conda `miner` 环境(Python 3.10,与 minerocr 项目共用)。 ```bash conda activate miner # 0. GPU 库路径(本机 WSL 必须,否则 paddle 找不到 libcuda.so) # 已固化到环境:conda env config vars set LD_LIBRARY_PATH=/usr/lib/wsl/lib export LD_LIBRARY_PATH=/usr/lib/wsl/lib:$LD_LIBRARY_PATH # 1. 推理运行时(本机已装 vllm 0.21.0 + torch 2.11.0+cu130) pip install vllm # 需支持 Qwen3_5 架构,已验证 # 2. 管线依赖 pip install pypdfium2 Pillow wordfreq # 3. PaddlePaddle —— ⚠️ RTX 5090 (Blackwell, sm_120) 必须用 nightly 版! # stable 版(3.3.x/3.4.0)在 sm_120 上 GPU 算子静默返回 0,不可用。 pip install 'paddlepaddle-gpu==3.5.0.dev20260924+67dafa8ce58' --pre \ --index-url https://www.paddlepaddle.org.cn/packages/nightly/cu130/ --no-deps # 4. 版面检测依赖(须装 paddlex[cv],裸 paddlex 会缺 PDFReaderBackend / Shapely) pip install "paddlex[cv]" shapely # 5. NCCL:torch 2.11 需要 ≥2.32 的 dc12 符号(见「坑」#3) pip install -U nvidia-nccl-cu12 # 6. 仓库源码(pipeline/ 与 postprocess/ 模块) git clone https://github.com/SeerRay-Lab/Xiaomi-OCR-0.git # 放在项目根目录下 # 7. 启动模型服务(保持窗口开着) export HF_ENDPOINT=https://hf-mirror.com export HF_HUB_DISABLE_XET=1 # 见「坑」#1 vllm serve SeerRay-Lab/Xiaomi-OCR-0 --host 127.0.0.1 --port 8000 \ --max-model-len 40960 --trust-remote-code --gpu-memory-utilization 0.80 ``` Windows 侧兜底工具:`tools/pandoc-3.12/`(便携版 pandoc)。 ### 已踩过的坑 1. **hf-mirror 下大权重 401**:xet 协议直连官方 cas-server 不认镜像身份,必须 `HF_HUB_DISABLE_XET=1`。 2. **`--max_tokens` 撞上下文上限**:vLLM 的 `max_model_len` 是输入+输出共享。服务开 40960,调用侧 max_tokens 16384。 3. **`undefined symbol: ncclCommResume` / `ncclCommWindowDeregister`**:torch 2.11 需要 NCCL ≥ 2.32 的符号,但 pip 依赖(paddle)会钉死 `nvidia-nccl-cu12==2.25.1`。手动 `pip install -U nvidia-nccl-cu12`(2.32.3 已验证),pip 的版本警告可忽略。症状:连 `import torch` 都失败,报 `libtorch_cuda.so: undefined symbol: ncclCommWindowDeregister`。 4. **⚠️ RTX 5090 (Blackwell, sm_120) 必须用 paddle nightly**:官方 stable 版(3.3.1/3.4.0,任何 CUDA 目录)在 sm_120 上 **GPU 算子静默返回全 0**(`(1+1)` 得 0,`randn` 全 0),且 `paddle.utils.run_check()` **仍报 "works well"**(假通过),只在真实推理时报 `RuntimeError: std::exception`。改用 nightly `3.5.0.dev` 后 GPU 完全正常。排查命令:`paddle.set_device("gpu"); print((paddle.ones([4,4])+1).numpy().sum())` 应为 16.0。 5. **WSL 里 paddle 找不到 `libcuda.so`**:需 `export LD_LIBRARY_PATH=/usr/lib/wsl/lib:$LD_LIBRARY_PATH`(已固化到 miner 环境变量)。 6. **pandoc 不解析 `\(...\)`**:必须 `-f markdown+tex_math_single_backslash`,否则公式全变纯文本。`\textcircled` 不支持,管线已预处理为 Unicode 圈号。 7. **分段函数跨片段**:OCR 偶发把 `\left\{\begin{array}...` 切成多个 `\(...\)` 片段导致不闭合,管线内置 `rebalance_math` 自动合并重包(配不平则原样放行)。 8. **`\begin{array}{r l}` 列格式带空格 → 整块公式变纯文本**:pandoc 的 `texmath` 只认紧凑列格式 `{rl}`,OCR 输出的 `{r l}`/`{l l}`/`{c c c c c}` 会让它报 `unexpected "l"` 并把整块公式(含 `$$`)降级成纯文本塞进 ``。另外 OCR 会把列类型 `l` 误认成数字 `1`。管线内置 `normalize_arrays` 在预处理阶段归一化(去空格 + `1`→`l`),只动 `\begin{array}`,不碰 `\begin{matrix}`。**症状自查**:pandoc 刷 `[WARNING] Could not convert TeX math ... unexpected "l"`。 9. **OCR 输出的零散 TeX 伪命令污染公式**:模型偶尔吐出四类畸形片段,pandoc 会各刷一条 `[WARNING] Could not convert TeX math`,并把**整块公式**降级为纯文本: - `\textcircled{1}` / `\textcircled{D}` —— 圈号本应输出 Unicode 圈号字符;数字 1~~20 映射 `①`~~`⑳`,字母/其它直接去壳取内容。 - `\arcsina` / `\arcsinb` —— `\arcsin` 被从中间劈开,还原成 `\arcsin a`。 - 公式里混入的裸引号 `"` —— 直接删掉(多为 OCR 把 `''` 认成引号)。 - 纯垃圾公式块 `$$ ## 145810 $$` —— 内容不含任何数学命令且形如 `#+数字`,整块丢弃。 以上统一由 `preprocess()` 里的 `fix_ocr_artifacts()` 处理,**在 `normalize_arrays` 之后、`rebalance_math` 之前**执行。**症状自查**:pandoc 刷 `unexpected control sequence \textcircled` / `unexpected '"'` / `unexpected '#'`。 10. **⚠️ HTML 表格被 pandoc 静默丢弃(表格全丢的根因)**:OCR 的表格后处理(`postprocess/otsl.py`)产出的是一段 HTML `…
`,但 pandoc 的 markdown reader 会把标签行解析成 **`RawBlock (Format "html")`**,而 docx writer 对 raw HTML **没有任何表示**,于是**整张表被无声丢弃**(不报错、不警告,只留 `=0`)。实测三种走法: | 走法 | Word 表格 | 公式 (oMath) | | ---------------------------- | ------- | --------------- | | HTML 表 + `-f markdown+…`(原状) | **0** ❌ | 2 ✅ | | `-f html` | 1 ✅ | **0** ❌ 公式全变纯文本 | | **改写成 markdown 管道表格** | **1** ✅ | **2** ✅ | ⚠️ 不能用 `-f html` 绕:那样表格是有了,但**行内 `\(…\)` 公式会全部退化成纯文本**(html reader 不认 LaTeX)。唯一正解是**送 pandoc 前先把 HTML 表转成 markdown 管道表格** —— 管线内置 `html_tables_to_markdown()`,在 `preprocess()` 最后一步执行。**症状自查**:docx 里 `w:tbl` 计数为 0,但源 md/中间产物里明明有 ``。 11. **⚠️ 图片整块丢失(`image`/`chart` 属于 skip 标签)**:`Xiaomi-OCR-0/pipeline/twostage_img2md.py` 的 `LABEL_TASK_MAPPING` 把 `chart`/`image`/`header_image`/`footer_image` 归入 **`"skip"`**,主循环遇到 `task_type == "skip"` 直接 `continue` —— 版面检测明明框出了图,却**既不做 VLM 识别、也不产出任何内容**。数学卷里的几何图、函数图像一旦丢失,题目就废了。这是「图片被丢失了」的根因。 修法:管线新增 `extract_images()`,在合并分页 md **之前**从 `.tmp_ocr/*.layout.jsonl` 读区域坐标,把 `image`/`chart` 区域从页面 PNG 裁出来存到 `.tmp_ocr/assets/`,再把 `![图形](assets/xxx.png)` 追加到该页 md 末尾;`pandoc_convert()` 传 `--resource-path` 指向 md 所在目录(`assets/` 才能被解析)。**顺序很重要**:`extract_images` → 合并 md → `pandoc_convert` → `clean_tmp`。 ⚠️ 两个实现陷阱: - **`.layout.jsonl` 的键是 `images`(列表),不是 `image_path`**(`results.jsonl` 才用 `image_path`)。读错键会让路径为空、正则匹配不上,`extract_images` 静默返回 0。 - **必须按尺寸/位置过滤误框**。版面模型会把校徽、水印、装订线也标成 `image`(实测把「华兴」校徽框成 64×58px)。过滤逻辑抽成了纯函数 `image_region_reject_reason(bbox, w, h)`,阈值见 `IMG_MIN_W/H`、`IMG_MIN_AREA`、`IMG_BAND_TOP/BOTTOM`;丢弃时会打印 `[img-filter] … 丢弃 N 个误框: p4#0[too-narrow(64px)]`。 **症状自查**:`verify_docx.py` 输出里 `images=0(media 0)`。验收期望值(C12 集):华兴 **3** 张、广工月考一 **1** 张,其余 3 份为 0(版面确实没标出图)。 11.5. **pandoc 不认 `\left/ … \right/`(竖线被 OCR 读成斜杠)**:OCR 会把绝对值/线段长的竖线 `|` 认成 `/`,产出 `\left/ F_{1} F_{2} \right/`。pandoc 认不出这种定界符,**整块 `$$` 公式降级为纯文本**(比裸 `/ PF_1 /` 更严重——后者 pandoc 反而能容忍)。修复:`_fix_delim_slash()` 把 `\left/`、`\right/` 还原成 `\left|`、`\right|`(保留原有空格),并对含数学特征的行还原成对出现的裸 `/ X /`;普通除法、日期 `2026/10/08`、`\frac` 不受影响。实测华科 p10 修复后 `oMath 424→425`、残留 TeX 4 处 → **0**。 11.6. **pandoc 把图片放大 ~2.08~2.78 倍(DPI 假设不一致)**:`pdf2imgs.py::DPI = 200`,裁出来的图是 200 DPI 的位图;而 pandoc 默认按 **96 DPI** 反推尺寸,于是 `crop_px / 96` ⇒ 图片被放大 **200/96 ≈ 2.08 倍**,宽图还会被卡到页宽上限 **5.83in**。实测 308×304px 的图标 pandoc 输出 4.28×4.22in(≈2.78 倍,因为又触发了页宽 clamp)。 - ❌ `![图](a.png){width=308px}` **不管用** —— pandoc 仍按 96dpi 解释,得到 3.21in。 - ✅ 正解:`extract_images.py` 的 `marker()` 显式写**英寸**:`{width={w_in:.2f}in height={h_in:.2f}in}`,其中 `w_in = crop_px / PDF_DPI`。 - `PDF_DPI` 常量必须与 `pdf2imgs.py::DPI` 一致(`test_preprocess.py` §13 锁定)。 - 症状自查:`verify_docx.py` 输出 `max_img=5.83x?.??in` 且 `clamped_img>0` ⇒ 有图被放大/截断。修复后华兴 23 张、华科 25 张全部 = `像素/200` 英寸,`clamped_img=0`。 11.8. **图片「四周型环绕」怎么改才对(`set_image_wrap_square`)**:pandoc 生成的图全是 ``(嵌入型)。用户要求改成 Word 的**四周型** ⇒ OOXML 里即 `` + ``。三个参数是**用 Word 渲染成 PDF 目视比对了 9 种组合**才定下来的: - **图片右对齐**(`positionH relativeFrom="column"` + `right`):原卷插图本就在右侧、文字绕左(华科第 1/2/4 题、华兴第 2 题都如此)。 - **`wrapText="left"`**:文字只在图片**左侧**排。图片右对齐后左侧≈整栏宽,长段落不会折断。 - **留白 0.125in**(`distL=distR=WRAP_SIDE_GAP=114300` EMU,Word 默认值)。 - ⚠️ **踩坑**:若用 **`bothSides` + 居中**,图片左右两侧都会分走文字 —— 长段落(华科第 4 题选项 D、第 5/6 题)被挤成**每行 2~3 个字**的窄列,惨不忍睹。**别用**。 - ⚠️ 另一个坑:`re.search(r"(.*)")` 只认**无属性**的 ``;若上游已写成 `` 就匹配不上、静默改 0 张。**必须用 `]*>`**。 - 校验:解包 `word/document.xml` 看 `inline=0 / anchor=N / wrapSquare=N / distL="114300"` 是否与图片数一致。 1. **paddlex 注册的 vLLM 插件刷一堆 ERROR**:`paddlex-3.7.2.dist-info/entry_points.txt` 里有 `[vllm.general_plugins] register_paddlex_genai_models`,加载它需要未安装的 `paddlex_genai_engine`,于是 vLLM 启动时刷 30 行 `RuntimeError: The generative AI engine plugins are not available` traceback。**无害**(vLLM 对插件失败只记录不中断,`Application startup complete` 照常),但我们用不到它 —— 设 `VLLM_PLUGINS=`(空串)即跳过所有插件,日志立刻干净。 2. **HF 缓存 `refs/main` 可能指向缺权重的快照**:下载中断会留下两个快照,`refs/main` 指向只有配置没权重的那个,报 `Cannot find any model weights with .../snapshots/xxxx`(联网时则是 `huggingface_hub.errors.FileMetadataError`)。两快照 `config.json` 通常一致,直接改 `refs/main` 指回含 `model.safetensors` 的快照即可,**不用重下 1.7G**。修复脚本:`_fix_hf.sh`(含大小校验 + 离线自检)。 3. **缓存齐全后加 `HF_HUB_OFFLINE=1`**:跳过 huggingface.co 的元数据校验,启动时间从 ~550s 降到 **115~135s**。 4. **WSL VM 空闲自动关机杀进程**:WSL 2.7.x 的 `[wsl2] vmIdleTimeout` 默认 60000ms,**只按「有没有 wsl.exe 会话」计时,VM 内跑着进程不算活跃**。`setsid nohup` 起的长任务在 wsl.exe 退出约 60s 后随 VM 一起被杀,连 `/tmp` 日志都被清空(这就是"服务莫名消失"的真相)。处置:`.wslconfig` 的 `[wsl2]` 设 `vmIdleTimeout=-1` 后**必须 `wsl --shutdown` 才生效**。 5. **WSL 突然报 `Wsl/0x8007000e`(E_OUTOFMEMORY)**:即使宿主还剩大量空闲内存也可能出现,是 WSL VM 自身故障。`wsl --shutdown` + 等几秒即可恢复。顺带一提,重启 VM 会带走里面所有进程(包括其它环境的训练任务)。 6. **`.wslconfig` 键放错 section**:WSL 3.0.x 里 `vmIdleTimeout` 属于 **`[wsl2]`**,`instanceIdleTimeout` 属于 **`[general]`**。放错会看到 `wsl: general.vmIdleTimeout:...` 之类告警且**静默失效**(VM 照样被回收)。当前配置两个 section 各设 `-1`。 7. **`Ctrl-C` 时 `except Exception` 抓不到中断 → 拉起中的 vLLM 变孤儿**:写「自己起服务」的清理逻辑时, `except Exception` **不会**捕获 `KeyboardInterrupt`(它直接继承 `BaseException`)。 更隐蔽的是:`KeyboardInterrupt` 会在 `started_by_us = start_server()` **赋值完成之前**抛出, 于是 `main()` 的 `finally` 看到 `started_by_us` 还是 `False`,跳过清理 —— 实测泄漏 **18,868 MiB** 显存。 正解:**清理写进 `start_server()` 内部用 `except BaseException:`**(就地收尸), 并让 `start_server()` 只在**真正就绪时**返回 `True`,`main()` 只负责关「就绪之后」的服务。 验收脚本思路:在等待期的不同时刻发 `SIGINT`,断言 `CLEANUP_RAN=True` 且 `nvidia-smi` 回到 0 MiB。 8. **关服务别用「杀掉监听 8000 的进程」兜底**:那是「本脚本起的」的过度近似, 会**误杀用户手动启动**、并希望跑完继续用的服务。只认自己写的 PID 文件 (`/tmp/vllm_xocr.pid`),文件不在就说明不是我们起的,跳过。 9. **`pkill -f 'vllm serve'` 会杀掉自己的 shell**:`-f` 匹配整条命令行,而发起这条 `pkill` 的 shell 命令行里**正好含有** `vllm serve` 字样 → 自杀(退出码 15/SIGTERM)。 要用 `pgrep -f` 取 PID 列表后逐个 `kill -TERM`,或写成脚本文件(脚本正文不含该字样也叫安全)。 ### 模式选择经验 | 文档类型 | 模式 | 说明 | | ------------------ | -------------- | -------------------------------------- | | 印刷文档 / 多页 PDF / 试卷 | **region**(默认) | 版面检测 + 区域并发,复杂页面更完整 | | 手写、古籍、不规则版面 | e2e 全页 | region 切分错误会累积,走 `pipeline.e2e_img2md` | ## 文件说明 | 文件 | 用途 | | --------------------------------------------- | ----------------------------------------------- | | `oxr_batch.py` | **OXR 一键批处理**(服务自管 + 解析 + 公式修复 + md→docx,主入口) | | `books_pipeline.py` | 四本《数学精编》专用管线(保留 md/json 中间产物供检测-修复循环) | | `detect_bare.py` | 裸公式检测:整篇过 pandoc 收集转换失败的公式,按模式聚类 | | `scan_all.py` / `scan_bare_formula.py` | docx 级验收:扫 document.xml 里残留的 LaTeX 纯文本 | | `docx_para.py` / `dump_docx.py` | docx 调试:按关键词提取完整段落 / 查看段落结构 | | `regen_one.py` | 重新解析单个 PDF 并存 md+docx | | `md2docx.py` | Windows 侧单文件 md→docx(复用 `repair_math`) | | `start_api.sh` | 手动启动解析 API 的参考脚本 | | `test_math.py` / `test_repair.py` | 公式修复规则的回归测试 | | `run_pipeline.py` | 旧管线一键管线(编排 + 图片提取 + 合并 + 公式后处理) | | `pdf2imgs.py` | 旧管线 PDF 渲染(WSL 内运行,支持 `--file` 单文件) | | `merge_md2docx.py` | 旧管线独立版 md→docx 合并(预处理与管线共用) | | `verify_docx.py` | docx 验收(公式数/表格数/**图片数**/分页符/残留 TeX,传文件或目录) | | `test_preprocess.py` | 旧管线文本预处理回归测试(改 `preprocess` / 图片过滤 / 图片环绕后必跑) | | `replace_omml2mml.py` / `ReplaceOMML2MML.exe` | 管理员权限替换 Office 的 OMML2MML.XSL(自动备份原文件) | | `setup_xocr.sh` / `serve_xocr.sh` | 旧管线 WSL 环境安装 / 服务启动参考脚本 | | `_fix_hf.sh` | HF 缓存 `refs/main` 指回含权重快照(含大小校验 + 离线自检),见「坑」#13 | ## 环境诊断小抄(Windows 侧) **「docx 被别人锁死了、改不动 / 删不掉」怎么查**:先用 Windows **重启管理器 API** (`RmStartSession` / `RmRegisterResources` / `RmGetList`)问系统「谁拿着这个句柄」—— 它能给出 `PID + 进程名`,比猜要快得多。本机实测过一次: ``` PID=40428 App=editor_sdk.exe Type=RmConsole Restartable=False ``` `editor_sdk.exe` 是 **WorkBuddy 内置的腾讯文档本地编辑器后端** (`%LOCALAPPDATA%\Programs\WorkBuddy\resources\app.asar.unpacked\node_modules\@tencent\tencent-docs-ai-engine\bin\win32-x64\editor_sdk.exe`, 带 `--port 39099`)。**只要在应用里预览/打开过某个 docx,它就会长期持有该文件句柄**, 直到进程被杀。下次再有「刚生成的 docx 打不开/覆盖不了」,先怀疑它。 两个配套判据: - **`~$<名字>.docx` 锁文件**:WPS/Word 打开文档时**必定**生成。**没有这个文件**基本可排除 是 Office 系应用在占用(本例就是这种情况,才定位到 `editor_sdk.exe`)。 - **WSL / DrvFs 下 `flock` 不可信**:`flock` 在 Windows 文件系统上只是 **advisory**, 报 `FREE` 也可能是被占的。跨边界的真信号是 **`mv`/rename 返回 `Permission denied`**。 **PowerShell 5.1 传中文路径会被搞坏**:`.ps1` 必须是 **UTF-8 with BOM** 才不吞中文; 即便如此,通过工具直接传的中文字面量仍会乱码(`Test-Path` 明明存在却返回 `False`)。 可靠做法:**把目标路径用 UTF-8 写进一个纯文本文件**,在 PowerShell 里用 `[System.IO.File]::ReadAllText($f, [System.Text.Encoding]::UTF8)` 读回来, **绝不在脚本里嵌中文字面量**。另注:`*>` 重定向产出 UTF-16,用 Read 工具看会乱码, 要写文件请用 `[System.IO.File]::WriteAllLines(..., UTF8Encoding($false))`。 ## 识别质量备注 - 印刷试卷(数学公式密集):可用,偶发结构级瑕疵(分段函数定界符拆分) - 手写公式:ρ/p 混淆等符号级错误,仅供草稿 - 古籍行书:会简体化,不建议