Files
ChunkyPanda29-ComfyUI-Polli…/pollinations_nodes.py
T
Krissy baa66594e3 feat: Add official BYOP authentication module
Implements Pollinations BYOP specification:
- Device Code Flow for browser-free CLI authentication
- App Key attribution for traffic tracking
- User Info endpoint integration
- New PollinationsBYOPLogin node for ComfyUI
- Updated README with BYOP documentation

Reference: https://github.com/pollinations/pollinations/blob/main/BRING_YOUR_OWN_POLLEN.md
2026-03-26 13:26:42 +00:00

259 lines
10 KiB
Python

import urllib.parse
import requests
import torch
import numpy as np
from PIL import Image
import io
import json
import os
import logging
from server import PromptServer
from aiohttp import web
# SILENCE LOGGERS
logging.getLogger("requests").setLevel(logging.CRITICAL)
logging.getLogger("urllib3").setLevel(logging.CRITICAL)
# --- CONFIGURATION HANDLERS ---
def get_config_path():
return os.path.join(os.path.dirname(os.path.abspath(__file__)), "pollinations_config.json")
def get_models():
current_dir = os.path.dirname(os.path.abspath(__file__))
json_path = os.path.join(current_dir, "models.json")
try:
if os.path.exists(json_path):
with open(json_path, 'r', encoding='utf-8') as f:
return json.load(f)
except: pass
return {"image": ["flux"], "video": ["wan"], "text": ["openai"], "audio": ["elevenlabs"]}
def get_api_key(manual_key):
if manual_key and manual_key.strip() != "":
return manual_key.strip()
config_path = get_config_path()
if os.path.exists(config_path):
try:
with open(config_path, 'r') as f:
config = json.load(f)
key = config.get("api_key")
if key: return key.strip()
except: pass
return os.environ.get("POLLINATIONS_API_KEY", "")
# --- SERVER API ROUTE FOR SETTINGS ---
@PromptServer.instance.routes.post("/pollinations/save_key")
async def save_pollinations_key(request):
json_data = await request.json()
api_key = json_data.get("api_key")
config_path = get_config_path()
try:
with open(config_path, 'w') as f:
json.dump({"api_key": api_key}, f)
return web.json_response({"status": "success"})
except Exception as e:
return web.json_response({"status": "error", "message": str(e)}, status=500)
# --- IMAGE UPLOADER ---
def upload_to_pollinations(image_tensor):
try:
i = 255. * image_tensor.cpu().numpy().squeeze()
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
buffered = io.BytesIO()
img.save(buffered, format="PNG")
resp = requests.post("https://media.pollinations.ai/upload",
files={"file": ("image.png", buffered.getvalue(), "image/png")})
if resp.status_code == 200: return resp.json().get("url")
except: pass
return None
# --- NODE CLASSES ---
class PollinationsImageGen:
@classmethod
def INPUT_TYPES(s):
model_data = get_models()
return {
"required": {
"prompt": ("STRING", {"multiline": True, "default": "a cat in space"}),
"model": (model_data["image"], {"default": "flux"}),
"width": ("INT", {"default": 1024, "min": 256, "max": 4096, "step": 8}),
"height": ("INT", {"default": 1024, "min": 256, "max": 4096, "step": 8}),
"seed": ("INT", {"default": 42, "min": -1, "max": 2147483647}),
},
"optional": {
"api_key": ("STRING", {"default": "", "multiline": False, "placeholder": "Optional: Overrides Global Settings"}),
"image_input": ("IMAGE",),
"negative_prompt": ("STRING", {"multiline": True, "default": ""})
}
}
RETURN_TYPES = ("IMAGE", "STRING")
RETURN_NAMES = ("image", "url")
FUNCTION = "generate"
CATEGORY = "Pollinations/Image"
def generate(self, prompt, model, width, height, seed, api_key="", image_input=None, negative_prompt=""):
final_key = get_api_key(api_key)
model = model.replace("💎", "").strip()
encoded_prompt = urllib.parse.quote(prompt.replace("\n", " ").strip())
url = f"https://gen.pollinations.ai/image/{encoded_prompt}?model={model}&width={width}&height={height}&seed={seed}&nologo=true"
if image_input is not None:
img_url = upload_to_pollinations(image_input)
if img_url: url += f"&image={urllib.parse.quote(img_url)}"
if negative_prompt.strip(): url += f"&negative_prompt={urllib.parse.quote(negative_prompt.strip())}"
headers = {"User-Agent": "Mozilla/5.0"}
if final_key: headers["Authorization"] = f"Bearer {final_key}"
try:
r = requests.get(url, headers=headers, timeout=120)
if r.status_code == 200:
img = Image.open(io.BytesIO(r.content)).convert("RGB")
return (torch.from_numpy(np.array(img).astype(np.float32) / 255.0)[None,], url)
except: pass
return (torch.zeros((1, 512, 512, 3)), url)
class PollinationsAudioGen:
@classmethod
def INPUT_TYPES(s):
model_data = get_models()
return {
"required": {
"text": ("STRING", {"multiline": True, "default": "Hello world"}),
"model": (model_data["audio"], {"default": "elevenlabs"}),
"voice": (["alloy", "echo", "fable", "onyx", "nova", "shimmer", "sarah", "rachel", "charlie"], {"default": "sarah"}),
},
"optional": {"api_key": ("STRING", {"default": ""})}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("audio_url",)
FUNCTION = "generate"
CATEGORY = "Pollinations/Audio"
def generate(self, text, model, voice, api_key=""):
final_key = get_api_key(api_key)
model = model.replace("💎", "").strip()
url = f"https://gen.pollinations.ai/audio/{urllib.parse.quote(text.strip())}?model={model}&voice={voice}"
if final_key: url += f"&key={final_key}"
return (url,)
class PollinationsVideoGen:
@classmethod
def INPUT_TYPES(s):
model_data = get_models()
return {
"required": {
"prompt": ("STRING", {"multiline": True, "default": "cinematic sunset"}),
"model": (model_data["video"], {"default": "wan"}),
},
"optional": {
"api_key": ("STRING", {"default": ""}),
"image_input": ("IMAGE",),
"duration": ("INT", {"default": 5, "min": 2, "max": 15}),
"seed": ("INT", {"default": 42, "min": -1, "max": 2147483647}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("video_url",)
FUNCTION = "generate"
CATEGORY = "Pollinations/Video"
def generate(self, prompt, model, api_key="", image_input=None, duration=5, seed=42):
final_key = get_api_key(api_key)
model = model.replace("💎", "").strip()
url = f"https://gen.pollinations.ai/video/{urllib.parse.quote(prompt.strip())}?model={model}&duration={duration}&seed={seed}"
if image_input is not None:
img_url = upload_to_pollinations(image_input)
if img_url: url += f"&image={urllib.parse.quote(img_url)}"
if final_key: url += f"&key={final_key}"
return (url,)
class PollinationsTextGen:
@classmethod
def INPUT_TYPES(s):
model_data = get_models()
return {
"required": {
"prompt": ("STRING", {"multiline": True, "default": "Hi!"}),
"model": (model_data["text"], {"default": "openai"}),
"system_instruction": ("STRING", {"multiline": True, "default": "You are a helpful assistant."}),
},
"optional": {"api_key": ("STRING", {"default": ""})}
}
RETURN_TYPES = ("STRING",)
FUNCTION = "generate"
CATEGORY = "Pollinations/Text"
def generate(self, prompt, model, system_instruction, api_key=""):
final_key = get_api_key(api_key)
model = model.replace("💎", "").strip()
url = "https://gen.pollinations.ai/v1/chat/completions"
headers = {"Content-Type": "application/json"}
if final_key: headers["Authorization"] = f"Bearer {final_key}"
payload = {"model": model, "messages": [{"role": "system", "content": system_instruction}, {"role": "user", "content": prompt}]}
try:
r = requests.post(url, json=payload, headers=headers)
return (r.json()["choices"][0]["message"]["content"],)
except: return ("Error connecting to Text API",)
class PollinationsBYOPLogin:
"""
BYOP (Bring Your Own Pollen) Login Node
Implements device code flow for headless/CLI authentication
Reference: https://github.com/pollinations/pollinations/blob/main/BRING_YOUR_OWN_POLLEN.md
"""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"login_trigger": ("BOOLEAN", {"default": False, "label_on": "Click to Login", "label_off": "Idle"}),
},
"optional": {
"app_key": ("STRING", {"default": "", "multiline": False, "placeholder": "Optional: Your pk_... app key for attribution"}),
}
}
RETURN_TYPES = ("STRING", "STRING", "STRING", "INT")
RETURN_NAMES = ("api_key", "username", "email", "balance")
FUNCTION = "login"
CATEGORY = "Pollinations/BYOP"
OUTPUT_NODE = True
def login(self, login_trigger, app_key=""):
# If not triggered, return empty values
if not login_trigger:
return ("", "", "", 0)
# Import here to avoid startup overhead
from .byop_auth import BYOPAuth
auth = BYOPAuth(app_key if app_key.strip() else None)
api_key = auth.authenticate_interactive()
if api_key:
# Get user info
user_info = auth.get_user_info(api_key)
if user_info:
username = user_info.get('name', user_info.get('preferred_username', 'Unknown'))
email = user_info.get('email', '')
balance = user_info.get('balance', 0)
# Save to config for other nodes
config_path = get_config_path()
try:
config = {}
if os.path.exists(config_path):
with open(config_path, 'r') as f:
config = json.load(f)
config["api_key"] = api_key
config["byop_username"] = username
with open(config_path, 'w') as f:
json.dump(config, f)
except Exception as e:
print(f"[BYOP] Failed to save config: {e}")
return (api_key, username, email, balance)
return ("", "", "", 0)