feat: add suno music generate node

This commit is contained in:
Dreaming Computers
2026-02-01 11:55:08 +07:00
parent 1ae7d716c3
commit c1a0c0dd9c
4 changed files with 258 additions and 0 deletions
+4
View File
@@ -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.
+131
View File
@@ -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)
+92
View File
@@ -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)",
}
+31
View File
@@ -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`