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.
@@ -1,376 +1,187 @@
1
1
  """
2
- Voice Service - Voice cloning and TTS operations
3
- """
2
+ Voice Service - Voice cloning and text-to-speech
4
3
 
5
- from typing import List, Optional, Dict, Any, Union
6
- from pathlib import Path
4
+ API Routes:
5
+ - GET /api/v1/voice/voice-profiles - List all voices
6
+ - GET /api/v1/voice/voices/{id}/status - Get voice details
7
+ - POST /api/v1/voice/voice-profiles - Create voice clone
8
+ - DELETE /api/v1/voice/voices/{id} - Delete voice
9
+ - POST /api/v1/voice/voices/{id}/generate - Generate TTS
10
+ - GET /api/v1/voice/tts-jobs/{id}/status - Get TTS job status
11
+ """
7
12
 
13
+ from typing import Optional, Dict, Any, List, Union
8
14
  from .base import BaseService
9
- from ..models import Job, VoiceProfile, JobStatus
10
- from ..exceptions import ValidationError
11
15
 
12
16
 
13
17
  class VoiceService(BaseService):
14
- """Service for voice cloning and text-to-speech operations"""
15
-
16
- def clone_voice(
18
+ """Service for voice cloning and text-to-speech."""
19
+
20
+ def list_voices(
17
21
  self,
18
- voice_file: str,
19
- text: str,
20
- language: Optional[str] = None,
21
- speed: float = 1.0,
22
- wait_for_completion: bool = False,
23
- timeout: int = 300
24
- ) -> Union[Job, Dict[str, Any]]:
25
- """
26
- Clone a voice from an audio file
27
-
28
- Args:
29
- voice_file: Path to audio file containing voice to clone
30
- text: Text to generate with the cloned voice
31
- language: Target language code (e.g., 'en', 'es')
32
- speed: Speech speed (0.5 to 2.0)
33
- wait_for_completion: Whether to wait for job completion
34
- timeout: Maximum time to wait if wait_for_completion=True
35
-
36
- Returns:
37
- Job object if wait_for_completion=False, otherwise job result
38
- """
39
- # Validate inputs
40
- text = self._validate_text_input(text)
41
- if language:
42
- language = self._validate_language_code(language)
43
- if not 0.5 <= speed <= 2.0:
44
- raise ValidationError("Speed must be between 0.5 and 2.0")
45
-
46
- # Prepare file upload
47
- files = self._prepare_file_upload(voice_file, "file")
48
-
49
- # Prepare form data
50
- data = {
51
- "input_text": text,
52
- "speed": speed
22
+ skip: int = 0,
23
+ limit: int = 100,
24
+ include_public: bool = True,
25
+ ) -> List[Dict[str, Any]]:
26
+ """List available voices (both custom and public)."""
27
+ params = {
28
+ "skip": skip,
29
+ "limit": limit,
30
+ "include_public": str(include_public).lower(),
53
31
  }
54
- if language:
55
- data["target_language"] = language
56
-
57
- # Make request
58
32
  if self.async_mode:
59
- return self._async_clone_voice(files, data, wait_for_completion, timeout)
60
- else:
61
- response = self.client.request(
62
- "POST",
63
- "/api/v1/voice/voice-clone",
64
- data=data,
65
- files=files
66
- )
67
-
68
- job = Job.from_dict(response)
69
-
70
- if wait_for_completion:
71
- job = self._wait_for_completion(job.id, timeout)
72
- return job.result if job.result else job
73
-
74
- return job
75
-
76
- async def _async_clone_voice(
77
- self,
78
- files: Dict[str, Any],
79
- data: Dict[str, Any],
80
- wait_for_completion: bool,
81
- timeout: int
82
- ) -> Union[Job, Dict[str, Any]]:
83
- """Async version of clone_voice"""
84
- response = await self.client.request(
85
- "POST",
86
- "/api/v1/voice/voice-clone",
87
- data=data,
88
- files=files
89
- )
90
-
91
- job = Job.from_dict(response)
92
-
93
- if wait_for_completion:
94
- job = await self._async_wait_for_completion(job.id, timeout)
95
- return job.result if job.result else job
96
-
97
- return job
98
-
99
- def create_voice_profile(
33
+ return self._async_list_voices(params)
34
+ return self.client.request("GET", "/api/v1/voice/voice-profiles", params=params)
35
+
36
+ async def _async_list_voices(self, params: Dict) -> List[Dict[str, Any]]:
37
+ return await self.client.request("GET", "/api/v1/voice/voice-profiles", params=params)
38
+
39
+ def get_voice(self, voice_id: Union[int, str]) -> Dict[str, Any]:
40
+ """Get voice details by ID or UUID."""
41
+ if self.async_mode:
42
+ return self._async_get_voice(voice_id)
43
+ return self.client.request("GET", f"/api/v1/voice/voices/{voice_id}/status")
44
+
45
+ async def _async_get_voice(self, voice_id: Union[int, str]) -> Dict[str, Any]:
46
+ return await self.client.request("GET", f"/api/v1/voice/voices/{voice_id}/status")
47
+
48
+ def create_voice(
100
49
  self,
101
50
  name: str,
102
- voice_file: str,
51
+ audio_file: str,
103
52
  description: Optional[str] = None,
104
- is_public: bool = False,
105
- wait_for_completion: bool = False,
106
- timeout: int = 600
107
- ) -> Union[Job, VoiceProfile]:
108
- """
109
- Create a reusable voice profile
110
-
111
- Args:
112
- name: Name for the voice profile
113
- voice_file: Path to audio file containing voice sample
114
- description: Optional description
115
- is_public: Whether to make the voice profile public
116
- wait_for_completion: Whether to wait for processing completion
117
- timeout: Maximum time to wait if wait_for_completion=True
118
-
119
- Returns:
120
- Job object if wait_for_completion=False, otherwise VoiceProfile
121
- """
122
- # Validate inputs
123
- if not name or len(name.strip()) < 1:
124
- raise ValidationError("Voice profile name cannot be empty")
125
- if len(name) > 100:
126
- raise ValidationError("Voice profile name too long (max 100 characters)")
127
-
128
- # Prepare file upload
129
- files = self._prepare_file_upload(voice_file, "file")
130
-
131
- # Prepare form data
132
- data = {
133
- "name": name.strip(),
134
- "is_public": is_public
135
- }
53
+ ) -> Dict[str, Any]:
54
+ """Create a new voice clone from an audio file."""
55
+ files = self._prepare_file_upload(audio_file, "file")
56
+ data = {"name": name}
136
57
  if description:
137
- data["description"] = description.strip()
138
-
139
- # Make request
58
+ data["description"] = description
59
+
140
60
  if self.async_mode:
141
- return self._async_create_voice_profile(files, data, wait_for_completion, timeout)
142
- else:
143
- response = self.client.request(
144
- "POST",
145
- "/api/v1/voice/voice-profiles",
146
- data=data,
147
- files=files
148
- )
149
-
150
- if wait_for_completion:
151
- voice_id = response["id"]
152
- # Poll for completion
153
- import time
154
- start_time = time.time()
155
- while time.time() - start_time < timeout:
156
- voice_data = self.client.request("GET", f"/api/v1/voice/voice-profiles/{voice_id}")
157
- if voice_data["status"] == "completed":
158
- return VoiceProfile.from_dict(voice_data)
159
- elif voice_data["status"] == "failed":
160
- raise ValidationError(f"Voice profile creation failed: {voice_data.get('error_message')}")
161
- time.sleep(5)
162
- raise ValidationError("Voice profile creation timed out")
163
- else:
164
- return VoiceProfile.from_dict(response)
165
-
166
- async def _async_create_voice_profile(
167
- self,
168
- files: Dict[str, Any],
169
- data: Dict[str, Any],
170
- wait_for_completion: bool,
171
- timeout: int
172
- ) -> Union[Job, VoiceProfile]:
173
- """Async version of create_voice_profile"""
174
- import asyncio
175
-
176
- response = await self.client.request(
177
- "POST",
178
- "/api/v1/voice/voice-profiles",
179
- data=data,
180
- files=files
181
- )
182
-
183
- if wait_for_completion:
184
- voice_id = response["id"]
185
- # Poll for completion
186
- start_time = time.time()
187
- while time.time() - start_time < timeout:
188
- voice_data = await self.client.request("GET", f"/api/v1/voice/voice-profiles/{voice_id}")
189
- if voice_data["status"] == "completed":
190
- return VoiceProfile.from_dict(voice_data)
191
- elif voice_data["status"] == "failed":
192
- raise ValidationError(f"Voice profile creation failed: {voice_data.get('error_message')}")
193
- await asyncio.sleep(5)
194
- raise ValidationError("Voice profile creation timed out")
195
- else:
196
- return VoiceProfile.from_dict(response)
197
-
61
+ return self._async_create_voice(data, files)
62
+ return self.client.request("POST", "/api/v1/voice/voice-profiles", data=data, files=files)
63
+
64
+ async def _async_create_voice(self, data: Dict, files: Dict) -> Dict[str, Any]:
65
+ return await self.client.request("POST", "/api/v1/voice/voice-profiles", data=data, files=files)
66
+
67
+ def delete_voice(self, voice_id: Union[int, str]) -> Dict[str, str]:
68
+ """Delete a voice by ID or UUID."""
69
+ if self.async_mode:
70
+ return self._async_delete_voice(voice_id)
71
+ return self.client.request("DELETE", f"/api/v1/voice/voices/{voice_id}")
72
+
73
+ async def _async_delete_voice(self, voice_id: Union[int, str]) -> Dict[str, str]:
74
+ return await self.client.request("DELETE", f"/api/v1/voice/voices/{voice_id}")
75
+
198
76
  def generate_speech(
199
77
  self,
200
78
  voice_id: Union[int, str],
201
79
  text: str,
202
- language: Optional[str] = None,
203
80
  speed: float = 1.0,
81
+ language: str = "en",
204
82
  audio_format: str = "mp3",
205
83
  wait_for_completion: bool = False,
206
- timeout: int = 300
207
- ) -> Union[Job, Dict[str, Any]]:
84
+ timeout: int = 300,
85
+ ) -> Dict[str, Any]:
208
86
  """
