audiopod 1.0.0__py3-none-any.whl → 1.1.1__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 CHANGED
@@ -47,7 +47,7 @@ from .models import (
47
47
  TranslationResult
48
48
  )
49
49
 
50
- __version__ = "1.0.0"
50
+ __version__ = "1.1.1"
51
51
  __author__ = "AudioPod AI"
52
52
  __email__ = "support@audiopod.ai"
53
53
  __license__ = "MIT"
audiopod/client.py CHANGED
@@ -23,7 +23,8 @@ from .services import (
23
23
  SpeakerService,
24
24
  DenoiserService,
25
25
  KaraokeService,
26
- CreditService
26
+ CreditService,
27
+ StemExtractionService
27
28
  )
28
29
 
29
30
  logger = logging.getLogger(__name__)
@@ -139,6 +140,7 @@ class Client(BaseClient):
139
140
  self.denoiser = DenoiserService(self)
140
141
  self.karaoke = KaraokeService(self)
141
142
  self.credits = CreditService(self)
143
+ self.stem_extraction = StemExtractionService(self)
142
144
 
143
145
  def request(
144
146
  self,
@@ -227,6 +229,7 @@ class AsyncClient(BaseClient):
227
229
  self.denoiser = DenoiserService(self, async_mode=True)
228
230
  self.karaoke = KaraokeService(self, async_mode=True)
229
231
  self.credits = CreditService(self, async_mode=True)
232
+ self.stem_extraction = StemExtractionService(self, async_mode=True)
230
233
 
231
234
  @property
232
235
  def session(self) -> aiohttp.ClientSession:
audiopod/models.py CHANGED
@@ -151,13 +151,18 @@ class MusicGenerationResult:
151
151
 
152
152
  @dataclass
153
153
  class TranslationResult:
154
- """Translation job result"""
154
+ """Speech translation job result"""
155
155
  job: Job
156
156
  source_language: Optional[str] = None
157
157
  target_language: Optional[str] = None
158
- audio_output_url: Optional[str] = None
159
- video_output_url: Optional[str] = None
158
+ display_name: Optional[str] = None
159
+ audio_output_path: Optional[str] = None
160
+ video_output_path: Optional[str] = None
160
161
  transcript_path: Optional[str] = None
162
+ translated_audio_url: Optional[str] = None
163
+ video_output_url: Optional[str] = None
164
+ transcript_urls: Optional[Dict[str, str]] = None
165
+ is_video: bool = False
161
166
 
162
167
  @classmethod
163
168
  def from_dict(cls, data: Dict[str, Any]) -> 'TranslationResult':
@@ -166,10 +171,20 @@ class TranslationResult:
166
171
  job=Job.from_dict(data),
167
172
  source_language=data.get('source_language'),
168
173
  target_language=data.get('target_language'),
169
- audio_output_url=data.get('audio_output_path'),
170
- video_output_url=data.get('video_output_path'),
171
- transcript_path=data.get('transcript_path')
174
+ display_name=data.get('display_name'),
175
+ audio_output_path=data.get('audio_output_path'),
176
+ video_output_path=data.get('video_output_path'),
177
+ transcript_path=data.get('transcript_path'),
178
+ translated_audio_url=data.get('translated_audio_url'),
179
+ video_output_url=data.get('video_output_url'),
180
+ transcript_urls=data.get('transcript_urls'),
181
+ is_video=data.get('is_video', False)
172
182
  )
183
+
184
+ @property
185
+ def audio_output_url(self) -> Optional[str]:
186
+ """Backward compatibility property - returns translated_audio_url"""
187
+ return self.translated_audio_url
173
188
 
174
189
 
175
190
  @dataclass
audiopod/py.typed ADDED
@@ -0,0 +1,2 @@
1
+ # Marker file for PEP 561
2
+ # This package supports type checking
@@ -11,6 +11,7 @@ from .speaker import SpeakerService
11
11
  from .denoiser import DenoiserService
12
12
  from .karaoke import KaraokeService
13
13
  from .credits import CreditService
14
+ from .stem_extraction import StemExtractionService
14
15
 
