ComfyUI × PopNow
Cloud-KI-Modelle direkt in ComfyUI
Ein kleiner Custom Node holt jedes Bild-, Video- und Audiomodell von PopNow in deinen ComfyUI-Graphen. Keine GPU, keine Modell-Downloads: Er schickt den Job ab, wartet und gibt das Ergebnis an den nächsten Node weiter.
In vier Schritten installiert
- Speicher den Code unten als custom_nodes/popnow_api.py in deinem ComfyUI-Ordner.
- Setz POPNOW_API_KEY in der Umgebung, aus der du ComfyUI startest, und starte ComfyUI neu.
- Doppelklick auf die Arbeitsfläche, nach PopNow suchen und den Node PopNow Generate hinzufügen.
- Modellnamen und Prompt eintragen, dann in die Warteschlange. Bilder kommen als IMAGE heraus, Videos und Audio landen im output-Ordner.
Der Node
Schlichtes Python, das nur requests, Pillow, NumPy und PyTorch nutzt, die ComfyUI ohnehin mitbringt. Lies ihn, bevor du ihn ausführst: Er ist mit Absicht kurz.
"""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"}Ein- und Ausgaben
model- Modellname, zum Beispiel google/nano-banana-2.1/text-to-image. Jede API-Seite eines Modells zeigt ihn.
prompt- Was entstehen soll. Bei Modellen ohne Prompt einfach leer lassen.
options- Weitere Felder als JSON, etwa resolution oder duration_seconds. Die API-Seite des Modells listet alle auf.
image- Optionaler IMAGE-Eingang. Er wird für dich hochgeladen und in das Feld gesetzt, das image_field nennt.
image_field- Welches Eingabefeld das Bild bekommt: image_url bei den meisten Videomodellen, image_urls bei der Bildbearbeitung.
image · url · path- IMAGE bei Bildmodellen, dazu die Datei-URL und der lokale Pfad bei Video und Audio.
Warum Cloud-Modelle aus ComfyUI
- Keine GPU nötig
Videomodelle, die 24 GB und mehr brauchen, laufen bei uns. Dein Laptop schickt nur den Prompt.
- Mit lokalen Nodes kombinieren
Startbild lokal erzeugen, mit Kling oder Seedance animieren und danach wieder lokal hochskalieren.
- Fester Preis pro Lauf
Jedes Modell nennt seine Credits vorab, und fehlgeschlagene Läufe werden erstattet.
Gute Modelle für den Anfang
Fragen zu ComfyUI
Ist das ein offizielles Paket?
Es ist eine einzelne Datei, die wir auf dieser Seite veröffentlichen, kein Registry-Paket. Kopier sie, lies sie und aktualisier sie von hier, wenn sich die API ändert.
Funktioniert das mit ComfyUI Desktop und Comfy Cloud?
Es funktioniert überall, wo du einen Custom Node hinzufügen und eine Umgebungsvariable setzen kannst: ComfyUI Portable, Desktop und selbst gehostete Server.
Wie bekomme ich Videoframes zurück?
Der Node speichert das Video im output-Ordner von ComfyUI und gibt den Pfad zurück. Lad es mit einem Video-Loader-Node, um an den Frames weiterzuarbeiten.
Was kostet ein Lauf?
Dieselben Credits wie auf der Website, angegeben auf der API-Seite jedes Modells. Setz dem Schlüssel in der Konsole ein Tageslimit, solange du experimentierst.

