☰
基于Umi-OCR API的局域网多用户文档识别服务部署指南
2026/10/21 18:23:22 网站建设 项目流程

标题里这个工具,说白了就是把我常用的开源OCR程序(Umi-OCR)用API方式跑在一台内网机器上,前面套一个网页,让整个办公室的人都能共享文档转换能力。

这东西的起因其实特别朴素:我们组里隔三差五就有“把这几页合同转成文字”“把扫描版的PDF导成可编辑文档”的活儿,挨个拿本地软件去识别、复制、排版,效率太低了。虽然Umi-OCR本身在个人电脑上用得挺顺手,但不能要求每个同事都装一遍、都会用命令行。与其反复传文件,不如直接在局域网里搭一个“多用户都能上传、自动识别、统一导出”的小工具,谁要用谁打开网页就行。

如果你也在纠结类似的问题——本地OCR工具挺好用,但不知道怎么变成团队服务;或者你在研究Umi-OCR的API到底怎么接、参数怎么传、并发能扛多少——那这篇就是写给你看的。下面是我从选型到落地、再到给同事试用的完整思路和踩坑记录,代码不会很长,但坑都是真踩过的。

1. 为什么不做“每人装一个客户端”,非要单独搭服务

先说结论:如果你的需求只是偶尔识别一两张图,那直接在个人电脑上装Umi-OCR的图形版就够了,折腾服务纯属浪费。但一旦到了“多人、批量、需要统一管理结果”这个层面,单机版的短板就很明显了。

1.1 单机版遇到的问题

Umi-OCR本身是一个开源的OCR工具,基于深度学习模型做文字检测和识别,中文、英文、公式都能处理,桌面版的实测准确率在常见印刷体场景下表现相当好。但它默认是“打开一个窗口,选图片,点识别”的交互模式,这带来两个问题。

第一,识别能力被绑定在某一台电脑上。一个几十人的团队里,只给一个人装Umi-OCR,那他就是全组的“人肉中转站”,文件传来传去,识别完再传回去,路径一乱就全乱套。

第二,结果不沉淀。今天识别了一份合同,明天想搜一下“违约金”在哪个文件里,单机版给你的是一段段txt,既不集中,也没法检索。时间一长,文件散落在各个聊天记录和临时目录里,等于白干。

1.2 恰好Umi-OCR提供了API模式

Umi-OCR的项目里其实带了一个独立的API服务,可以把OCR能力封装成HTTP接口,别的程序通过请求就能调用,不需要打开图形界面。这个设计本来是为了方便二次开发,但拿来部署成内网服务刚刚好。

我在这一步做个简单的技术判断:如果自己用PaddleOCR或者Tesseract重新封装,不是不行,但训练和调参成本太高。Umi-OCR的API模式等于把“识别模型、图像预处理、文字检测、结果格式化”这些脏活全都包好了,我只需要关心HTTP请求怎么写、任务怎么调度。**能用现成的优质轮子,就不要自己造。**尤其是OCR这种高度依赖模型效果和工程经验的领域,自己从零搞一套,效果大概率不如开源项目调好的方案。

1.3 部署成服务带来的三个好处

真正搭完跑起来之后,我对比了一下和单机版的使用体验,差别非常明显:

  1. 多用户并行:每个人打开网页就能传文件,任务在后端排队,互不干扰。
  2. 结果统一管理:识别完的文本自动存到服务器的指定目录,还能顺便生成可下载的txt、Markdown文件,管理成本很低。
  3. 维护集中:模型升级、参数调整只动服务器上一份配置就行,不用挨个帮同事更新客户端。

所以,最终方案定为:在局域网的一台Windows/Linux机器上常驻运行Umi-OCR的API服务,再写一个简单的Web后端做文件接收、任务分发、结果回传,前端就是一张极简的上传页面。

2. 整体架构:核心链路到底是怎么走的

这个工具本质上是一个“中转站”,它本身不识别图片,只负责把用户上传的文件转成Umi-OCR能识别的格式,再把识别结果转回给用户。实际上分为四条链路。

2.1 从浏览器到服务端的链路

用户打开浏览器,访问的是一个FastAPI(或者Flask,看你顺手)服务。服务端负责接收上传文件、保存到临时目录、创建任务ID、把任务加入队列,然后立即返回“任务已接收”。这一步用异步库处理,避免大文件上传把请求卡死。

