From 75258b6024f62d4720e66533d00cd65da7d705db Mon Sep 17 00:00:00 2001 From: AI Lab <129358391+1038lab@users.noreply.github.com> Date: Sun, 6 Jul 2025 22:36:53 -0700 Subject: [PATCH] Add files via upload --- PREMIUM_MODELS.md | 86 +++++++++ pollinations.py | 233 ++++++++++++------------ pollinations_premium.py | 392 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 591 insertions(+), 120 deletions(-) create mode 100644 PREMIUM_MODELS.md create mode 100644 pollinations_premium.py diff --git a/PREMIUM_MODELS.md b/PREMIUM_MODELS.md new file mode 100644 index 0000000..592a11c --- /dev/null +++ b/PREMIUM_MODELS.md @@ -0,0 +1,86 @@ +# Pollinations Premium Models + +## Overview +Premium models require authentication and tier-based access from Pollinations.ai. These models offer enhanced capabilities but need API tokens. + +## Available Premium Models + +### 🔑 Premium Image Generation +- **gptimage**: Advanced image generation (requires "flower tier") +- **kontext**: Image-to-image editing and enhancement (requires "seed tier") + +### 🔑 Premium Text-to-Speech (Private Mode Only) +- **openai-audio**: OpenAI GPT-4o Mini Audio Preview (works anonymously, private mode requires "seed tier") +- **hypnosis-tracy**: Hypnosis Tracy voice model (works anonymously, private mode requires "seed tier") + +## How to Get Access + +1. **Visit Authentication Page**: https://auth.pollinations.ai +2. **Create Account**: Sign up or log in +3. **Request Tier Access**: + - For gptimage: Request "flower tier" access + - For kontext: Request "seed tier" access + - For text-to-speech private mode: Request "seed tier" access +4. **Get API Token**: Copy your API token from the dashboard + +## Using Premium Nodes + +### Premium Image Generator 🔑 +- **Node**: `PollinationsPremiumImageGen` +- **Required**: API token, prompt, model selection +- **Models**: gptimage, kontext +- **Features**: Enhanced prompting, higher quality output + +### Premium Image Editor 🔑 +- **Node**: `PollinationsPremiumImageEdit` +- **Required**: Input image, API token, prompt, model selection +- **Models**: gptimage, kontext +- **Features**: Advanced image-to-image editing + +### Text-to-Speech (Private Mode) 🔑 +- **Node**: `PollinationsTextToSpeech` +- **Required**: Text input, authentication only for private mode +- **Models**: openai-audio, hypnosis-tracy +- **Features**: High-quality voice synthesis with multiple voice options +- **Note**: Works without authentication by default, private mode requires authentication + +## Setup Instructions + +1. **Install Nodes**: The premium nodes are automatically loaded with the main plugin +2. **Get API Token**: Follow the authentication process above +3. **Add Token**: Enter your API token in the node's "api_token" field +4. **Select Model**: Choose the appropriate premium model +5. **Generate**: Create your content with enhanced capabilities + +## Error Messages + +- **"API token required"**: You need to enter a valid API token +- **"Authentication failed"**: Check your API token is correct +- **"Requires higher tier access"**: Your account needs tier upgrade +- **"tier access"**: Model requires specific tier level + +## Free vs Premium + +### Free Models (No Token Required) +- **flux**: Fast, good quality image generation +- **turbo**: Ultra-fast image generation + +### Premium Models (Authentication Required) +- **gptimage**: Enhanced image generation with better prompt understanding +- **kontext**: Advanced image editing and enhancement capabilities +- **openai-audio (private mode)**: High-quality text-to-speech with multiple voices +- **hypnosis-tracy (private mode)**: Alternative text-to-speech model + +## Support + +For authentication issues or tier access requests: +- Visit: https://auth.pollinations.ai +- Contact Pollinations support for tier upgrades +- Check your account dashboard for token management + +## Notes + +- Premium models have higher quality but require authentication +- Free models work without any setup +- API tokens are account-specific and should be kept secure +- Different models require different tier levels diff --git a/pollinations.py b/pollinations.py index 95a5f81..fbdecd1 100644 --- a/pollinations.py +++ b/pollinations.py @@ -13,92 +13,47 @@ from pydub import AudioSegment import io import folder_paths -# Updated based on https://image.pollinations.ai/models DEFAULT_IMAGE_MODELS = ["flux", "turbo"] -# Top models from https://text.pollinations.ai/models (first few models) -DEFAULT_TEXT_MODELS = ["openai", "openai-fast", "openai-large", "qwen-coder", "llama", "mistral"] -MODELS_CACHE = {"models": [], "last_update": 0} -TEXT_MODELS_CACHE = {"model_info": [], "last_update": 0} +TEXT_GENERATION_MODELS = [ + "openai", # OpenAI GPT-4o Mini + "mistral", # Mistral Small 3.1 24B + "llamascout", # Llama 4 Scout 17B + "openai-fast", # OpenAI GPT-4.1 Nano + "openai-reasoning", # OpenAI O3 + "phi", # Phi-4 Mini Instruct + "qwen-coder", # Qwen 2.5 Coder 32B + "bidara", # NASA BIDARA + "midijourney" # MIDIjourney +] -def get_available_models(): - """Get available image models from API with caching""" - current_time = time.time() - - if current_time - MODELS_CACHE["last_update"] > 3600 or not MODELS_CACHE["models"]: - try: - response = requests.get("https://image.pollinations.ai/models", timeout=10) - if response.status_code == 200: - models_data = response.json() - if models_data and len(models_data) > 0: - MODELS_CACHE["models"] = models_data - else: - MODELS_CACHE["models"] = DEFAULT_IMAGE_MODELS - MODELS_CACHE["last_update"] = current_time - else: - MODELS_CACHE["models"] = DEFAULT_IMAGE_MODELS - except Exception as e: - print(f"Error fetching image models: {e}") - MODELS_CACHE["models"] = DEFAULT_IMAGE_MODELS - - return MODELS_CACHE["models"] +SEARCH_MODELS = [ + "searchgpt", # OpenAI GPT-4o Mini Search Preview + "elixposearch" # Elixpo Search +] -def get_text_models(): - """Get available text models from API with caching""" - current_time = time.time() - - if current_time - TEXT_MODELS_CACHE["last_update"] > 3600 or not TEXT_MODELS_CACHE["model_info"]: - try: - response = requests.get("https://text.pollinations.ai/models", timeout=10) - if response.status_code == 200: - models_data = response.json() - if models_data and len(models_data) > 0: - # Store model info as {name, description} pairs - model_info = [] - for model in models_data: - model_info.append({ - 'name': model['name'], - 'description': model.get('description', f"Default {model['name']} model") - }) - TEXT_MODELS_CACHE["model_info"] = model_info - else: - TEXT_MODELS_CACHE["model_info"] = [ - {'name': name, 'description': f"Default {name} model"} - for name in DEFAULT_TEXT_MODELS - ] - TEXT_MODELS_CACHE["last_update"] = current_time - else: - TEXT_MODELS_CACHE["model_info"] = [ - {'name': name, 'description': f"Default {name} model"} - for name in DEFAULT_TEXT_MODELS - ] - except Exception as e: - print(f"Error fetching text models: {e}") - TEXT_MODELS_CACHE["model_info"] = [ - {'name': name, 'description': f"Default {name} model"} - for name in DEFAULT_TEXT_MODELS - ] - - # Return only descriptions for display - return [model['description'] for model in TEXT_MODELS_CACHE["model_info"]] +TEXT_TO_SPEECH_MODELS = [ + "openai-audio", # OpenAI GPT-4o Mini Audio Preview + "hypnosis-tracy" # Hypnosis Tracy +] +# Updated voice list from API +AVAILABLE_VOICES = ["alloy", "echo", "fable", "onyx", "nova", "shimmer", "coral", "verse", "ballad", "ash", "sage", "amuch", "dan"] class PollinationsImageGen: - + @classmethod def INPUT_TYPES(cls): - models = get_available_models() - default_model = "flux" if "flux" in models else models[0] if models else "flux" - + # Use fixed model list + return { "required": { "prompt": ("STRING", {"multiline": True, "placeholder": "Enter a description of the image you want..."}), - "model": (models, {"default": default_model}), + "model": (DEFAULT_IMAGE_MODELS, {"default": "flux"}), "width": ("INT", {"default": 1024, "min": 512, "max": 4096, "step": 8}), "height": ("INT", {"default": 1024, "min": 512, "max": 4096, "step": 8}), "batch_size": ("INT", {"default": 1, "min": 1, "max": 4, "step": 1}), }, "optional": { - "negative_prompt": ("STRING", {"multiline": True, "default": ""}), "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), "enhance": ("BOOLEAN", {"default": True}), "nologo": ("BOOLEAN", {"default": True}), @@ -113,41 +68,38 @@ class PollinationsImageGen: FUNCTION = "generate" CATEGORY = "🧪AILab/🌸Pollinations" - def generate(self, prompt, model, width, height, batch_size=1, negative_prompt="", seed=0, + def generate(self, prompt, model, width, height, batch_size=1, seed=0, enhance=True, nologo=True, private=True, safe=False): """Generate multiple images""" + # Use fixed model list + images = [] urls = [] prompts = [] - + for i in range(batch_size): current_seed = seed + i if seed != 0 else 0 try: image, url, final_prompt = self._generate_single( - prompt, model, width, height, negative_prompt, + prompt, model, width, height, current_seed, enhance, nologo, private, safe ) images.append(image) urls.append(url) prompts.append(final_prompt) except Exception as e: - print(f"Error generating image {i+1}: {e}") images.append(torch.zeros(1, 512, 512, 3)) urls.append(f"Error: {str(e)}") prompts.append(prompt) - + return (images, urls, prompts) - def _generate_single(self, prompt, model, width, height, negative_prompt="", seed=0, + def _generate_single(self, prompt, model, width, height, seed=0, enhance=True, nologo=True, private=True, safe=False): """Generate a single image""" try: base_url = "https://image.pollinations.ai/prompt/" - full_prompt = prompt - if negative_prompt: - full_prompt = f"{prompt} ### {negative_prompt}" - - encoded_prompt = quote(full_prompt) + encoded_prompt = quote(prompt) params = { "model": model, "width": width, @@ -167,23 +119,21 @@ class PollinationsImageGen: param_str = "&".join([f"{k}={v}" for k, v in params.items()]) url = f"{base_url}{encoded_prompt}?{param_str}" - - print(f"Generating image, URL: {url}") + response = requests.get(url, stream=True) response.raise_for_status() - final_prompt = full_prompt - + final_prompt = prompt + try: image_url = response.url if "/prompt/" in image_url: encoded_part = image_url.split("/prompt/")[1].split("?")[0] extracted_prompt = unquote(encoded_part) - if extracted_prompt != full_prompt and enhance: + if extracted_prompt != prompt and enhance: final_prompt = extracted_prompt - print(f"Enhanced prompt: {final_prompt}") - except Exception as ee: - print(f"Error extracting enhanced prompt: {ee}") + except Exception: + pass temp_dir = tempfile.gettempdir() filename = f"pollinations_{int(time.time())}.png" @@ -200,7 +150,6 @@ class PollinationsImageGen: except Exception as e: error_msg = f"Pollinations API error: {str(e)}" - print(error_msg) empty_image = torch.zeros(1, 512, 512, 3) return (empty_image, error_msg, prompt) @@ -208,16 +157,15 @@ class PollinationsImageGen: def IS_CHANGED(cls, **kwargs): return time.time() + class PollinationsTextGen: @classmethod def INPUT_TYPES(cls): - text_models = get_text_models() - default_description = next((model['description'] for model in TEXT_MODELS_CACHE.get("model_info", []) - if model['name'] == "openai"), text_models[0] if text_models else "Default openai model") + # Use fixed model list return { "required": { "prompt": ("STRING", {"multiline": True, "placeholder": "Enter your text prompt..."}), - "model": (text_models, {"default": default_description}), + "model": (TEXT_GENERATION_MODELS, {"default": "openai"}), "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), }, "optional": { @@ -232,15 +180,8 @@ class PollinationsTextGen: def generate_text(self, prompt, model, seed, private=True): try: - # Find the model name that matches this description - model_name = None - for model_info in TEXT_MODELS_CACHE["model_info"]: - if model_info['description'] == model: - model_name = model_info['name'] - break - - if not model_name: - model_name = "openai" # fallback + # Use model directly from fixed list + model_name = model if model in TEXT_GENERATION_MODELS else "openai" params = { "model": model_name, @@ -249,8 +190,7 @@ class PollinationsTextGen: } param_str = "&".join([f"{k}={v}" for k, v in params.items()]) url = f"https://text.pollinations.ai/{quote(prompt)}?{param_str}" - - print(f"Generating Text, URL: {url}") + response = requests.get(url) if response.status_code == 200: return (response.text,) @@ -259,6 +199,48 @@ class PollinationsTextGen: except Exception as e: return (f"Text generation failed: {str(e)}",) +# Adding Search node +class PollinationsSearch: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "query": ("STRING", {"multiline": True, "placeholder": "Enter your search query..."}), + "model": (SEARCH_MODELS, {"default": "searchgpt"}), + }, + "optional": { + "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), + "private": ("BOOLEAN", {"default": True, "tooltip": "Keep the search private"}) + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("search_results",) + FUNCTION = "search" + CATEGORY = "🧪AILab/🌸Pollinations" + + def search(self, query, model, seed=0, private=True): + try: + params = { + "model": model, + "seed": seed, + "private": str(private).lower() + } + param_str = "&".join([f"{k}={v}" for k, v in params.items()]) + url = f"https://text.pollinations.ai/{quote(query)}?{param_str}" + + response = requests.get(url, timeout=30) + if response.status_code == 200: + return (response.text,) + else: + return (f"Search error: {response.status_code}",) + except Exception as e: + return (f"Search failed: {str(e)}",) + + @classmethod + def IS_CHANGED(cls, **kwargs): + return float("NaN") + # Adding Text-to-Speech node class PollinationsTextToSpeech: @classmethod @@ -266,11 +248,11 @@ class PollinationsTextToSpeech: return { "required": { "text": ("STRING", {"multiline": True, "placeholder": "Enter text to convert to speech..."}), - "voice": (["nova", "alloy", "echo", "fable", "onyx", "shimmer"], {"default": "nova"}), - "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), + "model": (TEXT_TO_SPEECH_MODELS, {"default": "openai-audio"}), + "voice": (AVAILABLE_VOICES, {"default": "nova"}), }, "optional": { - "private": ("BOOLEAN", {"default": True, "tooltip": "Keep the generation private"}) + "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "tooltip": "Random seed (requires authentication)"}) } } @@ -279,20 +261,25 @@ class PollinationsTextToSpeech: FUNCTION = "generate_speech" CATEGORY = "🧪AILab/🌸Pollinations" - def generate_speech(self, text, voice, seed, private=True): + def generate_speech(self, text, model, voice, seed=None): try: + # Use selected model, fallback to openai-audio if not in list + model_name = model if model in TEXT_TO_SPEECH_MODELS else "openai-audio" + params = { - "model": "openai-audio", - "voice": voice, - "seed": seed, - "private": str(private).lower() + "model": model_name, + "voice": voice } + + # Only add seed if provided and not 0 (requires authentication) + if seed is not None and seed != 0: + params["seed"] = seed + param_str = "&".join([f"{k}={v}" for k, v in params.items()]) url = f"https://text.pollinations.ai/{quote(text)}?{param_str}" - - print(f"Generating Speech, URL: {url}") + response = requests.get(url, stream=True) - + if response.status_code == 200: # Get ComfyUI's temp directory temp_dir = os.path.join(folder_paths.get_output_directory(), "pollinations_temp") @@ -332,12 +319,16 @@ class PollinationsTextToSpeech: } return (audio_dict, mp3_path) + elif response.status_code == 402: + # Payment required - authentication needed (usually when using seed) + error_msg = "Text-to-Speech with seed parameter requires authentication. Remove seed or visit https://auth.pollinations.ai to get authentication." + print(f"[PollinationsTextToSpeech] {error_msg}") + return ({"waveform": torch.zeros(1, 1, 16000), "sample_rate": 16000}, "") else: - print(f"Error generating speech: {response.status_code}") + print(f"[PollinationsTextToSpeech] Error: HTTP {response.status_code} - {response.text[:200]}") return ({"waveform": torch.zeros(1, 1, 16000), "sample_rate": 16000}, "") except Exception as e: - error_msg = f"Speech generation failed: {str(e)}" - print(error_msg) + print(f"[PollinationsTextToSpeech] Exception: {str(e)}") return ({"waveform": torch.zeros(1, 1, 16000), "sample_rate": 16000}, "") @classmethod @@ -347,11 +338,13 @@ class PollinationsTextToSpeech: NODE_CLASS_MAPPINGS = { "PollinationsImageGen": PollinationsImageGen, "PollinationsTextGen": PollinationsTextGen, + "PollinationsSearch": PollinationsSearch, "PollinationsTextToSpeech": PollinationsTextToSpeech, } NODE_DISPLAY_NAME_MAPPINGS = { "PollinationsImageGen": "Image Gen 🖼️ (Pollinations)", "PollinationsTextGen": "Text Gen 📝 (Pollinations)", + "PollinationsSearch": "Search 🔍 (Pollinations)", "PollinationsTextToSpeech": "Text To Speech Chat 🔊 (Pollinations)", -} \ No newline at end of file +} \ No newline at end of file diff --git a/pollinations_premium.py b/pollinations_premium.py new file mode 100644 index 0000000..ebd3e12 --- /dev/null +++ b/pollinations_premium.py @@ -0,0 +1,392 @@ +import torch +import requests +import tempfile +import os +import time +from PIL import Image +import numpy as np +from urllib.parse import quote, unquote +import base64 +import torchaudio +import folder_paths + +# Premium models that require authentication +PREMIUM_IMAGE_MODELS = ["gptimage", "kontext"] +TEXT_TO_SPEECH_MODELS = ["openai-audio", "hypnosis-tracy"] +AVAILABLE_VOICES = ["alloy", "echo", "fable", "onyx", "nova", "shimmer", "coral", "verse", "ballad", "ash", "sage", "amuch", "dan"] + +class PollinationsPremiumImageGen: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "api_token": ("STRING", {"default": ""}), + "prompt": ("STRING", {"multiline": True, "default": "a beautiful landscape"}), + "model": (PREMIUM_IMAGE_MODELS, {"default": "gptimage"}), + "width": ("INT", {"default": 512, "min": 64, "max": 2048, "step": 64}), + "height": ("INT", {"default": 512, "min": 64, "max": 2048, "step": 64}), + "seed": ("INT", {"default": 0, "min": 0, "max": 2147483647}), + "count": ("INT", {"default": 1, "min": 1, "max": 4}), + }, + "optional": { + "enhance": ("BOOLEAN", {"default": True}), + "nologo": ("BOOLEAN", {"default": True}), + "private": ("BOOLEAN", {"default": True}), + "safe": ("BOOLEAN", {"default": False}), + "transparent": ("BOOLEAN", {"default": False}), + } + } + + RETURN_TYPES = ("IMAGE", "STRING", "STRING") + RETURN_NAMES = ("images", "urls", "prompts") + FUNCTION = "generate" + CATEGORY = "🧪AILab/🌸Pollinations/💎Premium" + + def generate(self, prompt, model, api_token, width=512, height=512, seed=0, count=1, + enhance=True, nologo=True, private=True, safe=False, transparent=False): + + if not api_token.strip(): + error_msg = f"API token required for {model} model. Get your token at: https://auth.pollinations.ai" + empty_image = torch.zeros(1, 512, 512, 3) + return ([empty_image], [error_msg], [prompt]) + + images = [] + urls = [] + prompts = [] + + for i in range(count): + try: + image, url, final_prompt = self._generate_single( + prompt, model, api_token, width, height, seed + i, + enhance, nologo, private, safe, transparent + ) + images.append(image) + urls.append(url) + prompts.append(final_prompt) + except Exception as e: + images.append(torch.zeros(1, 512, 512, 3)) + urls.append(f"Error: {str(e)}") + prompts.append(prompt) + + return (images, urls, prompts) + + def _generate_single(self, prompt, model, api_token, width, height, seed, + enhance=True, nologo=True, private=True, safe=False, transparent=False): + try: + base_url = "https://image.pollinations.ai/prompt/" + + encoded_prompt = quote(prompt) + + params = { + "model": model, + "width": str(width), + "height": str(height), + "seed": str(seed), + "nologo": str(nologo).lower(), + "private": str(private).lower() + } + + if enhance and model in ["gptimage"]: + params["enhance"] = "true" + + if safe: + params["safe"] = "true" + + if transparent and model == "gptimage": + params["transparent"] = "true" + + param_str = "&".join([f"{k}={v}" for k, v in params.items()]) + url = f"{base_url}{encoded_prompt}?{param_str}" + + headers = {"Authorization": f"Bearer {api_token}"} + response = requests.get(url, stream=True, headers=headers, timeout=60) + response.raise_for_status() + + final_prompt = prompt + + try: + image_url = response.url + if "/prompt/" in image_url: + encoded_part = image_url.split("/prompt/")[1].split("?")[0] + extracted_prompt = unquote(encoded_part) + if extracted_prompt != prompt and enhance: + final_prompt = extracted_prompt + except Exception: + pass + + temp_dir = tempfile.gettempdir() + filename = f"pollinations_premium_{int(time.time())}.png" + image_path = os.path.join(temp_dir, filename) + + with open(image_path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + + image = Image.open(image_path) + image_array = np.array(image).astype(np.float32) / 255.0 + image_tensor = torch.from_numpy(image_array).unsqueeze(0) + + os.unlink(image_path) + + return (image_tensor, url, final_prompt) + + except Exception as e: + if "401" in str(e) or "403" in str(e): + error_msg = f"Authentication failed. Check your API token for {model} model." + elif "tier" in str(e).lower(): + error_msg = f"Model {model} requires higher tier access. Upgrade at: https://auth.pollinations.ai" + else: + error_msg = f"Premium API error: {str(e)}" + + empty_image = torch.zeros(1, 512, 512, 3) + return (empty_image, error_msg, prompt) + + @classmethod + def IS_CHANGED(cls, **kwargs): + return float("NaN") + +class PollinationsPremiumImageEdit: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "image": ("IMAGE",), + "api_token": ("STRING", {"default": ""}), + "prompt": ("STRING", {"multiline": True, "default": "enhance this image"}), + "model": (PREMIUM_IMAGE_MODELS, {"default": "kontext"}), + "width": ("INT", {"default": 512, "min": 64, "max": 2048, "step": 64}), + "height": ("INT", {"default": 512, "min": 64, "max": 2048, "step": 64}), + "seed": ("INT", {"default": 0, "min": 0, "max": 2147483647}), + }, + "optional": { + "enhance": ("BOOLEAN", {"default": True}), + "nologo": ("BOOLEAN", {"default": True}), + "private": ("BOOLEAN", {"default": True}), + "safe": ("BOOLEAN", {"default": False}), + } + } + + RETURN_TYPES = ("IMAGE", "STRING", "STRING") + RETURN_NAMES = ("image", "url", "prompt") + FUNCTION = "edit_image" + CATEGORY = "🧪AILab/🌸Pollinations/💎Premium" + + def edit_image(self, image, prompt, model, api_token, width=512, height=512, seed=0, + enhance=True, nologo=True, private=True, safe=False): + + if not api_token.strip(): + error_msg = f"API token required for {model} model. Get your token at: https://auth.pollinations.ai" + empty_image = torch.zeros(1, 512, 512, 3) + return (empty_image, error_msg, prompt) + + try: + image_np = (image.squeeze().cpu().numpy() * 255).astype(np.uint8) + pil_image = Image.fromarray(image_np) + + max_size = 512 + if pil_image.width > max_size or pil_image.height > max_size: + pil_image.thumbnail((max_size, max_size), Image.Resampling.LANCZOS) + + temp_dir = tempfile.gettempdir() + input_filename = f"pollinations_premium_input_{int(time.time())}.jpg" + input_path = os.path.join(temp_dir, input_filename) + pil_image.save(input_path, "JPEG", quality=85) + + with open(input_path, "rb") as img_file: + img_base64 = base64.b64encode(img_file.read()).decode('utf-8') + + os.unlink(input_path) + + image_url = f"data:image/jpeg;base64,{img_base64}" + + if len(image_url) > 8000: + pass + + edited_image, result_url, final_prompt = self._edit_single( + prompt, model, api_token, width, height, image_url, + seed, enhance, nologo, private, safe + ) + + return (edited_image, result_url, final_prompt) + + except Exception as e: + error_msg = f"Premium image editing error: {str(e)}" + empty_image = torch.zeros(1, 512, 512, 3) + return (empty_image, error_msg, prompt) + + def _edit_single(self, prompt, model, api_token, width, height, image_url, seed=0, + enhance=True, nologo=True, private=True, safe=False): + try: + base_url = "https://image.pollinations.ai/prompt/" + + encoded_prompt = quote(prompt) + + params = { + "model": model, + "width": str(width), + "height": str(height), + "seed": str(seed), + "image": image_url, + "nologo": str(nologo).lower(), + "private": str(private).lower() + } + + if enhance and model in ["kontext"]: + params["enhance"] = "true" + + if safe: + params["safe"] = "true" + + param_str = "&".join([f"{k}={quote(str(v))}" for k, v in params.items()]) + url = f"{base_url}{encoded_prompt}?{param_str}" + + headers = {"Authorization": f"Bearer {api_token}"} + response = requests.get(url, stream=True, timeout=60, headers=headers) + response.raise_for_status() + + final_prompt = prompt + + temp_dir = tempfile.gettempdir() + filename = f"pollinations_premium_edit_{int(time.time())}.png" + image_path = os.path.join(temp_dir, filename) + + with open(image_path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + + edited_image = Image.open(image_path) + edited_array = np.array(edited_image).astype(np.float32) / 255.0 + edited_tensor = torch.from_numpy(edited_array).unsqueeze(0) + + os.unlink(image_path) + + return (edited_tensor, url, final_prompt) + + except Exception as e: + if "401" in str(e) or "403" in str(e): + error_msg = f"Authentication failed. Check your API token for {model} model." + elif "tier" in str(e).lower(): + error_msg = f"Model {model} requires higher tier access. Upgrade at: https://auth.pollinations.ai" + else: + error_msg = f"Premium editing API error: {str(e)}" + + empty_image = torch.zeros(1, 512, 512, 3) + return (empty_image, error_msg, prompt) + + @classmethod + def IS_CHANGED(cls, **kwargs): + return float("NaN") + + +class PollinationsPremiumTextToSpeech: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "api_token": ("STRING", {"default": ""}), + "text": ("STRING", {"multiline": True, "placeholder": "Enter text to convert to speech..."}), + "model": (TEXT_TO_SPEECH_MODELS, {"default": "openai-audio"}), + "voice": (AVAILABLE_VOICES, {"default": "nova"}), + }, + "optional": { + "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), + "private": ("BOOLEAN", {"default": True, "tooltip": "Keep the generation private"}) + } + } + + RETURN_TYPES = ("AUDIO", "STRING") + RETURN_NAMES = ("audio", "filename") + FUNCTION = "generate_speech" + CATEGORY = "🧪AILab/🌸Pollinations/💎Premium" + + def generate_speech(self, api_token, text, model, voice, seed=0, private=True): + if not api_token.strip(): + error_msg = f"API token required for premium text-to-speech. Get your token at: https://auth.pollinations.ai" + print(f"[PollinationsPremiumTextToSpeech] {error_msg}") + return ({"waveform": torch.zeros(1, 1, 16000), "sample_rate": 16000}, "") + + try: + # Use selected model, fallback to openai-audio if not in list + model_name = model if model in TEXT_TO_SPEECH_MODELS else "openai-audio" + + params = { + "model": model_name, + "voice": voice, + "seed": seed + } + + # Add private parameter for premium features + if private: + params["private"] = "true" + + param_str = "&".join([f"{k}={v}" for k, v in params.items()]) + url = f"https://text.pollinations.ai/{quote(text)}?{param_str}" + + headers = { + "Authorization": f"Bearer {api_token}", + "User-Agent": "ComfyUI-Pollinations/1.3.0" + } + + print(f"[PollinationsPremiumTextToSpeech] Generating speech with model: {model_name}, voice: {voice}") + + response = requests.get(url, headers=headers, stream=True) + + if response.status_code == 200: + # Save the audio file + output_dir = folder_paths.get_temp_directory() + timestamp = int(time.time()) + filename = f"pollinations_tts_premium_{timestamp}.mp3" + filepath = os.path.join(output_dir, filename) + + with open(filepath, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + + # Load audio using torchaudio + waveform, sample_rate = torchaudio.load(filepath) + + # Convert to the format expected by ComfyUI + audio_dict = { + "waveform": waveform.unsqueeze(0), # Add batch dimension + "sample_rate": sample_rate + } + + print(f"[PollinationsPremiumTextToSpeech] Audio generated successfully: {filename}") + return (audio_dict, filename) + + elif response.status_code == 401: + error_msg = "Invalid API token. Please check your token at https://auth.pollinations.ai" + print(f"[PollinationsPremiumTextToSpeech] {error_msg}") + return ({"waveform": torch.zeros(1, 1, 16000), "sample_rate": 16000}, "") + + elif response.status_code == 402: + error_msg = "Insufficient tier access. Premium text-to-speech requires 'seed' tier or higher." + print(f"[PollinationsPremiumTextToSpeech] {error_msg}") + return ({"waveform": torch.zeros(1, 1, 16000), "sample_rate": 16000}, "") + + else: + error_msg = f"API request failed with status {response.status_code}: {response.text}" + print(f"[PollinationsPremiumTextToSpeech] {error_msg}") + return ({"waveform": torch.zeros(1, 1, 16000), "sample_rate": 16000}, "") + + except Exception as e: + error_msg = f"Error generating speech: {str(e)}" + print(f"[PollinationsPremiumTextToSpeech] {error_msg}") + return ({"waveform": torch.zeros(1, 1, 16000), "sample_rate": 16000}, "") + + @classmethod + def IS_CHANGED(cls, **kwargs): + return float("NaN") + + +NODE_CLASS_MAPPINGS = { + "PollinationsPremiumImageGen": PollinationsPremiumImageGen, + "PollinationsPremiumImageEdit": PollinationsPremiumImageEdit, + "PollinationsPremiumTextToSpeech": PollinationsPremiumTextToSpeech, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "PollinationsPremiumImageGen": "Premium Image Gen 🔑 (Pollinations)", + "PollinationsPremiumImageEdit": "Premium Image Edit 🔑 (Pollinations)", + "PollinationsPremiumTextToSpeech": "Premium Text-to-Speech 🔑 (Pollinations)", +}