news 2026/10/15 3:50:49

OlibTauri OCR 文字识别实战:用 Umi-OCR 常驻服务插件把扫描版 PDF 变成双层可搜索 PDF

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
OlibTauri OCR 文字识别实战:用 Umi-OCR 常驻服务插件把扫描版 PDF 变成双层可搜索 PDF
  • 桌面应用

【免费下载链接】OlibTauri

O-lib is a free and open-source software application for PC

项目地址:https://gitcode.com/gh_mirrors/ol/OlibTauri
点击查看免费下载

本文围绕 OlibTauri 的 OCR 功能(设计文档见 dev_docs/ocr.md),完整讲解「扫描版 PDF → 双层可搜索 PDF」的实现方案:为什么选用 Umi-OCR 而非 Tesseract、umi-ocr插件清单如何声明一个常驻 HTTP 服务、服务型插件的拉起/就绪/隐藏/退出生命周期由谁接管,以及上传 → 轮询 → 下载 → 清理五步 HTTP 接口的具体参数和两个实测踩坑。读完本文,你能理解 OlibTauri 如何用插件系统把第三方 OCR 引擎变成「按需下载、完全离线、文件不出网」的一键功能,并有把握参考 src-tauri/src/commands/ocr_commands.rs 实现同类的服务化插件调用。

什么是双层可搜索 PDF

扫描版 PDF 的每一页本质上是一张图片,页面上没有任何文本对象。对这类文件直接做「转换格式」,转出来的内容要么是空白要么是乱码。OCR 补上的正是这一环:先 OCR 拿到文本层,再谈转换。

OlibTauri 的目标产物不是「PDF + 一份 TXT」,而是双层 PDF:原始扫描页面完整保留,在图片下方叠加一层不可见的文本层。于是这份 PDF 可以搜索、可以选中复制,版式与原件完全一致。识别引擎选用 Umi-OCR(MIT 协议),走 OlibTauri 的插件系统按需下载,识别全程本地完成,文件不出网。

为什么是 Umi-OCR,不是 Tesseract

Tesseract 只吃图片、只吐文本,要从「扫描 PDF」走到「双层 PDF」,得自己拼一条「PDF 渲染 → 识别 → 合成文本层」的管线,还要单独拖中文语言包和 Ghostscript,Windows 上的便携化非常脏。Umi-OCR 一个便携包就覆盖整条管线:解压即用、内置中英日韩等语言库、原生输出双层可搜索 PDF。清单里选用的是它的 Rapid 内核(约 98 MB,ONNX 推理,不依赖独立显卡)。

插件清单:umi-ocr如何声明一个服务型插件

OCR 引擎不是随应用分发的,而是编译进应用的内置插件清单 src-tauri/plugins.builtin.json 中的一个条目。OlibTauri 的插件契约是「一个插件 = 一份 JSON 声明」:从哪下载、怎么校验、怎么解压、可执行文件在哪、怎么调用,全部写在清单里,通用引擎按清单行事,插件作者无需写一行 Rust(契约定义见 src-tauri/src/plugins/manifest.rs)。

umi-ocr的清单条目如下(摘自 src-tauri/plugins.builtin.json):

{ "id": "umi-ocr", "name": "OCR 文字识别", "description": "把扫描版 PDF 变成双层可搜索 PDF:保留原版式,在图片下叠加隐藏文本层,从此能搜索、能选中复制。内置中英日韩等语言库,完全离线,文件不出网。", "capability": "ocr", "publisher": "hiroi-sora", "homepage": "https://github.com/hiroi-sora/Umi-OCR", "license": "MIT", "inputs": ["pdf", "epub", "mobi", "xps", "cbz"], "outputs": ["pdf"], "targets": { "windows": { "download": { "sources": [ { "name": "GitHub", "url": "https://github.com/hiroi-sora/Umi-OCR/releases/download/v2.1.5/Umi-OCR_Rapid_v2.1.5.7z.exe" } ], "sha256": "659c55896c32a5e019dc7bde1713d0e5c73186a2c653bed84c4480fa1795b722", "min_bytes": 52428800 }, "install": { "kind": "sfx-exe", "args": ["-o{dir}", "-y"] }, "entry": "Umi-OCR_Rapid_v2.1.5/Umi-OCR.exe" } }, "service": { "port": 1224, "health": "/api/doc/get_options", "post_start_args": ["--hide"], "quit_args": ["--quit"], "startup_timeout_secs": 90 } }

