audiopod 1.0.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/config.py ADDED
@@ -0,0 +1,63 @@
1
+ """
2
+ AudioPod Client Configuration
3
+ """
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Optional
7
+
8
+
9
+ @dataclass
10
+ class ClientConfig:
11
+ """Configuration for AudioPod API client"""
12
+
13
+ # API settings
14
+ base_url: str = "https://api.audiopod.ai"
15
+ api_version: str = "v1"
16
+
17
+ # Request settings
18
+ timeout: int = 30
19
+ max_retries: int = 3
20
+ verify_ssl: bool = True
21
+
22
+ # Client settings
23
+ debug: bool = False
24
+ version: str = "1.0.0"
25
+
26
+ # Rate limiting
27
+ rate_limit_per_minute: int = 600
28
+
29
+ # File upload limits
30
+ max_file_size_mb: int = 100
31
+ supported_audio_formats: tuple = (
32
+ ".mp3", ".wav", ".m4a", ".flac", ".ogg",
33
+ ".aac", ".wma", ".aiff", ".au"
34
+ )
35
+ supported_video_formats: tuple = (
36
+ ".mp4", ".avi", ".mov", ".mkv", ".wmv",
37
+ ".flv", ".webm", ".m4v"
38
+ )
39
+
40
+ @property
41
+ def api_base_url(self) -> str:
42
+ """Get full API base URL with version"""
43
+ return f"{self.base_url}/api/{self.api_version}"
44
+
45
+ def validate_file_format(self, filename: str, file_type: str = "audio") -> bool:
46
+ """
47
+ Validate if file format is supported
48
+
49
+ Args:
50
+ filename: Name of the file
51
+ file_type: Type of file ('audio' or 'video')
52
+
53
+ Returns:
54
+ True if format is supported
55
+ """
56
+ filename = filename.lower()
57
+
58
+ if file_type == "audio":
59
+ return any(filename.endswith(fmt) for fmt in self.supported_audio_formats)
60
+ elif file_type == "video":
61
+ return any(filename.endswith(fmt) for fmt in self.supported_video_formats)
62
+ else:
63
+ return False
audiopod/exceptions.py ADDED
@@ -0,0 +1,96 @@
1
+ """
2
+ AudioPod API Client Exceptions
3
+ """
4
+
5
+ from typing import Optional, Dict, Any
6
+
7
+
8
+ 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
18
+
19
+
20
+ class AuthenticationError(AudioPodError):
21
+ """Raised when API key authentication fails"""
22
+ pass
23
+
24
+
25
+ class APIError(AudioPodError):
26
+ """Raised when API returns an error response"""
27
+
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)
35
+ self.status_code = status_code
36
+
37
+
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
49
+
50
+
51
+ class ValidationError(AudioPodError):
52
+ """Raised when input validation fails"""
53
+ pass
54
+
55
+
56
+ class ProcessingError(APIError):
57
+ """Raised when audio processing fails"""
58
+
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
+
83
+
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
audiopod/models.py ADDED
@@ -0,0 +1,235 @@
1
+ """
2
+ AudioPod API Client Models
3
+ Data structures for API responses
4
+ """
5
+
6
+ from datetime import datetime
7
+ from typing import Optional, Dict, Any, List, Union
8
+ from dataclasses import dataclass
9
+ from enum import Enum
10
+
11
+
12
+ class JobStatus(str, Enum):
13
+ """Job processing status"""
14
+ PENDING = "pending"
15
+ PROCESSING = "processing"
16
+ COMPLETED = "completed"
17
+ FAILED = "failed"
18
+ CANCELLED = "cancelled"
19
+
20
+
21
+ class VoiceType(str, Enum):
22
+ """Voice profile types"""
23
+ CUSTOM = "custom"
24
+ STANDARD = "standard"
25
+
26
+
27
+ class TTSProvider(str, Enum):
28
+ """Text-to-speech providers"""
29
+ AUDIOPOD_SONIC = "audiopod_sonic"
30
+ OPENAI = "openai"
31
+ GOOGLE_GEMINI = "google_gemini"
32
+
33
+
34
+ @dataclass
35
+ class Job:
36
+ """Base job information"""
37
+ id: int
38
+ status: JobStatus
39
+ created_at: datetime
40
+ updated_at: Optional[datetime] = None
41
+ completed_at: Optional[datetime] = None
42
+ progress: float = 0.0
43
+ error_message: Optional[str] = None
44
+ parameters: Optional[Dict[str, Any]] = None
45
+ result: Optional[Dict[str, Any]] = None
46
+
47
+ @classmethod
48
+ def from_dict(cls, data: Dict[str, Any]) -> 'Job':
49
+ """Create Job from API response data"""
50
+ return cls(
51
+ id=data['id'],
52
+ status=JobStatus(data['status']),
53
+ created_at=datetime.fromisoformat(data['created_at'].replace('Z', '+00:00')),
54
+ updated_at=datetime.fromisoformat(data['updated_at'].replace('Z', '+00:00')) if data.get('updated_at') else None,
55
+ completed_at=datetime.fromisoformat(data['completed_at'].replace('Z', '+00:00')) if data.get('completed_at') else None,
56
+ progress=data.get('progress', 0.0),
57
+ error_message=data.get('error_message'),
58
+ parameters=data.get('parameters'),
59
+ result=data.get('result')
60
+ )
61
+
62
+
63
+ @dataclass
64
+ class VoiceProfile:
65
+ """Voice profile information"""
66
+ id: int
67
+ uuid: str
68
+ name: str
69
+ display_name: Optional[str]
70
+ description: Optional[str]
71
+ voice_type: VoiceType
72
+ provider: TTSProvider
73
+ is_public: bool
74
+ language_code: Optional[str] = None
75
+ language_name: Optional[str] = None
76
+ gender: Optional[str] = None
77
+ accent: Optional[str] = None
78
+ created_at: Optional[datetime] = None
79
+ status: Optional[JobStatus] = None
80
+
81
+ @classmethod
82
+ def from_dict(cls, data: Dict[str, Any]) -> 'VoiceProfile':
83
+ """Create VoiceProfile from API response data"""
84
+ return cls(
85
+ id=data['id'],
86
+ uuid=data['uuid'],
87
+ name=data['name'],
88
+ display_name=data.get('display_name'),
89
+ description=data.get('description'),
90
+ voice_type=VoiceType(data['voice_type']),
91
+ provider=TTSProvider(data['provider']),
92
+ is_public=data['is_public'],
93
+ language_code=data.get('language_code'),
94
+ language_name=data.get('language_name'),
95
+ gender=data.get('gender'),
96
+ accent=data.get('accent'),
97
+ created_at=datetime.fromisoformat(data['created_at'].replace('Z', '+00:00')) if data.get('created_at') else None,
98
+ status=JobStatus(data['status']) if data.get('status') else None
99
+ )
100
+
101
+
102
+ @dataclass
103
+ class TranscriptionResult:
104
+ """Transcription job result"""
105
+ job: Job
106
+ transcript: Optional[str] = None
107
+ detected_language: Optional[str] = None
108
+ confidence_score: Optional[float] = None
109
+ segments: Optional[List[Dict[str, Any]]] = None
110
+ audio_duration: Optional[float] = None
111
+
112
+ @classmethod
113
+ def from_dict(cls, data: Dict[str, Any]) -> 'TranscriptionResult':
114
+ """Create TranscriptionResult from API response data"""
115
+ return cls(
116
+ job=Job.from_dict(data),
117
+ transcript=data.get('transcript'),
118
+ detected_language=data.get('detected_language'),
119
+ confidence_score=data.get('confidence_score'),
120
+ segments=data.get('segments'),
121
+ audio_duration=data.get('total_duration')
122
+ )
123
+
124
+
125
+ @dataclass
126
+ class MusicGenerationResult:
127
+ """Music generation job result"""
128
+ job: Job
129
+ output_url: Optional[str] = None
130
+ output_urls: Optional[Dict[str, str]] = None # Format -> URL mapping
131
+ audio_duration: Optional[float] = None
132
+ actual_seeds: Optional[List[int]] = None
133
+ share_token: Optional[str] = None
134
+ share_url: Optional[str] = None
135
+ is_shared: bool = False
136
+
137
+ @classmethod
138
+ def from_dict(cls, data: Dict[str, Any]) -> 'MusicGenerationResult':
139
+ """Create MusicGenerationResult from API response data"""
140
+ return cls(
141
+ job=Job.from_dict(data),
142
+ output_url=data.get('output_url'),
143
+ output_urls=data.get('output_urls'),
144
+ audio_duration=data.get('audio_duration'),
145
+ actual_seeds=data.get('actual_seeds'),
146
+ share_token=data.get('share_token'),
147
+ share_url=data.get('share_url'),
148
+ is_shared=data.get('is_shared', False)
149
+ )
150
+
151
+
152
+ @dataclass
153
+ class TranslationResult:
154
+ """Translation job result"""
155
+ job: Job
156
+ source_language: Optional[str] = None
157
+ target_language: Optional[str] = None
158
+ audio_output_url: Optional[str] = None
159
+ video_output_url: Optional[str] = None
160
+ transcript_path: Optional[str] = None
161
+
162
+ @classmethod
163
+ def from_dict(cls, data: Dict[str, Any]) -> 'TranslationResult':
164
+ """Create TranslationResult from API response data"""
165
+ return cls(
166
+ job=Job.from_dict(data),
167
+ source_language=data.get('source_language'),
168
+ target_language=data.get('target_language'),
169
+ audio_output_url=data.get('audio_output_path'),
170
+ video_output_url=data.get('video_output_path'),
171
+ transcript_path=data.get('transcript_path')
172
+ )
173
+
174
+
175
+ @dataclass
176
+ class SpeakerAnalysisResult:
177
+ """Speaker analysis job result"""
178
+ job: Job
179
+ num_speakers: Optional[int] = None
180
+ speaker_segments: Optional[List[Dict[str, Any]]] = None
181
+ output_paths: Optional[Dict[str, str]] = None
182
+ rttm_path: Optional[str] = None
183
+
184
+ @classmethod
185
+ def from_dict(cls, data: Dict[str, Any]) -> 'SpeakerAnalysisResult':
186
+ """Create SpeakerAnalysisResult from API response data"""
187
+ return cls(
188
+ job=Job.from_dict(data),
189
+ num_speakers=data.get('num_speakers'),
190
+ speaker_segments=data.get('speaker_segments'),
191
+ output_paths=data.get('output_paths'),
192
+ rttm_path=data.get('rttm_path')
193
+ )
194
+
195
+
196
+ @dataclass
197
+ class DenoiseResult:
198
+ """Audio denoising job result"""
199
+ job: Job
200
+ output_url: Optional[str] = None
201
+ video_output_url: Optional[str] = None
202
+ stats: Optional[Dict[str, Any]] = None
203
+ is_video: bool = False
204
+
205
+ @classmethod
206
+ def from_dict(cls, data: Dict[str, Any]) -> 'DenoiseResult':
207
+ """Create DenoiseResult from API response data"""
208
+ return cls(
209
+ job=Job.from_dict(data),
210
+ output_url=data.get('output_path'),
211
+ video_output_url=data.get('video_output_path'),
212
+ stats=data.get('stats'),
213
+ is_video=data.get('is_video', False)
214
+ )
215
+
216
+
217
+ @dataclass
218
+ class CreditInfo:
219
+ """User credit information"""
220
+ balance: int
221
+ payg_balance: int
222
+ total_available_credits: int
223
+ next_reset_date: Optional[datetime] = None
224
+ total_credits_used: int = 0
225
+
226
+ @classmethod
227
+ def from_dict(cls, data: Dict[str, Any]) -> 'CreditInfo':
228
+ """Create CreditInfo from API response data"""
229
+ return cls(
230
+ balance=data['balance'],
231
+ payg_balance=data['payg_balance'],
232
+ total_available_credits=data['total_available_credits'],
233
+ next_reset_date=datetime.fromisoformat(data['next_reset_date'].replace('Z', '+00:00')) if data.get('next_reset_date') else None,
234
+ total_credits_used=data.get('total_credits_used', 0)
235
+ )
@@ -0,0 +1,24 @@
1
+ """
2
+ AudioPod API Services
3
+ Service classes for different API endpoints
4
+ """
5
+
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 .karaoke import KaraokeService
13
+ from .credits import CreditService
14
+
15
+ __all__ = [
16
+ "VoiceService",
17
+ "MusicService",
18
+ "TranscriptionService",
19
+ "TranslationService",
20
+ "SpeakerService",
21
+ "DenoiserService",
22
+ "KaraokeService",
23
+ "CreditService"
24
+ ]
@@ -0,0 +1,213 @@
1
+ """
2
+ Base service class for AudioPod API services
3
+ """
4
+
5
+ import os
6
+ 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
15
+
16
+
17
+ class BaseService:
18
+ """Base class for all AudioPod API services"""
19
+
20
+ def __init__(self, client: Union["Client", "AsyncClient"], async_mode: bool = False):
21
+ self.client = client
22
+ 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
+
80
+ 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
+ """
100
+ start_time = time.time()
101
+
102
+ 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
+
119
+ 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
+
126
+ 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
+ """
146
+ import asyncio
147
+
148
+ start_time = time.time()
149
+
150
+ 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
+
167
+ 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()
@@ -0,0 +1,46 @@
1
+ """
2
+ Credits Service - User credits and usage tracking
3
+ """
4
+
5
+ from typing import List, Dict, Any
6
+ from .base import BaseService
7
+ from ..models import CreditInfo
8
+
9
+
10
+ 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"""
15
+ 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
+
26
+ def get_usage_history(self) -> List[Dict[str, Any]]:
27
+ """Get credit usage history"""
28
+ if self.async_mode:
29
+ return self._async_get_usage_history()
30
+ else:
31
+ return self.client.request("GET", "/api/v1/credits/usage")
32
+
33
+ async def _async_get_usage_history(self) -> List[Dict[str, Any]]:
34
+ """Async version of get_usage_history"""
35
+ 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"""
39
+ 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"""
46
+ return await self.client.request("GET", "/api/v1/credits/multipliers")