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.
audiopod/exceptions.py CHANGED
@@ -1,51 +1,29 @@
1
1
  """
2
- AudioPod API Client Exceptions
2
+ AudioPod SDK Exceptions
3
3
  """
4
4
 
5
- from typing import Optional, Dict, Any
6
-
7
5
 
8
6
  class AudioPodError(Exception):
9
- """Base exception for AudioPod API client"""
10
-
11
- def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
12
- super().__init__(message)
13
- self.message = message
14
- self.details = details or {}
15
-
16
- def __str__(self) -> str:
17
- return self.message
7
+ """Base exception for AudioPod SDK"""
8
+ pass
18
9
 
19
10
 
20
11
  class AuthenticationError(AudioPodError):
21
- """Raised when API key authentication fails"""
12
+ """Raised when authentication fails"""
22
13
  pass
23
14
 
24
15
 
25
16
  class APIError(AudioPodError):
26
- """Raised when API returns an error response"""
17
+ """Raised when API returns an error"""
27
18
 
28
- def __init__(
29
- self,
30
- message: str,
31
- status_code: Optional[int] = None,
32
- details: Optional[Dict[str, Any]] = None
33
- ):
34
- super().__init__(message, details)
19
+ def __init__(self, message: str, status_code: int = None):
20
+ super().__init__(message)
35
21
  self.status_code = status_code
36
22
 
37
23
 
38
- class RateLimitError(APIError):
39
- """Raised when API rate limit is exceeded"""
40
-
41
- def __init__(
42
- self,
43
- message: str = "Rate limit exceeded",
44
- retry_after: Optional[int] = None,
45
- details: Optional[Dict[str, Any]] = None
46
- ):
47
- super().__init__(message, status_code=429, details=details)
48
- self.retry_after = retry_after
24
+ class RateLimitError(AudioPodError):
25
+ """Raised when rate limit is exceeded"""
26
+ pass
49
27
 
50
28
 
51
29
  class ValidationError(AudioPodError):
@@ -53,44 +31,11 @@ class ValidationError(AudioPodError):
53
31
  pass
54
32
 
55
33
 
56
- class ProcessingError(APIError):
57
- """Raised when audio processing fails"""
34
+ class InsufficientBalanceError(AudioPodError):
35
+ """Raised when wallet balance is insufficient"""
58
36
 
59
- def __init__(
60
- self,
61
- message: str,
62
- job_id: Optional[str] = None,
63
- details: Optional[Dict[str, Any]] = None
64
- ):
65
- super().__init__(message, details=details)
66
- self.job_id = job_id
67
-
68
-
69
- class FileError(AudioPodError):
70
- """Raised when file operations fail"""
71
- pass
72
-
73
-
74
- class NetworkError(AudioPodError):
75
- """Raised when network operations fail"""
76
- pass
77
-
78
-
79
- class TimeoutError(AudioPodError):
80
- """Raised when operations timeout"""
81
- pass
82
-
37
+ def __init__(self, message: str, required_cents: int = None, available_cents: int = None):
38
+ super().__init__(message)
39
+ self.required_cents = required_cents
40
+ self.available_cents = available_cents
83
41
 
84
- class InsufficientCreditsError(APIError):
85
- """Raised when user has insufficient credits"""
86
-
87
- def __init__(
88
- self,
89
- message: str = "Insufficient credits",
90
- credits_needed: Optional[int] = None,
91
- credits_available: Optional[int] = None,
92
- details: Optional[Dict[str, Any]] = None
93
- ):
94
- super().__init__(message, status_code=402, details=details)
95
- self.credits_needed = credits_needed
96
- self.credits_available = credits_available
@@ -1,26 +1,30 @@
1
1
  """
2
- AudioPod API Services
3
- Service classes for different API endpoints
2
+ AudioPod SDK Services
4
3
  """
5
4
 
5
+ from .base import BaseService
6
6
  from .voice import VoiceService
7
7
  from .music import MusicService
8
8
  from .transcription import TranscriptionService