几个字段值得展开说明:

  • capability: "ocr":能力标识。OCR 特有逻辑(src-tauri/src/commands/ocr_commands.rs 中的ocr_plugin())就是按这个值从插件列表里查找目标插件的,应用据此决定在书架右键菜单露出入口。
  • inputs: ["pdf", "epub", "mobi", "xps", "cbz"]:描述「这个能力吃什么」。书架页只对扩展名落在这份清单里的文件显示「OCR 识别」菜单项;后端ocr_document也会再校验一次,不在清单内直接报OCR 不支持的文件类型。
  • download.sha256:安装包指纹,锁定Umi-OCR_Rapid_v2.1.5.7z.exe这一具体版本。min_bytes: 52428800(50 MB)是体积下限,用于识破「代理返回错误页」这类假成功——真实包约 98.6 MB。
  • install.kind: "sfx-exe"与args: ["-o{dir}", "-y"]:自解压安装器,{dir}会被替换为插件安装目录,-y表示免交互静默安装。
  • service段:这是与格式转换类插件(exec段)的本质区别。声明service的插件是常驻 HTTP 服务而非一次性命令行工具,应用负责拉起进程、等它就绪、退出时关掉它;而具体的 HTTP 协议因工具而异,由对应 capability 的处理器实现(见 src-tauri/src/plugins/manifest.rs 中ServiceSpec的注释)。ServiceSpec各字段含义:
字段取值(umi-ocr)含义
port1224服务监听端口,base URL 为http://127.0.0.1:1224
health/api/doc/get_options健康检查路径,GET 返回 2xx 即视为就绪
post_start_args["--hide"]就绪后立即执行的参数,把 GUI 收进托盘
quit_args["--quit"]应用退出时优雅关闭服务的参数
startup_timeout_secs90等待就绪的超时秒数(缺省值也是 90)

清单校验还有一套安全红线(PluginManifest::validate,并有 src-tauri/src/plugins/manifest.rs 内的单元测试覆盖):第三方来源的插件必须提供合法 sha256 否则拒绝安装、下载源必须 HTTPS、entry不得包含..逃逸插件目录、插件必须声明exec或service之一。内置清单(Origin::Builtin,编译进应用)代表应用作者背书,可以省略 sha256 或使用动态解析器。

服务型插件的生命周期管理

服务的拉起 / 就绪 / 隐藏 / 退出全部由通用的 src-tauri/src/plugins/service.rs 接管,这里不含任何 Umi-OCR 特有的知识——一切参数都来自清单的service段。

按需启动并复用进程

核心入口是service::ensure_running(plugin) -> Result<String, String>,返回服务 base URL。流程:

  1. 校验清单里存在service段、插件已安装(installer::engine_path能找到可执行文件);
  2. 先做健康检查:GET http://127.0.0.1:{port}{health},3 秒超时,2xx 即视为就绪。这意味着「已在跑」(哪怕是用户自己开的同一个程序)就直接复用,不会重复拉起进程;
  3. 不健康才真正spawn子进程,并登记plugin_id → 子进程 pid到进程内映射表RUNNING(供退出时收尾);
  4. Windows 下用CREATE_NO_WINDOW(0x08000000)创建标志启动,并把工作目录设为可执行文件所在目录——注释写明原因:「服务的工作目录必须是它自己的安装目录,否则大概率找不到模型/配置」;
  5. 每 600 ms 轮询健康检查,直到就绪或超过startup_timeout_secs(90 秒),超时则报「启动超时」;
  6. 就绪后立即执行post_start_args(--hide)把界面收进托盘,然后返回 base URL。

所以第一个 OCR 任务会慢一些(付一次冷启动开销),之后的任务复用同一进程。

应用退出时的收尾

shutdown_all()在应用退出时调用:遍历所有由本应用拉起的服务,先按清单声明的quit_args(--quit)优雅退出;若失败,Windows 下用taskkill /PID <pid> /T /F、其他平台用kill -9强杀。注释里说得很直白:否则会留下孤儿进程——GUI 程序不会自己消失,收不干净就会一直挂在后台。

OCR 任务的五步接口流程

前端ocr_document命令(经 src/api/ocr.ts 的invoke("ocr_document", { inputPath })调用)返回任务 id,真正的识别在异步任务里执行,实现见 src-tauri/src/commands/ocr_commands.rs 的run_ocr()。整个文件走 Umi-OCR 的 HTTP 文档接口,默认端口 1224:

第 1 步:POST /api/doc/upload

multipart 表单上传文件,同时带一个 JSON 字段:

{ "doc.extractionMode": "mixed" }

mixed模式是关键优化:有文本层的页直接提取、扫描页才走 OCR,又快又准——纯图片模式会把每一页都跑一遍识别,而全文档模式又会漏掉混合文档里的扫描件。响应结构是{ code, data },成功码为100,data即任务 id(doc_id),后续所有步骤都围绕它。

第 2 步:POST /api/doc/result轮询进度

每 800 ms 轮询一次,请求体{"id": doc_id, "is_data": false, "format": "dict"},直到is_done && state == "success"。响应里的processed_count / pages_count(已处理页数 / 总页数)直接折算成进度喂给任务表:

let pct = res.processed_count as f64 / res.pages_count as f64 * 100.0; // 留一点余量给「生成 PDF」这一步,别让进度条提前满格 runner::set_progress(task_id, pct * 0.9);

注意这里乘了 0.9 系数:识别完成之后还有「生成双层 PDF」这一步,把进度条上限压到 90% 避免它提前满格。轮询最多 3600 次(约 48 分钟),超限报「识别超时」。任何一步失败都会先调cleanup()清掉服务端临时文件再返回错误。

第 3 步:POST /api/doc/download

{ "id": "<doc_id>", "file_types": ["pdfLayered"], "ignore_blank": true }

pdfLayered这个值就是「双层可搜索 PDF」——Umi-OCR 支持多种结果类型(纯图片、纯文本、标注图等),只有pdfLayered会保留原页面图像并在下面叠加可搜索文本层。ignore_blank: true让空白页不产生无意义的文本。响应data是结果文件的 URL。

第 4 步:GET <URL>取回文件

源码里有段注释直指一个实测踩坑(详见下一节):接口返回的是绝对 URL,不能直接拼 base:

let file_url = if dl.data.starts_with("http://") || dl.data.starts_with("https://") { dl.data.clone() } else { format!("{}{}", base, dl.data) };

取回字节流后落盘到预先算好的输出路径;若文件为空则报错并清理。

第 5 步:GET /api/doc/clear/<id>

清掉服务端临时文件。这步失败不影响用户拿到结果(let _ = cleanup(...)),但成功路径也会执行,避免 Umi-OCR 的临时目录越积越多。

前置校验与输出命名

ocr_document在真正发起任务前做了一系列防护:

  • ensure_within_download_root把输入路径约束在书架根目录内,再经runner::simplify_path去掉 Windows\\?\前缀(外部程序不认扩展长度路径);
  • 扩展名必须在该插件清单的inputs里,否则报OCR 不支持的文件类型;
  • runner::is_running(input_path)防重复提交——同一文件已有任务在跑时直接报「该文件正在处理中」;
  • 输出路径由runner::unique_output_path(&input, "-OCR", "pdf")生成:同目录、<原名>-OCR.pdf,重名自动追加(n),绝不覆盖原扫描件(实现见 src-tauri/src/plugins/runner.rs);
  • 任务通过runner::create_task登记进内存任务表,exec 型和 service 型插件共用同一张表,前端因此不必区分两者;
  • 任务失败时先remove_file删掉半成品输出,再mark_failed落错误;成功则mark_success落库留痕——任务表是内存态重启即失,但「这个文件 OCR 过」的标记会写进数据库,书架据此显示「已 OCR」徽标。

两个实测踩到的坑

这两个坑都记录在 dev_docs/ocr.md 里,且在源码中有对应防护:

  1. /api/doc/download返回的是绝对 URL,不是文档里写的相对路径。如果按文档假设直接base + path拼接,会得到http://127.0.0.1:1224http://127.0.0.1:1224/...这种废 URL,OCR 会 100% 失败。代码里按http:///https://前缀判断(见上节第 4 步),别改回去。
  2. Umi-OCR 没有无界面启动参数。它是个 GUI 程序,启动时窗口会闪一下。策略是:先正常拉起 → 健康检查通过后立刻用--hide收进托盘 → 应用退出时用--quit优雅关闭,失败则强杀。这三件事全部声明在清单的service段(post_start_args/quit_args),由 src-tauri/src/plugins/service.rs 通用执行,ocr_commands.rs本身对「这是个 GUI 服务」一无所知。

