Przepisy
Gotowe do uruchomienia przepisy w Pythonie i Node.js dla API PopNow do obrazów, wideo i syntezy mowy AI — obrazy z CSV, zdjęcie na wideo, nagrania lektorskie wiersz po wierszu i alert w Slacku po ukończeniu wideo.
Cztery małe skrypty do zadań, które zwykle automatyzuje się na początku. Każdy jest kompletny: zainstaluj requests (lub użyj Node 18+), ustaw POPNOW_API_KEY i uruchom. Korzystają z tych samych trzech funkcji pomocniczych (wywołanie, oczekiwanie, zapis), więc po przeczytaniu jednego pozostałe sprowadzają się do kilku linii. Strona przepisów na popnow.ai tworzy te same skrypty dla dowolnego wybranego modelu.
| Przepis | Model | Koszt jednego elementu |
|---|---|---|
| Obrazy z pliku CSV | alibaba/z-image-turbo/text-to-image | 0,01 USD za obraz |
| Zdjęcie na wideo | minimax/h3/image-to-video | 0,185 USD za 5 s w 768p |
| Nagranie lektorskie dla każdego wiersza | google/gemini-3.8-flash-tts/text-to-speech | około 0,007 USD za 20 s mowy |
| Alert w Slacku po ukończeniu wideo | minimax/h3/image-to-video | 0,185 USD za 5 s w 768p |
Możesz podmienić model na dowolny ze strony Modele; w input pozostaw tylko pola wymienione w schemacie tego modelu.
Funkcje pomocnicze
Każdy przepis w Pythonie korzysta na końcu z tych funkcji. create wysyła zadanie z kluczem idempotencji, wait odpytuje je z coraz dłuższymi przerwami, aż osiągnie stan końcowy i jego pliki zostaną skopiowane, a save pobiera pierwszy plik.
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)Obrazy z pliku CSV
prompts.csv zawiera kolumnę prompt; każdy wiersz staje się jednym obrazem w out/. Najpierw wysyłane są wszystkie zadania, a potem odbierane wyniki, więc generują się równolegle. Klucz idempotencji powstaje na podstawie wiersza, dlatego ponowne uruchomienie po awarii nie powoduje drugiego obciążenia za już przyjęty wiersz.
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)To samo w Node.js 18+, bez dodatkowych pakietów:
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));
}Zdjęcie na wideo
Przesyła lokalne zdjęcie, a następnie prosi model obraz-na-wideo, by je ożywił. Uruchom jako 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")Jeśli chcesz najpierw przetestować zapytanie bez pisania kodu:
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}}'Nagranie lektorskie dla każdego wiersza
lines.txt zawiera po jednym wierszu tekstu w każdej linii; każdy trafia do osobnego pliku audio w voice/, zawsze tym samym głosem. Opcje voice_id obejmują Kore, Puck, Charon i Aoede. Strona modelu wymienia wszystkie, wraz z style i 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)Alert w Slacku po ukończeniu wideo
Zamiast odpytywania przekaż callBackUrl przy tworzeniu zadania. Niewielki serwer Express sprawdza podpis na surowej treści żądania i wysyła wynik do przychodzącego webhooka Slacka. Ustaw POPNOW_WEBHOOK_SECRET (z konsoli API) i 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);Link w wiadomości na Slacku jest podpisany i działa około 20 minut. Jeśli ktoś może otworzyć go później, dołącz też taskId. Strona Webhooki opisuje ponowienia i duplikaty powiadomień.
Ceny i limity
Każdy przepis zużywa osobne saldo API w USD według zwykłej ceny modelu za każdy element, bez dodatkowych opłat; odczytywanie zadań i przesyłanie plików jest bezpłatne. Nieudane elementy nic nie kosztują, a ponieważ każdy ma własny klucz idempotencji, ponowne uruchomienie skryptu tworzy tylko te zadania, które wcześniej nie zostały przyjęte. Przed dużą partią wyceń jeden element przez jobs/quote i ustaw dzienny limit wydatków klucza (Uwierzytelnianie). Możesz wysłać setki zadań naraz, o ile mieścisz się w limitach zapytań.
Typowe błędy
| Błąd | Przyczyna | Rozwiązanie |
|---|---|---|
KeyError: 'POPNOW_API_KEY' | Zmienna nie jest ustawiona w tej powłoce | export POPNOW_API_KEY="sk-popnow-..." |
RuntimeError: 400 … z nazwą pola | Model nie przyjmuje tego pola lub wartości | Sprawdź stronę modelu; pola różnią się między modelami |
RuntimeError: 40201 … | Za mało USD w API na następne zadanie | Doładuj API w rozliczeniach API; przyjęte zadania nadal działają |
RuntimeError: 429 … | Zbyt wiele zapytań naraz | Wysyłaj mniejsze grupy i respektuj Retry-After |
RuntimeError: failed: … | Jeden element się nie powiódł lub został odrzucony | Nic za niego nie pobrano; popraw wiersz i uruchom ponownie |
RuntimeError: 40003 … przy zatwierdzaniu przesłania | Przesłane bajty nie odpowiadają danym zgłoszenia przesyłania | Poproś o nowe zgłoszenie i prześlij plik ponownie |
Webhook zwraca 401 | Podpis obliczono na sparsowanym JSON-ie zamiast surowych bajtów albo sekret jest błędny | Użyj express.raw i sekretu z konsoli API |
Czy istnieje oficjalne SDK dla Pythona lub Node?
Tak, oba mają otwarty kod na GitHubie i są opublikowane jako popnow w PyPI i npm: pip install popnow oraz npm install popnow. Szczegóły znajdziesz na stronie SDK, CLI i narzędzia open source. Te przepisy korzystają z requests i wbudowanego fetch, by nie wymagały instalowania niczego więcej.
Ile zadań może działać naraz w jednej partii?
Wiele: wyślij wszystkie, a potem odbierz wyniki. Przestrzegaj limitów opisanych na stronie limitów zapytań i ustaw dzienny limit wydatków klucza używanego do przetwarzania partii.
Co się stanie, jeśli skrypt przerwie się w połowie partii?
Uruchom go ponownie. Każdy wiersz używa tego samego klucza idempotencji co wcześniej, więc już przyjęte wiersze zwrócą pierwotne zadanie zamiast powodować drugie obciążenie.
Powiązane
Poradnik: zamiana obrazów w wideo
Ożyw zdjęcie, wybierz pierwszą i ostatnią klatkę, użyj obrazów referencyjnych, przedłuż klip i trzymaj koszty w ryzach.
Lista kontrolna przed wdrożeniem
Co sprawdzić, zanim prawdziwi użytkownicy zaczną polegać na twojej integracji — klucze, ponowienia, webhooki, przechowywanie, koszty i monitoring.

