PopNowDocs API
Guides
Contenu principal

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.

RecetteModèleCoût par élément
Images en lot depuis un CSValibaba/z-image-turbo/text-to-image$0.01 par image
Photo vers vidéominimax/h3/image-to-video$0.185 pour 5 s en 768p
Une voix off par lignegoogle/gemini-3.8-flash-tts/text-to-speechenviron $0.007 par 20 s de parole
Alerte Slack quand une vidéo est prêteminimax/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.

helpers.py
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.

batch_images.py
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 :

batch_images.mjs
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.

photo_to_video.py
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.

voiceover.py
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.

notify.mjs (npm install express)
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

ErreurCauseSolution
KeyError: 'POPNOW_API_KEY'La variable n’est pas définie dans ce shellexport POPNOW_API_KEY="sk-popnow-..."
RuntimeError: 400 … avec un nom de champLe modèle n’accepte pas ce champ ou cette valeurConsultez la page du modèle ; les champs varient selon le modèle
RuntimeError: 40201 …Pas assez d’USD API pour la prochaine tâcheApprovisionnez Facturation API ; les tâches déjà acceptées continuent
RuntimeError: 429 …Trop de requêtes à la foisEnvoyez 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 validationLes octets envoyés ne correspondent pas au ticketDemandez un nouveau ticket et envoyez à nouveau
Le webhook renvoie 401Signature calculée sur le JSON analysé au lieu des octets bruts, ou mauvais secretUtilisez 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.

Sur cette page