设计实践

Ideogram 海报生成:文字排版与 API 费用预估

Ideogram 的强项是图片里的文字。这篇做一个海报生成命令行:先用 dryrun=true 报价(官方说明:只校验和计价,不生成、不存储、不计费),确认后加 --yes 再真正生成。 生成结果的链接是临时的,所以拿到后立刻下载。isimagesafe=false 的图没有 url,记录下来并跳过。最后写出 meta.json,里面有 seed 和 mag

BINBHO · 图文教程

Ideogram 的强项是图片里的文字。这篇做一个海报生成命令行:先用 dry_run=true 报价(官方说明:只校验和计价,不生成、不存储、不计费),确认后加 --yes 再真正生成。 生成结果的链接是临时的,所以拿到后立刻下载。is_image_safe=false 的图没有 url,记录下来并跳过。最后写出 meta.json,里面有 seed 和 magic prompt 改写后的最终提示词。

先看清成品(在 box 上对着模拟服务器运行)

  • pytest 3 项通过。
  • 报价那一步没有计费:模拟器的计费计数为 0。
  • 非法分辨率在本地就被拦下。
  • 不安全的图被跳过。

合规提醒:IDEOGRAM_API_KEY 只放在服务器端。海报里的文字、品牌和人物要有使用权:不要仿冒他人商标或冒充真人;需要时可以开启 enable_copyright_detection,被标记的图会返回 is_image_safe: false。在中国境内公开发布生成内容,要遵守《生成式人工智能服务管理暂行办法》,并按《人工智能生成合成内容标识办法》加上“AI 生成”标识。Ideogram API 按次计费,本文不列价格;需要价格时用 dry_run=true 实时询价,示例中的金额一律写作 x.xx。

运行说明:box 上没有 Ideogram API Key,所以没有真正生成过图片。生成流程跑在我自己写的模拟服务器 fake_ideogram.py 上,图片是程序画的文字海报。用错误的 Key 请求真实接口确实运行过,返回 401 {"message": "Access denied. Please verify your API Token is valid."}。

做完之后你会得到什么?

成果 说明
ideo_poster.py 报价 → 确认 → 生成 → 下载 → 写出 meta.json
fake_ideogram.py 本地模拟服务器(127.0.0.1:18501),根据提示词引号里的文字画海报
test_poster.py 测试报价不计费、跳过不安全图片、分辨率和 Key 报错

需要准备哪些环境?

项目 要求
Python 3.9 以上,pip install requests flask pillow pytest
真实使用 在 Ideogram 开发者后台创建 API Key,并充值
环境变量 IDEOGRAM_API_KEY

接口要点(来自官方文档)

  • 地址:POST https://api.ideogram.ai/v2/image/generate/ideogram-4,请求头 Api-Key,请求体是 JSON。
  • 常用字段:
    • prompt、num_images、seed、resolution(枚举值,代码里收录了官方列出的全部值)
    • rendering_speed:turbo、default 或 quality
    • magic_prompt:auto、on 或 off
  • 返回:generation_id、seed、data[];data 的每一项包含 url、prompt、resolution、seed 和 is_image_safe。
  • 报错:402/429 的返回体里有 reject_reason,取值为 insufficient_funds、daily_limit、inflight_limit 等。
  • 报价:PriceQuote 的具体字段本文没有查到,所以代码不解析它,原样打印。

文件结构

50-ideogram-A-src/
├── ideo_poster.py
├── fake_ideogram.py
└── test_poster.py

第 1 步:代码

"""Ideogram 4.0 海报生成:先 dry_run 报价(不生成、不计费),确认后再生成;
结果链接是临时的,生成完立刻下载;is_image_safe=false 的图没有 url,记录后跳过。"""
import argparse, json, os, pathlib, sys
import requests

BASE = os.environ.get("IDEOGRAM_API_BASE") or "https://api.ideogram.ai"   # 空字符串也回落到默认值
URL = BASE + "/v2/image/generate/ideogram-4"
UA = "binbho-tutorial-ideogram/1.0"
# 官方文档 ideogram-4 的 resolution 枚举(2026-10 抓取)
RESOLUTIONS = set("""2048x2048 1440x2880 2880x1440 1664x2496 2496x1664 1792x2240 2240x1792 1440x2560 2560x1440
1600x2560 2560x1600 1728x2304 2304x1728 1296x3168 3168x1296 1152x2944 2944x1152 1248x3328 3328x1248 1280x3072
3072x1280 1024x3072 3072x1024 1024x1024 896x1120 1120x896 864x1152 1152x864 832x1248 1248x832 800x1280 1280x800
720x1280 1280x720 720x1440 1440x720 512x1536 1536x512""".split())