15
16
  __all__ = [
16
17
  "VoiceService",
@@ -20,5 +21,6 @@ __all__ = [
20
21
  "SpeakerService",
21
22
  "DenoiserService",
22
23
  "KaraokeService",
23
- "CreditService"
24
+ "CreditService",
25
+ "StemExtractionService"
24
26
  ]
@@ -51,15 +51,15 @@ class MusicService(BaseService):
51
51
  if seed is not None and (seed < 0 or seed > 2**32 - 1):
52
52
  raise ValidationError("Seed must be between 0 and 2^32 - 1")
53
53
 
54
- # Prepare request data
54
+ # Prepare request data - FIXED: Use correct parameter names matching API schema
55
55
  data = {
56
56
  "prompt": prompt,
57
- "duration": duration,
57
+ "audio_duration": duration, # FIXED: API expects "audio_duration" not "duration"
58
58
  "guidance_scale": guidance_scale,
59
- "num_inference_steps": num_inference_steps
59
+ "infer_step": num_inference_steps # FIXED: API expects "infer_step" not "num_inference_steps"
60
60
  }
61
61
  if seed is not None:
62
- data["seed"] = seed
62
+ data["manual_seeds"] = [seed] # FIXED: API expects "manual_seeds" list not "seed"
63
63
  if display_name:
64
64
  data["display_name"] = display_name.strip()
65
65
 
@@ -68,7 +68,9 @@ class MusicService(BaseService):
68
68
  return self._async_generate_music(data, wait_for_completion, timeout)
69
69
  else:
70
70
  response = self.client.request("POST", "/api/v1/music/text2music", data=data)
71
- job = Job.from_dict(response)
71
+ # FIXED: Handle response format correctly - API returns {"job": {...}, "message": "..."}
72
+ job_data = response.get("job", response)
73
+ job = Job.from_dict(job_data)
72
74
 
73
75
  if wait_for_completion:
74
76
  completed_job = self._wait_for_completion(job.id, timeout)
@@ -84,7 +86,9 @@ class MusicService(BaseService):
84
86
  ) -> Union[Job, MusicGenerationResult]:
85
87
  """Async version of generate_music"""
86
88
  response = await self.client.request("POST", "/api/v1/music/text2music", data=data)
87
- job = Job.from_dict(response)
89
+ # FIXED: Handle response format correctly
90
+ job_data = response.get("job", response)
91
+ job = Job.from_dict(job_data)
88
92
 
89
93
  if wait_for_completion:
90
94
  completed_job = await self._async_wait_for_completion(job.id, timeout)
@@ -122,11 +126,14 @@ class MusicService(BaseService):
122
126
  if style not in ["modern", "classic", "trap"]:
123
127
  raise ValidationError("Style must be 'modern', 'classic', or 'trap'")
124
128
 
125
- # Prepare request data
129
+ # Prepare request data - FIXED: Match API schema for text2rap
126
130
  data = {
131
+ "prompt": f"rap music, {style} style", # FIXED: API expects "prompt" field
127
132
  "lyrics": lyrics,
128
- "style": style,
129
- "tempo": tempo
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
130
137
  }
131
138
  if display_name:
132
139
  data["display_name"] = display_name.strip()
@@ -136,7 +143,9 @@ class MusicService(BaseService):
136
143
  return self._async_generate_rap(data, wait_for_completion, timeout)
137
144
  else:
138
145
  response = self.client.request("POST", "/api/v1/music/text2rap", data=data)
139
- job = Job.from_dict(response)
146
+ # FIXED: Handle response format correctly
147
+ job_data = response.get("job", response)
148
+ job = Job.from_dict(job_data)
140
149
 
141
150
  if wait_for_completion:
142
151
  completed_job = self._wait_for_completion(job.id, timeout)
@@ -152,7 +161,9 @@ class MusicService(BaseService):
152
161
  ) -> Union[Job, MusicGenerationResult]:
153
162
  """Async version of generate_rap"""
154
163
  response = await self.client.request("POST", "/api/v1/music/text2rap", data=data)
155
- job = Job.from_dict(response)
164
+ # FIXED: Handle response format correctly
165
+ job_data = response.get("job", response)
166
+ job = Job.from_dict(job_data)
156
167
 
157
168
  if wait_for_completion:
158
169
  completed_job = await self._async_wait_for_completion(job.id, timeout)
@@ -194,10 +205,12 @@ class MusicService(BaseService):
194
205
  if tempo is not None and not 60 <= tempo <= 200:
195
206
  raise ValidationError("Tempo must be between 60 and 200 BPM")
