支持直接审查 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 丢图框块,离线侧暂无等价手段),留待后续打磨。
108 lines
3.6 KiB
Python
108 lines
3.6 KiB
Python
"""统一证据模型:多来源(CAD / PDF / 图片)证据的字段常量与访问器。
|
||
|
||
设计要点:
|
||
- evidence 是跨阶段 + 落盘的「线格式」数据,统一用 dict 承载(不用 dataclass)。
|
||
- 图片统一为 `images: [{"b64","mime","label"}]`;访问器兼容旧的 `image_b64/image_mime`
|
||
单图字段(历史 evidence.json 与旧测试构造仍可读)。
|
||
- 本模块不含任何 CAD / 工具逻辑,可被 phases、findings、__main__、tests 共同引用。
|
||
"""
|
||
|
||
SOURCE_CAD = "cad"
|
||
SOURCE_PDF = "pdf"
|
||
SOURCE_IMAGE = "image"
|
||
|
||
# 结构化证据键(CAD 侧各查询工具产出的信封);下游 handle 收集 / 派生检查统一引用,
|
||
# 避免在多处硬编码同一组键名。
|
||
STRUCTURED_SOURCES = ("entities", "texts", "dimensions", "blocks", "polylines")
|
||
|
||
|
||
def empty_envelope(total=0):
|
||
"""构造空的**结构化信封** `{"total": N, "items": []}`。
|
||
|
||
CAD 侧各查询工具统一返回此形(`items` 明细 + `total` 计数)。非 CAD 源
|
||
(PDF / 图片)没有结构化数据,用本工厂补齐同形键,使下游 handle 收集 /
|
||
派生检查无需分支即可统一遍历(空 items 自然不产出任何检查项)。
|
||
"""
|
||
return {"total": int(total), "items": []}
|
||
|
||
|
||
def make_image(b64, mime="image/png", label=None, window=None):
|
||
"""构造 images 列表元素。
|
||
|
||
`window` 为该截图**实际可见的世界范围** `(min_x,min_y,max_x,max_y)`(可选)。
|
||
审查阶段的 `inspect_region` 靠它把「截图上的归一化坐标」映射回 WCS(逐级下钻)。
|
||
"""
|
||
img = {"b64": b64, "mime": mime or "image/png", "label": label}
|
||
if window is not None:
|
||
img["window"] = window
|
||
return img
|
||
|
||
|
||
def source_type_of(container):
|
||
"""读取来源类型,缺省视为 CAD。
|
||
|
||
container 可以是审查单元(unit)/ 图框 / evidence 等任何 dict。
|
||
"""
|
||
if isinstance(container, dict):
|
||
value = container.get("source_type")
|
||
if isinstance(value, str) and value:
|
||
return value
|
||
return SOURCE_CAD
|
||
|
||
|
||
def evidence_images(evidence):
|
||
"""规范化证据里的图片列表(单点归一)。
|
||
|
||
优先读 `images`;否则回退旧的 `image_b64` / `image_mime` 单图字段。
|
||
统一返回 `[{"b64","mime","label","window"}]`,过滤掉空 b64。
|
||
"""
|
||
if not isinstance(evidence, dict):
|
||
return []
|
||
images = []
|
||
raw = evidence.get("images")
|
||
if isinstance(raw, list):
|
||
for item in raw:
|
||
if not isinstance(item, dict):
|
||
continue
|
||
b64 = item.get("b64")
|
||
if not b64:
|
||
continue
|
||
images.append(
|
||
{
|
||
"b64": b64,
|
||
"mime": item.get("mime") or "image/png",
|
||
"label": item.get("label"),
|
||
"window": item.get("window"),
|
||
}
|
||
)
|
||
if images:
|
||
return images
|
||
# 回退:旧的单图字段
|
||
b64 = evidence.get("image_b64")
|
||
if b64:
|
||
return [
|
||
{
|
||
"b64": b64,
|
||
"mime": evidence.get("image_mime") or "image/png",
|
||
"label": None,
|
||
"window": None,
|
||
}
|
||
]
|
||
return []
|
||
|
||
|
||
def primary_image(evidence):
|
||
"""返回首图(label == "overall" 优先),无图返回 None。"""
|
||
images = evidence_images(evidence)
|
||
if not images:
|
||
return None
|
||
for img in images:
|
||
if img.get("label") == "overall":
|
||
return img
|
||
return images[0]
|
||
|
||
|
||
def image_labels(evidence):
|
||
"""返回图片标签列表(用于日志 / payload 说明)。"""
|
||
return [img.get("label") for img in evidence_images(evidence)]
|