def build(prompt, resolution=None, n=1, seed=None, speed="default", magic="auto"):
    if resolution and resolution not in RESOLUTIONS:
        raise SystemExit(f"不支持的 resolution:{resolution}")
    body = {"prompt": prompt, "num_images": n, "rendering_speed": speed, "magic_prompt": magic}
    if resolution: body["resolution"] = resolution
    if seed is not None: body["seed"] = seed
    return body


def post(body, dry_run):
    key = os.environ["IDEOGRAM_API_KEY"]
    r = requests.post(URL, params={"dry_run": "true"} if dry_run else None, json=body, timeout=180,
                      headers={"Api-Key": key, "User-Agent": UA})
    if r.status_code >= 400:
        try: d = r.json()
        except ValueError: d = {"raw": r.text[:200]}
        reason = d.get("reject_reason", "")
        raise SystemExit(f"HTTP {r.status_code} {reason} {d.get('error') or d.get('message') or d}")
    return r.json()


def save(resp, outdir):
    out = pathlib.Path(outdir); out.mkdir(parents=True, exist_ok=True)
    meta = {"generation_id": resp["generation_id"], "images": []}
    for i, im in enumerate(resp.get("data", [])):
        rec = {k: im.get(k) for k in ("seed", "resolution", "is_image_safe", "prompt")}
        if im.get("is_image_safe") and im.get("url"):
            p = out / f"poster_{i}_seed{im['seed']}.png"
            p.write_bytes(requests.get(im["url"], timeout=120).content)   # 链接会过期:立刻下载
            rec["file"] = str(p); print(f"✓ {p}  seed={im['seed']}  {im['resolution']}")
        else:
            print(f"✗ 第 {i} 张未通过安全检查,没有下载链接(seed={im['seed']})")
        meta["images"].append(rec)
    (out / "meta.json").write_text(json.dumps(meta, ensure_ascii=False, indent=2))
    print("最终提示词(magic prompt 改写后):", (resp.get("data") or [{}])[0].get("prompt", "")[:80])
    return meta


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("prompt"); ap.add_argument("--resolution"); ap.add_argument("-n", type=int, default=1)
    ap.add_argument("--seed", type=int); ap.add_argument("--speed", default="default", choices=["turbo", "default", "quality"])
    ap.add_argument("--magic", default="auto", choices=["auto", "on", "off"])
    ap.add_argument("--yes", action="store_true", help="看过报价后加上它才真正生成")
    ap.add_argument("--out", default="outputs")
    a = ap.parse_args()
    body = build(a.prompt, a.resolution, a.n, a.seed, a.speed, a.magic)
    quote = post(body, dry_run=True)
    print("报价(dry_run,未生成、未计费):", json.dumps(quote, ensure_ascii=False))
    if not a.yes:
        print("确认后加 --yes 重新运行。"); return
    save(post(body, dry_run=False), a.out)


if __name__ == "__main__":
    main()

第 2 步:模拟服务器

"""本地模拟服务器(自己写的,不是 Ideogram 的)。按提示词里引号中的文字画一张“海报”。
为了给 B 篇的 OCR 检查提供素材,seed 能被 3 整除时故意把第一个字画错,模拟文字渲染出错。
dry_run 返回的报价结构是模拟器自定的,真实的 PriceQuote 字段以官方文档为准。"""
import io, os, re, random, uuid
from flask import Flask, request, jsonify, send_file
from PIL import Image, ImageDraw, ImageFont

app = Flask(__name__)
BASE = os.environ.get("FAKE_BASE", "http://127.0.0.1:18501")
FONT = "/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc"
IMGS, CALLS = {}, {"billed": 0}
WRONG = {"秋": "伙", "夜": "液", "市": "巿", "咖": "加"}


@app.post("/v2/image/generate/ideogram-4")
def gen():
    if request.headers.get("Api-Key") != "demo-key":
        return jsonify(message="Access denied. Please verify your API Token is valid."), 401
    b = request.get_json()
    n = int(b.get("num_images", 1))
    if request.args.get("dry_run") == "true":
        return jsonify(mock_quote=True, num_images=n, amount="x.xx", note="模拟报价")
    CALLS["billed"] += 1
    seed0 = b.get("seed", random.randint(1, 10**6))
    gid = uuid.uuid4().hex[:22]
    data = []
    for i in range(n):
        sd = seed0 + i
        unsafe = "UNSAFE" in b["prompt"] and i == 0
        IMGS[f"{gid}_{i}"] = (b["prompt"], sd, b.get("resolution", "1024x1024"))
        data.append(dict(prompt="(模拟 magic prompt) " + b["prompt"], resolution=b.get("resolution", "1024x1024"),
                         is_image_safe=not unsafe, seed=sd, url="" if unsafe else f"{BASE}/img/{gid}_{i}.png"))
    return jsonify(generation_id=gid, seed=seed0, data=data)