前端不是实打实地一直等待识别结果,而是通过轮询接口查询任务状态。识别一小张图可能就两三秒,但识别一份几十页的PDF可能要几分钟,用户不可能一直保持一个HTTP连接不关闭。

所以整体请求流向是这样的:

浏览器(上传PDF/图片) -> Web服务(FastAPI) -> 临时目录/任务队列 -> 文档解析模块(PDF渲染成PNG) -> Umi-OCR API服务(端口默认1224) -> 识别文本 -> 结果写入结果目录 -> 浏览器轮询任务状态,完成后触发下载

这一条链路上,每一步都要有明确的职责和超时控制,不然任何一个环节卡住都会导致整个任务僵在那里。

2.2 文档转换的细节:PDF要先变成图片

这里有个重要的前提:OCR识别的对象是“图片像素”,不是“PDF文件”本身。Umi-OCR的API接口接收的是图片或者图片的base64编码,而PDF是一种矢量/混合格式,不能直接丢给OCR引擎。

所以我的流程是:如果用户上传的是PDF,先调用PyMuPDF(fitz)把每一页渲染成PNG格式的图片,再逐页调用OCR接口。渲染参数有一个关键值,叫DPI。默认我用200-300 DPI,太低的话小字会糊,OCR准确率会明显下降;太高的话图片尺寸过大,传输和识别内存都会飙升,识别速度也不成比例地变慢。实测下来,300 DPI对绝大多数办公文档来说已经是性价比极优的选择。

如果有朋友上传的是高清长截图,那就不需要渲染,直接走“接收图片→调OCR接口”的路径。这里我还做了一个分支判断:先尝试用PyMuPDF直接提取PDF里的文本层,如果能提取到,说明这份PDF是文字版,直接返回即可,根本不需要OCR;只有“提取不到任何文字”的PDF才进入OCR流程。这个前置判断能省掉大量不必要的计算。

2.3 技术栈到底怎么选

我当时后端用的是FastAPI,因为这个框架处理文件上传、异步任务、JSON返回都很顺手,而且自带接口文档,局域网里别人调用时也方便。

前端没有用框架,就是一个单HTML页面,里面写了上传组件、任务列表、轮询逻辑。这是刻意为之——工具面向的是部门内部,不需要复杂的构建工程,越简单越不容易坏。

另外,为了配合Umi-OCR API的调用,后端还要配一个HTTP客户端库,我用的是httpx,因为它同时支持同步和异步,对超时控制也更细致。数据库方面,初期没有上MySQL,直接用了SQLite,存任务ID、用户标识、文件路径、状态、结果路径这几张表,对内网几十人的使用量完全够。

3. 核心实现:Umi-OCR API调用时的工程化细节

这一章是技术含量最高的一部分,也是网上资料最少的部分。官方文档里给的示例大多是“打开API,发一张图,返回一句话”,但真正拿到生产环境里用,还差着好几层包装。

3.1 如何启动Umi-OCR的API服务

Umi-OCR安装包里通常会自带一个专门的服务端启动程序,找类似umi-ocr_server的入口,双击启动后,它会默认监听127.0.0.1的某个端口,常见是1224:

# 以Windows服务方式举例 umi-ocr_server.exe

如果是在Linux服务器上跑,也可以用命令行方式启动,然后让它常驻后台。启动成功后,它其实就是一个小型HTTP服务,我们可以用浏览器访问它的根路径确认状态。

注意:Umi-OCR的版本迭代比较活跃,不同版本的服务端接口路径和字段名可能有变化。你在本地部署时,先用浏览器打开API地址,查看它提供的接口说明页面,确认当前版本的请求字段再写代码,千万别直接照抄旧文档。

3.2 单张图片识别的最简调用

假设API接收的是base64编码的图片数据,那么最直接的请求是这样的:

import base64 import httpx def ocr_image_bytes(image_bytes: bytes, api_url: str = "http://127.0.0.1:1224/api/ocr") -> str: b64 = base64.b64encode(image_bytes).decode("utf-8") payload = { "base64": b64, "options": {} } # 这里的超时是个关键,长图识别可能超过10秒 resp = httpx.post(api_url, json=payload, timeout=60) resp.raise_for_status() data = resp.json() # 返回结构里通常包含data字段,存的是识别出的文本 return data.get("data", "")

