Claude Fable 5 is now on CometAPI — state-of-the-art performance in coding, agents, and scientific research. Try it now

Sådan tilføjer du AI-videogenerering til en SaaS-app

CometAPI
AnnaJun 5, 2026
Sådan tilføjer du AI-videogenerering til en SaaS-app

At tilføje videogenerering til din app er ikke det samme som at tilføje billedgenerering. API-kaldet returnerer med det samme — men videoen er ikke klar endnu. Du får et task-ID, og du skal blive ved med at spørge "er den færdig?" indtil den er.

De fleste udviklere rammer dette første gang, de kalder et video-API, venter på et response body med en video-URL og i stedet får et task-ID tilbage. Denne guide gennemgår det fulde flow: indsende en opgave, polle efter resultater, håndtere fejl og gemme outputtet, før URL’en udløber.

Hvad du skal bygge

En backend-tjeneste, der modtager en tekstprompt eller et billede, indsender en videogenereringsopgave, poller indtil den er færdig, og returnerer den endelige video-URL. Du arbejder med fire modeller — Veo 3 Fast, Sora 2, Kling Video og Runway — alle via en enkelt API-nøgle.

Forudsætninger:

  • Python 3.8+ eller Node.js 18+
  • En CometAPI-nøgle
  • Grundlæggende kendskab til REST-API’er

Forstå hvorfor videogenerering er anderledes

Med billedgenerering sender du en forespørgsel og får billedet tilbage i samme svar. Videogenerering bruger en asynkron opgavekø:

  1. Indsend en genereringsforespørgsel → få et task_id tilbage
  2. Poll et status-endpoint hvert par sekunder
  3. Når status når en terminal tilstand, får du video-URL’en
  4. Download og gem videoen — URL’en er midlertidig

Hvis du behandler videogenerering som billedgenerering og venter på, at den første respons indeholder din video, vil din forespørgsel time out hver gang.

I en produktions-webservice bør denne polling-løkke køre i en baggrundsarbejder (Celery, Bull eller lignende), ikke i din request handler. Eksemplerne nedenfor bruger synkron polling — fint til scripts og prototyper, men ikke til at håndtere samtidige brugere.

Vælg en model

ModelUdbyderMaks. varighedPris (via CometAPI)Velegnet til
Veo 3 FastGoogle8 sek$0.05/secHurtig prototyping, sociale klip
Sora 2OpenAI (via CometAPI model ID)~10 sek$0.08/secKreative kortfilm i høj kvalitet
Kling VideoKuaishou10 sek$0.13–$2.64/taskMarketingindhold, granulær kontrol
Runway Gen-3A TurboRunway5 eller 10 sek$0.32/taskImage-to-video, kommercielt indhold

Kilde: CometAPI modelsider, maj 2026. Bemærk: "Sora 2" er CometAPI’s modelidentifikator — se deres modelside for detaljer om den underliggende model.

  • Veo 3 Fast understøtter både tekst-til-video og billede-til-video. Billigst pr. sekund, godt startpunkt.
  • Sora 2 genererer lyd nativt sammen med videoen — dialog, baggrundslyd og effekter uden et separat TTS-step.
  • Kling Video giver dig negative_prompt, cfg_scale, kamerabevægelser og en pro-tilstand. Mest kontrol af de fire.
  • Runway er kun image-to-video via CometAPI. Giv den et statisk billede og en bevægelsesbeskrivelse, og den animerer det.

Indsend en Veo-opgave

Veo bruger multipart/form-data. Brug files= i Python requests for at sende det korrekt — data=dict sender application/x-www-form-urlencoded, hvilket ikke er det samme:

import requestsimport osfrom dotenv import load_dotenv​load_dotenv()​def submit_veo_task(prompt: str, size: str = "16x9") -> str:    """Submit a Veo 3 Fast text-to-video task. Returns task_id."""    api_key = os.getenv("COMETAPI_KEY")    if not api_key:        raise ValueError("COMETAPI_KEY environment variable is not set")​    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("A paper kite drifting above a wheat field on a windy afternoon")print(f"Task submitted: {task_id}")

Poll efter resultatet

import time​def poll_veo_task(task_id: str, interval: int = 10, max_wait: int = 600) -> str:    """Poll until Veo task completes. Returns video URL."""    api_key = os.getenv("COMETAPI_KEY")    if not api_key:        raise ValueError("COMETAPI_KEY environment variable is not set")​    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"Task {task_id} failed with status '{status}': "                f"{result.get('error', 'no error detail returned')}"            )​        time.sleep(interval)        elapsed += interval​    raise TimeoutError(f"Task {task_id} did not complete within {max_wait} seconds")​​video_url = poll_veo_task(task_id)print(f"Video ready: {video_url}")

