audiopod 1.2.0__py3-none-any.whl → 1.5.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,53 +1,134 @@
1
1
  """
2
- Speaker Service - Speaker analysis and diarization
2
+ Speaker Service - Speaker diarization and extraction
3
3
  """
4
4
 
5
- from typing import Optional, Union
5
+ from typing import Optional, Dict, Any, List
6
6
  from .base import BaseService
7
- from ..models import Job, SpeakerAnalysisResult
8
7
 
9
8
 
10
9
  class SpeakerService(BaseService):
11
- """Service for speaker diarization and analysis"""
12
-
13
- def diarize_speakers(
10
+ """Service for speaker diarization and extraction."""
11
+
12
+ def diarize(
14
13
  self,
15
- audio_file: str,
14
+ audio_file: Optional[str] = None,
15
+ url: Optional[str] = None,
16
16
  num_speakers: Optional[int] = None,
17
17
  wait_for_completion: bool = False,
18
- timeout: int = 600
19
- ) -> Union[Job, SpeakerAnalysisResult]:
20
- """Identify and separate speakers in audio"""
21
- files = self._prepare_file_upload(audio_file, "file")
18
+ timeout: int = 600,
19
+ ) -> Dict[str, Any]:
20
+ """
21
+ Identify and separate speakers in audio.
22
+
23
+ Args:
24
+ audio_file: Path to local audio file
25
+ url: URL of audio file
26
+ num_speakers: Expected number of speakers (auto-detected if not provided)
27
+ wait_for_completion: Wait for completion
28
+ timeout: Max wait time in seconds
29
+
30
+ Returns:
31
+ Job dict with speaker segments when completed
32
+ """
22
33
  data = {}
23
34
  if num_speakers:
24
35
  data["num_speakers"] = num_speakers
25
-
36
+ if url:
37
+ data["url"] = url
38
+
39
+ files = self._prepare_file_upload(audio_file, "file") if audio_file else None
40
+
26
41
  if self.async_mode:
27
- return self._async_diarize(files, data, wait_for_completion, timeout)
28
- else:
29
- response = self.client.request(
30
- "POST", "/api/v1/speaker/diarize",
31
- data=data, files=files
32
- )
33
- job = Job.from_dict(response)
34
-
35
- if wait_for_completion:
36
- completed_job = self._wait_for_completion(job.id, timeout)
37
- return SpeakerAnalysisResult.from_dict(completed_job.result or completed_job.__dict__)
38
-
39
- return job
40
-
41
- async def _async_diarize(self, files, data, wait_for_completion, timeout):
42
- """Async version of diarize_speakers"""
42
+ return self._async_diarize(data, files, wait_for_completion, timeout)
43
+
44
+ response = self.client.request("POST", "/api/v1/speaker/diarize", data=data, files=files)
45
+
46
+ if wait_for_completion:
47
+ return self._wait_for_completion(response["id"], timeout)
48
+ return response
49
+
50
+ async def _async_diarize(
51
+ self, data: Dict, files: Optional[Dict], wait_for_completion: bool, timeout: int
52
+ ) -> Dict[str, Any]:
43
53
  response = await self.client.request(
44
- "POST", "/api/v1/speaker/diarize",
45
- data=data, files=files
54
+ "POST", "/api/v1/speaker/diarize", data=data, files=files
46
55
  )
47
- job = Job.from_dict(response)
48
-
49
56
  if wait_for_completion:
50
- completed_job = await self._async_wait_for_completion(job.id, timeout)
51
- return SpeakerAnalysisResult.from_dict(completed_job.result or completed_job.__dict__)
52
-
53
- return job
57
+ return await self._async_wait_for_completion(response["id"], timeout)
58
+ return response
59
+
60
+ def extract(
61
+ self,
62
+ audio_file: Optional[str] = None,
63
+ url: Optional[str] = None,
64
+ wait_for_completion: bool = False,
65
+ timeout: int = 600,
66
+ ) -> Dict[str, Any]:
67
+ """
68
+ Extract individual speaker audio tracks.
69
+
70
+ Args:
71
+ audio_file: Path to local audio file
72
+ url: URL of audio file
73
+ wait_for_completion: Wait for completion
74
+ timeout: Max wait time in seconds
75
+
76
+ Returns:
77
+ Job dict with speaker audio URLs when completed
78
+ """
79
+ data = {}
80
+ if url:
81
+ data["url"] = url
82
+
83
+ files = self._prepare_file_upload(audio_file, "file") if audio_file else None
84
+
85
+ if self.async_mode:
86
+ return self._async_extract(data, files, wait_for_completion, timeout)
87
+
88
+ response = self.client.request("POST", "/api/v1/speaker/extract", data=data, files=files)
89
+
90
+ if wait_for_completion:
91
+ return self._wait_for_completion(response["id"], timeout)
92
+ return response
93
+
94
+ async def _async_extract(
95
+ self, data: Dict, files: Optional[Dict], wait_for_completion: bool, timeout: int
96
+ ) -> Dict[str, Any]:
97
+ response = await self.client.request(
98
+ "POST", "/api/v1/speaker/extract", data=data, files=files
99
+ )
100
+ if wait_for_completion:
101
+ return await self._async_wait_for_completion(response["id"], timeout)
102
+ return response
103
+
104
+ def get_job(self, job_id: int) -> Dict[str, Any]:
105
+ """Get speaker job details and status."""
106
+ if self.async_mode:
107
+ return self._async_get_job(job_id)
108
+ return self.client.request("GET", f"/api/v1/speaker/jobs/{job_id}")
109
+
110
+ async def _async_get_job(self, job_id: int) -> Dict[str, Any]:
111
+ return await self.client.request("GET", f"/api/v1/speaker/jobs/{job_id}")
112
+
113
+ def list_jobs(self, skip: int = 0, limit: int = 50) -> List[Dict[str, Any]]:
114
+ """List speaker jobs."""
115
+ if self.async_mode:
116
+ return self._async_list_jobs(skip, limit)
117
+ return self.client.request(
118
+ "GET", "/api/v1/speaker/jobs", params={"skip": skip, "limit": limit}
119
+ )
120
+
121
+ async def _async_list_jobs(self, skip: int, limit: int) -> List[Dict[str, Any]]:
122
+ return await self.client.request(
123
+ "GET", "/api/v1/speaker/jobs", params={"skip": skip, "limit": limit}
124
+ )
125
+
126
+ def delete_job(self, job_id: int) -> Dict[str, str]:
127
+ """Delete a speaker job."""
128
+ if self.async_mode:
129
+ return self._async_delete_job(job_id)
130
+ return self.client.request("DELETE", f"/api/v1/speaker/jobs/{job_id}")
131
+
132
+ async def _async_delete_job(self, job_id: int) -> Dict[str, str]:
133
+ return await self.client.request("DELETE", f"/api/v1/speaker/jobs/{job_id}")
134
+
@@ -1,180 +1,287 @@
1
1
  """
2
- Stem Extraction Service - Audio stem separation operations
2
+ Stem Extraction Service - Audio stem separation
3
3
  """
4
4
 
5
- from typing import List, Optional, Dict, Any, Union
5
+ from typing import List, Optional, Dict, Any, Literal
6
6
  from .base import BaseService
7
- from ..models import Job
8
7
  from ..exceptions import ValidationError
9
8
 
10
9
 
10
+ # Valid separation modes for the new API
11
+ StemMode = Literal["single", "two", "four", "six", "producer", "studio", "mastering"]
12
+ SingleStem = Literal["vocals", "drums", "bass", "guitar", "piano", "other", "instrumental"]
13
+
14
+
11
15
  class StemExtractionService(BaseService):