9
9
  from .translation import TranslationService
10
10
  from .speaker import SpeakerService
11
11
  from .denoiser import DenoiserService
12
- from .karaoke import KaraokeService
13
12
  from .credits import CreditService
14
13
  from .stem_extraction import StemExtractionService
14
+ from .wallet import WalletService
15
+ from .video import VideoService
15
16
 
16
17
  __all__ = [
18
+ "BaseService",
17
19
  "VoiceService",
18
- "MusicService",
20
+ "MusicService",
19
21
  "TranscriptionService",
20
22
  "TranslationService",
21
23
  "SpeakerService",
22
24
  "DenoiserService",
23
- "KaraokeService",
24
25
  "CreditService",
25
- "StemExtractionService"
26
+ "StemExtractionService",
27
+ "WalletService",
28
+ "VideoService",
26
29
  ]
30
+
audiopod/services/base.py CHANGED
@@ -1,213 +1,69 @@
1
1
  """
2
- Base service class for AudioPod API services
2
+ Base Service Class
3
3
  """
4
4
 
5
- import os
6
5
  import time
7
- from typing import TYPE_CHECKING, Optional, Dict, Any, Union, BinaryIO
8
- from pathlib import Path
9
-
10
- from ..exceptions import ValidationError, FileError, ProcessingError
11
- from ..models import Job, JobStatus
12
-
13
- if TYPE_CHECKING:
14
- from ..client import Client, AsyncClient
6
+ from typing import Any, Dict, Optional, Tuple, BinaryIO
15
7
 
16
8
 
17
9
  class BaseService:
18
- """Base class for all AudioPod API services"""
19
-
20
- def __init__(self, client: Union["Client", "AsyncClient"], async_mode: bool = False):
10
+ """Base class for all services"""
11
+
12
+ def __init__(self, client: Any, async_mode: bool = False):
21
13
  self.client = client
22
14
  self.async_mode = async_mode
23
-
24
- def _validate_file(self, file_path: str, file_type: str = "audio") -> str:
25
- """
26
- Validate file exists and format is supported
27
-
28
- Args:
29
- file_path: Path to the file
30
- file_type: Type of file ('audio' or 'video')
31
-
32
- Returns:
33
- Absolute path to the file
34
-
35
- Raises:
36
- FileError: If file doesn't exist or format not supported
37
- """
38
- path = Path(file_path)
39
-
40
- if not path.exists():
41
- raise FileError(f"File not found: {file_path}")
42
-
43
- if not self.client.config.validate_file_format(path.name, file_type):
44
- supported_formats = (
45
- self.client.config.supported_audio_formats
46
- if file_type == "audio"
47
- else self.client.config.supported_video_formats
48
- )
49
- raise FileError(
50
- f"Unsupported {file_type} format. "
51
- f"Supported formats: {', '.join(supported_formats)}"
52
- )
53
-
54
- # Check file size
55
- file_size_mb = path.stat().st_size / (1024 * 1024)
56
- if file_size_mb > self.client.config.max_file_size_mb:
57
- raise FileError(
58
- f"File too large: {file_size_mb:.1f}MB. "
59
- f"Maximum size: {self.client.config.max_file_size_mb}MB"
60
- )
61
-
62
- return str(path.absolute())
63
-
64
- def _prepare_file_upload(self, file_path: str, field_name: str = "file") -> Dict[str, Any]:
65
- """
66
- Prepare file for upload
67
-
68
- Args:
69
- file_path: Path to the file
70
- field_name: Form field name for the file
71
-
72
- Returns:
73
- Files dict for requests
74
- """
75
- validated_path = self._validate_file(file_path)
76
-
77
- with open(validated_path, 'rb') as f:
78
- return {field_name: (Path(validated_path).name, f.read())}
79
-
15
+
16
+ def _prepare_file_upload(
17
+ self, file_path: str, field_name: str = "file"
18
+ ) -> Dict[str, Tuple[str, BinaryIO, str]]:
19
+ """Prepare file for upload."""
20
+ import mimetypes
21
+
22
+ mime_type, _ = mimetypes.guess_type(file_path)
23
+ mime_type = mime_type or "application/octet-stream"
24
+
25
+ file_handle = open(file_path, "rb")
26
+ filename = file_path.split("/")[-1]
27
+
28
+ return {field_name: (filename, file_handle, mime_type)}
29
+
80
30
  def _wait_for_completion(
81
- self,
82
- job_id: int,
83
- timeout: int = 300,
84
- poll_interval: int = 5
85
- ) -> Job:
86
- """
87
- Wait for job completion with polling
88
-
89
- Args:
90
- job_id: Job ID to wait for
91
- timeout: Maximum time to wait in seconds
92
- poll_interval: How often to check status in seconds
93
-
94
- Returns:
95
- Completed job
96
-
97
- Raises:
98
- ProcessingError: If job fails or times out
99
- """
31
+ self, job_id: int, timeout: int = 900, poll_interval: int = 5
32
+ ) -> Dict[str, Any]:
33
+ """Wait for job completion."""
100
34
  start_time = time.time()