196
207
 
197
- # Prepare request data
208
+ # Prepare request data - FIXED: Match API schema for prompt2instrumental
198
209
  data = {
199
210
  "prompt": prompt,
200
- "duration": duration
211
+ "audio_duration": duration, # FIXED: API expects "audio_duration"
212
+ "guidance_scale": 7.5,
213
+ "infer_step": 50
201
214
  }
202
215
  if instruments:
203
216
  data["instruments"] = instruments
@@ -213,7 +226,9 @@ class MusicService(BaseService):
213
226
  return self._async_generate_instrumental(data, wait_for_completion, timeout)
214
227
  else:
215
228
  response = self.client.request("POST", "/api/v1/music/prompt2instrumental", data=data)
216
- job = Job.from_dict(response)
229
+ # FIXED: Handle response format correctly
230
+ job_data = response.get("job", response)
231
+ job = Job.from_dict(job_data)
217
232
 
218
233
  if wait_for_completion:
219
234
  completed_job = self._wait_for_completion(job.id, timeout)
@@ -229,7 +244,80 @@ class MusicService(BaseService):
229
244
  ) -> Union[Job, MusicGenerationResult]:
230
245
  """Async version of generate_instrumental"""
231
246
  response = await self.client.request("POST", "/api/v1/music/prompt2instrumental", data=data)
232
- job = Job.from_dict(response)
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(
258
+ self,
259
+ lyrics: str,
260
+ prompt: str = "vocals",
261
+ duration: float = 120.0,
262
+ display_name: Optional[str] = None,
263
+ 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)
233
321
 
234
322
  if wait_for_completion:
235
323
  completed_job = await self._async_wait_for_completion(job.id, timeout)
@@ -322,7 +410,7 @@ class MusicService(BaseService):
322
410
  """
323
411
  params = {
324
412
  "limit": limit,
325
- "skip": offset
413
+ "skip": offset # FIXED: API uses "skip" parameter for offset
326
414
  }
327
415
  if status:
328
416
  params["status"] = status
@@ -0,0 +1,180 @@
1
+ """
2
+ Stem Extraction Service - Audio stem separation operations
3
+ """
4
+
5
+ from typing import List, Optional, Dict, Any, Union
6
+ from .base import BaseService
7
+ from ..models import Job
8
+ from ..exceptions import ValidationError
9
+
10
+
11
+ class StemExtractionService(BaseService):
12
+ """Service for audio stem extraction operations"""
13
+
14
+ def extract_stems(
15
+ self,
16
+ audio_file: Optional[str] = None,
17
+ url: Optional[str] = None,
18
+ stem_types: List[str] = None,
19
+ model_name: str = "htdemucs",
20
+ two_stems_mode: Optional[str] = None,
21
+ wait_for_completion: bool = False,
22
+ timeout: int = 900
23
+ ) -> Job:
24
+ """
25
+ Extract stems from audio file
26
+
27
+ 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
+
36
+ Returns:
37
+ Job object with stem extraction details
38
+ """
39
+ if not audio_file and not url:
40
+ raise ValidationError("Either audio_file or url must be provided")
41
+
42
+ 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
46
+ 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
+
66
+ if url:
67
+ data["url"] = url
68
+
69
+ if two_stems_mode:
70
+ data["two_stems_mode"] = two_stems_mode
71
+
72
+ 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
+
89
+ async def _async_extract_stems(
90
+ self,
91
+ files: Dict[str, Any],
92
+ data: Dict[str, Any],
93
+ wait_for_completion: bool,
94
+ timeout: int
95
+ ) -> Job:
96
+ """Async version of extract_stems"""
97
+ response = await self.client.request(
98
+ "POST",
99
+ "/api/v1/stem-extraction/extract",
100
+ data=data,
101
+ files=files if files else None
102
+ )
103
+
104
+ job = Job.from_dict(response)
105
+
106
+ 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
+ """
121
+ 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
+
152
+ 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
+ """
173
+ 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"""
180
+ return await self.client.request("DELETE", f"/api/v1/stem-extraction/jobs/{job_id}")
@@ -1,42 +1,59 @@
1
1
  """
2
- Translation Service - Audio/video translation operations
2
+ Translation Service - Speech-to-speech translation operations
3
3
  """
4
4
 
5
5
  from typing import Optional, Union
6
6
  from .base import BaseService
