From 91b4ae2287a084f778ad248cdcea95b9b0229aeb Mon Sep 17 00:00:00 2001 From: caoxiaozhu Date: Tue, 18 Aug 2026 15:48:30 +0800 Subject: [PATCH] =?UTF-8?q?feat(data=5Fprocess):=20=E6=98=BE=E5=BC=8F?= =?UTF-8?q?=E5=85=B3=E9=97=AD=20docling=20OCR=20=E5=B9=B6=E6=8B=92?= =?UTF-8?q?=E7=BB=9D=E6=97=A0=E6=96=87=E6=9C=AC=E5=B1=82=20PDF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docling 转换器通过 PdfPipelineOptions 显式设置 do_ocr=False, 混合 PDF 的图片页不再产出 OCR 文字;不提供重新开启 OCR 的参数。 - 无文本层 PDF 的错误文案改为"扫描版或图片型 PDF 不支持", 上传阶段整批拒绝,保留混合 PDF 的可处理判定。 - 新增 test_layout_converter_disables_ocr 守护开关状态, 同步设计文档与 disable-ocr 实施计划/设计说明。 --- .../data_process/algorithms/parsers/pdf.py | 4 +- .../modules/data_process/document_chunking.py | 12 +- backend/tests/test_data_process_algorithms.py | 5 +- backend/tests/test_document_chunking.py | 46 ++++ docs/data-process-design.md | 2 +- .../plans/2026-08-18-disable-ocr.md | 204 ++++++++++++++++++ .../specs/2026-08-18-disable-ocr-design.md | 69 ++++++ 7 files changed, 337 insertions(+), 5 deletions(-) create mode 100644 docs/superpowers/plans/2026-08-18-disable-ocr.md create mode 100644 docs/superpowers/specs/2026-08-18-disable-ocr-design.md diff --git a/backend/app/modules/data_process/algorithms/parsers/pdf.py b/backend/app/modules/data_process/algorithms/parsers/pdf.py index 2c20588..5c31bb8 100644 --- a/backend/app/modules/data_process/algorithms/parsers/pdf.py +++ b/backend/app/modules/data_process/algorithms/parsers/pdf.py @@ -104,7 +104,9 @@ def extract_pdf_page_texts(raw: bytes) -> tuple[PdfPageText, ...]: ) has_text = True if not has_text: - raise ValueError("PDF contains no extractable text; scanned PDF requires OCR") + raise ValueError( + "PDF contains no extractable text; scanned or image-only PDF files are not supported" + ) return tuple(pages) def _pdf_page_lines(page: PdfPageText) -> tuple[_PdfLine, ...]: diff --git a/backend/app/modules/data_process/document_chunking.py b/backend/app/modules/data_process/document_chunking.py index 2ad97ed..830c70a 100644 --- a/backend/app/modules/data_process/document_chunking.py +++ b/backend/app/modules/data_process/document_chunking.py @@ -262,9 +262,17 @@ def _make_text_chunk(source: str, start: int, end: int) -> DocumentChunk: @lru_cache(maxsize=1) def _document_converter(): - from docling.document_converter import DocumentConverter + from docling.datamodel.base_models import InputFormat + from docling.datamodel.pipeline_options import PdfPipelineOptions + from docling.document_converter import DocumentConverter, PdfFormatOption - return DocumentConverter() + pipeline_options = PdfPipelineOptions() + pipeline_options.do_ocr = False + return DocumentConverter( + format_options={ + InputFormat.PDF: PdfFormatOption(pipeline_options=pipeline_options), + } + ) class _MarkdownSerializerProvider(ChunkingSerializerProvider): diff --git a/backend/tests/test_data_process_algorithms.py b/backend/tests/test_data_process_algorithms.py index 5647d35..cda83f5 100644 --- a/backend/tests/test_data_process_algorithms.py +++ b/backend/tests/test_data_process_algorithms.py @@ -654,7 +654,10 @@ def test_office_zip_bomb_and_invalid_pdf_are_rejected_before_parsing() -> None: blank_writer = PdfWriter() blank_writer.add_blank_page(width=612, height=792) blank_writer.write(blank_pdf) - with pytest.raises(ValueError, match="scanned PDF requires OCR"): + with pytest.raises( + ValueError, + match="scanned or image-only PDF files are not supported", + ): parse_text_content(blank_pdf.getvalue(), filename="scanned.pdf") aes_pdf_without_open_password = parse_text_content( diff --git a/backend/tests/test_document_chunking.py b/backend/tests/test_document_chunking.py index cf02442..f705104 100644 --- a/backend/tests/test_document_chunking.py +++ b/backend/tests/test_document_chunking.py @@ -1,10 +1,14 @@ from __future__ import annotations +import sys +from types import ModuleType, SimpleNamespace + from llama_index.core.embeddings import MockEmbedding from app.modules.data_process.document_chunking import ( DocumentChunk, _compact_with_offsets, + _document_converter, _project_layout_span, chunk_fixed_text, chunk_semantic_text, @@ -42,6 +46,48 @@ def test_semantic_splitter_uses_llamaindex_and_reapplies_maximum_size() -> None: assert "".join(chunk.original_content for chunk in chunks) == text +def test_layout_converter_disables_ocr(monkeypatch) -> None: + class FakePipelineOptions: + def __init__(self) -> None: + self.do_ocr = True + + class FakePdfFormatOption: + def __init__(self, *, pipeline_options) -> None: + self.pipeline_options = pipeline_options + + class FakeDocumentConverter: + def __init__(self, *, format_options) -> None: + self.format_options = format_options + + docling_module = ModuleType("docling") + docling_module.__path__ = [] + document_converter_module = ModuleType("docling.document_converter") + document_converter_module.DocumentConverter = FakeDocumentConverter + document_converter_module.PdfFormatOption = FakePdfFormatOption + datamodel_module = ModuleType("docling.datamodel") + datamodel_module.__path__ = [] + base_models_module = ModuleType("docling.datamodel.base_models") + base_models_module.InputFormat = SimpleNamespace(PDF="pdf") + pipeline_options_module = ModuleType("docling.datamodel.pipeline_options") + pipeline_options_module.PdfPipelineOptions = FakePipelineOptions + for name, module in { + "docling": docling_module, + "docling.document_converter": document_converter_module, + "docling.datamodel": datamodel_module, + "docling.datamodel.base_models": base_models_module, + "docling.datamodel.pipeline_options": pipeline_options_module, + }.items(): + monkeypatch.setitem(sys.modules, name, module) + + _document_converter.cache_clear() + try: + converter = _document_converter() + options = converter.format_options["pdf"].pipeline_options + assert options.do_ocr is False + finally: + _document_converter.cache_clear() + + def test_layout_projection_ignores_layout_whitespace_but_keeps_source_lines() -> None: source = "标题\n第一条 这是正文。\n第二条 后续正文。" compact_source, offsets = _compact_with_offsets(source) diff --git a/docs/data-process-design.md b/docs/data-process-design.md index 7b9cb48..337a1ef 100644 --- a/docs/data-process-design.md +++ b/docs/data-process-design.md @@ -118,7 +118,7 @@ pending ──start/generate──> running ──success──> completed 抽取幻灯片文本与表格,随后统一进入切片算法。 - 旧版二进制 DOC、XLS、PPT 不直接解析,返回 415 并提示分别转换为 DOCX、XLSX、PPTX。 -- 扫描 PDF 没有文本层时明确提示需要 OCR;当前流程不执行 OCR。加密、损坏或 +- 扫描 PDF 没有文本层时明确提示扫描版或图片型 PDF 不支持。加密、损坏或 超出页数/工作表/行列/解压规模限制的文件整批拒绝。 现代 Office 文件在交给解析库前检查 ZIP 成员路径、重复成员、加密标记、活动 diff --git a/docs/superpowers/plans/2026-08-18-disable-ocr.md b/docs/superpowers/plans/2026-08-18-disable-ocr.md new file mode 100644 index 0000000..9f4c907 --- /dev/null +++ b/docs/superpowers/plans/2026-08-18-disable-ocr.md @@ -0,0 +1,204 @@ +# 屏蔽数据处理 OCR 能力实施计划 + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 在保留 Docling 版面分析和 `layout_hybrid` 的前提下,显式关闭 PDF pipeline 的 OCR,并拒绝无文本层 PDF。 + +**Architecture:** 只修改 `document_chunking.py` 中的 Docling converter 构造,使 PDF 使用 `PdfPipelineOptions(do_ocr=False)`;不修改切分方式、依赖、前端配置或版面 chunker。pypdf 继续负责上传阶段文本提取和扫描 PDF 边界判断。 + +**Tech Stack:** Python 3.12、FastAPI、pypdf、Docling 2.115.0、LlamaIndex、pytest、Ruff、Vue 3、Node 回归脚本。 + +## Global Constraints + +- 必须保留 `layout_hybrid`、Docling 版面分析和现有前端切分选项。 +- 只能关闭 OCR,不删除 Docling、版面/表格分析、PDF 页码映射或普通切分。 +- 纯扫描/图片型 PDF 明确拒绝;混合 PDF 保留,图片页不产生文字。 +- 不新增 OCR 开关,不提供重新开启 OCR 的参数或环境变量。 +- 不修改 `.codex-tmp/` 和 `outputs/` 等既有未跟踪用户文件。 +- 不执行 `git commit`;提交前必须取得用户明确确认。 + +--- + +### Task 1: 显式关闭 Docling PDF OCR + +**Files:** +- Modify: `backend/app/modules/data_process/document_chunking.py:261-265` +- Test: `backend/tests/test_document_chunking.py` + +**Interfaces:** +- `_document_converter()` 保持无参数、缓存和返回 `DocumentConverter` 的现有接口。 +- PDF 的 `PdfFormatOption` 必须收到 `pipeline_options.do_ocr is False`。 + +- [ ] **Step 1: Add a failing configuration test** + +在 `backend/tests/test_document_chunking.py` 导入 `_document_converter`、`sys`、`ModuleType` 和 `SimpleNamespace`,添加使用假的 Docling 模块的测试: + +```python +def test_layout_converter_disables_ocr(monkeypatch) -> None: + class FakePipelineOptions: + def __init__(self) -> None: + self.do_ocr = True + + class FakePdfFormatOption: + def __init__(self, *, pipeline_options) -> None: + self.pipeline_options = pipeline_options + + class FakeDocumentConverter: + def __init__(self, *, format_options) -> None: + self.format_options = format_options + + # 将这些 fake 类注册到 docling.document_converter、 + # docling.datamodel.base_models 和 docling.datamodel.pipeline_options。 + # 调用 _document_converter 后断言: + _document_converter.cache_clear() + converter = _document_converter() + assert converter.format_options["pdf"].pipeline_options.do_ocr is False + _document_converter.cache_clear() +``` + +测试保留现有版面投影和 provenance 测试,证明本任务不是删除版面分析。 + +- [ ] **Step 2: Run the new test before implementation** + +Run: + +```bash +cd backend && PYTHONPATH=. .venv/bin/pytest tests/test_document_chunking.py::test_layout_converter_disables_ocr -q +``` + +Expected: FAIL,因为当前 `_document_converter()` 没有 PDF pipeline options,fake converter 不会收到 `format_options`。 + +- [ ] **Step 3: Implement the explicit PDF options** + +将 `_document_converter()` 改为: + +```python +@lru_cache(maxsize=1) +def _document_converter(): + from docling.datamodel.base_models import InputFormat + from docling.datamodel.pipeline_options import PdfPipelineOptions + from docling.document_converter import DocumentConverter, PdfFormatOption + + pipeline_options = PdfPipelineOptions() + pipeline_options.do_ocr = False + return DocumentConverter( + format_options={ + InputFormat.PDF: PdfFormatOption(pipeline_options=pipeline_options), + } + ) +``` + +不要删除 `_MarkdownSerializerProvider`、`chunk_layout_document`、`HybridChunker`、来源页码或 bbox 代码。 + +- [ ] **Step 4: Run OCR and layout regression tests** + +Run: + +```bash +cd backend && PYTHONPATH=. .venv/bin/pytest tests/test_document_chunking.py::test_layout_converter_disables_ocr tests/test_document_chunking.py::test_layout_projection_ignores_layout_whitespace_but_keeps_source_lines -q +``` + +Expected: PASS;OCR 配置关闭,版面文本投影仍通过。 + +--- + +### Task 2: 更新无文本 PDF 的错误和文档 + +**Files:** +- Modify: `backend/app/modules/data_process/algorithms/parsers/pdf.py:106-108` +- Modify: `backend/tests/test_data_process_algorithms.py:650-658` +- Modify: `docs/data-process-design.md:121-122` + +**Interfaces:** +- `extract_pdf_page_texts()` 的判定逻辑不变,只更新无文本 PDF 的错误文案。 +- 混合 PDF 仍由 `has_text` 判定为可处理。 + +- [ ] **Step 1: Update the expected error** + +将测试断言改为: + +```python +with pytest.raises( + ValueError, + match="scanned or image-only PDF files are not supported", +): + parse_text_content(blank_pdf.getvalue(), filename="scanned.pdf") +``` + +- [ ] **Step 2: Change only the error message** + +将解析器分支改为: + +```python +if not has_text: + raise ValueError( + "PDF contains no extractable text; scanned or image-only PDF files are not supported" + ) +``` + +不要修改 `has_text` 逻辑,以保留混合 PDF。 + +- [ ] **Step 3: Update the design documentation** + +将 `docs/data-process-design.md` 中“扫描 PDF 没有文本层时明确提示需要 OCR;当前流程不执行 OCR”改为“扫描版或图片型 PDF 不支持”,保留其余格式限制说明。 + +- [ ] **Step 4: Run PDF boundary tests** + +Run: + +```bash +cd backend && PYTHONPATH=. .venv/bin/pytest tests/test_data_process_algorithms.py::test_office_zip_bomb_and_invalid_pdf_are_rejected_before_parsing -q +``` + +Expected: PASS;纯扫描 PDF 明确拒绝。 + +--- + +### Task 3: 运行受影响验证并检查范围 + +**Files:** +- Test: `backend/tests/test_document_chunking.py` +- Test: `backend/tests/test_data_process_algorithms.py` +- Test: `frontend/scripts/regression-data-process-wizard.mjs` + +- [ ] **Step 1: Run targeted backend tests and Ruff** + +Run: + +```bash +cd backend && PYTHONPATH=. .venv/bin/pytest tests/test_document_chunking.py::test_layout_converter_disables_ocr tests/test_document_chunking.py::test_layout_projection_ignores_layout_whitespace_but_keeps_source_lines tests/test_data_process_algorithms.py::test_office_zip_bomb_and_invalid_pdf_are_rejected_before_parsing -q +.venv/bin/ruff check app/modules/data_process/document_chunking.py tests/test_document_chunking.py app/modules/data_process/algorithms/parsers/pdf.py +``` + +Expected: targeted tests pass. Any pre-existing Ruff findings outside the changed OCR configuration must be reported separately, not fixed as unrelated refactoring. + +- [ ] **Step 2: Run the frontend regression and type check** + +Run: + +```bash +cd frontend && node scripts/regression-data-process-wizard.mjs +npm run type-check +``` + +Expected: existing `layout_hybrid`/semantic/fixed assertions remain unchanged; any unrelated pre-existing regression failure is reported separately. + +- [ ] **Step 3: Check OCR references and final diff** + +Run from the repository root: + +```bash +rg -n -i --hidden \ + --glob '!.git/**' \ + --glob '!node_modules/**' \ + --glob '!dist/**' \ + --glob '!outputs/**' \ + --glob '!.codex-tmp/**' \ + '(tesseract|paddleocr|easyocr|ocrmypdf|pytesseract)' \ + backend frontend docs/data-process-design.md + +git diff --check +git status --short +``` + +Expected: no OCR engine dependency or executable OCR integration; the only intended OCR-related runtime setting is `do_ocr = False`, and the diff does not touch layout options, frontend method choices, Docling dependencies, `.codex-tmp/` or `outputs/`. diff --git a/docs/superpowers/specs/2026-08-18-disable-ocr-design.md b/docs/superpowers/specs/2026-08-18-disable-ocr-design.md new file mode 100644 index 0000000..98384b3 --- /dev/null +++ b/docs/superpowers/specs/2026-08-18-disable-ocr-design.md @@ -0,0 +1,69 @@ +# 屏蔽数据处理中的 OCR 能力设计 + +日期:2026-08-18 + +## 1. 目标 + +屏蔽数据处理运行时的 OCR 能力,同时保留 Docling、`layout_hybrid` 版面分析、文本型 PDF 解析和现有前端切分选项。版面分析继续使用 Docling 的结构识别模型,但 PDF pipeline 必须显式设置 `do_ocr=False`,不再从图片或扫描页面识别文字。 + +## 2. 行为边界 + +- `layout_hybrid`、`semantic`、`fixed` 三种切分方式保持不变。 +- `layout_hybrid` 继续通过 Docling 识别文本层 PDF 的版面、阅读顺序、表格和列表结构。 +- OCR 是独立阶段,只在 Docling PDF pipeline 中关闭;不删除 Docling 版面/表格分析模型。 +- 整份 PDF 没有可提取文本时继续拒绝,并提示扫描版或图片型 PDF 不支持。 +- 混合 PDF 继续接收;有文本页正常处理,无文本图片页不产生文字,也不触发 OCR。 +- TXT、Markdown、结构化文件、DOCX、XLSX、PPTX 和文本型 PDF 的其他流程不变。 +- 不新增 OCR 配置项,不提供任何重新开启 OCR 的请求参数或环境开关。 + +## 3. 数据流 + +上传阶段仍由 `pypdf` 提取 PDF 文本。整份 PDF 没有文本层时在上传阶段失败;混合 PDF 保留空文本页。 + +预览阶段的 `layout_hybrid` 仍读取原始文件并调用 `DocumentConverter`,但转换器使用 PDF 专用配置: + +```python +pipeline_options = PdfPipelineOptions() +pipeline_options.do_ocr = False +DocumentConverter( + format_options={ + InputFormat.PDF: PdfFormatOption(pipeline_options=pipeline_options), + } +) +``` + +这样 Docling 仍可对已有文本层执行版面分析,图片内容不会被 OCR 成文字。`semantic` 继续使用 LlamaIndex/HuggingFace embedding 模型;该模型与 OCR 无关。 + +## 4. 代码改动 + +### 4.1 PDF 版面转换 + +修改 `backend/app/modules/data_process/document_chunking.py` 的 `_document_converter()`: + +- 导入 `InputFormat`、`PdfPipelineOptions` 和 `PdfFormatOption`; +- 创建 `PdfPipelineOptions` 后将 `do_ocr` 固定为 `False`; +- 只为 `InputFormat.PDF` 注册该配置; +- 保留现有 `HybridChunker`、版面序列化、来源页码和 bbox 映射。 + +### 4.2 扫描 PDF 错误与文档 + +将无文本 PDF 的错误从“需要 OCR”改为“扫描版或图片型 PDF 不支持”,避免向用户暗示系统可以提供 OCR。同步更新数据处理设计文档;不删除测试中与 DPO 字段验证无关的历史文本样例。 + +### 4.3 依赖与界面 + +不删除 `docling`、`docling_core`、`layout_hybrid`、前端版面切分选项或相关配置。仅增加 OCR 关闭回归测试,不修改切分类型、默认值和前端交互。 + +## 5. 测试设计 + +- 使用假的 Docling 模块测试 `_document_converter()` 传入的 PDF pipeline options 最终为 `do_ocr=False`,不要求测试环境安装 Docling 才能验证配置。 +- 保留并运行版面投影、来源页码、短块合并和已有文本 PDF 测试,确认版面分析代码未被删除。 +- 更新纯扫描 PDF 错误断言。 +- 运行受影响的后端 pytest、Ruff 和前端数据处理回归;前端切分方式回归应继续要求 `layout_hybrid`、`semantic`、`fixed` 三种配置。 +- 静态检查确认没有新增 OCR 开关、OCR 调用或 OCR 引擎依赖;`do_ocr=False` 是唯一运行时设置。 + +## 6. 非目标 + +- 不移除版面结构混合切分。 +- 不移除 Docling 或其版面/表格分析模型。 +- 不把 `layout_hybrid` 改为语义或固定切分。 +- 不删除 PDF 支持,不改变混合 PDF 的接收策略。