fix(seedream): size 面积钳制——修 1920x1080 被 ARK 400 打回(bump 0.37.1)

模型自选 16:9 出图(1920x1080=2.07M px)触发 ARK 硬门
`image size must be at least 3686400 pixels`(=1920²,卡总面积非单边),
整次文生图 400 失败。

- tools/seedream.py: 新增 _normalize_size(),出图前把 size 钳进
  [min_pixels, max_pixels]:面积不足按 sqrt(min/area) 等比放大、
  取整到 8 的倍数并复核达标(1920x1080→2560x1440);超上限等比缩小;
  已合规原样透传(向后兼容)。归一化时返回串附 [note]、meta 记
  requested_size,记账按真实出图尺寸。
- config/media/doubao.yaml: seedream_5 加 min_pixels/max_pixels
  (旧 yaml 缺键=不设该侧,行为不变)。
- bump 0.37.0→0.37.1;PROGRESS 加一条。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
caoqianming 2026-07-03 12:41:46 +08:00
parent 0e02cff6c6
commit 6f27b7cc5a
4 changed files with 82 additions and 4 deletions

View File

@ -2,7 +2,7 @@
> 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`
最后更新:2026-07-03(ppt 门体系二轮硬化:逃生口收紧 + 导出自动质检 + svg_final 嵌图修复,bump 0.36.1)
最后更新:2026-07-03(seedream size 面积钳制:修 1920x1080 被 ARK 400 打回,bump 0.37.1)
---
@ -21,6 +21,9 @@
## 已完成关键能力
### 2026-07-03 / seedream size 面积钳制(修 1920x1080 被 ARK 400 打回,bump 0.37.1)
模型自选 16:9 出图(如 `1920x1080`=2,073,600px)触发 ARK 硬门 `image size must be at least 3686400 pixels`(=1920²),整次文生图直接 400 失败。根因:`tools/seedream.py` 把 `size` 原样透传,不校验 ARK 的**面积**约束(卡的是总像素不是单边,故 16:9 最小合规是 2560x1440)。修:tool 内新增 `_normalize_size()`,拿到 `chosen_size` 前先钳进 `[min_pixels, max_pixels]`——面积 `<min``sqrt(min/area)` 等比放大、两边向上取整到 8 的倍数并复核达标(1920x1080→2560x1440);`>max`(3072²=9,437,184)等比缩小;已合规原样透传(向后兼容)。约束值加到 `config/media/doubao.yaml` seedream_5 档(`min_pixels`/`max_pixels`,旧 yaml 缺键则视为不设该侧、行为不变)。归一化时返回串附 `[note]` 提示 + meta 记 `requested_size`,usage 记账按**真实出图尺寸**。选自动钳而非返错让模型重试:省一轮往返、避免二次错。新增 tests 手验 9 例全落合法区间。
### 2026-07-03 / ppt 对齐网格锁 + 错位/单调质检(d1285247 陶瓷 deck 复盘,bump 0.37.0)
对 d1285247 产物(25 页陶瓷方案 PPTX)逐页几何量测 + PowerPoint COM 渲图目视复盘,三类缺陷:①跨页左基线漂移(0.6560.75in 七个值)+ 并排块顶差 212px 的"想对齐没对齐"(S8/S19/S23);②5 页同为"图标+标题+三行字"卡网格,零流程箭头/零分层图形,单调;③标题语义不兑现("五层架构"画成五条等宽横条、"矩阵"画成卡片格)。根因:executor 手写绝对坐标但 spec_lock 无网格常量可依;质检只查重叠/越界不查对齐;"节奏不雷同"只约束相邻页。修四层:**A spec_lock 新增 `layout_grid` 锁段**(margin_x/content_top/footer_y/gutter,strategist 派生、executor 每页吸附、checker 强制;design_spec_reference §V 同步);**B executor-base §3 网格对齐纪律**(并排卡片同 top 同高等 gutter、打破网格 ≥16px 干净打破、同行文字 ≥0.3em 禁贴字);**C svg_quality_checker 新增 check 14**——兄弟卡片近失对齐(精确几何,212px error;底对齐/中心对齐/绘图区内数据柱三类豁免,71 charts 模板回归误报清零)、layout_grid 偏离 215px error、行内 gap 不等 warning、无锁存量项目跨页左缘聚类漂移 warning、版式指纹单调门(≥3 页同指纹 warn、≥4 或过半 error;仅对 NN_ 编号 deck 页聚合,模板库静默);**D 策略纪律升级**——同一版式原型整本 ≤2 次 + 标题语义必须被图形兑现(SKILL.md 大纲纪律 + strategist visual-floor GATE)。顺手修 comparison_columns 模板胶囊 5px 错位。新增 tests/test_svg_alignment_check.py 21 项,全量 153 过。已知边界:页面平衡类(底部大空白/重心偏移,S18/S22)误报风险高未进 checker,只进阶段五验收 checklist 眼看;错位 error 会被导出边界自动质检门连带拦截,存量项目重导出若报新 error 属预期(真缺陷)。

View File

@ -21,6 +21,11 @@ image:
endpoint: /images/generations
price_cny_per_image: 0.22 # 计费单位:成功输出张数;调价改这里 + 重启
default_size: 2048x2048 # 原生最高 3072x3072;2K 兼顾质量/体积
# 输出尺寸面积约束(ARK 硬门):面积 < min_pixels → 400 InvalidParameter。
# 模型自选 16:9 之类小尺寸(如 1920x1080=2.07M)会栽,故 tool 侧等比钳到合法区间:
# min = 1920² = 3,686,400(16:9 最小合规即 2560x1440);max = 3072² = 9,437,184。
min_pixels: 3686400
max_pixels: 9437184
default_watermark: false # 默认无水印(申报/PPT 场景反需求)
default_search: false # web search 额外加价 ~¥0.05/张;默认关
request_timeout_s: 60 # 出图慢于此判超时

View File

@ -1,3 +1,3 @@
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
# 改版本只动这一行。
__version__ = "0.37.0"
__version__ = "0.37.1"

View File

@ -138,7 +138,15 @@ class SeedreamTool(Tool):
cfg = self.cfg
model_id = cfg["model_id"]
chosen_size = size or cfg.get("default_size", "2048x2048")
requested_size = size or cfg.get("default_size", "2048x2048")
# ARK 硬门:输出面积必须落在 [min_pixels, max_pixels],否则 400 InvalidParameter。
# 模型自选 16:9 之类小尺寸(1920x1080=2.07M < 3.69M)会被打回,这里等比钳到合法区间,
# 静默纠错省一轮往返;已合规的尺寸原样透传。归一化时给用户一行提示。
chosen_size, size_note = self._normalize_size(
requested_size,
min_pixels=int(cfg.get("min_pixels", 0)),
max_pixels=int(cfg.get("max_pixels", 0)),
)
chosen_watermark = bool(cfg.get("default_watermark", False)) if watermark is None else bool(watermark)
chosen_search = bool(cfg.get("default_search", False)) if search is None else bool(search)
timeout_s = float(cfg.get("request_timeout_s", 60))
@ -185,6 +193,7 @@ class SeedreamTool(Tool):
"prompt": prompt,
"model_id": model_id,
"size": chosen_size,
"requested_size": requested_size, # 归一化前模型/用户请求的原始尺寸(=chosen_size 表示未钳)
"watermark": chosen_watermark,
"search": chosen_search,
"mode": "i2i" if is_i2i else "t2i",
@ -219,14 +228,75 @@ class SeedreamTool(Tool):
# 格式严格 key=value · 分隔,parse 用正则 `key=([^·\n]+)` 抓。
mode_seg = " · mode=i2i" if is_i2i else ""
ref_line = f"\nreference={ref_disp[0]}" if is_i2i else ""
note_line = f"\n{size_note}" if size_note else ""
return (
f"[seedream] model={model_id} · size={chosen_size} · "
f"cost=¥{cost_cny:.2f} · elapsed={elapsed:.1f}s{mode_seg}\n"
f"saved: {disp}{ref_line}\n"
f"prompt={prompt!r}\n"
f"watermark={chosen_watermark} search={chosen_search}"
f"watermark={chosen_watermark} search={chosen_search}{note_line}"
)
@staticmethod
def _normalize_size(
requested: str, *, min_pixels: int = 0, max_pixels: int = 0
) -> tuple[str, str]:
"""把请求尺寸钳进 ARK 面积约束 [min_pixels, max_pixels],保持宽高比。
返回 (chosen_size, note):note 非空表示发生了钳制(用于提示用户 + 记账用真实尺寸)
- 无法解析成 "WxH" / 任一边 <= 0 原样返回,不阻塞(交给 API 自己报错,行为不回退)
- min/max 0 视为不设该侧约束(向后兼容: yaml 无这两个键时不改变行为)
- 面积 < min: s=sqrt(min/area) 等比放大,两边向上取整到 8 的倍数,复核达标(不够再 +8)
- 面积 > max: s=sqrt(max/area) 等比缩小,两边向下取整到 8 的倍数,复核达标(超了再 -8)
- 已在区间内 原样透传,note 为空
"""
raw = (requested or "").strip().lower().replace(" ", "")
parts = raw.split("x")
if len(parts) != 2:
return requested, ""
try:
w, h = int(parts[0]), int(parts[1])
except ValueError:
return requested, ""
if w <= 0 or h <= 0:
return requested, ""
import math
def _round8(v: float, *, up: bool) -> int:
n = math.ceil(v / 8) if up else math.floor(v / 8)
return max(8, n * 8)
area = w * h
if min_pixels > 0 and area < min_pixels:
s = math.sqrt(min_pixels / area)
nw, nh = _round8(w * s, up=True), _round8(h * s, up=True)
# 取整可能把面积压回下限之下,补到达标为止(沿较长边加 8,尽量不破坏比例)
while nw * nh < min_pixels:
if nw >= nh:
nh += 8
else:
nw += 8
chosen = f"{nw}x{nh}"
return chosen, (
f"[note] 请求尺寸 {w}x{h}({area:,}px)低于模型最小面积 {min_pixels:,}px,"
f"已等比放大到 {chosen} 出图。"
)
if max_pixels > 0 and area > max_pixels:
s = math.sqrt(max_pixels / area)
nw, nh = _round8(w * s, up=False), _round8(h * s, up=False)
while nw * nh > max_pixels:
if nw >= nh:
nw -= 8
else:
nh -= 8
chosen = f"{nw}x{nh}"
return chosen, (
f"[note] 请求尺寸 {w}x{h}({area:,}px)超过模型最大面积 {max_pixels:,}px,"
f"已等比缩小到 {chosen} 出图。"
)
return requested, ""
@staticmethod
def _extract_url(resp: dict) -> tuple[str, str]:
"""ark images/generations 响应解析,容忍几种已知 shape: