feat(review): PDF/图片离线源适配(C1)—— 无需 CAD 的整条审查链路

支持直接审查 PDF 与位图图纸,全链路离线(不连 CAD、不连 8080 端口)。
渲染在内存内完成,复用既有墨迹热力图 + 自适应瓦片规划。

- 新增 dwg_review/sources.py
  - detect_source / plan_file / collect 统一入口
  - PdfReader:pypdfium2 惰性导入,页序即图框序,处理 /Rotate 与 crop 边距
  - ImageReader:世界单位 = 源像素,snap_box 整数对齐保证窗口与像素严格一致
  - ink_grid:纯 PIL 灰度阈值 → 占用网格(不引入 numpy)
- ReviewContext.create(connect_cad=False):离线时 client/tools 均为 None
- 全链路 ctx.tools is None 守卫:review 只读子循环 / inspect_region / annotator /
  checks 的窗口类检查 / 绘图区最大化
- checklist.build_system_prompt(allow_tools=False, offline=True):离线提示词
- __main__:位置参数 source,按扩展名自动判定源类型并决定是否连 CAD
- config 新增 8 个常量(PDF_MAX_PAGES / 文本最小字数 / 各源缩放上限 / 墨迹阈值等)
- tiling.content_predicate 与 heatmap.cell_box 提升为公开函数

验证:
- pytest 457 passed
- verify_pdf_source.py:PDF 23/23、图片 9/9(不需 CAD),含实测瓦片字高像素与
  旋转映射自检(文本区墨迹 0.311 vs 忽略 /Rotate 反向对照 0.145)
- 真实 LLM 端到端:20 页 HVAC 图纸前 3 页跑通,结论具体可核验且未编造 handle

已知局限:页边线在墨迹网格里会把整页连成单簇,瓦片常退化为整页象限(CAD 侧
靠 drop_oversized 丢图框块,离线侧暂无等价手段),留待后续打磨。
This commit is contained in:
gjm committed 2026-10-16 23:07:23 +08:00
1 parent d4a9442e30
commit 6143bb9ab2
32 files changed
+2617 -77

No files matched your search

