audiopod 1.4.0__py3-none-any.whl → 2.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
audiopod/__init__.py CHANGED
@@ -1,29 +1,34 @@
1
1
  """
2
2
  AudioPod SDK for Python
3
3
  Professional Audio Processing powered by AI
4
+
5
+ Example:
6
+ from audiopod import AudioPod
7
+
8
+ client = AudioPod(api_key="ap_...")
9
+
10
+ # Transcribe audio
11
+ job = client.transcription.create(url="https://...")
12
+ result = client.transcription.wait_for_completion(job.id)
4
13
  """
5
14
 
6
- __version__ = "1.3.0"
15
+ __version__ = "2.1.0"
7
16
 
8
- from .client import Client, AsyncClient
17
+ from .client import AudioPod
9
18
  from .exceptions import (
10
19
  AudioPodError,
11
20
  AuthenticationError,
12
21
  APIError,
13
22
  RateLimitError,
14
- ValidationError,
15
23
  InsufficientBalanceError,
16
24
  )
17
25
 
18
26
  __all__ = [
19
- "Client",
20
- "AsyncClient",
27
+ "AudioPod",
21
28
  "AudioPodError",
22
- "AuthenticationError",
29
+ "AuthenticationError",
23
30
  "APIError",
24
31
  "RateLimitError",
25
- "ValidationError",
26
32
  "InsufficientBalanceError",
27
33
  "__version__",
28
34
  ]
29
-
audiopod/client.py CHANGED
@@ -1,82 +1,102 @@
1
1
  """
2
2
  AudioPod API Client
3
+
4
+ Clean, minimal API inspired by OpenAI's SDK design.
3
5
  """
4
6
 
5
7
  import os
6
- import logging
7
- from typing import Optional, Dict, Any
8
+ from typing import Optional, Dict, Any, BinaryIO
8
9
  from urllib.parse import urljoin
9
10
 
10
11
  import requests
11
- import aiohttp
12
12
  from requests.adapters import HTTPAdapter
13
13
  from urllib3.util.retry import Retry
14
14
 