101
-
35
+
102
36
  while time.time() - start_time < timeout:
103
- job_data = self.client.request("GET", f"/api/v1/jobs/{job_id}")
104
- job = Job.from_dict(job_data)
105
-
106
- if job.status == JobStatus.COMPLETED:
107
- return job
108
- elif job.status == JobStatus.FAILED:
109
- raise ProcessingError(
110
- f"Job {job_id} failed: {job.error_message}",
111
- job_id=str(job_id)
112
- )
113
- elif job.status == JobStatus.CANCELLED:
114
- raise ProcessingError(
115
- f"Job {job_id} was cancelled",
116
- job_id=str(job_id)
117
- )
118
-
37
+ response = self.client.request("GET", f"/api/v1/jobs/{job_id}")
38
+
39
+ status = response.get("status", "").upper()
40
+ if status == "COMPLETED":
41
+ return response
42
+ elif status in ["FAILED", "ERROR"]:
43
+ raise Exception(f"Job failed: {response.get('error_message', 'Unknown error')}")
44
+
119
45
  time.sleep(poll_interval)
120
-
121
- raise ProcessingError(
122
- f"Job {job_id} timed out after {timeout} seconds",
123
- job_id=str(job_id)
124
- )
125
-
46
+
47
+ raise TimeoutError(f"Job {job_id} did not complete within {timeout} seconds")
48
+
126
49
  async def _async_wait_for_completion(
127
- self,
128
- job_id: int,
129
- timeout: int = 300,
130
- poll_interval: int = 5
131
- ) -> Job:
132
- """
133
- Async version of wait_for_completion
134
-
135
- Args:
136
- job_id: Job ID to wait for
137
- timeout: Maximum time to wait in seconds
138
- poll_interval: How often to check status in seconds
139
-
140
- Returns:
141
- Completed job
142
-
143
- Raises:
144
- ProcessingError: If job fails or times out
145
- """
50
+ self, job_id: int, timeout: int = 900, poll_interval: int = 5
51
+ ) -> Dict[str, Any]:
52
+ """Async wait for job completion."""
146
53
  import asyncio
147
-
54
+
148
55
  start_time = time.time()
149
-
56
+
150
57
  while time.time() - start_time < timeout:
151
- job_data = await self.client.request("GET", f"/api/v1/jobs/{job_id}")
152
- job = Job.from_dict(job_data)
153
-
154
- if job.status == JobStatus.COMPLETED:
155
- return job
156
- elif job.status == JobStatus.FAILED:
157
- raise ProcessingError(
158
- f"Job {job_id} failed: {job.error_message}",
159
- job_id=str(job_id)
160
- )
161
- elif job.status == JobStatus.CANCELLED:
162
- raise ProcessingError(
163
- f"Job {job_id} was cancelled",
164
- job_id=str(job_id)
165
- )
166
-
58
+ response = await self.client.request("GET", f"/api/v1/jobs/{job_id}")
59
+
60
+ status = response.get("status", "").upper()
61
+ if status == "COMPLETED":
62
+ return response
63
+ elif status in ["FAILED", "ERROR"]:
64
+ raise Exception(f"Job failed: {response.get('error_message', 'Unknown error')}")
65
+
167
66
  await asyncio.sleep(poll_interval)
