Claude Opus 5.5 jest już dostępny w SeedRouter

Wsadowe żądania GPT Image 2 bez gubienia zadań

Wyślij zestaw promptów GPT Image 2, zapisz identyfikatory zadań, wznów odpytywanie po przerwie i pobierz obrazy bez ponownego wysyłania żądań.

Czytaj jako Markdown

Generowanie wsadowe z GPT Image 2 w SeedRouter polega na wysyłaniu zwykłych żądań o obrazy i śledzeniu każdego zwróconego identyfikatora zadania. Ten przewodnik używa małego skryptu po stronie klienta, a nie osobnego Batch API ani taryfy z rabatem za partię. Każdy prompt jest osobnym zadaniem; skrypt zapisuje postęp, żebyś mógł sprawdzić te zadania ponownie po przerwaniu pracy.

To dwie odrębne czynności: wygenerowanie obrazu i pobranie go. Nieudane pobranie nie wymaga nowej generacji. Upływ limitu czasu odpytywania nie oznacza, że zadanie obrazu się nie powiodło. Utrzymanie tych rozróżnień w skrypcie oszczędza i pracę, i zamieszanie.

Czego potrzebujesz przed startem?

Potrzebujesz Node.js 22 lub nowszego, serwerowej zmiennej środowiskowej SEEDROUTER_API_KEY i wystarczającego salda konta na zestaw testowy. Zacznij od dwóch promptów. Zanim zwiększysz liczbę obrazów, sprawdź aktualny cennik.

Zapisz poniższy kod jako images.mjs w prywatnym katalogu roboczym. Tworzy tam image-tasks.json i pobiera pliki PNG do images/. Zachowaj plik stanu: to on łączy twoje lokalne etykiety z przyjętymi zadaniami. Nie wrzucaj sekretów ani promptów z wrażliwymi danymi klientów do publicznego repozytorium.

Przykłady proszą o jeden obraz na prompt. Parametru n używaj wtedy, gdy chcesz kilka wyników dla tego samego promptu; nie myl tego z wysyłaniem różnych promptów. Dopuszczalny zakres i pozostałe ograniczenia opisuje dokumentacja modelu.

Wyślij raz, a potem zapisz identyfikatory zadań

Skrypt zapisuje wpis unconfirmed przed wysłaniem żądania. Jeśli proces zatrzyma się przed zapisaniem odpowiedzi, kolejne uruchomienie nie wyśle po cichu tej pozycji ponownie. Zanim zdecydujesz, co zrobić z niepotwierdzonym wpisem, obejrzyj zadanie na swoim koncie.

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));

Uruchom node images.mjs submit, a potem node images.mjs poll. Trzymaj jednocześnie tylko jedną kopię skryptu i nie zmieniaj nazw etykiet w pliku stanu. To mały lokalny przykład, a nie wieloprocesowy system przechowywania. W usłudze z kilkoma instancjami aplikacji użyj trwałego magazynu z wyłącznym właścicielem każdego wysłania, zamiast współdzielić ten plik JSON.

Wznów odpytywanie bez uruchamiania nowej generacji

Po upływie lokalnego limitu czasu albo po zerwaniu sieci uruchom node images.mjs poll ponownie. Skrypt czyta zapisane identyfikatory zadań i sprawdza je żądaniami GET. Nie wysyła kolejnego żądania generacji. Ukończone zadanie z niedokończonym pobraniem zostaje sprawdzone raz jeszcze, a jego pliki PNG trafiają pod te same, przewidywalne nazwy.

Dziesięciominutowy limit należy do tego przykładowego skryptu. Nie jest obiecanym czasem realizacji ani anulowaniem po stronie serwera. Pojedyncze trwające żądanie sieciowe może zakończyć się po upływie limitu pętli. Rzeczywiste stany — processing, completed i failed — definiuje dokumentacja cyklu życia zadań.