12
- """Service for audio stem extraction operations"""
13
-
16
+ """
17
+ Service for audio stem separation.
18
+
19
+ Example:
20
+ ```python
21
+ from audiopod import Client
22
+
23
+ client = Client()
24
+
25
+ # Simple mode-based extraction (recommended)
26
+ result = client.stem_extraction.separate(
27
+ url="https://youtube.com/watch?v=VIDEO_ID",
28
+ mode="six"
29
+ )
30
+ for stem, url in result["download_urls"].items():
31
+ print(f"{stem}: {url}")
32
+
33
+ # Or extract only vocals
34
+ result = client.stem_extraction.separate(
35
+ file="song.mp3",
36
+ mode="single",
37
+ stem="vocals"
38
+ )
39
+ ```
40
+ """
41
+
42
+ def separate(
43
+ self,
44
+ file: Optional[str] = None,
45
+ url: Optional[str] = None,
46
+ mode: StemMode = "four",
47
+ stem: Optional[SingleStem] = None,
48
+ wait_for_completion: bool = True,
49
+ timeout: int = 900,
50
+ ) -> Dict[str, Any]:
51
+ """
52
+ Separate audio into stems using simple mode selection.
53
+
54
+ Args:
55
+ file: Path to local audio file
56
+ url: URL of audio/video (YouTube, SoundCloud, direct link)
57
+ mode: Separation mode:
58
+ - "single": Extract one stem (specify stem param)
59
+ - "two": Vocals + Instrumental
60
+ - "four": Vocals, Drums, Bass, Other (default)
61
+ - "six": Vocals, Drums, Bass, Guitar, Piano, Other
62
+ - "producer": 8 stems with kick, snare, hihat
63
+ - "studio": 12 stems for professional mixing
64
+ - "mastering": 16 stems maximum detail
65
+ stem: For mode="single", which stem to extract
66
+ wait_for_completion: Wait for job to complete (default: True)
67
+ timeout: Max wait time in seconds
68
+
69
+ Returns:
70
+ Job dict with id, status, download_urls (when completed)
71
+ """
72
+ if not file and not url:
73
+ raise ValidationError("Provide file or url")
74
+
75
+ if file and url:
76
+ raise ValidationError("Provide file or url, not both")
77
+
78
+ if mode == "single" and not stem:
79
+ raise ValidationError(
80
+ "stem parameter required for mode='single'. "
81
+ "Options: vocals, drums, bass, guitar, piano, other, instrumental"
82
+ )
83
+
84
+ data = {"mode": mode}
85
+ if stem:
86
+ data["stem"] = stem
87
+ if url:
88
+ data["url"] = url
89
+
90
+ files = self._prepare_file_upload(file, "file") if file else None
91
+
92
+ if self.async_mode:
93
+ return self._async_separate(data, files, wait_for_completion, timeout)
94
+
95
+ response = self.client.request(
96
+ "POST", "/api/v1/stem-extraction/api/extract", data=data, files=files
97
+ )
98
+
99
+ if wait_for_completion:
100
+ return self._wait_for_stem_job(response["id"], timeout)
101
+
102
+ return response
103
+
104
+ async def _async_separate(
105
+ self,
106
+ data: Dict[str, Any],
107
+ files: Optional[Dict[str, Any]],
108
+ wait_for_completion: bool,
109
+ timeout: int,
110
+ ) -> Dict[str, Any]:
111
+ response = await self.client.request(
112
+ "POST", "/api/v1/stem-extraction/api/extract", data=data, files=files
113
+ )
114
+ if wait_for_completion:
115
+ return await self._async_wait_for_stem_job(response["id"], timeout)
116
+ return response
117
+
118
+ def extract(
119
+ self,
120
+ file: Optional[str] = None,
121
+ url: Optional[str] = None,
122
+ mode: StemMode = "four",
123
+ stem: Optional[SingleStem] = None,
124
+ ) -> Dict[str, Any]:
125
+ """
126
+ Submit stem extraction job (returns immediately without waiting).
127
+
128
+ Use wait_for_completion() to poll for results.
129
+ """
130
+ return self.separate(file=file, url=url, mode=mode, stem=stem, wait_for_completion=False)
131
+
132
+ def wait_for_completion(self, job_id: int, timeout: int = 900) -> Dict[str, Any]:
133
+ """Wait for stem extraction job to complete."""
134
+ return self._wait_for_stem_job(job_id, timeout)
135
+
136
+ def status(self, job_id: int) -> Dict[str, Any]:
137
+ """Get stem extraction job status (alias for get_job)."""
138
+ return self.get_job(job_id)
139
+
140
+ def modes(self) -> Dict[str, Any]:
141
+ """Get available separation modes."""
142
+ if self.async_mode:
143
+ return self._async_modes()
144
+ return self.client.request("GET", "/api/v1/stem-extraction/modes")
145
+
146
+ async def _async_modes(self) -> Dict[str, Any]:
147
+ return await self.client.request("GET", "/api/v1/stem-extraction/modes")
148
+
149
+ # Legacy method - kept for backward compatibility
14
150
  def extract_stems(
15
151
  self,
16
152
  audio_file: Optional[str] = None,
17
153
  url: Optional[str] = None,
18
- stem_types: List[str] = None,
154
+ stem_types: Optional[List[str]] = None,
19
155
  model_name: str = "htdemucs",
20
156
  two_stems_mode: Optional[str] = None,
21
157
  wait_for_completion: bool = False,
22
- timeout: int = 900
23
- ) -> Job:
158
+ timeout: int = 900,
159
+ ) -> Dict[str, Any]:
24
160
  """
25
- Extract stems from audio file
26
-
161
+ Extract stems from audio (legacy method).
162
+
163
+ For new code, use separate() instead which uses the simpler mode-based API.
164
+
27
165
  Args:
28
- audio_file: Path to audio file to process
29
- url: URL of audio file to process (alternative to audio_file)
30
- stem_types: List of stems to extract (e.g., ['vocals', 'drums', 'bass', 'other'])
31
- model_name: Model to use for separation ('htdemucs' or 'htdemucs_6s')
32
- two_stems_mode: Two-stem mode for vocals/instrumental separation
33
- wait_for_completion: Whether to wait for completion
34
- timeout: Maximum time to wait
35
-
166
+ audio_file: Path to local audio file
167
+ url: URL of audio file (alternative to audio_file)
168
+ stem_types: Stems to extract (e.g., ["vocals", "drums", "bass", "other"])
169
+ model_name: Model to use ("htdemucs" or "htdemucs_6s")
170
+ two_stems_mode: Two-stem mode ("vocals", "drums", or "bass")
171
+ wait_for_completion: Wait for job to complete
172
+ timeout: Max wait time in seconds
173
+
36
174
  Returns:
37
- Job object with stem extraction details
175
+ Job dict with id, status, download_urls (when completed)
38
176
  """
