ComfyUI × PopNow

Modelos de IA en la nube dentro de ComfyUI

Un pequeño nodo personalizado lleva todos los modelos de imagen, vídeo y audio de PopNow a tu grafo de ComfyUI. Sin GPU y sin descargar modelos: envía la tarea, espera y pasa el resultado al siguiente nodo.

Instálalo en cuatro pasos

  1. Guarda el código de abajo como custom_nodes/popnow_api.py dentro de tu carpeta de ComfyUI.
  2. Define POPNOW_API_KEY en el entorno desde el que inicias ComfyUI y reinícialo.
  3. Haz doble clic en el lienzo, busca PopNow y añade el nodo PopNow Generate.
  4. Escribe un nombre de modelo y un prompt, y ponlo en cola. Las imágenes salen como IMAGE; los vídeos y el audio se guardan en la carpeta output.

El nodo

Python sencillo que solo usa requests, Pillow, NumPy y PyTorch, que ya vienen con ComfyUI. Léelo antes de ejecutarlo: es corto a propósito.

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

Entradas y salidas

model
Nombre del modelo, por ejemplo google/nano-banana-2.1/text-to-image. Cada página de API de modelo lo muestra.
prompt
Qué quieres crear. Déjalo vacío en los modelos que no usan prompt.
options
Otros campos en JSON, como resolution o duration_seconds. La página de API del modelo los enumera todos.
image
Entrada IMAGE opcional. Se sube por ti y se coloca en el campo que indique image_field.
image_field
Qué campo recibe la imagen: image_url en la mayoría de modelos de vídeo, image_urls en la edición de imágenes.
image · url · path
IMAGE en los modelos de imagen, además de la URL del archivo y la ruta local en vídeo y audio.

Por qué usar modelos en la nube desde ComfyUI

  • Sin GPU

    Los modelos de vídeo que necesitan 24 GB o más se ejecutan en nuestros servidores. Tu portátil solo envía el prompt.

  • Combínalos con tus nodos locales

    Crea un fotograma inicial en local, anímalo con Kling o Seedance y vuelve a escalarlo en local.

  • Precio conocido por generación

    Cada modelo muestra sus créditos por adelantado y las generaciones fallidas se reembolsan.

Buenos modelos para empezar

Preguntas sobre ComfyUI

¿Es un paquete oficial?

Es un único archivo que publicamos en esta página, no un paquete de un registro. Cópialo, léelo y actualízalo desde aquí cuando cambie la API.

¿Funciona con ComfyUI Desktop y Comfy Cloud?

Funciona en cualquier sitio donde puedas añadir un nodo personalizado y definir una variable de entorno: ComfyUI portable, Desktop y servidores autoalojados.

¿Cómo recupero los fotogramas de un vídeo?

El nodo guarda el vídeo en la carpeta output de ComfyUI y devuelve su ruta. Cárgalo con un nodo de carga de vídeo para seguir trabajando con los fotogramas.

¿Cuánto cuesta una generación?

Los mismos créditos que en la web, indicados en la página de API de cada modelo. Pon un límite diario a la clave en la consola mientras experimentas.