Lancer des requêtes GPT Image 2 par lots sans perdre de tâches
Envoyez plusieurs prompts GPT Image 2, conservez les identifiants de tâche, reprenez le suivi après une coupure et téléchargez les images sans les renvoyer.
Lire en MarkdownLa génération par lots avec GPT Image 2 sur SeedRouter consiste à envoyer des requêtes d’images ordinaires et à suivre chaque identifiant de tâche renvoyé. Ce guide utilise un petit script côté client, pas une API Batch distincte ni une offre à remise. Chaque prompt est une tâche à part ; le script enregistre la progression afin que vous puissiez revérifier ces tâches après une interruption.
Il y a deux travaux distincts : générer une image et la télécharger. Un téléchargement échoué n’impose pas une nouvelle génération. Un délai de suivi dépassé ne signifie pas que la tâche d’image a échoué. Conserver ces distinctions dans le script épargne du travail et des confusions.
De quoi avez-vous besoin avant de commencer ?
Utilisez Node.js 22 ou plus récent, une variable d’environnement SEEDROUTER_API_KEY côté serveur et un solde suffisant pour votre jeu de tests. Commencez par deux prompts. Consultez les tarifs actuels avant d’augmenter le nombre d’images.
Enregistrez le code suivant sous images.mjs dans un répertoire de travail privé. Il y crée image-tasks.json et télécharge les fichiers PNG dans images/. Conservez le fichier d’état : c’est lui qui relie vos libellés locaux aux tâches acceptées. N’ajoutez ni secrets ni prompts sensibles pour vos clients à un dépôt public.
Les exemples demandent une image par prompt. Utilisez le paramètre n lorsque vous voulez plusieurs sorties pour un même prompt ; ne confondez pas cela avec l’envoi de prompts différents. SeedRouter documente la plage acceptée et les autres contraintes dans la référence du modèle.
Envoyez une fois, puis enregistrez les identifiants
Le script écrit une entrée unconfirmed avant d’envoyer une requête. Si le processus s’arrête avant d’enregistrer la réponse, l’exécution suivante ne renverra pas silencieusement cet élément. Examinez la tâche dans votre compte avant de décider du sort d’une entrée non confirmée.
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));Exécutez node images.mjs submit, puis node images.mjs poll. N’exécutez qu’une seule copie du script à la fois et ne renommez pas les libellés du fichier d’état. C’est un petit exemple local, pas un système de stockage multiprocessus. Pour un service comptant plusieurs instances, utilisez un stockage durable avec une propriété exclusive de chaque envoi plutôt que de partager ce fichier JSON.
Reprendre le suivi sans relancer de génération
Relancez node images.mjs poll après un délai local dépassé ou une coupure réseau. Le script lit les identifiants enregistrés et les vérifie par des requêtes GET. Il n’envoie pas de nouvelle requête de génération. Une tâche terminée dont le téléchargement est incomplet est revérifiée, et ses fichiers PNG sont téléchargés sous les mêmes noms déterministes.
Le délai de dix minutes appartient à ce script d’exemple. Ce n’est ni une durée d’achèvement promise ni une annulation côté serveur. Une requête réseau en cours peut se terminer après ce délai de boucle. La référence du cycle de vie des tâches définit les états réels : processing, completed et failed.
Une entrée unconfirmed demande une vérification manuelle, car le script n’a pas d’identifiant enregistré. Ne supprimez pas cette entrée pour relancer l’envoi simplement afin de voir si cela passe la seconde fois. Consultez d’abord l’historique des tâches de votre compte. Si vous retrouvez l’identifiant, inscrivez-le dans l’entrée enregistrée et utilisez le suivi ; sinon, tranchez le cas de l’envoi incertain avant de choisir de générer à nouveau.
Quel niveau de parallélisme adopter ?
Commencez par un envoi séquentiel et un petit jeu de tests, comme le fait cet exemple. Chaque tâche acceptée poursuit son traitement pendant que le script envoie la suivante, mais les requêtes ne partent pas en rafale illimitée. Ce code n’implique aucun nombre de requêtes simultanées universellement sûr.
Dans une application plus grande, faites du nombre maximal de tâches en cours un réglage explicite. Cessez d’accepter de nouveaux travaux lorsque cette limite est atteinte et continuez de vérifier les tâches déjà acceptées. Traitez les limites de compte selon les consignes d’erreur documentées, et non en répétant aveuglément des requêtes POST.
Tenez une liste distincte des résultats qui demandent une reprise créative. Une image terminée qui ne vous plaît pas n’est pas une tâche d’API en échec. C’est une nouvelle décision éditoriale et, si vous renvoyez une requête, une nouvelle génération à inscrire au budget.
Vérifiez le lot avant de le déclarer terminé
Comparez vos libellés d’entrée avec le fichier d’état et les fichiers téléchargés. Chaque libellé doit avoir une explication : téléchargé, encore en traitement, en échec, ou en attente de vérification de l’envoi. Compter les fichiers seuls peut masquer une tâche inachevée ou un téléchargement interrompu à mi-parcours.
Ouvrez aussi les images. Le script vérifie le statut HTTP et le type de contenu PNG, mais il ne peut pas juger si une tasse a la couleur demandée ni si une étiquette est lisible. Examinez l’exigence visuelle séparément de la réussite du transport.
Si vous changez output_format, mettez à jour ensemble le type de contenu attendu et l’extension de fichier. Renommer un JPEG téléchargé en .png ne le convertit pas. L’exemple fixe volontairement PNG pour que cette question de format ne masque pas la reprise des tâches.
Questions fréquentes
S’agit-il de l’API Batch d’OpenAI ?
Non. Le script envoie des tâches d’images SeedRouter ordinaires. Il n’utilise aucun point de terminaison Batch et n’implique aucun tarif de lot particulier.
Puis-je reprendre uniquement les téléchargements ?
Oui. Lancez la commande de suivi avec l’état enregistré. Elle récupère de nouveau la sortie des tâches terminées et télécharge les résultats manquants sans créer d’autre tâche d’image.
Une tâche en échec arrête-t-elle toutes les autres ?
Non. La boucle de suivi consigne cette tâche comme échouée et poursuit la vérification des autres. Examinez l’élément en échec séparément. Une tâche qui échoue n’est pas facturée ; un délai local dépassé ne suffit pas à établir un échec.
Conservez le relevé des tâches avec les images
Une génération par lots fiable avec GPT Image 2 repose sur le maintien du lien entre chaque prompt et son identifiant de tâche. Préservez ce relevé, reprenez les tâches existantes et retentez les téléchargements indépendamment de la génération. Une fois qu’un petit lot se comporte comme prévu, augmentez la charge dans un budget que vous pouvez suivre grâce au guide des tarifs.



