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 endnu ikke klar. 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 svar med en video-URL, og i stedet får et task ID tilbage. Denne guide gennemgår hele flowet: at indsende en opgave, polle for resultater, håndtere fejl og gemme output før URL’en udløber.
Hvad du bygger
En backend-tjeneste, der accepterer 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
Ved billedgenerering sender du en anmodning og får billedet tilbage i samme svar. Videogenerering bruger en asynkron opgavekø:
- Indsend en genereringsanmodning → få et
task_id - Poll et status-endpoint hvert par sekunder
- Når status når en terminal tilstand, får du video-URL’en
- Download og gem videoen — URL’en er midlertidig
Hvis du behandler videogenerering som billedgenerering og venter på, at det første svar indeholder din video, vil din anmodning time out hver gang.
I en produktionstjeneste bør denne pollingsløjfe køre i en baggrundsworker (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
| Model | Udbyder | Maks varighed | Pris (via CometAPI) | Bedst til |
|---|---|---|---|---|
| Veo 3 Fast | 8 sek | $0.05/sek | Hurtig prototyping, sociale klip | |
| Sora 2 | OpenAI (via CometAPI model ID) | ~10 sek | $0.08/sek | Kreative kortfilm i høj kvalitet |
| Kling Video | Kuaishou | 10 sek | $0.13–$2.64/opgave | Marketingindhold, granulær kontrol |
| Runway Gen-3A Turbo | Runway | 5 eller 10 sek | $0.32/opgave | Image-to-video, kommercielt indhold |
Kilde*: CometAPI-modellsider, maj 2026. Bemærk: “Sora 2” er CometAPI’s model* identifikator — 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 udgangspunkt.
- Sora 2 genererer lyd nativen sammen med videoen — dialog, baggrundslyd og effekter uden et separat TTS-step.
- Kling Video giver dig
negative_prompt,cfg_scale, kamerabevægelsesindstillinger og enpro-tilstand. Mest kontrol af de fire. - Runway er kun billede-til-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 korrekt — data=dict sender application/x-www-form-urlencoded, hvilket ikke er det samme:
import requestsimport osfrom dotenv import load_dotenvload_dotenv()def submit_veo_task(prompt: str, size: str = "16x9") -> str: """Indsend en Veo 3 Fast tekst-til-video-opgave. Returnerer task_id.""" api_key = os.getenv("COMETAPI_KEY") if not api_key: raise ValueError("Miljøvariablen COMETAPI_KEY er ikke sat") 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("En papirdrage, der driver over en hvedemark på en blæsende eftermiddag")print(f"Opgave indsendt: {task_id}")
Poll for resultatet
import timedef poll_veo_task(task_id: str, interval: int = 10, max_wait: int = 600) -> str: """Poll indtil Veo-opgaven er fuldført. Returnerer video-URL.""" api_key = os.getenv("COMETAPI_KEY") if not api_key: raise ValueError("Miljøvariablen COMETAPI_KEY er ikke sat") 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"Opgave {task_id} fejlede med status '{status}': " f"{result.get('error', 'ingen fejldetaljer returneret')}" ) time.sleep(interval) elapsed += interval raise TimeoutError(f"Opgave {task_id} blev ikke færdig inden for {max_wait} sekunder")video_url = poll_veo_task(task_id)print(f"Video klar: {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: """Indsend en Kling tekst-til-video-opgave. Returnerer task_id.""" api_key = os.getenv("COMETAPI_KEY") if not api_key: raise ValueError("Miljøvariablen COMETAPI_KEY er ikke sat") 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" eller "pro" "aspect_ratio": "16:9", "duration": duration # "5" eller "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-opgave indtil fuldført. Returnerer video-URL.""" api_key = os.getenv("COMETAPI_KEY") if not api_key: raise ValueError("Miljøvariablen COMETAPI_KEY er ikke sat") 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 bruger "succeed", ikke "succeeded" return result["data"]["task_result"]["videos"][0]["url"] elif status == "failed": error_detail = result.get("data", {}).get("task_result", "ingen detaljer") raise RuntimeError( f"Kling-opgave {task_id} mislykkedes: {error_detail}" ) time.sleep(interval) elapsed += interval raise TimeoutError(f"Kling-opgave {task_id} timede out efter {max_wait}s")
Kilde*:* CometAPI Kling Video-dokumentation
Animer et statisk billede med Runway
Runway er kun billede-til-video. Den kræver også en ekstra header (X-Runway-Version):
def submit_runway_task(image_url: str, motion_prompt: str, duration: int = 5) -> str: """Indsend en Runway billede-til-video-opgave. Returnerer task_id.""" api_key = os.getenv("COMETAPI_KEY") if not api_key: raise ValueError("Miljøvariablen COMETAPI_KEY er ikke sat") 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, # skal være en stabil 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-opgave. Returnerer video-URL ved fuldførelse.""" api_key = os.getenv("COMETAPI_KEY") if not api_key: raise ValueError("Miljøvariablen COMETAPI_KEY er ikke sat") 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-specifikt: opgaven initialiseres stadig, prøv igen efter nogle sekunder time.sleep(interval) elapsed += interval continue elif status == "succeeded": return result["output"][0] elif status in ("failed", "cancelled"): raise RuntimeError(f"Runway-opgave {task_id} mislykkedes: {result.get('error', 'ingen detaljer')}") time.sleep(interval) elapsed += interval raise TimeoutError(f"Runway-opgave {task_id} timede out efter {max_wait}s")
Kilde*:* CometAPI Runway-dokumentation
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 pathlibdef download_video(url: str, output_path: str) -> None: """Download video fra URL til lokal fil med 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"Gemt til {output_path}")# Fuldt flowtask_id = submit_veo_task("En timelapse af skyer, der bevæger sig over en bysilhuet")video_url = poll_veo_task(task_id)download_video(video_url, "output/city_timelapse.mp4")
I produktion bør du bytte den lokale filskrivning ud med en upload til S3, Cloudflare R2 eller din foretrukne lagring. Streamingmønstret er det samme — pip bytes direkte i stedet for at indlæse hele videoen i hukommelsen.
Håndtér fejl
| Symptom | Sandsynlig årsag | Løsning |
|---|---|---|
| Opgave sidder fast i queued i 10+ min | Serverbelastning eller model utilgængelig | Prøv igen med en anden model |
| task_not_exist ved første Runway-poll | Opgaven initialiseres stadig | Vent 5 sek og prøv igen — dokumenteret CometAPI-adfærd |
| failed uden fejlmeddelelse | Prompt udløste indholdsfilter | Omformuler prompten |
| Video-URL returnerer 403 | URL udløb før download | Download straks efter du modtager URL’en |
| Timeout efter 10 min | Genereringen tog for lang tid | Øg max_wait eller skift til Veo 3 Fast |
| Kling returnerer "succeed" ikke "succeeded" | Klings API bruger en ikke-standard statusstreng | Dette er korrekt — se Kling-pollingkoden ovenfor |
Kilde: CometAPI videogenereringsdokumentation
Node.js-version
Node.js 18+ inkluderer fetch og FormData nativt. Dette eksempel dækker alle fire modeller:
// Node.js 18+ — ingen ekstra pakker nødvendigeconst API_KEY = process.env.COMETAPI_KEY;if (!API_KEY) throw new Error('COMETAPI_KEY er ikke sat');// --- 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-indsendelse mislykkedes: ${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 mislykkedes: ${res.status}`); const result = await res.json(); if (result.status === 'succeeded') return result.output[0]; if (['failed', 'cancelled'].includes(result.status)) { throw new Error(`Opgave ${taskId} mislykkedes: ${result.error ?? 'ingen detaljer'}`); } await new Promise(r => setTimeout(r, intervalMs)); elapsed += intervalMs; } throw new Error(`Opgave ${taskId} timede 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-indsendelse mislykkedes: ${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 mislykkedes: ${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-opgave ${taskId} mislykkedes: ${JSON.stringify(result.data.task_result ?? 'ingen detaljer')}`); } await new Promise(r => setTimeout(r, intervalMs)); elapsed += intervalMs; } throw new Error(`Kling-opgave ${taskId} timede 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-indsendelse mislykkedes: ${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 mislykkedes: ${res.status}`); const result = await res.json(); const status = result.status; if (status === 'task_not_exist') { // CometAPI-specifikt: opgaven initialiseres stadig 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-opgave ${taskId} mislykkedes: ${result.error ?? 'ingen detaljer'}`); } await new Promise(r => setTimeout(r, intervalMs)); elapsed += intervalMs; } throw new Error(`Runway-opgave ${taskId} timede out`);}// Brugseksempelkconst taskId = await submitVeoTask('A paper kite drifting above a wheat field');const videoUrl = await pollVeoTask(taskId);console.log('Video klar:', videoUrl);
Hvad nu
Du har nu fungerende kode for fire videomodeller, en pollingsløjfe der håndterer fejl, og et download-step der forhindrer, at du mister genereret indhold.
Det næste problem, de fleste udviklere støder på: de har hardkodet én model, og at skifte til en billigere eller hurtigere løsning betyder at røre ved flere filer. Den næste artikel dækker, hvordan du router anmodninger på tværs af modeller uden at omskrive din kode.
Næste: How to Switch Between AI Models Without Rewriting Your Code
FAQ
Sp.: Hvorfor får jeg et task ID i stedet for en video i API-svaret?
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 anmodning ikke timeouter. Du poller et separat status-endpoint, indtil opgaven når en terminal tilstand (succeeded, succeed, failed).
Sp.: Hvor længe er en genereret video-URL gyldig?
Video-URL’er fra genererings-API’er er midlertidige. Download filen straks efter at have fået URL’en, og gem den i din egen lagring (S3, Cloudflare R2 osv.). Gem ikke URL’en og forvent, at den virker timer senere.
Sp.: Hvad er forskellen mellem Veo 3 Fast og Kling Video?
Veo 3 Fast er billigere ($0.05/sek), hurtigere og enklere at kalde. Kling Video giver dig mere kontrol: negative_prompt, cfg_scale, kamerabevægelsesindstillinger og en pro-kvalitetstilstand. Hvis du har brug for at finjustere outputtet, brug Kling. Hvis du har brug for hastighed og lav pris, brug Veo 3 Fast.
Sp.: Kan jeg generere video fra et billede i stedet for en tekstprompt?
Ja. Veo understøtter billede-til-video ved at sende en input_reference-fil. Kling understøtter det via /kling/v1/videos/image2video-endpointet med en image-parameter (URL eller base64). Runway er kun billede-til-video — den accepterer ikke rene tekstprompter via CometAPI.
Sp.: Hvorfor returnerer Runway task_not_exist ved første poll?
Dette er dokumenteret CometAPI-adfærd — opgaven initialiseres stadig i backend. Vent et par sekunder og prøv igen. Det er ikke en fejl. Pollingkoden ovenfor håndterer dette automatisk.
Sp.: Hvorfor bruger Kling "succeed" i stedet for "succeeded"?
Det er Klings faktiske API-responsformat. Det er ikke en tastefejl. Veo og Runway bruger "succeeded" — Kling bruger "succeed". Hvis du bygger en samlet polling-wrapper, skal du håndtere begge strenge.
Sp.: Er den synkrone pollingsløjfe sikker at bruge i en webserver?
Nej. Pollingsløjfen i denne guide blokerer tråden i flere minutter ad gangen. I en rigtig webtjeneste med samtidige brugere skal polling køre i en baggrundsworker (Celery til Python, Bull til Node.js). Indsend opgaven i request-handleren, returnér task ID til klienten, og lad workeren underrette klienten, når videoen er klar.
