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/__init__.py +83 -0
- audiopod/cli.py +285 -0
- audiopod/client.py +332 -0
- audiopod/config.py +63 -0
- audiopod/exceptions.py +96 -0
- audiopod/models.py +235 -0
- audiopod/services/__init__.py +24 -0
- audiopod/services/base.py +213 -0
- audiopod/services/credits.py +46 -0
- audiopod/services/denoiser.py +51 -0
- audiopod/services/karaoke.py +61 -0
- audiopod/services/music.py +434 -0
- audiopod/services/speaker.py +53 -0
- audiopod/services/transcription.py +212 -0
- audiopod/services/translation.py +81 -0
- audiopod/services/voice.py +376 -0
- audiopod-1.0.0.dist-info/METADATA +395 -0
- audiopod-1.0.0.dist-info/RECORD +21 -0
- audiopod-1.0.0.dist-info/WHEEL +5 -0
- audiopod-1.0.0.dist-info/entry_points.txt +2 -0
- audiopod-1.0.0.dist-info/top_level.txt +1 -0
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")
|