Pozycja unconfirmed wymaga ręcznego przeglądu, bo skrypt nie ma zapisanego identyfikatora zadania. Nie usuwaj tego wpisu i nie wysyłaj ponownie tylko po to, by sprawdzić, czy za drugim razem się uda. Najpierw zajrzyj do historii zadań na swoim koncie. Jeśli odzyskasz identyfikator, wpisz go do zapisanej pozycji i użyj odpytywania; w przeciwnym razie wyjaśnij niepewne wysłanie, zanim zdecydujesz się generować ponownie.

Ile równoległości zastosować?

Zacznij od wysyłania po kolei i małego zestawu testowego, tak jak w tym przykładzie. Każde przyjęte zadanie może działać dalej, gdy skrypt wysyła kolejne, ale żądania nie wylatują nieograniczoną serią. Kod nie sugeruje żadnej uniwersalnie bezpiecznej liczby równoległych żądań.

W większej aplikacji zrób z maksymalnej liczby niedokończonych zadań jawne ustawienie. Po osiągnięciu tego limitu przestań przyjmować nową pracę i dalej sprawdzaj zadania już przyjęte. Limity konta obsługuj zgodnie z udokumentowanymi wskazówkami dotyczącymi błędów, a nie przez ślepe powtarzanie żądań POST.

Prowadź osobną listę wyników, które wymagają poprawki twórczej. Ukończony obraz, który ci się nie podoba, nie jest nieudanym zadaniem API. To nowa decyzja redakcyjna, a jeśli wyślesz kolejne żądanie — także nowa generacja do uwzględnienia w budżecie.

Sprawdź cały zestaw, zanim uznasz go za gotowy

Zestaw swoje etykiety wejściowe z plikiem stanu i pobranymi plikami. Każda etykieta powinna mieć wyjaśnienie: pobrana, wciąż przetwarzana, nieudana albo czekająca na przegląd wysłania. Samo liczenie plików może ukryć niedokończone zadanie albo pobranie przerwane w połowie.

Otwórz też same obrazy. Skrypt sprawdza status HTTP i typ treści PNG, ale nie rozstrzygnie, czy kubek ma zamówiony kolor i czy etykieta jest czytelna. Oceniaj brief wizualny osobno od tego, czy transfer się udał.

Jeśli zmienisz output_format, zaktualizuj razem oczekiwany typ treści i rozszerzenie pliku. Zmiana nazwy pobranego JPEG na .png go nie konwertuje. Przykład celowo trzyma się PNG, żeby dodatkowy problem z formatami nie przesłonił tematu odzyskiwania zadań.

Najczęstsze pytania

Czy to jest Batch API od OpenAI?

Nie. Skrypt wysyła zwykłe zadania obrazowe SeedRouter. Nie korzysta z endpointu Batch API ani nie sugeruje osobnych cen za partię.

Czy mogę wznowić same pobrania?

Tak. Uruchom polecenie odpytywania z zapisanym stanem. Pobierze ponownie wynik ukończonych zadań i ściągnie brakujące pliki bez tworzenia kolejnego zadania obrazowego.

Czy nieudane zadanie zatrzymuje wszystkie pozostałe?

Pętla odpytywania zapisuje to zadanie jako nieudane i sprawdza dalej pozostałe. Nieudaną pozycję przejrzyj osobno. Zadanie zakończone błędem nie jest rozliczane; sam lokalny limit czasu nie dowodzi niepowodzenia.

Trzymaj zapis zadań razem z obrazami

Niezawodne generowanie wsadowe z GPT Image 2 opiera się na utrzymaniu powiązania między każdym promptem a jego identyfikatorem zadania. Zachowaj ten zapis, wznawiaj istniejące zadania i ponawiaj pobrania osobno od generowania. Gdy mały zestaw zacznie działać zgodnie z oczekiwaniami, zwiększ obciążenie w ramach budżetu, który możesz kontrolować dzięki przewodnikowi po cenach.

Powiązane poradniki