Files
cad-agent/dwg_review/config.py
T
gjm cba05b0c65 feat(tiling): 自适应瓦片规划器 + VLM 分辨率离线标定
背景:逐图框审查固定「整体 + 2x2 象限」截图,大图幅(A0/A1)读不清细节。
改为按**实体分布**自适应决定截几张、截哪里。

- 新增 dwg_review/tiling.py(纯函数):
  SHEET_SIZES_MM / frame_world_size / frame_scale(sheet_scale>0 用之,否则由
  bbox/纸张反算,否则 default)/ effective_px(与 downsample 同规则)/
  world_per_px / is_text_type(标准名 + 自定义类名含 TEXT/LABEL/DIM/NOTE/TAG/
  ANNO/TITLE)/ cluster_text_flags / tiles_for_box / subdivide / plan_regions
  (文字簇优先、空白区不出图、max_tiles 截断、窗口夹到图框范围)
- 新增 verify_vlm_resolution_offline.py:PIL 合成类图纸图(精确像素字高 +
  可控线间距/线宽/虚线)→ 问 VLM 强约束 JSON → 定 R_text / R_line。
  实测:R_text=12px(黑底;白底 10px);R_line=6px。
  另:实测该模型**无法可靠计数**(6 条线在 2~10px 间距下答 4/10/4/5/4/4),
  故线条判据改用「1 条 vs 2 条」二选一。
- config 新增 TILING_ENABLED / TEXT_HEIGHT_MM / TEXT_READABLE_PX=12 /
  LINE_READABLE_PX=6 / TILE_MAX_COUNT / TILE_OVERLAP / HEATMAP_CELL_DIV /
  HEATMAP_CLUSTER_DILATE / HEATMAP_MAX_AREA_FRAC;EVIDENCE_DETAIL_TILES 与
  EVIDENCE_MAX_IMAGES 保留作回退路径参数
- llm_client 新增 image_size_b64(测量绘图区实际像素,决定单张覆盖范围)
- phases/evidence.capture_images 支持瓦片规划:先截整体图并测像素 →
  plan_regions 成功用规划窗口,否则退回 sub_windows(行为零变化);
  末尾始终复位整体视口。collect_frame 传实体/文字 bbox;任一信封
  total>returned(截断)→ 退回固定策略,避免误删有内容的瓦片
- 测试:tests/test_tiling.py(28 项)、tests/test_evidence.py 新增规划路径与
  降级用例、test_config / test_llm_client 补新常量与 image_size_b64;
  全量 283 项全绿
2026-10-15 23:03:27 +08:00

