audiopod 1.2.0__py3-none-any.whl → 1.4.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.
@@ -1,522 +1,217 @@
1
1
  """
2
- Music Service - Music generation operations
3
- """
2
+ Music Service - Music generation
4
3
 
5
- from typing import List, Optional, Dict, Any, Union
6
- from pathlib import Path
4
+ API Routes:
5
+ - POST /api/v1/music/text2music - Generate music with vocals
6
+ - POST /api/v1/music/prompt2instrumental - Generate instrumental music
7
+ - POST /api/v1/music/lyric2vocals - Generate vocals from lyrics
8
+ - POST /api/v1/music/text2rap - Generate rap music
9
+ - GET /api/v1/music/jobs/{id}/status - Get job status
10
+ - GET /api/v1/music/jobs - List jobs
11
+ - DELETE /api/v1/music/jobs/{id} - Delete job
12
+ - GET /api/v1/music/presets - Get genre presets
13
+ """
7
14
 
15
+ from typing import Optional, Dict, Any, List, Literal
8
16
  from .base import BaseService
9
- from ..models import Job, MusicGenerationResult
10
- from ..exceptions import ValidationError
17
+
18
+
19
+ MusicTask = Literal["text2music", "prompt2instrumental", "lyric2vocals", "text2rap", "text2samples"]
11
20
 
12
21
 
13
22
  class MusicService(BaseService):
14
- """Service for music generation operations"""
15
-
16
- def generate_music(
23
+ """Service for AI music generation."""
24
+
25
+ def generate(
17
26
  self,
18
27
  prompt: str,
19
- duration: float = 120.0,
20
- guidance_scale: float = 7.5,
21
- num_inference_steps: int = 50,
22
- seed: Optional[int] = None,
28
+ task: MusicTask = "prompt2instrumental",
29
+ duration: int = 30,
30
+ lyrics: Optional[str] = None,
31
+ genre_preset: Optional[str] = None,
23
32
  display_name: Optional[str] = None,
24
33
  wait_for_completion: bool = False,
25
- timeout: int = 600
26
- ) -> Union[Job, MusicGenerationResult]:
34
+ timeout: int = 600,
35
+ ) -> Dict[str, Any]:
27
36
  """
28
- Generate music from text prompt
29
-
37
+ Generate music from text prompt.
38
+
30
39
  Args:
31
- prompt: Text description of the music to generate
32
- duration: Duration in seconds (10-600)
33
- guidance_scale: How closely to follow the prompt (1.0-20.0)
34
- num_inference_steps: Number of denoising steps (20-100)
35
- seed: Random seed for reproducible results
36
- display_name: Custom name for the generated track
37
- wait_for_completion: Whether to wait for generation completion
38
- timeout: Maximum time to wait if wait_for_completion=True
39
-
40
+ prompt: Text description of desired music
41
+ task: Generation task type:
42
+ - "prompt2instrumental": Instrumental music (no vocals)
43
+ - "text2music": Music with vocals (requires lyrics)
44
+ - "text2rap": Rap music (requires lyrics)
45
+ - "lyric2vocals": Generate vocals from lyrics
46
+ duration: Duration in seconds (default 30, max varies by task)
47
+ lyrics: Lyrics for vocal tasks
48
+ genre_preset: Genre preset name
49
+ display_name: Custom name for the job
50
+ wait_for_completion: Wait for completion
51
+ timeout: Max wait time in seconds
52
+
40
53
  Returns:
41
- Job object if wait_for_completion=False, otherwise MusicGenerationResult
54
+ Job dict with audio URL when completed
42
55
  """
43
- # Validate inputs
44
- prompt = self._validate_text_input(prompt, max_length=1000)
45
- if not 10.0 <= duration <= 600.0:
46
- raise ValidationError("Duration must be between 10 and 600 seconds")
47
- if not 1.0 <= guidance_scale <= 20.0:
48
- raise ValidationError("Guidance scale must be between 1.0 and 20.0")
49
- if not 20 <= num_inference_steps <= 100:
50
- raise ValidationError("Inference steps must be between 20 and 100")
51
- if seed is not None and (seed < 0 or seed > 2**32 - 1):
52
- raise ValidationError("Seed must be between 0 and 2^32 - 1")
53
-
54
- # Prepare request data - FIXED: Use correct parameter names matching API schema
55
56
  data = {
56
57
  "prompt": prompt,
57
- "audio_duration": duration, # FIXED: API expects "audio_duration" not "duration"
58
- "guidance_scale": guidance_scale,
59
- "infer_step": num_inference_steps # FIXED: API expects "infer_step" not "num_inference_steps"
58
+ "audio_duration": duration,
60
59
  }
61
- if seed is not None:
62
- data["manual_seeds"] = [seed] # FIXED: API expects "manual_seeds" list not "seed"
63
- if display_name:
64
- data["display_name"] = display_name.strip()
65
-
66
- # Make request
67
- if self.async_mode:
68
- return self._async_generate_music(data, wait_for_completion, timeout)
69
- else:
70
- response = self.client.request("POST", "/api/v1/music/text2music", data=data)
71
- # FIXED: Handle response format correctly - API returns {"job": {...}, "message": "..."}
72
- job_data = response.get("job", response)
73
- job = Job.from_dict(job_data)
74
-
75
- if wait_for_completion:
76
- completed_job = self._wait_for_completion(job.id, timeout)
77
- return MusicGenerationResult.from_dict(completed_job.result or completed_job.__dict__)
78
-
79
- return job
80
-
81
- async def _async_generate_music(
82
- self,
83
- data: Dict[str, Any],
84
- wait_for_completion: bool,
85
- timeout: int
86
- ) -> Union[Job, MusicGenerationResult]:
87
- """Async version of generate_music"""
88
- response = await self.client.request("POST", "/api/v1/music/text2music", data=data)
89
- # FIXED: Handle response format correctly
90
- job_data = response.get("job", response)
91
- job = Job.from_dict(job_data)
92
-
93
- if wait_for_completion:
94
- completed_job = await self._async_wait_for_completion(job.id, timeout)
95
- return MusicGenerationResult.from_dict(completed_job.result or completed_job.__dict__)
96
-
97
- return job
98
-
99
- def generate_rap(
100
- self,
101
- lyrics: str,
102
- style: str = "modern",
103
- tempo: int = 120,
104
- display_name: Optional[str] = None,
105
- wait_for_completion: bool = False,
106
- timeout: int = 600
107
- ) -> Union[Job, MusicGenerationResult]:
108
- """
109
- Generate rap music with lyrics
110
60
 
