Requisições GPT Image 2 em lote sem perder tarefas
Envie um conjunto de prompts do GPT Image 2, salve os IDs das tarefas, retome o polling após uma interrupção e baixe as imagens sem reenviá-las.
Ler em MarkdownGeração em lote com o GPT Image 2 na SeedRouter significa enviar requisições de imagem comuns e acompanhar cada ID de tarefa retornado. Este guia usa um pequeno script do lado do cliente, não uma Batch API separada nem um produto com desconto por lote. Cada prompt é uma tarefa própria; o script salva o progresso para você consultar essas tarefas de novo depois de uma interrupção.
São dois trabalhos distintos: gerar uma imagem e baixá-la. Um download que falha não exige nova geração. Um prazo de polling esgotado não significa que a tarefa de imagem falhou. Manter essas distinções no script poupa trabalho e confusão.
O que você precisa antes de começar?
Use Node.js 22 ou mais recente, uma variável de ambiente SEEDROUTER_API_KEY no servidor e saldo suficiente para o seu conjunto de teste. Comece com dois prompts. Confira os preços atuais antes de aumentar o número de imagens.
Salve o código a seguir como images.mjs em um diretório de trabalho privado. Ele cria ali o image-tasks.json e baixa os arquivos PNG em images/. Guarde o arquivo de estado: é ele que liga seus rótulos locais às tarefas aceitas. Não coloque segredos nem prompts com dados sensíveis de clientes em um repositório público.
Os exemplos pedem uma imagem por prompt. Use o parâmetro n quando quiser várias saídas para o mesmo prompt; não confunda isso com enviar prompts diferentes. A SeedRouter documenta o intervalo aceito e as demais restrições na documentação do modelo.
Envie uma vez e salve os IDs das tarefas
O script grava uma entrada unconfirmed antes de enviar uma requisição. Se o processo parar antes de salvar a resposta, a execução seguinte não reenvia aquele item em silêncio. Examine a tarefa na sua conta antes de decidir o que fazer com uma entrada não confirmada.
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));Rode node images.mjs submit e depois node images.mjs poll. Execute só uma cópia do script por vez e não renomeie rótulos no arquivo de estado. Este é um exemplo local pequeno, não um sistema de armazenamento multiprocesso. Para um serviço com várias instâncias da aplicação, use armazenamento durável com posse exclusiva de cada envio em vez de compartilhar este arquivo JSON.
Retome o polling sem iniciar outra geração
Rode node images.mjs poll de novo depois de um prazo local esgotado ou de uma queda de rede. Ele lê os IDs de tarefa salvos e faz requisições GET para consultá-los. Não envia outra requisição de geração. Uma tarefa concluída com download incompleto é consultada novamente e seus arquivos PNG são baixados com os mesmos nomes determinísticos.
O prazo de dez minutos pertence a este script de exemplo. Não é um tempo de conclusão prometido nem um cancelamento no servidor. Uma requisição de rede já em andamento pode terminar depois desse prazo do laço. A documentação do ciclo de vida das tarefas define os estados reais: processing, completed e failed.
Um item unconfirmed precisa de revisão manual porque o script não tem um ID de tarefa salvo. Não apague essa entrada e reenvie só para ver se funciona na segunda vez. Primeiro consulte o histórico de tarefas da sua conta. Se recuperar o ID, coloque-o na entrada salva e use o polling; caso contrário, resolva o envio incerto antes de decidir gerar de novo.
Quanta concorrência usar?
Comece com envio sequencial e um conjunto de teste pequeno, como faz este exemplo. Cada tarefa aceita pode seguir enquanto o script envia a próxima, mas as requisições não saem em uma rajada sem limite. O código não sugere nenhum número de concorrência seguro que sirva para todos.
Em uma aplicação maior, transforme o número máximo de tarefas não concluídas em um ajuste explícito. Pare de aceitar trabalho novo quando esse limite for atingido e continue consultando as tarefas já aceitas. Trate os limites da conta seguindo as orientações de erro documentadas, não repetindo requisições POST às cegas.
Mantenha um registro separado dos resultados que pedem revisão criativa. Uma imagem concluída de que você não gostou não é uma tarefa de API que falhou. É uma nova decisão editorial e, se você enviar outra requisição, uma nova geração para entrar no orçamento.
Confira o conjunto antes de dá-lo por encerrado
Compare os rótulos de entrada com o arquivo de estado e com os arquivos baixados. Todo rótulo deve ter uma explicação: baixado, ainda em processamento, falhou ou aguardando revisão do envio. Contar apenas arquivos pode esconder uma tarefa não concluída ou um download interrompido no meio.
Abra também as imagens. O script confere o status HTTP e o tipo de conteúdo PNG, mas não consegue decidir se a caneca está na cor pedida ou se um rótulo está legível. Avalie o briefing visual separadamente do sucesso do transporte.
Se mudar output_format, atualize junto o tipo de conteúdo esperado e a extensão do arquivo. Renomear um download JPEG para .png não o converte. O exemplo fixa o PNG de propósito para que esse problema extra de formato não atrapalhe a recuperação das tarefas.
Perguntas frequentes
Isto é a Batch API da OpenAI?
Não. Ele envia tarefas comuns de imagem da SeedRouter a partir de um script. Não usa um endpoint de Batch API nem implica preço especial por lote.
Dá para retomar só os downloads?
Sim. Rode o comando de polling com o estado salvo. Ele busca de novo a saída das tarefas concluídas e baixa os resultados que faltam sem criar outra tarefa de imagem.
Uma tarefa que falha interrompe todas as outras?
O laço de polling registra aquela tarefa como falha e segue consultando as demais. Analise o item que falhou à parte. Uma tarefa que falha não é cobrada; um tempo esgotado local, sozinho, não comprova falha.
Guarde o registro das tarefas junto das imagens
Uma geração em lote confiável com o GPT Image 2 depende de manter o vínculo entre cada prompt e seu ID de tarefa. Preserve esse registro, retome as tarefas existentes e tente os downloads de novo separadamente da geração. Quando um conjunto pequeno se comportar como esperado, aumente a carga dentro de um orçamento que você consiga acompanhar com o guia de preços.