这个代码里有两处是经验值,尤其值得新人注意:

第一个是timeout=60。很多第一次写的人会默认用requests的默认超时,也就是不设超时,结果是服务端一旦识别慢,前端请求就挂着占线程,任务一多整个Web服务都被拖死。我后来统一把超时调成60秒,并且做了一个简单的重试机制:超时或者返回空文本时,最多重试一次,因为OCR偶尔会因为内存或模型推理的偶发问题返回空结果。

第二个是串行限制。Umi-OCR的API服务虽然可以接收并发请求,但它的识别引擎处理图片是消耗CPU和内存的,并发太猛会直接把内存打满。在初期测试时,我试着同时提交10张图片,结果服务直接假死了。所以后端里我加了一个全局的信号量(Semaphore),限制同一时间最多只有1个(或者按服务器CPU核数放宽到2个)OCR请求在跑,其他任务老老实实在队列里排队。

核心观点:Umi-OCR API本身不难调,难点在于“并发控制”。多用户场景下,你必须把“并发请求”降级成“队列请求”,识别正确率才会稳定。

3.3 多页PDF逐页识别与结果拼装

PDF渲染之后会产生多张PNG图片,我的处理逻辑是逐页调用上面的ocr_image_bytes函数,然后把每一页的文本拼在一起,中间用分页符隔开,方便用户在导出文本时看清楚“第几页内容结束,第几页内容开始”。

这个做法看起来没什么技术含量,但省掉了传统导出时“一页一个txt文件”的麻烦。用户拿到的是一个完整度很高的纯文本文件,或者Markdown文件,后续直接交给别的大模型做摘要、关键词抽取都行。

同时,我还把每一页的“平均识别置信度”打印到日志里。Umi-OCR的返回对象里如果带了置信度字段,就顺手取出来;如果整页置信度偏低,就给前端任务加一个“低置信度”标识,让用户自己决定要不要人工校对。这一点是我后面被同事抱怨“识别出来有错字”之后加上的——OCR工具不可能100%准确,与其让用户默默接受错字,不如主动提示可信度,至少让用户心里有数。

3.4 图片预处理和后处理

在实际调用中,我发现有些图片识别效果不好,并不是引擎不行,而是图像本身没做好预处理。以下两个场景是最高频的:

  • 手机拍照的文档:倾斜严重,Umi-OCR虽然内置了方向检测和校正,但有时还是会误判。我这边做了一个简单策略:用OpenCV先做一次边缘检测,计算文档主体轮廓的角度,超过阈值就做透视校正,再送识别。效果提升非常明显。
  • 文字颜色浅或者背景有底纹:直接转灰度化,再用自适应阈值处理,能极大减少背景干扰。

预处理这块我控制得比较克制,只做了校正和灰度化,没有用太重的滤波,因为过度处理反而会让笔画断裂,降低准确率。工程上什么都要克制,不要一上来就叠滤镜。

4. 多用户任务队列与并发控制:核心难点是怎么不“打架”

既然是给局域网内多用户用的工具,就不能只关心“识别的准不准”,还要关心“同时10个人用会不会崩”。这一章的标题已经说明了,并发控制才是这个工具最需要花心思的地方。

4.1 任务状态与数据库设计

我为每一个上传的任务创建了一条记录,字段大概是:

字段类型说明
task_id字符串唯一任务ID,用uuid生成
user_id字符串用户名或IP标识,用于隔离目录
file_name字符串原始文件名
status字符串pending / processing / done / failed
result_path字符串识别结果文件路径
created_at时间戳任务创建时间
progress整数识别进度,用于前端展示

SQLite存这些完全够用。之所以没用内存结构全存,是因为担心进程重启导致任务状态丢失,用户看不到历史记录会觉得莫名其妙。

前端每2秒轮询一次/api/task/status/{task_id}接口,拿到progress和status后更新页面上的任务卡片。任务结束之后,按钮从“识别中”变成“下载结果”。

4.2 临时目录按用户隔离

这里有个安全细节容易被忽略:如果所有用户的上传文件都放在同一个目录下,文件名一旦相同就会互相覆盖。我用的方案是:

uploads/{user_id}/{task_id}/原始文件.pdf results/{user_id}/{task_id}/识别结果.txt

user_id直接用登录时的用户名或内网IP前两段生成,目录结构天然隔离,不仅避免了覆盖问题,还方便定期清理老任务——超过一周的目录直接按创建时间删除。