158 lines
7.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""集中配置:全部来自环境变量,禁止硬编码密钥。
用法:所有模块统一 `from . import config` 后读取 config.XXX。
"""
import os
from pathlib import Path
# 仓库根目录(本文件位于 <root>/dwg_review/config.py)
PROJECT_ROOT = Path(__file__).resolve().parent.parent
def _resolve_path(value: str) -> Path:
"""相对路径按仓库根目录解析(避免依赖当前工作目录)。"""
p = Path(value)
return p if p.is_absolute() else (PROJECT_ROOT / p)
def _read_env(name: str) -> str:
"""读取环境变量。
Windows 下新设置的系统/用户环境变量不会传播到**已启动**的进程,
因此当前进程读不到时,回退到注册表中的 Machine / User 级环境变量。
"""
value = os.environ.get(name)
if value:
return value
if os.name != "nt":
return ""
try:
import winreg
except ImportError: # pragma: no cover
return ""
locations = (
(winreg.HKEY_LOCAL_MACHINE,
r"SYSTEM\CurrentControlSet\Control\Session Manager\Environment"),
(winreg.HKEY_CURRENT_USER, r"Environment"),
)
for hive, subkey in locations:
try:
with winreg.OpenKey(hive, subkey) as key:
value, _ = winreg.QueryValueEx(key, name)
if value:
return str(value)
except OSError:
continue
return ""
def _read_bool(name: str, default: bool = True) -> bool:
"""读取布尔型环境变量;未设置或空串时返回 default。"""
value = os.environ.get(name)
if value is None or value.strip() == "":
return default
return value.strip().lower() in ("1", "true", "yes", "on")
# ---------------------------------------------------------------------------
# CAD MCP 基座(TCP JSON-RPC)
# ---------------------------------------------------------------------------
HOST = os.environ.get("CAD_MCP_HOST", "127.0.0.1")
PORT = int(os.environ.get("CAD_MCP_PORT", "8080"))
# 单次 TCP 调用超时(秒)
CALL_TIMEOUT = float(os.environ.get("CAD_MCP_TIMEOUT", "30"))
# 连接/超时类错误的重试次数(写类工具不重试)
CALL_RETRIES = int(os.environ.get("CAD_MCP_RETRIES", "2"))
# 帧大小上限(与基座 8MB 上限对齐,留余量)
MAX_FRAME_BYTES = int(os.environ.get("MAX_FRAME_BYTES", str(4 * 1024 * 1024)))
# ---------------------------------------------------------------------------
# LLM(默认 DeepSeek,视觉模型 deepseek-flash)
# ---------------------------------------------------------------------------
API_KEY = _read_env("DEEPSEEK_API_KEY")
BASE_URL = os.environ.get("DEEPSEEK_BASE_URL", "https://api.deepseek.com")
MODEL_NAME = os.environ.get("DEEPSEEK_MODEL", "deepseek-flash")
LLM_TIMEOUT = float(os.environ.get("LLM_TIMEOUT", "120"))
LLM_RETRIES = int(os.environ.get("LLM_RETRIES", "2"))
# ---------------------------------------------------------------------------
# 审查流程
# ---------------------------------------------------------------------------
# 审查清单路径
CHECKLIST_PATH = _resolve_path(os.environ.get("REVIEW_CHECKLIST", "review_checklist.json"))
# 最多审查多少个图框(0 = 全部)
MAX_FRAMES = int(os.environ.get("MAX_FRAMES", "0"))
# 每个图框取数据的条数上限
FRAME_ENTITY_LIMIT = int(os.environ.get("FRAME_ENTITY_LIMIT", "200"))
# 缩放后等待 CAD 渲染的秒数
RENDER_WAIT = float(os.environ.get("RENDER_WAIT", "0.6"))
# 审查阶段受控工具子循环的最大回合数
REVIEW_MAX_ITERS = int(os.environ.get("REVIEW_MAX_ITERS", "4"))
# 报告输出目录
REPORT_DIR = _resolve_path(os.environ.get("REPORT_DIR", "reports"))
# 批注回写使用的图层
ANNOTATE_LAYER = os.environ.get("ANNOTATE_LAYER", "AI_REVIEW")
# ---------------------------------------------------------------------------
# 可信度三件套:确定性检查前置 / 结论结构化 / 证据交叉校验
# ---------------------------------------------------------------------------
# 是否在 LLM 审查前跑确定性检查(零长度、重复实体、空文字、未闭合多段线)
DETERMINISTIC_CHECKS = _read_bool("DETERMINISTIC_CHECKS", True)
# 是否校验 LLM 引用的 handle 确实出现在已采集证据中(不在则剥掉 handle)
VALIDATE_HANDLES = _read_bool("VALIDATE_HANDLES", True)
# 是否检查未闭合多段线(中心线/开放轮廓多时误报,可关闭)
CHECK_UNCLOSED_POLYLINES = _read_bool("CHECK_UNCLOSED_POLYLINES", True)
# find_zero_length 的长度阈值
ZERO_LENGTH_TOLERANCE = float(os.environ.get("ZERO_LENGTH_TOLERANCE", "0.001"))
# find_duplicate_entities 的几何量化容差
DUP_TOLERANCE = float(os.environ.get("DUP_TOLERANCE", "0.001"))
# find_duplicate_entities 的 limit(语义是「最大重复组数」,非条目数)
DUP_GROUP_LIMIT = int(os.environ.get("DUP_GROUP_LIMIT", "50"))
# ---------------------------------------------------------------------------
# 截图(视觉 token 成本控制)
# ---------------------------------------------------------------------------
# 长边上限(超过才缩放),0 表示不缩放
IMAGE_MAX_EDGE = int(os.environ.get("IMAGE_MAX_EDGE", "1280"))
# 缩放后的输出格式:png(默认,无损,适合 CAD 线稿)或 jpeg(体积更小)
IMAGE_FORMAT = os.environ.get("IMAGE_FORMAT", "png").strip().lower()
# JPEG 质量(仅 IMAGE_FORMAT=jpeg 时生效)
IMAGE_JPEG_QUALITY = int(os.environ.get("IMAGE_JPEG_QUALITY", "80"))
# 取证阶段每图框的局部截图数(0=仅整体;4=整体+2x2 象限)
# —— 仅在自适应瓦片规划不可用时作为**回退**路径参数
EVIDENCE_DETAIL_TILES = int(os.environ.get("EVIDENCE_DETAIL_TILES", "4"))
# 每图框图片硬上限(整体 + 局部总张数)
EVIDENCE_MAX_IMAGES = int(os.environ.get("EVIDENCE_MAX_IMAGES", "5"))
# ---------------------------------------------------------------------------
# 自适应瓦片规划(按实体分布决定截几张、截哪里)
# ---------------------------------------------------------------------------
# 是否启用瓦片规划;False = 退回固定 EVIDENCE_DETAIL_TILES(行为与旧版一致)
TILING_ENABLED = _read_bool("TILING_ENABLED", True)
# 纸面字高(mm)。国标标注字高 2~3mm,取保守下限
TEXT_HEIGHT_MM = float(os.environ.get("TEXT_HEIGHT_MM", "2.0"))
# VLM 可读的**最小像素字高**。
# 离线标定(verify_vlm_resolution_offline.py,repeat=2):黑底 12px / 白底 10px 全行正确。
# CAD 模型空间默认黑底 → 取 12;离线合成图偏干净,故以 B2 CAD 实测为最终依据。
TEXT_READABLE_PX = int(os.environ.get("TEXT_READABLE_PX", "12"))
# VLM 可分辨的**最小线间距像素**。
# 同上标定:4~5px 有噪声、≥6px 稳定(1 条 vs 2 条二选一)。取 6 保守。
LINE_READABLE_PX = int(os.environ.get("LINE_READABLE_PX", "6"))
# 单图框瓦片数硬上界(成本兜底)
TILE_MAX_COUNT = int(os.environ.get("TILE_MAX_COUNT", "36"))
# 相邻瓦片重叠比例(防边界切字)
TILE_OVERLAP = float(os.environ.get("TILE_OVERLAP", "0.0"))
# 热力图网格划分(长边切几份)
HEATMAP_CELL_DIV = int(os.environ.get("HEATMAP_CELL_DIV", "32"))
# 热力图聚类前的膨胀格数(合并相近内容;0=不膨胀)
HEATMAP_CLUSTER_DILATE = int(os.environ.get("HEATMAP_CLUSTER_DILATE", "0"))
# 丢弃 bbox 面积超过图框该比例的实体(去图框边框/图签块)
HEATMAP_MAX_AREA_FRAC = float(os.environ.get("HEATMAP_MAX_AREA_FRAC", "0.5"))
def has_api_key() -> bool:
return bool(API_KEY.strip())