ComfyUI × PopNow
Облачные модели ИИ прямо в ComfyUI
Одна небольшая кастомная нода добавляет в ваш граф ComfyUI все модели изображений, видео и аудио PopNow. Без GPU и скачивания моделей: нода отправляет задачу, ждёт и передаёт результат следующей ноде.
Установка в четыре шага
- Сохраните код ниже как custom_nodes/popnow_api.py в папке ComfyUI.
- Задайте POPNOW_API_KEY в окружении, из которого запускаете ComfyUI, и перезапустите его.
- Дважды щёлкните по холсту, найдите PopNow и добавьте ноду PopNow Generate.
- Введите имя модели и промпт и поставьте в очередь. Изображения выходят как IMAGE, видео и аудио сохраняются в папку output.
Нода
Простой Python, которому нужны только requests, Pillow, NumPy и PyTorch — всё это уже есть в ComfyUI. Прочитайте код перед запуском: он намеренно короткий.
"""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"}Входы и выходы
model- Имя модели, например google/nano-banana-2.1/text-to-image. Оно указано на API-странице каждой модели.
prompt- Что нужно создать. Для моделей без промпта оставьте пустым.
options- Остальные поля в JSON, например resolution или duration_seconds. Все они перечислены на API-странице модели.
image- Необязательный вход IMAGE. Он загружается автоматически и попадает в поле, указанное в image_field.
image_field- Какое поле получает изображение: image_url у большинства видеомоделей, image_urls при редактировании изображений.
image · url · path- IMAGE для моделей изображений, а для видео и аудио — ещё URL файла и локальный путь.
Зачем запускать облачные модели из ComfyUI
- GPU не нужен
Видеомодели, которым нужно 24 ГБ и больше, работают на нашей стороне. Ваш ноутбук только отправляет промпт.
- Сочетайте с локальными нодами
Сделайте стартовый кадр локально, оживите его в Kling или Seedance, а затем снова увеличьте локально.
- Понятная цена за запуск
Каждая модель заранее показывает стоимость в кредитах, а за неудачные запуски кредиты возвращаются.
Хорошие модели для начала
Вопросы о ComfyUI
Это официальный пакет?
Это один файл, который мы публикуем на этой странице, а не пакет из реестра. Скопируйте его, прочитайте и обновляйте отсюда, когда API меняется.
Работает ли это в ComfyUI Desktop и Comfy Cloud?
Работает везде, где можно добавить кастомную ноду и задать переменную окружения: в портативной ComfyUI, Desktop и на собственных серверах.
Как получить кадры видео?
Нода сохраняет видео в папку output ComfyUI и возвращает путь. Загрузите его нодой загрузки видео, чтобы продолжить работу с кадрами.
Сколько стоит один запуск?
Те же кредиты, что и на сайте; цена указана на API-странице каждой модели. Пока экспериментируете, задайте ключу дневной лимит в консоли.