Brug Kling Video for mere kontrol

Kling har en anden endpoint-struktur og bruger JSON. Bemærk, at Klings terminale statusstreng er "succeed" (ikke "succeeded") — dette matcher API’ets faktiske responsformat:

def submit_kling_task(prompt: str, duration: str = "5", mode: str = "std") -> str:    """Submit a Kling text-to-video task. Returns task_id."""    api_key = os.getenv("COMETAPI_KEY")    if not api_key:        raise ValueError("COMETAPI_KEY environment variable is not set")​    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" or "pro"            "aspect_ratio": "16:9",            "duration": duration  # "5" or "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:    """Poll Kling task until complete. Returns video URL."""    api_key = os.getenv("COMETAPI_KEY")    if not api_key:        raise ValueError("COMETAPI_KEY environment variable is not set")​    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 uses "succeed", not "succeeded"            return result["data"]["task_result"]["videos"][0]["url"]        elif status == "failed":            error_detail = result.get("data", {}).get("task_result", "no detail")            raise RuntimeError(                f"Kling task {task_id} failed: {error_detail}"            )​        time.sleep(interval)        elapsed += interval​    raise TimeoutError(f"Kling task {task_id} timed out after {max_wait}s")

Kilde: CometAPI Kling Video docs

Animer et statisk billede med Runway

Runway er kun image-to-video. Det kræver også en ekstra header (X-Runway-Version):

def submit_runway_task(image_url: str, motion_prompt: str, duration: int = 5) -> str:    """Submit a Runway image-to-video task. Returns task_id."""    api_key = os.getenv("COMETAPI_KEY")    if not api_key:        raise ValueError("COMETAPI_KEY environment variable is not set")​    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,  # must be a stable HTTPS URL            "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:    """Poll Runway task. Returns video URL when done."""    api_key = os.getenv("COMETAPI_KEY")    if not api_key:        raise ValueError("COMETAPI_KEY environment variable is not set")​    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":            # CometAPI-specific: task is still initializing, retry after a few seconds            time.sleep(interval)            elapsed += interval            continue        elif status == "succeeded":            return result["output"][0]        elif status in ("failed", "cancelled"):            raise RuntimeError(f"Runway task {task_id} failed: {result.get('error', 'no detail')}")​        time.sleep(interval)        elapsed += interval​    raise TimeoutError(f"Runway task {task_id} timed out after {max_wait}s")

Kilde: CometAPI Runway docs

Gem videoen, før URL’en udløber

Video-URL’er fra genererings-API’er er midlertidige. Download filen med det samme og gem den et sted, du kontrollerer:

import requestsimport pathlib​def download_video(url: str, output_path: str) -> None:    """Download video from URL to local file using streaming."""    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"Saved to {output_path}")​​# Full flowtask_id = submit_veo_task("A timelapse of clouds moving over a city skyline")video_url = poll_veo_task(task_id)download_video(video_url, "output/city_timelapse.mp4")

I produktion bør du bytte det lokale filskrivekald ud med en upload til S3, Cloudflare R2 eller din foretrukne storage. Streamingmønsteret er det samme — pip byte direkte i stedet for at læsse hele videoen i hukommelsen.

Håndter fejl

SymptomSandsynlig årsagLøsning
Opgave sidder fast i queued i 10+ minServerbelastning eller model ikke tilgængeligPrøv igen med en anden model
task_not_exist ved første Runway-pollOpgaven initialiserer stadigVent 5 sek og prøv igen — dokumenteret CometAPI-adfærd
failed uden fejlbeskedPrompt udløste indholdsfilterOmformuler prompten
Video-URL returnerer 403URL udløb før downloadDownload straks efter, du får URL’en
Timeout efter 10 minGenereringen tog for lang tidØg max_wait eller skift til Veo 3 Fast
Kling returnerer "succeed" ikke "succeeded"Klings API bruger ikke-standard statusstrengDette er korrekt — se Kling-pollingkoden ovenfor

Kilde: CometAPI video generation docs

Node.js-version

Node.js 18+ inkluderer fetch og FormData nativt. Dette eksempel dækker alle fire modeller:

// Node.js 18+ — no extra packages needed​const API_KEY = process.env.COMETAPI_KEY;if (!API_KEY) throw new Error('COMETAPI_KEY is not set');​// --- 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(`Veo submit failed: ${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(`Poll failed: ${res.status}`);    const result = await res.json();​    if (result.status === 'succeeded') return result.output[0];    if (['failed', 'cancelled'].includes(result.status)) {      throw new Error(`Task ${taskId} failed: ${result.error ?? 'no detail'}`);    }    await new Promise(r => setTimeout(r, intervalMs));    elapsed += intervalMs;  }  throw new Error(`Task ${taskId} timed out`);}​// --- 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(`Kling submit failed: ${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(`Kling poll failed: ${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(`Kling task ${taskId} failed: ${JSON.stringify(result.data.task_result ?? 'no detail')}`);    }    await new Promise(r => setTimeout(r, intervalMs));    elapsed += intervalMs;  }  throw new Error(`Kling task ${taskId} timed out`);}​// --- 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(`Runway submit failed: ${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(`Runway poll failed: ${res.status}`);    const result = await res.json();    const status = result.status;​    if (status === 'task_not_exist') {      // CometAPI-specific: task still initializing      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(`Runway task ${taskId} failed: ${result.error ?? 'no detail'}`);    }    await new Promise(r => setTimeout(r, intervalMs));    elapsed += intervalMs;  }  throw new Error(`Runway task ${taskId} timed out`);}​// Usage exampleconst taskId = await submitVeoTask('A paper kite drifting above a wheat field');const videoUrl = await pollVeoTask(taskId);console.log('Video ready:', videoUrl);

Hvad er det næste

Du har nu fungerende kode til fire videomodeller, en polling-løkke der håndterer fejl, og et download-trin der forhindrer dig i at miste genereret indhold.

Det næste problem, de fleste udviklere rammer: de har hardcodet én model, og at skifte til en billigere eller hurtigere mulighed kræver ændringer i flere filer. Den næste artikel dækker, hvordan du kan route forespørgsler på tværs af modeller uden at omskrive din kode.

Næste: Sådan skifter du mellem AI-modeller uden at omskrive din kode

FAQ

Q: Hvorfor får jeg et task-ID i stedet for en video i API-responsen?

Videogenerering er asynkron — modeller som Veo, Sora, Kling og Runway bruger 2–5 minutter på at rendere. API’et returnerer et task-ID med det samme, så din forespørgsel ikke timeout’er. Du poller et separat status-endpoint, indtil opgaven når en terminal tilstand (succeeded, succeed, failed).

Q: Hvor længe er en genereret video-URL gyldig?

Video-URL’er fra genererings-API’er er midlertidige. Download filen straks efter, du får URL’en, og gem den i din egen storage (S3, Cloudflare R2 osv.). Gem ikke URL’en og forvent, at den virker timer senere.

Q: Hvad er forskellen mellem Veo 3 Fast og Kling Video?

Veo 3 Fast er billigere ($0.05/sec), hurtigere og enklere at kalde. Kling Video giver dig mere kontrol: negative_prompt, cfg_scale, kamerabevægelser og en pro-kvalitetstilstand. Hvis du har brug for at finjustere outputtet, så brug Kling. Hvis du har brug for hastighed og lav pris, så brug Veo 3 Fast.

Q: Kan jeg generere video ud fra et billede i stedet for en tekstprompt?

Ja. Veo understøtter image-to-video ved at sende en input_reference-fil. Kling understøtter det via endpointet /kling/v1/videos/image2video med en image-parameter (URL eller base64). Runway er kun image-to-video — den accepterer ikke tekst-only-prompter via CometAPI.

Q: Hvorfor returnerer Runway task_not_exist ved den første poll?

Dette er dokumenteret CometAPI-adfærd — opgaven er stadig ved at initialisere i backend. Vent et par sekunder og prøv igen. Det er ikke en fejl. Pollingkoden ovenfor håndterer dette automatisk.

Q: Hvorfor bruger Kling "succeed" i stedet for "succeeded"?

Det er Klings faktiske API-responsformat. Det er ikke en slåfejl. Veo og Runway bruger "succeeded" — Kling bruger "succeed". Hvis du bygger en samlet polling-wrapper, skal du håndtere begge strenge.

Q: Er den synkrone polling-løkke sikker at bruge i en webserver?

Nej. Polling-løkken i denne guide blokerer tråden i flere minutter ad gangen. I en rigtig webservice med samtidige brugere skal polling køre i en baggrundsarbejder (Celery for Python, Bull for Node.js). Indsend opgaven i request handleren, returnér task-ID’et til klienten, og lad arbejderen give besked, når videoen er klar.

Klar til at skære AI-udviklingsomkostninger med 20%?

Kom gratis i gang på få minutter. Gratis prøvekreditter inkluderet. Intet kreditkort påkrævet.

Læs mere