タスクを見失わずに GPT Image 2 をバッチ実行する
複数の GPT Image 2 プロンプトを送信し、タスク ID を保存し、中断後もポーリングを再開して、再送信せずに完了画像をダウンロードします。
Markdown で読むSeedRouter での GPT Image 2 バッチ生成とは、通常の画像リクエストを送信し、返される各タスク ID を追跡することです。このガイドで使うのは小さなクライアント側スクリプトであり、独立した Batch API やバッチ割引の製品ではありません。プロンプトごとに別のタスクになります。スクリプトが進捗を保存するので、中断後もそれらのタスクを確認できます。
ここには別々の作業が 2 つあります。画像を生成することと、それをダウンロードすることです。ダウンロードの失敗は、新たな生成を必要としません。ポーリングの期限に達しても、画像タスクが失敗したことにはなりません。この区別をスクリプトに残しておけば、手間も混乱も減らせます。
始める前に必要なもの
Node.js 22 以降、サーバー側の SEEDROUTER_API_KEY 環境変数、そしてテスト一式に足りるアカウント残高が必要です。まずはプロンプト 2 件から始めてください。枚数を増やす前に現在の料金を確認してください。
次のコードを、非公開の作業ディレクトリに images.mjs として保存してください。そこに image-tasks.json を作り、PNG ファイルを images/ にダウンロードします。この状態ファイルは残してください。ローカルのラベルと受理されたタスクを結び付けているのはこれです。機密情報や顧客に関わるプロンプトを公開リポジトリに追加しないでください。
例ではプロンプトごとに 1 枚を要求しています。同じプロンプトで複数の出力が欲しい場合は n を使ってください。異なるプロンプトを送ることと混同しないでください。受け付けられる範囲やその他の制約はモデルリファレンスに記載されています。
一度送信し、タスク ID を保存する
スクリプトはリクエストを送る前に unconfirmed のエントリを書き込みます。レスポンスを保存する前にプロセスが止まっても、次の実行でその項目を黙って再送信することはありません。unconfirmed のエントリをどう扱うか決める前に、アカウントでタスクの状態を確認してください。
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
const mode = process.argv[2];
if (!['submit', 'poll'].includes(mode)) {
throw new Error('Use: node images.mjs submit | poll');
}
const key = process.env.SEEDROUTER_API_KEY;
if (!key) throw new Error('Set SEEDROUTER_API_KEY on your server.');
const base = 'https://api.seedrouter.ai/v1';
const headers = { Authorization: `Bearer ${key}` };
const file = 'image-tasks.json';
let state;
try {
state = JSON.parse(await readFile(file, 'utf8'));
} catch (error) {
if (error.code !== 'ENOENT') throw error;
state = {};
}
const save = async () => {
await writeFile(`${file}.tmp`, JSON.stringify(state, null, 2), { mode: 0o600 });
await rename(`${file}.tmp`, file);
};
const pause = () => new Promise((resolve) => setTimeout(resolve, 3000));
const jobs = [
['blue-mug', 'A cobalt-blue ceramic mug on a pale gray tabletop.'],
['green-bowl', 'A green ceramic bowl on a pale gray tabletop.'],
];
if (mode === 'submit') {
for (const [name, prompt] of jobs) {
if (state[name]) continue;
state[name] = { status: 'unconfirmed' };
await save();
try {
const response = await fetch(`${base}/images/generations`, {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'gpt-image-2', prompt,
size: '1024x1024', quality: 'low', output_format: 'png', n: 1,
}),
signal: AbortSignal.timeout(60000),
});
const task = await response.json();
if (typeof task.id === 'string' && task.id) {
state[name] = { id: task.id, status: 'processing' };
await save();
}
if (!response.ok || !state[name].id) {
throw new Error('Submission needs review.');
}
} catch {
console.error(`${name}: stopped; inspect the saved state before continuing.`);
process.exitCode = 1;
break;
}
}
} else {
await mkdir('images', { recursive: true });
const deadline = Date.now() + 600000;
do {
for (const [name, entry] of Object.entries(state)) {
if (!entry.id || entry.status === 'failed' || entry.downloaded) continue;
try {
const response = await fetch(`${base}/tasks/${encodeURIComponent(entry.id)}`, {
headers, signal: AbortSignal.timeout(30000),
});
if (!response.ok) throw new Error('Task check failed.');
const task = await response.json();
if (!['processing', 'completed', 'failed'].includes(task.status)) {
throw new Error('Unexpected task status.');
}
entry.status = task.status;
await save();
if (task.status !== 'completed') continue;
const images = task.output?.data;
if (!Array.isArray(images) || !images.length) {
throw new Error('Completed task has no image URLs.');
}
for (const [index, image] of images.entries()) {
const url = new URL(image.url);
if (url.protocol !== 'https:') throw new Error('Expected an HTTPS image URL.');
// The download is a separate request: never attach the API key.
const result = await fetch(url, { signal: AbortSignal.timeout(60000) });
if (!result.ok || !result.headers.get('content-type')?.startsWith('image/png')) {
throw new Error('PNG download failed.');
}
await writeFile(`images/${name}-${index}.png`, Buffer.from(await result.arrayBuffer()));
}
entry.downloaded = true;
await save();
} catch {
console.error(`${name}: check or download incomplete; task ID retained.`);
}
}
if (!Object.values(state).some((entry) => entry.id && entry.status !== 'failed' && !entry.downloaded)) break;
await pause();
} while (Date.now() < deadline);
}
console.log(JSON.stringify(state, null, 2));node images.mjs submit を実行し、その後 node images.mjs poll を実行します。スクリプトは同時に 1 つだけ動かし、状態ファイルのラベル名は変更しないでください。これは小さなローカルの例であって、複数プロセス向けのストレージではありません。アプリケーションのインスタンスが複数あるサービスでは、この JSON ファイルを共有するのではなく、各送信の所有権が明確な永続ストレージを使ってください。
生成をやり直さずにポーリングを再開する
ローカルの期限やネットワークの中断のあとは、もう一度 node images.mjs poll を実行してください。保存済みのタスク ID を読み、GET リクエストで確認します。別の生成リクエストは送りません。完了済みでダウンロードが終わっていないタスクは再度確認され、PNG が同じ決まったファイル名でダウンロードされます。
10 分という期限はこの例のスクリプトのものです。約束された完了時間でも、サーバー側のキャンセルでもありません。進行中のネットワークリクエスト 1 件が、そのループ期限の後に完了することもあります。実際の状態はタスクのライフサイクルに定義されています。processing、completed、failed の 3 つです。
unconfirmed の項目は、スクリプトにタスク ID が保存されていないため手動の確認が必要です。二度目はうまくいくかを見るためだけに、そのエントリを削除して送信をやり直さないでください。まずアカウントのタスク履歴を確認します。ID を回収できたら、それを保存済みエントリに書き入れてポーリングを使ってください。回収できない場合は、再生成を選ぶ前に不確かな送信を解決してください。
同時実行数はどれくらいにすべきですか?
この例のように、まずは逐次送信と小さなテストセットから始めてください。受理されたタスクはスクリプトが次を送信する間も進みますが、リクエストが無制限に一斉送出されるわけではありません。このコードが、どこでも安全な同時実行数を示しているわけではありません。
大きなアプリケーションでは、未完了タスクの上限を明示的な設定にしてください。上限に達したら新しい作業の受け入れを止め、受理済みのタスクの確認は続けます。アカウントの制限には、POST を闇雲に繰り返すのではなく、文書化されたエラーの指針に従って対応してください。
作り直しが必要な結果は、別に記録してください。気に入らない完成画像は、失敗した API タスクではありません。それは新たな編集上の判断であり、もう一度リクエストを送るなら、予算に含めるべき新しい生成です。
完了と宣言する前にセットを確認する
入力したラベルを、状態ファイルとダウンロード済みファイルと突き合わせてください。すべてのラベルに説明がつくはずです。ダウンロード済み、処理中、失敗、あるいは送信結果の確認待ちのいずれかです。ファイル数を数えるだけでは、未完了のタスクや途中で止まったダウンロードを見落とすことがあります。
画像そのものも開いて確認してください。スクリプトは HTTP ステータスと PNG のコンテンツタイプを確認しますが、マグが求めた色かどうか、ラベルが読めるかどうかは判断できません。視覚的な要件は、転送の成否とは分けて確認してください。
output_format を変える場合は、期待するコンテンツタイプとファイル拡張子も一緒に更新してください。ダウンロードした JPEG を .png に改名しても変換はされません。この例が PNG に固定しているのは、形式の扱いという別の問題でタスク復旧の話が見えにくくならないようにするためです。
よくある質問
これは OpenAI の Batch API ですか?
いいえ。スクリプトから通常の SeedRouter 画像タスクを送信しているだけです。Batch API のエンドポイントは使っておらず、特別なバッチ料金を意味するものでもありません。
ダウンロードだけ再開できますか?
できます。保存済みの状態でポーリングのコマンドを実行してください。完了タスクの出力を取得し直し、不足している結果をダウンロードします。新しい画像タスクは作りません。
1 件が失敗すると他のタスクも止まりますか?
止まりません。ポーリングのループはそのタスクを失敗として記録し、他の確認を続けます。失敗した項目は個別に確認してください。失敗したタスクは課金されません。ローカルのタイムアウトだけでは失敗とは判断できません。
タスクの記録を画像と一緒に残す
信頼できる GPT Image 2 のバッチ生成は、各プロンプトとそのタスク ID の結び付きを保つことにかかっています。その記録を残し、既存のタスクを再開し、ダウンロードの再試行は生成とは分けて行ってください。小さなセットが期待どおりに動いたら、料金ガイドで監視できる予算の範囲内で作業量を増やしてください。



