perf(review): 安装 Pillow 并优化截图下采样策略
- 视觉 token 成本由像素尺寸决定,故仅在长边超过 IMAGE_MAX_EDGE(默认1280) 时才缩放 - 不需要缩放且格式未变时原样返回,不重新编码,避免 CAD 线稿的无谓压缩损失 - 缩放后默认输出无损 PNG(IMAGE_FORMAT=png),可设 jpeg 换取更小体积 - 缺少 Pillow 时自动降级为原图直传
This commit is contained in:
1 parent
ec5292debe
commit
1cbcbb92f4
3 files changed
+35
-12
No files matched your search
@@ -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()` 在进程环境读不到时**回退读注册表**
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user