Recettes
Recettes Python et Node.js à copier et lancer pour l’API PopNow AI d’images, de vidéos et de synthèse vocale : images en lot depuis un CSV, photo vers vidéo, voix off ligne par ligne, et alerte Slack quand une vidéo est prête.
Quatre petits scripts pour les tâches que l’on automatise en premier. Chacun est complet : installez requests (ou utilisez Node 18+), définissez POPNOW_API_KEY, puis lancez-le. Ils partagent les mêmes trois fonctions (call, wait, save) ; une fois la première lue, les autres ne font que quelques lignes de plus. La page Recettes de popnow.ai génère les mêmes scripts pour le modèle de votre choix.
| Recette | Modèle | Coût par élément |
|---|---|---|
| Images en lot depuis un CSV | alibaba/z-image-turbo/text-to-image | $0.01 par image |
| Photo vers vidéo | minimax/h3/image-to-video | $0.185 pour 5 s en 768p |
| Une voix off par ligne | google/gemini-3.8-flash-tts/text-to-speech | environ $0.007 par 20 s de parole |
| Alerte Slack quand une vidéo est prête | minimax/h3/image-to-video | $0.185 pour 5 s en 768p |
Remplacez le modèle par un autre de la page Modèles ; ne gardez dans input que les champs listés dans le schéma de ce modèle.
Les fonctions communes
Chaque recette Python se termine par ces fonctions. create envoie une tâche avec une clé d’idempotence, wait interroge la tâche avec un délai croissant jusqu’à ce qu’elle soit finale et que ses fichiers soient copiés, save télécharge le premier fichier.
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)Images en lot depuis un CSV
prompts.csv a une colonne prompt ; chaque ligne devient une image dans out/. Toutes les tâches sont envoyées d’abord et récupérées ensuite, si bien qu’elles s’exécutent en parallèle. La clé d’idempotence est dérivée de la ligne : relancer après un plantage ne facture donc jamais deux fois une ligne déjà passée.
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)La même chose en Node.js 18+, sans aucun paquet :
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));
}Photo vers vidéo
Envoie une photo locale, puis demande à un modèle image-vers-vidéo de lui donner vie. Lancez-le avec 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")Pour tester la requête sans code, d’abord :
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}}'Une voix off par ligne
lines.txt contient une réplique par ligne ; chacune devient un fichier audio distinct dans voice/, toutes avec la même voix. Les options de voice_id incluent Kore, Puck, Charon et Aoede ; la page du modèle les liste toutes, ainsi que style et 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)Alerte Slack quand une vidéo est prête
Plutôt que d’interroger la tâche, passez callBackUrl lors de sa création. Un petit serveur Express vérifie la signature sur le corps brut et publie le résultat dans un webhook entrant Slack. Définissez POPNOW_WEBHOOK_SECRET (depuis la console API) et 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);Le lien du message Slack est signé et fonctionne une vingtaine de minutes ; ajoutez aussi le taskId si l’on peut l’ouvrir plus tard. Webhooks traite des nouvelles tentatives et des livraisons en double.
Tarifs et limites
Chaque recette dépense le solde API séparé en USD au prix normal du modèle par élément, et rien d’autre ; la lecture des tâches et l’envoi de fichiers sont gratuits. Les éléments échoués ne coûtent rien, et comme chaque élément a sa propre clé d’idempotence, relancer un script ne crée que les éléments jamais passés. Avant un gros lot, chiffrez un élément avec jobs/quote et fixez un plafond quotidien sur la clé (Authentification). Soumettre des centaines de tâches d’un coup ne pose pas de problème, dans les limites de débit.
Erreurs courantes
| Erreur | Cause | Solution |
|---|---|---|
KeyError: 'POPNOW_API_KEY' | La variable n’est pas définie dans ce shell | export POPNOW_API_KEY="sk-popnow-..." |
RuntimeError: 400 … avec un nom de champ | Le modèle n’accepte pas ce champ ou cette valeur | Consultez la page du modèle ; les champs varient selon le modèle |
RuntimeError: 40201 … | Pas assez d’USD API pour la prochaine tâche | Approvisionnez Facturation API ; les tâches déjà acceptées continuent |
RuntimeError: 429 … | Trop de requêtes à la fois | Envoyez par groupes plus petits et respectez Retry-After |
RuntimeError: failed: … | Un élément a échoué ou a été refusé | Rien n’a été facturé pour lui ; corrigez cette ligne et relancez |
RuntimeError: 40003 … à la validation | Les octets envoyés ne correspondent pas au ticket | Demandez un nouveau ticket et envoyez à nouveau |
Le webhook renvoie 401 | Signature calculée sur le JSON analysé au lieu des octets bruts, ou mauvais secret | Utilisez express.raw, et le secret de la console API |
Existe-t-il un SDK officiel Python ou Node ?
Oui : les deux sont open source sur GitHub et publiés sous le nom popnow sur PyPI et npm : pip install popnow et npm install popnow. SDK, CLI et outils open source donne les détails. Ces recettes s’en tiennent à requests et au fetch intégré, pour fonctionner sans rien installer de plus.
Combien de tâches un lot peut-il lancer en même temps ?
Beaucoup : envoyez-les toutes, puis récupérez-les. Restez dans les limites de débit de la page Rate limits, et fixez un plafond de dépense quotidien sur la clé que vous utilisez pour les lots.
Que se passe-t-il si mon script plante au milieu d’un lot ?
Relancez-le. Chaque ligne utilise la même clé d’idempotence qu’auparavant, donc les lignes déjà acceptées renvoient leur tâche d’origine au lieu d’être facturées deux fois.
Voir aussi
Guide : transformer des images en vidéo
Animer une photo, choisir la première et la dernière image, utiliser des images de référence, prolonger un clip et garder les coûts prévisibles.
Checklist de mise en production
Ce qu’il faut vérifier avant que de vrais utilisateurs dépendent de votre intégration — clés, nouvelles tentatives, webhooks, stockage, coûts et supervision.