39
177
  if not audio_file and not url:
40
- raise ValidationError("Either audio_file or url must be provided")
41
-
178
+ raise ValidationError("Provide audio_file or url")
179
+
42
180
  if audio_file and url:
43
- raise ValidationError("Provide either audio_file or url, not both")
44
-
45
- # Set default stem types based on model
181
+ raise ValidationError("Provide audio_file or url, not both")
182
+
46
183
  if stem_types is None:
47
- if model_name == "htdemucs_6s":
48
- stem_types = ["vocals", "drums", "bass", "other", "piano", "guitar"]
49
- else:
50
- stem_types = ["vocals", "drums", "bass", "other"]
51
-
52
- # Validate model name
53
- if model_name not in ["htdemucs", "htdemucs_6s"]:
54
- raise ValidationError("Model name must be 'htdemucs' or 'htdemucs_6s'")
55
-
56
- # Prepare request
57
- files = {}
58
- data = {
59
- "stem_types": str(stem_types), # API expects string representation
60
- "model_name": model_name
61
- }
62
-
63
- if audio_file:
64
- files = self._prepare_file_upload(audio_file, "file")
65
-
184
+ stem_types = (
185
+ ["vocals", "drums", "bass", "other", "piano", "guitar"]
186
+ if model_name == "htdemucs_6s"
187
+ else ["vocals", "drums", "bass", "other"]
188
+ )
189
+
190
+ data = {"stem_types": str(stem_types), "model_name": model_name}
191
+
66
192
  if url:
67
193
  data["url"] = url
68
-
194
+
69
195
  if two_stems_mode:
70
196
  data["two_stems_mode"] = two_stems_mode
71
-
197
+
198
+ files = self._prepare_file_upload(audio_file, "file") if audio_file else None
199
+
72
200
  if self.async_mode:
73
- return self._async_extract_stems(files, data, wait_for_completion, timeout)
74
- else:
75
- response = self.client.request(
76
- "POST",
77
- "/api/v1/stem-extraction/extract",
78
- data=data,
79
- files=files if files else None
80
- )
81
-
82
- job = Job.from_dict(response)
83
-
84
- if wait_for_completion:
85
- return self._wait_for_completion(job.id, timeout)
86
-
87
- return job
88
-
201
+ return self._async_extract_stems(data, files, wait_for_completion, timeout)
202
+
203
+ response = self.client.request(
204
+ "POST", "/api/v1/stem-extraction/extract", data=data, files=files
205
+ )
206
+
207
+ if wait_for_completion:
208
+ return self._wait_for_stem_job(response["id"], timeout)
209
+
210
+ return response
211
+
89
212
  async def _async_extract_stems(
90
213
  self,
91
- files: Dict[str, Any],
92
214
  data: Dict[str, Any],
215
+ files: Optional[Dict[str, Any]],
93
216
  wait_for_completion: bool,
94
- timeout: int
95
- ) -> Job:
96
- """Async version of extract_stems"""
217
+ timeout: int,
218
+ ) -> Dict[str, Any]:
97
219
  response = await self.client.request(
98
- "POST",
99
- "/api/v1/stem-extraction/extract",
100
- data=data,
101
- files=files if files else None
220
+ "POST", "/api/v1/stem-extraction/extract", data=data, files=files
102
221
  )
103
-
104
- job = Job.from_dict(response)
105
-
106
222
  if wait_for_completion:
107
- return await self._async_wait_for_completion(job.id, timeout)
108
-
109
- return job
110
-
111
- def get_stem_job(self, job_id: int) -> Job:
112
- """
113
- Get stem extraction job status
114
-
115
- Args:
116
- job_id: ID of the stem extraction job
117
-
118
- Returns:
119
- Job object with current status
120
- """
223
+ return await self._async_wait_for_stem_job(response["id"], timeout)
224
+ return response
225
+
226
+ def get_job(self, job_id: int) -> Dict[str, Any]:
227
+ """Get stem extraction job status."""
121
228
  if self.async_mode:
122
- return self._async_get_stem_job(job_id)
123
- else:
124
- response = self.client.request("GET", f"/api/v1/stem-extraction/status/{job_id}")
125
- return Job.from_dict(response)
126
-
127
- async def _async_get_stem_job(self, job_id: int) -> Job:
128
- """Async version of get_stem_job"""
129
- response = await self.client.request("GET", f"/api/v1/stem-extraction/status/{job_id}")
130
- return Job.from_dict(response)
131
-
132
- def list_stem_jobs(
133
- self,
134
- skip: int = 0,
135
- limit: int = 50
136
- ) -> List[Job]:
137
- """
138
- List stem extraction jobs
139
-
140
- Args:
141
- skip: Number of jobs to skip
142
- limit: Maximum number of jobs to return
143
-
144
- Returns:
145
- List of stem extraction jobs
146
- """
147
- params = {
148
- "skip": skip,
149
- "limit": limit
150
- }
151
-
229
+ return self._async_get_job(job_id)
230
+ return self.client.request("GET", f"/api/v1/stem-extraction/status/{job_id}")
231
+
232
+ async def _async_get_job(self, job_id: int) -> Dict[str, Any]:
233
+ return await self.client.request("GET", f"/api/v1/stem-extraction/status/{job_id}")
234
+
235
+ def list_jobs(self, skip: int = 0, limit: int = 50) -> List[Dict[str, Any]]:
236
+ """List stem extraction jobs."""
152
237
  if self.async_mode:
153
- return self._async_list_stem_jobs(params)
154
- else:
155
- response = self.client.request("GET", "/api/v1/stem-extraction/jobs", params=params)
156
- return [Job.from_dict(job_data) for job_data in response]
157
-
158
- async def _async_list_stem_jobs(self, params: Dict[str, Any]) -> List[Job]:
159
- """Async version of list_stem_jobs"""
160
- response = await self.client.request("GET", "/api/v1/stem-extraction/jobs", params=params)
161
- return [Job.from_dict(job_data) for job_data in response]
162
-
163
- def delete_stem_job(self, job_id: int) -> Dict[str, str]:
164
- """
165
- Delete a stem extraction job
166
-
167
- Args:
168
- job_id: ID of the job to delete
169
-
170
- Returns:
171
- Deletion confirmation
172
- """
238
+ return self._async_list_jobs(skip, limit)
239
+ return self.client.request(
240
+ "GET", "/api/v1/stem-extraction/jobs", params={"skip": skip, "limit": limit}
241
+ )
242
+
243
+ async def _async_list_jobs(self, skip: int, limit: int) -> List[Dict[str, Any]]:
244
+ return await self.client.request(
245
+ "GET", "/api/v1/stem-extraction/jobs", params={"skip": skip, "limit": limit}
246
+ )
247
+
248
+ def delete_job(self, job_id: int) -> Dict[str, str]:
249
+ """Delete a stem extraction job."""
173
250
  if self.async_mode:
174
- return self._async_delete_stem_job(job_id)
175
- else:
176
- return self.client.request("DELETE", f"/api/v1/stem-extraction/jobs/{job_id}")
177
-
178
- async def _async_delete_stem_job(self, job_id: int) -> Dict[str, str]:
179
- """Async version of delete_stem_job"""
251
+ return self._async_delete_job(job_id)
252
+ return self.client.request("DELETE", f"/api/v1/stem-extraction/jobs/{job_id}")
253
+
254
+ async def _async_delete_job(self, job_id: int) -> Dict[str, str]:
180
255
  return await self.client.request("DELETE", f"/api/v1/stem-extraction/jobs/{job_id}")
256
+
257
+ def _wait_for_stem_job(self, job_id: int, timeout: int = 900) -> Dict[str, Any]:
258
+ """Wait for stem job completion."""
259
+ import time
260
+
261
+ start = time.time()
262
+ while time.time() - start < timeout:
263
+ job = self.get_job(job_id)
264
+ status = job.get("status", "").upper()
265
+ if status == "COMPLETED":
266
+ return job
267
+ elif status in ["FAILED", "ERROR"]:
268
+ raise Exception(f"Job failed: {job.get('error_message', 'Unknown')}")
269
+ time.sleep(5)
270
+ raise TimeoutError(f"Job {job_id} timed out after {timeout}s")
271
+
272
+ async def _async_wait_for_stem_job(self, job_id: int, timeout: int = 900) -> Dict[str, Any]:
273
+ """Async wait for stem job completion."""
274
+ import asyncio
275
+ import time
276
+
277
+ start = time.time()
278
+ while time.time() - start < timeout:
279
+ job = await self.get_job(job_id)
280
+ status = job.get("status", "").upper()
281
+ if status == "COMPLETED":
282
+ return job
283
+ elif status in ["FAILED", "ERROR"]:
284
+ raise Exception(f"Job failed: {job.get('error_message', 'Unknown')}")
285
+ await asyncio.sleep(5)
286
+ raise TimeoutError(f"Job {job_id} timed out after {timeout}s")
287
+