209
- Generate speech using an existing voice profile
87
+ Generate speech from text using a voice.
210
88
 
211
89
  Args:
212
- voice_id: ID or UUID of the voice profile
213
- text: Text to generate speech for
214
- language: Target language code
215
- speed: Speech speed (0.5 to 2.0)
216
- audio_format: Output audio format (mp3, wav)
217
- wait_for_completion: Whether to wait for completion
218
- timeout: Maximum time to wait
90
+ voice_id: Voice ID (int) or UUID (str) to use for generation
91
+ text: Text to convert to speech
92
+ speed: Speech speed (0.25 to 4.0, default 1.0)
93
+ language: Language code (default "en")
94
+ audio_format: Output format - mp3, wav, ogg (default "mp3")
95
+ wait_for_completion: If True, poll until job completes
96
+ timeout: Max seconds to wait for completion
219
97
 
220
98
  Returns:
221
- Job object or generation result
99
+ Job info dict with job_id, status, etc.
100
+ If wait_for_completion=True, includes output_url when done.
222
101
  """
223
- # Validate inputs
224
- text = self._validate_text_input(text)
225
- if language:
226
- language = self._validate_language_code(language)
227
- if not 0.5 <= speed <= 2.0:
228
- raise ValidationError("Speed must be between 0.5 and 2.0")
229
- if audio_format not in ["mp3", "wav"]:
230
- raise ValidationError("Audio format must be 'mp3' or 'wav'")
231
-
232
- # Prepare form data
233
102
  data = {
234
103
  "input_text": text,
235
104
  "speed": speed,
236
- "audio_format": audio_format
105
+ "language": language,
106
+ "audio_format": audio_format,
237
107
  }
238
- if language:
239
- data["language"] = language
240
-
241
- # Make request
242
- endpoint = f"/api/v1/voice/voices/{voice_id}/generate"
243
-
108
+
244
109
  if self.async_mode:
245
- return self._async_generate_speech(endpoint, data, wait_for_completion, timeout)
246
- else:
247
- response = self.client.request("POST", endpoint, data=data)
248
-
249
- if "job_id" in response:
250
- job = Job.from_dict(response)
251
- if wait_for_completion:
252
- job = self._wait_for_completion(job.id, timeout)
253
- return job.result if job.result else job
254
- return job
255
- else:
256
- # Direct response with audio URL
257
- return response
258
-
110
+ return self._async_generate_speech(voice_id, data, wait_for_completion, timeout)
111
+
112
+ response = self.client.request(
113
+ "POST",
114
+ f"/api/v1/voice/voices/{voice_id}/generate",
115
+ data=data,
116
+ )
117
+
118
+ if wait_for_completion:
119
+ job_id = response.get("job_id") or response.get("id")
120
+ return self._wait_for_job_completion(job_id, timeout)
121
+ return response
122
+
259
123
  async def _async_generate_speech(
260
- self,
261
- endpoint: str,
262
- data: Dict[str, Any],
263
- wait_for_completion: bool,
264
- timeout: int
265
- ) -> Union[Job, Dict[str, Any]]:
266
- """Async version of generate_speech"""
267
- response = await self.client.request("POST", endpoint, data=data)
268
-
269
- if "job_id" in response:
270
- job = Job.from_dict(response)
271
- if wait_for_completion:
272
- job = await self._async_wait_for_completion(job.id, timeout)
273
- return job.result if job.result else job
274
- return job
275
- else:
276
- return response
277
-
278
- def list_voice_profiles(
279
- self,
280
- voice_type: Optional[str] = None,
281
- is_public: Optional[bool] = None,
282
- include_public: bool = True,
283
- limit: int = 50
284
- ) -> List[VoiceProfile]:
124
+ self, voice_id: Union[int, str], data: Dict, wait_for_completion: bool, timeout: int
125
+ ) -> Dict[str, Any]:
126
+ response = await self.client.request(
127
+ "POST",
128
+ f"/api/v1/voice/voices/{voice_id}/generate",
129
+ data=data,
130
+ )
131
+ if wait_for_completion:
132
+ job_id = response.get("job_id") or response.get("id")
133
+ return await self._async_wait_for_job_completion(job_id, timeout)
134
+ return response
135
+
136
+ def get_job_status(self, job_id: int) -> Dict[str, Any]:
285
137
  """