+1 -1
View File
@@ -12,5 +12,5 @@
- [用户协作偏好](feedback_preferences.md) — 调试用 Debug/R230、模型 deepseek-flash、DLL 集中在 cad_mcp_plugins 等
- [环境与外部资源](reference_environment.md) — 构建/运行要点、CAD SDK 路径、测试脚本
- [工具需求格式](tool_request_format.md) — 现有工具不够用时的结构化「工具需求」模板(面向 AI 自己实现)
- [Agent 路线与多源架构](project_agent_roadmap.md) — 总目标(多格式图纸)、视觉为最大公约数、第一步多源证据层+多图、后续路线
- [Agent 路线与多源架构](project_agent_roadmap.md) — 总目标(多格式图纸)、视觉为最大公约数、多源证据层+多图、热力图/瓦片规划/inspect_region/find_collisions、**PDF+图片离线源(pypdfium2)**、后续路线
- [工具层与 agent 层的正交分工](feedback_tool_layering.md) — 反对无休止加工具;工具层正交稳定,业务规则进 agent 层
+76 -6
View File
@@ -1,6 +1,6 @@
---
name: Agent 路线与多源架构(2026-10-15)
description: dwg_review agent 的总目标、多源图纸分层架构(视觉为最大公约数)、工具层与 agent 层的正交分工、多源证据层、截图去 UI、grounding 实测、热力图 + 自适应瓦片规划 + VLM 分辨率标定、绘图区最大化(set_drawing_area)与空瓦片过滤、审查阶段分辨率对齐、agent 侧虚拟工具 inspect_region(放大取证闭环)、几何干涉检查 find_collisions(BVH + 容器过滤)、后续路线
name: Agent 路线与多源架构(2026-10-16)
description: dwg_review agent 的总目标、多源图纸分层架构(视觉为最大公约数)、工具层与 agent 层的正交分工、多源证据层、截图去 UI、grounding 实测、热力图 + 自适应瓦片规划 + VLM 分辨率标定、绘图区最大化(set_drawing_area)与空瓦片过滤、审查阶段分辨率对齐、agent 侧虚拟工具 inspect_region(放大取证闭环)、几何干涉检查 find_collisions(BVH + 容器过滤)、PDF/图片离线源(pypdfium2 + 墨迹瓦片)、后续路线
type: project
---
@@ -75,9 +75,8 @@ Agent 推理层(审查逻辑 / 清单 / 规则)
1. **VLM 主动放大(自适应取证)**:**2026-10-16 已落地**(见下节 `inspect_region`)。
剩余可选打磨:把「看清没」的判定进一步形式化(目前靠 agent 用标定数学**构造保证**可读 +
回合上限 + 截图预算兜底),以及 grounding→WCS 之外的其它指认方式。
2. **PDF / 图片源**:在 `phases/evidence.collect()` 加分支,产出**同形** evidence
(`window=None`、结构化空信封、`images` 带 label);规划阶段抽象"审查单元"
(CAD=图框 / PDF=页 / 图片=单);批注对非 CAD 源降级(`annotator` 已天然跳过无 handle 的 issue)。
2. **PDF / 图片源**:**2026-10-16 已落地**(见下节「C1:PDF / 图片离线源」)。
剩余:真实 PDF 图纸量大时按需增量取页;图片源的低分辨率原图无法放大(设计如此)。
3. **审图规范接入**:清单升级为规范库。
## 热力图 → 自适应瓦片规划 → VLM 分辨率标定(2026-10-15,进行中)
@@ -298,4 +297,75 @@ top 全是 `INSERT/40B × 一切` 且 ratio=1.0(图框完全包住小实体)
LLM 在受控子循环里**主动调用** `find_collisions {'min_overlap': 0.2, 'narrow': True,
'limit': 60, 'window': <图框>}`,并结合 `inspect_region`;最终**未误报干涉**(正确真阴性)。
**待办**:C1(PDF / 图片源适配,见「后续路线」第 2 条)仍未做。
**待办**:无(C1 / C2 均已完成)。
## C1:PDF / 图片**离线源**(2026-10-16)
**用户拍板的关键决定**:
- 支持 **PDF + 图片**两种源;PDF 先做,图片「顺手做了」。
- **完全离线,不连 CAD**(`connect_cad=False`)。
- **不做 OCR**:「都有 VLM 了,还 OCR 实在是画蛇添足」。
- **文本抽取只是次要补充**:国标图纸的文字多为 **SHX 描边**(导出成线条),文本层几乎为空
(实测该暖通 PDF:正文页 **0 个文本矩形**;有文本层的是目录页/表格页)。
- **PDF 在内存渲染,复用既有瓦片规划**能力。
- 后端选 **pypdfium2**(Apache-2.0/BSD、`py3-none-<platform>` wheel 与 Python 版本无关、
~4MB)而非 PyMuPDF(**AGPL-3.0** 有商业约束 + cpXX wheel)。文本抽取已降级为次要,
瓦片规划改用**像素墨迹密度**而非矢量路径,故 pypdfium2 足够。
**新增 `dwg_review/sources.py`(agent 侧,不编 DLL)**:
- 世界坐标系统一(都 y 向上,与 `heatmap`/`tiling` 一致):**PDF = 页内点**(已应用 `/Rotate`)、
**图片 = 源像素**。故 `window`/`bbox` 与 CAD 侧同形,直接复用 `tiling`/`heatmap` 纯函数。
- `detect_source(path)`(扩展名 → cad/pdf/image)、`plan_file(ctx)`(每页一个「图框」,
**保持原始页序**——不能再按 bbox 重排)、`collect(ctx, index, unit)`(同形 evidence)。
- **墨迹网格 `ink_grid(image, box)`**(纯 PIL,无 numpy):灰度 → 缩略 → 阈值二值化 →
`Image.BOX` 块均值降到网格 → 与 `heatmap.build_grid` **同形**的 grid + 占用格 bbox。
于是 `heatmap.cluster_cells` → `tiling.plan_regions` **原样复用**。
- **每张瓦片的渲染倍率由瓦片世界尺寸反推**:`zoom = IMAGE_MAX_EDGE / max(w,h)`,夹到源上限
(PDF `PDF_TILE_MAX_ZOOM`;图片 `IMAGE_MAX_ZOOM=1` **不放大**)。这与 `plan_regions` 的
`s_text`/`s_line` 是**同一套数学**(`zoom = 1/s`):规划出的文字簇瓦片恰好落在
`zoom_text = TEXT_READABLE_PX / (TEXT_HEIGHT_MM · PT_PER_MM)`。
- `PdfReader` / `ImageReader` 读取器(PDF 惰性 import pypdfium2);`open_reader/close_reader`
把读取器缓存在 `ctx.source_reader`(避免每页重复打开)。
**pypdfium2 的关键 API 事实(踩过的坑)**:
- `page.render(scale, rotation, crop)` 的 **`crop` 是「要裁掉的边距」(left,bottom,right,top),
不是绝对矩形**,单位是点、且在**旋转后**的页面上生效 → `crop_of(box, W, H) = (x0, y0, W-x1, H-y1)`。
写成绝对矩形会报 `Crop exceeds page dimensions`(或裁错区域)。
- `page.get_size()` **已应用 `/Rotate`**,`page.get_mediabox()` 未应用。
- **`tp.get_rect(i)` 前必须先调一次 `tp.count_rects()`**,否则 `PdfiumError: Failed to get rectangle`。
- 文本 rect 是**未旋转**坐标 → `/Rotate 90` 映射 `(x,y) → (y, mw - x)`(**实测验证**:
文本区墨迹命中率 0.335 vs 其它假设 ≤0.064,并目视确认「暖通专业图纸」正向)。
- 退化 bbox(宽/高 ≤ 0)会让 crop 边距之和等于页尺寸 → 提前拒绝,别交给 pdfium。
**离线链路打通(守卫 `ctx.tools is None`)**:
`context.ReviewContext.create(connect_cad=False)` → `client/tools` 均为 None;
`phases/evidence.maximized_drawing_area`、`phases/review._review_tools`/`_run_readonly_tool`/
`_reset_viewport`、`checks.run_deterministic_checks`、`annotator.annotate`/`clear` 全部先判 None。
`checklist.build_system_prompt(offline=True)` 换成**纯视觉提示词**(无工具协议、明确要求
**不要输出 handle**、说明文本层为空属正常)。
**CLI**:`python -m dwg_review <source>`(位置参数);`.pdf`/图片 → 离线模式,
`.dwg/.dxf` → 提示后按 CAD 在线模式(Python 打不开 DWG),未知格式/文件不存在 → exit 2。
`--list-tools`/`--check`/`--clear`/`--restore` 在离线源下被拒绝。
**实测(`verify_pdf_source.py`,20 页暖通图纸)**:
- PDF 前 4 页 **20/20 PASS**;图片源(由该 PDF 第 3 页渲染成 2717×1945 PNG)**9/9 PASS**。
- **分辨率自检(实测而非推导)**:每张瓦片的「纸面字高像素」≥ 线条下限 6px,
典型瓦片达 **8.5~13.9px**(文字档 8px);整体图 ~4.3px(整页缩略,**设计如此**,细节由瓦片负责)。
- **旋转自检自动化**:文本区墨迹 0.311 vs 整页平均 0.012;反向对照(忽略 `/Rotate`)0.145 → 映射正确。
- **真实 LLM 端到端**(`python -m dwg_review <pdf> --frames 3`):VLM **读懂了真实工程图**——
报出「设备标注框叠压家具/洁具/门扇弧线」「设备平面定位尺寸缺失」「标高以『梁底』代替数值不统一」
「1-1 剖面室外机未表达与墙柱间距」「材料表单重/共重两列全空」「审定未签署」等**具体且可核验**的问题,
且**未编造 handle**(离线证据无 handle,全部 issue 无 handle 字段)。
**新增配置**:`PDF_MAX_PAGES`(0=全部) / `PDF_TEXT_MIN_CHARS`(2) / `PDF_OVERVIEW_MAX_ZOOM`(4.0) /
`PDF_TILE_MAX_ZOOM`(6.0) / `IMAGE_MAX_ZOOM`(1.0) / `INK_ANALYSIS_MAX_EDGE`(1024) /
`INK_THRESHOLD`(200) / `INK_MIN_CELL_FRAC`(0.01);模块级 `PT_PER_MM = 72/25.4`。
**已知局限**:
- 图框/页边线是**细线**,在墨迹网格里把整页连成**一个大连通簇** → 瓦片常退化为
「整页 2×2 象限 + 少量密集小岛」。「空白区不出图」的收益因此打折(CAD 侧靠
`drop_oversized` 丢掉图框块,离线侧暂无等价手段)。成本由 `TILE_MAX_COUNT`/`EVIDENCE_MAX_IMAGES` 兜底。
- 图片源不能放大(`IMAGE_MAX_ZOOM=1`):低分辨率扫描件无法凭空提升可读性。
- 文本层对国标图纸基本无用(正文页 0 个文本矩形),**这是预期**,主力仍是 VLM 看图。
- 依赖新增:`requirements.txt`(openai / Pillow / pypdfium2)。
+23 -1
View File
@@ -49,11 +49,22 @@ type: reference
| `verify_drawing_area.py` | 绘图区最大化验收:基线 → `set_drawing_area(max)` → `restore`,断言窗口矩形/最大化态/绘图区尺寸全回位(实测 3.68×) |
| `verify_review_maximized.py` | 审查全链路验收:在 `collect`/`review_frame` 入口各采一次窗口矩形,断言**取证与审查两阶段都在最大化态**、结束精确还原;并统计子循环 zoom/截图次数 + 记录 LLM 实际调用的取证工具(含虚拟工具 `inspect_region`) |
| `verify_collisions.py` | `find_collisions` 验收:工具注册 / 全模型与逐图框粗筛 / **容器过滤**(图框块剔除)/ `min_overlap` 阈值 / 分页 / `narrow` 细筛 / `--inject` 注入干涉正例(画 2 个深重叠矩形 → 应报出 ratio≈0.902 → 清理)。**只读**(`--inject` 段除外,会自动清理) |
| `verify_pdf_source.py` | **PDF / 图片离线源验收(不需要 CAD)**:离线上下文断言 / 页序 / evidence 同形 / 图片窗口在图框内 / **实测瓦片字高像素达标** / **旋转映射自检**(文本区墨迹 vs 忽略 `/Rotate` 反向对照)/ `--dump` 导出目视包 / `--llm` 追加真实 `python -m dwg_review` 端到端 |
> 历史:根目录 `test.py` / `test_autoagent.py` / `countOfCircle.py` / `agent_loop.py` /
> `dwg_review_agent.py` 五个遗留调试脚本已于 2026-10-14 删除(前三个含硬编码 API Key,
> 后两个是重复实现)。`test_screenshot.png` 是 `test_autoagent.py` 的残留产物。
## Python 运行时依赖(2026-10-16 起)
- 仓库根新增 **`requirements.txt`**(运行时)与 `requirements-dev.txt`(仅 pytest)。
- `openai>=1.0`(LLM 客户端,Key 走环境变量 `DEEPSEEK_API_KEY`)
- `Pillow>=10.0`(截图下采样、墨迹分析、离线标定)
- `pypdfium2>=4.30`(PDF 源适配:内存渲染 + 带 bbox 抽文本)
- 选它而非 PyMuPDF:**许可证干净**(Apache-2.0/BSD vs AGPL-3.0)、
wheel 为 `py3-none-<platform>`(**与 Python 版本无关**,实测 Python 3.14.5 直接可用)、体积小(~4MB)。
- 实测环境:Python **3.14.5**,`pypdfium2 5.14.0`(`py3-none-win_amd64`)。
## 两个构建目标
| 目标 | 产物 | 作用 |
@@ -93,7 +104,9 @@ type: reference
本仓库已设置 `git config core.sshCommand "ssh -i ~/.ssh/id_ed25519_guojianming -o IdentitiesOnly=yes"`。
换机器时需补该配置或加入 `~/.ssh/config`。
## 真实测试图纸(2026-10-15)
## 真实测试图纸
**CAD 源(2026-10-15)**
- 图纸:`e:\desktop\agent_test.dwg`(用户制作的真实 Plant 3D 图纸:3 个图框 A0/A1/A2,
86 个实体:`AECDBEWFITTING` 40 / `AECDBEWPIPE` 29 / `AECDBEWEQUIPMENT` 14 / `INSERT` 3)。
@@ -101,3 +114,12 @@ type: reference
—— 必须带 `/p SHS-ENVI2026` profile,否则**自定义实体无法显示**(该 profile 自动加载
用户写的显示插件)。
- 实测:`python -m dwg_review --frames 1` 端到端可跑通并出报告。
**PDF 源(2026-10-16)**
- 图纸:`E:\desktop\05 暖通专业图纸.pdf`(**20 页**,A3/A4 混排,`/Rotate 90`)。
页 0 = 封面(仅一行标题字);页 1 = 图纸目录(**有文本层**,43 个文本矩形);
页 2 起 = 真实施工图(**0 个文本矩形**,文字全是 SHX 描边)。
- 实测:`python -m dwg_review "E:\desktop\05 暖通专业图纸.pdf" --frames 3`(**离线、不连 CAD**)
端到端跑通,VLM 报出设备标注叠压、定位尺寸缺失、材料表空白等具体问题。
- 图片源样本:由该 PDF 第 3 页渲染的 `out/verify_pdf_source/page2.png`(2717×1945,测试产物)。