教程
如何通过 HiAPI API 使用 nano-banana:curl、Python 与可运行请求
从首次请求到下载输出的实战全流程
nano-banana 请求的样子
在写代码之前,了解一下 nano-banana 调用在 HiAPI 上的生命周期会很有帮助。每个请求都遵循同样的模式:你发送一个描述所需内容的 JSON 请求体,HiAPI 接受该任务,然后你要么直接拿回已完成的结果,要么拿到一个任务句柄,用它轮询完成状态。
有几个特征定义了它的形态:
- 一个端点,一个 API 密钥。 所有 nano-banana 流量都经过 HiAPI 统一接口。你用 HiAPI 密钥进行认证,永远不直接接触上游。
- JSON 进,JSON 出。 请求体是 JSON。响应也是 JSON,要么包含内联结果,要么包含一个任务 id 加一个状态字段。
- 文本必填,图像可选。 至少你需要发送一个提示词。你可以添加参考图像来引导主体、风格或角色。
- 异步友好。 图像生成可能比典型 HTTP 超时耗时更长,因此 API 支持异步任务,你可以轮询直到它们完成。
请求体的大致结构:
{
"model": "nano-banana",
"prompt": "a description of the image you want",
"image_input": ["https://.../reference.jpg"],
"consistency": "character",
"async": true
}
异步时的响应:
{
"id": "job_abc123",
"status": "queued",
"created_at": "..."
}
当任务完成并且你获取它时,status 会变成 succeeded(或 failed),结果负载会携带输出 URL。请记住这个两阶段模型,因为本教程后面的所有内容都建立在它之上。
一个可运行的 curl 请求
这是能生成图像的最小请求。它假设 HIAPI_API_KEY 已在你的 shell 中导出:
curl -X POST https://api.hiapi.ai/v1/images/generations \
-H "Authorization: Bearer $HIAPI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "nano-banana",
"prompt": "a cozy reading nook by a rain-streaked window, warm lamp light, watercolor style",
"async": true
}'
逐字段说明:
model— 设置为nano-banana。HiAPI 依靠它来路由请求。prompt— 唯一严格必填的创作字段。要具体描述主体、构图和风格;含糊的提示词是输出令人失望的最常见原因。async— 当为true时,API 立即返回一个任务句柄。如果你希望阻塞直到完成,可设为false,但要准备好更长的响应时间。image_input— 可选的参考图像 URL 数组。纯文本生成图像时省略它。consistency— 可选的跨图像角色或主体控制;详见下文。
如果你不确定当前版本支持哪些可选参数,在添加字段之前先查看 HiAPI 上该模型的文档页面——未知字段可能根据参数被忽略或拒绝。
运行命令并检查 JSON。假设 async 为 true,你会拿回一个任务对象而不是图像。保存 id;那是你下一步的句柄。
将请求移植到 Python
curl 调用可以直接翻译。这里的目标不仅是写一个一次性脚本,而是一个可以放入流水线的可复用封装。
import os
import time
import requests
BASE = "https://api.hiapi.ai"
HEADERS = {
"Authorization": f"Bearer {os.environ['HIAPI_API_KEY']}",
"Content-Type": "application/json",
}
def create_image(prompt, image_input=None, consistency=None, async_job=True):
body = {"model": "nano-banana", "prompt": prompt, "async": async_job}
if image_input:
body["image_input"] = image_input
if consistency:
body["consistency"] = consistency
resp = requests.post(
f"{BASE}/v1/images/generations",
headers=HEADERS,
json=body,
timeout=60,
)
if resp.status_code >= 400:
raise RuntimeError(f"create failed: {resp.status_code} {resp.text}")
return resp.json()
以下几点让它稳健而不仅仅是能用:
- 显式超时。 始终传入一个;"永远等待"的默认值会把一个暂时性网络问题变成挂起的进程。
- 解析前检查状态。 对错误响应抛出异常,这样坏请求就不会悄悄返回一个你随后去索引的 JSON 请求体。
- 省略可选字段,而不是置空。 与其发送
"image_input": null,不如只在有值时添加该键。这能让日志更易读,并避免null表示"无"还是"未设置"的歧义。
把它包装成函数也让重试变得简单:因为函数在失败时抛出异常,调用方可以在不触碰请求代码的情况下将其包在重试循环中。一旦你从单张图像转向批量处理,这种分离就会带来回报。
发送文本和图像输入
文本生成图像是简单的情况。nano-banana 有趣的部分在于在单个请求中结合提示词与参考图像。你将 image_input 作为 URL 数组传入,提示词描述如何处理它们。
result = create_image(
prompt="the person in the first image standing in a Tokyo alley at night, neon reflections, cinematic",
image_input=["https://example.com/portrait.jpg"],
)
混合模态的实用建议:
- 明确角色。 如果你发送多张图像,在提示词中说明哪张提供主体、哪张提供风格、哪张提供背景。"组合这些图像"是含糊的;"使用图像一中的脸和图像二中的光照"则不是。
- 使用可达且稳定的 URL。 API 必须能够获取图像。几分钟后过期的签名 URL 对实时调用没问题,但如果你稍后重试排队的任务就会失效。
- 优先使用干净的参考。 清晰、光线良好的肖像比繁忙的合影能产生更忠实的人物相似度。垃圾进垃圾出在图像条件化上比在文本上更明显。
- 首先迭代提示词,而不是参考图像。 如果相似度不对,先调整你描述主体的方式,再更换源图像——这通常是更省事的改变。
当参考图包含你打算在多次生成中复用的一个人物时,你就进入了角色一致性的领域,也就是下一节。
角色一致性选项
角色一致性回答一个具体需求:生成多张看似以同一个角色或主体为特色的图像。nano-banana 将其暴露为一个可选控制,而不是单独的模型。
在高层面上,权衡大致如下:
- 无一致性控制。 最适合一次性图像。快速简单,但如果重新生成,角色可能会漂移。用于背景、物体和独立插画。
- 基于参考的一致性。 你提供一张或多张角色图像并设置一致性选项,让模型锚定它们。这是故事板、漫画和产品序列的正确选择,因为身份必须在帧间保持。代价是你必须首先有一张好的参考图像。
- 文本描述的一致性。 你在每个提示词中详细描述角色——发色、服装、体型、显著特征——而不使用参考。当你没有源照片时有用,但它要求纪律:将描述放在可复用的常量中,以免在调用之间漂移。
一个系列的务实工作流:
- 在没有一致性的情况下生成第一帧,迭代提示词直到角色看起来正确。
- 将该输出用作你的参考图像。
- 开启一致性控制并生成后续帧,保持参考固定。
- 如果角色仍然漂移,缩短提示词,让参考做更多工作——与参考矛盾的长提示词会让模型困惑。
一致性是一个旋钮,不是保证。即使在调优良好的流水线中,也要预期审查输出并重新生成一小部分帧。
轮询异步任务
当你以 async: true 提交时,你会拿回一个任务 id。轮询很直接,但细节比看起来更重要。
一个最小且正确的轮询器:
def wait_for_job(job_id, timeout=300, initial_delay=2, max_delay=15):
deadline = time.time() + timeout
delay = initial_delay
while time.time() < deadline:
resp = requests.get(
f"{BASE}/v1/images/generations/{job_id}",
headers=HEADERS,
timeout=30,
)
resp.raise_for_status()
data = resp.json()
status = data.get("status")
if status == "succeeded":
return data
if status in ("failed", "cancelled"):
raise RuntimeError(f"job {job_id} ended as {status}: {data}")
time.sleep(delay)
delay = min(delay * 1.5, max_delay)
raise TimeoutError(f"job {job_id} did not finish within {timeout}s")
每项选择的理由:
- 带上限的近似指数退避。 从几秒开始,乘以 1.5 直到上限。对三十秒的任务固定一秒轮询会浪费几十个请求;固定三十秒轮询会给快速任务增加不必要的延迟。
- 硬性截止时间。 没有它,卡住的任务会无限期阻塞你的工作进程。宁可大声失败。
- 终态处理。 将
failed和cancelled视为异常,而不是继续轮询的状态。处于失败状态的任务在下次轮询时永远不会成功。 - 每次迭代都发起新的 GET。 不要缓存第一个响应并假设它会更新。
对于批处理工作,先提交所有任务,收集 id,然后在循环中轮询它们——这样可以让生成时间跨任务重叠,而不是串行化。
下载并验证输出
任务成功后,结果负载包含一个输出 URL。下载是一个普通的 GET,但在检查之前你不应假设这些字节就是图像。
def download_image(url, path):
with requests.get(url, stream=True, timeout=60) as r:
r.raise_for_status()
content_type = r.headers.get("Content-Type", "")
if not content_type.startswith("image/"):
raise RuntimeError(f"unexpected content type: {content_type}")
with open(path, "wb") as f:
for chunk in r.iter_content(chunk_size=8192):
f.write(chunk)
return path
每次下载都值得做的合理性检查:
- Content-Type。 如果你要求图像却得到
application/json或text/html,你实际收到的是一个错误页面。检查头部可以在你写入损坏文件之前捕获这一点。 - 流式传输。 流式写入磁盘而不是将整个响应体加载到内存;这在你并行下载时能保持内存平稳。
- 大小非零。 零字节文件意味着即使 HTTP 状态正常也出了问题。
- 在日志中记录校验和或字节数。 将其与任务 id 一起记录,以便你能将坏文件追溯到特定的生成。
输出 URL 可能是临时的。如果你打算保留结果,应在同一次运行中下载到你自己的存储,而不是保存链接供以后使用。
常见故障点
大多数问题都归入几个类别。在假设模型行为异常之前,先检查这些。
认证与路由
- 缺失或格式错误的
Authorization头会返回认证错误,而不是图像。确认头部读作Bearer <key>,中间有一个空格。 - 发送错误的
model值会路由到错误的地方或直接失败。它应该正好是nano-banana。
请求体问题
- 含糊的提示词。 "输出差"的最常见单一原因。在改变其他任何东西之前,先添加构图、光照和风格细节。
- 不可达的图像 URL。 返回 404、需要认证或指向本地路径的参考 URL 会失败或被静默忽略。自己先获取一次 URL 以确认它能工作。
- 发送
async: false且提示词很长 并触及客户端超时。切换到异步并轮询,而不是提高超时。
任务处理
- 轮询一个从未存在的任务。 任务 id 中的拼写错误会产生看起来像失败的未找到错误;记录你提交的 id。
- 将
failed当作仍在运行。 始终在终态退出轮询循环。 - 没有截止时间。 批处理任务中代价最高的 bug:一张卡住的图像拖住整个运行。
输出处理
- 不检查 Content-Type。 如上所述,以
.png扩展名保存的错误页面稍后看起来就像损坏的图像。 - 在生成数小时后依赖过期的结果 URL。 及时下载。
按认证、请求体、任务生命周期、下载的顺序逐一排查,大多数故障几分钟内就能解决。