286
- List available voice profiles
138
+ Get TTS job status.
287
139
 
288
140
  Args:
289
- voice_type: Filter by voice type ('custom', 'standard')
290
- is_public: Filter by public status
291
- include_public: Include public voices
292
- limit: Maximum number of results
141
+ job_id: The job ID returned from generate_speech
293
142
 
294
143
  Returns:
295
- List of voice profiles
144
+ Job status dict with status, progress, output_url (when completed), etc.
296
145
  """
297
- params = {
298
- "limit": limit,
299
- "include_public": include_public
300
- }
301
- if voice_type:
302
- params["voice_type"] = voice_type
303
- if is_public is not None:
304
- params["is_public"] = is_public
305
-
306
146
  if self.async_mode:
307
- return self._async_list_voice_profiles(params)
308
- else:
309
- response = self.client.request("GET", "/api/v1/voice/voice-profiles", params=params)
310
- return [VoiceProfile.from_dict(voice_data) for voice_data in response]
311
-
312
- async def _async_list_voice_profiles(self, params: Dict[str, Any]) -> List[VoiceProfile]:
313
- """Async version of list_voice_profiles"""
314
- response = await self.client.request("GET", "/api/v1/voice/voice-profiles", params=params)
315
- return [VoiceProfile.from_dict(voice_data) for voice_data in response]
316
-
317
- def get_voice_profile(self, voice_id: Union[int, str]) -> VoiceProfile:
318
- """
319
- Get details of a specific voice profile
147
+ return self._async_get_job_status(job_id)
148
+ return self.client.request("GET", f"/api/v1/voice/tts-jobs/{job_id}/status")
149
+
150
+ async def _async_get_job_status(self, job_id: int) -> Dict[str, Any]:
151
+ return await self.client.request("GET", f"/api/v1/voice/tts-jobs/{job_id}/status")
152
+
153
+ def _wait_for_job_completion(self, job_id: int, timeout: int) -> Dict[str, Any]:
154
+ """Poll job status until completion or timeout."""
155
+ import time
156
+ start_time = time.time()
320
157
 