清理脚本我写成了一个后台定时任务,每天晚上把7天前的临时文件全部删掉。这个清理必须做,否则服务器的磁盘很快会被几十页的高清PNG塞满。

4.3 全局并发限制

OCR识别是CPU密集型的活儿,那台内网机器再强也经不住无限制的并发。我给OCR调用加了锁:

import asyncio ocr_semaphore = asyncio.Semaphore(2) async def ocr_with_limit(render_image): async with ocr_semaphore: return await asyncio.to_thread(ocr_image_bytes, render_image)

设置Semaphore=2是因为那台服务器有4核CPU,留出两个核给Web服务和系统调度。如果你跑在Windows办公电脑上,建议设成1,因为Windows的进程调度和内存控制都不如Linux那么从容。这个数值可以在配置项里改,不写死。

4.4 前端轮询策略

前端我用一个极简的setInterval去轮询。需要注意的一个坑是:任务处理完,如果用户还开着页面,一定要让轮询停下来,否则会一直对服务器发起无效请求:

function checkTaskStatus(taskId) { const timer = setInterval(async () => { const res = await fetch(`/api/task/status/${taskId}`); const data = await res.json(); if (data.status === 'done' || data.status === 'failed') { clearInterval(timer); renderResult(data); } else { updateProgress(data.progress); } }, 2000); }

2秒一个请求,几十个人同时用,对服务器的压力非常小,完全够用。

5. 实测中踩过的那些坑:从中文乱码到PDF页面丢失

这一章全是实打实的教训。有些问题排查了一天,最后发现只是一个编码没处理对。我不把它们列全,挑几个最典型的说。

5.1 中文文件名的编码问题

当一个用户上传“第1季度报-扫描版.pdf”时,FastAPI的UploadFile.filename在Windows浏览器下通常是UTF-8编码的,但在某些国产浏览器或者老旧的HTTP代理环境下,后端拿到的文件名会出现乱码,最常见的就是“锟斤拷”。

解决方法是明确后端和前端的字符编码一致,前端在FormData提交时不需要特殊处理,后端在保存文件时强制转成UTF-8,并且不直接用原始文件名落盘,而是用task_id + 扩展名的方式来命名文件。原始文件名只存进数据库字段用于展示。

# 不要这样做 file_path = os.path.join(task_dir, upload_file.filename) # 应该这样做 ext = os.path.splitext(upload_file.filename)[-1] file_path = os.path.join(task_dir, f"{task_id}{ext}")

这个坑主要坑在“直接从浏览器调试时正常,一用内网部署的反向代理就乱”,排序下来优先级很高。

5.2 图片过大导致API返回空结果

Umi-OCR识别很清晰,但如果你把一张5000x10000像素的长截图直接塞过去,它可能会默默返回空文本,而不是报错。这个问题非常坑,因为它表现为“识别完成但结果为空”,没有异常日志。

我当时的解决思路是:在调用OCR接口之前,先检查图片的像素尺寸。如果单边超过2000像素,就先等比缩小到2000以内,然后再送识别。或者更稳妥的,把长图从上到下切成若干块,逐块识别,最后拼接。切分方案对合同、聊天记录这类长截图效果极好。

这个方法顺带提升了整体准确率,因为Umi-OCR对“单张图片里文字密度太高”的情况,偶尔会漏检,切成块之后反而更稳。

5.3 把“文字版PDF”也送去OCR,白白浪费时间

扫描版PDF识别很慢,但有些用户上传的其实是“由Word保存而成的PDF”,这种PDF里本身就带文本层,根本不需要OCR。刚开始没做判断,一份50页的文字版PDF送去逐页渲染识别,足足跑了十几分钟,用户还以为工具坏了。

后来在渲染之前增加了一个文本提取尝试:

import fitz doc = fitz.open(pdf_path) full_text = "" for page in doc: full_text += page.get_text() if full_text.strip(): break

只要某一页能提取到非空文本,就判定为文字版PDF,直接跳过OCR流程,速度从十几分钟降到几秒钟。这个优化带来的体验提升是巨大的,也让我意识到一个好工具不仅是“能干活”,还得“聪明地干活”。

5.4 OCR服务与Web服务的端口暴露问题

