perf(review): 安装 Pillow 并优化截图下采样策略

- 视觉 token 成本由像素尺寸决定,故仅在长边超过 IMAGE_MAX_EDGE(默认1280) 时才缩放
- 不需要缩放且格式未变时原样返回,不重新编码,避免 CAD 线稿的无谓压缩损失
- 缩放后默认输出无损 PNG(IMAGE_FORMAT=png),可设 jpeg 换取更小体积
- 缺少 Pillow 时自动降级为原图直传
This commit is contained in:
gjm committed 2026-10-13 23:48:46 +08:00
1 parent ec5292debe
commit 1cbcbb92f4
3 files changed
+35 -12

No files matched your search

+5 -2
View File
@@ -43,8 +43,11 @@ type: project
## 环境事实 ## 环境事实
- 本机 **未安装 Pillow** → 截图下采样为**可选**(缺失时原样传原图,视觉 token 成本更高)。 - **Pillow 已安装**(12.3.0)。截图下采样策略(`dwg_review/llm_client.py: downsample_image_b64`):
如需控成本:`pip install pillow`。 视觉 token 成本由**像素尺寸**决定而非文件大小 → **只在长边超 `IMAGE_MAX_EDGE`(默认 1280)时才缩放**;
既不需要缩放、格式又没变时**原样返回不重编码**(避免给 CAD 线稿引入无谓压缩损失);
缩放后默认输出**无损 PNG**(`IMAGE_FORMAT=png`,适合线稿),需要更小体积可设 `IMAGE_FORMAT=jpeg`。
缺少 Pillow 时自动降级为原图直传。
- 视觉模型 `deepseek-flash`。 - 视觉模型 `deepseek-flash`。
- **`DEEPSEEK_API_KEY` 设在 Windows `Machine`(系统)级**。因新设的系统变量**不会传播到已启动 - **`DEEPSEEK_API_KEY` 设在 Windows `Machine`(系统)级**。因新设的系统变量**不会传播到已启动
的进程**,`dwg_review/config.py` 的 `_read_env()` 在进程环境读不到时**回退读注册表** 的进程**,`dwg_review/config.py` 的 `_read_env()` 在进程环境读不到时**回退读注册表**
+4 -2
View File
@@ -90,9 +90,11 @@ ANNOTATE_LAYER = os.environ.get("ANNOTATE_LAYER", "AI_REVIEW")
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 截图(视觉 token 成本控制) # 截图(视觉 token 成本控制)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 长边上限(下采样目标),0 表示不下采样 # 长边上限(超过才缩放),0 表示不缩放
IMAGE_MAX_EDGE = int(os.environ.get("IMAGE_MAX_EDGE", "1280")) IMAGE_MAX_EDGE = int(os.environ.get("IMAGE_MAX_EDGE", "1280"))
# JPEG 质量 # 缩放后的输出格式: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")) IMAGE_JPEG_QUALITY = int(os.environ.get("IMAGE_JPEG_QUALITY", "80"))
+26 -8
View File
@@ -23,28 +23,46 @@ class LlmError(Exception):
pass pass
def downsample_image_b64(b64, mime="image/png", max_edge=None, quality=None): def downsample_image_b64(b64, mime="image/png", max_edge=None, fmt=None, quality=None):
"""把 base64 截图下采样并转 JPEG,降低视觉 token 成本。 """缩放截图,降低视觉 token 成本。
缺少 Pillow 或参数为 0 时原样返回 (b64, mime)。 策略(视觉 token 成本主要由**像素尺寸**决定,而非文件大小):
- 仅当长边超过 `max_edge` 时才缩放;
- 既不需要缩放、目标格式又与原格式一致时,**原样返回**(不重新编码,
避免给 CAD 线稿引入无谓的压缩损失);
- 需要缩放时按 `fmt` 编码:`png`(默认,无损,适合线稿)/ `jpeg`(体积更小)。
缺少 Pillow 或 max_edge <= 0 时原样返回 (b64, mime)。
""" """
max_edge = config.IMAGE_MAX_EDGE if max_edge is None else max_edge max_edge = config.IMAGE_MAX_EDGE if max_edge is None else max_edge
fmt = (config.IMAGE_FORMAT if fmt is None else fmt).strip().lower()
quality = config.IMAGE_JPEG_QUALITY if quality is None else quality quality = config.IMAGE_JPEG_QUALITY if quality is None else quality
if not HAS_PIL or max_edge <= 0: if not HAS_PIL or max_edge <= 0:
return b64, mime return b64, mime
target_mime = "image/jpeg" if fmt == "jpeg" else "image/png"
try: try:
img = Image.open(io.BytesIO(base64.b64decode(b64))) img = Image.open(io.BytesIO(base64.b64decode(b64)))
if img.mode not in ("RGB", "L"):
img = img.convert("RGB")
w, h = img.size w, h = img.size
scale = max_edge / float(max(w, h)) scale = max_edge / float(max(w, h))
if scale < 1.0: need_resize = scale < 1.0
if not need_resize and mime == target_mime:
return b64, mime
if need_resize:
img = img.resize( img = img.resize(
(max(1, int(w * scale)), max(1, int(h * scale))), Image.LANCZOS (max(1, int(w * scale)), max(1, int(h * scale))), Image.LANCZOS
) )
buf = io.BytesIO() buf = io.BytesIO()
img.save(buf, format="JPEG", quality=quality) if target_mime == "image/jpeg":
return base64.b64encode(buf.getvalue()).decode("ascii"), "image/jpeg" if img.mode not in ("RGB", "L"):
img = img.convert("RGB")
img.save(buf, format="JPEG", quality=quality)
else:
img.save(buf, format="PNG", optimize=True)
return base64.b64encode(buf.getvalue()).decode("ascii"), target_mime
except Exception: except Exception:
return b64, mime return b64, mime