7
7
  from ..models import Job, TranslationResult
8
+ from ..exceptions import ValidationError
8
9
 
9
10
 
10
11
  class TranslationService(BaseService):
11
- """Service for audio and video translation operations"""
12
+ """Service for speech-to-speech translation operations"""
12
13
 
13
14
  def translate_audio(
14
15
  self,
15
- audio_file: str,
16
- target_language: str,
16
+ audio_file: Optional[str] = None,
17
+ url: Optional[str] = None,
18
+ target_language: str = "en",
17
19
  source_language: Optional[str] = None,
18
20
  wait_for_completion: bool = False,
19
21
  timeout: int = 900
20
22
  ) -> Union[Job, TranslationResult]:
21
23
  """
22
- Translate audio to another language
24
+ Translate speech from audio/video file to another language while preserving voice characteristics
23
25
 
24
26
  Args:
25
- audio_file: Path to audio file
26
- target_language: Target language code
27
- source_language: Source language (auto-detect if None)
27
+ audio_file: Path to audio/video file (required if no URL)
28
+ url: Direct media URL (required if no file)
29
+ target_language: Target language code (ISO 639-1, e.g., 'es' for Spanish)
30
+ source_language: Source language code (auto-detect if None)
28
31
  wait_for_completion: Whether to wait for completion
29
32
  timeout: Maximum time to wait
30
33
 
31
34
  Returns:
32
35
  Job object or translation result
33
36
  """
37
+ if not audio_file and not url:
38
+ raise ValidationError("Either audio_file or url must be provided")
39
+
40
+ if audio_file and url:
41
+ raise ValidationError("Provide either audio_file or url, not both")
42
+
34
43
  target_language = self._validate_language_code(target_language)
35
44
  if source_language:
36
45
  source_language = self._validate_language_code(source_language)
37
46
 
38
- files = self._prepare_file_upload(audio_file, "file")
47
+ # Prepare request data
48
+ files = {}
39
49
  data = {"target_language": target_language}
50
+
51
+ if audio_file:
52
+ files = self._prepare_file_upload(audio_file, "file")
53
+
54
+ if url:
55
+ data["url"] = url
56
+
40
57
  if source_language:
41
58
  data["source_language"] = source_language
42
59
 
@@ -44,7 +61,10 @@ class TranslationService(BaseService):
44
61
  return self._async_translate_audio(files, data, wait_for_completion, timeout)
45
62
  else:
46
63
  response = self.client.request(
47
- "POST", "/api/v1/translation/translate", data=data, files=files
64
+ "POST",
65
+ "/api/v1/translation/translate/speech", # FIXED: Use correct speech-to-speech endpoint
66
+ data=data,
67
+ files=files if files else None
48
68
  )
49
69
  job = Job.from_dict(response)
50
70
 
@@ -53,11 +73,35 @@ class TranslationService(BaseService):
53
73
  return TranslationResult.from_dict(completed_job.result or completed_job.__dict__)
54
74
 
55
75
  return job
76
+
77
+ def translate_speech(
78
+ self,
79
+ audio_file: Optional[str] = None,
80
+ url: Optional[str] = None,
81
+ target_language: str = "en",
82
+ source_language: Optional[str] = None,
83
+ wait_for_completion: bool = False,
84
+ timeout: int = 900
85
+ ) -> Union[Job, TranslationResult]:
86
+ """
87
+ Alias for translate_audio - more descriptive method name for speech translation
88
+ """
89
+ return self.translate_audio(
90
+ audio_file=audio_file,
91
+ url=url,
92
+ target_language=target_language,
93
+ source_language=source_language,
94
+ wait_for_completion=wait_for_completion,
95
+ timeout=timeout
96
+ )
56
97
 
57
98
  async def _async_translate_audio(self, files, data, wait_for_completion, timeout):
58
99
  """Async version of translate_audio"""
59
100
  response = await self.client.request(
60
- "POST", "/api/v1/translation/translate", data=data, files=files
101
+ "POST",
102
+ "/api/v1/translation/translate/speech", # FIXED: Use correct speech-to-speech endpoint
103
+ data=data,
104
+ files=files if files else None
61
105
  )
62
106
  job = Job.from_dict(response)
63
107
 
@@ -79,3 +123,74 @@ class TranslationService(BaseService):
79
123
  """Async version of get_translation_job"""
