feat: add suno music generate node
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
@@ -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)",
|
||||
}
|
||||
|
||||
@@ -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`
|
||||
Reference in New Issue
Block a user