🤖
AI审核中

READING IN THE WILD · 读一篇手记

博客接入 GPT Image 2.6,如何处理 GIF、连续改图和降级

文章摘要

一次 256×256、5 帧 GIF 的失败,让我重新梳理了博客的生图链路。记录连续改图如何选对输入,GIF 和 SVG 如何原样保存,以及备用接口什么时候可以接手。

给博客的留言助手加上 GPT Image 2.6 以后,联调发现了一个小尺寸 GIF 的问题。测试请求指定生成 256×256、5 帧的动画,OpenAI 返回了 400。当时应用把它归为“服务暂不可用”,菜谱后台还重试了一次。尺寸和帧数原封不动,再等半分钟当然也不会出图。

这次接入涉及文章助手和留言助手。用到的是 GPT Image 2.6 Sunburst,默认 quality=max、complexity=high。接入时,我把请求意图、原格式保存和供应商降级分开处理。这三处都能通过具体的请求和返回结果来验证。

一句“把上一张改一下”应该怎么执行

留言里提到图片,有可能是在问“这张图为什么是空的”,也可能是在要求“把盘子换成蓝色”。如果看到“图片”就直接调用生图,文字问题会被误转成一次昂贵的请求。博客先用文字模型给出结构化判断,再由 Java 代码校验并执行。只有当前留言确实要求生成或编辑时,才会进入图片接口。

下面是一条静态改图的执行计划,省略了审核分数等辅助字段。

{
  "action": "edit",
  "reference_indices": [2],
  "image_options": { "output_format": "png" }
}

这段计划表示编辑候选列表中的第二张图。候选列表由服务端从这次留言的附件和被回复的图片构造,模型只能选编号,不能填一个外部 URL 让后端下载。计划还要通过审核、编号校验和额度检查,才会带着已读取的图片调用 /v1/images/edits。纯文字生成走 /v1/images/generations。

连续改图时,每次回复都以用户所回复的那一轮结果为准。比如先把蓝茶壶改成红色,再回复红茶壶那条要求“加一片柠檬”。第二次编辑应从红茶壶出发。实测中,我核对了后一轮输入与前一轮输出的 SHA-256,并目检确认红色保留、柠檬出现。这两个检查分别回答了“选对了图吗”和“这次编辑做到了吗”。

对局部编辑,接口还有两种引导方式。可以提交画有标记的透明 PNG,也可以提交透明 PNG 蒙版。应用要求引导图和目标原图像素尺寸一致,一次只接收其中一种;提交时把目标原图放在第一张。测试用同一张包含蓝圆与红方块的原图分别走两条路径,两次都把蓝圆改绿并保留了旁边的红方块。模型会重新渲染图像,需要像素级无损保留的场景仍要按输出检查。

按原格式保存 GIF 和 SVG

普通静态图通常用 PNG,照片也可以指定 JPEG 或 WebP。GIF 和 SVG 有独立约束。当前版本允许按文字生成这两种格式,直接编辑 GIF 或 SVG 则返回明确提示。SVG 生图只允许一张。官方接口返回 Base64,存储时按实际字节识别媒体类型,再用匹配的扩展名和 Content-Type 上传,避免浏览器收到名为 GIF 的静态图。

GPT Image 2.6 Sunburst 实测生成的九帧循环动画

这张 GIF 的画布是 512×512,共 9 帧,每帧 200 毫秒,总时长 1.8 秒。发布验收不仅检查图片能打开,也读取了 GIF 帧结构。只检查 HTTP 200 或文件后缀,很难发现图床缩略处理已经把动画压成了首帧。

SVG 的处理比改后缀多一步。服务端用 XML 解析器检查它是否是静态矢量,拒绝脚本、事件属性和外部资源等内容,再按 image/svg+xml 保存。文章助手这次生成的是下面的叶片图标,线上验收读取到的是原始 SVG。

博客文章助手实际生成的青绿叶片 SVG

