Merge branch 'ft_wyt' of http://www.caoxiaozhu.com:13001/YG-Soft/YG_FT into ft_wyt
This commit is contained in:
@@ -104,7 +104,9 @@ def extract_pdf_page_texts(raw: bytes) -> tuple[PdfPageText, ...]:
|
|||||||
)
|
)
|
||||||
has_text = True
|
has_text = True
|
||||||
if not has_text:
|
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)
|
return tuple(pages)
|
||||||
|
|
||||||
def _pdf_page_lines(page: PdfPageText) -> tuple[_PdfLine, ...]:
|
def _pdf_page_lines(page: PdfPageText) -> tuple[_PdfLine, ...]:
|
||||||
|
|||||||
@@ -90,8 +90,10 @@ def _tokenizer() -> tiktoken.Encoding:
|
|||||||
token, rank = line.split()
|
token, rank = line.split()
|
||||||
mergeable_ranks[base64.b64decode(token)] = int(rank)
|
mergeable_ranks[base64.b64decode(token)] = int(rank)
|
||||||
|
|
||||||
# 构造 Encoding 对象
|
# 构造 Encoding 对象(模块顶部已 import tiktoken,
|
||||||
import tiktoken.core
|
# 此处不能再 import tiktoken.core,否则会把 tiktoken
|
||||||
|
# 变成局部变量,使函数开头的 tiktoken.get_encoding 抛
|
||||||
|
# UnboundLocalError)
|
||||||
return tiktoken.core.Encoding(
|
return tiktoken.core.Encoding(
|
||||||
name="cl100k_base",
|
name="cl100k_base",
|
||||||
pat_str=r"""'(?i:[sdmt]|ll|ve|re)|[^\r\n\p{L}\p{N}]?+\p{L}+|\p{N}{1,3}| ?[^\s\p{L}\p{N}]++[\r\n]*|\s*[\r\n]|\s+(?!\S)|\s+""",
|
pat_str=r"""'(?i:[sdmt]|ll|ve|re)|[^\r\n\p{L}\p{N}]?+\p{L}+|\p{N}{1,3}| ?[^\s\p{L}\p{N}]++[\r\n]*|\s*[\r\n]|\s+(?!\S)|\s+""",
|
||||||
@@ -260,9 +262,17 @@ def _make_text_chunk(source: str, start: int, end: int) -> DocumentChunk:
|
|||||||
|
|
||||||
@lru_cache(maxsize=1)
|
@lru_cache(maxsize=1)
|
||||||
def _document_converter():
|
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):
|
class _MarkdownSerializerProvider(ChunkingSerializerProvider):
|
||||||
|
|||||||
@@ -45,6 +45,25 @@ MINIMAX_M3_API_HOSTS = {"api.minimax.io", "api.minimaxi.com"}
|
|||||||
MINIMAX_M3_MIN_COMPLETION_TOKENS = 4096
|
MINIMAX_M3_MIN_COMPLETION_TOKENS = 4096
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# 问题表述风格规则:防止模型产出“请描述/请说明”式模板化问句。
|
||||||
|
_QUESTION_STYLE_RULE = (
|
||||||
|
"各条问题必须覆盖不同的信息点并使用不同的句式,只替换关键词套用同一句式视为重复。"
|
||||||
|
"问题表述要像真实用户自然提出的问题:具体、口语化、直奔信息点,"
|
||||||
|
"避免“请描述”“请说明”“根据文档”等模板化开头,"
|
||||||
|
"也不要把原文句子直接改成问句;多条问题时交替使用直接疑问、场景式提问、追问式等句式。"
|
||||||
|
"表述示例(仅示意风格,不要照搬内容):"
|
||||||
|
"避免——“请描述系统的权限控制机制”;"
|
||||||
|
"推荐——“不同角色能看到的菜单不一样,平台是怎么控制的?”"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 任务配置未提供提示语时的兜底,与前端内置默认提示语保持同等信息量。
|
||||||
|
_DEFAULT_GENERATION_PROMPT = (
|
||||||
|
"你是一名专业的数据生成专家。请基于来源内容生成高质量、"
|
||||||
|
"可直接用于监督微调的问答数据:问题聚焦核心信息点、"
|
||||||
|
"表述像真实用户自然提出的问题,具体、口语化,多条问题使用不同句式;"
|
||||||
|
"答案严格依据来源内容,准确、完整、语言自然,不引入来源之外的信息。"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _is_retryable_generation_error(exc: Exception) -> bool:
|
def _is_retryable_generation_error(exc: Exception) -> bool:
|
||||||
if isinstance(exc, _TerminalModelGenerationError):
|
if isinstance(exc, _TerminalModelGenerationError):
|
||||||
@@ -310,11 +329,11 @@ def _prompt_messages(
|
|||||||
)
|
)
|
||||||
schema_instruction = (
|
schema_instruction = (
|
||||||
f"必须只返回 JSON 对象,格式为 {schema};items 必须包含 {count} 条。"
|
f"必须只返回 JSON 对象,格式为 {schema};items 必须包含 {count} 条。"
|
||||||
f"这是总计 {total_count} 条中的第 {start_index}-{end_index} 条,"
|
f"这是总计 {total_count} 条中的第 {start_index}-{end_index} 条。"
|
||||||
"各条必须使用不同的提问角度和表述,避免重复。"
|
f"{_QUESTION_STYLE_RULE}{output_rule}"
|
||||||
f"{output_rule}不要输出 Markdown 代码围栏或 JSON 之外的说明。"
|
"不要输出 Markdown 代码围栏或 JSON 之外的说明。"
|
||||||
)
|
)
|
||||||
base_prompt = normalize_text(prompt) or "请根据来源内容生成可用于监督微调的问答数据。"
|
base_prompt = normalize_text(prompt) or _DEFAULT_GENERATION_PROMPT
|
||||||
if "{{ content }}" in base_prompt:
|
if "{{ content }}" in base_prompt:
|
||||||
user_prompt = base_prompt.replace("{{ content }}", content)
|
user_prompt = base_prompt.replace("{{ content }}", content)
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -654,7 +654,10 @@ def test_office_zip_bomb_and_invalid_pdf_are_rejected_before_parsing() -> None:
|
|||||||
blank_writer = PdfWriter()
|
blank_writer = PdfWriter()
|
||||||
blank_writer.add_blank_page(width=612, height=792)
|
blank_writer.add_blank_page(width=612, height=792)
|
||||||
blank_writer.write(blank_pdf)
|
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")
|
parse_text_content(blank_pdf.getvalue(), filename="scanned.pdf")
|
||||||
|
|
||||||
aes_pdf_without_open_password = parse_text_content(
|
aes_pdf_without_open_password = parse_text_content(
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ def test_generate_model_records_uses_prompt_auth_and_stable_split() -> None:
|
|||||||
assert "客户反馈页面加载慢" in payload["messages"][1]["content"]
|
assert "客户反馈页面加载慢" in payload["messages"][1]["content"]
|
||||||
assert "你正在生成标准监督微调问答数据" in payload["messages"][0]["content"]
|
assert "你正在生成标准监督微调问答数据" in payload["messages"][0]["content"]
|
||||||
assert "禁止输出分析、推理过程" in payload["messages"][0]["content"]
|
assert "禁止输出分析、推理过程" in payload["messages"][0]["content"]
|
||||||
|
assert "真实用户自然提出的问题" in payload["messages"][0]["content"]
|
||||||
|
assert "模板化开头" in payload["messages"][0]["content"]
|
||||||
return httpx.Response(
|
return httpx.Response(
|
||||||
200,
|
200,
|
||||||
json={
|
json={
|
||||||
@@ -88,6 +90,59 @@ def test_generate_model_records_uses_prompt_auth_and_stable_split() -> None:
|
|||||||
assert progress_updates == [(1, 1)]
|
assert progress_updates == [(1, 1)]
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_model_records_falls_back_to_rich_default_prompt() -> None:
|
||||||
|
captured: dict[str, str] = {}
|
||||||
|
|
||||||
|
def handler(request: httpx.Request) -> httpx.Response:
|
||||||
|
payload = json.loads(request.content)
|
||||||
|
captured["system"] = payload["messages"][0]["content"]
|
||||||
|
captured["user"] = payload["messages"][1]["content"]
|
||||||
|
return httpx.Response(
|
||||||
|
200,
|
||||||
|
json={
|
||||||
|
"choices": [
|
||||||
|
{
|
||||||
|
"message": {
|
||||||
|
"content": json.dumps(
|
||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"instruction": "平台如何控制不同角色的菜单可见性?",
|
||||||
|
"input": "",
|
||||||
|
"output": "按角色分配权限。",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
ensure_ascii=False,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
client = httpx.Client(transport=httpx.MockTransport(handler))
|
||||||
|
records = generate_model_records(
|
||||||
|
[{"id": "preview-1", "edited_content": "平台按角色分配菜单权限。"}],
|
||||||
|
model={
|
||||||
|
"name": "Qwen",
|
||||||
|
"online_model_name": "qwen-plus",
|
||||||
|
"api_url": "model.example",
|
||||||
|
},
|
||||||
|
config={},
|
||||||
|
task_id="task-fallback",
|
||||||
|
split={"train": 100, "validation": 0, "test": 0},
|
||||||
|
qa_pairs_per_item=1,
|
||||||
|
client=client,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(records) == 1
|
||||||
|
assert records[0]["status"] == "valid"
|
||||||
|
assert "数据生成专家" in captured["system"]
|
||||||
|
assert "真实用户自然提出的问题" in captured["system"]
|
||||||
|
assert "平台按角色分配菜单权限。" in captured["user"]
|
||||||
|
|
||||||
|
|
||||||
def test_generate_model_records_builds_native_dpo_pair() -> None:
|
def test_generate_model_records_builds_native_dpo_pair() -> None:
|
||||||
def handler(request: httpx.Request) -> httpx.Response:
|
def handler(request: httpx.Request) -> httpx.Response:
|
||||||
payload = json.loads(request.content)
|
payload = json.loads(request.content)
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from types import ModuleType, SimpleNamespace
|
||||||
|
|
||||||
from llama_index.core.embeddings import MockEmbedding
|
from llama_index.core.embeddings import MockEmbedding
|
||||||
|
|
||||||
from app.modules.data_process.document_chunking import (
|
from app.modules.data_process.document_chunking import (
|
||||||
DocumentChunk,
|
DocumentChunk,
|
||||||
_compact_with_offsets,
|
_compact_with_offsets,
|
||||||
|
_document_converter,
|
||||||
_project_layout_span,
|
_project_layout_span,
|
||||||
chunk_fixed_text,
|
chunk_fixed_text,
|
||||||
chunk_semantic_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
|
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:
|
def test_layout_projection_ignores_layout_whitespace_but_keeps_source_lines() -> None:
|
||||||
source = "标题\n第一条 这是正文。\n第二条 后续正文。"
|
source = "标题\n第一条 这是正文。\n第二条 后续正文。"
|
||||||
compact_source, offsets = _compact_with_offsets(source)
|
compact_source, offsets = _compact_with_offsets(source)
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ pending ──start/generate──> running ──success──> completed
|
|||||||
抽取幻灯片文本与表格,随后统一进入切片算法。
|
抽取幻灯片文本与表格,随后统一进入切片算法。
|
||||||
- 旧版二进制 DOC、XLS、PPT 不直接解析,返回 415 并提示分别转换为
|
- 旧版二进制 DOC、XLS、PPT 不直接解析,返回 415 并提示分别转换为
|
||||||
DOCX、XLSX、PPTX。
|
DOCX、XLSX、PPTX。
|
||||||
- 扫描 PDF 没有文本层时明确提示需要 OCR;当前流程不执行 OCR。加密、损坏或
|
- 扫描 PDF 没有文本层时明确提示扫描版或图片型 PDF 不支持。加密、损坏或
|
||||||
超出页数/工作表/行列/解压规模限制的文件整批拒绝。
|
超出页数/工作表/行列/解压规模限制的文件整批拒绝。
|
||||||
|
|
||||||
现代 Office 文件在交给解析库前检查 ZIP 成员路径、重复成员、加密标记、活动
|
现代 Office 文件在交给解析库前检查 ZIP 成员路径、重复成员、加密标记、活动
|
||||||
|
|||||||
204
docs/superpowers/plans/2026-08-18-disable-ocr.md
Normal file
204
docs/superpowers/plans/2026-08-18-disable-ocr.md
Normal file
@@ -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/`.
|
||||||
69
docs/superpowers/specs/2026-08-18-disable-ocr-design.md
Normal file
69
docs/superpowers/specs/2026-08-18-disable-ocr-design.md
Normal file
@@ -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 的接收策略。
|
||||||
@@ -72,6 +72,12 @@ onUnmounted(() => {
|
|||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 表格页画布需允许收缩,否则内容自然高度超出视口时分页条会被裁掉且无滚动条可及 */
|
||||||
|
&:has(.data-table-page) .page-canvas {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
&:has(.create-wizard-layout) {
|
&:has(.create-wizard-layout) {
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export const PREVIOUS_DEFAULT_REASONING_GENERATION_PROMPT_3 = `你是一名具
|
|||||||
数据格式要求:
|
数据格式要求:
|
||||||
请确保每条生成的数据清晰包含“问题”、“中间思考过程”和“最终答案”三个部分,且内容充实、专业。`
|
请确保每条生成的数据清晰包含“问题”、“中间思考过程”和“最终答案”三个部分,且内容充实、专业。`
|
||||||
|
|
||||||
export const DEFAULT_STANDARD_GENERATION_PROMPT = `你是一名专业的数据生成专家。请基于下方来源内容,生成高质量、可直接用于大模型监督微调(SFT)的问答数据。
|
export const PREVIOUS_DEFAULT_STANDARD_GENERATION_PROMPT_4 = `你是一名专业的数据生成专家。请基于下方来源内容,生成高质量、可直接用于大模型监督微调(SFT)的问答数据。
|
||||||
|
|
||||||
来源内容:
|
来源内容:
|
||||||
{{ content }}
|
{{ content }}
|
||||||
@@ -62,7 +62,7 @@ export const DEFAULT_STANDARD_GENERATION_PROMPT = `你是一名专业的数据
|
|||||||
2. 答案忠于来源:严格依据来源内容作答,不得引入来源中不存在的事实、观点或数据;若来源信息不足以完整回答该问题,请如实说明不足,不要编造或臆测。
|
2. 答案忠于来源:严格依据来源内容作答,不得引入来源中不存在的事实、观点或数据;若来源信息不足以完整回答该问题,请如实说明不足,不要编造或臆测。
|
||||||
3. 答案完整且自然:回答应直接回应问题,要点完整、逻辑清晰、语言流畅;涉及多项内容时建议分点陈述,但不必照搬原文措辞。`
|
3. 答案完整且自然:回答应直接回应问题,要点完整、逻辑清晰、语言流畅;涉及多项内容时建议分点陈述,但不必照搬原文措辞。`
|
||||||
|
|
||||||
export const DEFAULT_REASONING_GENERATION_PROMPT = `你是一名具有深厚专业知识的推理数据生成专家。请基于下方来源内容,生成包含完整思维链、可用于训练推理大语言模型(如 O1、DeepSeek-R1)的问答数据。
|
export const PREVIOUS_DEFAULT_REASONING_GENERATION_PROMPT_4 = `你是一名具有深厚专业知识的推理数据生成专家。请基于下方来源内容,生成包含完整思维链、可用于训练推理大语言模型(如 O1、DeepSeek-R1)的问答数据。
|
||||||
|
|
||||||
来源内容:
|
来源内容:
|
||||||
{{ content }}
|
{{ content }}
|
||||||
@@ -73,7 +73,7 @@ export const DEFAULT_REASONING_GENERATION_PROMPT = `你是一名具有深厚专
|
|||||||
3. 信息不足时如实处理:遇到来源信息缺失或相互矛盾时,需在推理中明确指出,并基于来源做出合理假设,但不得编造与来源无关的内容。
|
3. 信息不足时如实处理:遇到来源信息缺失或相互矛盾时,需在推理中明确指出,并基于来源做出合理假设,但不得编造与来源无关的内容。
|
||||||
4. 最终答案须能从前面的推理过程中自然得出,语言简洁、结构清晰。`
|
4. 最终答案须能从前面的推理过程中自然得出,语言简洁、结构清晰。`
|
||||||
|
|
||||||
export const DEFAULT_DPO_GENERATION_PROMPT = `你是一名偏好数据构造专家。请基于下方来源内容,生成可用于直接偏好优化(DPO)的成对问答数据。
|
export const PREVIOUS_DEFAULT_DPO_GENERATION_PROMPT = `你是一名偏好数据构造专家。请基于下方来源内容,生成可用于直接偏好优化(DPO)的成对问答数据。
|
||||||
|
|
||||||
来源内容:
|
来源内容:
|
||||||
{{ content }}
|
{{ content }}
|
||||||
@@ -84,6 +84,41 @@ export const DEFAULT_DPO_GENERATION_PROMPT = `你是一名偏好数据构造专
|
|||||||
3. Rejected 应当表面合理但包含可辨认的缺陷,例如遗漏关键条件、事实偏差、逻辑不完整或表达含混;不得用乱码、空话或危险内容凑数。
|
3. Rejected 应当表面合理但包含可辨认的缺陷,例如遗漏关键条件、事实偏差、逻辑不完整或表达含混;不得用乱码、空话或危险内容凑数。
|
||||||
4. Chosen 与 Rejected 必须明显不同,且都直接回答同一个问题。`
|
4. Chosen 与 Rejected 必须明显不同,且都直接回答同一个问题。`
|
||||||
|
|
||||||
|
export const DEFAULT_STANDARD_GENERATION_PROMPT = `你是一名专业的数据生成专家。请基于下方来源内容,生成高质量、可直接用于大模型监督微调(SFT)的问答数据。
|
||||||
|
|
||||||
|
来源内容:
|
||||||
|
{{ content }}
|
||||||
|
|
||||||
|
任务要求:
|
||||||
|
1. 提问有价值:问题应聚焦来源内容的核心信息点,避免无意义的字面抄录或过于宽泛的提问;优先设计需要理解、归纳或推理才能回答的问题,例如主旨概括、细节追问、因果解释或场景应用。
|
||||||
|
2. 问题表述自然:问题要像真实用户自然提出的问题——具体、口语化、直奔信息点;避免“请描述”“请说明”“根据文档”等模板化开头,不要把原文句子直接改成问句;多条问题应交替使用直接疑问、场景式提问、追问式等不同句式,不要只换关键词套用同一句式。
|
||||||
|
3. 答案忠于来源:严格依据来源内容作答,不得引入来源中不存在的事实、观点或数据;若来源信息不足以完整回答该问题,请如实说明不足,不要编造或臆测。
|
||||||
|
4. 答案完整且自然:回答应直接回应问题,要点完整、逻辑清晰、语言流畅;涉及多项内容时建议分点陈述,但不必照搬原文措辞。`
|
||||||
|
|
||||||
|
export const DEFAULT_REASONING_GENERATION_PROMPT = `你是一名具有深厚专业知识的推理数据生成专家。请基于下方来源内容,生成包含完整思维链、可用于训练推理大语言模型(如 O1、DeepSeek-R1)的问答数据。
|
||||||
|
|
||||||
|
来源内容:
|
||||||
|
{{ content }}
|
||||||
|
|
||||||
|
任务要求:
|
||||||
|
1. 提问有挑战性:从来源内容中提炼需要多步思考才能回答的问题,避免可直接定位答案的简单事实抽取;可涉及因果分析、逻辑推断或跨信息整合。
|
||||||
|
2. 问题表述自然:问题要像真实用户自然提出的问题——具体、口语化;避免“请分析”“请说明”等模板化开头连用;多条问题应交替使用不同句式,不要只换关键词套用同一句式。
|
||||||
|
3. 推理可核验:逐步展开思考过程,每个关键结论都应标注其在来源中的依据或必要的中间推导;不得跳过关键步骤只给结论,也不得引入来源中完全不存在的事实。
|
||||||
|
4. 信息不足时如实处理:遇到来源信息缺失或相互矛盾时,需在推理中明确指出,并基于来源做出合理假设,但不得编造与来源无关的内容。
|
||||||
|
5. 最终答案须能从前面的推理过程中自然得出,语言简洁、结构清晰。`
|
||||||
|
|
||||||
|
export const DEFAULT_DPO_GENERATION_PROMPT = `你是一名偏好数据构造专家。请基于下方来源内容,生成可用于直接偏好优化(DPO)的成对问答数据。
|
||||||
|
|
||||||
|
来源内容:
|
||||||
|
{{ content }}
|
||||||
|
|
||||||
|
任务要求:
|
||||||
|
1. 问题应聚焦来源内容的核心信息,并具有实际训练价值。
|
||||||
|
2. 问题表述自然:问题要像真实用户提出的问题——具体、口语化,避免“请描述”“请说明”等模板化开头。
|
||||||
|
3. Chosen 必须准确、完整、清晰且严格忠于来源内容。
|
||||||
|
4. Rejected 应当表面合理但包含可辨认的缺陷,例如遗漏关键条件、事实偏差、逻辑不完整或表达含混;不得用乱码、空话或危险内容凑数。
|
||||||
|
5. Chosen 与 Rejected 必须明显不同,且都直接回答同一个问题。`
|
||||||
|
|
||||||
export function defaultGenerationPrompt(outputType: 'standard' | 'reasoning' | 'dpo') {
|
export function defaultGenerationPrompt(outputType: 'standard' | 'reasoning' | 'dpo') {
|
||||||
if (outputType === 'reasoning') return DEFAULT_REASONING_GENERATION_PROMPT
|
if (outputType === 'reasoning') return DEFAULT_REASONING_GENERATION_PROMPT
|
||||||
if (outputType === 'dpo') return DEFAULT_DPO_GENERATION_PROMPT
|
if (outputType === 'dpo') return DEFAULT_DPO_GENERATION_PROMPT
|
||||||
@@ -98,9 +133,12 @@ export function isBuiltInGenerationPrompt(value: string) {
|
|||||||
PREVIOUS_DEFAULT_STANDARD_GENERATION_PROMPT,
|
PREVIOUS_DEFAULT_STANDARD_GENERATION_PROMPT,
|
||||||
PREVIOUS_DEFAULT_STANDARD_GENERATION_PROMPT_2,
|
PREVIOUS_DEFAULT_STANDARD_GENERATION_PROMPT_2,
|
||||||
PREVIOUS_DEFAULT_STANDARD_GENERATION_PROMPT_3,
|
PREVIOUS_DEFAULT_STANDARD_GENERATION_PROMPT_3,
|
||||||
|
PREVIOUS_DEFAULT_STANDARD_GENERATION_PROMPT_4,
|
||||||
PREVIOUS_DEFAULT_REASONING_GENERATION_PROMPT,
|
PREVIOUS_DEFAULT_REASONING_GENERATION_PROMPT,
|
||||||
PREVIOUS_DEFAULT_REASONING_GENERATION_PROMPT_2,
|
PREVIOUS_DEFAULT_REASONING_GENERATION_PROMPT_2,
|
||||||
PREVIOUS_DEFAULT_REASONING_GENERATION_PROMPT_3,
|
PREVIOUS_DEFAULT_REASONING_GENERATION_PROMPT_3,
|
||||||
|
PREVIOUS_DEFAULT_REASONING_GENERATION_PROMPT_4,
|
||||||
|
PREVIOUS_DEFAULT_DPO_GENERATION_PROMPT,
|
||||||
DEFAULT_STANDARD_GENERATION_PROMPT,
|
DEFAULT_STANDARD_GENERATION_PROMPT,
|
||||||
DEFAULT_REASONING_GENERATION_PROMPT,
|
DEFAULT_REASONING_GENERATION_PROMPT,
|
||||||
DEFAULT_DPO_GENERATION_PROMPT,
|
DEFAULT_DPO_GENERATION_PROMPT,
|
||||||
|
|||||||
Reference in New Issue
Block a user