""" MuAPI ComfyUI Nodes — Full Suite ================================== Universal nodes for every muapi.ai model category. MuAPITextToImage — T2I (Flux, HiDream, GPT-image, Imagen4, Seedream …) MuAPIImageToImage — I2I (Flux Kontext, GPT-4o edit, Seededit …) MuAPITextToVideo — T2V (Seedance, Kling, Veo3, Wan, Hunyuan …) MuAPIImageToVideo — I2V (all major I2V models) MuAPIExtendVideo — extend a previous generation MuAPIImageEnhance — upscale, bg-remove, face-swap, Ghibli, colorize … MuAPIVideoEdit — effects, dance, dress-change, upscale, watermark … MuAPILipsync — sync, veed, creatify MuAPIAudio — Suno create / remix / extend, mmaudio MuAPIGenerate — fully generic: any endpoint + raw JSON payload Auth: x-api-key header API base: https://api.muapi.ai/api/v1 """ import io import json import ntpath import os import time import numpy as np import requests import torch from PIL import Image try: import folder_paths except ImportError: class _FolderPaths: @staticmethod def get_input_directory(): return os.path.join(os.getcwd(), "input") @staticmethod def get_output_directory(): return os.path.join(os.getcwd(), "output") folder_paths = _FolderPaths() BASE_URL = "https://api.muapi.ai/api/v1" POLL_INTERVAL = 10 MAX_WAIT = 900 # ── Endpoint lists ───────────────────────────────────────────────────────────── T2I_ENDPOINTS = [ "flux-dev-image", "flux-schnell-image", "flux-2-dev", "flux-2-pro", "flux-2-flex", "flux-2-klein-4b", "flux-2-klein-9b", "flux-kontext-dev-t2i", "flux-kontext-pro-t2i", "flux-kontext-max-t2i", "flux-krea-dev", "hidream_i1_fast_image", "hidream_i1_dev_image", "hidream_i1_full_image", "gpt4o-text-to-image", "gpt-image-1.5", "google-imagen4-fast", "google-imagen4", "google-imagen4-ultra", "bytedance-seedream-image", "bytedance-seedream-v4", "bytedance-seedream-v4.5", "seedream-5.0", "wan2.1-text-to-image", "wan2.5-text-to-image", "wan2.6-text-to-image", "hunyuan-image-2.1", "hunyuan-image-3.0", "kling-o1-text-to-image", "ideogram-v3-t2i", "ideogram-v4", "grok-imagine-text-to-image", "chroma-image", "vidu-q2-text-to-image", "ai-anime-generator", "z-image-base", "z-image-turbo", "z-image-p", "qwen-image", "qwen-image-2.0", "qwen-image-2.0-pro", "nano-banana", "nano-banana-pro", "nano-banana-2", "nano-banana-2-1", "midjourney-v7-text-to-image", "midjourney-v7", "midjourney-v8", "midjourney-niji", "leonardoai-lucid-origin", "leonardoai-phoenix-1.0", "reve-text-to-image", "sdxl-image", "perfect-pony-xl", "neta-lumina", "kling-o3-image", "gpt-image-2-text-to-image", "flux-3-text-to-image", "qwen3-image-to-image", "qwen3-pro-image-to-image", "nano-banana-2-lite", "gpt-image-2.5-flare-text-to-image", "gpt-image-2.5-sunburst-text-to-image", "wan3.0-text-to-image", "bytedance-seedream-5.0-pro", "seedream-5.0-flash", "grok-imagine-text-to-image-quality", "wan2.7-text-to-image", "wan2.7-text-to-image-pro", "flux-2-klein-4b-turbo", "flux-2-klein-9b-turbo", "flux-2-klein-4b-text-to-image-lora", "flux-2-klein-9b-text-to-image-lora", "straico-generate-image", "custom", ] I2I_ENDPOINTS = [ "flux-kontext-dev-i2i", "flux-kontext-pro-i2i", "flux-kontext-max-i2i", "flux-kontext-effects", "flux-2-dev-edit", "flux-2-pro-edit", "flux-2-flex-edit", "flux-2-klein-4b-edit", "flux-2-klein-9b-edit", "gpt4o-edit", "gpt4o-image-to-image", "gpt-image-1.5-edit", "bytedance-seededit-image", "bytedance-seedream-edit-v4", "bytedance-seedream-v4.5-edit", "seedream-5.0-edit", "seedance-v2.0-character", "grok-imagine-image-to-image", "kling-o1-edit-image", "ideogram-v3-reframe", "ideogram-character", "wan2.5-image-edit", "wan2.6-image-edit", "flux-redux", "flux-pulid", "tiktok-carousel", "nano-banana-edit", "nano-banana-effects", "nano-banana-pro-edit", "nano-banana-2-edit", "nano-banana-2-1-edit", "qwen-image-edit", "qwen-image-edit-2511", "qwen-image-edit-plus", "qwen-image-edit-plus-lora", "qwen-text-to-image-2512", "qwen-image-2.0-edit", "qwen-image-2.0-pro-edit", "reve-image-edit", "midjourney-v7-image-to-image", "midjourney-v7-omni-reference", "midjourney-v7-style-reference", "minimax-01-subject-reference", "vidu-q2-reference-to-image", "kling-o3-image-edit", "gpt-image-2-image-to-image", "flux-3-image-to-image", "gpt-image-2.5-flare-image-to-image", "gpt-image-2.5-sunburst-image-to-image", "grok-imagine-image-2-edit", "nano-banana-2-lite-edit", "seedream-5.0-flash-edit", "seedream-5.0-flash-layerize", "bytedance-seedream-5.0-pro-edit", "bytedance-seedream-5.0-pro-layer", "wan3.0-image-edit", "muse-image-edit", "gemini-omni-character", "photo-pack", "portrait-stylist", "seedance-2-character", "wan2.7-image-edit", "wan2.7-image-edit-pro", "flux-2-klein-4b-turbo-edit", "flux-2-klein-9b-turbo-edit", "flux-2-klein-4b-edit-lora", "flux-2-klein-9b-edit-lora", "custom", ] T2V_ENDPOINTS = [ "seedance-v2.0-t2v", "seedance-pro-t2v", "seedance-pro-t2v-fast", "seedance-lite-t2v", "seedance-v1.5-pro-t2v", "seedance-v1.5-pro-t2v-fast", "seedance-2.0-new-t2v", "seedance-2.0-t2v-480p", "kling-o1-text-to-video", "kling-v2.1-master-t2v", "kling-v2.5-turbo-pro-t2v", "kling-v2.6-pro-t2v", "kling-v3.0-pro-text-to-video", "kling-v3.0-standard-text-to-video", "veo3-text-to-video", "veo3-fast-text-to-video", "veo3.1-text-to-video", "veo3.1-fast-text-to-video", "veo3.1-4k-video", "grok-imagine-text-to-video", "wan2.1-text-to-video", "wan2.2-text-to-video", "wan2.2-5b-fast-t2v", "wan2.5-text-to-video", "wan2.5-text-to-video-fast", "wan2.6-text-to-video", "hunyuan-text-to-video", "hunyuan-fast-text-to-video", "minimax-hailuo-02-std-t2v", "minimax-hailuo-02-standard-t2v", "minimax-hailuo-02-pro-t2v", "minimax-hailuo-2.3-pro-t2v", "minimax-hailuo-2.3-standard-t2v", "ltx-2-19b-text-to-video", "ltx-2-fast-text-to-video", "ltx-2-pro-text-to-video", "ltx-2.3-text-to-video", "ltx-2.5-text-to-video", "openai-sora", "openai-sora-2-text-to-video", "openai-sora-2-standard-text-to-video", "openai-sora-2-pro-text-to-video", "openai-sora-2-pro-storyboard", "runway-text-to-video", "pixverse-t2v", "pixverse-v4.5-t2v", "pixverse-v5-t2v", "pixverse-v5.5-t2v", "pixverse-v6-t2v", "ovi-text-to-video", "vidu-v2.0-t2v", "vidu-q2-pro-text-to-video", "vidu-q2-turbo-text-to-video", "vidu-q3-pro-text-to-video", "vidu-q3-turbo-text-to-video", "veo-4-text-to-video", "veo3.1-lite-text-to-video", "kling-v3.0-4k-text-to-video", "kling-v3.0-omni-standard-text-to-video", "kling-v3.0-omni-pro-text-to-video", "kling-v3.0-omni-4k-text-to-video", "seedance-2-text-to-video", "seedance-2-text-to-video-fast", "seedance-2-vip-text-to-video", "seedance-2-vip-text-to-video-fast", "seedance-2-vip-text-to-video-1080p", "seedance-2-vip-text-to-video-fast-1080p", "seedance-2-vip-extend", "seedance-2-vip-extend-1080p", "grok-imagine-extend", "happy-horse-1-text-to-video-720p", "happy-horse-1-text-to-video-1080p", "wan2.7-text-to-video", "seedance-2.5-text-to-video", "seedance-2.5-intl-text-to-video", "seedance-2.5-spicy-text-to-video", "seedance-2.5-text-to-video-480p", "seedance-2.5-text-to-video-1080p", "seedance-2.5-text-to-video-4k", "seedance-2.5-intl-text-to-video-480p", "seedance-2.5-intl-text-to-video-1080p", "seedance-2.5-intl-text-to-video-4k", "seedance-2.5-spicy-text-to-video-480p", "seedance-2.5-spicy-text-to-video-1080p", "seedance-2.5-spicy-text-to-video-4k", "flux-3-text-to-video", "flux-3-text-to-video-draft", "happy-horse-1.1-text-to-video-720p", "happy-horse-1.1-text-to-video-1080p", "minimax-h3-open-text-to-video", "minimax-h3-max-turbo-text-to-video", "kling-v3-turbo-pro-text-to-video", "kling-v3-turbo-standard-text-to-video", "wan3.0-text-to-video", "wan3.0-prime-text-to-video", "wan3.0-spicy-text-to-video", "seedance-2-spicy-text-to-video", "seedance-2-spicy-text-to-video-fast", "seedance-2-mini-spicy-text-to-video", "minimax-h3-text-to-video", "minimax-h3-max-text-to-video", "happy-horse-1.1-text-to-video", "custom", ] I2V_ENDPOINTS = [ "seedance-v2.0-i2v", "seedance-pro-i2v", "seedance-pro-i2v-fast", "seedance-lite-i2v", "seedance-v1.5-pro-i2v", "seedance-v1.5-pro-i2v-fast", "seedance-2.0-new-omni", "seedance-2.0-new-first-last", "seedance-2.0-i2v-480p", "seedance-v2.0-omni-reference", "seedance-lite-reference-to-video", "kling-o1-image-to-video", "kling-o1-standard-image-to-video", "kling-o1-reference-to-video", "kling-o1-standard-reference-to-video", "kling-v2.1-master-i2v", "kling-v2.1-pro-i2v", "kling-v2.1-standard-i2v", "kling-v2.5-turbo-pro-i2v", "kling-v2.5-turbo-std-i2v", "kling-v2.6-pro-i2v", "kling-v3.0-pro-image-to-video", "kling-v3.0-standard-image-to-video", "veo3-image-to-video", "veo3-fast-image-to-video", "veo3.1-image-to-video", "veo3.1-fast-image-to-video", "veo3.1-reference-to-video", "grok-imagine-image-to-video", "wan2.1-image-to-video", "wan2.1-lora-i2v", "wan2.1-reference-video", "wan2.2-image-to-video", "wan2.2-spicy-image-to-video", "wan2.6-image-to-video-spicy", "wan2.7-image-to-video-spicy", "wan3.0-spicy-image-to-video", "wan3.0-spicy-reference-to-video", "seedance-2-spicy-image-to-video", "seedance-2-spicy-image-to-video-fast", "seedance-2-mini-spicy-image-to-video", "wan2.5-image-to-video", "wan2.5-image-to-video-fast", "wan2.6-image-to-video", "hunyuan-image-to-video", "vidu-v2.0-i2v", "vidu-q2-pro-start-end-video", "vidu-q1-reference", "vidu-q2-reference", "vidu-q2-turbo-start-end-video", "infinitetalk-image-to-video", "midjourney-i2v", "midjourney-v7-image-to-video", "leonardoai-motion-2.0", "ltx-2-19b-image-to-video", "ltx-2-fast-image-to-video", "ltx-2-pro-image-to-video", "ltx-2.3-image-to-video", "minimax-hailuo-02-pro-i2v", "minimax-hailuo-02-standard-i2v", "minimax-hailuo-2.3-pro-i2v", "minimax-hailuo-2.3-standard-i2v", "minimax-hailuo-2.3-fast", "openai-sora-2-image-to-video", "openai-sora-2-standard-image-to-video", "openai-sora-2-pro-image-to-video", "ovi-image-to-video", "pixverse-v4.5-i2v", "pixverse-v5-i2v", "pixverse-v5.5-i2v", "pixverse-v6-i2v", "pixverse-v6-transition", "runway-image-to-video", "runway-act-two-i2v", "video-effects", "generate_wan_ai_effects", "veo-4-image-to-video", "veo3.1-lite-image-to-video", "kling-v3.0-4k-image-to-video", "kling-v3.0-omni-standard-image-to-video", "kling-v3.0-omni-pro-image-to-video", "kling-v3.0-omni-4k-image-to-video", "seedance-2-image-to-video", "seedance-2-image-to-video-fast", "seedance-2-first-last-frame", "seedance-2-first-last-frame-fast", "seedance-2-omni-reference-no-video", "seedance-2-omni-reference-no-video-fast", "seedance-2-vip-image-to-video", "seedance-2-vip-image-to-video-fast", "seedance-2-vip-first-last-frame", "seedance-2-vip-first-last-frame-fast", "seedance-2-vip-omni-reference", "seedance-2-vip-omni-reference-fast", "seedance-2.0-omni-reference", "seedance-2.0-omni-reference-480p", "seedance-2-vip-image-to-video-1080p", "seedance-2-vip-image-to-video-fast-1080p", "seedance-2-vip-first-last-frame-1080p", "seedance-2-vip-omni-reference-1080p", "seedance-2-vip-omni-reference-fast-1080p", "happy-horse-1-image-to-video-720p", "happy-horse-1-image-to-video-1080p", "happy-horse-1-reference-to-video-720p", "happy-horse-1-reference-to-video-1080p", "vidu-q2-pro-image-to-video", "vidu-q2-turbo-image-to-video", "vidu-q3-pro-image-to-video", "vidu-q3-turbo-image-to-video", "vidu-q4-image-to-video", "vidu-q4-reference-to-video", "vidu-q3-pro-first-last-frames", "vidu-q3-turbo-first-last-frames", "wan2.7-image-to-video", "wan2.7-reference-to-video", "seedance-2.5-image-to-video", "seedance-2.5-intl-image-to-video", "seedance-2.5-spicy-image-to-video", "seedance-2.5-first-last-frame", "seedance-2.5-omni-reference", "seedance-2.5-intl-first-last-frame", "seedance-2.5-intl-omni-reference", "seedance-2.5-spicy-first-last-frame", "seedance-2.5-spicy-omni-reference", "seedance-2.5-image-to-video-480p", "seedance-2.5-image-to-video-1080p", "seedance-2.5-image-to-video-4k", "seedance-2.5-intl-image-to-video-480p", "seedance-2.5-intl-image-to-video-1080p", "seedance-2.5-intl-image-to-video-4k", "seedance-2.5-spicy-image-to-video-480p", "seedance-2.5-spicy-image-to-video-1080p", "seedance-2.5-spicy-image-to-video-4k", "flux-3-image-to-video", "flux-3-omni", "flux-3-start-end-to-video", "wan3.0-image-to-video", "happy-horse-1.1-image-to-video-720p", "happy-horse-1.1-image-to-video-1080p", "happy-horse-1.1-reference-to-video-720p", "happy-horse-1.1-reference-to-video-1080p", "minimax-h3-image-to-video-spicy", "minimax-h3-max-turbo-image-to-video", "minimax-h3-open-image-to-video", "minimax-h3-open-reference-to-video", "kling-v3-turbo-pro-image-to-video", "kling-v3-turbo-standard-image-to-video", "flux-3-image-to-video", "flux-3-omni", "flux-3-start-end-to-video", "wan3.0-image-to-video", "happy-horse-1.1-image-to-video", "happy-horse-1.1-reference-to-video", "minimax-h3-image-to-video", "minimax-h3-reference-to-video", "minimax-h3-max-image-to-video", "ltx-2.5-image-to-video", "gemini-omni-image-to-video", "custom", ] EXTEND_ENDPOINTS = [ "seedance-v2.0-extend", "seedance-v1.5-pro-video-extend", "seedance-v1.5-pro-video-extend-fast", "veo3.1-extend-video", "wan2.2-spicy-video-extend", "grok-imagine-extend", "seedance-2-vip-extend", "seedance-2-vip-extend-1080p", "ltx-2.3-video-extend", "pixverse-v6-extend", "wan2.7-video-extend", "custom", ] ENHANCE_ENDPOINTS = [ "ai-image-upscale", "topaz-image-upscale", "seedvr2-image-upscale", "ai-background-remover", "ai-image-face-swap", "ai-skin-enhancer", "ai-ghibli-style", "ai-color-photo", "ai-image-extension", "ai-object-eraser", "ai-product-shot", "ai-product-photography", "add-image-watermark", "custom", ] VIDEO_EDIT_ENDPOINTS = [ "video-effects", "image-effects", "ai-dance-effects", "ai-dress-change", "ai-video-face-swap", "ai-video-upscaler", "ai-video-upscaler-pro", "topaz-video-upscale", "video-watermark-remover", "seedance-v2.0-watermark-remover", "seedance-v2.0-video-watermark-remover-pro", "seedance-v2.0-video-edit", "add-video-watermark", "ai-clipping", "wan2.2-edit-video", "wan2.2-animate", "heygen-video-translate", "infinitetalk-video-to-video", "kling-o1-video-edit", "kling-o1-video-edit-fast", "kling-o1-standard-video-edit", "kling-v2.6-pro-motion-control", "kling-v2.6-std-motion-control", "kling-v3.0-pro-motion-control", "kling-v3.0-std-motion-control", "luma-flash-reframe", "luma-modify-video", "runway-act-two-v2v", "runway-aleph-v2v", "remix-video", "mmaudio-v2/video-to-video", "ai-captions", "autocrop", "video-combiner", "ltx-2.3-video-extend", "pixverse-v6-extend", "wan2.7-video-edit", "wan2.7-video-extend", "seedance-2.5-video-edit", "seedance-2.5-intl-video-edit", "seedance-2.5-spicy-video-edit", "seedance-2.5-video-extend", "seedance-2.5-intl-video-extend", "seedance-2.5-spicy-video-extend", "seedance-2.5-motion-control", "seedance-2.5-intl-motion-control", "seedance-2.5-spicy-motion-control", "flux-3-video-extend", "flux-3-video-extend-draft", "flux-3-video-upscaler", "gemini-omni-video-edit", "gemini-omni-flash-1-1-edit", "happy-horse-1.1-video-edit", "happy-horse-1.1-video-edit-720p", "happy-horse-1.1-video-edit-1080p", "happy-horse-1-video-edit-720p", "happy-horse-1-video-edit-1080p", "seedance-2-video-watermark-remover-pro", "seedance-2.0-watermark-remover", "custom", ] LIPSYNC_ENDPOINTS = [ "sync-lipsync", "veed-lipsync", "creatify-lipsync", "latentsync-video", "ltx-2-19b-lipsync", "ltx-2.3-lipsync", "kling-v1-avatar-pro", "kling-v1-avatar-standard", "kling-v2-avatar-pro", "kling-v2-avatar-standard", "wan2.2-speech-to-video", "custom", ] AUDIO_ENDPOINTS = [ "suno-create-music", "suno-remix-music", "suno-extend-music", "suno-add-instrumental", "suno-add-vocals", "suno-generate-mashup", "suno-generate-sounds", "mmaudio-text-to-audio", "mmaudio-video-to-audio", "mmaudio-v2/text-to-audio", "minimax-speech-2.6-hd", "minimax-speech-2.6-turbo", "minimax-voice-clone", "elevenlabs-v4", "fish-s2-1-pro", "lyria-3-pro", "lyria-3", "lyria-2", "custom", ] IMAGE_TO_3D_ENDPOINTS = [ "meshy-6-text-to-3d", "meshy-6-image-to-3d", "meshy-6-multi-image-to-3d", "tripo3d-p1-text-to-3d", "tripo3d-p1-image-to-3d", "tripo3d-h31-text-to-3d", "tripo3d-h31-image-to-3d", "tripo3d-h31-multiview-to-3d", "custom", ] # ── Shared helpers ───────────────────────────────────────────────────────────── def _load_api_key(api_key_input): """Return api_key_input if set, otherwise fall back to ~/.muapi/config.json.""" if api_key_input and api_key_input.strip(): return api_key_input.strip() config_path = os.path.expanduser("~/.muapi/config.json") if os.path.isfile(config_path): try: with open(config_path) as f: key = json.load(f).get("api_key", "") if key: return key except Exception: pass raise ValueError( "No API key found. Either paste your key into the api_key field, " "or run `muapi auth configure --api-key YOUR_KEY` in a terminal." ) def _upload_image(api_key, image_tensor): if image_tensor.dim() == 4: image_tensor = image_tensor[0] arr = (image_tensor.cpu().numpy() * 255).astype("uint8") buf = io.BytesIO() Image.fromarray(arr, "RGB").save(buf, format="JPEG", quality=95) buf.seek(0) resp = requests.post(f"{BASE_URL}/upload_file", headers={"x-api-key": api_key}, files={"file": ("image.jpg", buf, "image/jpeg")}, timeout=120) _check(resp) return _url(resp.json()) def _confined_upload_path(path): """Resolve a user path and require it to stay in ComfyUI input/output.""" if not isinstance(path, str) or not path.strip(): raise ValueError("Upload path is required") candidate = path.strip() if os.path.isabs(candidate) or ntpath.isabs(candidate): raise ValueError("Upload path must be relative to ComfyUI input or output") path_parts = candidate.replace("\\", "/").split("/") if ".." in path_parts: raise ValueError("Upload path cannot contain '..'") resolved = os.path.realpath(candidate) roots = ( os.path.realpath(folder_paths.get_input_directory()), os.path.realpath(folder_paths.get_output_directory()), ) try: confined = any(os.path.commonpath((resolved, root)) == root for root in roots) except ValueError: confined = False if not confined: raise ValueError("Upload path must remain inside ComfyUI input or output") return resolved def _upload_file(api_key, path): safe_path = _confined_upload_path(path) ext = safe_path.lower().rsplit(".", 1)[-1] if "." in safe_path else "" if ext in ("mp4", "mov", "webm"): mime = "video/mp4" elif ext == "mp3": mime = "audio/mpeg" elif ext == "wav": mime = "audio/wav" elif ext == "aac": mime = "audio/aac" else: mime = "image/jpeg" with open(safe_path, "rb") as fh: resp = requests.post(f"{BASE_URL}/upload_file", headers={"x-api-key": api_key}, files={"file": (os.path.basename(safe_path), fh, mime)}, timeout=300) _check(resp) return _url(resp.json()) def _url(data): u = data.get("url") or data.get("file_url") or data.get("output") if not u: raise RuntimeError(f"Upload missing URL: {data}") return str(u) def _submit(api_key, endpoint, payload): resp = requests.post(f"{BASE_URL}/{endpoint}", headers={"x-api-key": api_key, "Content-Type": "application/json"}, json=payload, timeout=60) _check(resp) rid = resp.json().get("request_id") if not rid: raise RuntimeError(f"No request_id: {resp.json()}") return rid def _poll(api_key, request_id): deadline = time.time() + MAX_WAIT while time.time() < deadline: resp = requests.get(f"{BASE_URL}/predictions/{request_id}/result", headers={"x-api-key": api_key}, timeout=30) _check(resp, skip_400=True) data = resp.json() status = data.get("status") print(f"[MuAPI] {status} {request_id}") if status == "completed": return data if status == "failed" or resp.status_code == 400: error = data.get("error", "unknown error") if isinstance(error, dict): error = error.get("message", "unknown error") raise RuntimeError(f"Failed: {error}") time.sleep(POLL_INTERVAL) raise RuntimeError(f"Timeout {request_id}") def _output_url(result): out = result.get("outputs") or result.get("output") or [] if isinstance(out, list) and out: return str(out[0]) if isinstance(out, str): return out for k in ("video_url", "image_url", "audio_url", "url"): if result.get(k): return str(result[k]) raise RuntimeError(f"No output URL: {result}") def _check(resp, skip_400=False): if resp.status_code == 401: raise RuntimeError("Auth failed — check API key.") if resp.status_code == 402: raise RuntimeError("Insufficient credits — top up at muapi.ai") if resp.status_code == 429: raise RuntimeError("Rate limited — retry later.") if resp.status_code == 400 and skip_400: return resp.raise_for_status() def _img_from_url(url): try: r = requests.get(url, timeout=60) r.raise_for_status() arr = np.array(Image.open(io.BytesIO(r.content)).convert("RGB")).astype(np.float32) / 255.0 return torch.from_numpy(arr).unsqueeze(0) except Exception as e: print(f"[MuAPI] image download failed: {e}") return torch.zeros(1, 64, 64, 3) def _first_frame(video_url): try: import tempfile, cv2 r = requests.get(video_url, timeout=180, stream=True) r.raise_for_status() with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as tmp: for chunk in r.iter_content(8192): if chunk: tmp.write(chunk) path = tmp.name cap = cv2.VideoCapture(path) ret, frame = cap.read() cap.release(); os.remove(path) if not ret: raise RuntimeError("no frame") rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0 return torch.from_numpy(rgb).unsqueeze(0) except Exception as e: print(f"[MuAPI] first frame failed: {e}") return torch.zeros(1, 64, 64, 3) def _ep(model, custom): if model == "custom": ep = custom.strip() if not ep: raise ValueError("Set custom_endpoint when model='custom'.") return ep return model def _extra(s): try: return json.loads(s or "{}") except json.JSONDecodeError as e: raise ValueError(f"Invalid JSON: {e}") # ── Nodes ────────────────────────────────────────────────────────────────────── class MuAPITextToImage: @classmethod def INPUT_TYPES(cls): return {"required": { "model": (T2I_ENDPOINTS, {"default": "flux-dev-image"}), "prompt": ("STRING", {"multiline": True, "default": "A photorealistic portrait of an astronaut on Mars"}), "aspect_ratio": (["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3"], {"default": "1:1"}), }, "optional": { "api_key": ("STRING", {"multiline": False, "default": ""}), "negative_prompt": ("STRING", {"multiline": True, "default": ""}), "custom_endpoint": ("STRING", {"multiline": False, "default": ""}), "extra_params_json": ("STRING", {"multiline": True, "default": "{}"}), }} RETURN_TYPES = ("IMAGE", "STRING", "STRING") RETURN_NAMES = ("image", "image_url", "request_id") FUNCTION = "run" CATEGORY = "🎨 MuAPI" def run(self, model, prompt, aspect_ratio, api_key="", negative_prompt="", custom_endpoint="", extra_params_json="{}"): api_key = _load_api_key(api_key) endpoint = _ep(model, custom_endpoint) payload = {"prompt": prompt, "aspect_ratio": aspect_ratio, **_extra(extra_params_json)} if negative_prompt.strip(): payload["negative_prompt"] = negative_prompt.strip() needs_wh = [ "flux-dev", "flux-schnell", "flux-2-dev", "flux-2-pro", "flux-2-flex", "flux-2-klein", "hidream_i1", "ai-anime", "wan2.", "hunyuan-image", "chroma-image", "z-image-turbo", "z-image-p", "sdxl-image", "perfect-pony-xl", "neta-lumina", ] if any(endpoint.startswith(p) for p in needs_wh) and "kontext" not in endpoint: mapping = { "1:1": (1024, 1024), "16:9": (1344, 768), "9:16": (768, 1344), "4:3": (1152, 864), "3:4": (864, 1152), "3:2": (1216, 832), "2:3": (832, 1216), "21:9": (1536, 640), "9:21": (640, 1536) } w, h = mapping.get(aspect_ratio, (1024, 1024)) payload["width"] = w payload["height"] = h print(f"[MuAPI T2I] {endpoint}") rid = _submit(api_key, endpoint, payload) result = _poll(api_key, rid) url = _output_url(result) return (_img_from_url(url), url, rid) class MuAPIImageToImage: @classmethod def INPUT_TYPES(cls): return {"required": { "model": (I2I_ENDPOINTS, {"default": "flux-kontext-pro-i2i"}), "image": ("IMAGE",), "prompt": ("STRING", {"multiline": True, "default": "Transform into a watercolour painting"}), }, "optional": { "api_key": ("STRING", {"multiline": False, "default": ""}), "custom_endpoint": ("STRING", {"multiline": False, "default": ""}), "extra_params_json": ("STRING", {"multiline": True, "default": "{}"}), }} RETURN_TYPES = ("IMAGE", "STRING", "STRING") RETURN_NAMES = ("image", "image_url", "request_id") FUNCTION = "run" CATEGORY = "🎨 MuAPI" def run(self, model, image, prompt, api_key="", custom_endpoint="", extra_params_json="{}"): api_key = _load_api_key(api_key) endpoint = _ep(model, custom_endpoint) print(f"[MuAPI I2I] Uploading image...") img_url = _upload_image(api_key, image) # Detect if endpoint needs images_list (array) or image_url (string) needs_list = any(x in endpoint for x in [ "kontext", "wan2.", "vidu", "seedream", "seedance", "pixverse", "flux-2", "nano-banana", "qwen-image-edit", "openai-sora", "kling-o3", ]) if needs_list: payload = {"prompt": prompt, "images_list": [img_url], **_extra(extra_params_json)} else: payload = {"prompt": prompt, "image_url": img_url, **_extra(extra_params_json)} print(f"[MuAPI I2I] {endpoint}") rid = _submit(api_key, endpoint, payload) result = _poll(api_key, rid) url = _output_url(result) return (_img_from_url(url), url, rid) class MuAPITextToVideo: @classmethod def INPUT_TYPES(cls): return {"required": { "model": (T2V_ENDPOINTS, {"default": "seedance-v2.0-t2v"}), "prompt": ("STRING", {"multiline": True, "default": "A cinematic aerial shot of a futuristic city at dusk"}), "aspect_ratio": (["16:9", "9:16", "1:1", "4:3", "3:4", "21:9"], {"default": "16:9"}), "quality": (["basic", "high"], {"default": "basic"}), "duration": ("INT", {"default": 5, "min": 4, "max": 30, "step": 1}), }, "optional": { "api_key": ("STRING", {"multiline": False, "default": ""}), "custom_endpoint": ("STRING", {"multiline": False, "default": ""}), "extra_params_json": ("STRING", {"multiline": True, "default": "{}"}), }} RETURN_TYPES = ("STRING", "IMAGE", "STRING") RETURN_NAMES = ("video_url", "first_frame", "request_id") FUNCTION = "run" CATEGORY = "🎬 MuAPI" def run(self, model, prompt, aspect_ratio, quality, duration, api_key="", custom_endpoint="", extra_params_json="{}"): api_key = _load_api_key(api_key) endpoint = _ep(model, custom_endpoint) payload = {"prompt": prompt, "aspect_ratio": aspect_ratio, "quality": quality, "duration": duration, **_extra(extra_params_json)} print(f"[MuAPI T2V] {endpoint}") rid = _submit(api_key, endpoint, payload) result = _poll(api_key, rid) url = _output_url(result) return (url, _first_frame(url), rid) class MuAPIImageToVideo: @classmethod def INPUT_TYPES(cls): return {"required": { "model": (I2V_ENDPOINTS, {"default": "seedance-v2.0-i2v"}), "prompt": ("STRING", {"multiline": True, "default": "The character in @image1 walks through a beautiful garden"}), "aspect_ratio": (["16:9", "9:16", "1:1", "4:3", "3:4", "21:9"], {"default": "16:9"}), "quality": (["basic", "high"], {"default": "basic"}), "duration": ("INT", {"default": 5, "min": 5, "max": 10, "step": 5}), }, "optional": { "api_key": ("STRING", {"multiline": False, "default": ""}), "image_1": ("IMAGE",), "image_2": ("IMAGE",), "image_3": ("IMAGE",), "image_4": ("IMAGE",), "image_5": ("IMAGE",), "image_6": ("IMAGE",), "image_7": ("IMAGE",), "image_8": ("IMAGE",), "image_9": ("IMAGE",), "video_file_1": ("STRING", {"multiline": False, "default": ""}), "video_file_2": ("STRING", {"multiline": False, "default": ""}), "video_file_3": ("STRING", {"multiline": False, "default": ""}), "audio_file_1": ("STRING", {"multiline": False, "default": ""}), "audio_file_2": ("STRING", {"multiline": False, "default": ""}), "audio_file_3": ("STRING", {"multiline": False, "default": ""}), "custom_endpoint": ("STRING", {"multiline": False, "default": ""}), "extra_params_json": ("STRING", {"multiline": True, "default": "{}"}), }} RETURN_TYPES = ("STRING", "IMAGE", "STRING") RETURN_NAMES = ("video_url", "first_frame", "request_id") FUNCTION = "run" CATEGORY = "🎬 MuAPI" def run(self, model, prompt, aspect_ratio, quality, duration, api_key="", image_1=None, image_2=None, image_3=None, image_4=None, image_5=None, image_6=None, image_7=None, image_8=None, image_9=None, video_file_1="", video_file_2="", video_file_3="", audio_file_1="", audio_file_2="", audio_file_3="", custom_endpoint="", extra_params_json="{}"): api_key = _load_api_key(api_key) endpoint = _ep(model, custom_endpoint) images_list = [] for i, img in enumerate([image_1, image_2, image_3, image_4, image_5, image_6, image_7, image_8, image_9], 1): if img is not None: print(f"[MuAPI I2V] Uploading image {i}...") images_list.append(_upload_image(api_key, img)) if not images_list: raise ValueError("At least one image required.") # Detect the correct image field for each endpoint: # images_list — seedance, vidu, kontext, pixverse, nano-banana, veo-4, # happy-horse, kling-v3.0-omni, all *reference* endpoints # image_url — wan regular I2V, kling (non-omni), luma, runway, hailuo, # ltx-2, leonardo, midjourney, openai-sora-2-standard payload = {"prompt": prompt, "aspect_ratio": aspect_ratio, "quality": quality, "duration": duration, **_extra(extra_params_json)} if ( any(x in endpoint for x in [ "seedance", "vidu", "kontext", "pixverse", "nano-banana", "veo-4", "happy-horse", "kling-v3.0-omni", ]) or ("openai-sora" in endpoint and "standard" not in endpoint) or "reference" in endpoint ) and endpoint != "vidu-q4-image-to-video": payload["images_list"] = images_list else: payload["image_url"] = images_list[0] # omni-reference models additionally accept video_files and audio_files is_omni = "omni-reference" in endpoint and "no-video" not in endpoint if is_omni: video_urls = [] for i, path in enumerate([video_file_1, video_file_2, video_file_3], 1): if path and path.strip(): safe_path = _confined_upload_path(path) else: safe_path = "" if safe_path and os.path.isfile(safe_path): print(f"[MuAPI I2V] Uploading video_file_{i}...") video_urls.append(_upload_file(api_key, path)) if video_urls: payload["video_files"] = video_urls if "omni-reference" in endpoint: audio_urls = [] for i, path in enumerate([audio_file_1, audio_file_2, audio_file_3], 1): if path and path.strip(): safe_path = _confined_upload_path(path) else: safe_path = "" if safe_path and os.path.isfile(safe_path): print(f"[MuAPI I2V] Uploading audio_file_{i}...") audio_urls.append(_upload_file(api_key, path)) if audio_urls: payload["audio_files"] = audio_urls print(f"[MuAPI I2V] {endpoint} {len(images_list)} image(s)") rid = _submit(api_key, endpoint, payload) result = _poll(api_key, rid) url = _output_url(result) return (url, _first_frame(url), rid) class MuAPIExtendVideo: @classmethod def INPUT_TYPES(cls): return {"required": { "model": (EXTEND_ENDPOINTS, {"default": "seedance-v2.0-extend"}), "request_id": ("STRING", {"multiline": False, "default": ""}), "quality": (["basic", "high"], {"default": "basic"}), "duration": ("INT", {"default": 5, "min": 5, "max": 10, "step": 5}), }, "optional": { "api_key": ("STRING", {"multiline": False, "default": ""}), "custom_endpoint": ("STRING", {"multiline": False, "default": ""}), "extra_params_json": ("STRING", {"multiline": True, "default": "{}"}), }} RETURN_TYPES = ("STRING", "IMAGE", "STRING") RETURN_NAMES = ("video_url", "first_frame", "new_request_id") FUNCTION = "run" CATEGORY = "🎬 MuAPI" def run(self, model, request_id, quality, duration, api_key="", custom_endpoint="", extra_params_json="{}"): api_key = _load_api_key(api_key) if not request_id.strip(): raise ValueError("request_id required.") endpoint = _ep(model, custom_endpoint) payload = {"request_id": request_id.strip(), "quality": quality, "duration": duration, **_extra(extra_params_json)} print(f"[MuAPI Extend] {endpoint}") new_id = _submit(api_key, endpoint, payload) result = _poll(api_key, new_id) url = _output_url(result) return (url, _first_frame(url), new_id) class MuAPIImageEnhance: @classmethod def INPUT_TYPES(cls): return {"required": { "model": (ENHANCE_ENDPOINTS, {"default": "ai-image-upscale"}), "image": ("IMAGE",), }, "optional": { "api_key": ("STRING", {"multiline": False, "default": ""}), "swap_image": ("IMAGE",), "custom_endpoint": ("STRING", {"multiline": False, "default": ""}), "extra_params_json": ("STRING", {"multiline": True, "default": "{}"}), }} RETURN_TYPES = ("IMAGE", "STRING", "STRING") RETURN_NAMES = ("image", "image_url", "request_id") FUNCTION = "run" CATEGORY = "✨ MuAPI" def run(self, model, image, api_key="", swap_image=None, custom_endpoint="", extra_params_json="{}"): api_key = _load_api_key(api_key) endpoint = _ep(model, custom_endpoint) print(f"[MuAPI Enhance] Uploading base image...") img_url = _upload_image(api_key, image) payload = {"image_url": img_url, **_extra(extra_params_json)} if swap_image is not None: # Determine correct key for the secondary image based on endpoint key = "face_image_url" if "face-swap" in endpoint: key = "swap_url" elif "object-eraser" in endpoint: key = "mask_image_url" elif "watermark" in endpoint: key = "watermark_image_url" print(f"[MuAPI Enhance] Uploading secondary image (as {key})...") payload[key] = _upload_image(api_key, swap_image) print(f"[MuAPI Enhance] {endpoint}") rid = _submit(api_key, endpoint, payload) result = _poll(api_key, rid) url = _output_url(result) return (_img_from_url(url), url, rid) class MuAPIVideoEdit: @classmethod def INPUT_TYPES(cls): return {"required": { "model": (VIDEO_EDIT_ENDPOINTS, {"default": "video-effects"}), "video_url": ("STRING", {"multiline": False, "default": ""}), }, "optional": { "api_key": ("STRING", {"multiline": False, "default": ""}), "prompt": ("STRING", {"multiline": True, "default": ""}), "secondary_image": ("IMAGE",), "custom_endpoint": ("STRING", {"multiline": False, "default": ""}), "extra_params_json": ("STRING", {"multiline": True, "default": "{}"}), }} RETURN_TYPES = ("STRING", "IMAGE", "STRING") RETURN_NAMES = ("video_url", "first_frame", "request_id") FUNCTION = "run" CATEGORY = "🎬 MuAPI" def run(self, model, video_url, api_key="", prompt="", secondary_image=None, custom_endpoint="", extra_params_json="{}"): api_key = _load_api_key(api_key) if not video_url.strip(): raise ValueError("video_url required.") endpoint = _ep(model, custom_endpoint) payload = {"video_url": video_url.strip(), **_extra(extra_params_json)} if prompt.strip(): payload["prompt"] = prompt.strip() if secondary_image is not None: # For video face swap, face image often goes to "image_url" # For watermark, it usually goes to "watermark_image_url" key = "image_url" if "watermark" in endpoint: key = "watermark_image_url" print(f"[MuAPI VideoEdit] Uploading secondary image (as {key})...") payload[key] = _upload_image(api_key, secondary_image) print(f"[MuAPI VideoEdit] {endpoint}") rid = _submit(api_key, endpoint, payload) result = _poll(api_key, rid) url = _output_url(result) return (url, _first_frame(url), rid) class MuAPILipsync: @classmethod def INPUT_TYPES(cls): return {"required": { "model": (LIPSYNC_ENDPOINTS, {"default": "sync-lipsync"}), "video_url": ("STRING", {"multiline": False, "default": ""}), "audio_url": ("STRING", {"multiline": False, "default": ""}), }, "optional": { "api_key": ("STRING", {"multiline": False, "default": ""}), "audio_file_path": ("STRING", {"multiline": False, "default": ""}), "custom_endpoint": ("STRING", {"multiline": False, "default": ""}), "extra_params_json": ("STRING", {"multiline": True, "default": "{}"}), }} RETURN_TYPES = ("STRING", "IMAGE", "STRING") RETURN_NAMES = ("video_url", "first_frame", "request_id") FUNCTION = "run" CATEGORY = "🎬 MuAPI" def run(self, model, video_url, audio_url, api_key="", audio_file_path="", custom_endpoint="", extra_params_json="{}"): api_key = _load_api_key(api_key) endpoint = _ep(model, custom_endpoint) if audio_file_path.strip(): safe_audio_path = _confined_upload_path(audio_file_path) else: safe_audio_path = "" if safe_audio_path and os.path.isfile(safe_audio_path): print("[MuAPI Lipsync] Uploading audio...") audio_url = _upload_file(api_key, audio_file_path) if not audio_url.strip(): raise ValueError("audio_url or audio_file_path required.") payload = {"video_url": video_url.strip(), "audio_url": audio_url.strip(), **_extra(extra_params_json)} print(f"[MuAPI Lipsync] {endpoint}") rid = _submit(api_key, endpoint, payload) result = _poll(api_key, rid) url = _output_url(result) return (url, _first_frame(url), rid) class MuAPIAudio: @classmethod def INPUT_TYPES(cls): return {"required": { "model": (AUDIO_ENDPOINTS, {"default": "suno-create-music"}), "prompt": ("STRING", {"multiline": True, "default": "An upbeat electronic track with driving bass"}), }, "optional": { "api_key": ("STRING", {"multiline": False, "default": ""}), "custom_endpoint": ("STRING", {"multiline": False, "default": ""}), "extra_params_json": ("STRING", {"multiline": True, "default": "{}"}), }} RETURN_TYPES = ("STRING", "STRING") RETURN_NAMES = ("audio_url", "request_id") FUNCTION = "run" CATEGORY = "🎵 MuAPI" def run(self, model, prompt, api_key="", custom_endpoint="", extra_params_json="{}"): api_key = _load_api_key(api_key) endpoint = _ep(model, custom_endpoint) payload = {"prompt": prompt, **_extra(extra_params_json)} print(f"[MuAPI Audio] {endpoint}") rid = _submit(api_key, endpoint, payload) result = _poll(api_key, rid) url = _output_url(result) return (url, rid) class MuAPIGenerate: @classmethod def INPUT_TYPES(cls): return {"required": { "endpoint": ("STRING", {"multiline": False, "default": "seedance-v2.0-t2v"}), "payload_json": ("STRING", {"multiline": True, "default": json.dumps({ "image_url": "__file_1__", "swap_url": "__file_2__", "target_index": 0 }, indent=2)}), }, "optional": { "api_key": ("STRING", {"multiline": False, "default": ""}), "file_1": ("IMAGE",), "file_2": ("IMAGE",), "file_3": ("IMAGE",), "file_4": ("IMAGE",), "file_path_1": ("STRING", {"default": ""}), "file_path_2": ("STRING", {"default": ""}), }} RETURN_TYPES = ("STRING", "IMAGE", "STRING", "STRING") RETURN_NAMES = ("output_url", "preview", "request_id", "raw_result_json") FUNCTION = "run" CATEGORY = "🎬 MuAPI" def run(self, endpoint, payload_json, api_key="", file_1=None, file_2=None, file_3=None, file_4=None, file_path_1="", file_path_2=""): api_key = _load_api_key(api_key) if not endpoint.strip(): raise ValueError("endpoint required.") try: payload = json.loads(payload_json) except json.JSONDecodeError as e: raise ValueError(f"Invalid JSON: {e}") s = json.dumps(payload) for i, img in enumerate([file_1, file_2, file_3, file_4], 1): ph = f'"__file_{i}__"' if img is not None and ph in s: print(f"[MuAPI Generic] Uploading file_{i}...") s = s.replace(ph, f'"{_upload_image(api_key, img)}"') for i, p in enumerate([file_path_1, file_path_2], 1): ph = f'"__file_path_{i}__"' if p and p.strip() and ph in s: safe_path = _confined_upload_path(p) else: safe_path = "" if safe_path and os.path.isfile(safe_path): print(f"[MuAPI Generic] Uploading file_path_{i}...") s = s.replace(ph, f'"{_upload_file(api_key, p)}"') payload = json.loads(s) print(f"[MuAPI Generic] {endpoint.strip()}") rid = _submit(api_key, endpoint.strip(), payload) result = _poll(api_key, rid) try: out_url = _output_url(result) except RuntimeError: out_url = "" if out_url and out_url.split("?")[0].lower().endswith((".mp4", ".mov", ".webm")): preview = _first_frame(out_url) elif out_url: preview = _img_from_url(out_url) else: preview = torch.zeros(1, 64, 64, 3) return (out_url, preview, rid, json.dumps(result, indent=2)) class MuAPIImageTo3D: """ Generate 3D assets (GLB/OBJ) from text or an image. Text-to-3D: leave image empty. Image-to-3D: provide image_1 (and image_2/3 for multiview). """ @classmethod def INPUT_TYPES(cls): return {"required": { "model": (IMAGE_TO_3D_ENDPOINTS, {"default": "tripo3d-p1-image-to-3d"}), "prompt": ("STRING", {"multiline": True, "default": ""}), }, "optional": { "api_key": ("STRING", {"multiline": False, "default": ""}), "image_1": ("IMAGE",), "image_2": ("IMAGE",), "image_3": ("IMAGE",), "custom_endpoint": ("STRING", {"multiline": False, "default": ""}), "extra_params_json": ("STRING", {"multiline": True, "default": "{}"}), }} RETURN_TYPES = ("STRING", "STRING") RETURN_NAMES = ("model_url", "request_id") FUNCTION = "run" CATEGORY = "🧊 MuAPI" def run(self, model, prompt, api_key="", image_1=None, image_2=None, image_3=None, custom_endpoint="", extra_params_json="{}"): api_key = _load_api_key(api_key) endpoint = _ep(model, custom_endpoint) images = [] for i, img in enumerate([image_1, image_2, image_3], 1): if img is not None: print(f"[MuAPI 3D] Uploading image {i}...") images.append(_upload_image(api_key, img)) payload = _extra(extra_params_json) if prompt.strip(): payload["prompt"] = prompt.strip() if images: if "multi" in endpoint or "multiview" in endpoint: payload["images_list"] = images else: payload["image_url"] = images[0] print(f"[MuAPI 3D] {endpoint}") rid = _submit(api_key, endpoint, payload) result = _poll(api_key, rid) url = _output_url(result) return (url, rid) class MuAPIApiKey: """ Store your MuAPI API key once and wire it to any node. Leave all node api_key fields empty — they auto-read from this node or from ~/.muapi/config.json (set via `muapi auth configure`). """ @classmethod def INPUT_TYPES(cls): return {"required": { "api_key": ("STRING", {"multiline": False, "default": "", "tooltip": "Your muapi.ai API key. Get one at muapi.ai → Dashboard → API Keys"}), }} RETURN_TYPES = ("STRING",) RETURN_NAMES = ("api_key",) FUNCTION = "run" CATEGORY = "🎬 MuAPI" def run(self, api_key): return (_load_api_key(api_key),) NODE_CLASS_MAPPINGS = { "MuAPIApiKey": MuAPIApiKey, "MuAPITextToImage": MuAPITextToImage, "MuAPIImageToImage": MuAPIImageToImage, "MuAPITextToVideo": MuAPITextToVideo, "MuAPIImageToVideo": MuAPIImageToVideo, "MuAPIExtendVideo": MuAPIExtendVideo, "MuAPIImageEnhance": MuAPIImageEnhance, "MuAPIVideoEdit": MuAPIVideoEdit, "MuAPILipsync": MuAPILipsync, "MuAPIAudio": MuAPIAudio, "MuAPIImageTo3D": MuAPIImageTo3D, "MuAPIGenerate": MuAPIGenerate, } NODE_DISPLAY_NAME_MAPPINGS = { "MuAPIApiKey": "🔑 MuAPI API Key", "MuAPITextToImage": "🎨 MuAPI Text-to-Image", "MuAPIImageToImage": "🎨 MuAPI Image-to-Image", "MuAPITextToVideo": "🎬 MuAPI Text-to-Video", "MuAPIImageToVideo": "🎬 MuAPI Image-to-Video", "MuAPIExtendVideo": "🎬 MuAPI Extend Video", "MuAPIImageEnhance": "✨ MuAPI Image Enhance", "MuAPIVideoEdit": "🎬 MuAPI Video Edit", "MuAPILipsync": "🎬 MuAPI Lipsync", "MuAPIAudio": "🎵 MuAPI Audio", "MuAPIImageTo3D": "🧊 MuAPI Image/Text to 3D", "MuAPIGenerate": "🎬 MuAPI Generate (Generic)", }