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