15
- from .config import ClientConfig
16
- from .exceptions import AuthenticationError, APIError, RateLimitError, InsufficientBalanceError
17
- from .services import (
18
- VoiceService,
19
- MusicService,
20
- TranscriptionService,
21
- TranslationService,
22
- SpeakerService,
23
- DenoiserService,
24
- CreditService,
25
- StemExtractionService,
26
- WalletService,
15
+ from .exceptions import (
16
+ AuthenticationError,
17
+ APIError,
18
+ RateLimitError,
19
+ InsufficientBalanceError,
27
20
  )
21
+ from .resources.transcription import Transcription
22
+ from .resources.voice import Voice
23
+ from .resources.music import Music
24
+ from .resources.stems import StemExtraction
25
+ from .resources.denoiser import Denoiser
26
+ from .resources.speaker import Speaker
27
+ from .resources.wallet import Wallet
28
+
29
+ VERSION = "2.1.0"
30
+ DEFAULT_BASE_URL = "https://api.audiopod.ai"
31
+ DEFAULT_TIMEOUT = 60
32
+
28
33
 
29
- logger = logging.getLogger(__name__)
34
+ class AudioPod:
35
+ """
36
+ AudioPod API Client.
30
37
 
38
+ Args:
39
+ api_key: Your AudioPod API key (starts with 'ap_').
40
+ If not provided, reads from AUDIOPOD_API_KEY env var.
41
+ base_url: Base URL for the API (default: https://api.audiopod.ai)
42
+ timeout: Request timeout in seconds (default: 60)
43
+ max_retries: Maximum retries for failed requests (default: 3)
31
44
 
32
- class BaseClient:
33
- """Base client with common functionality"""
45
+ Example:
46
+ >>> from audiopod import AudioPod
47
+ >>> client = AudioPod(api_key="ap_...")
48
+ >>> result = client.transcription.transcribe(url="https://...")
49
+ """
34
50
 
35
51
  def __init__(
36
52
  self,
37
53
  api_key: Optional[str] = None,
38
54
  base_url: Optional[str] = None,
39
- timeout: int = 30,
55
+ timeout: int = DEFAULT_TIMEOUT,
40
56
  max_retries: int = 3,
41
- verify_ssl: bool = True,
42
- debug: bool = False,
43
57
  ):
44
- """
45
- Initialize the AudioPod API client.
46
-
47
- Args:
48
- api_key: Your AudioPod API key. If None, reads from AUDIOPOD_API_KEY env var.
49
- base_url: API base URL. Defaults to https://api.audiopod.ai
50
- timeout: Request timeout in seconds.
51
- max_retries: Maximum retries for failed requests.
52
- verify_ssl: Whether to verify SSL certificates.
53
- debug: Enable debug logging.
54
- """
55
- if debug:
56
- logging.basicConfig(level=logging.DEBUG)
57
-
58
58
  self.api_key = api_key or os.getenv("AUDIOPOD_API_KEY")
59
+
59
60
  if not self.api_key:
60
61
  raise AuthenticationError(
61
- "API key required. Pass api_key or set AUDIOPOD_API_KEY environment variable."
62
+ "API key is required. Pass api_key or set AUDIOPOD_API_KEY environment variable."
62
63
  )
63
64
 
64
65
  if not self.api_key.startswith("ap_"):
65
- raise AuthenticationError("Invalid API key format. Keys start with 'ap_'")
66
+ raise AuthenticationError(
67
+ "Invalid API key format. AudioPod API keys start with 'ap_'"
68
+ )
66
69
 
67
- self.config = ClientConfig(
68
- base_url=base_url or "https://api.audiopod.ai",
69
- timeout=timeout,
70
- max_retries=max_retries,
71
- verify_ssl=verify_ssl,
72
- debug=debug,
70
+ self.base_url = base_url or DEFAULT_BASE_URL
71
+ self.timeout = timeout
72
+
73
+ # Configure session with retries
74
+ self._session = requests.Session()
75
+ retry_strategy = Retry(
76
+ total=max_retries,
77
+ status_forcelist=[429, 500, 502, 503, 504],
78
+ allowed_methods=["HEAD", "GET", "OPTIONS", "POST", "DELETE"],
79
+ backoff_factor=1,
73
80
  )
81
+ adapter = HTTPAdapter(max_retries=retry_strategy)
82
+ self._session.mount("http://", adapter)
83
+ self._session.mount("https://", adapter)
84
+
85
+ # Initialize services
86
+ self.transcription = Transcription(self)
87
+ self.voice = Voice(self)
88
+ self.music = Music(self)
89
+ self.stems = StemExtraction(self)
90
+ self.denoiser = Denoiser(self)
91
+ self.speaker = Speaker(self)
92
+ self.wallet = Wallet(self)
74
93
 
75
94
  def _get_headers(self) -> Dict[str, str]:
76
95
  return {
77
96
  "Authorization": f"Bearer {self.api_key}",
97
+ "X-API-Key": self.api_key,
78
98
  "Content-Type": "application/json",
79
- "User-Agent": f"audiopod-python/{self.config.version}",
99
+ "User-Agent": f"audiopod-python/{VERSION}",
80
100
  "Accept": "application/json",
81
101
  }
82
102
 
@@ -86,221 +106,93 @@ class BaseClient:
86
106
  if response.status_code == 204:
87
107
  return {}
88
108
  return response.json()
89
- except requests.exceptions.HTTPError as e:
90
- if response.status_code == 401:
91
- raise AuthenticationError("Invalid API key")
92
- elif response.status_code == 402:
109
+ except requests.exceptions.HTTPError:
110
+ status = response.status_code
111
+ try:
112
+ data = response.json()
113
+ message = data.get("detail") or data.get("message") or str(data)
114
+ except Exception:
115
+ message = response.text or f"HTTP {status}"
116
+
117
+ if status == 401:
118
+ raise AuthenticationError(message)
119
+ elif status == 402:
93
120
  try:
94
121
  data = response.json()
95
122
  raise InsufficientBalanceError(
96
- data.get("message", "Insufficient balance"),
123
+ message,
97
124
  required_cents=data.get("required_cents"),
98
125
  available_cents=data.get("available_cents"),
99
126
  )
100
127
  except (ValueError, KeyError):
101
- raise InsufficientBalanceError("Insufficient wallet balance")
102
- elif response.status_code == 429:
103
- raise RateLimitError("Rate limit exceeded")
128
+ raise InsufficientBalanceError(message)
129
+ elif status == 429:
130
+ raise RateLimitError(message)
104
131
  else:
105
- try:
106
- error_data = response.json()
107
- message = error_data.get("detail", str(e))
108
- except:
109
- message = str(e)
110
- raise APIError(f"API error: {message}", status_code=response.status_code)
111
-
112
-
113
- class Client(BaseClient):
114
- """
115
- Synchronous AudioPod API Client.
132
+ raise APIError(message, status_code=status)
116
133
 
117
- Example:
118
- ```python
119
- from audiopod import Client
120
-
121
- client = Client(api_key="ap_your_key")
122
-
123
- # Check wallet balance
124
- balance = client.wallet.get_balance()
125
- print(f"Balance: {balance['balance_usd']}")
126
-
127
- # Extract stems
128
- job = client.stem_extraction.extract_stems(
129
- audio_file="song.mp3",
130
- stem_types=["vocals", "drums", "bass", "other"]
131
- )
132
- ```
133
- """
134
-
135
- def __init__(self, **kwargs):
136
- super().__init__(**kwargs)
137
-
138
- self.session = requests.Session()
139
- retry_strategy = Retry(
140
- total=self.config.max_retries,
141
- status_forcelist=[429, 500, 502, 503, 504],
142
- allowed_methods=["HEAD", "GET", "OPTIONS", "POST", "DELETE"],
143
- backoff_factor=1,
134
+ def get(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
135
+ """Make a GET request."""
136
+ url = urljoin(self.base_url, endpoint)
137
+ headers = self._get_headers()
138
+ response = self._session.get(
139
+ url, headers=headers, params=params, timeout=self.timeout
144
140
  )
145
- adapter = HTTPAdapter(max_retries=retry_strategy)
146
- self.session.mount("http://", adapter)
147
- self.session.mount("https://", adapter)
148
-
149
- # Services
150
- self.voice = VoiceService(self)
151
- self.music = MusicService(self)
152
- self.transcription = TranscriptionService(self)
153
- self.translation = TranslationService(self)
154
- self.speaker = SpeakerService(self)
155
- self.denoiser = DenoiserService(self)
156
- self.credits = CreditService(self)
157
- self.stem_extraction = StemExtractionService(self)
158
- self.wallet = WalletService(self)
141
+ return self._handle_response(response)
159
142
 
160
- def request(
143
+ def post(
161
144
  self,
162
- method: str,
163
145
  endpoint: str,
164
146
  data: Optional[Dict[str, Any]] = None,
165
147
  json_data: Optional[Dict[str, Any]] = None,
166
- files: Optional[Dict[str, Any]] = None,
167
- params: Optional[Dict[str, Any]] = None,
168
148
  ) -> Dict[str, Any]:
169
- """Make API request."""
170
- url = urljoin(self.config.base_url, endpoint)
149
+ """Make a POST request."""
150
+ url = urljoin(self.base_url, endpoint)
171
151
  headers = self._get_headers()
172
-
173
- if files:
174
- headers.pop("Content-Type", None)
175
-
176
- response = self.session.request(
177
- method=method,
178
- url=url,
179
- headers=headers,
180
- data=data,
181
- json=json_data,
182
- files=files,
183
- params=params,
184
- timeout=self.config.timeout,
185
- verify=self.config.verify_ssl,
152
+ response = self._session.post(
153
+ url, headers=headers, data=data, json=json_data, timeout=self.timeout
186
154
  )
187
155
  return self._handle_response(response)
188
156
 
189
- def get_user_info(self) -> Dict[str, Any]:
190
- """Get current user information."""
191
- return self.request("GET", "/api/v1/auth/me")
192
-
193
- def close(self):
194
- """Close client session."""
195
- self.session.close()
196
-
197
- def __enter__(self):
198
- return self
199
-
200
- def __exit__(self, exc_type, exc_val, exc_tb):
201
- self.close()
202
-
203
-
204
- class AsyncClient(BaseClient):
205
- """
206
- Asynchronous AudioPod API Client.
207
-
208
- Example:
209
- ```python
210
- import asyncio
211
- from audiopod import AsyncClient
212
-
213
- async def main():
214
- async with AsyncClient(api_key="ap_your_key") as client:
215
- balance = await client.wallet.get_balance()
216
- print(f"Balance: {balance['balance_usd']}")
217
-
218
- asyncio.run(main())
219
- ```
220
- """
221
-
222
- def __init__(self, **kwargs):
223
- super().__init__(**kwargs)
224
- self._session: Optional[aiohttp.ClientSession] = None
225
-
226
- # Services
227
- self.voice = VoiceService(self, async_mode=True)
228
- self.music = MusicService(self, async_mode=True)
229
- self.transcription = TranscriptionService(self, async_mode=True)
230
- self.translation = TranslationService(self, async_mode=True)
231
- self.speaker = SpeakerService(self, async_mode=True)
232
- self.denoiser = DenoiserService(self, async_mode=True)
233
- self.credits = CreditService(self, async_mode=True)
234
- self.stem_extraction = StemExtractionService(self, async_mode=True)
235
- self.wallet = WalletService(self, async_mode=True)
236
-
237
- @property
238
- def session(self) -> aiohttp.ClientSession:
239
- if self._session is None or self._session.closed:
240
- timeout = aiohttp.ClientTimeout(total=self.config.timeout)
241
- connector = aiohttp.TCPConnector(ssl=self.config.verify_ssl)
242
- self._session = aiohttp.ClientSession(
243
- timeout=timeout, connector=connector, headers=self._get_headers()
244
- )
245
- return self._session
157
+ def delete(self, endpoint: str) -> Dict[str, Any]:
158
+ """Make a DELETE request."""
159
+ url = urljoin(self.base_url, endpoint)
160
+ headers = self._get_headers()
161
+ response = self._session.delete(url, headers=headers, timeout=self.timeout)
162
+ return self._handle_response(response)
246
163
 
247
- async def request(
164
+ def upload(
248
165
  self,
249
- method: str,
250
166
  endpoint: str,
251
- data: Optional[Dict[str, Any]] = None,
252
- json_data: Optional[Dict[str, Any]] = None,
253
- files: Optional[Dict[str, Any]] = None,
254
- params: Optional[Dict[str, Any]] = None,
167
+ file_path: str,
168
+ field_name: str = "file",
169
+ additional_fields: Optional[Dict[str, Any]] = None,
255
170
  ) -> Dict[str, Any]:
256
- """Make async API request."""
257
- url = urljoin(self.config.base_url, endpoint)
258
-
259
- async with self.session.request(
260
- method=method,
261
- url=url,
262
- json=json_data,
263
- data=data,
264
- params=params,
265
- ) as response:
266
- return await self._handle_async_response(response)
267
-
268
- async def _handle_async_response(self, response: aiohttp.ClientResponse) -> Dict[str, Any]:
269
- if response.status == 204:
270
- return {}
271
- try:
272
- response.raise_for_status()
273
- return await response.json()
274
- except aiohttp.ClientResponseError as e:
275
- if response.status == 401:
276
- raise AuthenticationError("Invalid API key")
277
- elif response.status == 402:
278
- try:
279
- data = await response.json()
280
- raise InsufficientBalanceError(
281
- data.get("message", "Insufficient balance"),
282
- required_cents=data.get("required_cents"),
283
- available_cents=data.get("available_cents"),
284
- )
285
- except:
286
- raise InsufficientBalanceError("Insufficient wallet balance")
287
- elif response.status == 429:
288
- raise RateLimitError("Rate limit exceeded")
289
- else:
290
- raise APIError(f"API error: {e}", status_code=response.status)
171
+ """Upload a file."""
172
+ url = urljoin(self.base_url, endpoint)
173
+ headers = self._get_headers()
174
+ headers.pop("Content-Type", None) # Let requests set multipart boundary
175
+
176
+ with open(file_path, "rb") as f:
177
+ files = {field_name: f}
178
+ data = {}
179
+ if additional_fields:
180
+ for key, value in additional_fields.items():
181
+ if value is not None:
182
+ data[key] = str(value) if not isinstance(value, str) else value
183
+
184
+ response = self._session.post(
185
+ url, headers=headers, files=files, data=data, timeout=self.timeout
186
+ )
291
187
 
292
- async def get_user_info(self) -> Dict[str, Any]:
293
- """Get current user information."""
294
- return await self.request("GET", "/api/v1/auth/me")
188
+ return self._handle_response(response)
295
189
 
296
- async def close(self):
297
- """Close async client session."""
298
- if self._session and not self._session.closed:
299
- await self._session.close()
190
+ def close(self):
191
+ """Close the client session."""
192
+ self._session.close()
300
193
 
301
- async def __aenter__(self):
194
+ def __enter__(self):
302
195
  return self
303
196
 
304
- async def __aexit__(self, exc_type, exc_val, exc_tb):
305
- await self.close()
306
-
197
+ def __exit__(self, exc_type, exc_val, exc_tb):
198
+ self.close()
audiopod/exceptions.py CHANGED
@@ -2,40 +2,48 @@
2
2
  AudioPod SDK Exceptions
3
3
  """
4
4
 
5
+ from typing import Optional
6
+
5
7
 
6
8
  class AudioPodError(Exception):
7
- """Base exception for AudioPod SDK"""
8
- pass
9
+ """Base exception for AudioPod SDK."""
10
+
11
+ def __init__(self, message: str = "An error occurred"):
12
+ self.message = message
13
+ super().__init__(self.message)
9
14
 
10
15
 
11
16
  class AuthenticationError(AudioPodError):
12
- """Raised when authentication fails"""
13
- pass
17
+ """Raised when authentication fails."""
18
+
19
+ def __init__(self, message: str = "Authentication failed"):
20
+ super().__init__(message)
14
21
 
15
22
 
16
23
  class APIError(AudioPodError):
17
- """Raised when API returns an error"""
18
-
19
- def __init__(self, message: str, status_code: int = None):
20
- super().__init__(message)
24
+ """Raised when an API request fails."""
25
+
26
+ def __init__(self, message: str = "API request failed", status_code: Optional[int] = None):
21
27
  self.status_code = status_code
28
+ super().__init__(message)
22
29
 
23
30
 
24
31
  class RateLimitError(AudioPodError):
25
- """Raised when rate limit is exceeded"""
26
- pass
27
-
32
+ """Raised when rate limit is exceeded."""
28
33
 
29
- class ValidationError(AudioPodError):
30
- """Raised when input validation fails"""
31
- pass
34
+ def __init__(self, message: str = "Rate limit exceeded"):
35
+ super().__init__(message)
32
36
 
33
37
 
34
38
  class InsufficientBalanceError(AudioPodError):
35
- """Raised when wallet balance is insufficient"""
36
-
37
- def __init__(self, message: str, required_cents: int = None, available_cents: int = None):
38
- super().__init__(message)
39
+ """Raised when wallet balance is insufficient."""
40
+
41
+ def __init__(
42
+ self,
43
+ message: str = "Insufficient wallet balance",
44
+ required_cents: Optional[int] = None,
45
+ available_cents: Optional[int] = None,
46
+ ):
39
47
  self.required_cents = required_cents
40
48
  self.available_cents = available_cents
41
-
49
+ super().__init__(message)
@@ -0,0 +1,23 @@
1
+ """AudioPod SDK Resources"""
2
+
3
+ from .transcription import Transcription
4
+ from .voice import Voice
5
+ from .music import Music
6
+ from .stems import StemExtraction
7
+ from .denoiser import Denoiser
8
+ from .speaker import Speaker
9
+ from .wallet import Wallet
10
+
11
+ __all__ = [
12
+ "Transcription",
13
+ "Voice",
14
+ "Music",
15
+ "StemExtraction",
16
+ "Denoiser",
17
+ "Speaker",
18
+ "Wallet",
19
+ ]
20
+
21
+
22
+
23
+
@@ -0,0 +1,116 @@
1
+ """
2
+ Denoiser Service - Audio Noise Reduction
3
+ """
4
+
5
+ import time
6
+ from typing import Optional, List, Dict, Any, TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from ..client import AudioPod
10
+
11
+ POLL_INTERVAL = 3 # seconds
12
+ DEFAULT_TIMEOUT = 600 # 10 minutes
13
+
14
+
15
+ class Denoiser:
16
+ """Audio noise reduction service."""
17
+
18
+ def __init__(self, client: "AudioPod"):
19
+ self._client = client
20
+
21
+ def create(
22
+ self,
23
+ *,
24
+ file: Optional[str] = None,
25
+ url: Optional[str] = None,
26
+ ) -> Dict[str, Any]:
27
+ """
28
+ Create a denoise job.
29
+
30
+ Args:
31
+ file: Path to audio file
32
+ url: URL of audio/video
33
+
34
+ Returns:
35
+ Denoise job object
36
+
37
+ Example:
38
+ >>> job = client.denoiser.create(file="./noisy-audio.mp3")
39
+ """
40
+ if file:
41
+ return self._client.upload(
42
+ "/api/v1/denoiser/denoise",
43
+ file,
44
+ field_name="file",
45
+ )
46
+
47
+ if url:
48
+ return self._client.post(
49
+ "/api/v1/denoiser/denoise",
50
+ json_data={"url": url},
51
+ )
52
+
53
+ raise ValueError("Either file or url must be provided")
54
+
55
+ def get(self, job_id: int) -> Dict[str, Any]:
56
+ """Get a denoise job by ID."""
57
+ return self._client.get(f"/api/v1/denoiser/jobs/{job_id}")
58
+
59
+ def list(
60
+ self,
61
+ *,
62
+ skip: int = 0,
63
+ limit: int = 50,
64
+ status: Optional[str] = None,
65
+ ) -> List[Dict[str, Any]]:
66
+ """List denoise jobs."""
67
+ return self._client.get(
68
+ "/api/v1/denoiser/jobs",
69
+ params={"skip": skip, "limit": limit, "status": status},
70
+ )
71
+
72
+ def delete(self, job_id: int) -> None:
73
+ """Delete a denoise job."""
74
+ self._client.delete(f"/api/v1/denoiser/jobs/{job_id}")
75
+
76
+ def wait_for_completion(
77
+ self, job_id: int, timeout: int = DEFAULT_TIMEOUT
78
+ ) -> Dict[str, Any]:
79
+ """Wait for denoising to complete."""
80
+ start_time = time.time()
81
+
82
+ while time.time() - start_time < timeout:
83
+ job = self.get(job_id)
84
+ status = job.get("status", "")
85
+
86
+ if status == "COMPLETED":
87
+ return job
88
+ if status == "FAILED":
89
+ raise RuntimeError(
90
+ f"Denoising failed: {job.get('error_message', 'Unknown error')}"
91
+ )
92
+
93
+ time.sleep(POLL_INTERVAL)
94
+
95
+ raise TimeoutError(f"Denoising timed out after {timeout}s")
96
+
97
+ def denoise(
98
+ self,
99
+ *,
100
+ file: Optional[str] = None,
101
+ url: Optional[str] = None,
102
+ timeout: int = DEFAULT_TIMEOUT,
103
+ ) -> Dict[str, Any]:
104
+ """
105
+ Denoise audio and wait for completion.
106
+
107
+ Example:
108
+ >>> result = client.denoiser.denoise(file="./noisy-audio.mp3")
109
+ >>> print(result["output_url"])
110
+ """
111
+ job = self.create(file=file, url=url)
112
+ return self.wait_for_completion(job["id"], timeout=timeout)
113
+
114
+
115
+
116
+