Obrazy AI ze skryptu w Pythonie - od jednego pliku do serii
Generowanie obrazów przez API w Pythonie: gotowy kod dla GPT Image 2.5, Nano Banana Pro i FLUX.2 pro, edycja zdjęć, seria równoległa, błędy i koszt.
Obraz z modelu AI dostaniesz w Pythonie jednym wywołaniem requests.post. Podajesz identyfikator modelu (gpt-image-2.5, nano-banana-pro, flux-2-pro), prompt i format, a parametr ?wait=60 sprawia, że link do gotowego pliku wraca w tej samej odpowiedzi. Na Kadraris API pokazuję całą ścieżkę: pierwszy obraz, funkcję z zapisem na dysk, edycję zdjęcia, serię w kilku wątkach i obsługę błędów. Kod nadaje się do skopiowania.
Trzy modele i ich charaktery
Różnią się nie tyle jakością, co tym, w czym są najlepsze:
| Model | Za obraz | W czym błyszczy | Do czego go biorę |
|---|---|---|---|
| GPT Image 2.5 | 0,293 zł za obraz | wierność poleceniom przy kolejnych poprawkach | edycje zdjęć, grafiki według instrukcji |
| Nano Banana Pro | od 0,411 zł za obraz | napisy, wiele referencji, ta sama postać | plakaty, infografiki, seria z jedną osobą |
| FLUX.2 [pro] | od 0,403 zł za obraz | fotorealizm, faktury materiałów | produkty, wnętrza, portrety |
Co mówią producenci: GPT Image 2.5 (OpenAI, wrzesień 2026) bierze do 16 referencji i generuje do 4K. Nano Banana Pro, czyli Gemini 3 Pro Image, łączy do 14 obrazów wejściowych i według Google utrzymuje wygląd do 5 osób. FLUX.2 pro od Black Forest Labs tworzy i edytuje do 4 megapikseli, z kilkoma referencjami naraz.
Limity w Kadrarisie odczytasz ze schematu modelu - bez klucza:
curl https://kadraris.com/v1/models/nano-banana-proW odpowiedzi szukaj pola input_schema: lista przyjmowanych parametrów i ich zakresów.
Przygotowanie środowiska
- Python 3.10+ i
pip install requests. - Konto w Kadrarisie: rejestracja, doładowanie od 40 zł (BLIK lub karta), klucz
sk-kd-...z panelu. - Do edycji: miejsce na pliki z publicznym adresem HTTPS (bucket, własny serwer, CDN).
- Klucz trzymaj w zmiennej środowiskowej, nigdy w kodzie:
export KADRARIS_API_KEY="sk-kd-..."W PowerShellu to $env:KADRARIS_API_KEY = "sk-kd-...".
Pierwsze wywołanie
Obraz powstaje zwykle w kilkanaście do kilkudziesięciu sekund, dlatego dodaję ?wait=60. Serwer wstrzymuje odpowiedź do końca generacji. Jeśli nie zdąży, zwraca 202 i wtedy odpytujesz zadanie po id.
import os
import requests
API = "https://kadraris.com/v1"
KEY = os.environ["KADRARIS_API_KEY"]
HEADERS = {"Authorization": f"Bearer {KEY}"}
r = requests.post(
f"{API}/generate",
params={"wait": 60},
headers=HEADERS,
json={
"model": "gpt-image-2.5",
"input": {
"prompt": "Glass teapot on a wooden table, "
"green tea leaves, side window light, "
"product photo",
# GPT Image 2.5: wymiar w px albo "auto"
"aspect_ratio": "1536x1024",
},
},
timeout=130, # musi przekraczać wait
)
task = r.json()
print(r.status_code, task["status"])
if task["status"] == "succeeded":
print(task["output"][0]["url"], task["cost"])Dwie pułapki. Po pierwsze timeout po stronie klienta musi być dłuższy niż wait, inaczej requests przerwie połączenie przed odpowiedzią. Po drugie zakończone zadanie może mieć status failed - wtedy nic nie płacisz, blokada wraca na saldo, ale pliku nie ma.
Funkcja, której używam w projektach
W prawdziwym skrypcie zamykam to w jednej funkcji. Obsługuje generację, dopytywanie, gdy minuta nie wystarczy, i zapis na dysk:
import time
import pathlib
def wait_for_task(task_id, timeout=300):
end = time.time() + timeout
while time.time() < end:
task = requests.get(f"{API}/tasks/{task_id}",
headers=HEADERS, timeout=30).json()
if task["status"] in ("succeeded", "failed"):
return task
time.sleep(3)
raise TimeoutError(task_id)
def generate_image(model, prompt, folder="obrazy",
**params):
r = requests.post(
f"{API}/generate",
params={"wait": 60},
headers=HEADERS,
json={"model": model,
"input": {"prompt": prompt, **params}},
timeout=130,
)
if r.status_code not in (200, 202):
err = r.json()["error"]
raise RuntimeError(
f"{r.status_code} {err['type']}: {err['message']}")
task = r.json()
if task["status"] not in ("succeeded", "failed"):
task = wait_for_task(task["id"])
if task["status"] == "failed":
raise RuntimeError(
f"porażka, 0 zł: {task['error']['message']}")
out = pathlib.Path(folder)
out.mkdir(exist_ok=True)
saved = []
for i, item in enumerate(task["output"]):
p = out / f"{model}_{task['id']}_{i}.png"
p.write_bytes(
requests.get(item["url"], timeout=60).content)
saved.append(p)
return saved
print(generate_image(
"nano-banana-2",
"Small home office, plants, white desk, rainy daylight",
aspect_ratio="16:9", num_images=2))Pliki zapisuję od razu, bo linki w output działają przez ograniczony czas. Końcówka .png to uproszczenie - jeśli format ma znaczenie, odczytaj go z nagłówka Content-Type przy pobieraniu.
Ustawienia, które zmieniają wynik
Wspólne dla modeli obrazu są tylko dwa pola:
| Pole | Co podajesz | Po co |
|---|---|---|
prompt | tekst | opis obrazu albo polecenie zmiany |
image_urls | lista adresów HTTPS | edycja lub referencje (limit zależy od modelu) |
Reszta różni się między modelami:
aspect_ratio- we FLUX.2 pro i Nano Banana Pro to proporcja typu4:3, w GPT Image 2.5 wymiar w pikselach (1024x1024,1536x1024,1024x1536) alboauto.num_images(1-4) - mają go m.in. Nano Banana 2 i Wan 2.7 Image. Trzy modele z tabeli zwracają jeden obraz na zadanie, więc warianty to osobne zadania. Koszt rośnie razem z liczbą obrazów.seed- jest m.in. w FLUX.2 [max], FLUX.1 Kontext [pro] i Wan 2.7 Image, w trzech głównych modelach go nie ma. Ten sam seed z tym samym promptem daje zbliżony obraz, nie zawsze identyczny co do piksela. Używam go, gdy zmieniam jedno słowo i chcę zobaczyć tylko jego wpływ.negative_prompt,resolution,quality- tylko w części modeli.
Pewną listę daje GET /v1/models/{id}. Pole spoza schematu albo zła wartość kończy się kodem 422, zanim cokolwiek zostanie pobrane z salda. Przy szukaniu kompozycji robię 2-4 warianty, wybieram jeden i dalej już go edytuję, zamiast losować od zera.
Zmiana istniejącego zdjęcia
Edycja to ten sam endpoint z dodanym image_urls. Plik musi leżeć pod publicznym adresem HTTPS.
files = generate_image(
"gpt-image-2.5",
"Put the backpack on a clean light-grey studio "
"background. Do not change the backpack, its straps, "
"its logo or its shadow.",
image_urls=["https://twoja-domena.pl/pliki/plecak.jpg"],
)W prompcie zawsze wymieniam, czego nie ruszać. Bez tego model potrafi "przy okazji" poprawić kolor produktu czy kształt detalu. GPT Image 2.5 najlepiej słucha takich zastrzeżeń, więc do zdjęć produktowych sięgam po niego najczęściej.
Kilka referencji naraz, np. postać z jednego zdjęcia w miejscu z drugiego:
files = generate_image(
"nano-banana-pro",
"The man from the first image stands on the train "
"platform from the second image, holding a coffee, "
"same jacket, foggy morning",
image_urls=[
"https://twoja-domena.pl/pliki/osoba.jpg",
"https://twoja-domena.pl/pliki/peron.jpg",
],
aspect_ratio="9:16",
)Więcej edycji z polskimi poleceniami znajdziesz w poradniku Nano Banana Pro po polsku.
Wiele obrazów naraz
Gdy potrzebuję kilkudziesięciu ilustracji do wpisów albo serii banerów, nie czekam na każdą po kolei. Puszczam kilka zadań równolegle:
from concurrent.futures import ThreadPoolExecutor
from concurrent.futures import as_completed
PROMPTS = [
"Flat illustration of a man repairing a bike, warm tones",
"Flat illustration of kids building a sandcastle, warm tones",
"Flat illustration of a baker opening a shop, warm tones",
"Flat illustration of a dog walk in the park, warm tones",
]
with ThreadPoolExecutor(max_workers=4) as pool:
jobs = {pool.submit(generate_image, "flux-2-pro", p,
aspect_ratio="16:9"): p
for p in PROMPTS}
for job in as_completed(jobs):
try:
print("gotowe", job.result())
except Exception as e:
print("problem", jobs[job], e)Z wątkami nie szalej. Domyślnie klucz ma limit 60 zapytań na minutę, powyżej serwer zwraca 429. U mnie 4-8 równoległych zadań działa bez kłopotów.
Szablon promptu dla całej serii
W skryptach składam prompt z części: nazwa z bazy plus stały opis stylu. Styl trzymam w jednej stałej:
STYLE = ("product photo, soft light from the right, "
"plain warm-grey background, shallow depth of field")
def product_prompt(name, material):
return f"{name} made of {material}, centered, {STYLE}"
print(product_prompt("Desk lamp", "brushed brass"))Zmiana światła w 200 obrazach to wtedy jedna linijka. Co jeszcze poprawiło moje wyniki:
- Kolejność: obiekt, otoczenie, na końcu światło i kadr. Początek promptu waży najwięcej.
- Konkret zamiast przymiotników: "brushed brass, visible fingerprints" mówi więcej niż "elegant lamp".
- Napis do umieszczenia na obrazie - w cudzysłowie, w dokładnym brzmieniu.
- Podobna kompozycja w serii: jeden
seeddla wszystkich (tam, gdzie model go ma). - Prompt zapisuj obok pliku, choćby w JSON-ie. Za miesiąc nie będziesz pamiętać, co zadziałało.
Gdy coś pójdzie nie tak
| Kod | error.type | Reakcja |
|---|---|---|
| 401 | authentication_error | zły lub pusty klucz w zmiennej |
| 402 | insufficient_balance | available niższe niż wycena - doładuj |
| 404 | not_found | literówka w identyfikatorze modelu |
| 422 | invalid_request | pole spoza schematu, komunikat wskazuje które |
| 429 | rate_limit | odczekaj i spróbuj z opóźnieniem |
Ponawianie przy 429 i 502 z rosnącą przerwą:
def post_with_retry(payload, tries=4):
for n in range(tries):
r = requests.post(f"{API}/generate",
params={"wait": 60},
headers=HEADERS,
json=payload, timeout=130)
if r.status_code not in (429, 502):
return r
time.sleep(2 ** n * 3)
return rWszystkie kody opisuje dokumentacja błędów, a endpoint - dokumentacja generowania.
Rachunek
Każdy model ma stałą stawkę za obraz: GPT Image 2.5 0,293 zł za obraz, Nano Banana Pro od 0,411 zł za obraz, FLUX.2 pro od 0,403 zł za obraz. Seria 30 grafik to po prostu 30 x stawka wybranego modelu. Wycenę każdego zadania masz w estimated_cost zaraz po wysłaniu, a zadania zakończone błędem nie są płatne.
Konto polskie rozlicza się w złotych brutto, fakturę VAT zaznaczasz przy doładowaniu. Bieżące stawki: cennik. Chcesz potem ożywić obraz? Podajesz go jako pierwszą klatkę wideo - przykład jest w poradniku Seedance 2.5 API.
Pytania
Po co ustawiać timeout większy niż wait?
Bo przy ?wait=60 serwer może trzymać połączenie przez całą minutę. Jeśli requests ma krótszy timeout, przerwie je, zanim przyjdzie odpowiedź, i stracisz wynik w skrypcie (zadanie i tak się wykona, odbierzesz je po id).
Jak dostać cztery warianty jednego promptu?
W modelach z num_images, np. Nano Banana 2, wpisujesz 4. W GPT Image 2.5, Nano Banana Pro i FLUX.2 pro wysyłasz cztery osobne zadania, najlepiej równolegle. Koszt w obu przypadkach to cztery obrazy.
Czy zdjęcie do edycji mogę wysłać jako plik z dysku?
W image_urls podajesz adresy HTTPS, więc plik musi być wcześniej wystawiony publicznie, np. w buckecie.
Co się dzieje z kosztem, gdy dostanę status failed?
Nic nie płacisz. Kwota zablokowana przy starcie wraca na saldo w całości.


