@app.get("/img/<k>.png")
def img(k):
    prompt, seed, res = IMGS[k]
    w, h = (int(x) // 2 for x in res.split("x"))
    text = (re.findall(r"[“\"](.+?)[”\"]", prompt) or ["海报"])[0]
    if seed % 3 == 0 and text[0] in WRONG:
        text = WRONG[text[0]] + text[1:]
    rnd = random.Random(seed)
    im = Image.new("RGB", (w, h), tuple(rnd.randint(150, 240) for _ in range(3)))
    d = ImageDraw.Draw(im)
    size = max(24, int(w * 0.8 / max(len(text), 1)))
    f = ImageFont.truetype(FONT, size)
    tw = d.textlength(text, font=f)
    d.text(((w - tw) / 2, h * 0.38), text, fill=(30, 30, 30), font=f)
    buf = io.BytesIO(); im.save(buf, "PNG"); buf.seek(0)
    return send_file(buf, mimetype="image/png")


if __name__ == "__main__":
    app.run("127.0.0.1", 18501)

第 3 步:运行

$ python fake_ideogram.py &
$ export IDEOGRAM_API_BASE=http://127.0.0.1:18501 IDEOGRAM_API_KEY=demo-key
$ python ideo_poster.py '复古风格海报,正中央醒目的大字标题“秋日夜市”,暖橙色调' --resolution 1024x1024 -n 3 --seed 41
报价(dry_run,未生成、未计费): {"amount": "x.xx", "mock_quote": true, "note": "模拟报价", "num_images": 3}
确认后加 --yes 重新运行。

$ python ideo_poster.py '复古风格海报,正中央醒目的大字标题“秋日夜市”,暖橙色调' --resolution 1024x1024 -n 3 --seed 41 --yes
报价(dry_run,未生成、未计费): {"amount": "x.xx", "mock_quote": true, "note": "模拟报价", "num_images": 3}
✓ outputs/poster_0_seed41.png  seed=41  1024x1024
✓ outputs/poster_1_seed42.png  seed=42  1024x1024
✓ outputs/poster_2_seed43.png  seed=43  1024x1024
最终提示词(magic prompt 改写后): (模拟 magic prompt) 复古风格海报,正中央醒目的大字标题“秋日夜市”,暖橙色调

$ python ideo_poster.py 'x' --resolution 1000x1000
不支持的 resolution:1000x1000

$ IDEOGRAM_API_BASE= IDEOGRAM_API_KEY=not-a-real-key python ideo_poster.py '测试' --resolution 1024x1024   # 真实接口
HTTP 401  Access denied. Please verify your API Token is valid.

$ python -m pytest -q
3 passed in 1.65s

检查点:

  • outputs/meta.json 里每张图都有 seed。
  • 文字类海报建议用 --magic off,这样你写的文字不会被改写。

调试示例

IDEOGRAM_API_BASE= 设为空之后,程序崩溃了。 我想把请求切回真实接口,就把这个变量设成了空字符串,结果报错 MissingSchema: Invalid URL '/v2/image/generate/ideogram-4'。原因是 os.environ.get("X", 默认值) 只有在变量不存在时才会返回默认值,变量存在但为空时返回的是 ""。改成 os.environ.get("X") or 默认值 就好了。A 篇 Firefly 客户端里有同样的写法,也一起改了。

常见问题

现象 原因 处理
401 Access denied Key 错误或已作废 到后台重新生成 Key
402 insufficient_funds 余额不足 充值;先用 dry_run 估算费用
429 inflight_limit 同时进行中的任务太多 降低并发;返回里会给出 max_inflight_requests
图片链接打不开 临时链接已过期 生成后立即下载(本文的做法)
海报上的字被改了 magic prompt 改写了提示词 用 --magic off

模板规则

  • 先 dry_run 报价,人工确认后再生成。
  • 拿到链接立刻下载,同时保存 seed 和最终提示词。
  • is_image_safe=false 的图只记录,不重试,也不绕过。

项目资源

用途 地址
Ideogram 4.0 生成接口 https://developer.ideogram.ai/v2/api-reference/images/generate/ideogram-4
Ideogram API 文档索引(llms.txt) https://developer.ideogram.ai/v2/llms.txt
Ideogram 开发者文档首页 https://developer.ideogram.ai/

报价确认这一步拦住了误操作,生成之后再交给 B 篇检查文字有没有写对。