audiopod 2.1.0__py3-none-any.whl → 2.2.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 +12 -13
- audiopod/client.py +238 -124
- audiopod/config.py +17 -0
- audiopod/exceptions.py +19 -27
- audiopod/services/__init__.py +30 -0
- audiopod/services/base.py +69 -0
- audiopod/services/credits.py +42 -0
- audiopod/services/denoiser.py +136 -0
- audiopod/services/music.py +217 -0
- audiopod/services/speaker.py +134 -0
- audiopod/services/stem_extraction.py +287 -0
- audiopod/services/transcription.py +210 -0
- audiopod/services/translation.py +135 -0
- audiopod/services/video.py +329 -0
- audiopod/services/voice.py +187 -0
- audiopod/services/wallet.py +235 -0
- audiopod-2.2.0.dist-info/METADATA +206 -0
- audiopod-2.2.0.dist-info/RECORD +21 -0
- {audiopod-2.1.0.dist-info → audiopod-2.2.0.dist-info}/WHEEL +1 -1
- audiopod/resources/__init__.py +0 -23
- audiopod/resources/denoiser.py +0 -116
- audiopod/resources/music.py +0 -166
- audiopod/resources/speaker.py +0 -132
- audiopod/resources/stems.py +0 -267
- audiopod/resources/transcription.py +0 -205
- audiopod/resources/voice.py +0 -139
- audiopod/resources/wallet.py +0 -110
- audiopod-2.1.0.dist-info/METADATA +0 -205
- audiopod-2.1.0.dist-info/RECORD +0 -16
- {audiopod-2.1.0.dist-info → audiopod-2.2.0.dist-info}/licenses/LICENSE +0 -0
- {audiopod-2.1.0.dist-info → audiopod-2.2.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Stem Extraction Service - Audio stem separation
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import List, Optional, Dict, Any, Literal
|
|
6
|
+
from .base import BaseService
|
|
7
|
+
from ..exceptions import ValidationError
|
|
8
|
+
|
|
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
|
+
|
|
15
|
+
class StemExtractionService(BaseService):
|
|
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
|
|
150
|
+
def extract_stems(
|
|
151
|
+
self,
|
|
152
|
+
audio_file: Optional[str] = None,
|
|
153
|
+
url: Optional[str] = None,
|
|
154
|
+
stem_types: Optional[List[str]] = None,
|
|
155
|
+
model_name: str = "htdemucs",
|
|
156
|
+
two_stems_mode: Optional[str] = None,
|
|
157
|
+
wait_for_completion: bool = False,
|
|
158
|
+
timeout: int = 900,
|
|
159
|
+
) -> Dict[str, Any]:
|
|
160
|
+
"""
|
|
161
|
+
Extract stems from audio (legacy method).
|
|
162
|
+
|
|
163
|
+
For new code, use separate() instead which uses the simpler mode-based API.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
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
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Job dict with id, status, download_urls (when completed)
|
|
176
|
+
"""
|
|
177
|
+
if not audio_file and not url:
|
|
178
|
+
raise ValidationError("Provide audio_file or url")
|
|
179
|
+
|
|
180
|
+
if audio_file and url:
|
|
181
|
+
raise ValidationError("Provide audio_file or url, not both")
|
|
182
|
+
|
|
183
|
+
if stem_types is None:
|
|
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
|
+
|
|
192
|
+
if url:
|
|
193
|
+
data["url"] = url
|
|
194
|
+
|
|
195
|
+
if two_stems_mode:
|
|
196
|
+
data["two_stems_mode"] = two_stems_mode
|
|
197
|
+
|
|
198
|
+
files = self._prepare_file_upload(audio_file, "file") if audio_file else None
|
|
199
|
+
|
|
200
|
+
if self.async_mode:
|
|
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
|
+
|
|
212
|
+
async def _async_extract_stems(
|
|
213
|
+
self,
|
|
214
|
+
data: Dict[str, Any],
|
|
215
|
+
files: Optional[Dict[str, Any]],
|
|
216
|
+
wait_for_completion: bool,
|
|
217
|
+
timeout: int,
|
|
218
|
+
) -> Dict[str, Any]:
|
|
219
|
+
response = await self.client.request(
|
|
220
|
+
"POST", "/api/v1/stem-extraction/extract", data=data, files=files
|
|
221
|
+
)
|
|
222
|
+
if wait_for_completion:
|
|
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."""
|
|
228
|
+
if self.async_mode:
|
|
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."""
|
|
237
|
+
if self.async_mode:
|
|
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."""
|
|
250
|
+
if self.async_mode:
|
|
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]:
|
|
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
|
+
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Transcription Service - Speech-to-text
|
|
3
|
+
|
|
4
|
+
API Routes:
|
|
5
|
+
- POST /api/v1/transcription/transcribe - Transcribe from URL
|
|
6
|
+
- POST /api/v1/transcription/transcribe-upload - Transcribe from file upload
|
|
7
|
+
- GET /api/v1/transcription/jobs/{id} - Get job details
|
|
8
|
+
- GET /api/v1/transcription/jobs - List jobs
|
|
9
|
+
- DELETE /api/v1/transcription/jobs/{id} - Delete job
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import Optional, Dict, Any, List
|
|
13
|
+
from .base import BaseService
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TranscriptionService(BaseService):
|
|
17
|
+
"""Service for speech-to-text transcription."""
|
|
18
|
+
|
|
19
|
+
def create(
|
|
20
|
+
self,
|
|
21
|
+
file: Optional[str] = None,
|
|
22
|
+
url: Optional[str] = None,
|
|
23
|
+
language: Optional[str] = None,
|
|
24
|
+
speaker_diarization: bool = False,
|
|
25
|
+
wait_for_completion: bool = False,
|
|
26
|
+
timeout: int = 600,
|
|
27
|
+
) -> Dict[str, Any]:
|
|
28
|
+
"""Alias for transcribe() - matches Node.js SDK."""
|
|
29
|
+
return self.transcribe(
|
|
30
|
+
audio_file=file,
|
|
31
|
+
url=url,
|
|
32
|
+
language=language,
|
|
33
|
+
speaker_diarization=speaker_diarization,
|
|
34
|
+
wait_for_completion=wait_for_completion,
|
|
35
|
+
timeout=timeout,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def transcribe(
|
|
39
|
+
self,
|
|
40
|
+
audio_file: Optional[str] = None,
|
|
41
|
+
url: Optional[str] = None,
|
|
42
|
+
language: Optional[str] = None,
|
|
43
|
+
speaker_diarization: bool = False,
|
|
44
|
+
wait_for_completion: bool = False,
|
|
45
|
+
timeout: int = 600,
|
|
46
|
+
) -> Dict[str, Any]:
|
|
47
|
+
"""
|
|
48
|
+
Transcribe audio to text.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
audio_file: Path to local audio file
|
|
52
|
+
url: URL of audio file (or list of URLs)
|
|
53
|
+
language: Language code (auto-detected if not provided)
|
|
54
|
+
speaker_diarization: Enable speaker separation
|
|
55
|
+
wait_for_completion: Wait for completion
|
|
56
|
+
timeout: Max wait time in seconds
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Job dict with transcript when completed
|
|
60
|
+
"""
|
|
61
|
+
if audio_file:
|
|
62
|
+
# File upload endpoint
|
|
63
|
+
data = {
|
|
64
|
+
"enable_speaker_diarization": speaker_diarization,
|
|
65
|
+
}
|
|
66
|
+
if language:
|
|
67
|
+
data["language"] = language
|
|
68
|
+
|
|
69
|
+
files = self._prepare_file_upload(audio_file, "files")
|
|
70
|
+
|
|
71
|
+
if self.async_mode:
|
|
72
|
+
return self._async_transcribe_upload(data, files, wait_for_completion, timeout)
|
|
73
|
+
|
|
74
|
+
response = self.client.request(
|
|
75
|
+
"POST", "/api/v1/transcription/transcribe-upload", data=data, files=files
|
|
76
|
+
)
|
|
77
|
+
else:
|
|
78
|
+
# URL-based endpoint
|
|
79
|
+
data = {
|
|
80
|
+
"source_urls": [url] if isinstance(url, str) else url,
|
|
81
|
+
"enable_speaker_diarization": speaker_diarization,
|
|
82
|
+
}
|
|
83
|
+
if language:
|
|
84
|
+
data["language"] = language
|
|
85
|
+
|
|
86
|
+
if self.async_mode:
|
|
87
|
+
return self._async_transcribe(data, wait_for_completion, timeout)
|
|
88
|
+
|
|
89
|
+
response = self.client.request(
|
|
90
|
+
"POST", "/api/v1/transcription/transcribe", json_data=data
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
if wait_for_completion:
|
|
94
|
+
job_id = response.get("id") or response.get("job_id")
|
|
95
|
+
return self._wait_for_transcription(job_id, timeout)
|
|
96
|
+
return response
|
|
97
|
+
|
|
98
|
+
async def _async_transcribe(
|
|
99
|
+
self, data: Dict, wait_for_completion: bool, timeout: int
|
|
100
|
+
) -> Dict[str, Any]:
|
|
101
|
+
response = await self.client.request(
|
|
102
|
+
"POST", "/api/v1/transcription/transcribe", json_data=data
|
|
103
|
+
)
|
|
104
|
+
if wait_for_completion:
|
|
105
|
+
job_id = response.get("id") or response.get("job_id")
|
|
106
|
+
return await self._async_wait_for_transcription(job_id, timeout)
|
|
107
|
+
return response
|
|
108
|
+
|
|
109
|
+
async def _async_transcribe_upload(
|
|
110
|
+
self, data: Dict, files: Dict, wait_for_completion: bool, timeout: int
|
|
111
|
+
) -> Dict[str, Any]:
|
|
112
|
+
response = await self.client.request(
|
|
113
|
+
"POST", "/api/v1/transcription/transcribe-upload", data=data, files=files
|
|
114
|
+
)
|
|
115
|
+
if wait_for_completion:
|
|
116
|
+
job_id = response.get("id") or response.get("job_id")
|
|
117
|
+
return await self._async_wait_for_transcription(job_id, timeout)
|
|
118
|
+
return response
|
|
119
|
+
|
|
120
|
+
def get_job(self, job_id: int) -> Dict[str, Any]:
|
|
121
|
+
"""Get transcription job details and status."""
|
|
122
|
+
if self.async_mode:
|
|
123
|
+
return self._async_get_job(job_id)
|
|
124
|
+
return self.client.request("GET", f"/api/v1/transcription/jobs/{job_id}")
|
|
125
|
+
|
|
126
|
+
async def _async_get_job(self, job_id: int) -> Dict[str, Any]:
|
|
127
|
+
return await self.client.request("GET", f"/api/v1/transcription/jobs/{job_id}")
|
|
128
|
+
|
|
129
|
+
def list_jobs(self, skip: int = 0, limit: int = 50) -> List[Dict[str, Any]]:
|
|
130
|
+
"""List transcription jobs."""
|
|
131
|
+
if self.async_mode:
|
|
132
|
+
return self._async_list_jobs(skip, limit)
|
|
133
|
+
return self.client.request(
|
|
134
|
+
"GET", "/api/v1/transcription/jobs", params={"skip": skip, "limit": limit}
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
async def _async_list_jobs(self, skip: int, limit: int) -> List[Dict[str, Any]]:
|
|
138
|
+
return await self.client.request(
|
|
139
|
+
"GET", "/api/v1/transcription/jobs", params={"skip": skip, "limit": limit}
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def delete_job(self, job_id: int) -> Dict[str, str]:
|
|
143
|
+
"""Delete a transcription job."""
|
|
144
|
+
if self.async_mode:
|
|
145
|
+
return self._async_delete_job(job_id)
|
|
146
|
+
return self.client.request("DELETE", f"/api/v1/transcription/jobs/{job_id}")
|
|
147
|
+
|
|
148
|
+
async def _async_delete_job(self, job_id: int) -> Dict[str, str]:
|
|
149
|
+
return await self.client.request("DELETE", f"/api/v1/transcription/jobs/{job_id}")
|
|
150
|
+
|
|
151
|
+
def get_transcript(self, job_id: int, format: str = "json") -> Any:
|
|
152
|
+
"""
|
|
153
|
+
Get transcript content.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
job_id: Job ID
|
|
157
|
+
format: Output format - 'json', 'txt', 'srt', 'vtt'
|
|
158
|
+
"""
|
|
159
|
+
if self.async_mode:
|
|
160
|
+
return self._async_get_transcript(job_id, format)
|
|
161
|
+
return self.client.request(
|
|
162
|
+
"GET", f"/api/v1/transcription/jobs/{job_id}/transcript", params={"format": format}
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
async def _async_get_transcript(self, job_id: int, format: str) -> Any:
|
|
166
|
+
return await self.client.request(
|
|
167
|
+
"GET", f"/api/v1/transcription/jobs/{job_id}/transcript", params={"format": format}
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
def wait_for_completion(self, job_id: int, timeout: int = 600) -> Dict[str, Any]:
|
|
171
|
+
"""Wait for transcription job completion (matches Node.js SDK)."""
|
|
172
|
+
return self._wait_for_transcription(job_id, timeout)
|
|
173
|
+
|
|
174
|
+
def _wait_for_transcription(self, job_id: int, timeout: int) -> Dict[str, Any]:
|
|
175
|
+
"""Wait for transcription job completion."""
|
|
176
|
+
import time
|
|
177
|
+
start_time = time.time()
|
|
178
|
+
|
|
179
|
+
while time.time() - start_time < timeout:
|
|
180
|
+
job = self.get_job(job_id)
|
|
181
|
+
status = job.get("status", "").upper()
|
|
182
|
+
|
|
183
|
+
if status == "COMPLETED":
|
|
184
|
+
return job
|
|
185
|
+
elif status in ("FAILED", "ERROR", "CANCELLED"):
|
|
186
|
+
raise Exception(f"Transcription failed: {job.get('error_message', 'Unknown error')}")
|
|
187
|
+
|
|
188
|
+
time.sleep(3)
|
|
189
|
+
|
|
190
|
+
raise TimeoutError(f"Transcription {job_id} did not complete within {timeout} seconds")
|
|
191
|
+
|
|
192
|
+
async def _async_wait_for_transcription(self, job_id: int, timeout: int) -> Dict[str, Any]:
|
|
193
|
+
"""Async wait for transcription job completion."""
|
|
194
|
+
import asyncio
|
|
195
|
+
import time
|
|
196
|
+
start_time = time.time()
|
|
197
|
+
|
|
198
|
+
while time.time() - start_time < timeout:
|
|
199
|
+
job = await self.get_job(job_id)
|
|
200
|
+
status = job.get("status", "").upper()
|
|
201
|
+
|
|
202
|
+
if status == "COMPLETED":
|
|
203
|
+
return job
|
|
204
|
+
elif status in ("FAILED", "ERROR", "CANCELLED"):
|
|
205
|
+
raise Exception(f"Transcription failed: {job.get('error_message', 'Unknown error')}")
|
|
206
|
+
|
|
207
|
+
await asyncio.sleep(3)
|
|
208
|
+
|
|
209
|
+
raise TimeoutError(f"Transcription {job_id} did not complete within {timeout} seconds")
|
|
210
|
+
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Translation Service - Audio/speech translation
|
|
3
|
+
|
|
4
|
+
API Routes:
|
|
5
|
+
- POST /api/v1/translation/translate/speech - Translate speech
|
|
6
|
+
- GET /api/v1/translation/translations/{id} - Get translation job
|
|
7
|
+
- GET /api/v1/translation/translations - List translations
|
|
8
|
+
- DELETE /api/v1/translation/translations/{id} - Delete translation
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Optional, Dict, Any, List
|
|
12
|
+
from .base import BaseService
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TranslationService(BaseService):
|
|
16
|
+
"""Service for audio translation."""
|
|
17
|
+
|
|
18
|
+
def translate(
|
|
19
|
+
self,
|
|
20
|
+
audio_file: Optional[str] = None,
|
|
21
|
+
url: Optional[str] = None,
|
|
22
|
+
target_language: str = "en",
|
|
23
|
+
source_language: Optional[str] = None,
|
|
24
|
+
wait_for_completion: bool = False,
|
|
25
|
+
timeout: int = 900,
|
|
26
|
+
) -> Dict[str, Any]:
|
|
27
|
+
"""
|
|
28
|
+
Translate audio to another language.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
audio_file: Path to local audio file
|
|
32
|
+
url: URL of audio file
|
|
33
|
+
target_language: Target language code
|
|
34
|
+
source_language: Source language (auto-detected if not provided)
|
|
35
|
+
wait_for_completion: Wait for completion
|
|
36
|
+
timeout: Max wait time in seconds
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Job dict with translated audio URL when completed
|
|
40
|
+
"""
|
|
41
|
+
data = {"target_language": target_language}
|
|
42
|
+
if source_language:
|
|
43
|
+
data["source_language"] = source_language
|
|
44
|
+
if url:
|
|
45
|
+
data["url"] = url
|
|
46
|
+
|
|
47
|
+
files = self._prepare_file_upload(audio_file, "file") if audio_file else None
|
|
48
|
+
|
|
49
|
+
if self.async_mode:
|
|
50
|
+
return self._async_translate(data, files, wait_for_completion, timeout)
|
|
51
|
+
|
|
52
|
+
response = self.client.request("POST", "/api/v1/translation/translate/speech", data=data, files=files)
|
|
53
|
+
|
|
54
|
+
if wait_for_completion:
|
|
55
|
+
job_id = response.get("id") or response.get("job_id")
|
|
56
|
+
return self._wait_for_translation(job_id, timeout)
|
|
57
|
+
return response
|
|
58
|
+
|
|
59
|
+
async def _async_translate(
|
|
60
|
+
self, data: Dict, files: Optional[Dict], wait_for_completion: bool, timeout: int
|
|
61
|
+
) -> Dict[str, Any]:
|
|
62
|
+
response = await self.client.request("POST", "/api/v1/translation/translate/speech", data=data, files=files)
|
|
63
|
+
if wait_for_completion:
|
|
64
|
+
job_id = response.get("id") or response.get("job_id")
|
|
65
|
+
return await self._async_wait_for_translation(job_id, timeout)
|
|
66
|
+
return response
|
|
67
|
+
|
|
68
|
+
def get_job(self, job_id: int) -> Dict[str, Any]:
|
|
69
|
+
"""Get translation job details and status."""
|
|
70
|
+
if self.async_mode:
|
|
71
|
+
return self._async_get_job(job_id)
|
|
72
|
+
return self.client.request("GET", f"/api/v1/translation/translations/{job_id}")
|
|
73
|
+
|
|
74
|
+
async def _async_get_job(self, job_id: int) -> Dict[str, Any]:
|
|
75
|
+
return await self.client.request("GET", f"/api/v1/translation/translations/{job_id}")
|
|
76
|
+
|
|
77
|
+
def list_jobs(self, skip: int = 0, limit: int = 50) -> List[Dict[str, Any]]:
|
|
78
|
+
"""List translation jobs."""
|
|
79
|
+
if self.async_mode:
|
|
80
|
+
return self._async_list_jobs(skip, limit)
|
|
81
|
+
return self.client.request(
|
|
82
|
+
"GET", "/api/v1/translation/translations", params={"skip": skip, "limit": limit}
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
async def _async_list_jobs(self, skip: int, limit: int) -> List[Dict[str, Any]]:
|
|
86
|
+
return await self.client.request(
|
|
87
|
+
"GET", "/api/v1/translation/translations", params={"skip": skip, "limit": limit}
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def delete_job(self, job_id: int) -> Dict[str, str]:
|
|
91
|
+
"""Delete a translation job."""
|
|
92
|
+
if self.async_mode:
|
|
93
|
+
return self._async_delete_job(job_id)
|
|
94
|
+
return self.client.request("DELETE", f"/api/v1/translation/translations/{job_id}")
|
|
95
|
+
|
|
96
|
+
async def _async_delete_job(self, job_id: int) -> Dict[str, str]:
|
|
97
|
+
return await self.client.request("DELETE", f"/api/v1/translation/translations/{job_id}")
|
|
98
|
+
|
|
99
|
+
def _wait_for_translation(self, job_id: int, timeout: int) -> Dict[str, Any]:
|
|
100
|
+
"""Wait for translation job completion."""
|
|
101
|
+
import time
|
|
102
|
+
start_time = time.time()
|
|
103
|
+
|
|
104
|
+
while time.time() - start_time < timeout:
|
|
105
|
+
job = self.get_job(job_id)
|
|
106
|
+
status = job.get("status", "").upper()
|
|
107
|
+
|
|
108
|
+
if status == "COMPLETED":
|
|
109
|
+
return job
|
|
110
|
+
elif status in ("FAILED", "ERROR"):
|
|
111
|
+
raise Exception(f"Translation failed: {job.get('error_message', 'Unknown error')}")
|
|
112
|
+
|
|
113
|
+
time.sleep(5)
|
|
114
|
+
|
|
115
|
+
raise TimeoutError(f"Translation {job_id} did not complete within {timeout} seconds")
|
|
116
|
+
|
|
117
|
+
async def _async_wait_for_translation(self, job_id: int, timeout: int) -> Dict[str, Any]:
|
|
118
|
+
"""Async wait for translation job completion."""
|
|
119
|
+
import asyncio
|
|
120
|
+
import time
|
|
121
|
+
start_time = time.time()
|
|
122
|
+
|
|
123
|
+
while time.time() - start_time < timeout:
|
|
124
|
+
job = await self.get_job(job_id)
|
|
125
|
+
status = job.get("status", "").upper()
|
|
126
|
+
|
|
127
|
+
if status == "COMPLETED":
|
|
128
|
+
return job
|
|
129
|
+
elif status in ("FAILED", "ERROR"):
|
|
130
|
+
raise Exception(f"Translation failed: {job.get('error_message', 'Unknown error')}")
|
|
131
|
+
|
|
132
|
+
await asyncio.sleep(5)
|
|
133
|
+
|
|
134
|
+
raise TimeoutError(f"Translation {job_id} did not complete within {timeout} seconds")
|
|
135
|
+
|