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,196 +1,135 @@
1
1
  """
2
- Translation Service - Speech-to-speech translation operations
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
3
9
  """
4
10
 
5
- from typing import Optional, Union
11
+ from typing import Optional, Dict, Any, List
6
12
  from .base import BaseService
7
- from ..models import Job, TranslationResult
8
- from ..exceptions import ValidationError
9
13
 
10
14
 
11
15
  class TranslationService(BaseService):
12
- """Service for speech-to-speech translation operations"""
13
-
14
- def translate_audio(
16
+ """Service for audio translation."""
17
+
18
+ def translate(
15
19
  self,
16
20
  audio_file: Optional[str] = None,
17
21
  url: Optional[str] = None,
18
22
  target_language: str = "en",
19
23
  source_language: Optional[str] = None,
20
24
  wait_for_completion: bool = False,
21
- timeout: int = 900
22
- ) -> Union[Job, TranslationResult]:
25
+ timeout: int = 900,
26
+ ) -> Dict[str, Any]:
23
27
  """
24
- Translate speech from audio/video file to another language while preserving voice characteristics
25
-
28
+ Translate audio to another language.
29
+
26
30
  Args:
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)
31
- wait_for_completion: Whether to wait for completion
32
- timeout: Maximum time to wait
33
-
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
+
34
38
  Returns:
35
- Job object or translation result
39
+ Job dict with translated audio URL when completed
36
40
  """
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
-
43
- target_language = self._validate_language_code(target_language)
44
- if source_language:
45
- source_language = self._validate_language_code(source_language)
46
-
47
- # Prepare request data
48
- files = {}
49
41
  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
-
57
42
  if source_language:
58
43
  data["source_language"] = source_language
59
-
44
+ if url:
45
+ data["url"] = url
46
+
47
+ files = self._prepare_file_upload(audio_file, "file") if audio_file else None
48
+
60
49
  if self.async_mode:
61
- return self._async_translate_audio(files, data, wait_for_completion, timeout)
62
- else:
63
- response = self.client.request(
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
68
- )
69
- job = Job.from_dict(response)
70
-
71
- if wait_for_completion:
72
- completed_job = self._wait_for_completion(job.id, timeout)
73
- return TranslationResult.from_dict(completed_job.result or completed_job.__dict__)
74
-
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
- )
97
-
98
- async def _async_translate_audio(self, files, data, wait_for_completion, timeout):
99
- """Async version of translate_audio"""
100
- response = await self.client.request(
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
105
- )
106
- job = Job.from_dict(response)
107
-
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
+
108
54
  if wait_for_completion:
109
- completed_job = await self._async_wait_for_completion(job.id, timeout)
110
- return TranslationResult.from_dict(completed_job.result or completed_job.__dict__)
111
-
112
- return job
113
-
114
- def get_translation_job(self, job_id: int) -> TranslationResult:
115
- """Get translation job details"""
116
- if self.async_mode:
117
- return self._async_get_translation_job(job_id)
118
- else:
119
- response = self.client.request("GET", f"/api/v1/translation/translations/{job_id}")
120
- return TranslationResult.from_dict(response)
121
-
122
- async def _async_get_translation_job(self, job_id: int) -> TranslationResult:
123
- """Async version of get_translation_job"""
124
- response = await self.client.request("GET", f"/api/v1/translation/translations/{job_id}")
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
-
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."""
147
70
  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
- """
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."""
168
79
  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
- """
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."""
189
92
  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"""
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]:
196
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
+
@@ -0,0 +1,329 @@
1
+ """
2
+ Video Service - Video generation
3
+
4
+ API Routes:
5
+ - POST /api/v2/video/karaoke - Create karaoke video
6
+ - POST /api/v2/video/music-video - Create AI music video
7
+ - POST /api/v2/video/lyric-video - Create lyric video
8
+ - GET /api/v2/video/{id} - Get job status
9
+ - GET /api/v2/video/{id}/download - Get download URLs
10
+ - DELETE /api/v2/video/{id} - Delete job
11
+ """
12
+
13
+ from typing import Optional, Dict, Any, List, Literal
14
+ from .base import BaseService
15
+
16
+
17
+ VideoType = Literal["karaoke", "music_video", "lyric_video"]
18
+ AspectRatio = Literal["16:9", "9:16", "1:1"]
19
+ SceneGeneration = Literal["flux", "veo3", "static"]
20
+ AnimationStyle = Literal["typewriter", "fade-word", "slide-up", "bounce", "wave", "zoom"]
21
+
22
+
23
+ class VideoService(BaseService):
24
+ """Service for video generation."""
25
+
26
+ def create_karaoke(
27
+ self,
28
+ audio_file: Optional[str] = None,
29
+ url: Optional[str] = None,
30
+ aspect_ratio: AspectRatio = "16:9",
31
+ scene_generation: SceneGeneration = "flux",
32
+ key_shift: int = 0,
33
+ style: Optional[Dict[str, Any]] = None,
34
+ webhook_url: Optional[str] = None,
35
+ wait_for_completion: bool = False,
36
+ timeout: int = 600,
37
+ ) -> Dict[str, Any]:
38
+ """
39
+ Create a karaoke video from audio.
40
+
41
+ Args:
42
+ audio_file: Path to local audio file
43
+ url: URL of audio/video (YouTube, etc.)
44
+ aspect_ratio: Video aspect ratio
45
+ scene_generation: Background generation method
46
+ key_shift: Pitch shift in semitones (-12 to 12)
47
+ style: Style configuration (colors, font, etc.)
48
+ webhook_url: URL for completion notification
49
+ wait_for_completion: Wait for video to finish
50
+ timeout: Max wait time in seconds
51
+
52
+ Returns:
53
+ Job dict with id, status, video_url (when completed)
54
+ """
55
+ if audio_file:
56
+ return self._create_from_upload(
57
+ "karaoke", audio_file, aspect_ratio, scene_generation,
58
+ key_shift, webhook_url, wait_for_completion, timeout
59
+ )
60
+
61
+ data = {
62
+ "audio_url": url,
63
+ "aspect_ratio": aspect_ratio,
64
+ "scene_generation": scene_generation,
65
+ "key_shift": key_shift,
66
+ }
67
+ if style:
68
+ data["style"] = style
69
+ if webhook_url:
70
+ data["webhook_url"] = webhook_url
71
+
72
+ if self.async_mode:
73
+ return self._async_create("karaoke", data, wait_for_completion, timeout)
74
+
75
+ response = self.client.request("POST", "/api/v2/video/karaoke", json_data=data)
76
+
77
+ if wait_for_completion:
78
+ return self._wait_for_video(response["id"], timeout)
79
+ return response
80
+
81
+ def create_music_video(
82
+ self,
83
+ audio_file: Optional[str] = None,
84
+ url: Optional[str] = None,
85
+ music_job_id: Optional[str] = None,
86
+ aspect_ratio: AspectRatio = "16:9",
87
+ scene_generation: SceneGeneration = "flux",
88
+ scene_count: int = 10,
89
+ include_lyrics: bool = True,
90
+ style: Optional[Dict[str, Any]] = None,
91
+ webhook_url: Optional[str] = None,
92
+ wait_for_completion: bool = False,
93
+ timeout: int = 900,
94
+ ) -> Dict[str, Any]:
95
+ """
96
+ Create an AI music video.
97
+
98
+ Args:
99
+ audio_file: Path to local audio file
100
+ url: URL of audio file
101
+ music_job_id: AudioPod music generation job ID
102
+ aspect_ratio: Video aspect ratio
103
+ scene_generation: Scene generation method (flux or veo3)
104
+ scene_count: Number of scenes to generate (5-20)
105
+ include_lyrics: Overlay lyrics on video
106
+ style: Style configuration
107
+ webhook_url: URL for completion notification
108
+ wait_for_completion: Wait for video to finish
109
+ timeout: Max wait time in seconds
110
+
111
+ Returns:
112
+ Job dict with video_url when completed
113
+ """
114
+ data = {
115
+ "aspect_ratio": aspect_ratio,
116
+ "scene_generation": scene_generation,
117
+ "scene_count": scene_count,
118
+ "include_lyrics": include_lyrics,
119
+ }
120
+ if url:
121
+ data["audio_url"] = url
122
+ if music_job_id:
123
+ data["music_job_id"] = music_job_id
124
+ if style:
125
+ data["style"] = style
126
+ if webhook_url:
127
+ data["webhook_url"] = webhook_url
128
+
129
+ if self.async_mode:
130
+ return self._async_create("music-video", data, wait_for_completion, timeout)
131
+
132
+ response = self.client.request("POST", "/api/v2/video/music-video", json_data=data)
133
+
134
+ if wait_for_completion:
135
+ return self._wait_for_video(response["id"], timeout)
136
+ return response
137
+
138
+ def create_lyric_video(
139
+ self,
140
+ audio_file: Optional[str] = None,
141
+ url: Optional[str] = None,
142
+ lyrics: Optional[str] = None,
143
+ aspect_ratio: AspectRatio = "16:9",
144
+ animation_style: AnimationStyle = "fade-word",
145
+ background_color: str = "#1a1a2e",
146
+ accent_color: str = "#e94560",
147
+ particles_enabled: bool = True,
148
+ background_image_url: Optional[str] = None,
149
+ webhook_url: Optional[str] = None,
150
+ wait_for_completion: bool = False,
151
+ timeout: int = 600,
152
+ ) -> Dict[str, Any]:
153
+ """
154
+ Create a kinetic typography lyric video.
155
+
156
+ Args:
157
+ audio_file: Path to local audio file
158
+ url: URL of audio file
159
+ lyrics: Custom lyrics (auto-transcribed if not provided)
160
+ aspect_ratio: Video aspect ratio
161
+ animation_style: Text animation style
162
+ background_color: Background color
163
+ accent_color: Accent/highlight color
164
+ particles_enabled: Enable particle effects
165
+ background_image_url: Optional background image
166
+ webhook_url: URL for completion notification
167
+ wait_for_completion: Wait for video to finish
168
+ timeout: Max wait time in seconds
169
+
170
+ Returns:
171
+ Job dict with video_url when completed
172
+ """
173
+ data = {
174
+ "aspect_ratio": aspect_ratio,
175
+ "style": {
176
+ "animation_style": animation_style,
177
+ "background_color": background_color,
178
+ "accent_color": accent_color,
179
+ "particles_enabled": particles_enabled,
180
+ },
181
+ }
182
+ if url:
183
+ data["audio_url"] = url
184
+ if lyrics:
185
+ data["lyrics"] = lyrics
186
+ if background_image_url:
187
+ data["background_image_url"] = background_image_url
188
+ if webhook_url:
189
+ data["webhook_url"] = webhook_url
190
+
191
+ if self.async_mode:
192
+ return self._async_create("lyric-video", data, wait_for_completion, timeout)
193
+
194
+ response = self.client.request("POST", "/api/v2/video/lyric-video", json_data=data)
195
+
196
+ if wait_for_completion:
197
+ return self._wait_for_video(response["id"], timeout)
198
+ return response
199
+
200
+ def get_job(self, job_id: str) -> Dict[str, Any]:
201
+ """Get video job status."""
202
+ if self.async_mode:
203
+ return self._async_get_job(job_id)
204
+ return self.client.request("GET", f"/api/v2/video/{job_id}")
205
+
206
+ async def _async_get_job(self, job_id: str) -> Dict[str, Any]:
207
+ return await self.client.request("GET", f"/api/v2/video/{job_id}")
208
+
209
+ def get_download_urls(self, job_id: str) -> Dict[str, Any]:
210
+ """Get download URLs for completed video."""
211
+ if self.async_mode:
212
+ return self._async_get_download(job_id)
213
+ return self.client.request("GET", f"/api/v2/video/{job_id}/download")
214
+
215
+ async def _async_get_download(self, job_id: str) -> Dict[str, Any]:
216
+ return await self.client.request("GET", f"/api/v2/video/{job_id}/download")
217
+
218
+ def delete_job(self, job_id: str) -> Dict[str, str]:
219
+ """Delete a video job."""
220
+ if self.async_mode:
221
+ return self._async_delete(job_id)
222
+ return self.client.request("DELETE", f"/api/v2/video/{job_id}")
223
+
224
+ async def _async_delete(self, job_id: str) -> Dict[str, str]:
225
+ return await self.client.request("DELETE", f"/api/v2/video/{job_id}")
226
+
227
+ def list_jobs(
228
+ self,
229
+ video_type: Optional[VideoType] = None,
230
+ limit: int = 20,
231
+ offset: int = 0,
232
+ ) -> Dict[str, Any]:
233
+ """List video jobs."""
234
+ params = {"limit": limit, "offset": offset}
235
+ if video_type:
236
+ params["type"] = video_type
237
+
238
+ if self.async_mode:
239
+ return self._async_list(params)
240
+ return self.client.request("GET", "/api/v2/video/", params=params)
241
+
242
+ async def _async_list(self, params: Dict) -> Dict[str, Any]:
243
+ return await self.client.request("GET", "/api/v2/video/", params=params)
244
+
245
+ def _create_from_upload(
246
+ self,
247
+ video_type: str,
248
+ audio_file: str,
249
+ aspect_ratio: str,
250
+ scene_generation: str,
251
+ key_shift: int,
252
+ webhook_url: Optional[str],
253
+ wait_for_completion: bool,
254
+ timeout: int,
255
+ ) -> Dict[str, Any]:
256
+ """Create video from file upload."""
257
+ data = {
258
+ "aspect_ratio": aspect_ratio,
259
+ "scene_generation": scene_generation,
260
+ "key_shift": str(key_shift),
261
+ }
262
+ if webhook_url:
263
+ data["webhook_url"] = webhook_url
264
+
265
+ files = self._prepare_file_upload(audio_file, "file")
266
+
267
+ response = self.client.request(
268
+ "POST", f"/api/v2/video/{video_type}/upload", data=data, files=files
269
+ )
270
+
271
+ if wait_for_completion:
272
+ return self._wait_for_video(response["id"], timeout)
273
+ return response
274
+
275
+ async def _async_create(
276
+ self,
277
+ endpoint: str,
278
+ data: Dict,
279
+ wait_for_completion: bool,
280
+ timeout: int,
281
+ ) -> Dict[str, Any]:
282
+ response = await self.client.request(
283
+ "POST", f"/api/v2/video/{endpoint}", json_data=data
284
+ )
285
+ if wait_for_completion:
286
+ return await self._async_wait_for_video(response["id"], timeout)
287
+ return response
288
+
289
+ def _wait_for_video(self, job_id: str, timeout: int) -> Dict[str, Any]:
290
+ """Wait for video job completion."""
291
+ import time
292
+ start = time.time()
293
+
294
+ while time.time() - start < timeout:
295
+ job = self.get_job(job_id)
296
+ status = job.get("status", "")
297
+
298
+ if status == "completed":
299
+ # Get download URLs
300
+ download = self.get_download_urls(job_id)
301
+ job.update(download)
302
+ return job
303
+ elif status == "failed":
304
+ raise Exception(f"Video generation failed: {job.get('error', 'Unknown')}")
305
+
306
+ time.sleep(5)
307
+
308
+ raise TimeoutError(f"Video {job_id} timed out after {timeout}s")
309
+
310
+ async def _async_wait_for_video(self, job_id: str, timeout: int) -> Dict[str, Any]:
311
+ """Async wait for video job completion."""
312
+ import asyncio
313
+ import time
314
+ start = time.time()
315
+
316
+ while time.time() - start < timeout:
317
+ job = await self.get_job(job_id)
318
+ status = job.get("status", "")
319
+
320
+ if status == "completed":
321
+ download = await self.get_download_urls(job_id)
322
+ job.update(download)
323
+ return job
324
+ elif status == "failed":
325
+ raise Exception(f"Video generation failed: {job.get('error', 'Unknown')}")
326
+
327
+ await asyncio.sleep(5)
328
+
329
+ raise TimeoutError(f"Video {job_id} timed out after {timeout}s")