Code-Rezepte
Direkt ausführbare Python- und Node.js-Rezepte für die PopNow API für KI-Bilder, Video und Text-to-Speech – Bildserien aus CSV-Dateien, Foto zu Video, zeilenweise Sprachaufnahmen und eine Slack-Nachricht, sobald ein Video fertig ist.
Vier kleine Skripte für die Aufgaben, die häufig zuerst automatisiert werden. Jedes ist vollständig: requests installieren (oder Node 18+ nutzen), POPNOW_API_KEY setzen, ausführen. Alle nutzen dieselben drei Hilfsfunktionen für Aufruf, Warten und Speichern. Hast du ein Rezept gelesen, bestehen die anderen nur noch aus wenigen eigenen Zeilen. Die Recipes-Seite auf popnow.ai erstellt dieselben Skripte für jedes Modell deiner Wahl.
| Rezept | Modell | Kosten pro Element |
|---|---|---|
| Bildserien aus einer CSV-Datei | alibaba/z-image-turbo/text-to-image | $0.01 pro Bild |
| Foto zu Video | minimax/h3/image-to-video | $0.185 für 5 s bei 768p |
| Eine Sprachaufnahme pro Zeile | google/gemini-3.8-flash-tts/text-to-speech | etwa $0.007 pro 20 s Sprache |
| Slack-Nachricht, sobald ein Video fertig ist | minimax/h3/image-to-video | $0.185 für 5 s bei 768p |
Du kannst jedes andere Modell unter Modelle einsetzen. Verwende nur die input-Felder, die im Schema dieses Modells stehen.
Die Hilfsfunktionen
Jedes Python-Rezept verwendet am Ende diese Funktionen. create sendet einen Task mit Idempotency-Key, wait fragt mit wachsenden Abständen ab, bis der Task einen Endzustand erreicht hat und seine Dateien kopiert wurden, und save lädt die erste Datei herunter.
import os, time
import requests
BASE = "https://api.popnow.ai/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['POPNOW_API_KEY']}"}
def call(method, path, **kwargs):
headers = {**HEADERS, **kwargs.pop("headers", {})}
body = requests.request(method, BASE + path, headers=headers, timeout=60, **kwargs).json()
if body["code"] != 200:
raise RuntimeError(f'{body["code"]} {body["msg"]}')
return body["data"]
def create(payload, key):
return call("POST", "/jobs/createTask", json=payload, headers={"Idempotency-Key": key})["taskId"]
def wait(task_id):
delay = 2.0
while True:
time.sleep(delay)
task = call("GET", "/jobs/recordInfo", params={"taskId": task_id})
assets = (task.get("output") or {}).get("assets") or []
copying = any(a.get("pending") and not a.get("unavailable") for a in assets)
if task["state"] in ("succeeded", "failed", "canceled", "expired") and not copying:
return task
delay = min(delay * 1.5, 15.0)
def save(task, path):
if task["state"] != "succeeded":
raise RuntimeError(f'{task["state"]}: {task.get("errorMessage", "no detail")}')
url = task["output"]["assets"][0]["url"]
with open(path, "wb") as f:
f.write(requests.get(url, timeout=300).content)Bildserien aus einer CSV-Datei
prompts.csv enthält eine Spalte prompt; jede Zeile wird zu einem Bild in out/. Zuerst werden alle Tasks gesendet und danach die Ergebnisse abgeholt, sodass die Bilder parallel entstehen. Der Idempotency-Key wird aus der Zeile abgeleitet. Nach einem Absturz erneut zu starten kostet deshalb für bereits angenommene Zeilen nichts extra.
import csv, hashlib, pathlib
from helpers import create, wait, save
MODEL = "alibaba/z-image-turbo/text-to-image"
out = pathlib.Path("out")
out.mkdir(exist_ok=True)
with open("prompts.csv", newline="", encoding="utf-8") as f:
rows = [r["prompt"] for r in csv.DictReader(f) if r.get("prompt")]
tasks = []
for i, prompt in enumerate(rows, start=1):
key = "batch-" + hashlib.sha256(f"{i}:{prompt}".encode()).hexdigest()[:32]
tasks.append((i, create({"model": MODEL, "input": {"prompt": prompt, "aspect_ratio": "1:1"}}, key)))
for i, task_id in tasks:
save(wait(task_id), out / f"{i:03d}.png")
print("saved", i)Dasselbe mit Node.js 18+, ohne zusätzliche Pakete:
import { createHash } from 'node:crypto';
import { readFile, writeFile, mkdir } from 'node:fs/promises';
const API = 'https://api.popnow.ai/api/v1';
const auth = { Authorization: `Bearer ${process.env.POPNOW_API_KEY}` };
const call = async (path, init = {}) => {
const body = await fetch(API + path, { ...init, headers: { ...auth, ...init.headers } }).then((r) => r.json());
if (body.code !== 200) throw new Error(`${body.code} ${body.msg}`);
return body.data;
};
const prompts = (await readFile('prompts.txt', 'utf8')).split('\n').map((l) => l.trim()).filter(Boolean);
await mkdir('out', { recursive: true });
const ids = await Promise.all(prompts.map((prompt, i) => call('/jobs/createTask', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Idempotency-Key': 'batch-' + createHash('sha256').update(`${i}:${prompt}`).digest('hex').slice(0, 32) },
body: JSON.stringify({ model: 'alibaba/z-image-turbo/text-to-image', input: { prompt, aspect_ratio: '1:1' } }),
}).then((d) => d.taskId)));
for (const [i, id] of ids.entries()) {
let task;
do {
await new Promise((r) => setTimeout(r, 3000));
task = await call(`/jobs/recordInfo?taskId=${id}`);
} while (task.state === 'queued' || task.state === 'running');
if (task.state !== 'succeeded') { console.error(i, task.state, task.errorMessage); continue; }
const file = await fetch(task.output.assets[0].url).then((r) => r.arrayBuffer());
await writeFile(`out/${String(i + 1).padStart(3, '0')}.png`, Buffer.from(file));
}Foto zu Video
Lädt ein lokales Foto hoch und lässt es von einem Bild-zu-Video-Modell animieren. Starte es mit python photo_to_video.py photo.jpg.
import mimetypes, sys, uuid
import requests
from helpers import call, create, wait, save
MODEL = "minimax/h3/image-to-video"
def upload(path):
"""Local file -> popnow:// reference you can put in any image, video or audio field."""
data = open(path, "rb").read()
kind = mimetypes.guess_type(path)[0] or "image/jpeg"
ticket = call("POST", "/common/upload-url", json={"contentType": kind, "bytes": len(data)})
requests.put(ticket["uploadUrl"], data=data, headers=ticket.get("headers", {}), timeout=120).raise_for_status()
return call("POST", f'/files/{ticket["fileId"]}/commit')["uri"]
ref = upload(sys.argv[1])
task_input = {"image_url": ref, "prompt": "Gentle camera push-in, hair moving in the wind", "duration_seconds": 5, "resolution": "768p"}
task_id = create({"model": MODEL, "input": task_input}, str(uuid.uuid4()))
save(wait(task_id), "clip.mp4")
print("saved clip.mp4")Die Anfrage zuerst ohne eigenen Code testen:
curl -X POST "https://api.popnow.ai/api/v1/jobs/createTask" \
-H "Authorization: Bearer $POPNOW_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"model": "minimax/h3/image-to-video", "input": {"image_url": "https://media.popnow.ai/movie/lighthouse.webp", "prompt": "The waves roll in slowly", "duration_seconds": 5}}'Eine Sprachaufnahme pro Zeile
lines.txt enthält pro Zeile einen Textabschnitt; jeder wird zu einer eigenen Audiodatei in voice/, alle mit derselben Stimme. Zu den Optionen für voice_id gehören Kore, Puck, Charon und Aoede. Die Modellseite führt alle Stimmen auf, ebenso style und accent.
import hashlib, pathlib
from helpers import create, wait, save
MODEL = "google/gemini-3.8-flash-tts/text-to-speech"
out = pathlib.Path("voice")
out.mkdir(exist_ok=True)
lines = [l.strip() for l in open("lines.txt", encoding="utf-8") if l.strip()]
tasks = []
for i, line in enumerate(lines, start=1):
key = "voice-" + hashlib.sha256(f"{i}:{line}".encode()).hexdigest()[:32]
tasks.append((i, create({"model": MODEL, "input": {"text": line, "voice_id": "Kore", "style": "empathetic"}}, key)))
for i, task_id in tasks:
task = wait(task_id)
mime = ((task.get("output") or {}).get("assets") or [{}])[0].get("mime", "")
ext = {"audio/mpeg": ".mp3", "audio/wav": ".wav", "audio/ogg": ".ogg"}.get(mime, ".audio")
save(task, out / f"{i:03d}{ext}")
print("saved", i)Slack-Nachricht, sobald ein Video fertig ist
Statt zu pollen, übergib beim Erstellen des Tasks callBackUrl. Ein kleiner Express-Server prüft die Signatur des rohen Bodys und sendet das Ergebnis an einen eingehenden Slack-Webhook. Setze POPNOW_WEBHOOK_SECRET (aus der API-Konsole) und SLACK_WEBHOOK_URL.
import { createHash, createHmac, randomUUID, timingSafeEqual } from 'node:crypto';
import express from 'express';
const BASE = 'https://api.popnow.ai/api/v1';
// 1. Create the task with a callBackUrl that points at this server.
export async function createVideo() {
const res = await fetch(BASE + '/jobs/createTask', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.POPNOW_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': randomUUID(),
},
body: JSON.stringify({
model: 'minimax/h3/image-to-video',
input: { image_url: 'https://media.popnow.ai/movie/lighthouse.webp', prompt: 'The waves roll in slowly', duration_seconds: 5 },
callBackUrl: 'https://your-server.example.com/hooks/popnow',
}),
});
return (await res.json()).data.taskId;
}
// 2. Receive the finished task, check the signature on the raw body, then post to Slack.
const app = express();
app.post('/hooks/popnow', express.raw({ type: 'application/json' }), async (req, res) => {
const timestamp = req.header('X-Webhook-Timestamp');
const signature = req.header('X-Webhook-Signature') ?? '';
const event = JSON.parse(req.body.toString('utf8'));
const task = event.data;
const digest = createHash('sha256').update(req.body).digest('hex');
const expected = createHmac('sha256', process.env.POPNOW_WEBHOOK_SECRET)
.update(`${task.taskId}.${timestamp}.${digest}`)
.digest('base64');
const ok = expected.length === signature.length && timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
if (!ok || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return res.status(401).end();
res.status(200).end(); // answer first; deliveries are retried until you return 2xx
const text = task.state === 'succeeded'
? `Video ready: ${task.output.assets[0].url}`
: `Task ${task.taskId} ${task.state}: ${task.errorMessage ?? 'no detail'}`;
await fetch(process.env.SLACK_WEBHOOK_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text }) });
});
app.listen(3000);Der Link in der Slack-Nachricht ist signiert und gilt etwa 20 Minuten. Sende auch die taskId, falls jemand die Nachricht später öffnet. Unter Webhooks findest du mehr zu Wiederholungen und doppelten Zustellungen.
Preise und Limits
Jedes Rezept verbraucht das separate API-USD-Guthaben zum normalen Modellpreis pro Element, ohne Aufschlag. Tasks lesen und Dateien hochladen ist kostenlos. Fehlgeschlagene Elemente kosten nichts. Weil jedes Element einen eigenen Idempotency-Key hat, erstellt ein erneuter Skriptlauf nur die Elemente, die noch nicht angenommen wurden. Frag vor großen Serien den Preis eines Elements mit jobs/quote ab und setze ein Tageslimit für den Schlüssel (Authentifizierung). Du kannst Hunderte Tasks gleichzeitig senden, solange du die Ratenlimits einhältst.
Häufige Fehler
| Fehler | Ursache | Lösung |
|---|---|---|
KeyError: 'POPNOW_API_KEY' | Die Variable ist in dieser Shell nicht gesetzt | export POPNOW_API_KEY="sk-popnow-..." |
RuntimeError: 400 … mit einem Feldnamen | Das Modell unterstützt dieses Feld oder diesen Wert nicht | Prüfe die Modellseite; die Felder unterscheiden sich je nach Modell |
RuntimeError: 40201 … | Nicht genug API-USD für den nächsten Task | Unter API-Abrechnung aufladen; bereits angenommene Tasks laufen weiter |
RuntimeError: 429 … | Zu viele gleichzeitige Anfragen | In kleineren Gruppen senden und Retry-After beachten |
RuntimeError: failed: … | Ein Element ist fehlgeschlagen oder wurde abgelehnt | Dafür wurde nichts berechnet; korrigiere die Zeile und starte erneut |
RuntimeError: 40003 … beim Commit | Die hochgeladenen Bytes passen nicht zum Ticket | Ein neues Ticket anfordern und erneut hochladen |
Webhook liefert 401 | Signatur aus geparstem JSON statt aus rohen Bytes berechnet oder falsches Secret | express.raw und das Secret aus der API-Konsole verwenden |
Gibt es ein offizielles Python- oder Node-SDK?
Ja, beide sind Open Source auf GitHub und als popnow auf PyPI und npm veröffentlicht: pip install popnow und npm install popnow. Details stehen unter SDKs, CLI und Open-Source-Tools. Diese Rezepte nutzen requests und das eingebaute fetch, damit sie ohne weitere Installationen laufen.
Wie viele Tasks kann eine Serie gleichzeitig ausführen?
Viele: Sende zuerst alle und hole danach die Ergebnisse ab. Halte die Ratenlimits auf der Seite zu Ratenlimits ein und setze für den Schlüssel, mit dem du Serien ausführst, ein tägliches Ausgabelimit.
Was passiert, wenn mein Skript mitten in einer Serie abstürzt?
Starte es erneut. Jede Zeile verwendet denselben Idempotency-Key wie zuvor. Bereits angenommene Zeilen liefern deshalb ihren ursprünglichen Task zurück, statt doppelt berechnet zu werden.
Verwandte Seiten
Anleitung: Bilder in Videos verwandeln
Ein Foto animieren, erstes und letztes Bild festlegen, Referenzbilder nutzen, einen Clip verlängern und die Kosten im Griff behalten.
Checkliste für den Livegang
Was du prüfen solltest, bevor echte Nutzer von deiner Integration abhängen – Schlüssel, Wiederholungen, Webhooks, Speicher, Kosten und Monitoring.