111
- Args:
112
- lyrics: Rap lyrics
113
- style: Style of rap ('modern', 'classic', 'trap')
114
- tempo: Beats per minute (80-200)
115
- display_name: Custom name for the track
116
- wait_for_completion: Whether to wait for completion
117
- timeout: Maximum time to wait
118
-
119
- Returns:
120
- Job object or generation result
121
- """
122
- # Validate inputs
123
- lyrics = self._validate_text_input(lyrics, max_length=2000)
124
- if not 80 <= tempo <= 200:
125
- raise ValidationError("Tempo must be between 80 and 200 BPM")
126
- if style not in ["modern", "classic", "trap"]:
127
- raise ValidationError("Style must be 'modern', 'classic', or 'trap'")
128
-
129
- # Prepare request data - FIXED: Match API schema for text2rap
130
- data = {
131
- "prompt": f"rap music, {style} style", # FIXED: API expects "prompt" field
132
- "lyrics": lyrics,
133
- "audio_duration": 120.0, # Default duration
134
- "guidance_scale": 7.5,
135
- "infer_step": 50,
136
- "lora_name_or_path": "ACE-Step/ACE-Step-v1-chinese-rap-LoRA" # Rap-specific LoRA
137
- }
61
+ if lyrics:
62
+ data["lyrics"] = lyrics
63
+ if genre_preset:
64
+ data["genre_preset"] = genre_preset
138
65
  if display_name:
139
- data["display_name"] = display_name.strip()
140
-
141
- # Make request
66
+ data["display_name"] = display_name
67
+
68
+ endpoint = f"/api/v1/music/{task}"
69
+
142
70
  if self.async_mode:
143
- return self._async_generate_rap(data, wait_for_completion, timeout)
144
- else:
145
- response = self.client.request("POST", "/api/v1/music/text2rap", data=data)
146
- # FIXED: Handle response format correctly
147
- job_data = response.get("job", response)
148
- job = Job.from_dict(job_data)
149
-
150
- if wait_for_completion:
151
- completed_job = self._wait_for_completion(job.id, timeout)
152
- return MusicGenerationResult.from_dict(completed_job.result or completed_job.__dict__)
153
-
154
- return job
155
-
156
- async def _async_generate_rap(
157
- self,
158
- data: Dict[str, Any],
159
- wait_for_completion: bool,
160
- timeout: int
161
- ) -> Union[Job, MusicGenerationResult]:
162
- """Async version of generate_rap"""
163
- response = await self.client.request("POST", "/api/v1/music/text2rap", data=data)
164
- # FIXED: Handle response format correctly
165
- job_data = response.get("job", response)
166
- job = Job.from_dict(job_data)
167
-
71
+ return self._async_generate(endpoint, data, wait_for_completion, timeout)
72
+
73
+ response = self.client.request("POST", endpoint, json_data=data)
74
+
168
75
  if wait_for_completion:
169
- completed_job = await self._async_wait_for_completion(job.id, timeout)
170
- return MusicGenerationResult.from_dict(completed_job.result or completed_job.__dict__)
171
-
172
- return job
173
-
174
- def generate_instrumental(
76
+ job_id = response.get("id") or response.get("job_id")
77
+ return self._wait_for_music(job_id, timeout)
78
+ return response
79
+
80
+ async def _async_generate(
81
+ self, endpoint: str, data: Dict, wait_for_completion: bool, timeout: int
82
+ ) -> Dict[str, Any]:
83
+ response = await self.client.request("POST", endpoint, json_data=data)
84
+ if wait_for_completion:
85
+ job_id = response.get("id") or response.get("job_id")
86
+ return await self._async_wait_for_music(job_id, timeout)
87
+ return response
88
+
89
+ def instrumental(
175
90
  self,
176
91
  prompt: str,
177
- duration: float = 120.0,
178
- instruments: Optional[List[str]] = None,
179
- key: Optional[str] = None,
180
- tempo: Optional[int] = None,
181
- display_name: Optional[str] = None,
92
+ duration: int = 30,
182
93
  wait_for_completion: bool = False,
183
- timeout: int = 600
184
- ) -> Union[Job, MusicGenerationResult]:
185
- """
186
- Generate instrumental music
187
-
188
- Args:
189
- prompt: Description of the instrumental
190
- duration: Duration in seconds
191
- instruments: List of instruments to include
192
- key: Musical key (e.g., 'C', 'Am', 'F#')
193
- tempo: Beats per minute
194
- display_name: Custom name for the track
195
- wait_for_completion: Whether to wait for completion
196
- timeout: Maximum time to wait
197
-
198
- Returns:
199
- Job object or generation result
200
- """
201
- # Validate inputs
202
- prompt = self._validate_text_input(prompt, max_length=1000)
203
- if not 10.0 <= duration <= 600.0:
204
- raise ValidationError("Duration must be between 10 and 600 seconds")
205
- if tempo is not None and not 60 <= tempo <= 200:
206
- raise ValidationError("Tempo must be between 60 and 200 BPM")
207
-
208
- # Prepare request data - FIXED: Match API schema for prompt2instrumental
209
- data = {
210
- "prompt": prompt,
211
- "audio_duration": duration, # FIXED: API expects "audio_duration"
212
- "guidance_scale": 7.5,
213
- "infer_step": 50
214
- }
215
- if instruments:
216
- data["instruments"] = instruments
217
- if key:
218
- data["key"] = key
219
- if tempo:
220
- data["tempo"] = tempo
221
- if display_name:
222
- data["display_name"] = display_name.strip()
223
-
224
- # Make request
225
- if self.async_mode:
226
- return self._async_generate_instrumental(data, wait_for_completion, timeout)
227
- else:
228
- response = self.client.request("POST", "/api/v1/music/prompt2instrumental", data=data)
229
- # FIXED: Handle response format correctly
230
- job_data = response.get("job", response)
231
- job = Job.from_dict(job_data)
232
-
233
- if wait_for_completion:
234
- completed_job = self._wait_for_completion(job.id, timeout)
235
- return MusicGenerationResult.from_dict(completed_job.result or completed_job.__dict__)
236
-
237
- return job
238
-
239
- async def _async_generate_instrumental(
240
- self,
241
- data: Dict[str, Any],
242
- wait_for_completion: bool,
243
- timeout: int
244
- ) -> Union[Job, MusicGenerationResult]:
245
- """Async version of generate_instrumental"""
246
- response = await self.client.request("POST", "/api/v1/music/prompt2instrumental", data=data)
247
- # FIXED: Handle response format correctly
248
- job_data = response.get("job", response)
249
- job = Job.from_dict(job_data)
250
-
251
- if wait_for_completion:
252
- completed_job = await self._async_wait_for_completion(job.id, timeout)
253
- return MusicGenerationResult.from_dict(completed_job.result or completed_job.__dict__)
254
-
255
- return job
256
-
257
- def generate_vocals(
94
+ timeout: int = 600,
95
+ ) -> Dict[str, Any]:
96
+ """Generate instrumental music (no vocals)."""
97
+ return self.generate(
98
+ prompt=prompt,
99
+ task="prompt2instrumental",
100
+ duration=duration,
101
+ wait_for_completion=wait_for_completion,
102
+ timeout=timeout,
103
+ )
104
+
105
+ def song(
258
106
  self,
107
+ prompt: str,
259
108
  lyrics: str,
260
- prompt: str = "vocals",
261
- duration: float = 120.0,
262
- display_name: Optional[str] = None,
109
+ duration: int = 60,
263
110
  wait_for_completion: bool = False,
264
- timeout: int = 600
265
- ) -> Union[Job, MusicGenerationResult]:
266
- """
267
- Generate vocals from lyrics - NEW METHOD matching API lyric2vocals endpoint
268
-
269
- Args:
270
- lyrics: Song lyrics
271
- prompt: Vocal style description
272
- duration: Duration in seconds
273
- display_name: Custom name for the track
274
- wait_for_completion: Whether to wait for completion
275
- timeout: Maximum time to wait
276
-
277
- Returns:
278
- Job object or generation result
279
- """
280
- # Validate inputs
281
- lyrics = self._validate_text_input(lyrics, max_length=10000)
282
- prompt = self._validate_text_input(prompt, max_length=2000)
283
- if not 10.0 <= duration <= 600.0:
284
- raise ValidationError("Duration must be between 10 and 600 seconds")
285
-
286
- # Prepare request data - Match API schema for lyric2vocals
287
- data = {
288
- "prompt": prompt,
289
- "lyrics": lyrics,
290
- "audio_duration": duration,
291
- "guidance_scale": 7.5,
292
- "infer_step": 50
293
- }
294
- if display_name:
295
- data["display_name"] = display_name.strip()
296
-
297
- # Make request
298
- if self.async_mode:
299
- return self._async_generate_vocals(data, wait_for_completion, timeout)
300
- else:
301
- response = self.client.request("POST", "/api/v1/music/lyric2vocals", data=data)
302
- job_data = response.get("job", response)
303
- job = Job.from_dict(job_data)
304
-
305
- if wait_for_completion:
306
- completed_job = self._wait_for_completion(job.id, timeout)
307
- return MusicGenerationResult.from_dict(completed_job.result or completed_job.__dict__)
308
-
309
- return job
310
-
311
- async def _async_generate_vocals(
312
- self,
313
- data: Dict[str, Any],
314
- wait_for_completion: bool,
315
- timeout: int
316
- ) -> Union[Job, MusicGenerationResult]:
317
- """Async version of generate_vocals"""
318
- response = await self.client.request("POST", "/api/v1/music/lyric2vocals", data=data)
319
- job_data = response.get("job", response)
320
- job = Job.from_dict(job_data)
321
-
322
- if wait_for_completion:
323
- completed_job = await self._async_wait_for_completion(job.id, timeout)
324
- return MusicGenerationResult.from_dict(completed_job.result or completed_job.__dict__)
325
-
326
- return job
327
-
328
- def extend_music(
111
+ timeout: int = 600,
112
+ ) -> Dict[str, Any]:
113
+ """Generate a song with vocals."""
114
+ return self.generate(
115
+ prompt=prompt,
116
+ task="text2music",
117
+ lyrics=lyrics,
118
+ duration=duration,
119
+ wait_for_completion=wait_for_completion,
120
+ timeout=timeout,
121
+ )
122
+
123
+ def rap(
329
124
  self,
330
- source_job_id: int,
331
- extend_duration: float = 30.0,
332
- display_name: Optional[str] = None,
125
+ prompt: str,
126
+ lyrics: str,
127
+ duration: int = 60,
333
128
  wait_for_completion: bool = False,
334
- timeout: int = 600
335
- ) -> Union[Job, MusicGenerationResult]:
336
- """
337
- Extend an existing music track
338
-
339
- Args:
340
- source_job_id: ID of the original music generation job
341
- extend_duration: How many seconds to extend
342
- display_name: Custom name for the extended track
343
- wait_for_completion: Whether to wait for completion
344
- timeout: Maximum time to wait
345
-
346
- Returns:
347
- Job object or generation result
348
- """
349
- # Validate inputs
350
- if not 5.0 <= extend_duration <= 120.0:
351
- raise ValidationError("Extend duration must be between 5 and 120 seconds")
352
-
353
- # Prepare request data
354
- data = {
355
- "source_job_id": source_job_id,
356
- "extend_duration": extend_duration
357
- }
358
- if display_name:
359
- data["display_name"] = display_name.strip()
360
-
361
- # Make request
129
+ timeout: int = 600,
130
+ ) -> Dict[str, Any]:
131
+ """Generate rap music."""
132
+ return self.generate(
133
+ prompt=prompt,
134
+ task="text2rap",
135
+ lyrics=lyrics,
136
+ duration=duration,
137
+ wait_for_completion=wait_for_completion,
138
+ timeout=timeout,
139
+ )
140
+
141
+ def get_job(self, job_id: int) -> Dict[str, Any]:
142
+ """Get music generation job status."""
362
143
  if self.async_mode:
363
- return self._async_extend_music(data, wait_for_completion, timeout)
364
- else:
365
- response = self.client.request("POST", "/api/v1/music/extend", data=data)
366
- job = Job.from_dict(response)
367
-
368
- if wait_for_completion:
369
- completed_job = self._wait_for_completion(job.id, timeout)
370
- return MusicGenerationResult.from_dict(completed_job.result or completed_job.__dict__)
371
-
372
- return job
373
-
374
- async def _async_extend_music(
375
- self,
376
- data: Dict[str, Any],
377
- wait_for_completion: bool,
378
- timeout: int
379
- ) -> Union[Job, MusicGenerationResult]:
380
- """Async version of extend_music"""
381
- response = await self.client.request("POST", "/api/v1/music/extend", data=data)
382
- job = Job.from_dict(response)
383
-
384
- if wait_for_completion:
385
- completed_job = await self._async_wait_for_completion(job.id, timeout)
386
- return MusicGenerationResult.from_dict(completed_job.result or completed_job.__dict__)
387
-
388
- return job
389
-
390
- def list_music_jobs(
391
- self,
392
- limit: int = 50,
393
- offset: int = 0,
394
- status: Optional[str] = None,
395
- task: Optional[str] = None,
396
- liked: Optional[bool] = None
397
- ) -> List[MusicGenerationResult]:
398
- """
399
- List music generation jobs
400
-
401
- Args:
402
- limit: Maximum number of results
403
- offset: Number of results to skip
404
- status: Filter by job status
405
- task: Filter by task type
406
- liked: Filter by liked status
407
-
408
- Returns:
409
- List of music generation results
410
- """
411
- params = {
412
- "limit": limit,
413
- "skip": offset # FIXED: API uses "skip" parameter for offset
414
- }
415
- if status:
416
- params["status"] = status
144
+ return self._async_get_job(job_id)
145
+ return self.client.request("GET", f"/api/v1/music/jobs/{job_id}/status")
146
+
147
+ async def _async_get_job(self, job_id: int) -> Dict[str, Any]:
148
+ return await self.client.request("GET", f"/api/v1/music/jobs/{job_id}/status")
149
+
150
+ def list_jobs(self, skip: int = 0, limit: int = 50, task: Optional[str] = None) -> List[Dict[str, Any]]:
151
+ """List music generation jobs."""
152
+ params = {"skip": skip, "limit": limit}
417
153
  if task:
418
154
  params["task"] = task
419
- if liked is not None:
420
- params["liked"] = liked
421
-
422
- if self.async_mode:
423
- return self._async_list_music_jobs(params)
424
- else:
425
- response = self.client.request("GET", "/api/v1/music/jobs", params=params)
426
- return [MusicGenerationResult.from_dict(job_data) for job_data in response]
427
-
428
- async def _async_list_music_jobs(self, params: Dict[str, Any]) -> List[MusicGenerationResult]:
429
- """Async version of list_music_jobs"""
430
- response = await self.client.request("GET", "/api/v1/music/jobs", params=params)
431
- return [MusicGenerationResult.from_dict(job_data) for job_data in response]
432
-
433
- def get_music_job(self, job_id: int) -> MusicGenerationResult:
434
- """
435
- Get details of a specific music generation job
436
-
437
- Args:
438
- job_id: ID of the music job
439
-
440
- Returns:
441
- Music generation result
442
- """
443
- if self.async_mode:
444
- return self._async_get_music_job(job_id)
445
- else:
446
- response = self.client.request("GET", f"/api/v1/music/jobs/{job_id}/status")
447
- return MusicGenerationResult.from_dict(response)
448
-
449
- async def _async_get_music_job(self, job_id: int) -> MusicGenerationResult:
450
- """Async version of get_music_job"""
451
- response = await self.client.request("GET", f"/api/v1/music/jobs/{job_id}/status")
452
- return MusicGenerationResult.from_dict(response)
453
-
454
- def like_music_track(self, job_id: int) -> Dict[str, Any]:
455
- """
456
- Like a music track
457
-
458
- Args:
459
- job_id: ID of the music job
460
155
 
