From c6ce82df364e73f3931ee83cd0e43ce764cbbdcd Mon Sep 17 00:00:00 2001 From: Krissy Date: Sun, 1 Mar 2026 05:54:33 +0000 Subject: [PATCH] Auto-Sync: Quad-Modal Update (2026-03-01) --- README.md | 58 +++++----- __init__.py | 6 +- models.json | 68 ++++++------ pollinations_auto_updater.py | 83 ++++++-------- pollinations_nodes.py | 207 +++++++++++++++++------------------ 5 files changed, 205 insertions(+), 217 deletions(-) diff --git a/README.md b/README.md index fa2da2e..22d1f81 100644 --- a/README.md +++ b/README.md @@ -26,61 +26,61 @@ 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:** + * `flux` + * `gptimage` + * `gptimage-large πŸ’Ž` + * `grok-imagine` + * `imagen-4` + * `klein` + * `klein-large` * `kontext πŸ’Ž` * `nanobanana πŸ’Ž` * `nanobanana-pro πŸ’Ž` * `seedream πŸ’Ž` * `seedream-pro πŸ’Ž` - * `gptimage` - * `gptimage-large πŸ’Ž` - * `flux` * `zimage` - * `klein` - * `klein-large` - * `imagen-4` - * `grok-imagine` * **Parameters:** `prompt`, `model`, `width`, `height`, `seed`, `api_key`, `negative_prompt` ### 2. 🌸 Pollinations Video Gen (BYOP) Generates high-quality AI video. * **Supported Models:** - * `veo πŸ’Ž` - * `seedance` - * `seedance-pro πŸ’Ž` - * `wan πŸ’Ž` * `grok-video` * `ltx-2 πŸ’Ž` + * `seedance` + * `seedance-pro πŸ’Ž` + * `veo πŸ’Ž` + * `wan πŸ’Ž` * **Parameters:** `prompt`, `model`, `seed`, `api_key` ### 3. 🌸 Pollinations Text Gen (BYOP) Leverage top-tier LLMs for prompt expansion, dynamic tagging, or scriptwriting inside your workflow. * **Supported Models:** - * `openai` - * `openai-fast` - * `openai-large` - * `qwen-coder` - * `mistral` - * `openai-audio` + * `claude πŸ’Ž` + * `claude-fast` + * `claude-large πŸ’Ž` + * `deepseek` * `gemini πŸ’Ž` * `gemini-fast` - * `deepseek` - * `grok πŸ’Ž` + * `gemini-large πŸ’Ž` * `gemini-search` + * `glm` + * `grok πŸ’Ž` + * `kimi` * `midijourney` - * `claude-fast` - * `claude πŸ’Ž` - * `claude-large πŸ’Ž` + * `minimax` + * `mistral` + * `nomnom` + * `nova-fast` + * `openai` + * `openai-audio` + * `openai-fast` + * `openai-large` * `perplexity-fast` * `perplexity-reasoning` - * `kimi` - * `gemini-large πŸ’Ž` - * `nova-fast` - * `glm` - * `minimax` - * `nomnom` * `polly` - * `qwen-safety` * `qwen-character` + * `qwen-coder` + * `qwen-safety` * **Parameters:** `prompt`, `system_instruction`, `model`, `temperature`, `seed`, `api_key` --- diff --git a/__init__.py b/__init__.py index c43108d..6887c00 100644 --- a/__init__.py +++ b/__init__.py @@ -3,13 +3,15 @@ from .pollinations_nodes import PollinationsImageGen, PollinationsTextGen, Polli NODE_CLASS_MAPPINGS = { "PollinationsImageGen": PollinationsImageGen, "PollinationsTextGen": PollinationsTextGen, - "PollinationsVideoGen": PollinationsVideoGen + "PollinationsVideoGen": PollinationsVideoGen, + "PollinationsAudioGen": PollinationsAudioGen } NODE_DISPLAY_NAME_MAPPINGS = { "PollinationsImageGen": "πŸŒΈπŸ–ΌοΈ Pollinations Image Gen (BYOP)", "PollinationsTextGen": "πŸŒΈπŸ€– Pollinations Text Gen (BYOP)", - "PollinationsVideoGen": "🌸🎞️ Pollinations Video Gen URL (BYOP)" + "PollinationsVideoGen": "🌸🎞️ Pollinations Video Gen URL (BYOP)", + "PollinationsAudioGen": "πŸŒΈπŸ”Š Pollinations Audio Gen (BYOP)" } __all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] \ No newline at end of file diff --git a/models.json b/models.json index 77b3100..d28fdcc 100644 --- a/models.json +++ b/models.json @@ -1,53 +1,59 @@ { "image": [ + "flux", + "gptimage", + "gptimage-large \ud83d\udc8e", + "grok-imagine", + "imagen-4", + "klein", + "klein-large", "kontext \ud83d\udc8e", "nanobanana \ud83d\udc8e", "nanobanana-pro \ud83d\udc8e", "seedream \ud83d\udc8e", "seedream-pro \ud83d\udc8e", - "gptimage", - "gptimage-large \ud83d\udc8e", - "flux", - "zimage", - "klein", - "klein-large", - "imagen-4", - "grok-imagine" + "zimage" ], "video": [ - "veo \ud83d\udc8e", + "grok-video", + "ltx-2 \ud83d\udc8e", "seedance", "seedance-pro \ud83d\udc8e", - "wan \ud83d\udc8e", - "grok-video", - "ltx-2 \ud83d\udc8e" + "veo \ud83d\udc8e", + "wan \ud83d\udc8e" ], "text": [ - "openai", - "openai-fast", - "openai-large", - "qwen-coder", - "mistral", - "openai-audio", + "claude \ud83d\udc8e", + "claude-fast", + "claude-large \ud83d\udc8e", + "deepseek", "gemini \ud83d\udc8e", "gemini-fast", - "deepseek", - "grok \ud83d\udc8e", + "gemini-large \ud83d\udc8e", "gemini-search", + "glm", + "grok \ud83d\udc8e", + "kimi", "midijourney", - "claude-fast", - "claude \ud83d\udc8e", - "claude-large \ud83d\udc8e", + "minimax", + "mistral", + "nomnom", + "nova-fast", + "openai", + "openai-audio", + "openai-fast", + "openai-large", "perplexity-fast", "perplexity-reasoning", - "kimi", - "gemini-large \ud83d\udc8e", - "nova-fast", - "glm", - "minimax", - "nomnom", "polly", - "qwen-safety", - "qwen-character" + "qwen-character", + "qwen-coder", + "qwen-safety" + ], + "audio": [ + "elevenlabs", + "elevenmusic", + "scribe", + "whisper" ] } \ No newline at end of file diff --git a/pollinations_auto_updater.py b/pollinations_auto_updater.py index 34e541c..57d1206 100644 --- a/pollinations_auto_updater.py +++ b/pollinations_auto_updater.py @@ -11,57 +11,53 @@ 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 (Premium Aware)...") + print("πŸ›°οΈ Scouting Pollinations Official API (Quad-Modal Pass)...") try: - # 1. Fetch Text Models + # 1. Text Models text_resp = requests.get("https://gen.pollinations.ai/text/models", timeout=10).json() - # 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 - ] + 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 + # 2. Image & Video Models img_vid_resp = requests.get("https://gen.pollinations.ai/image/models", timeout=10).json() - - image_models = [] - video_models = [] - + image_models, video_models = [], [] for m in img_vid_resp: - model_id = m.get("name") - if not model_id: continue - - # 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(display_name) + name = m.get("name") + if not name: continue + display = f"{name} πŸ’Ž" if m.get("paid_only") else name + if "video" in m.get("output_modalities", []): + video_models.append(display) else: - image_models.append(display_name) - - # Safety defaults (Clean IDs - πŸ’Ž logic handled later in script if missing) - return {"image": image_models, "video": video_models, "text": text_models} + image_models.append(display) + + # 3. Audio Models (NEW) + audio_resp = requests.get("https://gen.pollinations.ai/audio/models", timeout=10).json() + audio_models = [f"{m['name']} πŸ’Ž" if m.get("paid_only") else m["name"] for m in audio_resp if "name" in m] + + # Safety Fallbacks + if not text_models: text_models = ["openai"] + if not image_models: image_models = ["flux"] + if not video_models: video_models = ["wan"] + if not audio_models: audio_models = ["elevenlabs"] + + return { + "image": sorted(image_models), + "video": sorted(video_models), + "text": sorted(text_models), + "audio": sorted(audio_models) + } except Exception as e: print(f"❌ API Fetch Failed: {e}") return None -def update_json_file(models): - print("πŸ’Ύ Updating models.json...") - with open(JSON_FILE, 'w', encoding='utf-8') as f: - json.dump(models, f, indent=4) - def update_readme(models): print("πŸ“ Updating README.md...") - if not os.path.exists(README_FILE): - return - + if not os.path.exists(README_FILE): return with open(README_FILE, 'r', encoding='utf-8') as f: content = f.read() def replace_md_list(section_id, new_list, text): escaped_id = re.escape(section_id) + # Regex looks for the section header down to the next parameter 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) @@ -69,42 +65,33 @@ def update_readme(models): content = replace_md_list("Image Gen", models["image"], content) content = replace_md_list("Video Gen", models["video"], content) content = replace_md_list("Text Gen", models["text"], content) + content = replace_md_list("Audio Gen", models["audio"], content) with open(README_FILE, 'w', encoding='utf-8') as f: f.write(content) def git_sync_everything(): - print("πŸš€ Synchronizing local machine to GitHub and HuggingFace...") + print("πŸš€ Synchronizing local machine to Cloud...") try: subprocess.run(["git", "add", "."], cwd=REPO_DIR) - 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.") return - date_str = datetime.now().strftime("%Y-%m-%d") - subprocess.run(["git", "commit", "-m", f"Auto-Sync: Premium Labels & Models ({date_str})"], cwd=REPO_DIR) - - print(" Merging cloud changes...") + subprocess.run(["git", "commit", "-m", f"Auto-Sync: Quad-Modal Update ({date_str})"], cwd=REPO_DIR) subprocess.run(["git", "pull", "origin", "main", "-X", "ours", "--no-edit"], cwd=REPO_DIR) - - print(" Pushing to GitHub (origin)...") subprocess.run(["git", "push", "origin", "main"], cwd=REPO_DIR) - - print(" Pushing to HuggingFace...") subprocess.run(["git", "push", "huggingface", "main", "--force"], cwd=REPO_DIR) - print("πŸŽ‰ Full sync complete!") except Exception as e: print(f"❌ Git Sync Failed: {e}") if __name__ == "__main__": - print(f"=== Pollinations Engine Updater: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} ===") os.chdir(REPO_DIR) - live_models = fetch_api_models() if live_models: - update_json_file(live_models) + with open(JSON_FILE, 'w', encoding='utf-8') as f: + json.dump(live_models, f, indent=4) update_readme(live_models) git_sync_everything() \ No newline at end of file diff --git a/pollinations_nodes.py b/pollinations_nodes.py index 0a34a92..8dabe6a 100644 --- a/pollinations_nodes.py +++ b/pollinations_nodes.py @@ -8,172 +8,165 @@ import json import os import logging -# SILENCE LOGGERS to prevent any console/pipe conflicts +# SILENCE LOGGERS logging.getLogger("requests").setLevel(logging.CRITICAL) logging.getLogger("urllib3").setLevel(logging.CRITICAL) -def load_model_list(category, default_list): +def get_models(): + current_dir = os.path.dirname(os.path.abspath(__file__)) + json_path = os.path.join(current_dir, "models.json") 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) - return data.get(category, default_list) - except Exception: - pass - return default_list + return json.load(f) + except: pass + return {"image": ["flux"], "video": ["wan"], "text": ["openai"], "audio": ["elevenlabs"]} + +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 class PollinationsImageGen: @classmethod def INPUT_TYPES(s): - model_list = load_model_list("image", ["flux", "zimage", "klein-large"]) + model_data = get_models() return { "required": { "prompt": ("STRING", {"multiline": True, "default": "a cat in space"}), - "model": (model_list, {"default": "flux"}), + "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}), + "image_input": ("IMAGE",), "negative_prompt": ("STRING", {"multiline": True, "default": ""}) } } - - RETURN_TYPES = ("IMAGE",) - RETURN_NAMES = ("image",) + RETURN_TYPES = ("IMAGE", "STRING") + RETURN_NAMES = ("image", "url") FUNCTION = "generate" - CATEGORY = "Pollinations" + CATEGORY = "Pollinations/Image" - def generate(self, prompt, model, width, height, seed, api_key, negative_prompt=""): - # Clean the model name by removing the diamond emoji and extra spaces - model = model.replace("πŸ’Ž", "").strip() - # 1. Official Path Construction + def generate(self, prompt, model, width, height, seed, api_key="", image_input=None, negative_prompt=""): + clean_model = model.replace("πŸ’Ž", "").strip() encoded_prompt = urllib.parse.quote(prompt.replace("\n", " ").strip()) + url = f"https://gen.pollinations.ai/image/{encoded_prompt}?model={clean_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())}" - # 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() != "": - 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", - } - - if api_key and api_key.strip() != "": - headers["Authorization"] = f"Bearer {api_key.strip()}" - + headers = {"User-Agent": "Mozilla/5.0"} + if api_key.strip(): headers["Authorization"] = f"Bearer {api_key.strip()}" try: - response = requests.get(base_url, params=params, headers=headers, timeout=120) - - if response.status_code == 200: - image = Image.open(io.BytesIO(response.content)).convert("RGB") - image_np = np.array(image).astype(np.float32) / 255.0 - return (torch.from_numpy(image_np)[None,],) - else: - # 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: - return (torch.zeros((1, 512, 512, 3)),) + r = requests.get(url, headers=headers, timeout=120) + if r.status_code == 200: + img = Image.open(io.BytesIO(r.content)).convert("RGB") + img_np = np.array(img).astype(np.float32) / 255.0 + return (torch.from_numpy(img_np)[None,], url) + except: pass + return (torch.zeros((1, 512, 512, 3)), url) -class PollinationsTextGen: +class PollinationsAudioGen: @classmethod def INPUT_TYPES(s): - model_list = load_model_list("text", ["openai", "deepseek", "gemini"]) + model_data = get_models() return { "required": { - "prompt": ("STRING", {"multiline": True, "default": "Hello!"}), - "model": (model_list, {"default": "openai"}), - "system_instruction": ("STRING", {"multiline": True, "default": "You are a helpful assistant."}), - "seed": ("INT", {"default": 42, "min": -1, "max": 9007199254740991}), + "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": "", "multiline": False}) - } + "optional": {"api_key": ("STRING", {"default": ""})} } - RETURN_TYPES = ("STRING",) - RETURN_NAMES = ("text",) + RETURN_NAMES = ("audio_url",) FUNCTION = "generate" - CATEGORY = "Pollinations" + CATEGORY = "Pollinations/Audio" - 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} - ], - "seed": seed - } - - try: - response = requests.post(url, headers=headers, json=payload, timeout=60) - if response.status_code == 200: - return (response.json()["choices"][0]["message"]["content"],) - return (f"Error: {response.status_code}",) - except Exception as e: - return (str(e),) + def generate(self, text, model, voice, api_key=""): + clean_model = model.replace("πŸ’Ž", "").strip() + encoded_text = urllib.parse.quote(text.strip()) + url = f"https://gen.pollinations.ai/audio/{encoded_text}?model={clean_model}&voice={voice}" + if api_key.strip(): url += f"&key={api_key.strip()}" + return (url,) class PollinationsVideoGen: @classmethod def INPUT_TYPES(s): - model_list = load_model_list("video", ["wan", "seedance", "veo"]) + model_data = get_models() return { "required": { - "prompt": ("STRING", {"multiline": True, "default": "a sunset timelapse"}), - "model": (model_list, {"default": "wan"}), + "prompt": ("STRING", {"multiline": True, "default": "cinematic sunset"}), + "model": (model_data["video"], {"default": "wan"}), }, "optional": { - "api_key": ("STRING", {"default": "", "multiline": False}) + "api_key": ("STRING", {"default": ""}), + "image_input": ("IMAGE",), + "duration": ("INT", {"default": 5, "min": 2, "max": 15}), } } - - RETURN_TYPES = ("STRING",) + RETURN_TYPES = ("STRING",) RETURN_NAMES = ("video_url",) FUNCTION = "generate" - CATEGORY = "Pollinations" + CATEGORY = "Pollinations/Video" - def generate(self, prompt, model, api_key): - # Clean the model name by removing the diamond emoji and extra spaces - model = model.replace("πŸ’Ž", "").strip() + def generate(self, prompt, model, api_key="", image_input=None, duration=5): + clean_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()}" + url = f"https://gen.pollinations.ai/video/{encoded_prompt}?model={clean_model}&duration={duration}" + 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 api_key.strip(): url += f"&key={api_key.strip()}" 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 helpful assistant."}), + }, + "optional": {"api_key": ("STRING", {"default": ""})} + } + RETURN_TYPES = ("STRING",) + FUNCTION = "generate" + CATEGORY = "Pollinations/Text" + + def generate(self, prompt, model, system_instruction, api_key=""): + clean_model = model.replace("πŸ’Ž", "").strip() + url = "https://gen.pollinations.ai/v1/chat/completions" + headers = {"Content-Type": "application/json"} + if api_key.strip(): headers["Authorization"] = f"Bearer {api_key.strip()}" + payload = {"model": clean_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",) + NODE_CLASS_MAPPINGS = { "PollinationsImageGen": PollinationsImageGen, "PollinationsTextGen": PollinationsTextGen, - "PollinationsVideoGen": PollinationsVideoGen + "PollinationsVideoGen": PollinationsVideoGen, + "PollinationsAudioGen": PollinationsAudioGen } NODE_DISPLAY_NAME_MAPPINGS = { "PollinationsImageGen": "πŸŒΈπŸ–ΌοΈ Pollinations Image Gen (BYOP)", "PollinationsTextGen": "πŸŒΈπŸ€– Pollinations Text Gen (BYOP)", - "PollinationsVideoGen": "🌸🎞️ Pollinations Video Gen URL (BYOP)" + "PollinationsVideoGen": "🌸🎞️ Pollinations Video Gen URL (BYOP)", + "PollinationsAudioGen": "πŸŒΈπŸ”Š Pollinations Audio Gen (BYOP)" } \ No newline at end of file