From 6f27b7cc5aa28823edc1a3e0f4b61f1f929bb0f8 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Fri, 3 Jul 2026 12:41:46 +0800 Subject: [PATCH] =?UTF-8?q?fix(seedream):=20size=20=E9=9D=A2=E7=A7=AF?= =?UTF-8?q?=E9=92=B3=E5=88=B6=E2=80=94=E2=80=94=E4=BF=AE=201920x1080=20?= =?UTF-8?q?=E8=A2=AB=20ARK=20400=20=E6=89=93=E5=9B=9E(bump=200.37.1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 模型自选 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) --- PROGRESS.md | 5 ++- config/media/doubao.yaml | 5 +++ core/__init__.py | 2 +- tools/seedream.py | 74 ++++++++++++++++++++++++++++++++++++++-- 4 files changed, 82 insertions(+), 4 deletions(-) diff --git a/PROGRESS.md b/PROGRESS.md index e0cefa8..b4d92b0 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -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]`——面积 `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.656–0.75in 七个值)+ 并排块顶差 2–12px 的"想对齐没对齐"(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**——兄弟卡片近失对齐(精确几何,2–12px error;底对齐/中心对齐/绘图区内数据柱三类豁免,71 charts 模板回归误报清零)、layout_grid 偏离 2–15px 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 属预期(真缺陷)。 diff --git a/config/media/doubao.yaml b/config/media/doubao.yaml index 11d2946..b0ee03b 100644 --- a/config/media/doubao.yaml +++ b/config/media/doubao.yaml @@ -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 # 出图慢于此判超时 diff --git a/core/__init__.py b/core/__init__.py index d7c1864..7e18a3f 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -1,3 +1,3 @@ # zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。 # 改版本只动这一行。 -__version__ = "0.37.0" +__version__ = "0.37.1" diff --git a/tools/seedream.py b/tools/seedream.py index 7a168c6..4efcff7 100644 --- a/tools/seedream.py +++ b/tools/seedream.py @@ -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: