HRouter

生图脚本示例

这一篇给自己写脚本批量生图的用户。下面的请求格式、参数和结果处理,和 生图工作台 实际发给 HRouter 的完全一样:工作台能出的图,照着写的脚本也能出。各分组能用哪些模型,先看 生图与生视频。

示例都从环境变量读取 Key:

export HROUTER_API_KEY="sk-xxxxx"

工作台的调用流程

每生成一张图,工作台按这个顺序做:

  1. 查模型:GET /v1/models,从返回里挑出生图模型(gpt-image-* 或名字里带 image 的 gemini-*)。
  2. 查参数能力(GPT 模型):GET /v1/images/capabilities。返回 "native_params": true 表示这个 Key 的分组全部走 OpenAI 官方通道,可以使用尺寸、质量、背景等官方参数;返回 false 时只用比例写法。
  3. 发生图请求:没有参考图用 /v1/images/generations(JSON),有参考图用 /v1/images/edits(multipart 表单);Gemini 用 generateContent。每次 n 为 1。
  4. 取图片:GPT 读 data[0].b64_json,Gemini 读 candidates[].content.parts[].inlineData.data,Base64 解码后按真实格式保存。
  5. 失败不自动重试:生图按次计费,接口不会按 Idempotency-Key 去重,重试可能重复扣费。确认没出图再手动重发。
curl https://hrouter.net/v1/images/capabilities -H "Authorization: Bearer $HROUTER_API_KEY"
# {"object":"images.capabilities","native_params":true}

GPT:官方参数写法(native_params 为 true)

和 OpenAI 官方控制台一样直接给参数,提示词原样发送:

参数 可选值 说明
size auto、1024x1024、1536x1024、1024x1536;gpt-image-2 系列另有 2048x2048、2048x1152、1152x2048、3840x2160、2160x3840 或自定义 自定义宽高须是 16 的倍数,最长边不超过 3840,长宽比不超过 3:1,总像素在 655,360 到 8,294,400 之间
quality auto、low、medium、high 质量越高越慢
background auto、opaque、transparent 透明背景不能配 jpeg
output_format png、jpeg、webp 决定保存的文件格式
output_compression 0–100 只对 jpeg、webp 生效
moderation auto、low 内容审核强度

尺寸越大单价越高,档位和价格见 模型广场。4K PNG 解码后可以超过 30 MB,脚本的超时至少留 4 分钟。

GPT:比例写法(native_params 为 false)

非官方通道的分组不保证认这些参数,工作台的做法是:size 固定填 auto,在提示词最前面加一段比例要求,参数只保留 model、prompt、n、size、response_format。比例要求的原文:

【画面比例要求】请生成 16:9 横幅构图,画面宽度大于高度,不要竖幅构图。请优先遵循此比例要求,包括使用参考图或重绘时。

<你的提示词>

正方形写「正方形构图,画面宽度等于高度,不要横幅或竖幅构图」,竖图写「竖幅构图,画面高度大于宽度,不要横幅构图」。这是给模型的要求,实际尺寸以返回的图片为准。

完整 Python 脚本

需要 pip install requests。脚本自动判断官方参数能力,支持参考图,结果按真实格式保存:

import base64, os, sys, requests

BASE = "https://hrouter.net"
KEY = os.environ["HROUTER_API_KEY"]
AUTH = {"Authorization": f"Bearer {KEY}"}


def ratio_prompt(ratio: str, prompt: str) -> str:
    w, h = map(int, ratio.split(":"))
    if w == h:
        shape = "正方形构图,画面宽度等于高度,不要横幅或竖幅构图"
    elif w > h:
        shape = "横幅构图,画面宽度大于高度,不要竖幅构图"
    else:
        shape = "竖幅构图,画面高度大于宽度,不要横幅构图"
    return f"【画面比例要求】请生成 {ratio} {shape}。请优先遵循此比例要求,包括使用参考图或重绘时。\n\n{prompt}"


def native_params() -> bool:
    r = requests.get(f"{BASE}/v1/images/capabilities", headers=AUTH, timeout=10)
    return r.ok and r.json().get("native_params") is True


def gpt_image(prompt, model="gpt-image-2", ratio="1:1", references=(), **options):
    """options 仅在官方通道生效:size、quality、background、output_format、output_compression、moderation"""
    native = native_params()
    fields = {"model": model, "n": 1, "response_format": "b64_json"}
    if native:
        fields.update({"prompt": prompt, "size": options.pop("size", "auto"), **options})
    else:
        fields.update({"prompt": ratio_prompt(ratio, prompt), "size": "auto"})
    if references:
        files = [("image[]", (os.path.basename(p), open(p, "rb"))) for p in references]
        data = {k: str(v) for k, v in fields.items()}
        r = requests.post(f"{BASE}/v1/images/edits", headers=AUTH, data=data, files=files, timeout=300)
    else:
        r = requests.post(f"{BASE}/v1/images/generations", headers=AUTH, json=fields, timeout=300)
    if not r.ok:
        sys.exit(f"生成失败 {r.status_code}: {r.text[:500]}(未自动重试)")
    return base64.b64decode(r.json()["data"][0]["b64_json"])


def save(image: bytes, name: str) -> str:
    ext = "png" if image[:4] == b"\x89PNG" else "jpg" if image[:2] == b"\xff\xd8" else "webp"
    path = f"{name}.{ext}"
    with open(path, "wb") as f:
        f.write(image)
    return path


if __name__ == "__main__":
    # 官方通道:直接给 4K 尺寸和高质量;非官方通道会自动退回比例写法
    img = gpt_image("雪山日出,电影感光影", ratio="16:9",
                    size="3840x2160", quality="high", output_format="png")
    print("已保存", save(img, "snow-mountain"))
    # 参考图改图(最多 4 张,单张 ≤ 8 MB,合计 ≤ 16 MB)
    # img = gpt_image("把背景换成海边", references=["input.png"])

Gemini 生图

Gemini 分组的高清模型(gemini-3.1-flash-image、gemini-3-pro-image*)支持 1K、2K、4K,比例可选 1:1、3:2、2:3、16:9、9:16;其他 Gemini 生图模型只支持默认分辨率和 1:1、3:2、2:3。参考图直接放进 parts:

import base64, os, requests

KEY = os.environ["HROUTER_API_KEY"]


def gemini_image(prompt, model="gemini-3.1-flash-image", ratio="16:9", size="4K", references=()):
    parts = [{"text": prompt}]
    for path in references:
        mime = "image/png" if path.endswith(".png") else "image/jpeg"
        with open(path, "rb") as f:
            parts.append({"inlineData": {"mimeType": mime, "data": base64.b64encode(f.read()).decode()}})
    body = {
        "contents": [{"role": "user", "parts": parts}],
        "generationConfig": {
            "responseModalities": ["TEXT", "IMAGE"],
            "imageConfig": {"aspectRatio": ratio, "imageSize": size},
        },
    }
    r = requests.post(f"https://hrouter.net/v1beta/models/{model}:generateContent",
                      headers={"x-goog-api-key": KEY}, json=body, timeout=300)
    r.raise_for_status()
    for candidate in r.json().get("candidates", []):
        for part in candidate.get("content", {}).get("parts", []):
            image = part.get("inlineData") or part.get("inline_data")
            if image:
                return base64.b64decode(image["data"])
    raise SystemExit("模型没有返回图片,可能被安全策略拒绝,请调整提示词")


with open("mountain.png", "wb") as f:
    f.write(gemini_image("雪山日出,电影感光影"))

Node.js(GPT)

Node 18 以上自带 fetch,不需要装依赖:

import { writeFile } from 'node:fs/promises'

const BASE = 'https://hrouter.net'
const auth = { Authorization: `Bearer ${process.env.HROUTER_API_KEY}` }

const caps = await fetch(`${BASE}/v1/images/capabilities`, { headers: auth }).then((r) => (r.ok ? r.json() : {}))
const body = caps.native_params
  ? { model: 'gpt-image-2', prompt: '雪山日出,电影感光影', n: 1, size: '3840x2160', quality: 'high', output_format: 'png' }
  : { model: 'gpt-image-2', prompt: '【画面比例要求】请生成 16:9 横幅构图,画面宽度大于高度,不要竖幅构图。请优先遵循此比例要求,包括使用参考图或重绘时。\n\n雪山日出,电影感光影', n: 1, size: 'auto', response_format: 'b64_json' }

const res = await fetch(`${BASE}/v1/images/generations`, {
  method: 'POST',
  headers: { ...auth, 'Content-Type': 'application/json' },
  body: JSON.stringify(body),
  signal: AbortSignal.timeout(300_000),
})
if (!res.ok) throw new Error(`生成失败 ${res.status}: ${(await res.text()).slice(0, 500)}(未自动重试)`)
const { data } = await res.json()
await writeFile('snow-mountain.png', Buffer.from(data[0].b64_json, 'base64'))

常见问题

现象 原因 处理
400 提示参数不支持(如 Unknown parameter) 分组不是官方通道,却传了官方参数 先调 /v1/images/capabilities,为 false 时改用比例写法
400 尺寸被拒 自定义尺寸不符合上面的规则 宽高取 16 的倍数,最长边 ≤ 3840,长宽比 ≤ 3:1
请求超时但扣了费 4K 出图慢,客户端先断开了 超时设到 4–5 分钟;先到 使用记录 确认,再决定是否重发
403 / 404 Key 所在分组不支持生图或该模型 见 生图与生视频
429 / 余额不足 并发或额度不够 见 429 限流与额度、余额不足