ComfyUI × PopNow

Chmurowe modele AI w ComfyUI

Jeden mały własny węzeł wnosi do twojego grafu ComfyUI wszystkie modele obrazu, wideo i audio PopNow. Bez GPU i pobierania modeli: wysyła zadanie, czeka i przekazuje wynik następnemu węzłowi.

Instalacja w czterech krokach

  1. Zapisz kod poniżej jako custom_nodes/popnow_api.py w folderze ComfyUI.
  2. Ustaw POPNOW_API_KEY w środowisku, z którego uruchamiasz ComfyUI, i uruchom je ponownie.
  3. Kliknij dwukrotnie obszar roboczy, wyszukaj PopNow i dodaj węzeł PopNow Generate.
  4. Wpisz nazwę modelu i prompt, a potem dodaj do kolejki. Obrazy wychodzą jako IMAGE, wideo i audio trafiają do folderu output.

Węzeł

Prosty Python, który korzysta tylko z requests, Pillow, NumPy i PyTorch, dostarczanych już z ComfyUI. Przeczytaj go przed uruchomieniem: jest celowo krótki.

custom_nodes/popnow_api.py
"""PopNow API node for ComfyUI.

Save as ComfyUI/custom_nodes/popnow_api.py, set POPNOW_API_KEY, restart ComfyUI.
Runs any PopNow image, video or audio model in the cloud and returns the result.
Docs: https://popnow.ai/docs
"""
import io
import json
import os
import time
import uuid

import numpy as np
import requests
import torch
from PIL import Image

import folder_paths

BASE = "https://api.popnow.ai/api/v1"
TERMINAL = ("succeeded", "failed", "canceled", "expired")


def _headers(extra=None):
    key = os.environ.get("POPNOW_API_KEY")
    if not key:
        raise RuntimeError("Set POPNOW_API_KEY before starting ComfyUI")
    return {"Authorization": f"Bearer {key}", **(extra or {})}


def _call(method, path, **kwargs):
    headers = _headers(kwargs.pop("headers", None))
    body = requests.request(method, BASE + path, headers=headers, timeout=60, **kwargs).json()
    if body.get("code") != 200:
        raise RuntimeError(f'PopNow {body.get("code")}: {body.get("msg")}')
    return body["data"]


def _upload(image):
    """IMAGE tensor -> PNG -> upload -> popnow:// reference."""
    arr = (image[0].cpu().numpy() * 255).clip(0, 255).astype(np.uint8)
    buf = io.BytesIO()
    Image.fromarray(arr).save(buf, format="PNG")
    data = buf.getvalue()
    ticket = _call("POST", "/common/upload-url", json={"contentType": "image/png", "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"]


class PopNowGenerate:
    CATEGORY = "PopNow"
    RETURN_TYPES = ("IMAGE", "STRING", "STRING")
    RETURN_NAMES = ("image", "url", "path")
    FUNCTION = "run"

    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "model": ("STRING", {"default": "google/nano-banana-2.1/text-to-image"}),
                "prompt": ("STRING", {"multiline": True, "default": "A ceramic teapot on a linen cloth, soft window light, muted palette"}),
                "options": ("STRING", {"multiline": True, "default": "{\"aspect_ratio\":\"16:9\",\"resolution\":\"1k\",\"output_format\":\"png\"}"}),
            },
            "optional": {
                "image": ("IMAGE",),
                "image_field": ("STRING", {"default": "image_url"}),
            },
        }

    def run(self, model, prompt, options, image=None, image_field="image_url"):
        task_input = json.loads(options or "{}")
        if prompt.strip():
            task_input.setdefault("prompt", prompt)
        if image is not None:
            ref = _upload(image)
            task_input[image_field] = [ref] if image_field.endswith("s") else ref

        task_id = _call(
            "POST",
            "/jobs/createTask",
            headers={"Idempotency-Key": str(uuid.uuid4())},
            json={"model": model, "input": task_input},
        )["taskId"]

        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 TERMINAL and not copying:
                break
            delay = min(delay * 1.5, 15.0)
        if task["state"] != "succeeded" or not assets:
            raise RuntimeError(f'PopNow {task["state"]}: {task.get("errorMessage", "no detail")}')

        asset = assets[0]
        content = requests.get(asset["url"], timeout=300).content
        mime = asset.get("mime", "")
        if mime.startswith("image/"):
            pil = Image.open(io.BytesIO(content)).convert("RGB")
            tensor = torch.from_numpy(np.asarray(pil).astype(np.float32) / 255.0)[None,]
            return (tensor, asset["url"], "")

        ext = {"video/mp4": ".mp4", "video/webm": ".webm", "audio/mpeg": ".mp3", "audio/wav": ".wav"}.get(mime, ".bin")
        path = os.path.join(folder_paths.get_output_directory(), f"popnow_{task_id}{ext}")
        with open(path, "wb") as f:
            f.write(content)
        # Video and audio: a small blank IMAGE keeps the graph valid; use the path output.
        return (torch.zeros((1, 64, 64, 3)), asset["url"], path)


NODE_CLASS_MAPPINGS = {"PopNowGenerate": PopNowGenerate}
NODE_DISPLAY_NAME_MAPPINGS = {"PopNowGenerate": "PopNow Generate"}

Wejścia i wyjścia

model
Nazwa modelu, na przykład google/nano-banana-2.1/text-to-image. Pokazuje ją strona API każdego modelu.
prompt
Co chcesz stworzyć. W modelach bez promptu zostaw puste.
options
Pozostałe pola jako JSON, np. resolution albo duration_seconds. Strona API modelu wymienia je wszystkie.
image
Opcjonalne wejście IMAGE. Zostanie wysłane za ciebie i wstawione do pola wskazanego w image_field.
image_field
Które pole dostaje obraz: image_url w większości modeli wideo, image_urls przy edycji obrazów.
image · url · path
IMAGE dla modeli obrazów, a dla wideo i audio także URL pliku i ścieżka lokalna.

Dlaczego uruchamiać modele w chmurze z ComfyUI

  • Bez GPU

    Modele wideo, które potrzebują 24 GB i więcej, działają po naszej stronie. Twój laptop wysyła tylko prompt.

  • Łącz z lokalnymi węzłami

    Zrób klatkę startową lokalnie, ożyw ją w Kling lub Seedance, a potem znowu przeskaluj lokalnie.

  • Znana cena za generację

    Każdy model pokazuje kredyty z góry, a nieudane generacje są zwracane.

Dobre modele na start

Pytania o ComfyUI

Czy to oficjalny pakiet?

To pojedynczy plik, który publikujemy na tej stronie, a nie pakiet z rejestru. Skopiuj go, przeczytaj i aktualizuj stąd, gdy API się zmieni.

Czy działa w ComfyUI Desktop i Comfy Cloud?

Działa wszędzie, gdzie możesz dodać własny węzeł i ustawić zmienną środowiskową: w ComfyUI portable, Desktop i na własnych serwerach.

Jak odzyskać klatki wideo?

Węzeł zapisuje wideo w folderze output ComfyUI i zwraca ścieżkę. Wczytaj je węzłem do ładowania wideo, by dalej pracować na klatkach.

Ile kosztuje jedna generacja?

Te same kredyty co na stronie, podane na stronie API każdego modelu. Na czas eksperymentów ustaw kluczowi dzienny limit w konsoli.