audiopod 1.4.0__py3-none-any.whl → 2.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.
@@ -0,0 +1,166 @@
1
+ """
2
+ Music Service - AI Music Generation
3
+ """
4
+
5
+ import time
6
+ from typing import Optional, List, Dict, Any, Literal, TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from ..client import AudioPod
10
+
11
+ POLL_INTERVAL = 5 # seconds
12
+ DEFAULT_TIMEOUT = 600 # 10 minutes
13
+
14
+ MusicTask = Literal[
15
+ "text2music",
16
+ "prompt2instrumental",
17
+ "lyric2vocals",
18
+ "text2rap",
19
+ "text2samples",
20
+ "audio2audio",
21
+ "songbloom",
22
+ ]
23
+
24
+
25
+ class Music:
26
+ """AI music generation service."""
27
+
28
+ def __init__(self, client: "AudioPod"):
29
+ self._client = client
30
+
31
+ def create(
32
+ self,
33
+ prompt: str,
34
+ *,
35
+ lyrics: Optional[str] = None,
36
+ duration: int = 30,
37
+ task: Optional[MusicTask] = None,
38
+ genre: Optional[str] = None,
39
+ display_name: Optional[str] = None,
40
+ ) -> Dict[str, Any]:
41
+ """
42
+ Generate music from text.
43
+
44
+ Args:
45
+ prompt: Text prompt describing the music
46
+ lyrics: Lyrics (for vocal tracks)
47
+ duration: Duration in seconds (default: 30)
48
+ task: Task type (auto-detected based on lyrics if not specified)
49
+ genre: Genre preset
50
+ display_name: Display name for the track
51
+
52
+ Returns:
53
+ Music job object
54
+
55
+ Example:
56
+ >>> job = client.music.create(
57
+ ... prompt="Upbeat electronic dance music",
58
+ ... duration=30,
59
+ ... )
60
+ """
61
+ if task is None:
62
+ task = "text2music" if lyrics else "prompt2instrumental"
63
+
64
+ endpoint = f"/api/v1/music/{task}"
65
+
66
+ return self._client.post(
67
+ endpoint,
68
+ json_data={
69
+ "prompt": prompt,
70
+ "lyrics": lyrics,
71
+ "audio_duration": duration,
72
+ "display_name": display_name,
73
+ "genre_preset": genre,
74
+ },
75
+ )
76
+
77
+ def instrumental(
78
+ self, prompt: str, duration: int = 30
79
+ ) -> Dict[str, Any]:
80
+ """Generate instrumental music."""
81
+ return self.create(prompt, duration=duration, task="prompt2instrumental")
82
+
83
+ def song(
84
+ self, prompt: str, lyrics: str, duration: int = 60
85
+ ) -> Dict[str, Any]:
86
+ """Generate a song with vocals."""
87
+ return self.create(prompt, lyrics=lyrics, duration=duration, task="text2music")
88
+
89
+ def rap(
90
+ self, prompt: str, lyrics: str, duration: int = 60
91
+ ) -> Dict[str, Any]:
92
+ """Generate rap music."""
93
+ return self.create(prompt, lyrics=lyrics, duration=duration, task="text2rap")
94
+
95
+ def get(self, job_id: int) -> Dict[str, Any]:
96
+ """Get a music job by ID."""
97
+ return self._client.get(f"/api/v1/music/jobs/{job_id}/status")
98
+
99
+ def list(
100
+ self,
101
+ *,
102
+ skip: int = 0,
103
+ limit: int = 50,
104
+ task: Optional[MusicTask] = None,
105
+ status: Optional[str] = None,
106
+ ) -> List[Dict[str, Any]]:
107
+ """List music jobs."""
108
+ return self._client.get(
109
+ "/api/v1/music/jobs",
110
+ params={"skip": skip, "limit": limit, "task": task, "status": status},
111
+ )
112
+
113
+ def delete(self, job_id: int) -> None:
114
+ """Delete a music job."""
115
+ self._client.delete(f"/api/v1/music/jobs/{job_id}")
116
+
117
+ def wait_for_completion(
118
+ self, job_id: int, timeout: int = DEFAULT_TIMEOUT
119
+ ) -> Dict[str, Any]:
120
+ """Wait for music generation to complete."""
121
+ start_time = time.time()
122
+
123
+ while time.time() - start_time < timeout:
124
+ job = self.get(job_id)
125
+ status = job.get("status", "").upper()
126
+
127
+ if status == "COMPLETED":
128
+ return job
129
+ if status == "FAILED":
130
+ raise RuntimeError(
131
+ f"Music generation failed: {job.get('error_message', 'Unknown error')}"
132
+ )
133
+
134
+ time.sleep(POLL_INTERVAL)
135
+
136
+ raise TimeoutError(f"Music generation timed out after {timeout}s")
137
+
138
+ def generate(
139
+ self,
140
+ prompt: str,
141
+ *,
142
+ lyrics: Optional[str] = None,
143
+ duration: int = 30,
144
+ task: Optional[MusicTask] = None,
145
+ timeout: int = DEFAULT_TIMEOUT,
146
+ ) -> Dict[str, Any]:
147
+ """
148
+ Generate music and wait for completion.
149
+
150
+ Example:
151
+ >>> result = client.music.generate(
152
+ ... prompt="Upbeat electronic dance music",
153
+ ... duration=30,
154
+ ... )
155
+ >>> print(result["output_url"])
156
+ """
157
+ job = self.create(prompt, lyrics=lyrics, duration=duration, task=task)
158
+ return self.wait_for_completion(job["id"], timeout=timeout)
159
+
160
+ def get_presets(self) -> Dict[str, Any]:
161
+ """Get available genre presets."""
162
+ return self._client.get("/api/v1/music/presets")
163
+
164
+
165
+
166
+
@@ -0,0 +1,132 @@
1
+ """
2
+ Speaker Service - Speaker Diarization & Separation
3
+ """
4
+
5
+ import time
6
+ from typing import Optional, List, Dict, Any, TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from ..client import AudioPod
10
+
11
+ POLL_INTERVAL = 3 # seconds
12
+ DEFAULT_TIMEOUT = 600 # 10 minutes
13
+
14
+
15
+ class Speaker:
16
+ """Speaker diarization and separation service."""
17
+
18
+ def __init__(self, client: "AudioPod"):
19
+ self._client = client
20
+
21
+ def diarize(
22
+ self,
23
+ *,
24
+ file: Optional[str] = None,
25
+ url: Optional[str] = None,
26
+ num_speakers: Optional[int] = None,
27
+ ) -> Dict[str, Any]:
28
+ """
29
+ Create a speaker diarization job.
30
+
31
+ Args:
32
+ file: Path to audio file
33
+ url: URL of audio/video
34
+ num_speakers: Number of speakers (hint for better accuracy)
35
+
36
+ Returns:
37
+ Speaker job object
38
+
39
+ Example:
40
+ >>> job = client.speaker.diarize(
41
+ ... file="./meeting.mp3",
42
+ ... num_speakers=3,
43
+ ... )
44
+ """
45
+ if file:
46
+ return self._client.upload(
47
+ "/api/v1/speaker/diarize",
48
+ file,
49
+ field_name="file",
50
+ additional_fields={"num_speakers": num_speakers},
51
+ )
52
+
53
+ if url:
54
+ return self._client.post(
55
+ "/api/v1/speaker/diarize",
56
+ json_data={"url": url, "num_speakers": num_speakers},
57
+ )
58
+
59
+ raise ValueError("Either file or url must be provided")
60
+
61
+ def get(self, job_id: int) -> Dict[str, Any]:
62
+ """Get a speaker job by ID."""
63
+ return self._client.get(f"/api/v1/speaker/jobs/{job_id}")
64
+
65
+ def list(
66
+ self,
67
+ *,
68
+ skip: int = 0,
69
+ limit: int = 50,
70
+ status: Optional[str] = None,
71
+ job_type: Optional[str] = None,
72
+ ) -> List[Dict[str, Any]]:
73
+ """List speaker jobs."""
74
+ return self._client.get(
75
+ "/api/v1/speaker/jobs",
76
+ params={
77
+ "skip": skip,
78
+ "limit": limit,
79
+ "status": status,
80
+ "job_type": job_type,
81
+ },
82
+ )
83
+
84
+ def delete(self, job_id: int) -> None:
85
+ """Delete a speaker job."""
86
+ self._client.delete(f"/api/v1/speaker/jobs/{job_id}")
87
+
88
+ def wait_for_completion(
89
+ self, job_id: int, timeout: int = DEFAULT_TIMEOUT
90
+ ) -> Dict[str, Any]:
91
+ """Wait for speaker job to complete."""
92
+ start_time = time.time()
93
+
94
+ while time.time() - start_time < timeout:
95
+ job = self.get(job_id)
96
+ status = job.get("status", "")
97
+
98
+ if status == "COMPLETED":
99
+ return job
100
+ if status == "FAILED":
101
+ raise RuntimeError(
102
+ f"Speaker processing failed: {job.get('error_message', 'Unknown error')}"
103
+ )
104
+
105
+ time.sleep(POLL_INTERVAL)
106
+
107
+ raise TimeoutError(f"Speaker processing timed out after {timeout}s")
108
+
109
+ def identify(
110
+ self,
111
+ *,
112
+ file: Optional[str] = None,
113
+ url: Optional[str] = None,
114
+ num_speakers: Optional[int] = None,
115
+ timeout: int = DEFAULT_TIMEOUT,
116
+ ) -> Dict[str, Any]:
117
+ """
118
+ Diarize audio and wait for completion.
119
+
120
+ Example:
121
+ >>> result = client.speaker.identify(
122
+ ... file="./meeting.mp3",
123
+ ... num_speakers=3,
124
+ ... )
125
+ >>> print(result["segments"])
126
+ """
127
+ job = self.diarize(file=file, url=url, num_speakers=num_speakers)
128
+ return self.wait_for_completion(job["id"], timeout=timeout)
129
+
130
+
131
+
132
+
@@ -0,0 +1,267 @@
1
+ """
2
+ Stem Extraction Service - Audio Stem Separation
3
+
4
+ Simple mode-based API for separating audio into individual stems.
5
+ """
6
+
7
+ import time
8
+ from typing import Optional, Dict, Any, Literal, TYPE_CHECKING
9
+
10
+ if TYPE_CHECKING:
11
+ from ..client import AudioPod
12
+
13
+ POLL_INTERVAL = 3 # seconds
14
+ DEFAULT_TIMEOUT = 600 # 10 minutes
15
+
16
+ # Separation modes
17
+ StemMode = Literal["single", "two", "four", "six", "producer", "studio", "mastering"]
18
+
19
+ # Single stem options
20
+ SingleStem = Literal["vocals", "drums", "bass", "guitar", "piano", "other"]
21
+
22
+
23
+ class StemExtraction:
24
+ """
25
+ Audio stem separation service.
26
+
27
+ Separate audio into individual components using simple mode selection.
28
+
29
+ Modes:
30
+ - single: Extract one stem (requires stem parameter)
31
+ - two: Vocals + Instrumental
32
+ - four: Vocals, Drums, Bass, Other
33
+ - six: Vocals, Drums, Bass, Guitar, Piano, Other
34
+ - producer: 8 stems with drum kit decomposition
35
+ - studio: 12 stems for professional mixing
36
+ - mastering: 16 stems maximum detail
37
+
38
+ Example:
39
+ >>> client = AudioPod(api_key="ap_...")
40
+ >>>
41
+ >>> # Six-stem separation from YouTube
42
+ >>> job = client.stems.extract(
43
+ ... url="https://youtube.com/watch?v=VIDEO_ID",
44
+ ... mode="six"
45
+ ... )
46
+ >>>
47
+ >>> # Wait for completion
48
+ >>> result = client.stems.wait_for_completion(job["id"])
49
+ >>> print(result["download_urls"])
50
+ """
51
+
52
+ def __init__(self, client: "AudioPod"):
53
+ self._client = client
54
+
55
+ def extract(
56
+ self,
57
+ *,
58
+ file: Optional[str] = None,
59
+ url: Optional[str] = None,
60
+ mode: StemMode = "four",
61
+ stem: Optional[SingleStem] = None,
62
+ ) -> Dict[str, Any]:
63
+ """
64
+ Extract stems from audio using simple mode selection.
65
+
66
+ Args:
67
+ file: Path to local audio file (MP3, WAV, FLAC, M4A, OGG)
68
+ url: URL of audio/video (YouTube, SoundCloud, direct link)
69
+ mode: Separation mode:
70
+ - "single": Extract one stem (specify stem param)
71
+ - "two": Vocals + Instrumental
72
+ - "four": Vocals, Drums, Bass, Other (default)
73
+ - "six": Vocals, Drums, Bass, Guitar, Piano, Other
74
+ - "producer": 8 stems with kick, snare, hihat
75
+ - "studio": 12 stems for professional mixing
76
+ - "mastering": 16 stems maximum detail
77
+ stem: For mode="single", which stem to extract:
78
+ vocals, drums, bass, guitar, piano, other
79
+
80
+ Returns:
81
+ Job object with id, status, task_id
82
+
83
+ Raises:
84
+ ValueError: If neither file nor url provided, or missing stem for single mode
85
+
86
+ Example:
87
+ >>> # From URL with 6 stems
88
+ >>> job = client.stems.extract(
89
+ ... url="https://youtube.com/watch?v=VIDEO_ID",
90
+ ... mode="six"
91
+ ... )
92
+ >>>
93
+ >>> # From file with 4 stems
94
+ >>> job = client.stems.extract(
95
+ ... file="./song.mp3",
96
+ ... mode="four"
97
+ ... )
98
+ >>>
99
+ >>> # Extract only vocals
100
+ >>> job = client.stems.extract(
101
+ ... url="https://youtube.com/watch?v=VIDEO_ID",
102
+ ... mode="single",
103
+ ... stem="vocals"
104
+ ... )
105
+ """
106
+ if not file and not url:
107
+ raise ValueError("Either file or url must be provided")
108
+
109
+ if mode == "single" and not stem:
110
+ raise ValueError(
111
+ "stem parameter required for mode='single'. Options: vocals, drums, bass, guitar, piano, other"
112
+ )
113
+
114
+ # Build form data
115
+ data = {"mode": mode}
116
+ if stem:
117
+ data["stem"] = stem
118
+
119
+ if file:
120
+ return self._client.upload(
121
+ "/api/v1/stem-extraction/api/extract",
122
+ file,
123
+ field_name="file",
124
+ additional_fields=data,
125
+ )
126
+
127
+ if url:
128
+ data["url"] = url
129
+ return self._client.post(
130
+ "/api/v1/stem-extraction/api/extract",
131
+ data=data,
132
+ )
133
+
134
+ raise ValueError("Either file or url must be provided")
135
+
136
+ def status(self, job_id: int) -> Dict[str, Any]:
137
+ """
138
+ Get the status of a stem extraction job.
139
+
140
+ Args:
141
+ job_id: The job ID returned from extract()
142
+
143
+ Returns:
144
+ Job status with download_urls when completed
145
+
146
+ Example:
147
+ >>> status = client.stems.status(5512)
148
+ >>> if status["status"] == "COMPLETED":
149
+ ... print(status["download_urls"])
150
+ """
151
+ return self._client.get(f"/api/v1/stem-extraction/status/{job_id}")
152
+
153
+ def get(self, job_id: int) -> Dict[str, Any]:
154
+ """Get a stem extraction job by ID."""
155
+ return self._client.get(f"/api/v1/stem-extraction/jobs/{job_id}")
156
+
157
+ def list(
158
+ self,
159
+ *,
160
+ skip: int = 0,
161
+ limit: int = 50,
162
+ status: Optional[str] = None,
163
+ ) -> Dict[str, Any]:
164
+ """
165
+ List stem extraction jobs.
166
+
167
+ Args:
168
+ skip: Number of jobs to skip (pagination)
169
+ limit: Maximum jobs to return (default: 50)
170
+ status: Filter by status (PENDING, PROCESSING, COMPLETED, FAILED)
171
+
172
+ Returns:
173
+ List of stem extraction jobs
174
+ """
175
+ return self._client.get(
176
+ "/api/v1/stem-extraction/jobs",
177
+ params={"skip": skip, "limit": limit, "status": status},
178
+ )
179
+
180
+ def delete(self, job_id: int) -> None:
181
+ """Delete a stem extraction job."""
182
+ self._client.delete(f"/api/v1/stem-extraction/jobs/{job_id}")
183
+
184
+ def wait_for_completion(self, job_id: int, timeout: int = DEFAULT_TIMEOUT) -> Dict[str, Any]:
185
+ """
186
+ Wait for stem extraction to complete.
187
+
188
+ Args:
189
+ job_id: The job ID to wait for
190
+ timeout: Maximum wait time in seconds (default: 600)
191
+
192
+ Returns:
193
+ Completed job with download_urls
194
+
195
+ Raises:
196
+ RuntimeError: If job fails
197
+ TimeoutError: If timeout exceeded
198
+
199
+ Example:
200
+ >>> job = client.stems.extract(url="...", mode="six")
201
+ >>> result = client.stems.wait_for_completion(job["id"])
202
+ >>> for stem, url in result["download_urls"].items():
203
+ ... print(f"{stem}: {url}")
204
+ """
205
+ start_time = time.time()
206
+
207
+ while time.time() - start_time < timeout:
208
+ job = self.status(job_id)
209
+ job_status = job.get("status", "")
210
+
211
+ if job_status == "COMPLETED":
212
+ return job
213
+ if job_status == "FAILED":
214
+ raise RuntimeError(
215
+ f"Stem extraction failed: {job.get('error_message', 'Unknown error')}"
216
+ )
217
+
218
+ time.sleep(POLL_INTERVAL)
219
+
220
+ raise TimeoutError(f"Stem extraction timed out after {timeout}s")
221
+
222
+ def separate(
223
+ self,
224
+ *,
225
+ file: Optional[str] = None,
226
+ url: Optional[str] = None,
227
+ mode: StemMode = "four",
228
+ stem: Optional[SingleStem] = None,
229
+ timeout: int = DEFAULT_TIMEOUT,
230
+ ) -> Dict[str, Any]:
231
+ """
232
+ Extract stems and wait for completion (convenience method).
233
+
234
+ Args:
235
+ file: Path to local audio file
236
+ url: URL of audio/video
237
+ mode: Separation mode (single, two, four, six, producer, studio, mastering)
238
+ stem: For mode="single", which stem to extract
239
+ timeout: Maximum wait time in seconds
240
+
241
+ Returns:
242
+ Completed job with download_urls
243
+
244
+ Example:
245
+ >>> # One-liner: extract and wait
246
+ >>> result = client.stems.separate(
247
+ ... url="https://youtube.com/watch?v=VIDEO_ID",
248
+ ... mode="six"
249
+ ... )
250
+ >>> print(result["download_urls"]["vocals"])
251
+ """
252
+ job = self.extract(file=file, url=url, mode=mode, stem=stem)
253
+ return self.wait_for_completion(job["id"], timeout=timeout)
254
+
255
+ def modes(self) -> Dict[str, Any]:
256
+ """
257
+ Get available separation modes.
258
+
259
+ Returns:
260
+ List of available modes with descriptions
261
+
262
+ Example:
263
+ >>> modes = client.stems.modes()
264
+ >>> for m in modes["modes"]:
265
+ ... print(f"{m['mode']}: {m['description']}")
266
+ """
267
+ return self._client.get("/api/v1/stem-extraction/modes")