Receitas
Receitas prontas para copiar e rodar em Python e Node.js para a API de imagem, vídeo e texto para fala do PopNow AI - imagens em lote a partir de um CSV, foto para vídeo, narrações linha por linha e um alerta no Slack quando um vídeo fica pronto.
Quatro scripts pequenos para as tarefas que as pessoas automatizam primeiro. Cada um está completo: instale requests (ou use Node 18+), defina POPNOW_API_KEY e rode. Todos compartilham os mesmos três auxiliares (chamar, esperar, salvar), então, depois de ler um, os outros são algumas linhas cada. A página de Receitas no popnow.ai monta os mesmos scripts para qualquer modelo que você escolher.
| Receita | Modelo | Custo por item |
|---|---|---|
| Imagens em lote a partir de um CSV | alibaba/z-image-turbo/text-to-image | $0.01 por imagem |
| Foto para vídeo | minimax/h3/image-to-video | $0.185 por 5 s em 768p |
| Uma narração por linha | google/gemini-3.8-flash-tts/text-to-speech | cerca de $0.007 por 20 s de fala |
| Alerta no Slack quando um vídeo fica pronto | minimax/h3/image-to-video | $0.185 por 5 s em 768p |
Troque o modelo por qualquer outro em Modelos; mantenha os campos de input apenas nos que o schema daquele modelo lista.
Os auxiliares
Todas as receitas em Python terminam com estes. create envia uma tarefa com uma chave de idempotência, wait consulta com espera crescente até a tarefa ser final e os arquivos estarem copiados, save baixa o primeiro arquivo.
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)Imagens em lote a partir de um CSV
prompts.csv tem uma coluna prompt; cada linha vira uma imagem em out/. Todas as tarefas são enviadas primeiro e coletadas depois, então são geradas em paralelo. A chave de idempotência é derivada da linha, então rodar de novo depois de uma falha nunca cobra duas vezes uma linha que já foi processada.
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)O mesmo em Node.js 18+, sem nenhum pacote:
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 para vídeo
Envia uma foto local e depois pede a um modelo de imagem para vídeo que a dê vida. Rode assim: 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")Teste a requisição sem código antes:
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}}'Uma narração por linha
lines.txt tem uma linha de roteiro por linha; cada uma vira o seu próprio arquivo de áudio em voice/, todos com a mesma voz. As opções de voice_id incluem Kore, Puck, Charon e Aoede; a página do modelo lista todas, junto com 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)Alerta no Slack quando um vídeo fica pronto
Em vez de consultar, passe callBackUrl ao criar a tarefa. Um pequeno servidor Express verifica a assinatura sobre o corpo bruto e publica o resultado num webhook de entrada do Slack. Defina POPNOW_WEBHOOK_SECRET (obtido no console da 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);O link na mensagem do Slack é assinado e funciona por uns 20 minutos; publique também o taskId, caso as pessoas abram o link depois. Webhooks explica as novas tentativas e as entregas duplicadas.
Preços e limites
Cada receita gasta o saldo separado da API em USD pelo preço normal do modelo por item e mais nada; ler tarefas e enviar arquivos é de graça. Itens que falham não custam nada, e como cada item tem a sua própria chave de idempotência, rodar um script de novo só cria os itens que nunca foram processados. Antes de um lote grande, calcule o preço de um item com jobs/quote e defina um limite diário na chave (Autenticação). Enviar centenas de tarefas de uma vez é aceitável, desde que dentro dos limites de taxa.
Erros comuns
| Erro | Por quê | Correção |
|---|---|---|
KeyError: 'POPNOW_API_KEY' | A variável não está definida neste terminal | export POPNOW_API_KEY="sk-popnow-..." |
RuntimeError: 400 … com um campo no nome | O modelo não aceita esse campo ou valor | Confira a página do modelo; os campos variam por modelo |
RuntimeError: 40201 … | USD insuficientes na API para a próxima tarefa | Adicione fundos em API billing; as tarefas já aceitas continuam rodando |
RuntimeError: 429 … | Requisições demais ao mesmo tempo | Envie em grupos menores e respeite o Retry-After |
RuntimeError: failed: … | Um item falhou ou foi recusado | Nada foi cobrado por ele; corrija essa linha e rode de novo |
RuntimeError: 40003 … no commit | Os bytes enviados não batem com o ticket | Peça um novo ticket e envie de novo |
O webhook retorna 401 | A assinatura foi calculada sobre o JSON já parseado em vez dos bytes brutos, ou o segredo está errado | Use express.raw e o segredo do console da API |
Existe um SDK oficial de Python ou de Node?
Existe, os dois são de código aberto no GitHub e publicados como popnow no PyPI e no npm: pip install popnow e npm install popnow. SDKs, CLI e ferramentas de código aberto traz os detalhes. Estas receitas usam apenas requests e o fetch nativo, para rodar sem nada extra instalado.
Quantas tarefas um lote pode rodar ao mesmo tempo?
Muitas: envie todas e depois colete. Fique dentro dos limites de taxa da página Limites de taxa e coloque um limite de gasto diário na chave que você usa para os lotes.
O que acontece se o meu script travar no meio de um lote?
Rode de novo. Cada linha usa a mesma chave de idempotência de antes, então as linhas que já tinham sido aceitas devolvem a tarefa original em vez de serem cobradas duas vezes.
Relacionados
Guia: transforme imagens em vídeo
Anime uma foto, defina o primeiro e o último quadro, use imagens de referência, estenda um clipe e mantenha os custos previsíveis.
Checklist de produção
O que conferir antes que usuários reais dependam da sua integração — chaves, novas tentativas, webhooks, armazenamento, custos e monitoramento.

