Nano Banana 2.1 相比上一版,变化集中在三件事上:支持原生 4K 直出,不再依赖"先出小图再放大"的补救流程;画面内中文文字的渲染稳定性提升;批量生成任务的接口与返回结构更完整。这三件事恰好也是上手时最容易卡住的地方——4K 请求被静默降级、中文变成错字或方块、批量跑到一半断掉。
下面按排错流程走一遍:先看清现象,再按概率排查,最后给批量脚本和兜底方案。文中命令可以直接照抄,涉及模型标识、接口路径、参数名的地方,以官方文档当前版本为准。
报错现象
现象一:请求 4K,拿到的是小图
触发操作:在请求里写 "size": "3840x2160" 或把宽高分别写成 width/height 字段。
报错或表现:
- 返回体里的
size/width/height字段与请求值不一致,比如请求 3840×2160,返回还是 1024×1024 一类的小尺寸; - 或者直接报错,文案类似
invalid size、size not supported、image_size must be one of ...(具体文案各平台不同); - 请求成功、图也能下,但下载到本地后用工具查,真实像素宽度不足 3840。
影响范围:需要印刷、大屏、电商主图的场景直接不可用;即使强行拉伸,文字边缘也会发虚。
现象二:中文文字渲染出错
触发操作:提示词里写了"画面中央横排四个字:第二杯半价"这类要求。
报错或表现:
- 出图上的字变成形近字("半价"写成"半介"),或笔画粘连、多一笔少一笔;
- 出现提示词里根本没写的字,或本该 4 个字变成 5 个字;
- 竖排被渲染成横排,或者行序从右往左、从左往右反了;
- 上一版能渲染正确的双字词,新版换了一种更漂亮的字体风格后字形反而走样(版式变好,错字率没同步变好)。
影响范围:电商海报、公众号封面、菜单、PPT 配图这类"图上必须带字"的场景,出图直接不可用,而且不容易一眼发现——错一个字,整张图就废了。
现象三:批量任务部分失败
触发操作:脚本一次提交几十到几百条提示词。
报错或表现:
- 返回里约五分之一是失败态,错误码常见 429(限流)、500/502/503/504(服务端抖动)、超时;
- 串行脚本跑到第 20 多张后卡住不动,不报错也不返回;
- 重试之后出现重复图、顺序和提示词对不上、文件名互相覆盖。
影响范围:批量出图是海报、素材库、多语言版本这类工作的主力用法,失败率上去之后,人工补图的时间会超过写脚本省下的时间。
可能原因
按出现概率从高到低:
1. 尺寸参数写法不对,或 4K 走的是独立参数/独立模型标识。很多平台并不是把 width 写大就能出大图,4K 往往对应另一个参数名、另一个模型标识,或者需要显式开启。概率靠前。
2. 客户端超时设置太短。4K 出图耗时明显长于低分辨率,HTTP 库默认 30 秒或 60 秒的读超时,很容易在服务端还在算的时候就被本地掐断。
3. 中文提示词写法问题。把要渲染的文字混在长句描述里、没有用引号或占位符隔离、字数太多、中英混排、同时要求字体风格和版式。
4. 并发太高触发限流。一次性并发几十个请求,返回 429。
5. 后处理环节把分辨率吃掉。保存时用了压缩参数、走了图片库的 resize、图片经过聊天软件或网盘中转被二次压缩。
6. 平台侧对 4K 或大尺寸有单独配额。账号权限、额度、并发上限未开。
7. 字体气质与提示词冲突。要求"毛笔""手写""书法"时,字形更容易崩。
8. 本地环境问题。依赖库版本、代理、证书、磁盘空间。
9. 读到了旧缓存或旧返回。文件名复用、本地缓存目录没清。
逐条排查与解决
第 1 条:确认尺寸参数到底有没有生效
先跑一个最小请求,把返回体完整打出来看字段:
```bash
curl -s -X POST "$API_BASE/v1/images/generations" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"'"$MODEL_ID"'","prompt":"一只橘猫坐在窗台上","size":"3840x2160","n":1}' \
| python -c "import sys,json;print(json.dumps(json.load(sys.stdin),ensure_ascii=False)[:800])"
```
判断方法:返回里如果有 size、width、height 字段,且和请求值不一致,就是被降级了。这时翻官方文档的"图片尺寸"或"参数说明"章节,确认 4K 对应的是哪个参数名、是否要换模型标识,以官方文档当前版本为准。
再验证真实像素,不看返回字段看文件本身:
```bash
file out.png # 有些格式会直接带尺寸信息
identify out.png # 装了 ImageMagick 时用这条
```
```python
from PIL import Image
im = Image.open("out.png")
print(im.size, im.mode) # 期望宽度 3840 左右
```
如果返回字段写的是 4K,但 Image.open().size 不是,问题出在下游:保存、传输或压缩环节。
第 2 条:确认是不是客户端超时
curl 加长超时:
```bash
curl -s --max-time 600 -X POST "$API_BASE/v1/images/generations" \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
-d '{"model":"'"$MODEL_ID"'","prompt":"测试超时","size":"3840x2160","n":1}' \
-o out.json -w "http=%{http_code} time=%{time_total}\n"
```
Python 侧把超时拆成连接超时和读超时:
```python
requests.post(url, headers=h, json=body, timeout=(10, 300))
```
判断方法:日志里出现 Read timed out、TimeoutError、ConnectionResetError,而平台后台任务列表里其实有成功记录——基本可以确定是本地超时太短,而不是服务端没出图。把读超时改到 300 秒以上再试一次即可确认。
第 3 条:把中文提示词的结构固定下来
可复用的写法是:场景描述 + 文字内容用引号单独拎出 + 明确数量和排布 + 明确"不出现其他文字"。
正例:
```
一张奶茶店促销海报。画面中央横排四个白色加粗黑体汉字,文字内容严格为:"第二杯半价"。
除这四个字以外,画面内不出现任何其他文字、字母或数字。背景为暖橙色渐变,右下角留白。
```
反例(同一需求,但约束被埋进长句):
```
做一个看起来像促销的、写着第二杯半价的感觉很热闹的奶茶海报,字要大一点酷一点。
```
判断方法:固定随机种子(平台支持时),只改文字部分,同一提示词连跑 5 次。
- 每次错的位置都不一样 → 提示词约束不够,按上面的结构改写;
- 每次都错同一个字 → 这个字本身难渲染,换成同义表达,或者把该字拆成后期叠字处理;
- 只有字数超过十来个字时才错 → 超出模型稳定区间,改成"先生成无字底图,再后期排版"。
和上一版做中文字形与画质对比,用同一套方法
不要凭印象说"新版更好",按同一条件出对图:
1. 同一提示词、同一种子、同一尺寸,分别用上一版和新版各出一张;
2. 把两张图并排放在一起,缩放到同一像素宽度再看,避免"大图显得清楚"的错觉;
3. 文字区单独裁出来放大 2~3 倍,看笔画边缘是连贯还是有毛刺、粘连;
4. 用开源 OCR(如 tesseract,中文需要单独装语言包)回读图上文字,和预期串逐字比对;
5. 把结果记进一张表,逐条填,不填数字不下结论:
| 检查项 | 上一版 | 新版 |
|---|---|---|
| 4 字短词正确率(跑 10 次) | ||
| 12 字长句正确率(跑 10 次) | ||
| 竖排是否按要求 | ||
| 笔画边缘(放大后主观评价) | ||
| 实际输出像素 |
经验上,新版在笔画连贯性和版式规整度上通常更稳,但长句和生僻字仍会出错;4K 直出相比"低分辨率再放大",边缘和细节保留更多,这一点在放大对比时能看出来。具体结论用你自己的表格数据说话。
第 4 条:限流与批量重试
判断方法:错误码 429,或响应头里带 Retry-After。把并发降到 2~4 起步,加上指数退避和随机抖动。
一个可以直接改的批量脚本骨架,包含重试、断点续跑和结果记录:
```python
import os, json, time, random, pathlib
from concurrent.futures import ThreadPoolExecutor
import requests
API_BASE = os.environ["API_BASE"]
API_KEY = os.environ["API_KEY"]
MODEL_ID = os.environ["MODEL_ID"] # 以官方文档当前版本为准
OUT = pathlib.Path("out"); OUT.mkdir(exist_ok=True)
LOG = pathlib.Path("log.jsonl")
def gen(prompt, size="3840x2160"):
r = requests.post(
f"{API_BASE}/v1/images/generations",
headers={"Authorization": f"Bearer {API_KEY}"},
json={"model": MODEL_ID, "prompt": prompt, "size": size, "n": 1},
timeout=(10, 300),
)
r.raise_for_status()
return r.json()
def gen_with_retry(prompt, tries=5):
last = None
for i in range(tries):
try:
return gen(prompt)
except requests.HTTPError as e:
code = e.response.status_code if e.response is not None else 0
last = f"HTTP {code}"
if code in (408, 429, 500, 502, 503, 504):
time.sleep(min(60, 2 ** i) + random.uniform(0, 1))
continue
raise
except (requests.Timeout, requests.ConnectionError) as e:
last = type(e).__name__
time.sleep(min(60, 2 ** i) + random.uniform(0, 1))
raise RuntimeError(f"重试耗尽: {last}")
def done_ids():
if not LOG.exists():
return set()
ok = set()
for line in LOG.read_text(encoding="utf-8").splitlines():
try:
rec = json.loads(line)
except json.JSONDecodeError:
continue
if rec.get("status") == "ok":
ok.add(rec["id"])
return ok
def run(task):
tid, prompt = task["id"], task["prompt"]
if tid in done_ids():
return
try:
data = gen_with_retry(prompt)
不同平台返回结构不同,按官方文档取图片内容或 URL
img = data["data"][0]
path = OUT / f"{tid}.png"
if "b64_json" in img:
import base64
path.write_bytes(base64.b64decode(img["b64_json"]))
else:
path.write_bytes(requests.get(img["url"], timeout=(10, 300)).content)
rec = {"id": tid, "status": "ok", "file": str(path), "prompt": prompt}
except Exception as e:
rec = {"id": tid, "status": "fail", "error": repr(e), "prompt": prompt}
with LOG.open("a", encoding="utf-8") as f:
f.write(json.dumps(rec, ensure_ascii=False) + "\n")
tasks = json.loads(pathlib.Path("tasks.json").read_text(encoding="utf-8"))
with ThreadPoolExecutor(max_workers=3) as pool: # 先 3,稳了再往上加
list(pool.map(run, tasks))
```
几个关键点:
- 幂等:文件名用任务 id,不用序号,重跑不会互相覆盖;
- 断点续跑:
done_ids()每次启动重读日志,已成功的不再重复提交; - 失败单独留痕:
status=fail的记录留在日志里,最后单独捞出来重试; - 并发从小到大:
max_workers从 2 或 3 开始,观察 429 出现频率再决定加不加。
判断方法:跑完后统计日志里 ok 和 fail 的比例。如果失败集中在某个时间段,多半是限流;如果失败分散且错误各不相同,多半是单条提示词或尺寸参数的问题。
第 5 条:检查后处理是否吃掉分辨率
绕开所有中间环节,直接把响应原始字节写盘:
```python
r = requests.post(url, headers=h, json=body, timeout=(10, 300))
open("raw.png", "wb").write(r.content)
```
然后 identify raw.png 看尺寸。如果这里是对的,但最终交付的文件不对,问题就在保存参数、resize 或传输环节,逐个关掉再验。
第 6 条:确认账号侧的大尺寸配额
先发一个低分辨率请求。低分辨率也失败,那是账号、密钥或网络问题;只有 4K 失败,就去看文档里有没有单独的开关、白名单或额度限制,以官方页面为准。
第 7 条:换个字体气质再试
把"毛笔""手写""书法"改成"黑体""无衬线",把装饰性要求降下来,看错字率是否下降。如果文字区域始终崩,改用"无字底图 + 后期叠字"。
第 8 条:本地环境
```bash
python -c "import sys, requests; print(sys.version); print(requests.__version__)"
df -h .
env | grep -i proxy
```
判断方法:磁盘写满、代理拦截 TLS、依赖库版本过旧,都会表现为"接口通但不返回完整数据"。
第 9 条:缓存与旧返回
换新文件名、加时间戳、清掉本地缓存目录再跑一次。这一步几秒钟就能排除,放在最后做。
都不管用时的兜底方案
方案一:降一级尺寸出图,再做放大。 把尺寸降到 2K 级别稳定出图,再用常见的图像放大工具做一次超分。画质不如原生 4K 直出,但至少能交付,适合赶时间的场景。
方案二:把文字从画面里拆出来。 提示词里只描述底图,明确"画面内不出现任何文字",然后用排版工具叠字。PIL 版本:
```python
from PIL import Image, ImageDraw, ImageFont
im = Image.open("bg.png").convert("RGB")
d = ImageDraw.Draw(im)
字体路径按本机实际字体改:Windows 常见 msyh.ttc,macOS 常见 PingFang.ttc,
Linux 常见 Noto Sans CJK
font = ImageFont.truetype("/path/to/NotoSansCJK-Bold.otf", 140)
text = "第二杯半价"
w = d.textlength(text, font=font)
d.text(((im.width - w) / 2, int(im.height * 0.42)), text,
font=font, fill=(255, 255, 255))
im.save("poster.png")
```
这样文字 100% 可控,模型只负责底图。ImageMagick 的 convert -annotate、Figma、PS 都能做同样的事。
方案三:换异步任务模式。 如果同步接口在 4K 下总超时,看文档里有没有"提交任务 + 轮询状态"的异步路径,以官方文档当前版本为准。异步模式对长耗时的容忍度更好。
方案四:从最小可复现例子重建。 关掉并发、关掉后处理、关掉所有可选参数,只留模型标识、提示词、尺寸三个字段,跑通一次,再一项一项往回加。哪一项加回去就坏,问题就在那一项。
如何预防再次发生
建一套回归测试集。 固定 5 条左右提示词:四字短词、十二字长句、竖排、中英混排、纯图形不带字。每次升级模型或改参数,跑一遍,人工过一眼字。字数少、结构固定的用例先跑,能第一时间发现字形退化。
把验收标准写死在脚本里。 三条硬指标:输出真实像素不低于请求值;OCR 回读的文字与预期完全一致;批量失败率低于你自己定的阈值(比如 5%)。不达标就不交付,不靠肉眼扫一遍。
参数集中管理。 把模型标识、尺寸、超时、并发数、重试次数放进一个配置文件或环境变量,改一处全局生效,避免脚本里散落着写死的尺寸字符串。
日志留全。 每条任务记录:任务 id、提示词、模型标识、尺寸、返回状态、错误信息、耗时。出问题时,日志能直接告诉你是限流还是参数问题,不用重跑一遍。
批量任务做幂等和断点续跑。 文件名用任务 id,成功的任务不重复提交,失败的任务单独放一个目录方便重试。跑长任务前先确认磁盘空间够。
中文字数控制在稳定区间。 画面内文字控制在几个字到十几个字;超过这个量级,直接走"无字底图 + 后期叠字",比反复重试省钱也省时间。
关注官方变更说明。 模型版本、参数名、尺寸支持范围都可能调整,上线前看一眼官方文档当前版本,比事后排查便宜得多。
