PopNowDokumentacja API
Poradniki
Treść główna

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.

PrzepisModelKoszt jednego elementu
Obrazy z pliku CSValibaba/z-image-turbo/text-to-image0,01 USD za obraz
Zdjęcie na wideominimax/h3/image-to-video0,185 USD za 5 s w 768p
Nagranie lektorskie dla każdego wierszagoogle/gemini-3.8-flash-tts/text-to-speechokoło 0,007 USD za 20 s mowy
Alert w Slacku po ukończeniu wideominimax/h3/image-to-video0,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.

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)

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.

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)

To samo w Node.js 18+, bez dodatkowych pakietów:

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));
}

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.

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")

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.

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)

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.

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);

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łądPrzyczynaRozwiązanie
KeyError: 'POPNOW_API_KEY'Zmienna nie jest ustawiona w tej powłoceexport POPNOW_API_KEY="sk-popnow-..."
RuntimeError: 400 … z nazwą polaModel nie przyjmuje tego pola lub wartościSprawdź stronę modelu; pola różnią się między modelami
RuntimeError: 40201 …Za mało USD w API na następne zadanieDoładuj API w rozliczeniach API; przyjęte zadania nadal działają
RuntimeError: 429 …Zbyt wiele zapytań narazWysyłaj mniejsze grupy i respektuj Retry-After
RuntimeError: failed: …Jeden element się nie powiódł lub został odrzuconyNic za niego nie pobrano; popraw wiersz i uruchom ponownie
RuntimeError: 40003 … przy zatwierdzaniu przesłaniaPrzesłane bajty nie odpowiadają danym zgłoszenia przesyłaniaPoproś o nowe zgłoszenie i prześlij plik ponownie
Webhook zwraca 401Podpis obliczono na sparsowanym JSON-ie zamiast surowych bajtów albo sekret jest błędnyUż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.

Na tej stronie