- 人工智能
- 大模型
- 微调
- LoRA
- 模型优化
- 模型量化
- 强化学习
【免费下载链接】unsloth
Local UI to run and train LLMs and diffusion models. Supports GGUF, MLX, Qwen3.8, DeepSeek-V4, MiniMax-H3, Gemma 4, FLUX and more.
Unsloth Studio(仓库 studio/ 目录)内置了一套完整的扫描版 PDF 文本提取能力:在 Chat 对话、项目来源(project sources)与 Data Recipes 三种场景中,凡是"纯图像页"的扫描 PDF,都会被自动转录为可检索文本;已有可选文字层(selectable text)的页面则原样保留。本文以官方文档 studio/PDF_OCR.md 为主线,结合仓库中core/rag模块的源码与测试,完整讲解本地 OCR 的搭建步骤、全部RAG_OCR_*环境变量、视觉模型与 Tesseract 的双引擎回退链路、独立进程提取机制,以及上传失败时如何精准定位并解决"不可读页面"问题。
一、OCR 工作流总览:三个入口、两套引擎
1.1 三种应用场景
扫描 PDF 的 OCR 并不只在某一个界面生效,而是贯穿 Unsloth Studio 的检索链路,覆盖三个入口:
- Chat 上传:对话中上传的扫描 PDF 会被纳入当前线程(thread scope)的 RAG 索引,之后可在对话中按语义检索到扫描件内容;
- 项目来源(project sources):加入项目的扫描文档同样走 OCR 预处理,项目内检索可用;
- Data Recipes:以扫描 PDF 为数据来源构建学习配方(learning recipe)时,需要先把 PDF 正文提取成可用文本,再走配方流程。
三者共享同一套页面分类与转录机制(核心实现在 studio/backend/core/rag/ 下的parsers.py、pdf_ocr.py、ingestion.py),区别仅在 OCR 引擎的优先级与进程模型上。
1.2 双引擎回退顺序
文档明确给出两条 OCR 路径,其优先级与触发条件在源码中一一对应:
| 场景 | 第一优先 | 回退引擎 | 是否依赖已加载的聊天模型 |
|---|---|---|---|
| Chat / 项目上传 | 已加载的本地vision GGUF模型 | 本地Tesseract(通过 PyMuPDF 集成) | 是(优先用视觉模型) |
| Data Recipes | 本地Tesseract | 无 | 否(不要求加载聊天模型) |
Chat 场景的视觉模型路径实现在 captioner.py 的ocr_pages():先把扫描页用render_pdf_pages()渲染成 PNG(默认 150 DPI),再逐页调用已加载的视觉 GGUF 模型转录;若视觉模型未加载或转录失败,则回退到本地 Tesseract。而 Data Recipes 的 PDF 提取直接走 seed.py 中的pdf_ocr.extract_text(),只依赖本地 Tesseract,因此即使当前没有加载任何视觉模型也能工作。
需要区分的是:正文 OCR 与图片描述(figure captioning)是两套独立功能。Chat 界面另有独立的 figure-captioning 设置,用于给正文之外的插图生成文字描述,对应RAG_CAPTION_*配置组(见 config.py),不要与扫描页 OCR 混淆。
二、哪些页面需要 OCR:页面分类的判定逻辑
"只对纯图像页做 OCR、已有文字层原样保留"这句话背后,是一套精确的逐页判定逻辑,位于 parsers.py 的_pdf()函数。每个页面最终生成一个Page对象,携带text、page_number(1 起始)、char_count与needs_ocr四个字段(见 parsers.py)。
判定规则可以归纳为三层:
- 无文字层的纯图像页:页面中存在嵌入图像(
page.get_image_info()非空)且没有可提取的纯文本(plain.strip()为空)时,needs_ocr = True。这是最典型的扫描页; - 带可选中页眉/页脚的扫描页:页面存在覆盖面积 ≥ 页面面积 50% 的大图时,把图像区域上下各裁掉 10% 得到"正文区域"(
body,见 parsers.py),若该区域内可选文字长度小于OCR_MIN_CHARS(默认 16 字符),则判定为扫描页。这正是文档所说的"带可选中页眉页脚的扫描件也要 OCR"——页眉页脚的少量文字不会让整页免于转录; - 不触发 OCR 的情形:面积小于页面一半的小 logo、以及页面本身有足够可选正文时,
needs_ocr = False;完全空白的间隔页也不会被标记(对应测试 test_pdf_local_ocr.py 中的test_blank_separator_needs_no_ocr)。
此外,PDF 解析层还有两项重要保护:一是密码保护的 PDF 会直接抛出encrypted PDF requires a password(parsers.py);二是默认启用布局感知的 Markdown 提取(RAG_PDF_MARKDOWN=1,基于 pymupdf4llm),但当检测到 RTL/印度系文字被字形重建破坏、或 Markdown 文本量显著少于原始文字层时,会自动回退到 PyMuPDF 的逻辑顺序纯文本,避免检索内容被破坏(parsers.py)。
三、本地 OCR 环境搭建:Tesseract 与语言数据
扫描页转录的后备引擎是Tesseract,通过PyMuPDF 的内置 OCR 集成(page.get_textpage_ocr())调用。搭建过程分三步,全部在运行 Unsloth Studio 后端的那台机器上完成:
3.1 安装 Tesseract 引擎与语言数据
按操作系统安装 Tesseract 软件包及其语言数据,例如:
# Debian / Ubuntu 示例(安装引擎 + 英文语言数据) sudo apt-get install tesseract-ocr tesseract-ocr-eng也可从 Tesseract 官方 tessdata 仓库获取语言文件。仓库不会自动下载任何 OCR 数据——ocr_pages()的 docstring 明确写着"No downloads or model loading. Tesseract language data must already be installed"(pdf_ocr.py),这一点务必在部署时自行满足。
3.2 设置 TESSDATA_PREFIX
TESSDATA_PREFIX必须指向存放.traineddata文件的 tessdata 目录,并且在启动 Unsloth Studio 之前设置好,因为它是在进程启动后读取的环境变量。在代码中,该值会被透传给 PyMuPDF 的tessdata参数(pdf_ocr.py):
# 示例:把 TESSDATA_PREFIX 指向系统 tessdata 目录后再启动 Studio export TESSDATA_PREFIX=/usr/share/tesseract-ocr/5/tessdata # 然后启动 Unsloth Studio 后端注意:
tessdata = os.environ.get("TESSDATA_PREFIX") or None,如果未设置,PyMuPDF 会使用其内置查找路径;为了可控性,官方文档建议显式配置。
3.3 设置 RAG_OCR_LANGUAGE
RAG_OCR_LANGUAGE指定要使用的语言代码,默认值为eng(见 pdf_ocr.py)。多个语言用+连接,此时对应语言的.traineddata文件都必须存在,例如eng+deu要求同时具备eng.traineddata与deu.traineddata:
export RAG_OCR_LANGUAGE="eng+deu"四、OCR 相关环境变量完整对照表
所有 OCR 参数都集中在 config.py,每个值均可通过环境变量覆盖,并统一使用RAG_OCR_前缀:
| 环境变量 | 默认值 | 源码字段 | 含义 |
|---|---|---|---|
RAG_OCR_SCANNED | 1(启用) | OCR_SCANNED | 是否默认对扫描页执行 OCR。设为0可关闭默认行为(详见第五节);Chat 界面的OCR scanned pages开关可对 Chat/项目上传单独覆盖 |
RAG_OCR_MIN_CHARS | 16 | OCR_MIN_CHARS | 页面可选文本长度低于该值时,才被纳入"可能扫描页"候选;也用于判定带页眉页脚的扫描页正文是否可读 |
RAG_OCR_MAX_PAGES | 20 | OCR_MAX_PAGES | 每个 PDF 最多转录的扫描页数上限。预期扫描件页数更多时,应在启动 Studio 前调大 |
RAG_OCR_DPI | 150 | OCR_DPI | 本地 OCR 与页面渲染使用的 DPI。分辨率过低的小字扫描件可尝试调高 |
RAG_OCR_TIMEOUT_S | 60 | OCR_TIMEOUT_S | 视觉模型单页 OCR 的超时秒数 |
RAG_OCR_MAX_TOKENS | 2048 | OCR_MAX_TOKENS | 视觉模型单页 OCR 的最大输出 token 数 |
RAG_OCR_LANGUAGE | eng | 直接读取 | Tesseract 语言代码,多语言用+连接 |
TESSDATA_PREFIX | 未设置 | 直接读取 | .traineddata文件所在目录 |
另外,正文之外还有一组RAG_CAPTION_*配置(RAG_CAPTION_IMAGES、RAG_CAPTION_MAX_IMAGES、RAG_FIGURE_DPI、RAG_FIGURE_TILE_ROWS/COLS等,见 config.py),它们控制 Chat 的图片描述功能,属于另一条独立链路,OCR 文档中提到的"Chat has a separate figure-captioning setting"即指此。
五、限制与失败上传:宁可报错,不可静默丢页
这是 Unsloth Studio 扫描 OCR 最有价值的设计:如果扫描页无法转录,上传会失败并明确列出页码,而不是静默接受一个内容残缺的文档。
5.1 默认开关与覆盖优先级
- 后端默认值在源码中是启用的(
OCR_SCANNED默认1);设置RAG_OCR_SCANNED=0可关闭默认扫描页 OCR; - Chat 的OCR scanned pages设置可以对该开关进行覆盖——即后端默认关闭时,Chat 仍可为 Chat/项目上传单独开启;
- Data Recipes 则严格遵循后端默认值,没有界面开关覆盖。
这一点在 ingestion.py 中得到印证:_ocr_scanned_pages()首先判断config.OCR_SCANNED if ocr is None else ocr,其中ocr参数即来自 Chat 侧的可选覆盖。
5.2 失败语义与错误信息
当 OCR 转录完毕后,仍有needs_ocr页面未被成功转录时,extract_text()会抛出unreadable_pages_error()(pdf_ocr.py)。错误信息包含:
- 不可读页码列表(最多列出前 20 个,超出时追加
(and N more)); - 修复提示:启用 OCR、配置 Tesseract 语言数据(
TESSDATA_PREFIX),或改传带文字层的可检索 PDF; - 当前
OCR_MAX_PAGES上限的说明。
对应测试 test_pdf_local_ocr.py 验证:当 OCR 引擎完全不可用时,上传状态为error,错误文本包含scanned PDF pages: N与TESSDATA_PREFIX提示,且不会残留任何半成品文件(_block_files(route) == [])。
5.3 页数上限不是静默截断
OCR_MAX_PAGES(默认 20)存在两种行为:
- 超出预算的部分:
_ocr_scanned_pages()会记录 warning("pages past the cap stay untranscribed (raise RAG_OCR_MAX_PAGES to cover them)")并截断(ingestion.py); - 已纳入预算但转录失败的部分:直接导致上传失败并报出页码。
测试test_ocr_page_cap_does_not_silently_drop_pages(test_pdf_local_ocr.py)把上限压到 1、构造 2 页扫描件,验证结果是error/failed而不是"少一页但成功"。另外,候选页排序时,可选短页/空白页不会挤占真正的扫描页预算(scanned.sort(key = lambda number: number not in required),见 ingestion.py)。
5.4 空文档也会失败
"空文档"同样不会被静默接受:一个提取后没有任何文本的文档,不会以"0 个可检索 chunk"的形态进入索引,而是作为失败处理,避免用户日后检索时遇到内容空洞的文件。
5.5 失败后的处置路径
上传失败后的标准处置,文档给出了四条建议:
- 配置正确的 OCR 语言数据(检查
TESSDATA_PREFIX与RAG_OCR_LANGUAGE是否匹配已安装的.traineddata); - 启用 OCR(对 Data Recipes 确认
RAG_OCR_SCANNED未设为0;对 Chat 检查OCR scanned pages开关); - 扫描件页数较多时,在启动 Studio 前调大
RAG_OCR_MAX_PAGES; - 改传一份带文字层的可检索 PDF(searchable PDF),然后重新附加文件。
测试test_failed_scan_replacement_preserves_searchable_original(test_pdf_local_ocr.py)进一步验证:用失败的新文件替换旧文件时,原有的可检索文档会被完整保留,不会被失败的替换拖下水。
六、源码级深入:一条扫描 PDF 的完整 OCR 旅程
把上述机制串起来,一个扫描 PDF 从上传到可检索,在 Chat 场景大致经历如下调用链(以 ingestion.py 的_ocr_scanned_pages()为中心):
- 解析分类:
parsers.parse()逐页生成Page,标记needs_ocr(判定规则见第二节); - 预算裁剪:收集所有候选扫描页,按"必需页优先"排序后截断到
OCR_MAX_PAGES; - 视觉模型优先:若
captioner.vision_endpoint()可用,先把页面渲染成 PNG(render_pdf_pages(dpi=OCR_DPI)),逐页交给视觉 GGUF 转录,进度通过_progress(conn, job_id, "ocr", ...)上报为 0.25 → 0.40 区间(ingestion.py); - Tesseract 回退:对
needs_ocr且视觉模型未转录的页面,调用pdf_ocr.ocr_pages()用本地 Tesseract 补齐(ingestion.py)。这使得只有文本类 GGUF 模型(无视觉能力)的部署也能处理扫描 PDF; - 合并与校验:把 OCR 文本写回对应
Page,仍缺页则整体失败(见第五节)。
6.1 文本合并策略:保留一切可选文字
OCR 结果与原有文字层按"互补不覆盖"的原则合并,逻辑见 pdf_ocr.py 与 ingestion.py:
- 若原页面没有文本,或原文本已完整包含在 OCR 结果中,直接用 OCR 结果;
- 若两者都存在且不同,则用
原文本 + "\n\n" + OCR 文本拼接,保证页眉页脚等可选文字与扫描正文都进入检索。
测试test_scan_with_digital_header_still_gets_ocr(test_pdf_local_ocr.py)验证:带数字页眉的扫描页,页眉文字只出现一次、扫描正文成功转录——既没丢页眉,也没重复。
6.2 Data Recipes 的独立进程提取
文档特别强调:Recipe PDF 提取运行在独立的 worker 进程中,OCR 不会阻塞其他请求。源码在 seed.py 中落实:
# MuPDF is not thread-safe; each worker owns its document and OCR state. raw = await to_process.run_sync( pdf_ocr.extract_text, str(file_path), config.OCR_SCANNED, config.OCR_MAX_PAGES, cancellable = True, limiter = _pdf_extraction_limiter, )这里有两层隔离:一是把pdf_ocr.extract_text()通过run_sync放到独立进程中执行(注释点明 MuPDF 非线程安全,每个 worker 独占自己的文档与 OCR 状态),二是通过CapacityLimiter(2)限制并发 PDF 提取数(_pdf_extraction_limiter,见 seed.py),防止大量扫描件同时转录打满 CPU。这也是"OCR 不阻塞其他请求"的工程实现——长文档的转录被移出主事件循环。
6.3 OCR 引擎自身的防御设计
ocr_pages()内部还有一处细节:当某页 OCR 抛异常(例如语言包缺失)时,会记录 warning 并break 提前退出,而不是对整份文档的每一页都反复尝试一个不可用的引擎(pdf_ocr.py)。测试test_local_ocr_engine_failure_returns_no_text(test_pdf_local_ocr.py)覆盖了这一路径:引擎不可用时返回空 dict,由上层统一触发失败提示。
七、测试验证矩阵:仓库如何保证 OCR 可靠性
仓库为扫描 PDF OCR 维护了两份测试:test_pdf_local_ocr.py(本地 Tesseract 路径)与 test_pdf_ocr_regressions.py(回归防护)。前者覆盖的关键场景如下,可作为自测清单:
| 测试用例 | 验证点 |
|---|---|
test_recipe_scanned_pdf_extracts_local_ocr | Data Recipes 上传纯扫描/混合 PDF,正文成功转录且页码正确 |
test_chat_scanned_pdf_searchable_without_vision | Chat 场景无视觉模型时,Tesseract 兜底让扫描件可检索 |
test_scan_with_digital_header_still_gets_ocr | 带数字页眉的扫描页仍判为需 OCR,页眉不重复 |
test_recipe_missing_ocr_rejects_incomplete_pdf | OCR 不可用时上传报错并列出页码,不留残件 |
test_ocr_page_cap_does_not_silently_drop_pages | 页数上限内失败必须显式报错,而非静默截断 |
test_chat_vision_failure_falls_back_locally | 视觉模型失败时自动落到本地 Tesseract |
test_blank_separator_needs_no_ocr | 空白间隔页不触发 OCR |
test_recipe_respects_disabled_ocr | RAG_OCR_SCANNED=0时 Data Recipes 拒绝扫描件并明确报错 |
test_failed_scan_replacement_preserves_searchable_original | 失败的替换不破坏原有可检索文档 |
八、局限与使用注意事项
OCR 本质是有损识别,文档明确提醒其可靠性边界,以下场景应重点核对转录结果:
- 手写内容:识别率显著低于印刷体,错误率高发;
- 低分辨率扫描:
RAG_OCR_DPI(默认 150)可适当调高,但原始扫描质量决定上限; - 复杂表格:行列结构可能被压平成线性文本,破坏表格语义;
- 多语言混排:需确认
RAG_OCR_LANGUAGE覆盖全部语种,缺少任一语言包都会导致该页转录失败。
因此,使用扫描 PDF 做 RAG 检索或构建 Data Recipe 后,建议把提取结果与原文档对照抽查。若某页反复无法识别,优先考虑替换为带文字层的可检索 PDF(例如通过 OCR 软件另存为带文本层的版本),而不是无限调高 DPI 或页数预算。
九、快速排障清单
把本文内容压缩成一张可执行的检查表,供遇到"扫描 PDF 上传失败"时逐项排查:
- 引擎是否可用:Tesseract 是否已安装?
TESSDATA_PREFIX是否指向包含.traineddata的目录? - 语言是否匹配:
RAG_OCR_LANGUAGE列出的每个语言代码是否都有对应的.traineddata文件? - 开关是否开启:Data Recipes 场景确认
RAG_OCR_SCANNED未被设为0;Chat 场景确认OCR scanned pages开关已开启; - 预算是否足够:扫描页总数是否超过
RAG_OCR_MAX_PAGES(默认 20)?超长扫描件需在启动 Studio 前调大; - 换一份 PDF:仍失败时,上传带文字层的可检索 PDF;
- 查看具体页码:错误信息中的页码列表会精确指出哪几页不可读,据此判断是单页质量差还是全局配置问题。
上述所有配置、默认值与行为均可直接在仓库中核验:环境变量见 config.py,OCR 执行与错误构造见 pdf_ocr.py,页面分类见 parsers.py,Chat 双引擎回退见 ingestion.py,Data Recipes 独立进程提取见 seed.py。
- 人工智能
- 大模型
- 微调
- LoRA
- 模型优化
- 模型量化
- 强化学习
【免费下载链接】unsloth
Local UI to run and train LLMs and diffusion models. Supports GGUF, MLX, Qwen3.8, DeepSeek-V4, MiniMax-H3, Gemma 4, FLUX and more.
相关推荐
bentopdf OCR PDF 工具深度指南:基于 Tesseract 的扫描件文字层识别与多语言配置
bentopdf OCR PDF 工具深度指南:基于 Tesseract 的扫描件文字层识别与多语言配置 本文围绕 bentopdf(Privacy First
前端Ekko Studio OCR 与文档提取技能实战:从扫描 PDF 到结构化 Markdown 的本地优先工作流
Ekko Studio OCR 与文档提取技能实战:从扫描 PDF 到结构化 Markdown 的本地优先工作流 导读 Ekko Studio(本地优先的多 A
AI 应用人工智能AI Agent本地部署前端后端工作流自动化为什么选择Workbench?5大优势让Dotfiles管理更简单高效
为什么选择Workbench?5大优势让Dotfiles管理更简单高效 如果你正为 macOS 上 .zshrc 、 .gitconfig 这类配置文件的备份而
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考