关于 ideogram-image
ideogram-image 是使用 Ideogram 官方 Developer API 生成和编辑图像的技能,面向需要接入图像工作流的开发者。它梳理当前路由、请求格式与生命周期注意事项,并区分文档事实和操作建议。
主要功能
- 01按任务选择当前 API 路由
- 02生成、改编与编辑图像
- 03提交异步生成并查询状态
- 04描述图像并移除背景
- 05管理自定义模型数据集与训练
适用场景
- 01将文本提示词转为图像
- 02通过参考图修改图像或重绘区域
- 03为图像生成描述提示词
- 04规划自定义模型训练流程
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ calesthio/ generative-media-skills --skill ideogram-image安装后在 Claude Code 中输入/ideogram-image确认已加载。
附带文件EVAL.md
详细介绍
Ideogram 图像
直接使用官方开发者 API,并将下方核对日期视为合同的一部分。
证据标签与刷新规则
- 已记录:由 Ideogram 第一方页面或官方 OpenAPI 文档明确说明。
- 推断:发布的合同未保证的结论。
- 生产实践建议:操作建议,并非 Ideogram 的承诺。
- 模型可用性、端点生命周期、价格、限制、政策和临时 URL 有效期都可能变化。发布或报价前,请重新检查文档索引、OpenAPI 3.1 JSON、价格、API 协议、使用政策和隐私政策。
除非段落另有说明,下方 API 信息均为已记录 — 核对日期:2026-07-09。
选择路由,而非臆造模型字符串
| 需求 | 规范路由 | 实际模型/生命周期 |
|---|---|---|
| 新建文本/布局生成 | POST /v1/ideogram-v4/generate | Ideogram 4.0;当前旗舰 API 路由 |
| 异步新建生成 | POST /v1/ideogram-v4/async/generate | Ideogram 4.0;Webhook 加轮询 |
| V4 图像引导的修改 | POST /v1/ideogram-v4/remix | Ideogram 4.0 |
| 风格/角色参考、蒙版编辑、重新构图、替换背景 | 下方的 /v1/ideogram-v3/* 路由 | Ideogram 3.0;当前专用路由 |
| 仅通过提示词进行多图编辑 | POST /v1/edit | 当前指令式编辑路由;文档提供 V3 分辨率控制,但没有请求模型字段 |
| 较旧的、通过请求体选择的生成 | /generate、/edit、/remix、/reframe | 参考文档明确标为旧版;新项目请避免使用 |
不要向 V4 或 V3 路由级端点发送 model。它们的模型标识就是路由段 ideogram-v4 和 ideogram-v3。旧版 ModelEnum 接受 V_1、V_1_TURBO、V_2、V_2_TURBO、V_2A、V_2A_TURBO 或 AUTO;不包含 V3/V4。通用 Describe 路由接受 V_2、V_3 或 V_4。V3 自定义模型通过 custom_model_uri 指定,格式为 model/<model_name>/version/<version_name>。
官方参考文档明确指出,POST /v1/ideogram-v3/edit 已是旧版,且将被移除;请迁移到 /v1/ideogram-v3/inpaint。官方未公布 V3 或其余旧版路由的 API 弃用日期。应用模型历史记录称 0.1 和 0.2 已退役,但它们不是当前的 API 请求 ID。不要根据应用历史推断 API 的退役计划。
进行身份验证并确立传输协议
使用基础 URL https://api.ideogram.ai。仅通过 Api-Key: <key> 发送密钥。API 计费/账户设置与 Ideogram 应用订阅分开。密钥完整内容仅在创建时显示一次。
**生产实践建议:**从 IDEOGRAM_API_KEY 读取密钥,在日志中对其脱敏,在已部署的系统中使用密钥管理器,并设置连接/读取超时。环境变量名称是客户端约定,并非 Ideogram 定义的名称。
大多数当前操作使用 multipart/form-data;让 HTTP 库设置边界。V4 Magic Prompt 使用 JSON。所有成功的生成/编辑响应都返回 URL,而非嵌入的图像字节。请立即下载所有需要的结果:第一方文档指出图像链接会过期,但未公布固定有效期。文档未定义输出格式选择器;不要根据 URL 后缀承诺 PNG/JPEG。下载后检查响应标头/字节。
仅使用有文档说明的操作
当前图像与视觉功能
| 方法和路径 | 必需请求 | 可选请求 | 成功响应结构 |
|---|---|---|---|
POST /v1/ideogram-v4/generate | text_prompt:string 或 json_prompt:V4JsonPrompt,二选一 | resolution:ResolutionV4、rendering_speed、`enable_copyright_detection:boolean | null` |
POST /v1/ideogram-v4/async/generate?webhook_url=<https-url> | 相同的 V4 表单;必须提供 HTTPS webhook 查询参数 | 相同的 V4 选项 | {generation_id:string} |
GET /v1/generations/{generation_id} | URL 安全的 base64 generation ID | 无 | 处理中/失败状态或已完成数据 |
POST /v1/ideogram-v4/remix | 表单 image:file、text_prompt:string | image_weight:1..100、V4 分辨率/速度/版权检测 | ImageGenerationResponseV4 |
POST /v1/ideogram-v4/magic-prompt | JSON text_prompt:string | aspect_ratio:AspectRatioV4=AUTO | {json_prompt:V4JsonPrompt, aspect_ratio} |
POST /v1/ideogram-v4/describe | 表单 image_file:file(JPEG/PNG/WebP,最大 10 MB) | include_bbox:boolean=true | {json_prompt:V4JsonPrompt} |
POST /v1/ideogram-v3/generate | 表单 prompt:string | 下文所列的 V3 生成/风格/参考字段 | ImageGenerationResponseV3 |
POST /v1/ideogram-v3/generate-transparent | 表单 prompt:string | seed、`upscale_factor:X1 | X2 |
POST /v1/ideogram-v3/inpaint | 表单 image:file、尺寸相同的黑白 mask:file、prompt:string | Magic Prompt、数量、seed、速度、V3 风格/参考字段 | V3 图像响应 |
POST /v1/ideogram-v3/remix | 表单 image:file、prompt:string | image_weight=50、seed、尺寸、速度、Magic Prompt、负面提示词、数量、风格/参考字段 | V3 图像响应 |
POST /v1/ideogram-v3/reframe | 表单 image:file、resolution:ResolutionV3 | 数量、seed、速度、预设/调色板/风格代码/参考 | V3 图像响应 |
POST /v1/ideogram-v3/replace-background | 表单 image:file、prompt:string | Magic Prompt、数量、seed、速度、预设/调色板/风格代码/参考 | V3 图像响应 |
POST /v1/remove-background | 表单 image:file | 无 | `{created, data:[{url:string |
POST /v1/ideogram-v3/layerize-text | 表单 image:file | prompt:string、seed | 见下文的文档歧义说明 |
POST /v1/edit | 表单 prompt:string,编辑输入时还需提供 images:file[] 或 image_urls:string[] | 数量、seed、Magic Prompt、V3 分辨率或宽高比、transparent_background=false | {created,data:V1EditImageObject[]} |
POST /upscale | 表单 image_file:file、image_request JSON 对象 | 对象字段 prompt、resemblance=50、detail=50、magic_prompt_option、num_images=1、seed | 旧版格式的图像响应 |
POST /describe | 表单 image_file:file | `describe_model_version:V_2 | V_3 |
上述所有图像上传字段均接受 JPEG、PNG 或 WebP,且文件大小限制为 10 MB;/v1/edit 除外:该路由最多接受 10 张上传图像,每张最大 10 MB,或最多 10 个 Ideogram 图像 URL。V3 风格参考组和角色参考组的总大小上限均为 10 MB。V3 API 目前支持一张角色参考图;对应的可选灰度蒙版必须与参考图数量和尺寸一致。
OpenAPI 还记录了自定义模型管理接口:GET/POST /datasets、GET /datasets/{dataset_id}、POST /datasets/{dataset_id}/upload_assets、POST /v1/ideogram-v3/train-model、GET /models 和 GET /models/{model_id}。当前训练路由/OpenAPI 契约要求数据集包含 15–100 张图像,而官方教程称 10 张即可开始训练。除非 Ideogram 解决此冲突,否则至少使用 15 张;在花费 $40 运行前进行验证,并记录所用的实时架构。model_name 需由 5–30 个字母数字/空格/连字符组成。仅在明确要求自定义模型的任务中使用这些操作。
仅限旧版文档记录的操作包括 POST /generate、/v1/ideogram-v3/edit、/edit、/remix 和 /reframe。迁移期间才保留这些操作;不要臆造未记录的 v4/edit、v4/inpaint、v4/reframe 或 v4/replace-background 路由。
精确的共享架构
V4JsonPrompt:
{
"high_level_description": "required string",
"style_description": {
"aesthetics": "optional string",
"art_style": "optional string",
"lighting": "optional string",
"medium": "optional string",
"photo": "optional string"
},
"compositional_deconstruction": {
"background": "required string",
"elements": [
{"type": "obj", "desc": "required string", "bbox": [0, 0, 1000, 1000]},
{"type": "text", "text": "required literal string", "desc": "required string", "bbox": [0, 0, 1000, 1000]}
]
}
}
必填字段:high_level_description、compositional_deconstruction.background 和 elements。对象必须包含 type 和 desc;文本必须包含 type、字面文本 text 和 desc。style_description 和每个 bbox 均为可选字段。bbox 表示 1000×1000 坐标系上的四个归一化整数 [y_min,x_min,y_max,x_max]。文档说明这四个值的范围为 [0,1000];已发布的 JSON Schema 未规定 minItems、maxItems 或数值范围,因此请在客户端进行验证。
V4 分辨率仅限以下值:
2048x2048, 1440x2880, 2880x1440, 1664x2496, 2496x1664, 1792x2240,
2240x1792, 1440x2560, 2560x1440, 1600x2560, 2560x1600, 1728x2304,
2304x1728, 1296x3168, 3168x1296, 1152x2944, 2944x1152, 1248x3328,
3328x1248, 1280x3072, 3072x1280, 1024x3072, 3072x1024
V4 Magic Prompt 的宽高比为 AUTO、1x4、1x3、1x2、9x16、10x16、2x3、3x4、4x5、1x1、5x4、4x3、3x2、16x10、16x9、2x1、3x1、4x1。
V3 宽高比为 1x3、3x1、1x2、2x1、9x16、16x9、10x16、16x10、2x3、3x2、3x4、4x3、4x5、5x4、1x1。不要在 V3 中同时发送 aspect_ratio 和 resolution;默认宽高比为 1x1。ResolutionV3 是 OpenAPI 架构中精确列出的 69 个枚举值;请依据该实时枚举进行验证,不要用任意尺寸近似替代。
V3 num_images 的范围为 1–8,默认值为 1。V3 seed 的范围为 0–2147483647。V4 generate/remix 不提供数量或请求种子参数;响应会返回一个种子。不要根据示例响应数组推断可控制的数量。
rendering_speed 可取 FLASH|TURBO|DEFAULT|QUALITY,默认值为 DEFAULT。文档中易变的例外情况: V4 目前对 FLASH 返回 400;透明背景生成也不接受 FLASH。V3 样式类型可取 AUTO|GENERAL|REALISTIC|DESIGN|FICTION,默认值为 GENERAL。V3 Magic Prompt 可取 AUTO|ON|OFF。
V3 样式预设仅限以下值:
80S_ILLUSTRATION, 90S_NOSTALGIA, ABSTRACT_ORGANIC, ANALOG_NOSTALGIA, ART_BRUT,
ART_DECO, ART_POSTER, AURA, AVANT_GARDE, BAUHAUS, BLUEPRINT, BLURRY_MOTION,
BRIGHT_ART, C4D_CARTOON, CHILDRENS_BOOK, COLLAGE, COLORING_BOOK_I, COLORING_BOOK_II,
CUBISM, DARK_AURA, DOODLE, DOUBLE_EXPOSURE, DRAMATIC_CINEMA, EDITORIAL,
EMOTIONAL_MINIMAL, ETHEREAL_PARTY, EXPIRED_FILM, FLAT_ART, FLAT_VECTOR,
FOREST_REVERIE, GEO_MINIMALIST, GLASS_PRISM, GOLDEN_HOUR, GRAFFITI_I, GRAFFITI_II,
HALFTONE_PRINT, HIGH_CONTRAST, HIPPIE_ERA, ICONIC, JAPANDI_FUSION, JAZZY,
LONG_EXPOSURE, MAGAZINE_EDITORIAL, MINIMAL_ILLUSTRATION, MIXED_MEDIA, MONOCHROME,
NIGHTLIFE, OIL_PAINTING, OLD_CARTOONS, PAINT_GESTURE, POP_ART, RETRO_ETCHING,
RIVIERA_POP, SPOTLIGHT_80S, STYLIZED_RED, SURREAL_COLLAGE, TRAVEL_POSTER,
VINTAGE_GEO, VINTAGE_POSTER, WATERCOLOR, WEIRD, WOODBLOCK_PRINT
颜色调色板使用以下一种变体:{"name":"EMBER|FRESH|JUNGLE|MAGIC|MELON|MOSAIC|PASTEL|ULTRAMARINE"} 或 {"members":[{"color_hex":"#RRGGBB","color_weight":0.05..1.0}]}。style_codes 使用 8 位十六进制值。不要将 style_codes 与 style_reference_images 或 style_type 结合使用。
V4 通用成功响应:
{"created":"date-time","data":[{"url":"uri or null","prompt":"string","resolution":"ResolutionV4","is_image_safe":true,"seed":12345}],"response_type":"url"}
V3 通用成功响应还包含可选的 upscaled_resolution 和 style_type;必须包含 created、data,以及每个条目中的 prompt、resolution、is_image_safe、seed。当 is_image_safe 为 false 时,url 为空字符串或 null。轮询时必须包含 generation_id、status:pending|completed|failed 和 created;只有已完成的响应才包含 response_type:"url" 和 data。
其他精确响应:/upscale 返回 {created, request_id?, data?},其中使用旧版 ImageObject 条目;/describe 返回 {descriptions:[{text?:string}]};/v1/ideogram-v4/describe 返回 {json_prompt:V4JsonPrompt};/v1/remove-background 返回 {created,data:[{url?:string|null,is_image_safe:boolean}]},且仅包含一个条目。OpenAPI 将 descriptions 和其中每个 text 正式标注为可选字段,尽管常规示例中包含这些字段;使用前请验证它们是否存在。
控制文本、布局、风格和参考
对于 V4 排版,优先使用 json_prompt,并将准确文案放入 type:"text" 元素的 text 中;在 desc 中描述字体类别、字重、大小写、颜色、对齐方式和用途;使用 bbox 控制布局。text_prompt 会自动启用 Magic Prompt,而 json_prompt 会绕过它并直接传入。可先通过 /v1/ideogram-v4/magic-prompt 生成结构化 JSON,或通过 /v1/ideogram-v4/describe 从图像中提取结构化 JSON。
没有任何一手 API Schema 公布提示词长度上限、字面文本长度上限、保证提供的字体目录或精确拼写服务等级协议。请勿凭空编造。**生产启发式做法:**让关键文案保持简短,将每个文案区域分开,避免在其他位置出现相互矛盾的可见文本,生成多个变体,并使用 OCR 加人工审核来验证拼写是否准确。
对于 V3,请在提示词中清楚写出要显示的字面文案,并在适用时选择 DESIGN。使用 style_preset 设置已知美学风格,使用风格参考进行视觉迁移,使用角色参考保持身份一致性,并在 Remix 中使用 image_weight 保留源图内容。请勿声称 V4 Generate 路由接受 V3 风格或角色参考字段;当前 OpenAPI 中并不支持这些字段。
对于局部重绘,掩码尺寸应与输入图像相同,并将可编辑像素标为黑色。对于重新取景,请指定 V3 分辨率,而不是宽高比字段。对于替换背景,只描述所需的新背景,同时保留前景主体。
执行完整示例
以下是示例,并非通用客户端架构。只有在主动使用有余额的密钥运行时,它们才会发起付费调用。
示例:同步 V4 结构化排版与持久化下载
**生产意图:**创建一张 1:1 的宣传海报,其中标题的重要性高于风格的随机性。**采用此结构的原因:**使用 json_prompt 绕过自动 Magic Prompt 改写,将字面文案独立出来并明确放置。**预期结果:**获得一个或多个通过安全检查的响应项,其中包含准确的标题候选文案和一张 2048×2048 的结果图。**常见故障:**文案拼写错误、bbox 空间不足、安全检查后 URL 为 null,或下载链接过期。**有意义的变体:**仅更改 desc 来测试排版,同时保持 text 不变;或者在构图尚未确定时先使用 /magic-prompt。
import hashlib
import json
import os
from io import BytesIO
from pathlib import Path
import requests
from PIL import Image
API = "https://api.ideogram.ai"
KEY = os.environ["IDEOGRAM_API_KEY"]
prompt = {
"high_level_description": "A premium square coffee poster with one centered headline.",
"style_description": {"medium": "screen-printed poster", "aesthetics": "minimal, warm"},
"compositional_deconstruction": {
"background": "Flat cream paper with subtle grain.",
"elements": [{
"type": "text",
"text": "MORNING, MADE BOLD",
"desc": "Large centered uppercase condensed black headline",
"bbox": [360, 100, 600, 900],
}],
},
}
with requests.Session() as session:
response = session.post(
f"{API}/v1/ideogram-v4/generate",
headers={"Api-Key": KEY},
files={
"json_prompt": (None, json.dumps(prompt)),
"resolution": (None, "2048x2048"),
"rendering_speed": (None, "TURBO"),
},
timeout=(10, 180),
)
response.raise_for_status()
manifest = {"request": {"json_prompt": prompt, "resolution": "2048x2048",
"rendering_speed": "TURBO"}, "results": []}
signatures = {
"image/png": (b"\x89PNG\r\n\x1a\n", ".png"),
"image/jpeg": (b"\xff\xd8\xff", ".jpg"),
"image/webp": (b"RIFF", ".webp"),
}
for index, item in enumerate(response.json()["data"], start=1):
if not item["is_image_safe"] or not item.get("url"):
raise RuntimeError(f"Ideogram candidate {index} is unusable")
with session.get(item["url"], timeout=(10, 60), stream=True) as download:
download.raise_for_status()
limit = 50 * 1024 * 1024
declared = int(download.headers.get("content-length", "0") or 0)
if declared > limit:
raise RuntimeError("Declared image exceeds local 50 MiB safety limit")
body = bytearray()
for chunk in download.iter_content(1024 * 1024):
body.extend(chunk)
if len(body) > limit:
raise RuntimeError("Streaming image exceeded local 50 MiB safety limit")
raw = bytes(body)
media_type = download.headers.get("content-type", "").split(";", 1)[0].lower()
if media_type not in signatures or not raw.startswith(signatures[media_type][0]):
raise RuntimeError(f"Unexpected image payload: {media_type}")
if media_type == "image/webp" and raw[8:12] != b"WEBP":
raise RuntimeError("RIFF payload is not WebP")
with Image.open(BytesIO(raw)) as decoded:
decoded.verify()
with Image.open(BytesIO(raw)) as decoded:
actual_size = decoded.size
expected_size = tuple(map(int, item["resolution"].lower().split("x")))
if actual_size != expected_size:
raise RuntimeError(f"Expected {expected_size}, decoded {actual_size}")
output = Path(f"poster-{index}{signatures[media_type][1]}")
temporary = output.with_suffix(output.suffix + ".tmp")
temporary.write_bytes(raw)
temporary.replace(output)
manifest["results"].append({**item, "path": str(output),
"download_content_type": media_type,
"sha256": hashlib.sha256(raw).hexdigest()})
Path("poster.metadata.json").write_text(json.dumps(manifest, indent=2), encoding="utf-8")
示例:使用风格参考进行 V3 局部重绘
**生产意图:**仅替换选定对象,同时借鉴已批准的视觉风格。**采用此结构的原因:**V3 Inpaint 是文档所述的掩码路由,并支持重复的风格参考部分。**预期结果:**生成两个候选结果,并保留黑色掩码之外的像素。**常见故障:**掩码语义颠倒、尺寸不匹配、出现可见接缝或风格偏移。**有意义的变体:**省略风格参考,以进行中性编辑;或者在需要保持身份一致性时,添加唯一受支持的角色参考,以及与之匹配的灰度参考掩码。
import hashlib
import json
import os
from io import BytesIO
from pathlib import Path
import requests
from PIL import Image
API = "https://api.ideogram.ai"
image_path, mask_path, style_path = map(Path, ("input.png", "mask.png", "style.png"))
mime_by_format = {"PNG": "image/png", "JPEG": "image/jpeg", "WEBP": "image/webp"}
formats = {}
for path in (image_path, mask_path, style_path):
if path.stat().st_size > 10 * 1024 * 1024:
raise ValueError(f"{path} exceeds 10 MB")
with Image.open(path) as check:
check.verify()
if check.format not in mime_by_format:
raise ValueError(f"Unsupported image format for {path}: {check.format}")
formats[path] = check.format
with Image.open(image_path) as source_check, Image.open(mask_path) as mask_check:
if source_check.size != mask_check.size:
raise ValueError("mask must match input dimensions")
if mask_check.mode not in {"1", "L"}:
raise ValueError("mask must be binary/grayscale")
if set(mask_check.convert("L").getdata()) - {0, 255}:
raise ValueError("mask must contain only black and white pixels")
if style_path.stat().st_size > 10 * 1024 * 1024:
raise ValueError("style-reference group exceeds 10 MB")
with image_path.open("rb") as image, mask_path.open("rb") as mask, style_path.open("rb") as style:
response = requests.post(
f"{API}/v1/ideogram-v3/inpaint",
headers={"Api-Key": os.environ["IDEOGRAM_API_KEY"]},
data={"prompt": "Replace only the black mask region with a red enamel teapot", "num_images": "2", "seed": "42"},
files=[("image", (image_path.name, image, mime_by_format[formats[image_path]])),
("mask", (mask_path.name, mask, mime_by_format[formats[mask_path]])),
("style_reference_images", (style_path.name, style, mime_by_format[formats[style_path]]))],
timeout=(10, 180),
)
response.raise_for_status()
payload = response.json()
manifest = {
"endpoint": "/v1/ideogram-v3/inpaint",
"request": {"prompt": "Replace only the black mask region with a red enamel teapot", "num_images": 2, "seed": 42},
"input_sha256": {p.name: hashlib.sha256(p.read_bytes()).hexdigest() for p in (image_path, mask_path, style_path)},
"results": [],
}
for index, item in enumerate(payload["data"], start=1):
if not item["is_image_safe"] or not item.get("url"):
raise RuntimeError(f"Candidate {index} is not usable")
with requests.get(item["url"], timeout=(10, 60), stream=True) as download:
download.raise_for_status()
limit = 50 * 1024 * 1024
declared = int(download.headers.get("content-length", "0") or 0)
if declared > limit:
raise RuntimeError("Declared image exceeds local 50 MiB safety limit")
body = bytearray()
for chunk in download.iter_content(1024 * 1024):
body.extend(chunk)
if len(body) > limit:
raise RuntimeError("Streaming image exceeded local 50 MiB safety limit")
raw = bytes(body)
media_type = download.headers.get("content-type", "").split(";", 1)[0].lower()
signatures = {"image/png": (b"\x89PNG\r\n\x1a\n", ".png"),
"image/jpeg": (b"\xff\xd8\xff", ".jpg"),
"image/webp": (b"RIFF", ".webp")}
if media_type not in signatures or not raw.startswith(signatures[media_type][0]):
raise RuntimeError(f"Unexpected output type: {media_type}")
if media_type == "image/webp" and raw[8:12] != b"WEBP":
raise RuntimeError("RIFF payload is not WebP")
with Image.open(BytesIO(raw)) as decoded:
decoded.verify()
with Image.open(BytesIO(raw)) as decoded:
actual_size = decoded.size
expected_size = tuple(map(int, item["resolution"].lower().split("x")))
if actual_size != expected_size:
raise RuntimeError(f"Expected {expected_size}, decoded {actual_size}")
output = Path(f"inpaint-{index}{signatures[media_type][1]}")
temporary = output.with_suffix(output.suffix + ".tmp")
temporary.write_bytes(raw)
temporary.replace(output)
manifest["results"].append({**item, "path": str(output), "content_type": media_type,
"sha256": hashlib.sha256(raw).hexdigest()})
Path("inpaint.metadata.json").write_text(json.dumps(manifest, indent=2), encoding="utf-8")
示例:提交 V4 异步任务并安全轮询
**生产意图:**提交任务时不让 HTTP 连接一直保持打开状态,并在 Webhook 投递失败时仍能恢复。**采用此结构的原因:**持久化服务端发出的 ID,并将轮询设为有界的备用方案。**预期结果:**状态从 pending 转为 completed 并返回可下载数据,或转为终态 failed。**常见故障:**Webhook URL 非公开、受到限流、投递丢失或任务无限期保持待处理状态。**有意义的变体:**让经过验证的 Webhook 先完成记录,并让轮询用于对账,而非作为主要的完成路径。
import hashlib
import json
import os
import random
import time
from io import BytesIO
from pathlib import Path
import requests
from PIL import Image
API = "https://api.ideogram.ai"
headers = {"Api-Key": os.environ["IDEOGRAM_API_KEY"]}
webhook_url = os.environ["IDEOGRAM_WEBHOOK_URL"]
if not webhook_url.startswith("https://"):
raise ValueError("IDEOGRAM_WEBHOOK_URL must be public HTTPS")
accepted = requests.post(
f"{API}/v1/ideogram-v4/async/generate",
params={"webhook_url": webhook_url},
headers=headers,
files={"text_prompt": (None, "A crisp editorial photograph of a red bicycle")},
timeout=(10, 30),
)
accepted.raise_for_status()
generation_id = accepted.json()["generation_id"]
Path("async.accepted.json").write_text(json.dumps({
"generation_id": generation_id,
"submitted_unix": int(time.time()),
"webhook_url_sha256": hashlib.sha256(webhook_url.encode("utf-8")).hexdigest(),
}, indent=2), encoding="utf-8")
deadline = time.monotonic() + 10 * 60
completed = False
for attempt in range(40):
if time.monotonic() >= deadline:
break
try:
result = requests.get(
f"{API}/v1/generations/{generation_id}", headers=headers, timeout=(10, 30)
)
except (requests.Timeout, requests.ConnectionError):
time.sleep(min(30, 2 ** min(attempt, 5)) + random.random())
continue
if result.status_code == 429:
time.sleep(min(30, 2 ** min(attempt, 5)) + random.random())
continue
if result.status_code in {500, 502, 503, 504}:
time.sleep(min(30, 2 ** min(attempt, 5)) + random.random())
continue
result.raise_for_status()
payload = result.json()
if payload["status"] == "completed":
manifest = {"generation_id": generation_id, "results": []}
for index, item in enumerate(payload["data"], start=1):
if not item["is_image_safe"] or not item.get("url"):
raise RuntimeError(f"Completed candidate {index} is unusable")
with requests.get(item["url"], timeout=(10, 60), stream=True) as download:
download.raise_for_status()
limit = 50 * 1024 * 1024
declared = int(download.headers.get("content-length", "0") or 0)
if declared > limit:
raise RuntimeError("Declared image exceeds local 50 MiB safety limit")
body = bytearray()
for chunk in download.iter_content(1024 * 1024):
body.extend(chunk)
if len(body) > limit:
raise RuntimeError("Streaming image exceeded local 50 MiB safety limit")
raw = bytes(body)
media_type = download.headers.get("content-type", "").split(";", 1)[0].lower()
signatures = {"image/png": (b"\x89PNG\r\n\x1a\n", ".png"),
"image/jpeg": (b"\xff\xd8\xff", ".jpg"),
"image/webp": (b"RIFF", ".webp")}
if media_type not in signatures or not raw.startswith(signatures[media_type][0]):
raise RuntimeError(f"Unexpected output type: {media_type}")
if media_type == "image/webp" and raw[8:12] != b"WEBP":
raise RuntimeError("RIFF payload is not WebP")
with Image.open(BytesIO(raw)) as decoded:
decoded.verify()
with Image.open(BytesIO(raw)) as decoded:
actual_size = decoded.size
expected_size = tuple(map(int, item["resolution"].lower().split("x")))
if actual_size != expected_size:
raise RuntimeError(f"Expected {expected_size}, decoded {actual_size}")
output = Path(f"async-{index}{signatures[media_type][1]}")
temporary = output.with_suffix(output.suffix + ".tmp")
temporary.write_bytes(raw)
temporary.replace(output)
manifest["results"].append({**item, "path": str(output), "content_type": media_type,
"sha256": hashlib.sha256(raw).hexdigest()})
Path("async.metadata.json").write_text(json.dumps(manifest, indent=2), encoding="utf-8")
completed = True
break
if payload["status"] == "failed":
raise RuntimeError(f"generation failed: {generation_id}")
time.sleep(min(30, 2 ** min(attempt, 5)) + random.random())
if not completed:
raise TimeoutError(f"generation still pending at deadline: {generation_id}")
示例:验证并持久化接收 Ideogram webhook
生产意图: 在存储原始投递内容之前先进行身份验证,拒绝过期或不匹配的请求,并以幂等方式确认重复请求。为何采用这种结构: Ideogram 会对原始字节进行签名,也可能重新投递;这里用 SQLite 代替生产环境中的事务型数据存储。预期结果: 有效的首次投递只存储一次,并返回 HTTP 204;有效的重复投递也会返回 204,且不会重复处理。可能的失败: 时间戳过期、十六进制格式错误、密钥未知或已轮换、请求体被重新序列化、生成 ID 不匹配,或 JSON 无效。有意义的变体: 将 SQLite 替换为应用程序持久化数据库中的事务型唯一约束,并在提交后将任务加入队列。
import base64
import hashlib
import json
import sqlite3
import time
import requests
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
JWKS_URL = "https://api.ideogram.ai/v1/.well-known/jwks.json"
H_GENERATION = "X-Ideogram-Webhook-Generation-Id"
H_USER = "X-Ideogram-Webhook-User-Id"
H_TIMESTAMP = "X-Ideogram-Webhook-Timestamp"
H_KEY = "X-Ideogram-Webhook-Key-Id"
H_SIGNATURE = "X-Ideogram-Webhook-Signature"
_JWKS_CACHE = {"keys": None, "expires": 0.0}
def b64url_decode(value: str) -> bytes:
return base64.urlsafe_b64decode(value + "=" * (-len(value) % 4))
def fetch_jwks(*, force_refresh: bool = False, ttl_seconds: int = 300) -> list[dict]:
now = time.monotonic()
if not force_refresh and _JWKS_CACHE["keys"] is not None and now < _JWKS_CACHE["expires"]:
return _JWKS_CACHE["keys"]
response = requests.get(JWKS_URL, timeout=(5, 15))
response.raise_for_status()
keys = response.json()["keys"]
_JWKS_CACHE.update({"keys": keys, "expires": now + ttl_seconds})
return keys
def verify_with_keys(keys: list[dict], key_hint: str, signature: bytes, message: bytes) -> bool:
ordered = sorted(keys, key=lambda key: 0 if key.get("kid") == key_hint else 1)
for key in ordered:
if key.get("kty") != "OKP" or key.get("crv") != "Ed25519" or not key.get("x"):
continue
try:
Ed25519PublicKey.from_public_bytes(b64url_decode(key["x"])).verify(signature, message)
return True
except (InvalidSignature, ValueError):
continue
return False
def verify_delivery(headers, raw_body: bytes, *, max_age_seconds: int = 300) -> dict:
required = [H_GENERATION, H_USER, H_TIMESTAMP, H_KEY, H_SIGNATURE]
missing = [name for name in required if not headers.get(name)]
if missing:
raise ValueError(f"Missing webhook headers: {missing}")
timestamp = int(headers[H_TIMESTAMP])
if abs(int(time.time()) - timestamp) > max_age_seconds:
raise ValueError("Stale webhook timestamp")
signature_hex = headers[H_SIGNATURE]
if signature_hex != signature_hex.lower():
raise ValueError("Signature must be lowercase hexadecimal")
try:
signature = bytes.fromhex(signature_hex)
except ValueError as exc:
raise ValueError("Invalid signature hexadecimal") from exc
body_hash = hashlib.sha256(raw_body).hexdigest()
message = "\n".join([
headers[H_GENERATION], headers[H_USER], headers[H_TIMESTAMP], body_hash
]).encode("utf-8")
keys = fetch_jwks()
if not verify_with_keys(keys, headers[H_KEY], signature, message):
# Refresh once so key rotation does not require a process restart.
if not verify_with_keys(fetch_jwks(force_refresh=True), headers[H_KEY], signature, message):
raise ValueError("Webhook signature verification failed")
payload = json.loads(raw_body)
if payload.get("generation_id") != headers[H_GENERATION]:
raise ValueError("Header/body generation_id mismatch")
return payload
def handle_webhook(headers, raw_body: bytes, database_path="ideogram-webhooks.sqlite") -> int:
payload = verify_delivery(headers, raw_body)
generation_id = payload["generation_id"]
with sqlite3.connect(database_path) as db:
db.execute("""CREATE TABLE IF NOT EXISTS deliveries (
generation_id TEXT PRIMARY KEY, payload_json TEXT NOT NULL,
received_unix INTEGER NOT NULL, status TEXT NOT NULL
)""")
db.execute(
"INSERT OR IGNORE INTO deliveries VALUES (?, ?, ?, 'received')",
(generation_id, json.dumps(payload, separators=(",", ":")), int(time.time())),
)
# Framework adapter should return this after commit; a worker processes 'received' rows.
return 204
为四个 Python 示例安装 requests、Pillow 和 cryptography。Web 框架必须将未改动的请求体字节和原始请求头传入 handle_webhook;不要先解析再重新序列化。
请严格按照官方 webhook 指南中的说明验证 webhook 签名:使用 SHA-256 对原始请求体字节进行哈希;用换行符连接生成 ID、用户 ID、Unix 时间戳和请求体哈希;针对 GET /v1/.well-known/jwks.json 返回的密钥验证小写十六进制 Ed25519 签名。哈希前绝不要重新序列化 JSON。生产环境经验法则: 缓存密钥,在验证失败或密钥轮换时刷新;使用本地设定的容差拒绝过期时间戳;要求请求头和请求体中的 ID 一致;并通过 generation_id 进行事务型投递去重。
处理生命周期、错误、重试和并发
默认限制为 10 个并发请求;如需更高吞吐量,请联系 Ideogram。此限制指并发数,而非有文档说明的每分钟请求配额。
| 状态 | 有文档说明的含义 | 操作 |
|---|---|---|
| 400 | 输入无效;也包括当前不支持的速度组合 | 修正请求;不要原样重试 |
| 401 | 缺少或无效的授权信息 | 停止请求,修复密钥或账户 |
| 402 | /v1/edit 的额度或配额不足 | 充值或更改为获准的方案;不要循环重试 |
| 403 | 禁止访问或未获授权,例如有文档说明的 V3 Remix/Upscale 路由 | 停止请求;确认账户权限和路由访问权限;不要原样重试 |
| 404 | 异步生成 ID 未知 | 核验存储的 ID 和环境 |
| 422 | 提示词或图像未通过安全检查;有文档说明的响应体为 { "error": "string" } | 不要规避检查;根据策略修改或拒绝 |
| 429 | 请求过多 | 降低并发量,并通过有界指数退避和随机抖动重试 |
| 503 | /v1/edit 等选定路由暂时不可用 | 退避后进行有界重试 |
OpenAPI 未对大多数 400/401/429/503 响应体定义类型。请谨慎解析,并记录状态、请求元数据以及经过脱敏或截断的响应体。文档未承诺一定提供 Retry-After;如果实际存在,请遵循该值,否则使用完全随机抖动。
文档未说明存在请求幂等键请求头。生产环境经验法则: 同步 POST 超时后,绝不要盲目重试,因为请求可能已经完成并产生了费用。优先使用 V4 异步接口,在进行其他工作前持久化保存 generation_id,并且只重试轮询用的 GET 请求。Ideogram 可能会有限次数地重试 webhook,但不保证一定投递;对成功的重复投递应以 2xx 确认,并在失败时改用轮询。
价格与支出门槛
**价格可能变动,文档页面最后注明修订日期为 2025-08-06;核查日期为 2026-07-09:**V4 Turbo/Default/Quality 的每张输出费用分别为 $0.03/$0.06/$0.10;V3 Flash/Turbo/Default/Quality 的费用分别为 $0.03/$0.03/$0.06/$0.09;搭配角色参考图的 V3 Turbo/Default/Quality 费用分别为 $0.10/$0.15/$0.20。透明背景 V3 X1 的 Turbo/Default/Quality 费用为 $0.04/$0.07/$0.10;2X 为 $0.16/$0.19/$0.22;4X 为 $0.20/$0.23/$0.26。指令编辑每张输出收费 $0.20,放大每张输入收费 $0.06,描述每张输入收费 $0.01,分层每张输入收费 $0.09,自助式自定义模型训练每次收费 $40。进行估算前,请查看实时页面。
价格页面还列出了一些仅提供价格信息或尚未完全路由的功能:Gemini 生成,1K/2K 收费 $0.20,4K 收费 $0.36;Topaz 放大,2K/4K/8K 分别收费 $0.12/$0.24/$0.48;Generate + Layerize V3 的 Turbo/Default/Quality 分别收费 $0.12/$0.15/$0.18;自定义推理的 V4 收费 $0.06/$0.12/$0.20,V3 收费 $0.06/$0.12/$0.18。不要为这些条目臆造端点。当前公开的 OpenAPI 并未提供价格表中所有功能的路由,且仅为 V3 记录了 custom_model_uri;在目标账户获得一手请求架构之前,应将这些功能视为尚未明确。
提交前先计算费用上限:请求的输出数量 × 每张输出的价格,再加上参考图、放大和编辑等额外费用。确保操作人员能看到 API 自动充值行为;设置文档说明了可配置的阈值和充值余额,以及单独的 API 额度。
确保安全、隐私、来源记录和权利合规
遵守 API 协议和使用政策,而不只是检查 is_image_safe。不得提交违法、侵权、非自愿、侵犯隐私、色情、过度暴力、仇恨、欺骗、欺凌、政治竞选/游说或有害/犯罪内容。不得绕过过滤器或速率限制。
确保对每张上传的图片、角色参考图、肖像、商标及字体/品牌素材均拥有相应权利并取得同意。Ideogram 的一般条款称,其不主张拥有用户输入或输出的所有权,也不限制商业用途;但用户仍须对法律合规及第三方权利负责,输出可能并非独一无二,且不提供不侵权保证。请将此视为操作指引,而非法律建议。
API 协议要求开发者应用标明输出由 Ideogram 生成(例如 “Powered by Ideogram”),在启用相应权限时展示经批准的 Ideogram 标志,并链接至使用政策。协议禁止将 AI 输出说成完全由人类创作,或说成真实事件的真实照片。请保留来源记录,包含提供商、端点/模型路由、UTC 时间、请求参数、返回的提示词/种子/分辨率、来源哈希值/权利信息、输出哈希值、安全检查结果以及人工审核批准。当前响应架构未记录 C2PA 字段或可下载的来源记录清单;不得声称提供了此类信息。
除非内容因违反使用政策而被标记,否则 API 输入/输出不会用于训练 Ideogram 模型;被标记的内容可能用于训练/改进滥用和安全检测。更广泛的隐私政策称会收集提示词/上传内容,并按用途设定非数值化的保留期限(“在合理必要的期限内”)。该政策还允许服务提供商处理数据以及进行国际数据处理。因此,未经审查确认合法依据并取得必要同意,不得发送健康数据、儿童数据、秘密或敏感个人数据。除非订单专属协议另有规定,否则不得承诺零保留或固定删除期限。
enable_copyright_detection=true 会为 V3/V4 请求启用额外的 Hive 肖像/徽标检查,除非组织设置已启用该功能;此操作会增加延迟,被标记的图片会返回 is_image_safe:false。请将其视为额外信号,而非法律许可。
运行 QA 并安全迁移
发送前:
- 在发布自动化中重新获取路由、尺寸和预设的 OpenAPI 枚举值。
- 验证文件类型/大小、蒙版尺寸、数量、seed、互斥字段,以及 V4 边界框的顺序/范围。
- 对费用、参考图/相似度、公开分发和政策敏感提示词要求审批。
- 提交前,存储脱敏后的规范请求和源文件哈希。
接收后:
- 拒绝缺少 URL 或不安全的结果;立即下载临时 URL。
- 验证 MIME 签名、像素尺寸、请求时所需的 alpha 通道,以及加密哈希。
- 通过 OCR 和人工审核对比字面文本;对比参考图中的身份/风格,但不要假设能完美还原。
- 对于编辑,比较受保护区域的差异;对于局部重绘,检查蒙版接缝;对于重新构图,检查主体保留情况和新增边缘;对于背景处理,检查头发/透明区域的光晕。
- 记录端点、返回的提示词、seed、实际分辨率、价格依据和审核结果。
迁移映射:
- 将
/v1/ideogram-v3/edit和/edit蒙版工作流迁移到/v1/ideogram-v3/inpaint;将image_file重命名为image,并使用黑色可编辑区域。 - 将旧版
/generate迁移到 V4 Generate(用于新的文本/布局工作),或者在需要 V3 参考图/预设/自定义模型时迁移到 V3 Generate。 - 根据所需控制项,将旧版
/remix和/reframe迁移到/v1/ideogram-v4/remix或对应的 V3 规范路由。 - 将旧版
model/magic_prompt_option/带前缀的宽高比枚举值替换为路由范围内的模型和当前准确的字段名。 - 使用冻结的提示词/素材对每项迁移进行金丝雀测试,比较尺寸、字面文本、风格、主体保留情况、安全行为、延迟和费用;不要假设不同模型代际之间 seed 等效。
已知的第一方歧义
- 自定义模型教程称数据集图片至少需要 10 张,而当前训练路由/OpenAPI 合约规定为 15–100 张。执行更严格的 15 张最低要求,并在付费训练运行前立即重新核实。
- OpenAPI 的
WebhookUrl组件称 webhook 正文与同步响应相同,包含request_id;而异步端点架构和专门的 webhook 指南使用generation_id。遵循专门指南/标头合约,并要求generation_id匹配;在问题解决前,将request_id的表述视为过时内容。 - Layerize Text 的说明文字和示例展示了包含字体/位置/样式数据的
text_blocks,而当前 OpenAPI 的LayerizeTextResponse架构仅声明base_image_url、可选的original_image_url和seed。按声明的架构编写代码,并通过特性检测识别text_blocks;在 Ideogram 解决此不一致前,不要将其设为必需字段。 - API 概览中的 V3 快速入门为一个路由展示了 JSON 正文,但该路由的参考文档/OpenAPI 声明其使用 multipart 表单。遵循路由参考文档/OpenAPI,尤其是在发送文件时。
- V4 响应示例展示了多个数据项,但请求架构没有
num_images;不要将示例中的数量视为保证。 - 定价说明涵盖多个操作类别的 V4 定价,而当前 OpenAPI 提供 V4 Generate/Remix,同时保留了仅限 V3 的局部重绘/重新构图/替换背景路径。应根据文档说明的路由可用性选择,而不是依据定价表的措辞。