前端交互细节

  • 入口:书架页(src/pages/BookshelfPage/index.tsx)右键文件 → 「OCR 识别(可搜索 PDF)」,仅对清单inputs里的格式(pdf/epub/mobi/xps/cbz)显示,由 src/pages/BookshelfPage/usePluginProcessing.ts 在挂载时拉取插件列表、按capability === "ocr"找到插件并取其inputs得到支持格式集合。菜单文案还有两处状态感知:任务在跑时显示「处理中…」,该文件已 OCR 过时显示「重新 OCR 识别」(见 src/pages/BookshelfPage/fileMenu.tsx 的buildFileMenu)。
  • 未安装插件时不报错:点击后先调ocrAvailable()(后端判断capability == "ocr"的插件是否已安装),未安装则 toast「需要先安装『OCR 文字识别』插件」并跳转到/plugins页,而不是直接抛错。
  • 进度展示:复用插件任务表。文件行内(src/pages/BookshelfPage/FileRow.tsx)显示「OCR 识别中 NN%」徽标;有运行中任务时前端每秒轮询getPluginTasks,任务结束通过plugin-task事件刷新文件列表,新产物xxx-OCR.pdf随即出现。「OCR 识别中」与格式转换的「转换为 XXX」按plugin_id区分(taskLabel逻辑在usePluginProcessing.ts)。
  • 溯源标记:mark_success落库后,书架会显示「已 OCR」(该文件被加工过)与「OCR 生成」(产物标记,hover 可见源文件名),避免用户分不清哪份是原扫描件、哪份是识别产物。

实测结论(2026-07-13,Windows 11)

设计文档记录了真实运行数据,可作为参考基线:

  • Rapid 便携包 98.6 MB,sha256 已锁定在清单里,并与 GitHub 直连下载逐字节比对一致;
  • 服务约 4 秒就绪(90 秒超时余量充足);
  • 中文识别置信度 0.92;
  • 一页图片型(无文本层)扫描 PDF:30,958 bytes → 43,395 bytes;
  • 输出同时含/Font+ToUnicode(可搜索、可复制)与/Image(原扫描图保留),确认是真正的双层 PDF 而非「图片 + 旁路文本」。

小结

OlibTauri 的 OCR 功能是一个「插件系统 + 服务化调用」的典型范例:引擎能力用 JSON 清单声明(下载源、sha256、service生命周期参数),进程管理由通用service.rs按清单执行(健康检查复用、CREATE_NO_WINDOW、--hide收托盘、--quit优雅退出 + 强杀兜底),OCR 特有的接口流程(mixed上传 → 页数进度轮询 →pdfLayered下载 →clear清理)则收敛在ocr_commands.rs一个文件里。全程本地、文件不出网,原扫描件不动,产物以-OCR后缀落盘并可追溯。

  • 桌面应用

【免费下载链接】OlibTauri

O-lib is a free and open-source software application for PC

项目地址:https://gitcode.com/gh_mirrors/ol/OlibTauri
点击查看免费下载

相关推荐

上一篇:NetWatch对接Prometheus教程:daemon导出/metrics指标,低基数设计轻松接入Grafana
下一篇:Mnemosyne的L3 Persona层:如何给AI Agent塑造持久稳定的『人格』

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 3:50:31

实时控制学习1

1、rclcpp::spin(node) 背后可以怎么理解&#xff1f;简单理解&#xff1a;spin(node) ↓ 创建/使用 Executor ↓ 把 Node 加进去 ↓ 持续等待 ROS2 事件 ↓ 执行 callbackSingleThreadedExecutor用一个线程依次执行 callback。比如 Node 有&#xff1a;Callback A Callback B …

作者头像 李华
网站建设 2026/10/15 3:49:38

foobox-cn 手机远程控制:10 分钟打通手机与电脑的音乐桥梁

foobox-cn 手机远程控制&#xff1a;10 分钟打通手机与电脑的音乐桥梁 【免费下载链接】foobox-cn DUI 配置 for foobar2000 项目地址: https://gitcode.com/GitHub_Trending/fo/foobox-cn foobox-cn 是为 foobar2000 打造的 DUI 皮肤方案&#xff0c;配合少量组件即可实…

作者头像 李华
网站建设 2026/10/15 3:48:35

联盟场-提高组-R4 ZYZSC-S-Round 7

QOJ7018 Insertion Sort 原题链接&#xff1a;Insertion Sort 分析 分讨的本质就是有问题的。如果继续容斥的话&#xff0c;那就完蛋了。 正确的式子就是&#xff1a; ansk!(1Cn−k1Cn−11)ansk!\times(1C_{n-k}^{1}\times C_{n-1}^{1}) ansk!(1Cn−k1​Cn−11​) 就是相当于…

作者头像 李华