发布这篇文章时还发现了一层容易漏掉的转换。博客正文默认会给图床链接追加 WebP 缩略参数,源文件保存在图床上和浏览器实际加载到原格式是两回事。本文两张样图使用保留原文件的链接,验收时检查页面里的图片地址,再对照图床返回的 MIME 和字节。

在请求里,quality 控制渲染质量,complexity 控制出图前的规划量。两者互相独立。站内默认用 max 和 high,尺寸仍按请求选择;提高画质不会把一张小图自动变成大图。SVG 还可以指定负责矢量绘制的文字模型,本项目把它固定为 GPT-6.1 Sol。

GIF 的限制发生在拼版阶段

这个案例容易误判,因为 256×256 对单帧来说看着完全合理。但模型需要把多帧排进一张画布再编码。实测 256×256、5 帧时,上游尝试的 1024×512 和 512×1024 拼版都低于它当时的最小像素要求,因此返回 image_generation_user_error,并把 param 指向 size。能否拼版取决于尺寸和帧数的组合,同一个单帧尺寸换一组帧数,校验结果也可能不同。

下面是已经实测成功的一组请求参数。若只在意动画内容,可用自动尺寸,把拼版选择交给接口。

{
  "model": "gpt-image-2.6-sunburst",
  "prompt": "一只橘猫在白色背景上慢慢眨眼,最后回到起始姿态,循环播放,无文字",
  "output_format": "gif",
  "size": "512x512",
  "frame_count": 9,
  "frame_duration_ms": 200,
  "quality": "max",
  "complexity": "high",
  "n": 1
}

问题修复后,应用根据上游的错误类型和参数定位问题,再给用户一条可操作的提示。它不会原样把上游错误正文转给用户,也不会因为某一组尺寸无效就把整条官方线路标记为不可用。回归测试先模拟参数错误,再提交合法请求,验证后续请求不会被冷却拦截;线上则分别验证了合法 GIF 和这组无效参数。

降级先看结果能否等价交付

普通静态图仍可沿用官方接口、Codex 反代和原供应商三层路径。实测时,我用受控额度错误模拟官方不可用,随后请求真实的备用路径。中间一层返回错误,原 2.5 供应商最终成功生成了静态图。

带有明确格式和编辑约束的请求要单独判断。用户指定 GIF 时,一张 PNG 即使清晰,也少了动画。指定蒙版时,普通改图少了区域约束。如果备用接口无法保住这些要求,应用会提示该能力暂时不可用。审核拒绝和用户参数错误也会就此结束,不会换一家继续发同一个请求。

遇到的情况 应用的处理
普通静态图遇到额度或可恢复的上游故障 依次尝试已配置的备用供应商
GIF 尺寸与帧数无法拼版 提示使用自动尺寸或调整尺寸和帧数
SVG、GIF、蒙版等需求无法由备用接口完成 说明这项能力暂不可用
上游已出图,上传对象存储失败 报告生成或存储失败,同一次调用不再向备用模型要一张新图

最后一行很容易被忽略。生成成功之后,存储和模型调用已经是两个阶段。把上传也包进“失败就换一家”的逻辑,图床抖动时会白白多花一次生成费用,还可能把一张完全不同的图交给用户。

上线时怎么验收

这次按用户最终拿到的内容验收,而不是只看接口状态。静态图和动图要能通过外链读回原始媒体,SVG 要用正确的 MIME 返回,连续改图要跟随明确选中的结果。还要至少测一次失败请求,看看它有没有误走重试、误降级,或者把旧图覆盖掉。

场景 已观察到的结果
GIF 与 SVG 原格式 实际读取到 9 帧 GIF;博客文章助手返回并可读取 SVG
连续两轮静态改图 上一轮输出作为下一轮输入,保留红茶壶并加入柠檬
标注与蒙版 两种路径分别修改目标蓝圆,保留相邻的红方块
无效 GIF 参数 用户看到尺寸与帧数提示,服务不会因此进入冷却
官方额度故障注入 普通静态图经过备用链路,由原 2.5 供应商出图

参考资料

1 条评论

如果你觉得文章对你有帮助,那就请作者喝杯咖啡吧

评论区 1

头像
 1 条评论
Nanami