audiopod 1.4.0__py3-none-any.whl → 1.5.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 +5 -1
- audiopod/client.py +6 -0
- audiopod/services/__init__.py +2 -0
- audiopod/services/denoiser.py +9 -4
- audiopod/services/stem_extraction.py +130 -11
- audiopod/services/transcription.py +23 -0
- audiopod/services/video.py +329 -0
- {audiopod-1.4.0.dist-info → audiopod-1.5.0.dist-info}/METADATA +1 -1
- audiopod-1.5.0.dist-info/RECORD +21 -0
- audiopod-1.4.0.dist-info/RECORD +0 -20
- {audiopod-1.4.0.dist-info → audiopod-1.5.0.dist-info}/WHEEL +0 -0
- {audiopod-1.4.0.dist-info → audiopod-1.5.0.dist-info}/licenses/LICENSE +0 -0
- {audiopod-1.4.0.dist-info → audiopod-1.5.0.dist-info}/top_level.txt +0 -0
audiopod/__init__.py
CHANGED
|
@@ -3,7 +3,7 @@ AudioPod SDK for Python
|
|
|
3
3
|
Professional Audio Processing powered by AI
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
__version__ = "1.
|
|
6
|
+
__version__ = "1.5.0"
|
|
7
7
|
|
|
8
8
|
from .client import Client, AsyncClient
|
|
9
9
|
from .exceptions import (
|
|
@@ -15,9 +15,13 @@ from .exceptions import (
|
|
|
15
15
|
InsufficientBalanceError,
|
|
16
16
|
)
|
|
17
17
|
|
|
18
|
+
# Alias for consistency with documentation and Node.js SDK
|
|
19
|
+
AudioPod = Client
|
|
20
|
+
|
|
18
21
|
__all__ = [
|
|
19
22
|
"Client",
|
|
20
23
|
"AsyncClient",
|
|
24
|
+
"AudioPod", # Alias for Client
|
|
21
25
|
"AudioPodError",
|
|
22
26
|
"AuthenticationError",
|
|
23
27
|
"APIError",
|
audiopod/client.py
CHANGED
|
@@ -24,6 +24,7 @@ from .services import (
|
|
|
24
24
|
CreditService,
|
|
25
25
|
StemExtractionService,
|
|
26
26
|
WalletService,
|
|
27
|
+
VideoService,
|
|
27
28
|
)
|
|
28
29
|
|
|
29
30
|
logger = logging.getLogger(__name__)
|
|
@@ -75,6 +76,7 @@ class BaseClient:
|
|
|
75
76
|
def _get_headers(self) -> Dict[str, str]:
|
|
76
77
|
return {
|
|
77
78
|
"Authorization": f"Bearer {self.api_key}",
|
|
79
|
+
"X-API-Key": self.api_key,
|
|
78
80
|
"Content-Type": "application/json",
|
|
79
81
|
"User-Agent": f"audiopod-python/{self.config.version}",
|
|
80
82
|
"Accept": "application/json",
|
|
@@ -155,7 +157,9 @@ class Client(BaseClient):
|
|
|
155
157
|
self.denoiser = DenoiserService(self)
|
|
156
158
|
self.credits = CreditService(self)
|
|
157
159
|
self.stem_extraction = StemExtractionService(self)
|
|
160
|
+
self.stems = self.stem_extraction # Alias for consistency with Node.js SDK
|
|
158
161
|
self.wallet = WalletService(self)
|
|
162
|
+
self.video = VideoService(self)
|
|
159
163
|
|
|
160
164
|
def request(
|
|
161
165
|
self,
|
|
@@ -232,7 +236,9 @@ class AsyncClient(BaseClient):
|
|
|
232
236
|
self.denoiser = DenoiserService(self, async_mode=True)
|
|
233
237
|
self.credits = CreditService(self, async_mode=True)
|
|
234
238
|
self.stem_extraction = StemExtractionService(self, async_mode=True)
|
|
239
|
+
self.stems = self.stem_extraction # Alias for consistency with Node.js SDK
|
|
235
240
|
self.wallet = WalletService(self, async_mode=True)
|
|
241
|
+
self.video = VideoService(self, async_mode=True)
|
|
236
242
|
|
|
237
243
|
@property
|
|
238
244
|
def session(self) -> aiohttp.ClientSession:
|
audiopod/services/__init__.py
CHANGED
|
@@ -12,6 +12,7 @@ from .denoiser import DenoiserService
|
|
|
12
12
|
from .credits import CreditService
|
|
13
13
|
from .stem_extraction import StemExtractionService
|
|
14
14
|
from .wallet import WalletService
|
|
15
|
+
from .video import VideoService
|
|
15
16
|
|
|
16
17
|
__all__ = [
|
|
17
18
|
"BaseService",
|
|
@@ -24,5 +25,6 @@ __all__ = [
|
|
|
24
25
|
"CreditService",
|
|
25
26
|
"StemExtractionService",
|
|
26
27
|
"WalletService",
|
|
28
|
+
"VideoService",
|
|
27
29
|
]
|
|
28
30
|
|
audiopod/services/denoiser.py
CHANGED
|
@@ -17,25 +17,30 @@ class DenoiserService(BaseService):
|
|
|
17
17
|
|
|
18
18
|
def denoise(
|
|
19
19
|
self,
|
|
20
|
-
|
|
20
|
+
file: Optional[str] = None,
|
|
21
21
|
url: Optional[str] = None,
|
|
22
22
|
mode: str = "balanced",
|
|
23
|
-
wait_for_completion: bool =
|
|
23
|
+
wait_for_completion: bool = True,
|
|
24
24
|
timeout: int = 300,
|
|
25
|
+
# Legacy parameter name
|
|
26
|
+
audio_file: Optional[str] = None,
|
|
25
27
|
) -> Dict[str, Any]:
|
|
26
28
|
"""
|
|
27
29
|
Remove noise from audio.
|
|
28
30
|
|
|
29
31
|
Args:
|
|
30
|
-
|
|
32
|
+
file: Path to local audio file
|
|
31
33
|
url: URL of audio file
|
|
32
34
|
mode: Denoise mode ("balanced", "studio", or "ultra")
|
|
33
|
-
wait_for_completion: Wait for completion
|
|
35
|
+
wait_for_completion: Wait for completion (default: True)
|
|
34
36
|
timeout: Max wait time in seconds
|
|
35
37
|
|
|
36
38
|
Returns:
|
|
37
39
|
Job dict with denoised audio URL when completed
|
|
38
40
|
"""
|
|
41
|
+
# Support both 'file' and legacy 'audio_file' parameter names
|
|
42
|
+
audio_file = file or audio_file
|
|
43
|
+
|
|
39
44
|
data = {"mode": mode}
|
|
40
45
|
if url:
|
|
41
46
|
data["url"] = url
|
|
@@ -2,11 +2,16 @@
|
|
|
2
2
|
Stem Extraction Service - Audio stem separation
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
from typing import List, Optional, Dict, Any
|
|
5
|
+
from typing import List, Optional, Dict, Any, Literal
|
|
6
6
|
from .base import BaseService
|
|
7
7
|
from ..exceptions import ValidationError
|
|
8
8
|
|
|
9
9
|
|
|
10
|
+
# Valid separation modes for the new API
|
|
11
|
+
StemMode = Literal["single", "two", "four", "six", "producer", "studio", "mastering"]
|
|
12
|
+
SingleStem = Literal["vocals", "drums", "bass", "guitar", "piano", "other", "instrumental"]
|
|
13
|
+
|
|
14
|
+
|
|
10
15
|
class StemExtractionService(BaseService):
|
|
11
16
|
"""
|
|
12
17
|
Service for audio stem separation.
|
|
@@ -17,19 +22,131 @@ class StemExtractionService(BaseService):
|
|
|
17
22
|
|
|
18
23
|
client = Client()
|
|
19
24
|
|
|
20
|
-
#
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
# Simple mode-based extraction (recommended)
|
|
26
|
+
result = client.stem_extraction.separate(
|
|
27
|
+
url="https://youtube.com/watch?v=VIDEO_ID",
|
|
28
|
+
mode="six"
|
|
29
|
+
)
|
|
30
|
+
for stem, url in result["download_urls"].items():
|
|
31
|
+
print(f"{stem}: {url}")
|
|
32
|
+
|
|
33
|
+
# Or extract only vocals
|
|
34
|
+
result = client.stem_extraction.separate(
|
|
35
|
+
file="song.mp3",
|
|
36
|
+
mode="single",
|
|
37
|
+
stem="vocals"
|
|
25
38
|
)
|
|
26
|
-
|
|
27
|
-
# Download stems
|
|
28
|
-
for stem_name, url in job["download_urls"].items():
|
|
29
|
-
print(f"{stem_name}: {url}")
|
|
30
39
|
```
|
|
31
40
|
"""
|
|
32
41
|
|
|
42
|
+
def separate(
|
|
43
|
+
self,
|
|
44
|
+
file: Optional[str] = None,
|
|
45
|
+
url: Optional[str] = None,
|
|
46
|
+
mode: StemMode = "four",
|
|
47
|
+
stem: Optional[SingleStem] = None,
|
|
48
|
+
wait_for_completion: bool = True,
|
|
49
|
+
timeout: int = 900,
|
|
50
|
+
) -> Dict[str, Any]:
|
|
51
|
+
"""
|
|
52
|
+
Separate audio into stems using simple mode selection.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
file: Path to local audio file
|
|
56
|
+
url: URL of audio/video (YouTube, SoundCloud, direct link)
|
|
57
|
+
mode: Separation mode:
|
|
58
|
+
- "single": Extract one stem (specify stem param)
|
|
59
|
+
- "two": Vocals + Instrumental
|
|
60
|
+
- "four": Vocals, Drums, Bass, Other (default)
|
|
61
|
+
- "six": Vocals, Drums, Bass, Guitar, Piano, Other
|
|
62
|
+
- "producer": 8 stems with kick, snare, hihat
|
|
63
|
+
- "studio": 12 stems for professional mixing
|
|
64
|
+
- "mastering": 16 stems maximum detail
|
|
65
|
+
stem: For mode="single", which stem to extract
|
|
66
|
+
wait_for_completion: Wait for job to complete (default: True)
|
|
67
|
+
timeout: Max wait time in seconds
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Job dict with id, status, download_urls (when completed)
|
|
71
|
+
"""
|
|
72
|
+
if not file and not url:
|
|
73
|
+
raise ValidationError("Provide file or url")
|
|
74
|
+
|
|
75
|
+
if file and url:
|
|
76
|
+
raise ValidationError("Provide file or url, not both")
|
|
77
|
+
|
|
78
|
+
if mode == "single" and not stem:
|
|
79
|
+
raise ValidationError(
|
|
80
|
+
"stem parameter required for mode='single'. "
|
|
81
|
+
"Options: vocals, drums, bass, guitar, piano, other, instrumental"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
data = {"mode": mode}
|
|
85
|
+
if stem:
|
|
86
|
+
data["stem"] = stem
|
|
87
|
+
if url:
|
|
88
|
+
data["url"] = url
|
|
89
|
+
|
|
90
|
+
files = self._prepare_file_upload(file, "file") if file else None
|
|
91
|
+
|
|
92
|
+
if self.async_mode:
|
|
93
|
+
return self._async_separate(data, files, wait_for_completion, timeout)
|
|
94
|
+
|
|
95
|
+
response = self.client.request(
|
|
96
|
+
"POST", "/api/v1/stem-extraction/api/extract", data=data, files=files
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
if wait_for_completion:
|
|
100
|
+
return self._wait_for_stem_job(response["id"], timeout)
|
|
101
|
+
|
|
102
|
+
return response
|
|
103
|
+
|
|
104
|
+
async def _async_separate(
|
|
105
|
+
self,
|
|
106
|
+
data: Dict[str, Any],
|
|
107
|
+
files: Optional[Dict[str, Any]],
|
|
108
|
+
wait_for_completion: bool,
|
|
109
|
+
timeout: int,
|
|
110
|
+
) -> Dict[str, Any]:
|
|
111
|
+
response = await self.client.request(
|
|
112
|
+
"POST", "/api/v1/stem-extraction/api/extract", data=data, files=files
|
|
113
|
+
)
|
|
114
|
+
if wait_for_completion:
|
|
115
|
+
return await self._async_wait_for_stem_job(response["id"], timeout)
|
|
116
|
+
return response
|
|
117
|
+
|
|
118
|
+
def extract(
|
|
119
|
+
self,
|
|
120
|
+
file: Optional[str] = None,
|
|
121
|
+
url: Optional[str] = None,
|
|
122
|
+
mode: StemMode = "four",
|
|
123
|
+
stem: Optional[SingleStem] = None,
|
|
124
|
+
) -> Dict[str, Any]:
|
|
125
|
+
"""
|
|
126
|
+
Submit stem extraction job (returns immediately without waiting).
|
|
127
|
+
|
|
128
|
+
Use wait_for_completion() to poll for results.
|
|
129
|
+
"""
|
|
130
|
+
return self.separate(file=file, url=url, mode=mode, stem=stem, wait_for_completion=False)
|
|
131
|
+
|
|
132
|
+
def wait_for_completion(self, job_id: int, timeout: int = 900) -> Dict[str, Any]:
|
|
133
|
+
"""Wait for stem extraction job to complete."""
|
|
134
|
+
return self._wait_for_stem_job(job_id, timeout)
|
|
135
|
+
|
|
136
|
+
def status(self, job_id: int) -> Dict[str, Any]:
|
|
137
|
+
"""Get stem extraction job status (alias for get_job)."""
|
|
138
|
+
return self.get_job(job_id)
|
|
139
|
+
|
|
140
|
+
def modes(self) -> Dict[str, Any]:
|
|
141
|
+
"""Get available separation modes."""
|
|
142
|
+
if self.async_mode:
|
|
143
|
+
return self._async_modes()
|
|
144
|
+
return self.client.request("GET", "/api/v1/stem-extraction/modes")
|
|
145
|
+
|
|
146
|
+
async def _async_modes(self) -> Dict[str, Any]:
|
|
147
|
+
return await self.client.request("GET", "/api/v1/stem-extraction/modes")
|
|
148
|
+
|
|
149
|
+
# Legacy method - kept for backward compatibility
|
|
33
150
|
def extract_stems(
|
|
34
151
|
self,
|
|
35
152
|
audio_file: Optional[str] = None,
|
|
@@ -41,7 +158,9 @@ class StemExtractionService(BaseService):
|
|
|
41
158
|
timeout: int = 900,
|
|
42
159
|
) -> Dict[str, Any]:
|
|
43
160
|
"""
|
|
44
|
-
Extract stems from audio.
|
|
161
|
+
Extract stems from audio (legacy method).
|
|
162
|
+
|
|
163
|
+
For new code, use separate() instead which uses the simpler mode-based API.
|
|
45
164
|
|
|
46
165
|
Args:
|
|
47
166
|
audio_file: Path to local audio file
|
|
@@ -16,6 +16,25 @@ from .base import BaseService
|
|
|
16
16
|
class TranscriptionService(BaseService):
|
|
17
17
|
"""Service for speech-to-text transcription."""
|
|
18
18
|
|
|
19
|
+
def create(
|
|
20
|
+
self,
|
|
21
|
+
file: Optional[str] = None,
|
|
22
|
+
url: Optional[str] = None,
|
|
23
|
+
language: Optional[str] = None,
|
|
24
|
+
speaker_diarization: bool = False,
|
|
25
|
+
wait_for_completion: bool = False,
|
|
26
|
+
timeout: int = 600,
|
|
27
|
+
) -> Dict[str, Any]:
|
|
28
|
+
"""Alias for transcribe() - matches Node.js SDK."""
|
|
29
|
+
return self.transcribe(
|
|
30
|
+
audio_file=file,
|
|
31
|
+
url=url,
|
|
32
|
+
language=language,
|
|
33
|
+
speaker_diarization=speaker_diarization,
|
|
34
|
+
wait_for_completion=wait_for_completion,
|
|
35
|
+
timeout=timeout,
|
|
36
|
+
)
|
|
37
|
+
|
|
19
38
|
def transcribe(
|
|
20
39
|
self,
|
|
21
40
|
audio_file: Optional[str] = None,
|
|
@@ -148,6 +167,10 @@ class TranscriptionService(BaseService):
|
|
|
148
167
|
"GET", f"/api/v1/transcription/jobs/{job_id}/transcript", params={"format": format}
|
|
149
168
|
)
|
|
150
169
|
|
|
170
|
+
def wait_for_completion(self, job_id: int, timeout: int = 600) -> Dict[str, Any]:
|
|
171
|
+
"""Wait for transcription job completion (matches Node.js SDK)."""
|
|
172
|
+
return self._wait_for_transcription(job_id, timeout)
|
|
173
|
+
|
|
151
174
|
def _wait_for_transcription(self, job_id: int, timeout: int) -> Dict[str, Any]:
|
|
152
175
|
"""Wait for transcription job completion."""
|
|
153
176
|
import time
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Video Service - Video generation
|
|
3
|
+
|
|
4
|
+
API Routes:
|
|
5
|
+
- POST /api/v2/video/karaoke - Create karaoke video
|
|
6
|
+
- POST /api/v2/video/music-video - Create AI music video
|
|
7
|
+
- POST /api/v2/video/lyric-video - Create lyric video
|
|
8
|
+
- GET /api/v2/video/{id} - Get job status
|
|
9
|
+
- GET /api/v2/video/{id}/download - Get download URLs
|
|
10
|
+
- DELETE /api/v2/video/{id} - Delete job
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from typing import Optional, Dict, Any, List, Literal
|
|
14
|
+
from .base import BaseService
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
VideoType = Literal["karaoke", "music_video", "lyric_video"]
|
|
18
|
+
AspectRatio = Literal["16:9", "9:16", "1:1"]
|
|
19
|
+
SceneGeneration = Literal["flux", "veo3", "static"]
|
|
20
|
+
AnimationStyle = Literal["typewriter", "fade-word", "slide-up", "bounce", "wave", "zoom"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class VideoService(BaseService):
|
|
24
|
+
"""Service for video generation."""
|
|
25
|
+
|
|
26
|
+
def create_karaoke(
|
|
27
|
+
self,
|
|
28
|
+
audio_file: Optional[str] = None,
|
|
29
|
+
url: Optional[str] = None,
|
|
30
|
+
aspect_ratio: AspectRatio = "16:9",
|
|
31
|
+
scene_generation: SceneGeneration = "flux",
|
|
32
|
+
key_shift: int = 0,
|
|
33
|
+
style: Optional[Dict[str, Any]] = None,
|
|
34
|
+
webhook_url: Optional[str] = None,
|
|
35
|
+
wait_for_completion: bool = False,
|
|
36
|
+
timeout: int = 600,
|
|
37
|
+
) -> Dict[str, Any]:
|
|
38
|
+
"""
|
|
39
|
+
Create a karaoke video from audio.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
audio_file: Path to local audio file
|
|
43
|
+
url: URL of audio/video (YouTube, etc.)
|
|
44
|
+
aspect_ratio: Video aspect ratio
|
|
45
|
+
scene_generation: Background generation method
|
|
46
|
+
key_shift: Pitch shift in semitones (-12 to 12)
|
|
47
|
+
style: Style configuration (colors, font, etc.)
|
|
48
|
+
webhook_url: URL for completion notification
|
|
49
|
+
wait_for_completion: Wait for video to finish
|
|
50
|
+
timeout: Max wait time in seconds
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Job dict with id, status, video_url (when completed)
|
|
54
|
+
"""
|
|
55
|
+
if audio_file:
|
|
56
|
+
return self._create_from_upload(
|
|
57
|
+
"karaoke", audio_file, aspect_ratio, scene_generation,
|
|
58
|
+
key_shift, webhook_url, wait_for_completion, timeout
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
data = {
|
|
62
|
+
"audio_url": url,
|
|
63
|
+
"aspect_ratio": aspect_ratio,
|
|
64
|
+
"scene_generation": scene_generation,
|
|
65
|
+
"key_shift": key_shift,
|
|
66
|
+
}
|
|
67
|
+
if style:
|
|
68
|
+
data["style"] = style
|
|
69
|
+
if webhook_url:
|
|
70
|
+
data["webhook_url"] = webhook_url
|
|
71
|
+
|
|
72
|
+
if self.async_mode:
|
|
73
|
+
return self._async_create("karaoke", data, wait_for_completion, timeout)
|
|
74
|
+
|
|
75
|
+
response = self.client.request("POST", "/api/v2/video/karaoke", json_data=data)
|
|
76
|
+
|
|
77
|
+
if wait_for_completion:
|
|
78
|
+
return self._wait_for_video(response["id"], timeout)
|
|
79
|
+
return response
|
|
80
|
+
|
|
81
|
+
def create_music_video(
|
|
82
|
+
self,
|
|
83
|
+
audio_file: Optional[str] = None,
|
|
84
|
+
url: Optional[str] = None,
|
|
85
|
+
music_job_id: Optional[str] = None,
|
|
86
|
+
aspect_ratio: AspectRatio = "16:9",
|
|
87
|
+
scene_generation: SceneGeneration = "flux",
|
|
88
|
+
scene_count: int = 10,
|
|
89
|
+
include_lyrics: bool = True,
|
|
90
|
+
style: Optional[Dict[str, Any]] = None,
|
|
91
|
+
webhook_url: Optional[str] = None,
|
|
92
|
+
wait_for_completion: bool = False,
|
|
93
|
+
timeout: int = 900,
|
|
94
|
+
) -> Dict[str, Any]:
|
|
95
|
+
"""
|
|
96
|
+
Create an AI music video.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
audio_file: Path to local audio file
|
|
100
|
+
url: URL of audio file
|
|
101
|
+
music_job_id: AudioPod music generation job ID
|
|
102
|
+
aspect_ratio: Video aspect ratio
|
|
103
|
+
scene_generation: Scene generation method (flux or veo3)
|
|
104
|
+
scene_count: Number of scenes to generate (5-20)
|
|
105
|
+
include_lyrics: Overlay lyrics on video
|
|
106
|
+
style: Style configuration
|
|
107
|
+
webhook_url: URL for completion notification
|
|
108
|
+
wait_for_completion: Wait for video to finish
|
|
109
|
+
timeout: Max wait time in seconds
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Job dict with video_url when completed
|
|
113
|
+
"""
|
|
114
|
+
data = {
|
|
115
|
+
"aspect_ratio": aspect_ratio,
|
|
116
|
+
"scene_generation": scene_generation,
|
|
117
|
+
"scene_count": scene_count,
|
|
118
|
+
"include_lyrics": include_lyrics,
|
|
119
|
+
}
|
|
120
|
+
if url:
|
|
121
|
+
data["audio_url"] = url
|
|
122
|
+
if music_job_id:
|
|
123
|
+
data["music_job_id"] = music_job_id
|
|
124
|
+
if style:
|
|
125
|
+
data["style"] = style
|
|
126
|
+
if webhook_url:
|
|
127
|
+
data["webhook_url"] = webhook_url
|
|
128
|
+
|
|
129
|
+
if self.async_mode:
|
|
130
|
+
return self._async_create("music-video", data, wait_for_completion, timeout)
|
|
131
|
+
|
|
132
|
+
response = self.client.request("POST", "/api/v2/video/music-video", json_data=data)
|
|
133
|
+
|
|
134
|
+
if wait_for_completion:
|
|
135
|
+
return self._wait_for_video(response["id"], timeout)
|
|
136
|
+
return response
|
|
137
|
+
|
|
138
|
+
def create_lyric_video(
|
|
139
|
+
self,
|
|
140
|
+
audio_file: Optional[str] = None,
|
|
141
|
+
url: Optional[str] = None,
|
|
142
|
+
lyrics: Optional[str] = None,
|
|
143
|
+
aspect_ratio: AspectRatio = "16:9",
|
|
144
|
+
animation_style: AnimationStyle = "fade-word",
|
|
145
|
+
background_color: str = "#1a1a2e",
|
|
146
|
+
accent_color: str = "#e94560",
|
|
147
|
+
particles_enabled: bool = True,
|
|
148
|
+
background_image_url: Optional[str] = None,
|
|
149
|
+
webhook_url: Optional[str] = None,
|
|
150
|
+
wait_for_completion: bool = False,
|
|
151
|
+
timeout: int = 600,
|
|
152
|
+
) -> Dict[str, Any]:
|
|
153
|
+
"""
|
|
154
|
+
Create a kinetic typography lyric video.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
audio_file: Path to local audio file
|
|
158
|
+
url: URL of audio file
|
|
159
|
+
lyrics: Custom lyrics (auto-transcribed if not provided)
|
|
160
|
+
aspect_ratio: Video aspect ratio
|
|
161
|
+
animation_style: Text animation style
|
|
162
|
+
background_color: Background color
|
|
163
|
+
accent_color: Accent/highlight color
|
|
164
|
+
particles_enabled: Enable particle effects
|
|
165
|
+
background_image_url: Optional background image
|
|
166
|
+
webhook_url: URL for completion notification
|
|
167
|
+
wait_for_completion: Wait for video to finish
|
|
168
|
+
timeout: Max wait time in seconds
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Job dict with video_url when completed
|
|
172
|
+
"""
|
|
173
|
+
data = {
|
|
174
|
+
"aspect_ratio": aspect_ratio,
|
|
175
|
+
"style": {
|
|
176
|
+
"animation_style": animation_style,
|
|
177
|
+
"background_color": background_color,
|
|
178
|
+
"accent_color": accent_color,
|
|
179
|
+
"particles_enabled": particles_enabled,
|
|
180
|
+
},
|
|
181
|
+
}
|
|
182
|
+
if url:
|
|
183
|
+
data["audio_url"] = url
|
|
184
|
+
if lyrics:
|
|
185
|
+
data["lyrics"] = lyrics
|
|
186
|
+
if background_image_url:
|
|
187
|
+
data["background_image_url"] = background_image_url
|
|
188
|
+
if webhook_url:
|
|
189
|
+
data["webhook_url"] = webhook_url
|
|
190
|
+
|
|
191
|
+
if self.async_mode:
|
|
192
|
+
return self._async_create("lyric-video", data, wait_for_completion, timeout)
|
|
193
|
+
|
|
194
|
+
response = self.client.request("POST", "/api/v2/video/lyric-video", json_data=data)
|
|
195
|
+
|
|
196
|
+
if wait_for_completion:
|
|
197
|
+
return self._wait_for_video(response["id"], timeout)
|
|
198
|
+
return response
|
|
199
|
+
|
|
200
|
+
def get_job(self, job_id: str) -> Dict[str, Any]:
|
|
201
|
+
"""Get video job status."""
|
|
202
|
+
if self.async_mode:
|
|
203
|
+
return self._async_get_job(job_id)
|
|
204
|
+
return self.client.request("GET", f"/api/v2/video/{job_id}")
|
|
205
|
+
|
|
206
|
+
async def _async_get_job(self, job_id: str) -> Dict[str, Any]:
|
|
207
|
+
return await self.client.request("GET", f"/api/v2/video/{job_id}")
|
|
208
|
+
|
|
209
|
+
def get_download_urls(self, job_id: str) -> Dict[str, Any]:
|
|
210
|
+
"""Get download URLs for completed video."""
|
|
211
|
+
if self.async_mode:
|
|
212
|
+
return self._async_get_download(job_id)
|
|
213
|
+
return self.client.request("GET", f"/api/v2/video/{job_id}/download")
|
|
214
|
+
|
|
215
|
+
async def _async_get_download(self, job_id: str) -> Dict[str, Any]:
|
|
216
|
+
return await self.client.request("GET", f"/api/v2/video/{job_id}/download")
|
|
217
|
+
|
|
218
|
+
def delete_job(self, job_id: str) -> Dict[str, str]:
|
|
219
|
+
"""Delete a video job."""
|
|
220
|
+
if self.async_mode:
|
|
221
|
+
return self._async_delete(job_id)
|
|
222
|
+
return self.client.request("DELETE", f"/api/v2/video/{job_id}")
|
|
223
|
+
|
|
224
|
+
async def _async_delete(self, job_id: str) -> Dict[str, str]:
|
|
225
|
+
return await self.client.request("DELETE", f"/api/v2/video/{job_id}")
|
|
226
|
+
|
|
227
|
+
def list_jobs(
|
|
228
|
+
self,
|
|
229
|
+
video_type: Optional[VideoType] = None,
|
|
230
|
+
limit: int = 20,
|
|
231
|
+
offset: int = 0,
|
|
232
|
+
) -> Dict[str, Any]:
|
|
233
|
+
"""List video jobs."""
|
|
234
|
+
params = {"limit": limit, "offset": offset}
|
|
235
|
+
if video_type:
|
|
236
|
+
params["type"] = video_type
|
|
237
|
+
|
|
238
|
+
if self.async_mode:
|
|
239
|
+
return self._async_list(params)
|
|
240
|
+
return self.client.request("GET", "/api/v2/video/", params=params)
|
|
241
|
+
|
|
242
|
+
async def _async_list(self, params: Dict) -> Dict[str, Any]:
|
|
243
|
+
return await self.client.request("GET", "/api/v2/video/", params=params)
|
|
244
|
+
|
|
245
|
+
def _create_from_upload(
|
|
246
|
+
self,
|
|
247
|
+
video_type: str,
|
|
248
|
+
audio_file: str,
|
|
249
|
+
aspect_ratio: str,
|
|
250
|
+
scene_generation: str,
|
|
251
|
+
key_shift: int,
|
|
252
|
+
webhook_url: Optional[str],
|
|
253
|
+
wait_for_completion: bool,
|
|
254
|
+
timeout: int,
|
|
255
|
+
) -> Dict[str, Any]:
|
|
256
|
+
"""Create video from file upload."""
|
|
257
|
+
data = {
|
|
258
|
+
"aspect_ratio": aspect_ratio,
|
|
259
|
+
"scene_generation": scene_generation,
|
|
260
|
+
"key_shift": str(key_shift),
|
|
261
|
+
}
|
|
262
|
+
if webhook_url:
|
|
263
|
+
data["webhook_url"] = webhook_url
|
|
264
|
+
|
|
265
|
+
files = self._prepare_file_upload(audio_file, "file")
|
|
266
|
+
|
|
267
|
+
response = self.client.request(
|
|
268
|
+
"POST", f"/api/v2/video/{video_type}/upload", data=data, files=files
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
if wait_for_completion:
|
|
272
|
+
return self._wait_for_video(response["id"], timeout)
|
|
273
|
+
return response
|
|
274
|
+
|
|
275
|
+
async def _async_create(
|
|
276
|
+
self,
|
|
277
|
+
endpoint: str,
|
|
278
|
+
data: Dict,
|
|
279
|
+
wait_for_completion: bool,
|
|
280
|
+
timeout: int,
|
|
281
|
+
) -> Dict[str, Any]:
|
|
282
|
+
response = await self.client.request(
|
|
283
|
+
"POST", f"/api/v2/video/{endpoint}", json_data=data
|
|
284
|
+
)
|
|
285
|
+
if wait_for_completion:
|
|
286
|
+
return await self._async_wait_for_video(response["id"], timeout)
|
|
287
|
+
return response
|
|
288
|
+
|
|
289
|
+
def _wait_for_video(self, job_id: str, timeout: int) -> Dict[str, Any]:
|
|
290
|
+
"""Wait for video job completion."""
|
|
291
|
+
import time
|
|
292
|
+
start = time.time()
|
|
293
|
+
|
|
294
|
+
while time.time() - start < timeout:
|
|
295
|
+
job = self.get_job(job_id)
|
|
296
|
+
status = job.get("status", "")
|
|
297
|
+
|
|
298
|
+
if status == "completed":
|
|
299
|
+
# Get download URLs
|
|
300
|
+
download = self.get_download_urls(job_id)
|
|
301
|
+
job.update(download)
|
|
302
|
+
return job
|
|
303
|
+
elif status == "failed":
|
|
304
|
+
raise Exception(f"Video generation failed: {job.get('error', 'Unknown')}")
|
|
305
|
+
|
|
306
|
+
time.sleep(5)
|
|
307
|
+
|
|
308
|
+
raise TimeoutError(f"Video {job_id} timed out after {timeout}s")
|
|
309
|
+
|
|
310
|
+
async def _async_wait_for_video(self, job_id: str, timeout: int) -> Dict[str, Any]:
|
|
311
|
+
"""Async wait for video job completion."""
|
|
312
|
+
import asyncio
|
|
313
|
+
import time
|
|
314
|
+
start = time.time()
|
|
315
|
+
|
|
316
|
+
while time.time() - start < timeout:
|
|
317
|
+
job = await self.get_job(job_id)
|
|
318
|
+
status = job.get("status", "")
|
|
319
|
+
|
|
320
|
+
if status == "completed":
|
|
321
|
+
download = await self.get_download_urls(job_id)
|
|
322
|
+
job.update(download)
|
|
323
|
+
return job
|
|
324
|
+
elif status == "failed":
|
|
325
|
+
raise Exception(f"Video generation failed: {job.get('error', 'Unknown')}")
|
|
326
|
+
|
|
327
|
+
await asyncio.sleep(5)
|
|
328
|
+
|
|
329
|
+
raise TimeoutError(f"Video {job_id} timed out after {timeout}s")
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
audiopod/__init__.py,sha256=kWBOxfIlOFoUz2mEhSJFWF9fsgmy4luK6_p7CIKHC8U,621
|
|
2
|
+
audiopod/client.py,sha256=UyUqCHh2l770Tr5cgThWbxf5na5T551ZOvfXp2vepE8,10482
|
|
3
|
+
audiopod/config.py,sha256=pbmISyy8K9QpR4aYON7xHG5kpimac2lbqgi9wXF7q-Q,316
|
|
4
|
+
audiopod/exceptions.py,sha256=djjLqVHGhUIe2SXFMWkVOw3TdHOZLQzFpQ5EWPjlGFM,946
|
|
5
|
+
audiopod/services/__init__.py,sha256=F7JgtEzVcUqDLjgavUNes6SozoqD74ebwUqPwBWRWY4,708
|
|
6
|
+
audiopod/services/base.py,sha256=-g3PQnXnrJyszj7wdV5Jb1SkUNSvm65TMpWqyIsh8Yo,2256
|
|
7
|
+
audiopod/services/credits.py,sha256=l8zk1XR77nsvMVwjSDOqN_8hUPbwWVCqSXo4JmocXrI,1450
|
|
8
|
+
audiopod/services/denoiser.py,sha256=LeV2zw-BVXkz-wWdXAQPx2mbtJeWBRPCvV0Q_avAAKA,4984
|
|
9
|
+
audiopod/services/music.py,sha256=iihNVLaGCDp5A0jDbziLNLDASdsY1XJbStWkpH6lfuQ,7676
|
|
10
|
+
audiopod/services/speaker.py,sha256=wrQcoKUS9VHjfYwDPYCncXJmeBSJm0bUH8fwlis-NMo,4692
|
|
11
|
+
audiopod/services/stem_extraction.py,sha256=JJwDT63bn7Y7UrRYNd0qXcl6lFkrV1o4yZXkmMzXdYg,10311
|
|
12
|
+
audiopod/services/transcription.py,sha256=OdIaqoYtVLg3O8zTZ1p3LZKH1arFprJuDJc2II9IgnU,7920
|
|
13
|
+
audiopod/services/translation.py,sha256=lpkXKpSGz_ounvDRMCa4vbvv9jHQtQ-Gk3BDR_9fWJo,5222
|
|
14
|
+
audiopod/services/video.py,sha256=y-gSpo8yu6U5bQrpOmT9wqgc4n9tuEag7PcPWyDOQRM,11711
|
|
15
|
+
audiopod/services/voice.py,sha256=JtlH9DCHi-D9eVSo3g95VeB99aBjdcOxTOm6RN8Re1E,7234
|
|
16
|
+
audiopod/services/wallet.py,sha256=Dg32Ak51w7BcbLVRl4TrI7yipA4QE5_RUZPHp2JS8T8,7735
|
|
17
|
+
audiopod-1.5.0.dist-info/licenses/LICENSE,sha256=hqEjnOaGNbnLSBxbtbC7WQVREU2vQI8FmwecCiZlMfA,1068
|
|
18
|
+
audiopod-1.5.0.dist-info/METADATA,sha256=IAGD-rCC4WzO5ZMir4uLwrfoyejbalQ0myf4w5hEcLw,4819
|
|
19
|
+
audiopod-1.5.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
20
|
+
audiopod-1.5.0.dist-info/top_level.txt,sha256=M6yyOFFNpLdH4i1AMRqJZLRIgfpg1NvrQVmnPd8A6N8,9
|
|
21
|
+
audiopod-1.5.0.dist-info/RECORD,,
|
audiopod-1.4.0.dist-info/RECORD
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
audiopod/__init__.py,sha256=JwoffsVicJ0KELRFWWIeQ11KSZczJHpXNz65OOzhagU,507
|
|
2
|
-
audiopod/client.py,sha256=punEkve_cMIOtUK78kKs0wyaY8d4zVP_Ir0kNySLiqs,10160
|
|
3
|
-
audiopod/config.py,sha256=pbmISyy8K9QpR4aYON7xHG5kpimac2lbqgi9wXF7q-Q,316
|
|
4
|
-
audiopod/exceptions.py,sha256=djjLqVHGhUIe2SXFMWkVOw3TdHOZLQzFpQ5EWPjlGFM,946
|
|
5
|
-
audiopod/services/__init__.py,sha256=SjHpRAcCyA6LGFHtNIkbR_GoP9N0VLO20eiHdYBXnv8,656
|
|
6
|
-
audiopod/services/base.py,sha256=-g3PQnXnrJyszj7wdV5Jb1SkUNSvm65TMpWqyIsh8Yo,2256
|
|
7
|
-
audiopod/services/credits.py,sha256=l8zk1XR77nsvMVwjSDOqN_8hUPbwWVCqSXo4JmocXrI,1450
|
|
8
|
-
audiopod/services/denoiser.py,sha256=rz17sNBEEokvoTfJo3_Q-dfseZe4LPRBkcbGasq7ywo,4788
|
|
9
|
-
audiopod/services/music.py,sha256=iihNVLaGCDp5A0jDbziLNLDASdsY1XJbStWkpH6lfuQ,7676
|
|
10
|
-
audiopod/services/speaker.py,sha256=wrQcoKUS9VHjfYwDPYCncXJmeBSJm0bUH8fwlis-NMo,4692
|
|
11
|
-
audiopod/services/stem_extraction.py,sha256=RLuQpGiMzaI-VcedempyNGIjmicbgvoAAk9XRrfBE-s,5963
|
|
12
|
-
audiopod/services/transcription.py,sha256=sRUjRoyMp-W5J5RJK6ufsDQ2NNZ7AQF7tqBaC_7ragE,7098
|
|
13
|
-
audiopod/services/translation.py,sha256=lpkXKpSGz_ounvDRMCa4vbvv9jHQtQ-Gk3BDR_9fWJo,5222
|
|
14
|
-
audiopod/services/voice.py,sha256=JtlH9DCHi-D9eVSo3g95VeB99aBjdcOxTOm6RN8Re1E,7234
|
|
15
|
-
audiopod/services/wallet.py,sha256=Dg32Ak51w7BcbLVRl4TrI7yipA4QE5_RUZPHp2JS8T8,7735
|
|
16
|
-
audiopod-1.4.0.dist-info/licenses/LICENSE,sha256=hqEjnOaGNbnLSBxbtbC7WQVREU2vQI8FmwecCiZlMfA,1068
|
|
17
|
-
audiopod-1.4.0.dist-info/METADATA,sha256=AjHo20pF1UznVZueio0sFmxGqdiXqS3r-W8F-Fc4a2M,4819
|
|
18
|
-
audiopod-1.4.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
19
|
-
audiopod-1.4.0.dist-info/top_level.txt,sha256=M6yyOFFNpLdH4i1AMRqJZLRIgfpg1NvrQVmnPd8A6N8,9
|
|
20
|
-
audiopod-1.4.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|