diff --git a/README.md b/README.md index 0e520a6..fa2da2e 100644 --- a/README.md +++ b/README.md @@ -26,13 +26,13 @@ Whether you are running on an 8GB laptop or a cloud server, this node suite offl ### 1. 🌸 Pollinations Image Gen (BYOP) Generates high-fidelity images directly to a ComfyUI `IMAGE` tensor. * **Supported Models:** - * `kontext` - * `nanobanana` - * `nanobanana-pro` - * `seedream` - * `seedream-pro` + * `kontext πŸ’Ž` + * `nanobanana πŸ’Ž` + * `nanobanana-pro πŸ’Ž` + * `seedream πŸ’Ž` + * `seedream-pro πŸ’Ž` * `gptimage` - * `gptimage-large` + * `gptimage-large πŸ’Ž` * `flux` * `zimage` * `klein` @@ -44,12 +44,12 @@ Generates high-fidelity images directly to a ComfyUI `IMAGE` tensor. ### 2. 🌸 Pollinations Video Gen (BYOP) Generates high-quality AI video. * **Supported Models:** - * `veo` + * `veo πŸ’Ž` * `seedance` - * `seedance-pro` - * `wan` + * `seedance-pro πŸ’Ž` + * `wan πŸ’Ž` * `grok-video` - * `ltx-2` + * `ltx-2 πŸ’Ž` * **Parameters:** `prompt`, `model`, `seed`, `api_key` ### 3. 🌸 Pollinations Text Gen (BYOP) @@ -61,19 +61,19 @@ Leverage top-tier LLMs for prompt expansion, dynamic tagging, or scriptwriting i * `qwen-coder` * `mistral` * `openai-audio` - * `gemini` + * `gemini πŸ’Ž` * `gemini-fast` * `deepseek` - * `grok` + * `grok πŸ’Ž` * `gemini-search` * `midijourney` * `claude-fast` - * `claude` - * `claude-large` + * `claude πŸ’Ž` + * `claude-large πŸ’Ž` * `perplexity-fast` * `perplexity-reasoning` * `kimi` - * `gemini-large` + * `gemini-large πŸ’Ž` * `nova-fast` * `glm` * `minimax` diff --git a/models.json b/models.json index 64d84e1..77b3100 100644 --- a/models.json +++ b/models.json @@ -1,12 +1,12 @@ { "image": [ - "kontext", - "nanobanana", - "nanobanana-pro", - "seedream", - "seedream-pro", + "kontext \ud83d\udc8e", + "nanobanana \ud83d\udc8e", + "nanobanana-pro \ud83d\udc8e", + "seedream \ud83d\udc8e", + "seedream-pro \ud83d\udc8e", "gptimage", - "gptimage-large", + "gptimage-large \ud83d\udc8e", "flux", "zimage", "klein", @@ -15,12 +15,12 @@ "grok-imagine" ], "video": [ - "veo", + "veo \ud83d\udc8e", "seedance", - "seedance-pro", - "wan", + "seedance-pro \ud83d\udc8e", + "wan \ud83d\udc8e", "grok-video", - "ltx-2" + "ltx-2 \ud83d\udc8e" ], "text": [ "openai", @@ -29,19 +29,19 @@ "qwen-coder", "mistral", "openai-audio", - "gemini", + "gemini \ud83d\udc8e", "gemini-fast", "deepseek", - "grok", + "grok \ud83d\udc8e", "gemini-search", "midijourney", "claude-fast", - "claude", - "claude-large", + "claude \ud83d\udc8e", + "claude-large \ud83d\udc8e", "perplexity-fast", "perplexity-reasoning", "kimi", - "gemini-large", + "gemini-large \ud83d\udc8e", "nova-fast", "glm", "minimax", diff --git a/pollinations_auto_updater.py b/pollinations_auto_updater.py index 91f900f..34e541c 100644 --- a/pollinations_auto_updater.py +++ b/pollinations_auto_updater.py @@ -11,11 +11,15 @@ JSON_FILE = os.path.join(REPO_DIR, "models.json") README_FILE = os.path.join(REPO_DIR, "README.md") def fetch_api_models(): - print("πŸ›°οΈ Scouting Pollinations API for new models...") + print("πŸ›°οΈ Scouting Pollinations API for new models (Premium Aware)...") try: # 1. Fetch Text Models text_resp = requests.get("https://gen.pollinations.ai/text/models", timeout=10).json() - text_models = [m["name"] for m in text_resp if "name" in m] + # We append πŸ’Ž if paid_only is True + text_models = [ + f"{m['name']} πŸ’Ž" if m.get("paid_only") else m["name"] + for m in text_resp if "name" in m + ] # 2. Fetch Image & Video Models img_vid_resp = requests.get("https://gen.pollinations.ai/image/models", timeout=10).json() @@ -27,18 +31,17 @@ def fetch_api_models(): model_id = m.get("name") if not model_id: continue - # FIXED LOGIC: Detect video models via output_modalities array + # Append diamond for paid models + display_name = f"{model_id} πŸ’Ž" if m.get("paid_only") else model_id + + # Detect video models via output_modalities array out_modalities = m.get("output_modalities", []) if "video" in out_modalities: - video_models.append(model_id) + video_models.append(display_name) else: - image_models.append(model_id) + image_models.append(display_name) - # Safety defaults to ensure nodes don't break if API is empty - if "flux" not in image_models: image_models.insert(0, "flux") - if "wan" not in video_models: video_models.insert(0, "wan") - if "openai" not in text_models: text_models.insert(0, "openai") - + # Safety defaults (Clean IDs - πŸ’Ž logic handled later in script if missing) return {"image": image_models, "video": video_models, "text": text_models} except Exception as e: print(f"❌ API Fetch Failed: {e}") @@ -58,9 +61,7 @@ def update_readme(models): content = f.read() def replace_md_list(section_id, new_list, text): - """Surgically replaces the bulleted list in the README based on section headers.""" escaped_id = re.escape(section_id) - # Matches from header + Supported Models line until the Parameters line or section break pattern = rf"(###.*?{escaped_id}.*?\n\* \*\*Supported Models:\*\*.*?\n)(.*?)(\n\* \*\*Parameters|\n---|\n##)" formatted_list = "\n".join([f" * `{m}`" for m in new_list]) return re.sub(pattern, f"\\1{formatted_list}\\3", text, flags=re.DOTALL | re.IGNORECASE) @@ -75,32 +76,26 @@ def update_readme(models): def git_sync_everything(): print("πŸš€ Synchronizing local machine to GitHub and HuggingFace...") try: - # 1. Add everything (Includes new folders: images/, workflows/) subprocess.run(["git", "add", "."], cwd=REPO_DIR) - # 2. Check if there are actually changes to commit status = subprocess.run(["git", "status", "--porcelain"], capture_output=True, text=True, cwd=REPO_DIR) if not status.stdout.strip(): - print(" βœ… Local state matches previous commit. No push needed.") + print(" βœ… Local state matches previous commit.") return - # 3. Create the commit date_str = datetime.now().strftime("%Y-%m-%d") - subprocess.run(["git", "commit", "-m", f"Auto-Sync: Models, Assets, and Workflows ({date_str})"], cwd=REPO_DIR) + subprocess.run(["git", "commit", "-m", f"Auto-Sync: Premium Labels & Models ({date_str})"], cwd=REPO_DIR) - # 4. Pull first to resolve any cloud-side changes (Force our version) print(" Merging cloud changes...") subprocess.run(["git", "pull", "origin", "main", "-X", "ours", "--no-edit"], cwd=REPO_DIR) - # 5. Push to GitHub print(" Pushing to GitHub (origin)...") subprocess.run(["git", "push", "origin", "main"], cwd=REPO_DIR) - # 6. Push to HuggingFace print(" Pushing to HuggingFace...") subprocess.run(["git", "push", "huggingface", "main", "--force"], cwd=REPO_DIR) - print("πŸŽ‰ Full sync complete! All folders and models are now live.") + print("πŸŽ‰ Full sync complete!") except Exception as e: print(f"❌ Git Sync Failed: {e}") diff --git a/pollinations_nodes.py b/pollinations_nodes.py index 69c8d9e..0a34a92 100644 --- a/pollinations_nodes.py +++ b/pollinations_nodes.py @@ -5,20 +5,17 @@ import numpy as np from PIL import Image import io import json -import time -import logging import os +import logging -# SILENCE LOGGERS (Prevent WinError 233) +# SILENCE LOGGERS to prevent any console/pipe conflicts logging.getLogger("requests").setLevel(logging.CRITICAL) logging.getLogger("urllib3").setLevel(logging.CRITICAL) def load_model_list(category, default_list): - """Safely loads model lists from models.json relative to this file.""" try: current_dir = os.path.dirname(os.path.abspath(__file__)) json_path = os.path.join(current_dir, "models.json") - if os.path.exists(json_path): with open(json_path, 'r', encoding='utf-8') as f: data = json.load(f) @@ -30,17 +27,14 @@ def load_model_list(category, default_list): class PollinationsImageGen: @classmethod def INPUT_TYPES(s): - # Default fallback if JSON fails - defaults = ["flux", "turbo", "klein"] - model_list = load_model_list("image", defaults) - + model_list = load_model_list("image", ["flux", "zimage", "klein-large"]) return { "required": { - "prompt": ("STRING", {"multiline": True, "default": "A cinematic product shot..."}), + "prompt": ("STRING", {"multiline": True, "default": "a cat in space"}), "model": (model_list, {"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": 0, "max": 0xffffffffffffffff}), + "seed": ("INT", {"default": 42, "min": -1, "max": 2147483647}), }, "optional": { "api_key": ("STRING", {"default": "", "multiline": False}), @@ -54,51 +48,60 @@ class PollinationsImageGen: CATEGORY = "Pollinations" def generate(self, prompt, model, width, height, seed, api_key, negative_prompt=""): - encoded_prompt = urllib.parse.quote(prompt) - url = f"https://image.pollinations.ai/prompt/{encoded_prompt}?model={model}&width={width}&height={height}&seed={seed}&nologo=true" + # Clean the model name by removing the diamond emoji and extra spaces + model = model.replace("πŸ’Ž", "").strip() + # 1. Official Path Construction + encoded_prompt = urllib.parse.quote(prompt.replace("\n", " ").strip()) + + # Base URL from official api.json + base_url = f"https://gen.pollinations.ai/image/{encoded_prompt}" + + # 2. Query Parameters (Official Spec) + params = { + "model": model, + "width": width, + "height": height, + "seed": seed, + "nologo": "true" + } if negative_prompt and negative_prompt.strip() != "": - encoded_neg = urllib.parse.quote(negative_prompt) - url += f"&nofeed={encoded_neg}" + params["negative_prompt"] = negative_prompt.strip() + # 3. Headers (Crucial for WAF bypass and BYOP) headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", - "Accept": "image/jpeg, image/png, image/*" } if api_key and api_key.strip() != "": headers["Authorization"] = f"Bearer {api_key.strip()}" try: - response = requests.get(url, headers=headers, timeout=120) + response = requests.get(base_url, params=params, headers=headers, timeout=120) + if response.status_code == 200: - image_bytes = io.BytesIO(response.content) - image = Image.open(image_bytes).convert("RGB") + image = Image.open(io.BytesIO(response.content)).convert("RGB") image_np = np.array(image).astype(np.float32) / 255.0 - image_tensor = torch.from_numpy(image_np)[None,] - return (image_tensor,) + return (torch.from_numpy(image_np)[None,],) else: - error_img = torch.zeros((1, 512, 512, 3)) - error_img[:, :, :, 0] = 1.0 - return (error_img,) + # Return Black square with Red pixel in corner on error + err_img = np.zeros((width, height, 3), dtype=np.float32) + err_img[0,0] = [1.0, 0, 0] + return (torch.from_numpy(err_img)[None,],) + except Exception: - error_img = torch.zeros((1, 512, 512, 3)) - error_img[:, :, :, 2] = 1.0 - return (error_img,) + return (torch.zeros((1, 512, 512, 3)),) class PollinationsTextGen: @classmethod def INPUT_TYPES(s): - defaults = ["openai", "claude", "mistral"] - model_list = load_model_list("text", defaults) - + model_list = load_model_list("text", ["openai", "deepseek", "gemini"]) return { "required": { - "prompt": ("STRING", {"multiline": True, "default": "Write a viral TikTok script..."}), - "system_instruction": ("STRING", {"multiline": True, "default": "You are a creative assistant."}), + "prompt": ("STRING", {"multiline": True, "default": "Hello!"}), "model": (model_list, {"default": "openai"}), - "temperature": ("FLOAT", {"default": 0.7, "min": 0.0, "max": 2.0, "step": 0.1}), - "seed": ("INT", {"default": 42, "min": 0, "max": 0xffffffffffffffff}), + "system_instruction": ("STRING", {"multiline": True, "default": "You are a helpful assistant."}), + "seed": ("INT", {"default": 42, "min": -1, "max": 9007199254740991}), }, "optional": { "api_key": ("STRING", {"default": "", "multiline": False}) @@ -110,50 +113,39 @@ class PollinationsTextGen: FUNCTION = "generate" CATEGORY = "Pollinations" - def generate(self, prompt, system_instruction, model, temperature, seed, api_key): - url = "https://text.pollinations.ai/" - headers = {"Content-Type": "application/json", "User-Agent": "ComfyUI-Pollinations-Node"} - + def generate(self, prompt, model, system_instruction, seed, api_key): + # Clean the model name by removing the diamond emoji and extra spaces + model = model.replace("πŸ’Ž", "").strip() + url = "https://gen.pollinations.ai/v1/chat/completions" + headers = {"Content-Type": "application/json"} if api_key and api_key.strip() != "": headers["Authorization"] = f"Bearer {api_key.strip()}" payload = { + "model": model, "messages": [ {"role": "system", "content": system_instruction}, {"role": "user", "content": prompt} ], - "model": model, - "temperature": temperature, "seed": seed } try: response = requests.post(url, headers=headers, json=payload, timeout=60) if response.status_code == 200: - if 'application/json' in response.headers.get('Content-Type', ''): - try: - result_text = response.json().get("choices", [{}])[0].get("message", {}).get("content", "") - return (result_text,) - except: - return (response.text,) - else: - return (response.text,) - else: - return (f"Error: {response.status_code}",) + return (response.json()["choices"][0]["message"]["content"],) + return (f"Error: {response.status_code}",) except Exception as e: - return (f"Exception: {e}",) + return (str(e),) class PollinationsVideoGen: @classmethod def INPUT_TYPES(s): - defaults = ["wan", "luma", "kling"] - model_list = load_model_list("video", defaults) - + model_list = load_model_list("video", ["wan", "seedance", "veo"]) return { "required": { - "prompt": ("STRING", {"multiline": True, "default": "A cinematic shot of..."}), + "prompt": ("STRING", {"multiline": True, "default": "a sunset timelapse"}), "model": (model_list, {"default": "wan"}), - "seed": ("INT", {"default": 42, "min": 0, "max": 0xffffffffffffffff}), }, "optional": { "api_key": ("STRING", {"default": "", "multiline": False}) @@ -165,7 +157,23 @@ class PollinationsVideoGen: FUNCTION = "generate" CATEGORY = "Pollinations" - def generate(self, prompt, model, seed, api_key): - encoded_prompt = urllib.parse.quote(prompt) - url = f"https://video.pollinations.ai/prompt/{encoded_prompt}?model={model}&seed={seed}" - return (url,) \ No newline at end of file + def generate(self, prompt, model, api_key): + # Clean the model name by removing the diamond emoji and extra spaces + model = model.replace("πŸ’Ž", "").strip() + encoded_prompt = urllib.parse.quote(prompt.strip()) + url = f"https://gen.pollinations.ai/video/{encoded_prompt}?model={model}" + if api_key and api_key.strip() != "": + url += f"&key={api_key.strip()}" + return (url,) + +NODE_CLASS_MAPPINGS = { + "PollinationsImageGen": PollinationsImageGen, + "PollinationsTextGen": PollinationsTextGen, + "PollinationsVideoGen": PollinationsVideoGen +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "PollinationsImageGen": "πŸŒΈπŸ–ΌοΈ Pollinations Image Gen (BYOP)", + "PollinationsTextGen": "πŸŒΈπŸ€– Pollinations Text Gen (BYOP)", + "PollinationsVideoGen": "🌸🎞️ Pollinations Video Gen URL (BYOP)" +} \ No newline at end of file