audiopod 1.1.1__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.
- audiopod/__init__.py +10 -64
- audiopod/client.py +143 -172
- audiopod/config.py +4 -50
- audiopod/exceptions.py +16 -71
- audiopod/services/__init__.py +8 -6
- audiopod/services/base.py +51 -195
- audiopod/services/credits.py +26 -30
- audiopod/services/denoiser.py +120 -40
- audiopod/services/music.py +180 -485
- audiopod/services/speaker.py +117 -36
- audiopod/services/stem_extraction.py +130 -142
- audiopod/services/transcription.py +159 -184
- audiopod/services/translation.py +109 -170
- audiopod/services/voice.py +138 -327
- audiopod/services/wallet.py +235 -0
- audiopod-1.4.0.dist-info/METADATA +206 -0
- audiopod-1.4.0.dist-info/RECORD +20 -0
- {audiopod-1.1.1.dist-info → audiopod-1.4.0.dist-info}/WHEEL +1 -1
- audiopod/cli.py +0 -285
- audiopod/models.py +0 -250
- audiopod/py.typed +0 -2
- audiopod/services/karaoke.py +0 -61
- audiopod-1.1.1.dist-info/METADATA +0 -404
- audiopod-1.1.1.dist-info/RECORD +0 -24
- audiopod-1.1.1.dist-info/entry_points.txt +0 -2
- {audiopod-1.1.1.dist-info → audiopod-1.4.0.dist-info}/licenses/LICENSE +0 -0
- {audiopod-1.1.1.dist-info → audiopod-1.4.0.dist-info}/top_level.txt +0 -0
audiopod/services/speaker.py
CHANGED
|
@@ -1,53 +1,134 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Speaker Service - Speaker
|
|
2
|
+
Speaker Service - Speaker diarization and extraction
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
from typing import Optional,
|
|
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
|
|
12
|
-
|
|
13
|
-
def
|
|
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
|
-
) ->
|
|
20
|
-
"""
|
|
21
|
-
|
|
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(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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,168 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Stem Extraction Service - Audio stem separation
|
|
2
|
+
Stem Extraction Service - Audio stem separation
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
from typing import List, Optional, Dict, Any
|
|
5
|
+
from typing import List, Optional, Dict, Any
|
|
6
6
|
from .base import BaseService
|
|
7
|
-
from ..models import Job
|
|
8
7
|
from ..exceptions import ValidationError
|
|
9
8
|
|
|
10
9
|
|
|
11
10
|
class StemExtractionService(BaseService):
|
|
12
|
-
"""
|
|
13
|
-
|
|
11
|
+
"""
|
|
12
|
+
Service for audio stem separation.
|
|
13
|
+
|
|
14
|
+
Example:
|
|
15
|
+
```python
|
|
16
|
+
from audiopod import Client
|
|
17
|
+
|
|
18
|
+
client = Client()
|
|
19
|
+
|
|
20
|
+
# Extract all stems
|
|
21
|
+
job = client.stem_extraction.extract_stems(
|
|
22
|
+
audio_file="song.mp3",
|
|
23
|
+
stem_types=["vocals", "drums", "bass", "other"],
|
|
24
|
+
wait_for_completion=True
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Download stems
|
|
28
|
+
for stem_name, url in job["download_urls"].items():
|
|
29
|
+
print(f"{stem_name}: {url}")
|
|
30
|
+
```
|
|
31
|
+
"""
|
|
32
|
+
|
|
14
33
|
def extract_stems(
|
|
15
34
|
self,
|
|
16
35
|
audio_file: Optional[str] = None,
|
|
17
36
|
url: Optional[str] = None,
|
|
18
|
-
stem_types: List[str] = None,
|
|
37
|
+
stem_types: Optional[List[str]] = None,
|
|
19
38
|
model_name: str = "htdemucs",
|
|
20
39
|
two_stems_mode: Optional[str] = None,
|
|
21
40
|
wait_for_completion: bool = False,
|
|
22
|
-
timeout: int = 900
|
|
23
|
-
) ->
|
|
41
|
+
timeout: int = 900,
|
|
42
|
+
) -> Dict[str, Any]:
|
|
24
43
|
"""
|
|
25
|
-
Extract stems from audio
|
|
26
|
-
|
|
44
|
+
Extract stems from audio.
|
|
45
|
+
|
|
27
46
|
Args:
|
|
28
|
-
audio_file: Path to audio file
|
|
29
|
-
url: URL of audio file
|
|
30
|
-
stem_types:
|
|
31
|
-
model_name: Model to use
|
|
32
|
-
two_stems_mode: Two-stem mode
|
|
33
|
-
wait_for_completion:
|
|
34
|
-
timeout:
|
|
35
|
-
|
|
47
|
+
audio_file: Path to local audio file
|
|
48
|
+
url: URL of audio file (alternative to audio_file)
|
|
49
|
+
stem_types: Stems to extract (e.g., ["vocals", "drums", "bass", "other"])
|
|
50
|
+
model_name: Model to use ("htdemucs" or "htdemucs_6s")
|
|
51
|
+
two_stems_mode: Two-stem mode ("vocals", "drums", or "bass")
|
|
52
|
+
wait_for_completion: Wait for job to complete
|
|
53
|
+
timeout: Max wait time in seconds
|
|
54
|
+
|
|
36
55
|
Returns:
|
|
37
|
-
Job
|
|
56
|
+
Job dict with id, status, download_urls (when completed)
|
|
38
57
|
"""
|
|
39
58
|
if not audio_file and not url:
|
|
40
|
-
raise ValidationError("
|
|
41
|
-
|
|
59
|
+
raise ValidationError("Provide audio_file or url")
|
|
60
|
+
|
|
42
61
|
if audio_file and url:
|
|
43
|
-
raise ValidationError("Provide
|
|
44
|
-
|
|
45
|
-
# Set default stem types based on model
|
|
62
|
+
raise ValidationError("Provide audio_file or url, not both")
|
|
63
|
+
|
|
46
64
|
if stem_types is None:
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
65
|
+
stem_types = (
|
|
66
|
+
["vocals", "drums", "bass", "other", "piano", "guitar"]
|
|
67
|
+
if model_name == "htdemucs_6s"
|
|
68
|
+
else ["vocals", "drums", "bass", "other"]
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
data = {"stem_types": str(stem_types), "model_name": model_name}
|
|
72
|
+
|
|
66
73
|
if url:
|
|
67
74
|
data["url"] = url
|
|
68
|
-
|
|
75
|
+
|
|
69
76
|
if two_stems_mode:
|
|
70
77
|
data["two_stems_mode"] = two_stems_mode
|
|
71
|
-
|
|
78
|
+
|
|
79
|
+
files = self._prepare_file_upload(audio_file, "file") if audio_file else None
|
|
80
|
+
|
|
72
81
|
if self.async_mode:
|
|
73
|
-
return self._async_extract_stems(
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if wait_for_completion:
|
|
85
|
-
return self._wait_for_completion(job.id, timeout)
|
|
86
|
-
|
|
87
|
-
return job
|
|
88
|
-
|
|
82
|
+
return self._async_extract_stems(data, files, wait_for_completion, timeout)
|
|
83
|
+
|
|
84
|
+
response = self.client.request(
|
|
85
|
+
"POST", "/api/v1/stem-extraction/extract", data=data, files=files
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
if wait_for_completion:
|
|
89
|
+
return self._wait_for_stem_job(response["id"], timeout)
|
|
90
|
+
|
|
91
|
+
return response
|
|
92
|
+
|
|
89
93
|
async def _async_extract_stems(
|
|
90
94
|
self,
|
|
91
|
-
files: Dict[str, Any],
|
|
92
95
|
data: Dict[str, Any],
|
|
96
|
+
files: Optional[Dict[str, Any]],
|
|
93
97
|
wait_for_completion: bool,
|
|
94
|
-
timeout: int
|
|
95
|
-
) ->
|
|
96
|
-
"""Async version of extract_stems"""
|
|
98
|
+
timeout: int,
|
|
99
|
+
) -> Dict[str, Any]:
|
|
97
100
|
response = await self.client.request(
|
|
98
|
-
"POST",
|
|
99
|
-
"/api/v1/stem-extraction/extract",
|
|
100
|
-
data=data,
|
|
101
|
-
files=files if files else None
|
|
101
|
+
"POST", "/api/v1/stem-extraction/extract", data=data, files=files
|
|
102
102
|
)
|
|
103
|
-
|
|
104
|
-
job = Job.from_dict(response)
|
|
105
|
-
|
|
106
103
|
if wait_for_completion:
|
|
107
|
-
return await self.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
"""
|
|
104
|
+
return await self._async_wait_for_stem_job(response["id"], timeout)
|
|
105
|
+
return response
|
|
106
|
+
|
|
107
|
+
def get_job(self, job_id: int) -> Dict[str, Any]:
|
|
108
|
+
"""Get stem extraction job status."""
|
|
121
109
|
if self.async_mode:
|
|
122
|
-
return self.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
110
|
+
return self._async_get_job(job_id)
|
|
111
|
+
return self.client.request("GET", f"/api/v1/stem-extraction/status/{job_id}")
|
|
112
|
+
|
|
113
|
+
async def _async_get_job(self, job_id: int) -> Dict[str, Any]:
|
|
114
|
+
return await self.client.request("GET", f"/api/v1/stem-extraction/status/{job_id}")
|
|
115
|
+
|
|
116
|
+
def list_jobs(self, skip: int = 0, limit: int = 50) -> List[Dict[str, Any]]:
|
|
117
|
+
"""List stem extraction jobs."""
|
|
152
118
|
if self.async_mode:
|
|
153
|
-
return self.
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
async def
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
def
|
|
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
|
-
"""
|
|
119
|
+
return self._async_list_jobs(skip, limit)
|
|
120
|
+
return self.client.request(
|
|
121
|
+
"GET", "/api/v1/stem-extraction/jobs", params={"skip": skip, "limit": limit}
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
async def _async_list_jobs(self, skip: int, limit: int) -> List[Dict[str, Any]]:
|
|
125
|
+
return await self.client.request(
|
|
126
|
+
"GET", "/api/v1/stem-extraction/jobs", params={"skip": skip, "limit": limit}
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def delete_job(self, job_id: int) -> Dict[str, str]:
|
|
130
|
+
"""Delete a stem extraction job."""
|
|
173
131
|
if self.async_mode:
|
|
174
|
-
return self.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
async def _async_delete_stem_job(self, job_id: int) -> Dict[str, str]:
|
|
179
|
-
"""Async version of delete_stem_job"""
|
|
132
|
+
return self._async_delete_job(job_id)
|
|
133
|
+
return self.client.request("DELETE", f"/api/v1/stem-extraction/jobs/{job_id}")
|
|
134
|
+
|
|
135
|
+
async def _async_delete_job(self, job_id: int) -> Dict[str, str]:
|
|
180
136
|
return await self.client.request("DELETE", f"/api/v1/stem-extraction/jobs/{job_id}")
|
|
137
|
+
|
|
138
|
+
def _wait_for_stem_job(self, job_id: int, timeout: int = 900) -> Dict[str, Any]:
|
|
139
|
+
"""Wait for stem job completion."""
|
|
140
|
+
import time
|
|
141
|
+
|
|
142
|
+
start = time.time()
|
|
143
|
+
while time.time() - start < timeout:
|
|
144
|
+
job = self.get_job(job_id)
|
|
145
|
+
status = job.get("status", "").upper()
|
|
146
|
+
if status == "COMPLETED":
|
|
147
|
+
return job
|
|
148
|
+
elif status in ["FAILED", "ERROR"]:
|
|
149
|
+
raise Exception(f"Job failed: {job.get('error_message', 'Unknown')}")
|
|
150
|
+
time.sleep(5)
|
|
151
|
+
raise TimeoutError(f"Job {job_id} timed out after {timeout}s")
|
|
152
|
+
|
|
153
|
+
async def _async_wait_for_stem_job(self, job_id: int, timeout: int = 900) -> Dict[str, Any]:
|
|
154
|
+
"""Async wait for stem job completion."""
|
|
155
|
+
import asyncio
|
|
156
|
+
import time
|
|
157
|
+
|
|
158
|
+
start = time.time()
|
|
159
|
+
while time.time() - start < timeout:
|
|
160
|
+
job = await self.get_job(job_id)
|
|
161
|
+
status = job.get("status", "").upper()
|
|
162
|
+
if status == "COMPLETED":
|
|
163
|
+
return job
|
|
164
|
+
elif status in ["FAILED", "ERROR"]:
|
|
165
|
+
raise Exception(f"Job failed: {job.get('error_message', 'Unknown')}")
|
|
166
|
+
await asyncio.sleep(5)
|
|
167
|
+
raise TimeoutError(f"Job {job_id} timed out after {timeout}s")
|
|
168
|
+
|