From c1a0c0dd9c8eaaaa87ed77d816d2d34550e8a26d Mon Sep 17 00:00:00 2001 From: Dreaming Computers Date: Sun, 1 Feb 2026 11:55:08 +0700 Subject: [PATCH] feat: add suno music generate node --- README.md | 4 ++ kie_api/suno_music.py | 131 +++++++++++++++++++++++++++++++++++++ nodes.py | 92 ++++++++++++++++++++++++++ web/docs/KIE_Suno_Music.md | 31 +++++++++ 4 files changed, 258 insertions(+) create mode 100644 kie_api/suno_music.py create mode 100644 web/docs/KIE_Suno_Music.md diff --git a/README.md b/README.md index 708a2fc..1eed283 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,10 @@ This node pack currently includes the following nodes: - Text generation node using Gemini 2.5/3 Pro/Flash chat completions. - Supports role selection, media inputs (images/video/audio), optional reasoning output, and Google Search toggle. +- **Suno Music (Generate)** + - Creates a music generation task and returns a task ID. + - Uses KIE Suno API `generate` endpoint. + - **Kling 2.6 Image-to-Video** - Generates video from a single input image. - Uses the Kling 2.6 image-to-video model. diff --git a/kie_api/suno_music.py b/kie_api/suno_music.py new file mode 100644 index 0000000..4d2d66f --- /dev/null +++ b/kie_api/suno_music.py @@ -0,0 +1,131 @@ +"""KIE Suno music generation helper (v1 create-only).""" + +import json +from typing import Any + +from .auth import _load_api_key +from .http import TransientKieError, requests +from .log import _log + +GENERATE_URL = "https://api.kie.ai/api/v1/generate" +MODEL_OPTIONS = ["V4", "V4_5", "V4_5PLUS", "V4_5ALL", "V5"] +VOCAL_GENDER_OPTIONS = ["m", "f"] + + +def _validate_length(field_name: str, value: str | None, max_len: int) -> None: + if value is None: + return + if len(value) > max_len: + raise RuntimeError(f"{field_name} exceeds max length of {max_len} characters.") + + +def _max_prompt_len(model: str) -> int: + return 3000 if model == "V4" else 5000 + + +def _max_style_len(model: str) -> int: + return 200 if model == "V4" else 1000 + + +def run_suno_generate( + *, + prompt: str, + custom_mode: bool, + instrumental: bool, + model: str, + callback_url: str, + style: str | None = None, + title: str | None = None, + negative_tags: str | None = None, + vocal_gender: str | None = None, + style_weight: float | None = None, + weirdness_constraint: float | None = None, + audio_weight: float | None = None, + persona_id: str | None = None, + log: bool = True, +) -> tuple[str, str]: + """Create a Suno music generation task. Returns (task_id, raw_json).""" + if model not in MODEL_OPTIONS: + raise RuntimeError("Invalid model. Use the pinned enum options.") + if vocal_gender and vocal_gender not in VOCAL_GENDER_OPTIONS: + raise RuntimeError("vocal_gender must be 'm' or 'f'.") + if not callback_url: + raise RuntimeError("callback_url is required by the API spec.") + + prompt_text = (prompt or "").strip() + style_text = (style or "").strip() + title_text = (title or "").strip() + + if custom_mode: + if not style_text: + raise RuntimeError("style is required when custom_mode is enabled.") + if not title_text: + raise RuntimeError("title is required when custom_mode is enabled.") + if not instrumental and not prompt_text: + raise RuntimeError("prompt (lyrics) is required when custom_mode is enabled and instrumental is false.") + _validate_length("prompt", prompt_text, _max_prompt_len(model)) + _validate_length("style", style_text, _max_style_len(model)) + _validate_length("title", title_text, 80) + else: + if not prompt_text: + raise RuntimeError("prompt is required when custom_mode is disabled.") + _validate_length("prompt", prompt_text, 500) + # Spec says other fields should be empty in non-custom mode. + if any([style_text, title_text, negative_tags, vocal_gender, persona_id]): + raise RuntimeError("style/title/negative_tags/vocal_gender/persona_id must be empty when custom_mode is false.") + + payload: dict[str, Any] = { + "prompt": prompt_text, + "customMode": bool(custom_mode), + "instrumental": bool(instrumental), + "model": model, + "callBackUrl": callback_url, + } + if style_text: + payload["style"] = style_text + if title_text: + payload["title"] = title_text + if negative_tags: + payload["negativeTags"] = negative_tags + if vocal_gender: + payload["vocalGender"] = vocal_gender + if style_weight is not None: + payload["styleWeight"] = style_weight + if weirdness_constraint is not None: + payload["weirdnessConstraint"] = weirdness_constraint + if audio_weight is not None: + payload["audioWeight"] = audio_weight + if persona_id: + payload["personaId"] = persona_id + + api_key = _load_api_key() + headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"} + + try: + response = requests.post(GENERATE_URL, headers=headers, json=payload, timeout=60) + except requests.RequestException as exc: + raise RuntimeError(f"Failed to call Suno generate endpoint: {exc}") from exc + + if response.status_code == 429 or response.status_code >= 500: + raise TransientKieError( + f"generate returned HTTP {response.status_code}: {response.text}", + status_code=response.status_code, + ) + + try: + payload_json: Any = response.json() + except json.JSONDecodeError as exc: + raise RuntimeError("generate endpoint did not return valid JSON.") from exc + + if payload_json.get("code") != 200: + message = payload_json.get("msg") or payload_json.get("message") + raise RuntimeError(f"generate endpoint returned error code {payload_json.get('code')}: {message}") + + task_id = (payload_json.get("data") or {}).get("taskId") + if not task_id: + raise RuntimeError("generate endpoint did not return a taskId.") + + if log: + _log(log, f"Suno task created: {task_id} (model={model})") + + return task_id, json.dumps(payload_json) diff --git a/nodes.py b/nodes.py index e3e30ac..e724be6 100644 --- a/nodes.py +++ b/nodes.py @@ -41,6 +41,7 @@ from .kie_api.kling26_t2v import ( DURATION_OPTIONS as KLING26_T2V_DURATION_OPTIONS, run_kling26_t2v_video, ) +from .kie_api.suno_music import MODEL_OPTIONS as SUNO_MODEL_OPTIONS, run_suno_generate from .kie_api.gemini3_pro_llm import ( MODEL_OPTIONS as GEMINI3_MODEL_OPTIONS, REASONING_EFFORT_OPTIONS as GEMINI3_REASONING_EFFORT_OPTIONS, @@ -722,6 +723,95 @@ Outputs: return (content, reasoning, raw_json) +class KIE_Suno_Music: + HELP = """ +KIE Suno Music (Generate) + +Create a Suno music generation task via KIE API. Returns a taskId. + +Inputs: +- prompt: Text prompt (lyrics in custom mode when instrumental is false) +- custom_mode: Enable custom mode (required) +- instrumental: Instrumental-only mode (required) +- model: V4 / V4_5 / V4_5PLUS / V4_5ALL / V5 +- callback_url: Webhook URL for results (required by API) +- style: Required in custom mode +- title: Required in custom mode +- negative_tags: Optional tags to avoid +- vocal_gender: m or f (custom mode only) +- style_weight / weirdness_constraint / audio_weight: 0..1 +- persona_id: Optional persona (custom mode only) +- log: Console logging on/off + +Outputs: +- STRING: task_id +- STRING: raw_json +""" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "prompt": ("STRING", {"multiline": True}), + "custom_mode": ("BOOLEAN", {"default": True}), + "instrumental": ("BOOLEAN", {"default": True}), + "model": ("COMBO", {"options": SUNO_MODEL_OPTIONS, "default": "V4_5"}), + "callback_url": ("STRING", {"default": ""}), + }, + "optional": { + "style": ("STRING", {"default": ""}), + "title": ("STRING", {"default": ""}), + "negative_tags": ("STRING", {"default": ""}), + "vocal_gender": ("COMBO", {"options": ["m", "f"], "default": "m"}), + "style_weight": ("FLOAT", {"default": 0.65, "min": 0.0, "max": 1.0, "step": 0.01}), + "weirdness_constraint": ("FLOAT", {"default": 0.65, "min": 0.0, "max": 1.0, "step": 0.01}), + "audio_weight": ("FLOAT", {"default": 0.65, "min": 0.0, "max": 1.0, "step": 0.01}), + "persona_id": ("STRING", {"default": ""}), + "log": ("BOOLEAN", {"default": True}), + }, + } + + RETURN_TYPES = ("STRING", "STRING") + RETURN_NAMES = ("task_id", "raw_json") + FUNCTION = "generate" + CATEGORY = "kie/api" + + def generate( + self, + prompt: str, + custom_mode: bool, + instrumental: bool, + model: str, + callback_url: str, + style: str = "", + title: str = "", + negative_tags: str = "", + vocal_gender: str = "m", + style_weight: float = 0.65, + weirdness_constraint: float = 0.65, + audio_weight: float = 0.65, + persona_id: str = "", + log: bool = True, + ): + task_id, raw_json = run_suno_generate( + prompt=prompt, + custom_mode=custom_mode, + instrumental=instrumental, + model=model, + callback_url=callback_url, + style=style, + title=title, + negative_tags=negative_tags, + vocal_gender=vocal_gender, + style_weight=style_weight, + weirdness_constraint=weirdness_constraint, + audio_weight=audio_weight, + persona_id=persona_id, + log=log, + ) + return (task_id, raw_json) + + class KIE_GridSlice: HELP = """ KIE Grid Slice @@ -909,6 +999,7 @@ NODE_CLASS_MAPPINGS = { "KIE_Kling26Motion_I2V": KIE_Kling26Motion_I2V, "KIE_Flux2_I2I": KIE_Flux2_I2I, "KIE_Gemini3Pro_LLM": KIE_Gemini3Pro_LLM, + "KIE_Suno_Music": KIE_Suno_Music, "KIE_GridSlice": KIE_GridSlice, "KIEParsePromptGridJSON": KIEParsePromptGridJSON, } @@ -925,6 +1016,7 @@ NODE_DISPLAY_NAME_MAPPINGS = { "KIE_Kling26Motion_I2V": "KIE Kling 2.6 Motion-Control (I2V)", "KIE_Flux2_I2I": "KIE Flux 2 (Image-to-Image)", "KIE_Gemini3Pro_LLM": "KIE Gemini (LLM) [Experimental]", + "KIE_Suno_Music": "KIE Suno Music (Generate)", "KIE_GridSlice": "KIE Grid Slice", "KIEParsePromptGridJSON": "KIE Parse Prompt Grid JSON (1..9)", } diff --git a/web/docs/KIE_Suno_Music.md b/web/docs/KIE_Suno_Music.md new file mode 100644 index 0000000..e6dad01 --- /dev/null +++ b/web/docs/KIE_Suno_Music.md @@ -0,0 +1,31 @@ +# KIE Suno Music (Generate) + +Create a Suno music generation task via KIE API. This v1 node returns a task ID; retrieval of the final audio is handled via callback or a future polling node. + +## Inputs +- **prompt** (STRING, required): Prompt text (lyrics in custom mode when instrumental is false). +- **custom_mode** (BOOLEAN, required): Enable custom mode. +- **instrumental** (BOOLEAN, required): Instrumental-only mode. +- **model** (COMBO, required): `V4`, `V4_5`, `V4_5PLUS`, `V4_5ALL`, `V5` +- **callback_url** (STRING, required): Webhook URL to receive results. + +Optional: +- **style** (STRING): Required in custom mode. Length limit depends on model. +- **title** (STRING): Required in custom mode. Max 80 chars. +- **negative_tags** (STRING): Optional. +- **vocal_gender** (COMBO): `m` or `f` (custom mode only). +- **style_weight** (FLOAT): 0..1 +- **weirdness_constraint** (FLOAT): 0..1 +- **audio_weight** (FLOAT): 0..1 +- **persona_id** (STRING): Optional (custom mode only). +- **log** (BOOLEAN): Enable console logging. + +## Outputs +- **task_id** (STRING): Task ID returned by the API. +- **raw_json** (STRING): Full API response JSON. + +## Notes +- Non-custom mode expects **prompt only** (500 chars max); other fields should be empty. +- Custom mode requirements: + - Instrumental: `style` + `title` + - Non-instrumental: `style` + `title` + `prompt`