Claude Opus 5 is now live on CometAPI →

Jak dodać generowanie wideo oparte na AI do aplikacji SaaS

CometAPI
AnnaJun 5, 2026
Jak dodać generowanie wideo oparte na AI do aplikacji SaaS

Dodanie generowania wideo do Twojej aplikacji nie jest tym samym co dodanie generowania obrazów. Wywołanie API zwraca odpowiedź od razu — ale wideo nie jest jeszcze gotowe. Otrzymujesz identyfikator zadania (task ID) i musisz w kółko pytać „czy już skończone?”, aż faktycznie będzie.

Większość deweloperów trafia na to przy pierwszym wywołaniu API wideo: czeka na odpowiedź z adresem URL wideo, a dostaje identyfikator zadania. Ten przewodnik przeprowadzi Cię przez pełny przepływ: przesłanie zadania, odpytywanie o wynik, obsługę błędów oraz zapis wygenerowanego pliku zanim adres URL wygaśnie.

Co zbudujesz

Usługę backend, która przyjmuje tekstowy prompt lub obraz, przesyła zadanie generowania wideo, odpyta aż do zakończenia i zwróci końcowy adres URL wideo. Pracujesz z czterema modelami — Veo 3 Fast, Sora 2, Kling Video i Runway — wszystkie przez jeden klucz API.

Prerequisites:

  • Python 3.8+ lub Node.js 18+
  • Klucz CometAPI
  • Podstawowa znajomość REST API

Zrozum, dlaczego generowanie wideo jest inne

W przypadku obrazów wysyłasz żądanie i otrzymujesz obraz w tej samej odpowiedzi. Generowanie wideo działa asynchronicznie z kolejką zadań:

  1. Wyślij żądanie generowania → otrzymasz task_id
  2. Odpytuj endpoint statusu co kilka sekund
  3. Gdy status osiągnie stan końcowy, dostaniesz adres URL wideo
  4. Pobierz i zapisz wideo — adres URL jest tymczasowy

Jeśli potraktujesz generowanie wideo jak generowanie obrazów i będziesz czekać, aż pierwszy response zawiera wideo, Twoje żądanie będzie za każdym razem przekraczać limit czasu.

W produkcyjnej usłudze webowej ta pętla odpytywania powinna działać w tle (Celery, Bull lub podobne), a nie w handlerze żądania. Przykłady poniżej używają synchronicznego odpytywania — w porządku do skryptów i prototypów, ale nie do obsługi wielu równoległych użytkowników.

Wybierz model

ModelDostawcaMax durationCena (via CometAPI)Najlepsze zastosowania
Veo 3 FastGoogle8 sec$0.05/secSzybkie prototypowanie, shorty
Sora 2OpenAI (via CometAPI model ID)~10 sec$0.08/secWysokiej jakości kreatywne krótkie
Kling VideoKuaishou10 sec$0.13–$2.64/taskMarketing, granularna kontrola
Runway Gen-3A TurboRunway5 or 10 sec$0.32/taskImage-to-video, treści komercyjne

Źródło**: strony modeli CometAPI, maj 2026. Uwaga: „Sora 2” to identyfikator modelu w CometAPI — szczegóły bazowego modelu znajdziesz na ich stronie modelu.

  • Veo 3 Fast obsługuje zarówno text-to-video, jak i image-to-video. Najtańszy za sekundę, dobry punkt startowy.
  • Sora 2 generuje audio natywnie wraz z wideo — dialog, dźwięki tła i efekty bez osobnego kroku TTS.
  • Kling Video daje negative_prompt, cfg_scale, ustawienia ruchu kamery i tryb pro. Najwięcej kontroli z całej czwórki.
  • Runway jest w CometAPI wyłącznie image-to-video. Podajesz statyczny obraz i opis ruchu, a on go animuje.

Wyślij zadanie do Veo

Veo używa multipart/form-data. Użyj files= w requests dla Pythona, aby wysłać poprawnie — data=dict wysyła application/x-www-form-urlencoded, co nie jest tym samym:

import requestsimport osfrom dotenv import load_dotenv​load_dotenv()​def submit_veo_task(prompt: str, size: str = "16x9") -> str:    """Wyślij zadanie Veo 3 Fast (text-to-video). Zwraca task_id."""    api_key = os.getenv("COMETAPI_KEY")    if not api_key:        raise ValueError("Zmienna środowiskowa COMETAPI_KEY nie jest ustawiona")​    response = requests.post(        "https://api.cometapi.com/v1/videos",        headers={"Authorization": f"Bearer {api_key}"},        files={            "prompt": (None, prompt),            "model": (None, "veo3-fast"),            "size": (None, size)        },        timeout=30    )    response.raise_for_status()    return response.json()["id"]​​task_id = submit_veo_task("Papierowy latawiec unoszący się nad polem pszenicy w wietrzne popołudnie")print(f"Zadanie wysłane: {task_id}")

Odpytuj o wynik

import time​def poll_veo_task(task_id: str, interval: int = 10, max_wait: int = 600) -> str:    """Odpytuj aż zadanie Veo zakończy się. Zwraca URL wideo."""    api_key = os.getenv("COMETAPI_KEY")    if not api_key:        raise ValueError("Zmienna środowiskowa COMETAPI_KEY nie jest ustawiona")​    headers = {"Authorization": f"Bearer {api_key}"}    url = f"https://api.cometapi.com/v1/videos/{task_id}"    elapsed = 0​    while elapsed < max_wait:        response = requests.get(url, headers=headers, timeout=30)        response.raise_for_status()        result = response.json()        status = result.get("status")​        if status == "succeeded":            return result["output"][0]        elif status in ("failed", "cancelled"):            raise RuntimeError(                f"Zadanie {task_id} zakończyło się statusem '{status}': "                f"{result.get('error', 'brak szczegółów błędu')}"            )​        time.sleep(interval)        elapsed += interval​    raise TimeoutError(f"Zadanie {task_id} nie zakończyło się w ciągu {max_wait} sekund")​​video_url = poll_veo_task(task_id)print(f"Wideo gotowe: {video_url}")

Użyj Kling Video dla większej kontroli

Kling ma inny układ endpointów i używa JSON. Zwróć uwagę, że końcowy status w Kling to "succeed" (nie "succeeded") — odpowiada to rzeczywistemu formatowi odpowiedzi API:

def submit_kling_task(prompt: str, duration: str = "5", mode: str = "std") -> str:    """Wyślij zadanie Kling (text-to-video). Zwraca task_id."""    api_key = os.getenv("COMETAPI_KEY")    if not api_key:        raise ValueError("Zmienna środowiskowa COMETAPI_KEY nie jest ustawiona")​    response = requests.post(        "https://api.cometapi.com/kling/v1/videos/text2video",        headers={            "Authorization": f"Bearer {api_key}",            "Content-Type": "application/json"        },        json={            "model_name": "kling-v1-6",            "prompt": prompt,            "negative_prompt": "blurry, low quality, watermark",            "cfg_scale": 0.5,            "mode": mode,         # "std" lub "pro"            "aspect_ratio": "16:9",            "duration": duration  # "5" lub "10"        },        timeout=30    )    response.raise_for_status()    return response.json()["data"]["task_id"]​​def poll_kling_task(task_id: str, interval: int = 10, max_wait: int = 600) -> str:    """Odpytuj zadanie Kling aż do zakończenia. Zwraca URL wideo."""    api_key = os.getenv("COMETAPI_KEY")    if not api_key:        raise ValueError("Zmienna środowiskowa COMETAPI_KEY nie jest ustawiona")​    headers = {"Authorization": f"Bearer {api_key}"}    url = f"https://api.cometapi.com/kling/v1/videos/text2video/{task_id}"    elapsed = 0​    while elapsed < max_wait:        response = requests.get(url, headers=headers, timeout=30)        response.raise_for_status()        result = response.json()        status = result["data"]["task_status"]​        if status == "succeed":  # Kling używa "succeed", nie "succeeded"            return result["data"]["task_result"]["videos"][0]["url"]        elif status == "failed":            error_detail = result.get("data", {}).get("task_result", "brak szczegółów")            raise RuntimeError(                f"Zadanie Kling {task_id} zakończyło się niepowodzeniem: {error_detail}"            )​        time.sleep(interval)        elapsed += interval​    raise TimeoutError(f"Zadanie Kling {task_id} przekroczyło limit czasu po {max_wait}s")

Źródło**:** Dokumentacja CometAPI Kling Video

Animuj statyczny obraz z Runway

Runway obsługuje wyłącznie image-to-video. Wymaga też dodatkowego nagłówka (X-Runway-Version):

def submit_runway_task(image_url: str, motion_prompt: str, duration: int = 5) -> str:    """Wyślij zadanie Runway image-to-video. Zwraca task_id."""    api_key = os.getenv("COMETAPI_KEY")    if not api_key:        raise ValueError("Zmienna środowiskowa COMETAPI_KEY nie jest ustawiona")​    response = requests.post(        "https://api.cometapi.com/runwayml/v1/image_to_video",        headers={            "Authorization": f"Bearer {api_key}",            "X-Runway-Version": "2024-11-06",            "Content-Type": "application/json"        },        json={            "model": "gen3a_turbo",            "promptImage": image_url,  # musi to być stabilny adres HTTPS            "promptText": motion_prompt,            "duration": duration,            "ratio": "1280:720",            "watermark": False        },        timeout=30    )    response.raise_for_status()    return response.json()["id"]​​def poll_runway_task(task_id: str, interval: int = 5, max_wait: int = 600) -> str:    """Odpytuj zadanie Runway. Zwraca URL wideo po zakończeniu."""    api_key = os.getenv("COMETAPI_KEY")    if not api_key:        raise ValueError("Zmienna środowiskowa COMETAPI_KEY nie jest ustawiona")​    headers = {        "Authorization": f"Bearer {api_key}",        "X-Runway-Version": "2024-11-06"    }    url = f"https://api.cometapi.com/runwayml/v1/tasks/{task_id}"    elapsed = 0​    while elapsed < max_wait:        response = requests.get(url, headers=headers, timeout=30)        response.raise_for_status()        result = response.json()        status = result.get("status")​        if status == "task_not_exist":            # Specyficzne dla CometAPI: zadanie wciąż się inicjalizuje, spróbuj ponownie za kilka sekund            time.sleep(interval)            elapsed += interval            continue        elif status == "succeeded":            return result["output"][0]        elif status in ("failed", "cancelled"):            raise RuntimeError(f"Zadanie Runway {task_id} zakończyło się niepowodzeniem: {result.get('error', 'brak szczegółów')}")​        time.sleep(interval)        elapsed += interval​    raise TimeoutError(f"Zadanie Runway {task_id} przekroczyło limit czasu po {max_wait}s")

Źródło**:** Dokumentacja CometAPI Runway

Zapisz wideo zanim adres URL wygaśnie

Adresy URL wideo z API generowania są tymczasowe. Pobierz plik od razu i zapisz go w miejscu, którym zarządzasz:

import requestsimport pathlib​def download_video(url: str, output_path: str) -> None:    """Pobierz wideo z URL do pliku lokalnego strumieniowo."""    out = pathlib.Path(output_path)    if out.parent != pathlib.Path("."):        out.parent.mkdir(parents=True, exist_ok=True)​    with requests.get(url, stream=True, timeout=60) as r:        r.raise_for_status()        with open(out, "wb") as f:            for chunk in r.iter_content(chunk_size=8192):                f.write(chunk)    print(f"Zapisano do {output_path}")​​# Pełny przepływtask_id = submit_veo_task("Timelapse chmur przemieszczających się nad panoramą miasta")video_url = poll_veo_task(task_id)download_video(video_url, "output/city_timelapse.mp4")

W produkcji zamień zapis lokalny na upload do S3, Cloudflare R2 lub wybranego magazynu. Wzorzec strumieniowania pozostaje taki sam — przekazuj bajty bezpośrednio, zamiast ładować całe wideo do pamięci.

Obsługa błędów

ObjawPrawdopodobna przyczynaRozwiązanie
Zadanie utknęło w queued przez 10+ minObciążenie serwera lub model niedostępnySpróbuj innego modelu
task_not_exist przy pierwszym pollu RunwayZadanie wciąż się inicjalizujePoczekaj 5 s i ponów — udokumentowane zachowanie CometAPI
failed bez komunikatu błęduPrompt wywołał filtr treściPrzeformułuj prompt
URL wideo zwraca 403Adres wygasł przed pobraniemPobierz od razu po otrzymaniu adresu URL
Timeout po 10 minGenerowanie trwało zbyt długoZwiększ max_wait lub przejdź na Veo 3 Fast
Kling zwraca „succeed”, nie „succeeded”API Kling używa niestandardowego statusuTo poprawne — patrz kod odpytywania Kling powyżej

Źródło: Dokumentacja CometAPI dla generowania wideo

Wersja Node.js

Node.js 18+ ma natywnie fetch i FormData. Ten przykład obejmuje wszystkie cztery modele:

// Node.js 18+ — brak potrzeby dodatkowych paczek​const API_KEY = process.env.COMETAPI_KEY;if (!API_KEY) throw new Error('COMETAPI_KEY nie jest ustawiony');​// --- Veo 3 Fast ---async function submitVeoTask(prompt, size = '16x9') {  const form = new FormData();  form.append('prompt', prompt);  form.append('model', 'veo3-fast');  form.append('size', size);​  const res = await fetch('https://api.cometapi.com/v1/videos', {    method: 'POST',    headers: { 'Authorization': `Bearer ${API_KEY}` },    body: form  });  if (!res.ok) throw new Error(`Nie udało się wysłać Veo: ${res.status}`);  return (await res.json()).id;}​async function pollVeoTask(taskId, intervalMs = 10000, maxWaitMs = 600000) {  let elapsed = 0;  while (elapsed < maxWaitMs) {    const res = await fetch(`https://api.cometapi.com/v1/videos/${taskId}`, {      headers: { 'Authorization': `Bearer ${API_KEY}` }    });    if (!res.ok) throw new Error(`Błąd przy odpytywaniu: ${res.status}`);    const result = await res.json();​    if (result.status === 'succeeded') return result.output[0];    if (['failed', 'cancelled'].includes(result.status)) {      throw new Error(`Zadanie ${taskId} nie powiodło się: ${result.error ?? 'brak szczegółów'}`);    }    await new Promise(r => setTimeout(r, intervalMs));    elapsed += intervalMs;  }  throw new Error(`Zadanie ${taskId} przekroczyło limit czasu`);}​// --- Kling Video ---async function submitKlingTask(prompt, duration = '5', mode = 'std') {  const res = await fetch('https://api.cometapi.com/kling/v1/videos/text2video', {    method: 'POST',    headers: {      'Authorization': `Bearer ${API_KEY}`,      'Content-Type': 'application/json'    },    body: JSON.stringify({      model_name: 'kling-v1-6',      prompt,      negative_prompt: 'blurry, low quality, watermark',      cfg_scale: 0.5,      mode,      aspect_ratio: '16:9',      duration    })  });  if (!res.ok) throw new Error(`Nie udało się wysłać do Kling: ${res.status}`);  return (await res.json()).data.task_id;}​async function pollKlingTask(taskId, intervalMs = 10000, maxWaitMs = 600000) {  let elapsed = 0;  while (elapsed < maxWaitMs) {    const res = await fetch(      `https://api.cometapi.com/kling/v1/videos/text2video/${taskId}`,      { headers: { 'Authorization': `Bearer ${API_KEY}` } }    );    if (!res.ok) throw new Error(`Błąd odpytywania Kling: ${res.status}`);    const result = await res.json();    const status = result.data.task_status;​    if (status === 'succeed') return result.data.task_result.videos[0].url;    if (status === 'failed') {      throw new Error(`Zadanie Kling ${taskId} nie powiodło się: ${JSON.stringify(result.data.task_result ?? 'brak szczegółów')}`);    }    await new Promise(r => setTimeout(r, intervalMs));    elapsed += intervalMs;  }  throw new Error(`Zadanie Kling ${taskId} przekroczyło limit czasu`);}​// --- Runway (image-to-video) ---async function submitRunwayTask(imageUrl, motionPrompt, duration = 5) {  const res = await fetch('https://api.cometapi.com/runwayml/v1/image_to_video', {    method: 'POST',    headers: {      'Authorization': `Bearer ${API_KEY}`,      'X-Runway-Version': '2024-11-06',      'Content-Type': 'application/json'    },    body: JSON.stringify({      model: 'gen3a_turbo',      promptImage: imageUrl,      promptText: motionPrompt,      duration,      ratio: '1280:720',      watermark: false    })  });  if (!res.ok) throw new Error(`Nie udało się wysłać do Runway: ${res.status}`);  return (await res.json()).id;}​async function pollRunwayTask(taskId, intervalMs = 5000, maxWaitMs = 600000) {  let elapsed = 0;  while (elapsed < maxWaitMs) {    const res = await fetch(      `https://api.cometapi.com/runwayml/v1/tasks/${taskId}`,      { headers: { 'Authorization': `Bearer ${API_KEY}`, 'X-Runway-Version': '2024-11-06' } }    );    if (!res.ok) throw new Error(`Błąd odpytywania Runway: ${res.status}`);    const result = await res.json();    const status = result.status;​    if (status === 'task_not_exist') {      // Specyficzne dla CometAPI: zadanie wciąż się inicjalizuje      await new Promise(r => setTimeout(r, intervalMs));      elapsed += intervalMs;      continue;    }    if (status === 'succeeded') return result.output[0];    if (['failed', 'cancelled'].includes(status)) {      throw new Error(`Zadanie Runway ${taskId} nie powiodło się: ${result.error ?? 'brak szczegółów'}`);    }    await new Promise(r => setTimeout(r, intervalMs));    elapsed += intervalMs;  }  throw new Error(`Zadanie Runway ${taskId} przekroczyło limit czasu`);}​// Przykład użyciaconst taskId = await submitVeoTask('Papierowy latawiec unoszący się nad polem pszenicy');const videoUrl = await pollVeoTask(taskId);console.log('Wideo gotowe:', videoUrl);

Co dalej

Masz działający kod dla czterech modeli wideo, pętlę odpytywania obsługującą błędy i krok pobierania, który zapobiega utracie wygenerowanych treści.

Kolejny problem, na który trafia większość deweloperów: zahardcodowali jeden model i przełączenie na tańszą lub szybszą opcję wymaga zmian w wielu plikach. Następny artykuł omawia, jak kierować żądania między modelami bez przepisywania kodu.

Dalej: Jak przełączać się między modelami AI bez przepisywania kodu

FAQ

Q: Dlaczego w odpowiedzi API dostaję identyfikator zadania zamiast wideo?

Generowanie wideo jest asynchroniczne — modele takie jak Veo, Sora, Kling i Runway renderują 2–5 minut. API zwraca identyfikator zadania od razu, aby Twoje żądanie nie przekroczyło limitu czasu. Odpytujesz osobny endpoint statusu aż zadanie osiągnie stan końcowy (succeeded, succeed, failed).

Q: Jak długo wygenerowany adres URL wideo pozostaje ważny?

Adresy URL wideo z API generowania są tymczasowe. Pobierz plik natychmiast po otrzymaniu adresu i zapisz we własnym magazynie (S3, Cloudflare R2 itd.). Nie zapisuj samego URL i nie zakładaj, że zadziała po kilku godzinach.

Q: Czym różni się Veo 3 Fast od Kling Video?

Veo 3 Fast jest tańszy ($0.05/sec), szybszy i prostszy w wywołaniu. Kling Video daje większą kontrolę: negative_prompt, cfg_scale, ustawienia ruchu kamery i tryb jakości pro. Jeśli potrzebujesz precyzyjnego strojenia, użyj Kling. Jeśli zależy Ci na szybkości i niskim koszcie, wybierz Veo 3 Fast.

Q: Czy mogę wygenerować wideo z obrazu zamiast z promptu tekstowego?

Tak. Veo obsługuje image-to-video przez przekazanie pliku input_reference. Kling obsługuje to przez endpoint /kling/v1/videos/image2video z parametrem image (URL lub base64). Runway jest wyłącznie image-to-video — nie akceptuje samych promptów tekstowych przez CometAPI.

Q: Dlaczego Runway zwraca task_not_exist przy pierwszym odpytywaniu?

To udokumentowane zachowanie CometAPI — zadanie wciąż się inicjalizuje w backendzie. Poczekaj kilka sekund i spróbuj ponownie. To nie jest błąd. Kod odpytywania powyżej obsługuje to automatycznie.

Q: Dlaczego Kling używa "succeed" zamiast "succeeded"?

To faktyczny format odpowiedzi API Kling. To nie literówka. Veo i Runway używają "succeeded" — Kling używa "succeed". Jeśli budujesz zunifikowany wrapper do odpytywania, musisz obsłużyć oba ciągi.

Q: Czy synchroniczna pętla odpytywania jest bezpieczna do użycia w serwerze webowym?

Nie. Pętla odpytywania w tym przewodniku blokuje wątek na kilka minut. W prawdziwej usłudze webowej z równoległymi użytkownikami uruchom odpytywanie w procesie pracownika w tle (Celery dla Pythona, Bull dla Node.js). Wyślij zadanie w handlerze żądania, zwróć klientowi identyfikator zadania, a pracownik niech powiadomi klienta, gdy wideo będzie gotowe.

Gotowy na obniżenie kosztów rozwoju AI o 20%?

Zacznij za darmo w kilka minut. Dołączone kredyty na bezpłatny okres próbny. Karta kredytowa nie jest wymagana.

Czytaj więcej