EN ZalogujOtwórz Studio
Wersja podglądowa. Katalog modeli, ceny, kalkulator i dokumentacja działają. Generowanie uruchomimy po starcie serwisu.

Obraz z AI w Node.js na samym fetch - zlecenie, czekanie i zapis pliku

Generowanie obrazu AI w Node.js bez SDK: fetch do API Kadrarisa, sprawdzenie kodu HTTP, zapis id zadania, odpytywanie z limitem i zapis gotowego pliku.

Na jeden obraz z AI nie trzeba instalować żadnej paczki. Node.js 18+ ma wbudowany fetch i to wystarcza, żeby zlecić zadanie, poczekać na wynik i zapisać plik. Biblioteka jest tu najmniej ważna - liczą się trzy nawyki: sprawdzanie kodu HTTP przed czytaniem JSON-a, limit prób przy odpytywaniu i zapisanie identyfikatora zadania, zanim cokolwiek pójdzie źle.

Zanim uruchomisz skrypt

  • Klucz w zmiennej środowiskowej KADRARIS_API_KEY. Nie w kodzie, nie w repozytorium, nie w pliku z promptami.
  • Treść zlecenia to identyfikator modelu i obiekt input. W tym przykładzie:
json
{
  "model": "gpt-image-2.5",
  "input": {
    "prompt": "A copper kettle on a stone windowsill, soft daylight, product photo, no text"
  }
}
  • Najpierw res.ok, potem pola. Jeśli odpowiedź to 401, 402 albo 422, JSON nie ma id i program wywróci się w dziwnym miejscu.
  • id i estimated_cost wypisuję na ekran i zapisuję do pliku. Po restarcie programu wracam do tego samego zadania.

Skrypt

Zapisz jako obraz.mjs i uruchom node obraz.mjs. Wynik trafia do wynik.bin jako surowe bajty - rozszerzenie nadaj według wypisanego Content-Type, a nie według tego, co było w prompcie.

js
import { writeFile } from "node:fs/promises";
import { setTimeout as sleep } from "node:timers/promises";

const base = "https://kadraris.com/v1";
const headers = {
  Authorization: `Bearer ${process.env.KADRARIS_API_KEY}`,
  "Content-Type": "application/json"
};

// jedno miejsce na timeout i sprawdzanie statusu HTTP
async function api(path, options = {}) {
  const res = await fetch(base + path, {
    ...options, headers,
    signal: AbortSignal.timeout(20000)
  });
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json();
}

let task = await api("/generate", {
  method: "POST",
  body: JSON.stringify({
    model: "gpt-image-2.5",
    input: { prompt: "A copper kettle on a windowsill" }
  })
});
// id na dysk: po restarcie wznawiasz tylko GET
await writeFile("task-id.txt", task.id);
console.log(task.id, task.estimated_cost);

// najwyżej 24 odczyty co 5 s, czyli około 2 minut
for (let i = 0; i < 24; i++) {
  if (["succeeded", "failed"].includes(task.status)) break;
  await sleep(5000);
  task = await api(`/tasks/${encodeURIComponent(task.id)}`);
}

if (task.status === "failed") {
  throw new Error(JSON.stringify(task.error));
}
if (task.status !== "succeeded") {
  throw new Error(`Zadanie ${task.id} jeszcze trwa`);
}

const url = task.output?.[0]?.url;
if (!url) throw new Error("Zadanie bez pliku w output");

const file = await fetch(url, {
  signal: AbortSignal.timeout(30000)
});
if (!file.ok) throw new Error(`Plik: HTTP ${file.status}`);
const bytes = new Uint8Array(await file.arrayBuffer());
await writeFile("wynik.bin", bytes);
console.log("wynik.bin,", file.headers.get("content-type"));

Co skrypt robi w nietypowych sytuacjach

  • Błąd sieci przy POST. Rzuca wyjątek i kończy. Nie ponawiaj zlecenia automatycznie - zadanie mogło powstać, choć odpowiedź nie dotarła. Najpierw zajrzyj do historii w panelu.
  • Zadanie wciąż trwa po dwóch minutach. Program kończy się komunikatem, ale task-id.txt zostaje. Uruchom później sam odczyt GET /v1/tasks/{id}.
  • failed. Wypisuje błąd i nie próbuje drugi raz. Wejście zmieniaj tylko wtedy, gdy rozumiesz przyczynę z komunikatu. Za takie zadanie nie płacisz.
  • succeeded bez pliku. Mało prawdopodobne, ale sprawdzone - lepiej jasny komunikat niż undefined w adresie.

Na koniec trzy rzeczy do sprawdzenia przed wdrożeniem: klucz pochodzi ze środowiska, każdy nieudany kod HTTP jest obsłużony przed użyciem JSON-a, a pobrany plik ląduje w twoim magazynie.

Koszt

Model z przykładu, GPT Image 2.5, kosztuje 0,293 zł za obraz. Inne ustawienia przeliczysz w kalkulatorze, stawki wszystkich modeli są w cenniku. Saldo nie wygasa, abonamentu nie ma, a nieudane zadania są bezpłatne. Obraz, który się udał, jest płatny także wtedy, gdy go nie użyjesz.

Pytania

Dlaczego zapisuję plik od razu, a nie trzymam linku?

Adres w output działa ograniczony czas i nie jest archiwum. Zaakceptowany obraz przenieś do własnego magazynu, a w bazie trzymaj ścieżkę do swojej kopii.

Czy mogę ominąć pętlę odpytywania?

Tak - dodaj do adresu ?wait=60, a odpowiedź poczeka na wynik do 60 sekund (maksymalnie 120). Jeśli zadanie nie zdąży, dostaniesz zwykłe 202 i wracasz do pętli. Przy wielu zleceniach wygodniejszy jest webhook.

Gdzie sprawdzę pola modelu i cenę przed pierwszym uruchomieniem?

Pola opisują strona modelu i dokumentacja generowania. Cenę zobaczysz w Studio na przycisku przed generowaniem, a w API w estimated_cost. Wersję tego samego przepisu w Pythonie opisuję w generowaniu obrazów przez API.