Umi-OCR的API服务默认监听的是127.0.0.1,也就是只有本机能访问,这是安全的。但如果你在启动时不小心让它绑定了0.0.0.0,那局域网内任何一个人都能绕过你的Web页面直接调用它,虽然不致命,但也不好。

我在部署时,把Umi-OCR的API服务保持只监听127.0.0.1,Web服务监听0.0.0.0:8080,所有流量都先到Web服务,再由Web服务转发给本机的OCR API。这样既保证了安全,又让外部访问统一,不暴露多余端口。

6. 部署到局域网:真正让同事用起来的最后一步

代码写完、本地测试通过之后,离“能用”还差一步,这一步叫部署。我把这个工具跑在实验室一台常开的办公电脑上,做了三件关键的事。

6.1 用systemd/开机自启服务管理进程

Linux服务器上我用systemd维护两个服务:一个是Umi-OCR的API服务,另一个是FastAPI的Web服务。写两个独立的service文件就可以,启动顺序上让Web服务依赖OCR服务:

# 大致思想 systemctl enable umi-ocr-api systemctl enable doc-converter-web

如果是Windows机器,可以用计划任务或者nssm把程序注册成Windows服务,实现开机自启,免登录自启。这一步不做好,同事周末加班要用时,发现电脑被重启过、服务没起来,就很头疼。

6.2 反向代理与前端访问

我在Web服务前面挂了一层Nginx做反向代理,端口统一收敛到8080。同事访问时只要在浏览器里输入http://内网IP:8080就行,不用记一串带端口号的诡异地址。Nginx的配置里额外加大了一点上传体积限制:

client_max_body_size 200m;

不加大这个值的话,一个100MB的高清扫描件会被Nginx直接拒收。

6.3 用户边界与访问控制

因为是内网工具,我没有做复杂的登录体系,但也不能完全裸奔。我直接用内网IP的前两段生成user_id做隔离。如果后续要接入公司统一的账号体系,只需要替换成从请求头里读取用户名即可,改动成本很低。

这里有必要提醒一句:这个工具只适用于可信内网,不要图省事把端口映射到公网。OCR服务一旦暴露在公网,任何人都能上传任意文件,那你的内网机器就成了别人的免费扫描器,还可能被恶意灌入超大文件把磁盘塞满。安全边界一定要守住。

7. 可扩展方向:别让工具停留在“能用”就结束

这个工具上线跑了半个多月,组里反馈很好,但我自己知道它还能做得更多。有些是顺手能做的,有些是下一步可以想的。

7.1 批量压缩包处理

目前每次只允许上传单个文件,但实际场景里,经常有人一次发来一个压缩包,里面有二十份扫描文件。下一步可以支持zip上传,解压后自动批量走队列,生成结果时也打包成zip返回。这个功能在代码上不复杂,只是把“单文件任务”改成“目录任务”就行。

7.2 对接全文搜索:让识别结果变成“可检索资产”

识别出来的文字如果只停留在txt里,价值就打折了。我现在正在做的是把结果文本同步到一份SQLite全文搜索引擎里,这样以后组里想查“哪份合同里出现过指定条款”,直接在搜索框里输入一句话就能定位到具体文件,识别结果才真正变成了数字资产。如果你有更高阶的需求,接Elasticsearch也行,但内网这个量级没必要。

7.3 和AI总结能力联动

现在识别出的文本大多是长段落,阅读成本还是偏高。下一步的计划是把识别整段文本送到本地或云端的大模型API做一个结构化摘要,直接生成“关键条款”“金额”“日期”这些结构化字段。这样用户上传一份合同扫描件,拿到的就不只是文字版,而是可直接录入系统的条目,工具也从“转换器”升级成了“信息抽取器”。

我个人建议你如果时间有限,优先做“压缩包批量+全文搜索”这两个,因为它们对日常效率的提升是肉眼可见的。识别完生成结构化摘要这个方向挺诱人,但要做好准确率评估,不要一上来就全量铺开。

最后分享一个我在实际运行中特别受用的心得:别把OCR当成一个一次性的功能,它更适合作为整个文档处理链路的一个前置步骤。只要把识别这步做到稳定、可控,再往后面接搜索、接摘要、接自动化分发,都会顺畅得多。这个工具我后续还会迭代,核心思路就是扩大“它能自动完成的事情占比”,让同事打开网页、上传、下载三个动作,甚至三个动作缩成一个。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询