Пакетные запросы GPT Image 2 без потери задач
Отправьте набор промптов GPT Image 2, сохраните идентификаторы задач, возобновите опрос после обрыва и скачайте изображения без повторной отправки.
Читать в MarkdownПакетная генерация GPT Image 2 в SeedRouter — это отправка обычных запросов на изображения и отслеживание каждого возвращённого идентификатора задачи. В руководстве используется небольшой клиентский скрипт, а не отдельный Batch API и не тариф со скидкой за пакет. Каждый промпт — самостоятельная задача; скрипт сохраняет прогресс, чтобы эти задачи можно было проверить снова после обрыва.
Это две разные работы: сгенерировать изображение и скачать его. Сбой загрузки не требует новой генерации. Истёкший срок опроса не означает, что задача генерации провалилась. Сохранение этих различий в скрипте экономит и время, и нервы.
Что нужно перед началом?
Понадобятся Node.js 22 или новее, серверная переменная окружения SEEDROUTER_API_KEY и достаточный баланс аккаунта для тестового набора. Начните с двух промптов. Прежде чем увеличивать число изображений, сверьтесь с текущими тарифами.
Сохраните код ниже как images.mjs в приватном рабочем каталоге. Он создаст там image-tasks.json и скачает PNG-файлы в images/. Берегите файл состояния: именно он связывает ваши локальные метки с принятыми задачами. Не кладите секреты и промпты с чувствительными данными клиентов в публичный репозиторий.
Примеры запрашивают по одному изображению на промпт. Используйте параметр n, когда нужно несколько результатов для одного промпта; не путайте это с отправкой разных промптов. Допустимый диапазон и прочие ограничения описаны в справочнике модели.
Отправьте один раз и сохраните идентификаторы задач
Скрипт записывает элемент 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. Запускайте одновременно только одну копию скрипта и не переименовывайте метки в файле состояния. Это маленький локальный пример, а не многопроцессное хранилище. Для сервиса с несколькими экземплярами приложения используйте надёжное хранилище с монопольным владением каждой отправкой вместо общего JSON-файла.
Возобновите опрос, не запуская новую генерацию
После истечения локального срока или обрыва сети запустите node images.mjs poll снова. Скрипт читает сохранённые идентификаторы задач и опрашивает их GET-запросами. Новый запрос на генерацию он не отправляет. Завершённая задача с незаконченной загрузкой проверяется повторно, а её PNG-файлы скачиваются под теми же предсказуемыми именами.
Десятиминутный срок принадлежит этому примерному скрипту. Это не обещанное время выполнения и не отмена на стороне сервера. Уже начатый сетевой запрос может завершиться после окончания цикла. Реальные состояния — processing, completed и failed — описаны в справочнике жизненного цикла задач.
Элемент unconfirmed требует ручного разбора, потому что у скрипта нет сохранённого идентификатора задачи. Не удаляйте такую запись и не запускайте отправку заново просто чтобы посмотреть, сработает ли со второго раза. Сначала загляните в историю задач своего аккаунта. Если идентификатор найдётся, впишите его в сохранённую запись и используйте опрос; иначе разберитесь с неясной отправкой, прежде чем генерировать снова.
Какую параллельность выбрать?
Начните с последовательной отправки и маленького тестового набора, как в этом примере. Каждая принятая задача может выполняться, пока скрипт отправляет следующую, но запросы не уходят неограниченным залпом. Код не подразумевает универсального безопасного числа параллельных запросов.
В более крупном приложении сделайте максимум незавершённых задач явной настройкой. По достижении предела прекращайте принимать новую работу и продолжайте опрашивать уже принятые задачи. С лимитами аккаунта работайте по документированным рекомендациям по ошибкам, а не вслепую повторяя POST-запросы.
Ведите отдельный список результатов, которым нужна творческая доработка. Готовое изображение, которое вам не нравится, не является провалившейся задачей API. Это новое редакторское решение и, если отправить ещё один запрос, новая генерация, которую надо учесть в бюджете.
Проверьте набор, прежде чем считать его готовым
Сверьте исходные метки с файлом состояния и со скачанными файлами. У каждой метки должно быть объяснение: скачано, ещё выполняется, провалилось или ждёт разбора отправки. Один лишь подсчёт файлов может скрыть незавершённую задачу или загрузку, оборвавшуюся на середине.
Откройте и сами изображения. Скрипт проверяет HTTP-статус и тип содержимого PNG, но не может решить, того ли цвета кружка и читается ли надпись. Оценивайте визуальное задание отдельно от успеха передачи данных.
Если меняете output_format, обновляйте вместе и ожидаемый тип содержимого, и расширение файла. Переименование скачанного JPEG в .png его не конвертирует. Пример намеренно фиксирует PNG, чтобы лишняя возня с форматами не заслоняла тему восстановления задач.
Частые вопросы
Это Batch API от OpenAI?
Нет. Скрипт отправляет обычные задачи генерации изображений SeedRouter. Он не использует конечную точку Batch API и не подразумевает особых пакетных тарифов.
Можно ли возобновить только загрузки?
Да. Запустите команду опроса с сохранённым состоянием. Она снова получит результаты завершённых задач и скачает недостающие файлы, не создавая новой задачи генерации.
Останавливает ли провалившаяся задача остальные?
Цикл опроса помечает такую задачу как провалившуюся и продолжает проверять остальные. Разберите неудачный элемент отдельно. Провалившаяся задача не тарифицируется; один лишь локальный таймаут провала не доказывает.
Храните записи о задачах вместе с изображениями
Надёжная пакетная генерация GPT Image 2 держится на связи между каждым промптом и его идентификатором задачи. Сохраняйте эту запись, возобновляйте существующие задачи и повторяйте загрузки отдельно от генерации. Когда небольшой набор начнёт вести себя предсказуемо, наращивайте объём в рамках бюджета, который можно контролировать по руководству по тарифам.