80
124
  response = await self.client.request("GET", f"/api/v1/translation/translations/{job_id}")
81
125
  return TranslationResult.from_dict(response)
126
+
127
+ def list_translation_jobs(
128
+ self,
129
+ skip: int = 0,
130
+ limit: int = 50
131
+ ) -> list:
132
+ """
133
+ List translation jobs
134
+
135
+ Args:
136
+ skip: Number of jobs to skip (pagination offset)
137
+ limit: Maximum number of jobs to return (max 100)
138
+
139
+ Returns:
140
+ List of translation jobs
141
+ """
142
+ params = {
143
+ "skip": skip,
144
+ "limit": min(limit, 100) # API max is 100
145
+ }
146
+
147
+ if self.async_mode:
148
+ return self._async_list_translation_jobs(params)
149
+ else:
150
+ response = self.client.request("GET", "/api/v1/translation/translations", params=params)
151
+ return [TranslationResult.from_dict(job_data) for job_data in response]
152
+
153
+ async def _async_list_translation_jobs(self, params: dict) -> list:
154
+ """Async version of list_translation_jobs"""
155
+ response = await self.client.request("GET", "/api/v1/translation/translations", params=params)
156
+ return [TranslationResult.from_dict(job_data) for job_data in response]
157
+
158
+ def retry_translation(self, job_id: int) -> Job:
159
+ """
160
+ Retry a failed translation job
161
+
162
+ Args:
163
+ job_id: ID of the failed translation job to retry
164
+
165
+ Returns:
166
+ New job object for the retry attempt
167
+ """
168
+ if self.async_mode:
169
+ return self._async_retry_translation(job_id)
170
+ else:
171
+ response = self.client.request("POST", f"/api/v1/translation/translations/{job_id}/retry")
172
+ return Job.from_dict(response)
173
+
174
+ async def _async_retry_translation(self, job_id: int) -> Job:
175
+ """Async version of retry_translation"""
176
+ response = await self.client.request("POST", f"/api/v1/translation/translations/{job_id}/retry")
177
+ return Job.from_dict(response)
178
+
179
+ def delete_translation_job(self, job_id: int) -> dict:
180
+ """
181
+ Delete a translation job
182
+
183
+ Args:
184
+ job_id: ID of the translation job to delete
185
+
186
+ Returns:
187
+ Deletion confirmation
188
+ """
189
+ if self.async_mode:
190
+ return self._async_delete_translation_job(job_id)
191
+ else:
192
+ return self.client.request("DELETE", f"/api/v1/translation/translations/{job_id}")
193
+
194
+ async def _async_delete_translation_job(self, job_id: int) -> dict:
195
+ """Async version of delete_translation_job"""
196
+ return await self.client.request("DELETE", f"/api/v1/translation/translations/{job_id}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: audiopod
3
- Version: 1.0.0
3
+ Version: 1.1.1
4
4
  Summary: Professional Audio Processing API Client for Python
5
5
  Home-page: https://github.com/audiopod-ai/audiopod-python
6
6
  Author: AudioPod AI
@@ -25,6 +25,7 @@ Classifier: Topic :: Multimedia :: Sound/Audio :: Conversion
25
25
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
26
26
  Requires-Python: >=3.8
27
27
  Description-Content-Type: text/markdown
28
+ License-File: LICENSE
28
29
  Requires-Dist: requests>=2.28.0
29
30
  Requires-Dist: aiohttp>=3.8.0
30
31
  Requires-Dist: pydantic>=1.10.0
@@ -46,6 +47,7 @@ Requires-Dist: sphinx-rtd-theme>=1.2.0; extra == "docs"
46
47
  Requires-Dist: sphinx-autodoc-typehints>=1.19.0; extra == "docs"
47
48
  Dynamic: author
48
49
  Dynamic: home-page
50
+ Dynamic: license-file
49
51
  Dynamic: requires-python
50
52
 
51
53
  # AudioPod Python SDK
@@ -140,17 +142,25 @@ print(f"Transcript: {transcript.transcript}")
140
142
  print(f"Detected {len(transcript.segments)} speakers")
141
143
  ```
142
144
 
143
- #### Audio Translation
145
+ #### Speech-to-Speech Translation
144
146
 
145
147
  ```python
146
- # Translate audio to another language
147
- translation = client.translation.translate_audio(
148
+ # Translate speech while preserving voice characteristics
149
+ translation = client.translation.translate_speech(
148
150
  audio_file="path/to/english_audio.wav",
149
151
  target_language="es", # Spanish
152
+ source_language="en", # English (optional - auto-detect)
150
153
  wait_for_completion=True
151
154
  )
152
155
 
153
- print(f"Translated audio URL: {translation.audio_output_url}")
156
+ print(f"Translated audio URL: {translation.translated_audio_url}")
157
+
158
+ # Or translate from URL
159
+ url_translation = client.translation.translate_speech(
160
+ url="https://example.com/audio.mp3",
161
+ target_language="fr", # French
162
+ wait_for_completion=True
163
+ )
154
164
  ```
155
165
 
156
166
  ### Async Support
@@ -380,11 +390,10 @@ audiopod transcription transcribe audio.mp3 --language en
380
390
 
381
391
  ## Support
382
392
 
383
- - 📖 [Documentation](https://docs.audiopod.ai)
384
- - 🎯 [API Reference](https://api.audiopod.ai/docs)
393
+ - 📖 [API Reference](https://docs.audiopod.ai)
385
394
  - 💬 [Discord Community](https://discord.gg/audiopod)
386
395
  - 📧 [Email Support](mailto:support@audiopod.ai)
387
- - 🐛 [Bug Reports](https://github.com/audiopod-ai/audiopod-python/issues)
396
+ - 🐛 [Bug Reports](https://github.com/AudiopodAI/audiopod)
388
397
 
389
398
  ## License
390
399
 
@@ -0,0 +1,24 @@
1
+ audiopod/__init__.py,sha256=UsB5ET6nUy1Upx8wCiB17mMErdF3RvEHfAr51-pPPGQ,1790
2
+ audiopod/cli.py,sha256=ZYzAQ3UpoYuOEWivMwMneJUf2z8DGGYTx1Nb6yRfdVY,9339
3
+ audiopod/client.py,sha256=67oPSInSNssJpTR00ZuYSdk9lbx5KiRnDQw8UYKNVsA,11742
4
+ audiopod/config.py,sha256=fuGtbuES4tXdHwqQqoZa5izCH6nVfFRP06D8eK1Cg10,1683
5
+ audiopod/exceptions.py,sha256=c3Ym2tWyRE1kemVkXDaXFcfP3h6AokhKcUcCBImwGes,2386
6
+ audiopod/models.py,sha256=R70iMqKDZfLtTB9FQ7KrFLBi-bFA5-FrS-5eMOtfK1o,8517
7
+ audiopod/py.typed,sha256=ixa8YukDZ3kLo0WsFJRGohLMyHzbMur1ALmmASML2cs,64
8
+ audiopod/services/__init__.py,sha256=9Ycl9VVscwwY42joBCSL67v8DrITW2T2QyuaokbpehM,653
9
+ audiopod/services/base.py,sha256=mNbziYy2KsWWZrdHFlTl9pKLvoQUW-ZkkJuVWfCVP74,6731
10
+ audiopod/services/credits.py,sha256=CjE2W8kmYjM4B02BwfPvvB3kl0YdVimM2kR7lhg43h8,1796
11
+ audiopod/services/denoiser.py,sha256=Oi_1rCKs_L5lCG_2QuKwHzLe8_mbEIzlzM2X8TJ7Rnc,1750
12
+ audiopod/services/karaoke.py,sha256=M0Befh4Y2DNM_uWFTmCIrzKfjqkvygrzCt0f_Dubj2o,2012
13
+ audiopod/services/music.py,sha256=hDPjTSj-gAeEWBVB7PRPQrptMHJyQTz8e9p-p7yaRPI,20032
14
+ audiopod/services/speaker.py,sha256=OPSOwArfrGXVzRgciS13n1QsCJSK1PB-Mz6VgwxuHAA,1866
15
+ audiopod/services/stem_extraction.py,sha256=3ibMFKFR25xKHpVs3WGMNriZ88sB5PriFNa_s2Bvon4,6026
16
+ audiopod/services/transcription.py,sha256=HyH6WpGWZsggYxIvt2dhB6_5UHaigk3XwXsVgarWzcE,7565
17
+ audiopod/services/translation.py,sha256=oUU82c61CeAt13lzlWx8S-9xEgYlskwX8bLMbQw2Ni8,7396
18
+ audiopod/services/voice.py,sha256=_IHv3zU3k184kfijxr1QRBenrIpmhhPOBS96DddZ8yw,13456
19
+ audiopod-1.1.1.dist-info/licenses/LICENSE,sha256=hqEjnOaGNbnLSBxbtbC7WQVREU2vQI8FmwecCiZlMfA,1068
20
+ audiopod-1.1.1.dist-info/METADATA,sha256=UG_csntfzscrZTjy-2v-lCTlS7-VSEM6WEwUw26hSYc,11217
21
+ audiopod-1.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
22
+ audiopod-1.1.1.dist-info/entry_points.txt,sha256=uLcNDzXuOXnJAz9j91TDGayVjjZ7-ZiHBGDydqNUErU,47
23
+ audiopod-1.1.1.dist-info/top_level.txt,sha256=M6yyOFFNpLdH4i1AMRqJZLRIgfpg1NvrQVmnPd8A6N8,9
24
+ audiopod-1.1.1.dist-info/RECORD,,
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 AudioPod AI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -1,21 +0,0 @@
1
- audiopod/__init__.py,sha256=GyQ7PUWGF3t4Ljuk_DD6O9MvG_A96T5bedXc3jJQCD4,1790
2
- audiopod/cli.py,sha256=ZYzAQ3UpoYuOEWivMwMneJUf2z8DGGYTx1Nb6yRfdVY,9339
3
- audiopod/client.py,sha256=AF5goB5CG5lJZ3XyGojRskvUNrNzn9A34BAx9Z1sEOE,11580
4
- audiopod/config.py,sha256=fuGtbuES4tXdHwqQqoZa5izCH6nVfFRP06D8eK1Cg10,1683
5
- audiopod/exceptions.py,sha256=c3Ym2tWyRE1kemVkXDaXFcfP3h6AokhKcUcCBImwGes,2386
6
- audiopod/models.py,sha256=gAfQkufA_hZdYBkniFJ_EXETy9ts-wr-soUEbT7ZKFM,7827
7
- audiopod/services/__init__.py,sha256=rt_9CHqmEJgHyVFdF2WwTE403CXpDGBCYzfCxJZZjgE,573
8
- audiopod/services/base.py,sha256=mNbziYy2KsWWZrdHFlTl9pKLvoQUW-ZkkJuVWfCVP74,6731
9
- audiopod/services/credits.py,sha256=CjE2W8kmYjM4B02BwfPvvB3kl0YdVimM2kR7lhg43h8,1796
10
- audiopod/services/denoiser.py,sha256=Oi_1rCKs_L5lCG_2QuKwHzLe8_mbEIzlzM2X8TJ7Rnc,1750
11
- audiopod/services/karaoke.py,sha256=M0Befh4Y2DNM_uWFTmCIrzKfjqkvygrzCt0f_Dubj2o,2012
12
- audiopod/services/music.py,sha256=m6cF_2cbB4X-BHvJH36oqD2NBF_dIWCAUB2HIpZgoxo,15948
13
- audiopod/services/speaker.py,sha256=OPSOwArfrGXVzRgciS13n1QsCJSK1PB-Mz6VgwxuHAA,1866
14
- audiopod/services/transcription.py,sha256=HyH6WpGWZsggYxIvt2dhB6_5UHaigk3XwXsVgarWzcE,7565
15
- audiopod/services/translation.py,sha256=Gpxmom-ZSLMXBwSQlOL5PyqxQCwRYNT2IUp2ZD5QhYc,3177
16
- audiopod/services/voice.py,sha256=_IHv3zU3k184kfijxr1QRBenrIpmhhPOBS96DddZ8yw,13456
17
- audiopod-1.0.0.dist-info/METADATA,sha256=S7iD1nLEk-xXknM1K8sze0ucL2oVGtxcXpvtIFRGtTY,10956
18
- audiopod-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
19
- audiopod-1.0.0.dist-info/entry_points.txt,sha256=uLcNDzXuOXnJAz9j91TDGayVjjZ7-ZiHBGDydqNUErU,47
20
- audiopod-1.0.0.dist-info/top_level.txt,sha256=M6yyOFFNpLdH4i1AMRqJZLRIgfpg1NvrQVmnPd8A6N8,9
21
- audiopod-1.0.0.dist-info/RECORD,,