321
- Args:
322
- voice_id: ID or UUID of the voice profile
158
+ while time.time() - start_time < timeout:
159
+ status = self.get_job_status(job_id)
323
160
 
324
- Returns:
325
- Voice profile details
326
- """
327
- if self.async_mode:
328
- return self._async_get_voice_profile(voice_id)
329
- else:
330
- response = self.client.request("GET", f"/api/v1/voice/voice-profiles/{voice_id}")
331
- return VoiceProfile.from_dict(response)
161
+ if status.get("status") in ("completed", "COMPLETED"):
162
+ return status
163
+ elif status.get("status") in ("failed", "FAILED", "error", "ERROR"):
164
+ raise Exception(f"Job failed: {status.get('error_message', 'Unknown error')}")
332
165
 
333
- async def _async_get_voice_profile(self, voice_id: Union[int, str]) -> VoiceProfile:
334
- """Async version of get_voice_profile"""
335
- response = await self.client.request("GET", f"/api/v1/voice/voice-profiles/{voice_id}")
336
- return VoiceProfile.from_dict(response)
166
+ time.sleep(2)
337
167
 
338
- def delete_voice_profile(self, voice_id: Union[int, str]) -> Dict[str, str]:
339
- """
340
- Delete a voice profile
168
+ raise TimeoutError(f"Job {job_id} did not complete within {timeout} seconds")
169
+
170
+ async def _async_wait_for_job_completion(self, job_id: int, timeout: int) -> Dict[str, Any]:
171
+ """Async poll job status until completion or timeout."""
172
+ import asyncio
173
+ import time
174
+ start_time = time.time()
341
175
 
