runapi-suno 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- runapi/suno/__init__.py +24 -0
- runapi/suno/_validators.py +208 -0
- runapi/suno/client.py +70 -0
- runapi/suno/contract_gen.py +723 -0
- runapi/suno/py.typed +0 -0
- runapi/suno/resources/__init__.py +45 -0
- runapi/suno/resources/add_instrumental.py +58 -0
- runapi/suno/resources/add_vocals.py +58 -0
- runapi/suno/resources/boost_style.py +34 -0
- runapi/suno/resources/check_voice.py +34 -0
- runapi/suno/resources/convert_audio.py +58 -0
- runapi/suno/resources/cover_audio.py +58 -0
- runapi/suno/resources/create_mashup.py +61 -0
- runapi/suno/resources/extend_music.py +58 -0
- runapi/suno/resources/generate_artwork.py +58 -0
- runapi/suno/resources/generate_lyrics.py +58 -0
- runapi/suno/resources/generate_midi.py +58 -0
- runapi/suno/resources/generate_persona.py +34 -0
- runapi/suno/resources/generate_voice.py +58 -0
- runapi/suno/resources/get_timestamped_lyrics.py +34 -0
- runapi/suno/resources/regenerate_validation_phrase.py +58 -0
- runapi/suno/resources/replace_section.py +58 -0
- runapi/suno/resources/separate_audio_stems.py +58 -0
- runapi/suno/resources/text_to_music.py +58 -0
- runapi/suno/resources/text_to_sound.py +58 -0
- runapi/suno/resources/visualize_music.py +58 -0
- runapi/suno/resources/voice_to_validation_phrase.py +58 -0
- runapi/suno/types.py +366 -0
- runapi_suno-0.1.0.dist-info/METADATA +103 -0
- runapi_suno-0.1.0.dist-info/RECORD +31 -0
- runapi_suno-0.1.0.dist-info/WHEEL +4 -0
runapi/suno/__init__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Suno client for RunAPI."""
|
|
2
|
+
|
|
3
|
+
from runapi.core import (
|
|
4
|
+
AuthenticationError,
|
|
5
|
+
InsufficientCreditsError,
|
|
6
|
+
NotFoundError,
|
|
7
|
+
RateLimitError,
|
|
8
|
+
TaskFailedError,
|
|
9
|
+
TaskTimeoutError,
|
|
10
|
+
ValidationError,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from .client import SunoClient
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"SunoClient",
|
|
17
|
+
"AuthenticationError",
|
|
18
|
+
"RateLimitError",
|
|
19
|
+
"InsufficientCreditsError",
|
|
20
|
+
"NotFoundError",
|
|
21
|
+
"ValidationError",
|
|
22
|
+
"TaskFailedError",
|
|
23
|
+
"TaskTimeoutError",
|
|
24
|
+
]
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""Shared Suno request validators.
|
|
2
|
+
|
|
3
|
+
Ported from the Ruby ``RunApi::Suno::Validators`` module. Each ``validate_*``
|
|
4
|
+
function mirrors its Ruby counterpart, including the exact ``ValidationError``
|
|
5
|
+
message strings. Resources call these from their ``_validate_params`` hook.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any, Dict, Sequence
|
|
11
|
+
|
|
12
|
+
from runapi.core import ValidationError
|
|
13
|
+
|
|
14
|
+
from . import types
|
|
15
|
+
|
|
16
|
+
_TRUTHY_VALUES = [True, 1, "1", "true", "TRUE", "True"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _param(params: Dict[str, Any], key: str) -> Any:
|
|
20
|
+
return params.get(key)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _truthy(value: Any) -> bool:
|
|
24
|
+
return value in _TRUTHY_VALUES
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _truthy_presence(value: Any) -> bool:
|
|
28
|
+
if hasattr(value, "__len__"):
|
|
29
|
+
return len(value) > 0
|
|
30
|
+
return value is not None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _to_i(value: Any) -> int:
|
|
34
|
+
try:
|
|
35
|
+
return int(value)
|
|
36
|
+
except (TypeError, ValueError):
|
|
37
|
+
if isinstance(value, str):
|
|
38
|
+
digits = ""
|
|
39
|
+
for ch in value.strip():
|
|
40
|
+
if ch in "+-" and not digits:
|
|
41
|
+
digits += ch
|
|
42
|
+
elif ch.isdigit():
|
|
43
|
+
digits += ch
|
|
44
|
+
else:
|
|
45
|
+
break
|
|
46
|
+
try:
|
|
47
|
+
return int(digits)
|
|
48
|
+
except ValueError:
|
|
49
|
+
return 0
|
|
50
|
+
return 0
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _to_f(value: Any) -> float:
|
|
54
|
+
try:
|
|
55
|
+
return float(value)
|
|
56
|
+
except (TypeError, ValueError):
|
|
57
|
+
return 0.0
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def require_param(params: Dict[str, Any], key: str) -> None:
|
|
61
|
+
if _param(params, key) is None:
|
|
62
|
+
raise ValidationError(f"{key} is required")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def require_all(params: Dict[str, Any], *keys: str) -> None:
|
|
66
|
+
for key in keys:
|
|
67
|
+
require_param(params, key)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def validate_optional(params: Dict[str, Any], key: str, allowed: Sequence[Any]) -> None:
|
|
71
|
+
value = params.get(key)
|
|
72
|
+
if value is None:
|
|
73
|
+
return
|
|
74
|
+
if value not in allowed:
|
|
75
|
+
joined = ", ".join(str(option) for option in allowed)
|
|
76
|
+
raise ValidationError(f"Invalid {key}: {value}. Must be one of: {joined}")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def validate_extend_music_prompt_shape(params: Dict[str, Any]) -> None:
|
|
80
|
+
if not _truthy_presence(_param(params, "lyrics")):
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
if _truthy_presence(_param(params, "prompt")):
|
|
84
|
+
raise ValidationError("prompt cannot be combined with lyrics")
|
|
85
|
+
|
|
86
|
+
if _truthy(_param(params, "instrumental")):
|
|
87
|
+
raise ValidationError("lyrics cannot be used when instrumental is true")
|
|
88
|
+
|
|
89
|
+
upload_mode = any(_truthy_presence(_param(params, key)) for key in ("audio_url", "upload_url"))
|
|
90
|
+
if _param(params, "parameter_mode") == "custom" and upload_mode:
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
raise ValidationError("lyrics can only be used when extending uploaded audio with custom parameters")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def validate_extend_music(params: Dict[str, Any]) -> None:
|
|
97
|
+
if not any(_param(params, key) for key in ("task_id", "audio_id", "audio_url", "upload_url")):
|
|
98
|
+
raise ValidationError("task_id, audio_id, audio_url, or upload_url is required")
|
|
99
|
+
require_param(params, "parameter_mode")
|
|
100
|
+
require_param(params, "model")
|
|
101
|
+
|
|
102
|
+
validate_optional(params, "parameter_mode", types.PARAMETER_MODES)
|
|
103
|
+
if _param(params, "parameter_mode") == "custom":
|
|
104
|
+
require_param(params, "style")
|
|
105
|
+
require_param(params, "title")
|
|
106
|
+
require_param(params, "continue_at")
|
|
107
|
+
validate_extend_music_prompt_shape(params)
|
|
108
|
+
validate_optional(params, "model", types.MODELS)
|
|
109
|
+
validate_optional(params, "vocal_gender", types.VOCAL_GENDERS)
|
|
110
|
+
validate_optional(params, "persona_type", types.PERSONA_TYPES)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def validate_generate_artwork(params: Dict[str, Any]) -> None:
|
|
114
|
+
require_param(params, "task_id")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def validate_add_instrumental(params: Dict[str, Any]) -> None:
|
|
118
|
+
require_all(params, "upload_url", "title", "negative_tags", "tags", "model")
|
|
119
|
+
validate_optional(params, "model", types.MODELS)
|
|
120
|
+
validate_optional(params, "vocal_gender", types.VOCAL_GENDERS)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def validate_add_vocals(params: Dict[str, Any]) -> None:
|
|
124
|
+
require_all(params, "upload_url", "lyrics", "title", "negative_tags", "style", "model")
|
|
125
|
+
validate_optional(params, "model", types.MODELS)
|
|
126
|
+
validate_optional(params, "vocal_gender", types.VOCAL_GENDERS)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def validate_separate_audio_stems(params: Dict[str, Any]) -> None:
|
|
130
|
+
require_all(params, "task_id", "audio_id")
|
|
131
|
+
validate_optional(params, "type", types.SEPARATE_AUDIO_STEMS_TYPES)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def validate_generate_midi(params: Dict[str, Any]) -> None:
|
|
135
|
+
require_param(params, "task_id")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def validate_convert_audio(params: Dict[str, Any]) -> None:
|
|
139
|
+
require_all(params, "task_id", "audio_id")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def validate_visualize_music(params: Dict[str, Any]) -> None:
|
|
143
|
+
require_all(params, "task_id", "audio_id")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def validate_generate_lyrics(params: Dict[str, Any]) -> None:
|
|
147
|
+
require_param(params, "prompt")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def validate_get_timestamped_lyrics(params: Dict[str, Any]) -> None:
|
|
151
|
+
require_all(params, "task_id", "audio_id")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def validate_replace_section(params: Dict[str, Any]) -> None:
|
|
155
|
+
require_all(
|
|
156
|
+
params,
|
|
157
|
+
"task_id",
|
|
158
|
+
"audio_id",
|
|
159
|
+
"lyrics",
|
|
160
|
+
"tags",
|
|
161
|
+
"title",
|
|
162
|
+
"infill_start_time",
|
|
163
|
+
"infill_end_time",
|
|
164
|
+
)
|
|
165
|
+
if _to_f(_param(params, "infill_end_time")) <= _to_f(_param(params, "infill_start_time")):
|
|
166
|
+
raise ValidationError("infill_end_time must be greater than infill_start_time")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def validate_text_to_sound(params: Dict[str, Any]) -> None:
|
|
170
|
+
require_all(params, "prompt", "model")
|
|
171
|
+
validate_optional(params, "model", types.SOUND_MODELS)
|
|
172
|
+
validate_optional(params, "sound_key", types.SOUND_KEYS)
|
|
173
|
+
tempo = _param(params, "sound_tempo")
|
|
174
|
+
if tempo is not None and not (1 <= _to_i(tempo) <= 300):
|
|
175
|
+
raise ValidationError("sound_tempo must be between 1 and 300")
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def validate_voice_to_validation_phrase(params: Dict[str, Any]) -> None:
|
|
179
|
+
require_all(params, "voice_url", "vocal_start_seconds", "vocal_end_seconds")
|
|
180
|
+
validate_optional(params, "language", types.VALIDATION_PHRASE_LANGUAGES)
|
|
181
|
+
|
|
182
|
+
start_seconds = _to_i(_param(params, "vocal_start_seconds"))
|
|
183
|
+
end_seconds = _to_i(_param(params, "vocal_end_seconds"))
|
|
184
|
+
if end_seconds > start_seconds:
|
|
185
|
+
return
|
|
186
|
+
|
|
187
|
+
raise ValidationError("vocal_end_seconds must be greater than vocal_start_seconds")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def validate_regenerate_validation_phrase(params: Dict[str, Any]) -> None:
|
|
191
|
+
require_param(params, "task_id")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def validate_generate_voice(params: Dict[str, Any]) -> None:
|
|
195
|
+
require_all(params, "task_id", "verify_url")
|
|
196
|
+
validate_optional(params, "singer_skill_level", types.SINGER_SKILL_LEVELS)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def validate_check_voice(params: Dict[str, Any]) -> None:
|
|
200
|
+
require_param(params, "task_id")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def validate_generate_persona(params: Dict[str, Any]) -> None:
|
|
204
|
+
require_all(params, "task_id", "audio_id", "name", "description")
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def validate_boost_style(params: Dict[str, Any]) -> None:
|
|
208
|
+
require_param(params, "description")
|
runapi/suno/client.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Suno client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
from runapi.core import ClientOptions, HttpClient, resolve_api_key
|
|
8
|
+
|
|
9
|
+
from .resources.add_instrumental import AddInstrumental
|
|
10
|
+
from .resources.add_vocals import AddVocals
|
|
11
|
+
from .resources.boost_style import BoostStyle
|
|
12
|
+
from .resources.check_voice import CheckVoice
|
|
13
|
+
from .resources.convert_audio import ConvertAudio
|
|
14
|
+
from .resources.cover_audio import CoverAudio
|
|
15
|
+
from .resources.create_mashup import CreateMashup
|
|
16
|
+
from .resources.extend_music import ExtendMusic
|
|
17
|
+
from .resources.generate_artwork import GenerateArtwork
|
|
18
|
+
from .resources.generate_lyrics import GenerateLyrics
|
|
19
|
+
from .resources.generate_midi import GenerateMidi
|
|
20
|
+
from .resources.generate_persona import GeneratePersona
|
|
21
|
+
from .resources.generate_voice import GenerateVoice
|
|
22
|
+
from .resources.get_timestamped_lyrics import GetTimestampedLyrics
|
|
23
|
+
from .resources.regenerate_validation_phrase import RegenerateValidationPhrase
|
|
24
|
+
from .resources.replace_section import ReplaceSection
|
|
25
|
+
from .resources.separate_audio_stems import SeparateAudioStems
|
|
26
|
+
from .resources.text_to_music import TextToMusic
|
|
27
|
+
from .resources.text_to_sound import TextToSound
|
|
28
|
+
from .resources.visualize_music import VisualizeMusic
|
|
29
|
+
from .resources.voice_to_validation_phrase import VoiceToValidationPhrase
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SunoClient:
|
|
33
|
+
"""Suno music, sound, lyrics, and voice client.
|
|
34
|
+
|
|
35
|
+
Example::
|
|
36
|
+
|
|
37
|
+
client = SunoClient(api_key="sk-...")
|
|
38
|
+
result = client.text_to_music.run(
|
|
39
|
+
prompt="A chill lo-fi beat with soft vocals",
|
|
40
|
+
model="suno-v4.5-plus",
|
|
41
|
+
vocal_mode="auto_lyrics",
|
|
42
|
+
)
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, api_key: Optional[str] = None, **options: Any) -> None:
|
|
46
|
+
resolved_api_key = resolve_api_key(api_key)
|
|
47
|
+
client_options = ClientOptions(api_key=resolved_api_key, **options)
|
|
48
|
+
http = client_options.http_client or HttpClient(client_options)
|
|
49
|
+
|
|
50
|
+
self.text_to_music = TextToMusic(http)
|
|
51
|
+
self.extend_music = ExtendMusic(http)
|
|
52
|
+
self.generate_artwork = GenerateArtwork(http)
|
|
53
|
+
self.cover_audio = CoverAudio(http)
|
|
54
|
+
self.add_instrumental = AddInstrumental(http)
|
|
55
|
+
self.add_vocals = AddVocals(http)
|
|
56
|
+
self.separate_audio_stems = SeparateAudioStems(http)
|
|
57
|
+
self.generate_midi = GenerateMidi(http)
|
|
58
|
+
self.convert_audio = ConvertAudio(http)
|
|
59
|
+
self.visualize_music = VisualizeMusic(http)
|
|
60
|
+
self.generate_lyrics = GenerateLyrics(http)
|
|
61
|
+
self.get_timestamped_lyrics = GetTimestampedLyrics(http)
|
|
62
|
+
self.replace_section = ReplaceSection(http)
|
|
63
|
+
self.create_mashup = CreateMashup(http)
|
|
64
|
+
self.text_to_sound = TextToSound(http)
|
|
65
|
+
self.voice_to_validation_phrase = VoiceToValidationPhrase(http)
|
|
66
|
+
self.regenerate_validation_phrase = RegenerateValidationPhrase(http)
|
|
67
|
+
self.generate_voice = GenerateVoice(http)
|
|
68
|
+
self.check_voice = CheckVoice(http)
|
|
69
|
+
self.generate_persona = GeneratePersona(http)
|
|
70
|
+
self.boost_style = BoostStyle(http)
|