Files
cad-agent/dwg_review/config.py
T
gjm 9c4ab62dc1 feat(tiling): CAD 实测标定 R_text/R_line + 瓦片粒度内容过滤 + 默认参数定稿
Stage B2(CAD 实测标定)与收尾:

- 新增 verify_text_resolution.py:临时加字(字高 200 图形单位 = 1:100 纸面 2mm)+
  平行线对,按目标像素换算 zoom 窗口 → 截图 → 问 VLM;测完 delete_entities 清理不保存。
- 标定结论:
  * **可读下限与字体强相关**——离线合成(Consolas 抗锯齿填充字体)12px;
    真实 CAD 单线体 .shx(描边字体)6px 即可读对(3/3,两轮)。
  * 线条「1 条 vs 2 条」:2/3/4px 失败,5/8/12px 成功 → R_line ≈ 5px。
  * **必须用 ACI 7 白字**:实测红字(ACI 1)12px 因对比度不足完全读不出。
- config 定稿:TEXT_READABLE_PX=8(折中)、LINE_READABLE_PX=6、
  EVIDENCE_MAX_IMAGES=12(原 5 会把规划结果截到只剩 4 张瓦片)。
- 新增 heatmap.occupied_boxes:占用格的**世界 bbox**。
- tiling.plan_regions 新增 keep 谓词;evidence._plan_tiles 按占用格剔除**空瓦片**
  (簇 bbox 是包围盒,L 形/斜向分布含大片空白,实测有效)。
- 测试:test_heatmap 补 occupied_boxes;test_tiling 补 keep 过滤;全量 286 项全绿。
- 真实图纸端到端验证:A0 图框出 12 张(1 整体 + 11 瓦片),单框审查 1m38s 通过。
- 记忆库同步:工具语义坑(type_filter 必须传列表 / layer_filter 是通配字符串 /
  add_* 返回句柄在 created 键)、标定结论与默认值。
2026-10-15 23:22:15 +08:00

161 lines
7.7 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"))
# 每图框图片硬上限(整体 + 局部总张数)。
# 默认 12 才能装下规划器给出的瓦片(实测测试图 A0 需 12 张);调小会截断规划结果。
EVIDENCE_MAX_IMAGES = int(os.environ.get("EVIDENCE_MAX_IMAGES", "12"))
# ---------------------------------------------------------------------------
# 自适应瓦片规划(按实体分布决定截几张、截哪里)
# ---------------------------------------------------------------------------
# 是否启用瓦片规划;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 可读的**最小像素字高**。实测与**字体强相关**:
# - 离线合成(Consolas 抗锯齿填充字体)12px(白底 10px);
# - 真实 CAD 单线体 .shx(描边字体)6px 即可读对(3/3,两轮)。
# 取 8 折中:兼顾描边与常见 TrueType。标定脚本见 verify_vlm_resolution_offline.py /
# verify_text_resolution.py(后者须用 ACI 7 白字——红字 12px 因对比度不足完全读不出)。
TEXT_READABLE_PX = int(os.environ.get("TEXT_READABLE_PX", "8"))
# VLM 可分辨的**最小线间距像素**(1 条 vs 2 条二选一)。
# 实测 2/3/4px 失败、5/8/12px 成功;取 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())