342
- Args:
343
- voice_id: ID or UUID of the voice profile
176
+ while time.time() - start_time < timeout:
177
+ status = await self.get_job_status(job_id)
344
178
 
345
- Returns:
346
- Deletion confirmation
347
- """
348
- if self.async_mode:
349
- return self._async_delete_voice_profile(voice_id)
350
- else:
351
- return self.client.request("DELETE", f"/api/v1/voice/voices/{voice_id}")
179
+ if status.get("status") in ("completed", "COMPLETED"):
180
+ return status
181
+ elif status.get("status") in ("failed", "FAILED", "error", "ERROR"):
182
+ raise Exception(f"Job failed: {status.get('error_message', 'Unknown error')}")
352
183
 
353
- async def _async_delete_voice_profile(self, voice_id: Union[int, str]) -> Dict[str, str]:
354
- """Async version of delete_voice_profile"""
355
- return await self.client.request("DELETE", f"/api/v1/voice/voices/{voice_id}")
356
-
357
- def get_job_status(self, job_id: int) -> Job:
358
- """
359
- Get status of a voice processing job
184
+ await asyncio.sleep(2)
360
185
 
361
- Args:
362
- job_id: ID of the job
363
-
364
- Returns:
365
- Job status and details
366
- """
367
- if self.async_mode:
368
- return self._async_get_job_status(job_id)
369
- else:
370
- response = self.client.request("GET", f"/api/v1/voice/clone/{job_id}/status")
371
- return Job.from_dict(response)
372
-
373
- async def _async_get_job_status(self, job_id: int) -> Job:
374
- """Async version of get_job_status"""
375
- response = await self.client.request("GET", f"/api/v1/voice/clone/{job_id}/status")
376
- return Job.from_dict(response)
186
+ raise TimeoutError(f"Job {job_id} did not complete within {timeout} seconds")
187
+