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/__init__.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AudioPod API Client
|
|
3
|
+
Professional Audio Processing SDK for Python
|
|
4
|
+
|
|
5
|
+
This package provides a comprehensive Python SDK for the AudioPod API,
|
|
6
|
+
enabling developers to integrate advanced audio processing capabilities
|
|
7
|
+
into their applications.
|
|
8
|
+
|
|
9
|
+
Basic Usage:
|
|
10
|
+
>>> import audiopod
|
|
11
|
+
>>> client = audiopod.Client(api_key="your-api-key")
|
|
12
|
+
>>>
|
|
13
|
+
>>> # Voice cloning
|
|
14
|
+
>>> job = client.voice.clone_voice(
|
|
15
|
+
... voice_file="path/to/voice.wav",
|
|
16
|
+
... text="Hello, this is a cloned voice!"
|
|
17
|
+
... )
|
|
18
|
+
>>>
|
|
19
|
+
>>> # Music generation
|
|
20
|
+
>>> music = client.music.generate(
|
|
21
|
+
... prompt="upbeat electronic dance music"
|
|
22
|
+
... )
|
|
23
|
+
>>>
|
|
24
|
+
>>> # Audio transcription
|
|
25
|
+
>>> transcript = client.transcription.transcribe(
|
|
26
|
+
... audio_file="path/to/audio.mp3",
|
|
27
|
+
... language="en"
|
|
28
|
+
... )
|
|
29
|
+
|
|
30
|
+
For more examples and documentation, visit: https://docs.audiopod.ai
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from .client import Client, AsyncClient
|
|
34
|
+
from .exceptions import (
|
|
35
|
+
AudioPodError,
|
|
36
|
+
AuthenticationError,
|
|
37
|
+
APIError,
|
|
38
|
+
RateLimitError,
|
|
39
|
+
ValidationError,
|
|
40
|
+
ProcessingError
|
|
41
|
+
)
|
|
42
|
+
from .models import (
|
|
43
|
+
Job,
|
|
44
|
+
VoiceProfile,
|
|
45
|
+
TranscriptionResult,
|
|
46
|
+
MusicGenerationResult,
|
|
47
|
+
TranslationResult
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
__version__ = "1.0.0"
|
|
51
|
+
__author__ = "AudioPod AI"
|
|
52
|
+
__email__ = "support@audiopod.ai"
|
|
53
|
+
__license__ = "MIT"
|
|
54
|
+
|
|
55
|
+
# Public API
|
|
56
|
+
__all__ = [
|
|
57
|
+
# Main clients
|
|
58
|
+
"Client",
|
|
59
|
+
"AsyncClient",
|
|
60
|
+
|
|
61
|
+
# Exceptions
|
|
62
|
+
"AudioPodError",
|
|
63
|
+
"AuthenticationError",
|
|
64
|
+
"APIError",
|
|
65
|
+
"RateLimitError",
|
|
66
|
+
"ValidationError",
|
|
67
|
+
"ProcessingError",
|
|
68
|
+
|
|
69
|
+
# Models
|
|
70
|
+
"Job",
|
|
71
|
+
"VoiceProfile",
|
|
72
|
+
"TranscriptionResult",
|
|
73
|
+
"MusicGenerationResult",
|
|
74
|
+
"TranslationResult",
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
# Package metadata
|
|
78
|
+
__all__.extend([
|
|
79
|
+
"__version__",
|
|
80
|
+
"__author__",
|
|
81
|
+
"__email__",
|
|
82
|
+
"__license__"
|
|
83
|
+
])
|
audiopod/cli.py
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AudioPod CLI - Command Line Interface
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import json
|
|
8
|
+
import click
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from .client import Client
|
|
12
|
+
from .exceptions import AudioPodError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@click.group()
|
|
16
|
+
@click.option('--api-key', envvar='AUDIOPOD_API_KEY', help='AudioPod API key')
|
|
17
|
+
@click.option('--base-url', default='https://api.audiopod.ai', help='API base URL')
|
|
18
|
+
@click.option('--debug', is_flag=True, help='Enable debug logging')
|
|
19
|
+
@click.pass_context
|
|
20
|
+
def cli(ctx, api_key: str, base_url: str, debug: bool):
|
|
21
|
+
"""AudioPod CLI - Professional Audio Processing powered by AI"""
|
|
22
|
+
ctx.ensure_object(dict)
|
|
23
|
+
|
|
24
|
+
if not api_key:
|
|
25
|
+
click.echo("Error: API key is required. Set AUDIOPOD_API_KEY environment variable or use --api-key option.")
|
|
26
|
+
sys.exit(1)
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
ctx.obj['client'] = Client(
|
|
30
|
+
api_key=api_key,
|
|
31
|
+
base_url=base_url,
|
|
32
|
+
debug=debug
|
|
33
|
+
)
|
|
34
|
+
except AudioPodError as e:
|
|
35
|
+
click.echo(f"Error: {e.message}")
|
|
36
|
+
sys.exit(1)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@cli.command()
|
|
40
|
+
@click.pass_context
|
|
41
|
+
def health(ctx):
|
|
42
|
+
"""Check API health status"""
|
|
43
|
+
try:
|
|
44
|
+
client = ctx.obj['client']
|
|
45
|
+
status = client.check_health()
|
|
46
|
+
click.echo(f"API Status: {status.get('status', 'Unknown')}")
|
|
47
|
+
except AudioPodError as e:
|
|
48
|
+
click.echo(f"Health check failed: {e.message}")
|
|
49
|
+
sys.exit(1)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@cli.group()
|
|
53
|
+
def credits():
|
|
54
|
+
"""Credit management commands"""
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@credits.command('balance')
|
|
59
|
+
@click.pass_context
|
|
60
|
+
def credits_balance(ctx):
|
|
61
|
+
"""Get current credit balance"""
|
|
62
|
+
try:
|
|
63
|
+
client = ctx.obj['client']
|
|
64
|
+
credits = client.credits.get_credit_balance()
|
|
65
|
+
|
|
66
|
+
click.echo("Credit Balance:")
|
|
67
|
+
click.echo(f" Subscription Credits: {credits.balance:,}")
|
|
68
|
+
click.echo(f" Pay-as-you-go Credits: {credits.payg_balance:,}")
|
|
69
|
+
click.echo(f" Total Available: {credits.total_available_credits:,}")
|
|
70
|
+
click.echo(f" Total Used: {credits.total_credits_used:,}")
|
|
71
|
+
if credits.next_reset_date:
|
|
72
|
+
click.echo(f" Next Reset: {credits.next_reset_date}")
|
|
73
|
+
|
|
74
|
+
except AudioPodError as e:
|
|
75
|
+
click.echo(f"Failed to get credit balance: {e.message}")
|
|
76
|
+
sys.exit(1)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@credits.command('usage')
|
|
80
|
+
@click.pass_context
|
|
81
|
+
def credits_usage(ctx):
|
|
82
|
+
"""Get credit usage history"""
|
|
83
|
+
try:
|
|
84
|
+
client = ctx.obj['client']
|
|
85
|
+
usage = client.credits.get_usage_history()
|
|
86
|
+
|
|
87
|
+
if not usage:
|
|
88
|
+
click.echo("No usage history found.")
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
click.echo("Recent Credit Usage:")
|
|
92
|
+
for record in usage[:10]: # Show last 10 records
|
|
93
|
+
click.echo(f" {record['created_at']}: {record['service_type']} - {record['credits_used']} credits")
|
|
94
|
+
|
|
95
|
+
except AudioPodError as e:
|
|
96
|
+
click.echo(f"Failed to get usage history: {e.message}")
|
|
97
|
+
sys.exit(1)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@cli.group()
|
|
101
|
+
def voice():
|
|
102
|
+
"""Voice cloning and TTS commands"""
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@voice.command('clone')
|
|
107
|
+
@click.argument('voice_file', type=click.Path(exists=True))
|
|
108
|
+
@click.argument('text')
|
|
109
|
+
@click.option('--language', '-l', help='Target language code (e.g., en, es)')
|
|
110
|
+
@click.option('--speed', '-s', default=1.0, help='Speech speed (0.5-2.0)')
|
|
111
|
+
@click.option('--wait', is_flag=True, help='Wait for completion')
|
|
112
|
+
@click.option('--output', '-o', help='Output file path')
|
|
113
|
+
@click.pass_context
|
|
114
|
+
def voice_clone(ctx, voice_file: str, text: str, language: Optional[str],
|
|
115
|
+
speed: float, wait: bool, output: Optional[str]):
|
|
116
|
+
"""Clone a voice from audio file"""
|
|
117
|
+
try:
|
|
118
|
+
client = ctx.obj['client']
|
|
119
|
+
|
|
120
|
+
click.echo(f"Cloning voice from {voice_file}...")
|
|
121
|
+
|
|
122
|
+
job = client.voice.clone_voice(
|
|
123
|
+
voice_file=voice_file,
|
|
124
|
+
text=text,
|
|
125
|
+
language=language,
|
|
126
|
+
speed=speed,
|
|
127
|
+
wait_for_completion=wait
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
if wait:
|
|
131
|
+
click.echo(f"Voice cloning completed!")
|
|
132
|
+
if 'output_url' in job:
|
|
133
|
+
click.echo(f"Generated audio URL: {job['output_url']}")
|
|
134
|
+
if output:
|
|
135
|
+
# Here you could add download functionality
|
|
136
|
+
click.echo(f"To download: curl -o {output} '{job['output_url']}'")
|
|
137
|
+
else:
|
|
138
|
+
click.echo(f"Voice cloning job started with ID: {job.id}")
|
|
139
|
+
click.echo(f"Check status with: audiopod voice status {job.id}")
|
|
140
|
+
|
|
141
|
+
except AudioPodError as e:
|
|
142
|
+
click.echo(f"Voice cloning failed: {e.message}")
|
|
143
|
+
sys.exit(1)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@voice.command('list')
|
|
147
|
+
@click.option('--limit', default=20, help='Maximum number of voices to show')
|
|
148
|
+
@click.pass_context
|
|
149
|
+
def voice_list(ctx, limit: int):
|
|
150
|
+
"""List available voice profiles"""
|
|
151
|
+
try:
|
|
152
|
+
client = ctx.obj['client']
|
|
153
|
+
voices = client.voice.list_voice_profiles(limit=limit)
|
|
154
|
+
|
|
155
|
+
if not voices:
|
|
156
|
+
click.echo("No voice profiles found.")
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
click.echo("Available Voice Profiles:")
|
|
160
|
+
for voice in voices:
|
|
161
|
+
status_icon = "✓" if voice.status == "completed" else "⏳"
|
|
162
|
+
visibility = "Public" if voice.is_public else "Private"
|
|
163
|
+
click.echo(f" {status_icon} {voice.name} (ID: {voice.id}) - {visibility}")
|
|
164
|
+
if voice.description:
|
|
165
|
+
click.echo(f" {voice.description}")
|
|
166
|
+
|
|
167
|
+
except AudioPodError as e:
|
|
168
|
+
click.echo(f"Failed to list voices: {e.message}")
|
|
169
|
+
sys.exit(1)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@cli.group()
|
|
173
|
+
def music():
|
|
174
|
+
"""Music generation commands"""
|
|
175
|
+
pass
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@music.command('generate')
|
|
179
|
+
@click.argument('prompt')
|
|
180
|
+
@click.option('--duration', '-d', default=120.0, help='Duration in seconds')
|
|
181
|
+
@click.option('--wait', is_flag=True, help='Wait for completion')
|
|
182
|
+
@click.option('--output', '-o', help='Output file path')
|
|
183
|
+
@click.pass_context
|
|
184
|
+
def music_generate(ctx, prompt: str, duration: float, wait: bool, output: Optional[str]):
|
|
185
|
+
"""Generate music from text prompt"""
|
|
186
|
+
try:
|
|
187
|
+
client = ctx.obj['client']
|
|
188
|
+
|
|
189
|
+
click.echo(f"Generating music: '{prompt}'...")
|
|
190
|
+
|
|
191
|
+
job = client.music.generate_music(
|
|
192
|
+
prompt=prompt,
|
|
193
|
+
duration=duration,
|
|
194
|
+
wait_for_completion=wait
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
if wait:
|
|
198
|
+
click.echo("Music generation completed!")
|
|
199
|
+
if hasattr(job, 'output_url') and job.output_url:
|
|
200
|
+
click.echo(f"Generated music URL: {job.output_url}")
|
|
201
|
+
if output:
|
|
202
|
+
click.echo(f"To download: curl -o {output} '{job.output_url}'")
|
|
203
|
+
else:
|
|
204
|
+
click.echo(f"Music generation job started with ID: {job.id}")
|
|
205
|
+
click.echo(f"Check status with: audiopod music status {job.id}")
|
|
206
|
+
|
|
207
|
+
except AudioPodError as e:
|
|
208
|
+
click.echo(f"Music generation failed: {e.message}")
|
|
209
|
+
sys.exit(1)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@music.command('list')
|
|
213
|
+
@click.option('--limit', default=20, help='Maximum number of tracks to show')
|
|
214
|
+
@click.pass_context
|
|
215
|
+
def music_list(ctx, limit: int):
|
|
216
|
+
"""List generated music tracks"""
|
|
217
|
+
try:
|
|
218
|
+
client = ctx.obj['client']
|
|
219
|
+
tracks = client.music.list_music_jobs(limit=limit)
|
|
220
|
+
|
|
221
|
+
if not tracks:
|
|
222
|
+
click.echo("No music tracks found.")
|
|
223
|
+
return
|
|
224
|
+
|
|
225
|
+
click.echo("Generated Music Tracks:")
|
|
226
|
+
for track in tracks:
|
|
227
|
+
status_icon = "✓" if track.job.status == "completed" else "⏳"
|
|
228
|
+
prompt = track.job.parameters.get('prompt', 'N/A') if track.job.parameters else 'N/A'
|
|
229
|
+
click.echo(f" {status_icon} Job {track.job.id}: '{prompt[:50]}...'")
|
|
230
|
+
|
|
231
|
+
except AudioPodError as e:
|
|
232
|
+
click.echo(f"Failed to list music tracks: {e.message}")
|
|
233
|
+
sys.exit(1)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@cli.group()
|
|
237
|
+
def transcription():
|
|
238
|
+
"""Transcription commands"""
|
|
239
|
+
pass
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@transcription.command('transcribe')
|
|
243
|
+
@click.argument('audio_file', type=click.Path(exists=True))
|
|
244
|
+
@click.option('--language', '-l', help='Language code (auto-detect if not specified)')
|
|
245
|
+
@click.option('--speakers', is_flag=True, help='Enable speaker diarization')
|
|
246
|
+
@click.option('--wait', is_flag=True, help='Wait for completion')
|
|
247
|
+
@click.option('--format', '-f', default='txt', help='Output format (txt, json, srt)')
|
|
248
|
+
@click.pass_context
|
|
249
|
+
def transcription_transcribe(ctx, audio_file: str, language: Optional[str],
|
|
250
|
+
speakers: bool, wait: bool, format: str):
|
|
251
|
+
"""Transcribe audio to text"""
|
|
252
|
+
try:
|
|
253
|
+
client = ctx.obj['client']
|
|
254
|
+
|
|
255
|
+
click.echo(f"Transcribing {audio_file}...")
|
|
256
|
+
|
|
257
|
+
job = client.transcription.transcribe_audio(
|
|
258
|
+
audio_file=audio_file,
|
|
259
|
+
language=language,
|
|
260
|
+
enable_speaker_diarization=speakers,
|
|
261
|
+
wait_for_completion=wait
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
if wait:
|
|
265
|
+
click.echo("Transcription completed!")
|
|
266
|
+
if hasattr(job, 'transcript') and job.transcript:
|
|
267
|
+
click.echo("Transcript:")
|
|
268
|
+
click.echo(job.transcript)
|
|
269
|
+
if speakers and hasattr(job, 'segments') and job.segments:
|
|
270
|
+
click.echo(f"\nFound {len(job.segments)} speaker segments")
|
|
271
|
+
else:
|
|
272
|
+
click.echo(f"Transcription job started with ID: {job.id}")
|
|
273
|
+
|
|
274
|
+
except AudioPodError as e:
|
|
275
|
+
click.echo(f"Transcription failed: {e.message}")
|
|
276
|
+
sys.exit(1)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def main():
|
|
280
|
+
"""Main CLI entry point"""
|
|
281
|
+
cli()
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
if __name__ == '__main__':
|
|
285
|
+
main()
|
audiopod/client.py
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AudioPod API Client
|
|
3
|
+
Main client classes for synchronous and asynchronous API access
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Optional, Dict, Any, Union
|
|
9
|
+
from urllib.parse import urljoin
|
|
10
|
+
|
|
11
|
+
import requests
|
|
12
|
+
import aiohttp
|
|
13
|
+
from requests.adapters import HTTPAdapter
|
|
14
|
+
from urllib3.util.retry import Retry
|
|
15
|
+
|
|
16
|
+
from .config import ClientConfig
|
|
17
|
+
from .exceptions import AuthenticationError, APIError, RateLimitError
|
|
18
|
+
from .services import (
|
|
19
|
+
VoiceService,
|
|
20
|
+
MusicService,
|
|
21
|
+
TranscriptionService,
|
|
22
|
+
TranslationService,
|
|
23
|
+
SpeakerService,
|
|
24
|
+
DenoiserService,
|
|
25
|
+
KaraokeService,
|
|
26
|
+
CreditService
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class BaseClient:
|
|
33
|
+
"""Base client with common functionality"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
api_key: Optional[str] = None,
|
|
38
|
+
base_url: Optional[str] = None,
|
|
39
|
+
timeout: int = 30,
|
|
40
|
+
max_retries: int = 3,
|
|
41
|
+
verify_ssl: bool = True,
|
|
42
|
+
debug: bool = False
|
|
43
|
+
):
|
|
44
|
+
"""
|
|
45
|
+
Initialize the AudioPod API client
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
api_key: Your AudioPod API key. If None, will try to read from AUDIOPOD_API_KEY env var
|
|
49
|
+
base_url: API base URL. Defaults to production endpoint
|
|
50
|
+
timeout: Request timeout in seconds
|
|
51
|
+
max_retries: Maximum number of retries for failed requests
|
|
52
|
+
verify_ssl: Whether to verify SSL certificates
|
|
53
|
+
debug: Enable debug logging
|
|
54
|
+
"""
|
|
55
|
+
# Set up logging
|
|
56
|
+
if debug:
|
|
57
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
58
|
+
|
|
59
|
+
# Get API key
|
|
60
|
+
self.api_key = api_key or os.getenv("AUDIOPOD_API_KEY")
|
|
61
|
+
if not self.api_key:
|
|
62
|
+
raise AuthenticationError(
|
|
63
|
+
"API key is required. Pass it as 'api_key' parameter or set AUDIOPOD_API_KEY environment variable."
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Validate API key format
|
|
67
|
+
if not self.api_key.startswith("ap_"):
|
|
68
|
+
raise AuthenticationError("Invalid API key format. AudioPod API keys start with 'ap_'")
|
|
69
|
+
|
|
70
|
+
# Configuration
|
|
71
|
+
self.config = ClientConfig(
|
|
72
|
+
base_url=base_url or "https://api.audiopod.ai",
|
|
73
|
+
timeout=timeout,
|
|
74
|
+
max_retries=max_retries,
|
|
75
|
+
verify_ssl=verify_ssl,
|
|
76
|
+
debug=debug
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def _get_headers(self) -> Dict[str, str]:
|
|
80
|
+
"""Get standard headers for API requests"""
|
|
81
|
+
return {
|
|
82
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
83
|
+
"Content-Type": "application/json",
|
|
84
|
+
"User-Agent": f"audiopod-python/{self.config.version}",
|
|
85
|
+
"Accept": "application/json"
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
def _handle_response(self, response: requests.Response) -> Dict[str, Any]:
|
|
89
|
+
"""Handle API response and raise appropriate exceptions"""
|
|
90
|
+
try:
|
|
91
|
+
response.raise_for_status()
|
|
92
|
+
return response.json()
|
|
93
|
+
except requests.exceptions.HTTPError as e:
|
|
94
|
+
if response.status_code == 401:
|
|
95
|
+
raise AuthenticationError("Invalid API key or authentication failed")
|
|
96
|
+
elif response.status_code == 429:
|
|
97
|
+
raise RateLimitError("Rate limit exceeded. Please try again later.")
|
|
98
|
+
elif response.status_code >= 400:
|
|
99
|
+
try:
|
|
100
|
+
error_data = response.json()
|
|
101
|
+
message = error_data.get("detail", str(e))
|
|
102
|
+
except:
|
|
103
|
+
message = str(e)
|
|
104
|
+
raise APIError(f"API request failed: {message}", status_code=response.status_code)
|
|
105
|
+
else:
|
|
106
|
+
raise APIError(f"Unexpected HTTP error: {e}")
|
|
107
|
+
except requests.exceptions.RequestException as e:
|
|
108
|
+
raise APIError(f"Request failed: {e}")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class Client(BaseClient):
|
|
112
|
+
"""
|
|
113
|
+
Synchronous AudioPod API Client
|
|
114
|
+
|
|
115
|
+
Provides access to all AudioPod services through a simple Python interface.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
def __init__(self, **kwargs):
|
|
119
|
+
super().__init__(**kwargs)
|
|
120
|
+
|
|
121
|
+
# Configure session with retries
|
|
122
|
+
self.session = requests.Session()
|
|
123
|
+
retry_strategy = Retry(
|
|
124
|
+
total=self.config.max_retries,
|
|
125
|
+
status_forcelist=[429, 500, 502, 503, 504],
|
|
126
|
+
method_whitelist=["HEAD", "GET", "OPTIONS", "POST"],
|
|
127
|
+
backoff_factor=1
|
|
128
|
+
)
|
|
129
|
+
adapter = HTTPAdapter(max_retries=retry_strategy)
|
|
130
|
+
self.session.mount("http://", adapter)
|
|
131
|
+
self.session.mount("https://", adapter)
|
|
132
|
+
|
|
133
|
+
# Initialize services
|
|
134
|
+
self.voice = VoiceService(self)
|
|
135
|
+
self.music = MusicService(self)
|
|
136
|
+
self.transcription = TranscriptionService(self)
|
|
137
|
+
self.translation = TranslationService(self)
|
|
138
|
+
self.speaker = SpeakerService(self)
|
|
139
|
+
self.denoiser = DenoiserService(self)
|
|
140
|
+
self.karaoke = KaraokeService(self)
|
|
141
|
+
self.credits = CreditService(self)
|
|
142
|
+
|
|
143
|
+
def request(
|
|
144
|
+
self,
|
|
145
|
+
method: str,
|
|
146
|
+
endpoint: str,
|
|
147
|
+
data: Optional[Dict[str, Any]] = None,
|
|
148
|
+
files: Optional[Dict[str, Any]] = None,
|
|
149
|
+
params: Optional[Dict[str, Any]] = None,
|
|
150
|
+
**kwargs
|
|
151
|
+
) -> Dict[str, Any]:
|
|
152
|
+
"""
|
|
153
|
+
Make a request to the AudioPod API
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
method: HTTP method (GET, POST, etc.)
|
|
157
|
+
endpoint: API endpoint path
|
|
158
|
+
data: JSON data to send
|
|
159
|
+
files: Files to upload
|
|
160
|
+
params: URL parameters
|
|
161
|
+
**kwargs: Additional requests parameters
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
API response data
|
|
165
|
+
"""
|
|
166
|
+
url = urljoin(self.config.base_url, endpoint)
|
|
167
|
+
headers = self._get_headers()
|
|
168
|
+
|
|
169
|
+
# Handle file uploads (don't set Content-Type for multipart)
|
|
170
|
+
if files:
|
|
171
|
+
headers.pop("Content-Type", None)
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
response = self.session.request(
|
|
175
|
+
method=method,
|
|
176
|
+
url=url,
|
|
177
|
+
headers=headers,
|
|
178
|
+
json=data,
|
|
179
|
+
files=files,
|
|
180
|
+
params=params,
|
|
181
|
+
timeout=self.config.timeout,
|
|
182
|
+
verify=self.config.verify_ssl,
|
|
183
|
+
**kwargs
|
|
184
|
+
)
|
|
185
|
+
return self._handle_response(response)
|
|
186
|
+
|
|
187
|
+
except Exception as e:
|
|
188
|
+
logger.error(f"Request failed: {method} {url} - {e}")
|
|
189
|
+
raise
|
|
190
|
+
|
|
191
|
+
def get_user_info(self) -> Dict[str, Any]:
|
|
192
|
+
"""Get current user information"""
|
|
193
|
+
return self.request("GET", "/api/v1/auth/me")
|
|
194
|
+
|
|
195
|
+
def check_health(self) -> Dict[str, Any]:
|
|
196
|
+
"""Check API health status"""
|
|
197
|
+
return self.request("GET", "/api/v1/health")
|
|
198
|
+
|
|
199
|
+
def close(self):
|
|
200
|
+
"""Close the client session"""
|
|
201
|
+
self.session.close()
|
|
202
|
+
|
|
203
|
+
def __enter__(self):
|
|
204
|
+
return self
|
|
205
|
+
|
|
206
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
207
|
+
self.close()
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class AsyncClient(BaseClient):
|
|
211
|
+
"""
|
|
212
|
+
Asynchronous AudioPod API Client
|
|
213
|
+
|
|
214
|
+
Provides async/await support for better performance in async applications.
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
def __init__(self, **kwargs):
|
|
218
|
+
super().__init__(**kwargs)
|
|
219
|
+
self._session: Optional[aiohttp.ClientSession] = None
|
|
220
|
+
|
|
221
|
+
# Initialize async services
|
|
222
|
+
self.voice = VoiceService(self, async_mode=True)
|
|
223
|
+
self.music = MusicService(self, async_mode=True)
|
|
224
|
+
self.transcription = TranscriptionService(self, async_mode=True)
|
|
225
|
+
self.translation = TranslationService(self, async_mode=True)
|
|
226
|
+
self.speaker = SpeakerService(self, async_mode=True)
|
|
227
|
+
self.denoiser = DenoiserService(self, async_mode=True)
|
|
228
|
+
self.karaoke = KaraokeService(self, async_mode=True)
|
|
229
|
+
self.credits = CreditService(self, async_mode=True)
|
|
230
|
+
|
|
231
|
+
@property
|
|
232
|
+
def session(self) -> aiohttp.ClientSession:
|
|
233
|
+
"""Get or create aiohttp session"""
|
|
234
|
+
if self._session is None or self._session.closed:
|
|
235
|
+
timeout = aiohttp.ClientTimeout(total=self.config.timeout)
|
|
236
|
+
connector = aiohttp.TCPConnector(verify_ssl=self.config.verify_ssl)
|
|
237
|
+
self._session = aiohttp.ClientSession(
|
|
238
|
+
timeout=timeout,
|
|
239
|
+
connector=connector,
|
|
240
|
+
headers=self._get_headers()
|
|
241
|
+
)
|
|
242
|
+
return self._session
|
|
243
|
+
|
|
244
|
+
async def request(
|
|
245
|
+
self,
|
|
246
|
+
method: str,
|
|
247
|
+
endpoint: str,
|
|
248
|
+
data: Optional[Dict[str, Any]] = None,
|
|
249
|
+
files: Optional[Dict[str, Any]] = None,
|
|
250
|
+
params: Optional[Dict[str, Any]] = None,
|
|
251
|
+
**kwargs
|
|
252
|
+
) -> Dict[str, Any]:
|
|
253
|
+
"""
|
|
254
|
+
Make an async request to the AudioPod API
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
method: HTTP method (GET, POST, etc.)
|
|
258
|
+
endpoint: API endpoint path
|
|
259
|
+
data: JSON data to send
|
|
260
|
+
files: Files to upload
|
|
261
|
+
params: URL parameters
|
|
262
|
+
**kwargs: Additional aiohttp parameters
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
API response data
|
|
266
|
+
"""
|
|
267
|
+
url = urljoin(self.config.base_url, endpoint)
|
|
268
|
+
|
|
269
|
+
try:
|
|
270
|
+
if files:
|
|
271
|
+
# Handle file uploads
|
|
272
|
+
form_data = aiohttp.FormData()
|
|
273
|
+
for key, value in (data or {}).items():
|
|
274
|
+
form_data.add_field(key, str(value))
|
|
275
|
+
for key, file_data in files.items():
|
|
276
|
+
form_data.add_field(key, file_data)
|
|
277
|
+
data = form_data
|
|
278
|
+
|
|
279
|
+
async with self.session.request(
|
|
280
|
+
method=method,
|
|
281
|
+
url=url,
|
|
282
|
+
json=data if not files else None,
|
|
283
|
+
data=data if files else None,
|
|
284
|
+
params=params,
|
|
285
|
+
**kwargs
|
|
286
|
+
) as response:
|
|
287
|
+
return await self._handle_async_response(response)
|
|
288
|
+
|
|
289
|
+
except Exception as e:
|
|
290
|
+
logger.error(f"Async request failed: {method} {url} - {e}")
|
|
291
|
+
raise
|
|
292
|
+
|
|
293
|
+
async def _handle_async_response(self, response: aiohttp.ClientResponse) -> Dict[str, Any]:
|
|
294
|
+
"""Handle async API response and raise appropriate exceptions"""
|
|
295
|
+
try:
|
|
296
|
+
response.raise_for_status()
|
|
297
|
+
return await response.json()
|
|
298
|
+
except aiohttp.ClientResponseError as e:
|
|
299
|
+
if response.status == 401:
|
|
300
|
+
raise AuthenticationError("Invalid API key or authentication failed")
|
|
301
|
+
elif response.status == 429:
|
|
302
|
+
raise RateLimitError("Rate limit exceeded. Please try again later.")
|
|
303
|
+
elif response.status >= 400:
|
|
304
|
+
try:
|
|
305
|
+
error_data = await response.json()
|
|
306
|
+
message = error_data.get("detail", str(e))
|
|
307
|
+
except:
|
|
308
|
+
message = str(e)
|
|
309
|
+
raise APIError(f"API request failed: {message}", status_code=response.status)
|
|
310
|
+
else:
|
|
311
|
+
raise APIError(f"Unexpected HTTP error: {e}")
|
|
312
|
+
except aiohttp.ClientError as e:
|
|
313
|
+
raise APIError(f"Request failed: {e}")
|
|
314
|
+
|
|
315
|
+
async def get_user_info(self) -> Dict[str, Any]:
|
|
316
|
+
"""Get current user information"""
|
|
317
|
+
return await self.request("GET", "/api/v1/auth/me")
|
|
318
|
+
|
|
319
|
+
async def check_health(self) -> Dict[str, Any]:
|
|
320
|
+
"""Check API health status"""
|
|
321
|
+
return await self.request("GET", "/api/v1/health")
|
|
322
|
+
|
|
323
|
+
async def close(self):
|
|
324
|
+
"""Close the async client session"""
|
|
325
|
+
if self._session and not self._session.closed:
|
|
326
|
+
await self._session.close()
|
|
327
|
+
|
|
328
|
+
async def __aenter__(self):
|
|
329
|
+
return self
|
|
330
|
+
|
|
331
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
332
|
+
await self.close()
|