168
-
169
- raise ProcessingError(
170
- f"Job {job_id} timed out after {timeout} seconds",
171
- job_id=str(job_id)
172
- )
173
-
174
- def _validate_language_code(self, language: str) -> str:
175
- """
176
- Validate language code format
177
-
178
- Args:
179
- language: Language code to validate
180
-
181
- Returns:
182
- Validated language code
183
-
184
- Raises:
185
- ValidationError: If language code is invalid
186
- """
187
- if not language or len(language) < 2:
188
- raise ValidationError("Language code must be at least 2 characters")
189
-
190
- # Convert to lowercase for consistency
191
- return language.lower()
192
-
193
- def _validate_text_input(self, text: str, max_length: int = 5000) -> str:
194
- """
195
- Validate text input
196
-
197
- Args:
198
- text: Text to validate
199
- max_length: Maximum allowed length
200
-
201
- Returns:
202
- Validated text
203
-
204
- Raises:
205
- ValidationError: If text is invalid
206
- """
207
- if not text or not text.strip():
208
- raise ValidationError("Text input cannot be empty")
209
-
210
- if len(text) > max_length:
211
- raise ValidationError(f"Text too long. Maximum length: {max_length} characters")
212
-
213
- return text.strip()
67
+
68
+ raise TimeoutError(f"Job {job_id} did not complete within {timeout} seconds")
69
+
@@ -1,46 +1,42 @@
1
1
  """
2
- Credits Service - User credits and usage tracking
2
+ Credits Service - User credits and usage (subscription credits)
3
3
  """
4
4
 
5
5
  from typing import List, Dict, Any
6
6
  from .base import BaseService
7
- from ..models import CreditInfo
8
7
 
9
8
 
10
9
  class CreditService(BaseService):
11
- """Service for managing user credits and usage"""
12
-
13
- def get_credit_balance(self) -> CreditInfo:
14
- """Get current credit balance and info"""
10
+ """
11
+ Service for managing subscription credits.
12
+
13
+ Note: For API wallet (USD-based billing), use client.wallet instead.
14
+ """
15
+
16
+ def get_balance(self) -> Dict[str, Any]:
17
+ """Get subscription credit balance."""
15
18
  if self.async_mode:
16
- return self._async_get_credit_balance()
17
- else:
18
- response = self.client.request("GET", "/api/v1/credits")
19
- return CreditInfo.from_dict(response)
20
-
21
- async def _async_get_credit_balance(self) -> CreditInfo:
22
- """Async version of get_credit_balance"""
23
- response = await self.client.request("GET", "/api/v1/credits")
24
- return CreditInfo.from_dict(response)
25
-
19
+ return self._async_get_balance()
20
+ return self.client.request("GET", "/api/v1/credits")
21
+
22
+ async def _async_get_balance(self) -> Dict[str, Any]:
23
+ return await self.client.request("GET", "/api/v1/credits")
24
+
26
25
  def get_usage_history(self) -> List[Dict[str, Any]]:
27
- """Get credit usage history"""
26
+ """Get credit usage history."""
28
27
  if self.async_mode:
29
28
  return self._async_get_usage_history()
30
- else:
31
- return self.client.request("GET", "/api/v1/credits/usage")
32
-
29
+ return self.client.request("GET", "/api/v1/credits/usage")
30
+
33
31
  async def _async_get_usage_history(self) -> List[Dict[str, Any]]:
34
- """Async version of get_usage_history"""
35
32
  return await self.client.request("GET", "/api/v1/credits/usage")
36
-
37
- def get_credit_multipliers(self) -> Dict[str, float]:
38
- """Get credit multipliers for different services"""
33
+
34
+ def get_multipliers(self) -> Dict[str, float]:
35
+ """Get credit multipliers for services."""
39
36
  if self.async_mode:
40
- return self._async_get_credit_multipliers()
41
- else:
42
- return self.client.request("GET", "/api/v1/credits/multipliers")
43
-
44
- async def _async_get_credit_multipliers(self) -> Dict[str, float]:
45
- """Async version of get_credit_multipliers"""
37
+ return self._async_get_multipliers()
38
+ return self.client.request("GET", "/api/v1/credits/multipliers")
39
+
40
+ async def _async_get_multipliers(self) -> Dict[str, float]:
46
41
  return await self.client.request("GET", "/api/v1/credits/multipliers")
42
+
@@ -1,51 +1,136 @@
1
1
  """
2
- Denoiser Service - Audio denoising operations
2
+ Denoiser Service - Audio noise reduction
3
+
4
+ API Routes:
5
+ - POST /api/v1/denoiser/denoise - Denoise audio
6
+ - GET /api/v1/denoiser/jobs/{id} - Get job details
7
+ - GET /api/v1/denoiser/jobs - List jobs
8
+ - DELETE /api/v1/denoiser/jobs/{id} - Delete job
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, DenoiseResult
8
13
 
9
14
 
10
15
  class DenoiserService(BaseService):
11
- """Service for audio denoising operations"""
12
-
13
- def denoise_audio(
16
+ """Service for audio noise reduction."""
17
+
18
+ def denoise(
14
19
  self,
15
- audio_file: str,
16
- quality_mode: str = "balanced",
17
- wait_for_completion: bool = False,
18
- timeout: int = 600
19
- ) -> Union[Job, DenoiseResult]:
20
- """Remove noise from audio"""
21
- files = self._prepare_file_upload(audio_file, "file")
22
- data = {"quality_mode": quality_mode}
20
+ file: Optional[str] = None,
21
+ url: Optional[str] = None,
22
+ mode: str = "balanced",
23
+ wait_for_completion: bool = True,
24
+ timeout: int = 300,
25
+ # Legacy parameter name
26
+ audio_file: Optional[str] = None,
27
+ ) -> Dict[str, Any]:
28
+ """
29
+ Remove noise from audio.
30
+
31
+ Args:
32
+ file: Path to local audio file
33
+ url: URL of audio file
34
+ mode: Denoise mode ("balanced", "studio", or "ultra")
35
+ wait_for_completion: Wait for completion (default: True)
36
+ timeout: Max wait time in seconds
37
+
38
+ Returns:
39
+ Job dict with denoised audio URL when completed
40
+ """
41
+ # Support both 'file' and legacy 'audio_file' parameter names
42
+ audio_file = file or audio_file
23
43
 
44
+ data = {"mode": mode}
45
+ if url:
46
+ data["url"] = url
47
+
48
+ files = self._prepare_file_upload(audio_file, "file") if audio_file else None
49
+
24
50
  if self.async_mode:
25
- return self._async_denoise(files, data, wait_for_completion, timeout)
26
- else:
27
- response = self.client.request(
28
- "POST", "/api/v1/denoiser/denoise",
29
- data=data, files=files
30
- )
31
- job = Job.from_dict(response)
32
-
33
- if wait_for_completion:
34
- completed_job = self._wait_for_completion(job.id, timeout)
35
- return DenoiseResult.from_dict(completed_job.result or completed_job.__dict__)
36
-
37
- return job
38
-
39
- async def _async_denoise(self, files, data, wait_for_completion, timeout):
40
- """Async version of denoise_audio"""
41
- response = await self.client.request(
42
- "POST", "/api/v1/denoiser/denoise",
43
- data=data, files=files
44
- )
45
- job = Job.from_dict(response)
46
-
51
+ return self._async_denoise(data, files, wait_for_completion, timeout)
52
+
53
+ response = self.client.request("POST", "/api/v1/denoiser/denoise", data=data, files=files)
54
+
55
+ if wait_for_completion:
56
+ job_id = response.get("id") or response.get("job_id")
57
+ return self._wait_for_denoise(job_id, timeout)
58
+ return response
59
+
60
+ async def _async_denoise(
61
+ self, data: Dict, files: Optional[Dict], wait_for_completion: bool, timeout: int
62
+ ) -> Dict[str, Any]:
63
+ response = await self.client.request("POST", "/api/v1/denoiser/denoise", data=data, files=files)
47
64
  if wait_for_completion:
48
- completed_job = await self._async_wait_for_completion(job.id, timeout)
49
- return DenoiseResult.from_dict(completed_job.result or completed_job.__dict__)
50
-
51
- return job
65
+ job_id = response.get("id") or response.get("job_id")
66
+ return await self._async_wait_for_denoise(job_id, timeout)
67
+ return response
68
+
69
+ def get_job(self, job_id: int) -> Dict[str, Any]:
70
+ """Get denoise job details and status."""
71
+ if self.async_mode:
72
+ return self._async_get_job(job_id)
73
+ return self.client.request("GET", f"/api/v1/denoiser/jobs/{job_id}")
74
+
75
+ async def _async_get_job(self, job_id: int) -> Dict[str, Any]:
76
+ return await self.client.request("GET", f"/api/v1/denoiser/jobs/{job_id}")
77
+
78
+ def list_jobs(self, skip: int = 0, limit: int = 50) -> List[Dict[str, Any]]:
79
+ """List denoiser jobs."""
80
+ if self.async_mode:
81
+ return self._async_list_jobs(skip, limit)
82
+ return self.client.request(
83
+ "GET", "/api/v1/denoiser/jobs", params={"skip": skip, "limit": limit}
84
+ )
85
+
86
+ async def _async_list_jobs(self, skip: int, limit: int) -> List[Dict[str, Any]]:
87
+ return await self.client.request(
88
+ "GET", "/api/v1/denoiser/jobs", params={"skip": skip, "limit": limit}
89
+ )
90
+
91
+ def delete_job(self, job_id: int) -> Dict[str, str]:
92
+ """Delete a denoiser job."""
93
+ if self.async_mode:
94
+ return self._async_delete_job(job_id)
95
+ return self.client.request("DELETE", f"/api/v1/denoiser/jobs/{job_id}")
96
+
97
+ async def _async_delete_job(self, job_id: int) -> Dict[str, str]:
98
+ return await self.client.request("DELETE", f"/api/v1/denoiser/jobs/{job_id}")
99
+
100
+ def _wait_for_denoise(self, job_id: int, timeout: int) -> Dict[str, Any]:
101
+ """Wait for denoise job completion."""
102
+ import time
103
+ start_time = time.time()
104
+
105
+ while time.time() - start_time < timeout:
106
+ job = self.get_job(job_id)
107
+ status = job.get("status", "").upper()
108
+
109
+ if status == "COMPLETED":
110
+ return job
111
+ elif status in ("FAILED", "ERROR"):
112
+ raise Exception(f"Denoising failed: {job.get('error_message', 'Unknown error')}")
113
+
114
+ time.sleep(3)
115
+
116
+ raise TimeoutError(f"Denoising {job_id} did not complete within {timeout} seconds")
117
+
118
+ async def _async_wait_for_denoise(self, job_id: int, timeout: int) -> Dict[str, Any]:
119
+ """Async wait for denoise job completion."""
120
+ import asyncio
121
+ import time
122
+ start_time = time.time()
123
+
124
+ while time.time() - start_time < timeout:
125
+ job = await self.get_job(job_id)
126
+ status = job.get("status", "").upper()
127
+
128
+ if status == "COMPLETED":
129
+ return job
130
+ elif status in ("FAILED", "ERROR"):
131
+ raise Exception(f"Denoising failed: {job.get('error_message', 'Unknown error')}")
132
+
133
+ await asyncio.sleep(3)
134
+
135
+ raise TimeoutError(f"Denoising {job_id} did not complete within {timeout} seconds")
136
+