Ricette
Ricette Python e Node.js pronte da copiare ed eseguire per l’API PopNow di generazione di immagini, video e sintesi vocale - immagini in batch da un CSV, da foto a video, voiceover riga per riga e un avviso Slack quando un video è pronto.
Quattro piccoli script per i lavori che si automatizzano per primi. Ognuno è completo: installa requests (oppure usa Node 18+), imposta POPNOW_API_KEY ed eseguilo. Condividono gli stessi tre helper (chiamata, attesa, salvataggio), quindi una volta letto uno, gli altri sono poche righe in più. La pagina Ricette su popnow.ai costruisce gli stessi script per qualsiasi modello tu scelga.
| Ricetta | Modello | Costo per elemento |
|---|---|---|
| Immagini in batch da un CSV | alibaba/z-image-turbo/text-to-image | $0.01 per immagine |
| Da foto a video | minimax/h3/image-to-video | $0.185 per 5 s a 768p |
| Un voiceover per riga | google/gemini-3.8-flash-tts/text-to-speech | circa $0.007 per 20 s di parlato |
| Avviso Slack quando un video è pronto | minimax/h3/image-to-video | $0.185 per 5 s a 768p |
Cambia modello con un altro qualsiasi da Modelli; mantieni i campi input a quelli elencati nello schema di quel modello.
Gli helper
Ogni ricetta Python termina con questi. create invia un’attività con una chiave di idempotenza, wait controlla lo stato con un’attesa crescente finché l’attività non è finale e i suoi file sono stati copiati, save scarica il primo file.
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)Immagini in batch da un CSV
prompts.csv ha una colonna prompt; ogni riga diventa un’immagine in out/. Tutte le attività vengono inviate per prime e raccolte dopo, così vengono generate in parallelo. La chiave di idempotenza deriva dalla riga, quindi rilanciare dopo un crash non addebita mai due volte una riga già passata.
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)Lo stesso in Node.js 18+, senza alcun pacchetto:
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));
}Da foto a video
Carica una foto locale, poi chiede a un modello da immagine a video di darle vita. Eseguilo come 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")Prova prima la richiesta senza codice:
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}}'Un voiceover per riga
lines.txt contiene una riga di copione per riga; ognuna diventa un proprio file audio in voice/, tutti con la stessa voce. Le opzioni di voice_id includono Kore, Puck, Charon e Aoede; la pagina del modello le elenca tutte, insieme a style e 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)Avviso Slack quando un video è pronto
Invece del polling, passa callBackUrl quando crei l’attività. Un piccolo server Express verifica la firma sul corpo grezzo e pubblica il risultato su un webhook in entrata di Slack. Imposta POPNOW_WEBHOOK_SECRET (dalla console API) e 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);Il link nel messaggio Slack è firmato e vale per circa 20 minuti; includi anche il taskId se le persone potrebbero aprirlo più tardi. Webhook copre i nuovi tentativi e le consegne duplicate.
Prezzi e limiti
Ogni ricetta usa il saldo API separato in USD al prezzo normale del modello per elemento e nient’altro; leggere le attività e caricare i file è gratuito. Gli elementi falliti non costano nulla e, poiché ogni elemento ha la propria chiave di idempotenza, rilanciare uno script crea solo gli elementi che non sono mai passati. Prima di un batch grande, calcola il prezzo di un elemento con jobs/quote e imposta un tetto giornaliero sulla chiave (Autenticazione). Inviare centinaia di attività insieme va bene, entro i limiti di frequenza.
Errori comuni
| Errore | Perché | Soluzione |
|---|---|---|
KeyError: 'POPNOW_API_KEY' | La variabile non è impostata in questa shell | export POPNOW_API_KEY="sk-popnow-..." |
RuntimeError: 400 … con un nome di campo | Il modello non accetta quel campo o quel valore | Controlla la pagina del modello; i campi cambiano da modello a modello |
RuntimeError: 40201 … | USD API insufficienti per la prossima attività | Aggiungi fondi in Fatturazione API; le attività già accettate continuano a girare |
RuntimeError: 429 … | Troppe richieste contemporaneamente | Invia in gruppi più piccoli e rispetta Retry-After |
RuntimeError: failed: … | Un elemento è fallito o è stato rifiutato | Per quell’elemento non è stato addebitato nulla; correggi quella riga e rilancia |
RuntimeError: 40003 … al commit | I byte caricati non corrispondono al ticket | Richiedi un nuovo ticket e carica di nuovo |
Il webhook restituisce 401 | Firma calcolata sul JSON già analizzato invece che sui byte grezzi, oppure segreto sbagliato | Usa express.raw e il segreto dalla console API |
C’è un SDK ufficiale Python o Node?
Sì, entrambi sono open source su GitHub e pubblicati come popnow su PyPI e npm: pip install popnow e npm install popnow. SDK, CLI e strumenti open source riporta i dettagli. Queste ricette si limitano a requests e al fetch integrato, così girano senza installare nulla in più.
Quante attività può far girare un batch insieme?
Molte: inviale tutte, poi raccogli i risultati. Resta entro i limiti di frequenza descritti nella pagina dedicata e imposta un tetto di spesa giornaliero sulla chiave che usi per i batch.
Cosa succede se il mio script si interrompe a metà di un batch?
Rilancialo. Ogni riga usa la stessa chiave di idempotenza di prima, quindi le righe già accettate restituiscono la loro attività originale invece di essere addebitate di nuovo.
Correlati
Guida: trasforma le immagini in video
Anima una foto, scegli il primo e l’ultimo fotogramma, usa immagini di riferimento, allunga una clip e tieni i costi prevedibili.
Checklist per la produzione
Cosa verificare prima che utenti reali dipendano dalla tua integrazione: chiavi, nuovi tentativi, webhook, archiviazione, costi e monitoraggio.