461
- Returns:
462
- Like response
463
- """
464
156
  if self.async_mode:
465
- return self._async_like_music_track(job_id)
466
- else:
467
- return self.client.request("POST", f"/api/v1/music/jobs/{job_id}/like")
468
-
469
- async def _async_like_music_track(self, job_id: int) -> Dict[str, Any]:
470
- """Async version of like_music_track"""
471
- return await self.client.request("POST", f"/api/v1/music/jobs/{job_id}/like")
472
-
473
- def share_music_track(
474
- self,
475
- job_id: int,
476
- platform: Optional[str] = None,
477
- message: Optional[str] = None
478
- ) -> Dict[str, Any]:
479
- """
480
- Share a music track
481
-
482
- Args:
483
- job_id: ID of the music job
484
- platform: Platform to share on
485
- message: Optional message to include
486
-
487
- Returns:
488
- Share response with shareable URL
489
- """
490
- data = {}
491
- if platform:
492
- data["platform"] = platform
493
- if message:
494
- data["message"] = message
495
-
496
- if self.async_mode:
497
- return self._async_share_music_track(job_id, data)
498
- else:
499
- return self.client.request("POST", f"/api/v1/music/jobs/{job_id}/share", data=data)
500
-
501
- async def _async_share_music_track(self, job_id: int, data: Dict[str, Any]) -> Dict[str, Any]:
502
- """Async version of share_music_track"""
503
- return await self.client.request("POST", f"/api/v1/music/jobs/{job_id}/share", data=data)
504
-
505
- def delete_music_job(self, job_id: int) -> Dict[str, str]:
506
- """
507
- Delete a music generation job
508
-
509
- Args:
510
- job_id: ID of the music job
511
-
512
- Returns:
513
- Deletion confirmation
514
- """
157
+ return self._async_list_jobs(params)
158
+ return self.client.request("GET", "/api/v1/music/jobs", params=params)
159
+
160
+ async def _async_list_jobs(self, params: Dict) -> List[Dict[str, Any]]:
161
+ return await self.client.request("GET", "/api/v1/music/jobs", params=params)
162
+
163
+ def delete_job(self, job_id: int) -> Dict[str, str]:
164
+ """Delete a music generation job."""
515
165
  if self.async_mode:
516
- return self._async_delete_music_job(job_id)
517
- else:
518
- return self.client.request("DELETE", f"/api/v1/music/jobs/{job_id}")
519
-
520
- async def _async_delete_music_job(self, job_id: int) -> Dict[str, str]:
521
- """Async version of delete_music_job"""
166
+ return self._async_delete_job(job_id)
167
+ return self.client.request("DELETE", f"/api/v1/music/jobs/{job_id}")
168
+
169
+ async def _async_delete_job(self, job_id: int) -> Dict[str, str]:
522
170
  return await self.client.request("DELETE", f"/api/v1/music/jobs/{job_id}")
171
+
172
+ def get_presets(self) -> Dict[str, Any]:
173
+ """Get available genre presets."""
174
+ if self.async_mode:
175
+ return self._async_get_presets()
176
+ return self.client.request("GET", "/api/v1/music/presets")
177
+
178
+ async def _async_get_presets(self) -> Dict[str, Any]:
179
+ return await self.client.request("GET", "/api/v1/music/presets")
180
+
181
+ def _wait_for_music(self, job_id: int, timeout: int) -> Dict[str, Any]:
182
+ """Wait for music generation job completion."""
183
+ import time
184
+ start_time = time.time()
185
+
186
+ while time.time() - start_time < timeout:
187
+ job = self.get_job(job_id)
188
+ status = job.get("status", "").upper()
189
+
190
+ if status == "COMPLETED":
191
+ return job
192
+ elif status in ("FAILED", "ERROR"):
193
+ raise Exception(f"Music generation failed: {job.get('error_message', 'Unknown error')}")
194
+
195
+ time.sleep(5)
196
+
197
+ raise TimeoutError(f"Music generation {job_id} did not complete within {timeout} seconds")
198
+
199
+ async def _async_wait_for_music(self, job_id: int, timeout: int) -> Dict[str, Any]:
200
+ """Async wait for music generation job completion."""
201
+ import asyncio
202
+ import time
203
+ start_time = time.time()
204
+
205
+ while time.time() - start_time < timeout:
206
+ job = await self.get_job(job_id)
207
+ status = job.get("status", "").upper()
208
+
209
+ if status == "COMPLETED":
210
+ return job
211
+ elif status in ("FAILED", "ERROR"):
212
+ raise Exception(f"Music generation failed: {job.get('error_message', 'Unknown error')}")
213
+
214
+ await asyncio.sleep(5)
215
+
216
+ raise TimeoutError(f"Music generation {job_id} did not complete within {timeout} seconds")
217
+