sonilo 0.2.0__tar.gz → 0.3.0__tar.gz
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.
- {sonilo-0.2.0 → sonilo-0.3.0}/PKG-INFO +35 -3
- {sonilo-0.2.0 → sonilo-0.3.0}/README.md +34 -2
- {sonilo-0.2.0 → sonilo-0.3.0}/pyproject.toml +1 -1
- {sonilo-0.2.0 → sonilo-0.3.0}/src/sonilo/__init__.py +6 -0
- {sonilo-0.2.0 → sonilo-0.3.0}/src/sonilo/_requests.py +32 -0
- sonilo-0.3.0/src/sonilo/_version.py +1 -0
- {sonilo-0.2.0 → sonilo-0.3.0}/src/sonilo/resources/tasks.py +103 -17
- sonilo-0.3.0/src/sonilo/resources/video_to_music.py +185 -0
- sonilo-0.3.0/src/sonilo/types.py +217 -0
- sonilo-0.3.0/tests/test_async_client.py +210 -0
- {sonilo-0.2.0 → sonilo-0.3.0}/tests/test_sync_client.py +224 -0
- sonilo-0.2.0/src/sonilo/_version.py +0 -1
- sonilo-0.2.0/src/sonilo/resources/video_to_music.py +0 -71
- sonilo-0.2.0/src/sonilo/types.py +0 -110
- sonilo-0.2.0/tests/test_async_client.py +0 -87
- {sonilo-0.2.0 → sonilo-0.3.0}/.github/workflows/ci.yml +0 -0
- {sonilo-0.2.0 → sonilo-0.3.0}/.github/workflows/publish.yml +0 -0
- {sonilo-0.2.0 → sonilo-0.3.0}/.gitignore +0 -0
- {sonilo-0.2.0 → sonilo-0.3.0}/LICENSE +0 -0
- {sonilo-0.2.0 → sonilo-0.3.0}/examples/generate.py +0 -0
- {sonilo-0.2.0 → sonilo-0.3.0}/examples/sfx.py +0 -0
- {sonilo-0.2.0 → sonilo-0.3.0}/src/sonilo/_async_client.py +0 -0
- {sonilo-0.2.0 → sonilo-0.3.0}/src/sonilo/_client.py +0 -0
- {sonilo-0.2.0 → sonilo-0.3.0}/src/sonilo/_streaming.py +0 -0
- {sonilo-0.2.0 → sonilo-0.3.0}/src/sonilo/errors.py +0 -0
- {sonilo-0.2.0 → sonilo-0.3.0}/src/sonilo/py.typed +0 -0
- {sonilo-0.2.0 → sonilo-0.3.0}/src/sonilo/resources/__init__.py +0 -0
- {sonilo-0.2.0 → sonilo-0.3.0}/src/sonilo/resources/account.py +0 -0
- {sonilo-0.2.0 → sonilo-0.3.0}/src/sonilo/resources/text_to_music.py +0 -0
- {sonilo-0.2.0 → sonilo-0.3.0}/src/sonilo/resources/text_to_sfx.py +0 -0
- {sonilo-0.2.0 → sonilo-0.3.0}/src/sonilo/resources/video_to_sfx.py +0 -0
- {sonilo-0.2.0 → sonilo-0.3.0}/tests/__init__.py +0 -0
- {sonilo-0.2.0 → sonilo-0.3.0}/tests/test_errors.py +0 -0
- {sonilo-0.2.0 → sonilo-0.3.0}/tests/test_requests.py +0 -0
- {sonilo-0.2.0 → sonilo-0.3.0}/tests/test_sfx.py +0 -0
- {sonilo-0.2.0 → sonilo-0.3.0}/tests/test_sfx_async.py +0 -0
- {sonilo-0.2.0 → sonilo-0.3.0}/tests/test_sfx_types.py +0 -0
- {sonilo-0.2.0 → sonilo-0.3.0}/tests/test_streaming.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sonilo
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Official Python client for the Sonilo API
|
|
5
5
|
Project-URL: Repository, https://github.com/sonilo-ai/sonilo-python
|
|
6
6
|
Author: Sonilo AI
|
|
@@ -49,6 +49,39 @@ track = client.video_to_music.generate(video="my_video.mp4", prompt="upbeat")
|
|
|
49
49
|
track = client.video_to_music.generate(video_url="https://example.com/clip.mp4")
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
+
### Vocal isolation (async)
|
|
53
|
+
|
|
54
|
+
Pass `isolate_vocals=True` to also get an isolated vocal stem and a muxed
|
|
55
|
+
video, alongside the scored audio. This requires async processing — submit
|
|
56
|
+
returns a `task_id` immediately, and `generate_async()` wraps submit + poll:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
result = client.video_to_music.generate_async(
|
|
60
|
+
video="my_video.mp4",
|
|
61
|
+
prompt="upbeat",
|
|
62
|
+
isolate_vocals=True, # implies mode="async"; omit mode to let it auto-select
|
|
63
|
+
)
|
|
64
|
+
result.save("mix.m4a") # result.audio[0] — the full mix
|
|
65
|
+
result.save("vocals.m4a", which="vocals")
|
|
66
|
+
result.save("video.mp4", which="mux") # video muxed with the generated audio
|
|
67
|
+
print(result.title.title if result.title else None)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Or control submission and polling yourself:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from sonilo.resources.tasks import parse_music_result
|
|
74
|
+
|
|
75
|
+
task = client.video_to_music.submit(video_url="https://example.com/clip.mp4", isolate_vocals=True)
|
|
76
|
+
result = client.tasks.wait(
|
|
77
|
+
task.task_id,
|
|
78
|
+
parser=parse_music_result, # required: tasks.wait()/get() default to the SFX parser
|
|
79
|
+
)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`isolate_vocals=True` with an explicit non-async `mode` raises `SoniloError`
|
|
83
|
+
locally before any request is sent.
|
|
84
|
+
|
|
52
85
|
## Streaming
|
|
53
86
|
|
|
54
87
|
```python
|
|
@@ -107,8 +140,7 @@ task = client.video_to_sfx.submit(
|
|
|
107
140
|
audio_format="wav",
|
|
108
141
|
)
|
|
109
142
|
result = client.tasks.wait(task.task_id, poll_interval=2.0, timeout=600.0)
|
|
110
|
-
result.save("audio.wav")
|
|
111
|
-
result.save("with_audio.mp4", which="video") # video-to-sfx also returns the muxed video
|
|
143
|
+
result.save("audio.wav") # video-to-sfx returns the generated audio only
|
|
112
144
|
```
|
|
113
145
|
|
|
114
146
|
`tasks.get(task_id)` fetches state once and never raises on a failed task;
|
|
@@ -32,6 +32,39 @@ track = client.video_to_music.generate(video="my_video.mp4", prompt="upbeat")
|
|
|
32
32
|
track = client.video_to_music.generate(video_url="https://example.com/clip.mp4")
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
+
### Vocal isolation (async)
|
|
36
|
+
|
|
37
|
+
Pass `isolate_vocals=True` to also get an isolated vocal stem and a muxed
|
|
38
|
+
video, alongside the scored audio. This requires async processing — submit
|
|
39
|
+
returns a `task_id` immediately, and `generate_async()` wraps submit + poll:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
result = client.video_to_music.generate_async(
|
|
43
|
+
video="my_video.mp4",
|
|
44
|
+
prompt="upbeat",
|
|
45
|
+
isolate_vocals=True, # implies mode="async"; omit mode to let it auto-select
|
|
46
|
+
)
|
|
47
|
+
result.save("mix.m4a") # result.audio[0] — the full mix
|
|
48
|
+
result.save("vocals.m4a", which="vocals")
|
|
49
|
+
result.save("video.mp4", which="mux") # video muxed with the generated audio
|
|
50
|
+
print(result.title.title if result.title else None)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Or control submission and polling yourself:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from sonilo.resources.tasks import parse_music_result
|
|
57
|
+
|
|
58
|
+
task = client.video_to_music.submit(video_url="https://example.com/clip.mp4", isolate_vocals=True)
|
|
59
|
+
result = client.tasks.wait(
|
|
60
|
+
task.task_id,
|
|
61
|
+
parser=parse_music_result, # required: tasks.wait()/get() default to the SFX parser
|
|
62
|
+
)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`isolate_vocals=True` with an explicit non-async `mode` raises `SoniloError`
|
|
66
|
+
locally before any request is sent.
|
|
67
|
+
|
|
35
68
|
## Streaming
|
|
36
69
|
|
|
37
70
|
```python
|
|
@@ -90,8 +123,7 @@ task = client.video_to_sfx.submit(
|
|
|
90
123
|
audio_format="wav",
|
|
91
124
|
)
|
|
92
125
|
result = client.tasks.wait(task.task_id, poll_interval=2.0, timeout=600.0)
|
|
93
|
-
result.save("audio.wav")
|
|
94
|
-
result.save("with_audio.mp4", which="video") # video-to-sfx also returns the muxed video
|
|
126
|
+
result.save("audio.wav") # video-to-sfx returns the generated audio only
|
|
95
127
|
```
|
|
96
128
|
|
|
97
129
|
`tasks.get(task_id)` fetches state once and never raises on a failed task;
|
|
@@ -13,6 +13,9 @@ from sonilo.errors import (
|
|
|
13
13
|
TaskTimeoutError,
|
|
14
14
|
)
|
|
15
15
|
from sonilo.types import (
|
|
16
|
+
MusicAudioMedia,
|
|
17
|
+
MusicResult,
|
|
18
|
+
MusicTitle,
|
|
16
19
|
Segment,
|
|
17
20
|
SfxMedia,
|
|
18
21
|
SfxResult,
|
|
@@ -28,6 +31,9 @@ __all__ = [
|
|
|
28
31
|
"AuthenticationError",
|
|
29
32
|
"BadRequestError",
|
|
30
33
|
"GenerationError",
|
|
34
|
+
"MusicAudioMedia",
|
|
35
|
+
"MusicResult",
|
|
36
|
+
"MusicTitle",
|
|
31
37
|
"PaymentRequiredError",
|
|
32
38
|
"RateLimitError",
|
|
33
39
|
"Segment",
|
|
@@ -66,6 +66,38 @@ def build_v2m_parts(
|
|
|
66
66
|
return data, files, opened
|
|
67
67
|
|
|
68
68
|
|
|
69
|
+
def _resolve_music_mode(mode: Optional[str], isolate_vocals: Optional[bool]) -> str:
|
|
70
|
+
"""isolate_vocals only works with async processing: auto-select mode
|
|
71
|
+
"async" when the caller didn't specify one, but fail fast (mirroring the
|
|
72
|
+
video/video_url XOR check above) if they explicitly asked for anything
|
|
73
|
+
else. Without isolate_vocals, submit() still needs an async response
|
|
74
|
+
(a task_id ack, not a stream), so "async" is also the default there.
|
|
75
|
+
"""
|
|
76
|
+
if isolate_vocals:
|
|
77
|
+
if mode is not None and mode != "async":
|
|
78
|
+
raise SoniloError("isolate_vocals=True requires mode='async'")
|
|
79
|
+
return "async"
|
|
80
|
+
return mode or "async"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def build_v2m_async_parts(
|
|
84
|
+
video: Any,
|
|
85
|
+
video_url: Optional[str],
|
|
86
|
+
prompt: Optional[str],
|
|
87
|
+
segments: Optional[List[Segment]],
|
|
88
|
+
mode: Optional[str],
|
|
89
|
+
isolate_vocals: Optional[bool],
|
|
90
|
+
) -> Tuple[Dict[str, str], Optional[Dict[str, tuple]], bool]:
|
|
91
|
+
"""Like build_v2m_parts, plus the async-only `mode`/`isolate_vocals`
|
|
92
|
+
fields used by the video-to-music submit()/generate_async() path."""
|
|
93
|
+
resolved_mode = _resolve_music_mode(mode, isolate_vocals)
|
|
94
|
+
data, files, opened = build_v2m_parts(video, video_url, prompt, segments)
|
|
95
|
+
data["mode"] = resolved_mode
|
|
96
|
+
if isolate_vocals is not None:
|
|
97
|
+
data["isolate_vocals"] = "true" if isolate_vocals else "false"
|
|
98
|
+
return data, files, opened
|
|
99
|
+
|
|
100
|
+
|
|
69
101
|
def build_sfx_t2s_data(
|
|
70
102
|
prompt: str, duration: int, audio_format: Optional[str]
|
|
71
103
|
) -> Dict[str, str]:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.0"
|
|
@@ -2,11 +2,11 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import time
|
|
5
|
-
from typing import TYPE_CHECKING, Any, Dict, Optional
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Protocol, TypeVar
|
|
6
6
|
from urllib.parse import quote
|
|
7
7
|
|
|
8
8
|
from sonilo.errors import SoniloError, TaskFailedError, TaskTimeoutError
|
|
9
|
-
from sonilo.types import SfxMedia, SfxResult, SfxTask
|
|
9
|
+
from sonilo.types import MusicAudioMedia, MusicResult, MusicTitle, SfxMedia, SfxResult, SfxTask
|
|
10
10
|
|
|
11
11
|
if TYPE_CHECKING:
|
|
12
12
|
from sonilo._async_client import AsyncSonilo
|
|
@@ -21,6 +21,19 @@ _async_sleep = asyncio.sleep
|
|
|
21
21
|
_monotonic = time.monotonic
|
|
22
22
|
|
|
23
23
|
|
|
24
|
+
class _PollableResult(Protocol):
|
|
25
|
+
"""Structural shape Tasks.get()/wait() need from any parsed result,
|
|
26
|
+
regardless of which endpoint produced it (SFX vs. music)."""
|
|
27
|
+
|
|
28
|
+
task_id: str
|
|
29
|
+
status: str
|
|
30
|
+
error: Optional[Dict[str, Any]]
|
|
31
|
+
refunded: Optional[bool]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
ResultT = TypeVar("ResultT", bound=_PollableResult)
|
|
35
|
+
|
|
36
|
+
|
|
24
37
|
def _media_from(data: Any) -> Optional[SfxMedia]:
|
|
25
38
|
if not isinstance(data, dict) or "url" not in data:
|
|
26
39
|
return None
|
|
@@ -48,6 +61,61 @@ def parse_sfx_result(body: Dict[str, Any]) -> SfxResult:
|
|
|
48
61
|
raise SoniloError(f"Malformed task response: missing {e.args[0]!r}") from e
|
|
49
62
|
|
|
50
63
|
|
|
64
|
+
def _music_audio_from(data: Any) -> Optional[MusicAudioMedia]:
|
|
65
|
+
if not isinstance(data, dict) or "url" not in data:
|
|
66
|
+
return None
|
|
67
|
+
return MusicAudioMedia(
|
|
68
|
+
stream_index=data.get("stream_index", 0),
|
|
69
|
+
url=data["url"],
|
|
70
|
+
content_type=data.get("content_type"),
|
|
71
|
+
file_size=data.get("file_size"),
|
|
72
|
+
sample_rate=data.get("sample_rate"),
|
|
73
|
+
channels=data.get("channels"),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _music_audio_list_from(data: Any) -> Optional[List[MusicAudioMedia]]:
|
|
78
|
+
if not isinstance(data, list):
|
|
79
|
+
return None
|
|
80
|
+
items = [item for item in (_music_audio_from(entry) for entry in data) if item is not None]
|
|
81
|
+
return items
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _music_title_from(data: Any) -> Optional[MusicTitle]:
|
|
85
|
+
if not isinstance(data, dict):
|
|
86
|
+
return None
|
|
87
|
+
return MusicTitle(
|
|
88
|
+
title=data.get("title"),
|
|
89
|
+
summary=data.get("summary"),
|
|
90
|
+
display_tags=data.get("display_tags"),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def parse_music_result(body: Dict[str, Any]) -> MusicResult:
|
|
95
|
+
"""Map a GET /v1/tasks/{id} body for a video-to-music task to
|
|
96
|
+
MusicResult; unknown fields are ignored.
|
|
97
|
+
|
|
98
|
+
`audio` is always a list; `vocals`/`mux` are only populated when the
|
|
99
|
+
task was submitted with isolate_vocals=True.
|
|
100
|
+
"""
|
|
101
|
+
try:
|
|
102
|
+
return MusicResult(
|
|
103
|
+
task_id=body["task_id"],
|
|
104
|
+
status=body["status"],
|
|
105
|
+
type=body.get("type"),
|
|
106
|
+
audio=_music_audio_list_from(body.get("audio")),
|
|
107
|
+
vocals=_media_from(body.get("vocals")),
|
|
108
|
+
mux=_music_audio_list_from(body.get("mux")),
|
|
109
|
+
title=_music_title_from(body.get("title")),
|
|
110
|
+
duration_seconds=body.get("duration_seconds"),
|
|
111
|
+
cost=body.get("cost"),
|
|
112
|
+
error=body.get("error"),
|
|
113
|
+
refunded=body.get("refunded"),
|
|
114
|
+
)
|
|
115
|
+
except KeyError as e:
|
|
116
|
+
raise SoniloError(f"Malformed task response: missing {e.args[0]!r}") from e
|
|
117
|
+
|
|
118
|
+
|
|
51
119
|
def parse_sfx_task(body: Dict[str, Any]) -> SfxTask:
|
|
52
120
|
"""Map a submission ack to SfxTask."""
|
|
53
121
|
try:
|
|
@@ -56,7 +124,7 @@ def parse_sfx_task(body: Dict[str, Any]) -> SfxTask:
|
|
|
56
124
|
raise SoniloError(f"Malformed task response: missing {e.args[0]!r}") from e
|
|
57
125
|
|
|
58
126
|
|
|
59
|
-
def _raise_if_failed(result:
|
|
127
|
+
def _raise_if_failed(result: _PollableResult) -> None:
|
|
60
128
|
if result.status == "failed":
|
|
61
129
|
error = result.error if isinstance(result.error, dict) else {}
|
|
62
130
|
message = error.get("message") or "Generation failed"
|
|
@@ -87,11 +155,19 @@ class Tasks:
|
|
|
87
155
|
def __init__(self, client: "Sonilo") -> None:
|
|
88
156
|
self._client = client
|
|
89
157
|
|
|
90
|
-
def get(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
158
|
+
def get(
|
|
159
|
+
self,
|
|
160
|
+
task_id: str,
|
|
161
|
+
*,
|
|
162
|
+
parser: Callable[[Dict[str, Any]], ResultT] = parse_sfx_result, # type: ignore[assignment]
|
|
163
|
+
) -> ResultT:
|
|
164
|
+
"""Fetch current task state. Never raises on a failed status.
|
|
165
|
+
|
|
166
|
+
`parser` maps the raw response body to a result type; it defaults to
|
|
167
|
+
the SFX parser for back-compat. Pass `parse_music_result` for
|
|
168
|
+
video-to-music async tasks.
|
|
169
|
+
"""
|
|
170
|
+
return parser(self._client._get_json(f"/v1/tasks/{quote(task_id, safe='')}"))
|
|
95
171
|
|
|
96
172
|
def wait(
|
|
97
173
|
self,
|
|
@@ -99,12 +175,13 @@ class Tasks:
|
|
|
99
175
|
*,
|
|
100
176
|
poll_interval: float = DEFAULT_POLL_INTERVAL,
|
|
101
177
|
timeout: float = DEFAULT_WAIT_TIMEOUT,
|
|
102
|
-
|
|
178
|
+
parser: Callable[[Dict[str, Any]], ResultT] = parse_sfx_result, # type: ignore[assignment]
|
|
179
|
+
) -> ResultT:
|
|
103
180
|
"""Poll until the task is terminal; raise on failure or deadline."""
|
|
104
181
|
_validate_wait_args(poll_interval, timeout)
|
|
105
182
|
deadline = _monotonic() + timeout
|
|
106
183
|
while True:
|
|
107
|
-
result = self.get(task_id)
|
|
184
|
+
result = self.get(task_id, parser=parser)
|
|
108
185
|
if result.status == "succeeded":
|
|
109
186
|
return result
|
|
110
187
|
_raise_if_failed(result)
|
|
@@ -118,11 +195,19 @@ class AsyncTasks:
|
|
|
118
195
|
def __init__(self, client: "AsyncSonilo") -> None:
|
|
119
196
|
self._client = client
|
|
120
197
|
|
|
121
|
-
async def get(
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
198
|
+
async def get(
|
|
199
|
+
self,
|
|
200
|
+
task_id: str,
|
|
201
|
+
*,
|
|
202
|
+
parser: Callable[[Dict[str, Any]], ResultT] = parse_sfx_result, # type: ignore[assignment]
|
|
203
|
+
) -> ResultT:
|
|
204
|
+
"""Fetch current task state. Never raises on a failed status.
|
|
205
|
+
|
|
206
|
+
`parser` maps the raw response body to a result type; it defaults to
|
|
207
|
+
the SFX parser for back-compat. Pass `parse_music_result` for
|
|
208
|
+
video-to-music async tasks.
|
|
209
|
+
"""
|
|
210
|
+
return parser(await self._client._get_json(f"/v1/tasks/{quote(task_id, safe='')}"))
|
|
126
211
|
|
|
127
212
|
async def wait(
|
|
128
213
|
self,
|
|
@@ -130,12 +215,13 @@ class AsyncTasks:
|
|
|
130
215
|
*,
|
|
131
216
|
poll_interval: float = DEFAULT_POLL_INTERVAL,
|
|
132
217
|
timeout: float = DEFAULT_WAIT_TIMEOUT,
|
|
133
|
-
|
|
218
|
+
parser: Callable[[Dict[str, Any]], ResultT] = parse_sfx_result, # type: ignore[assignment]
|
|
219
|
+
) -> ResultT:
|
|
134
220
|
"""Poll until the task is terminal; raise on failure or deadline."""
|
|
135
221
|
_validate_wait_args(poll_interval, timeout)
|
|
136
222
|
deadline = _monotonic() + timeout
|
|
137
223
|
while True:
|
|
138
|
-
result = await self.get(task_id)
|
|
224
|
+
result = await self.get(task_id, parser=parser)
|
|
139
225
|
if result.status == "succeeded":
|
|
140
226
|
return result
|
|
141
227
|
_raise_if_failed(result)
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any, AsyncIterator, Iterator, List, Optional
|
|
4
|
+
|
|
5
|
+
from sonilo._requests import build_v2m_async_parts, build_v2m_parts
|
|
6
|
+
from sonilo._streaming import acollect_track, collect_track
|
|
7
|
+
from sonilo.resources.tasks import (
|
|
8
|
+
DEFAULT_POLL_INTERVAL,
|
|
9
|
+
DEFAULT_WAIT_TIMEOUT,
|
|
10
|
+
parse_music_result,
|
|
11
|
+
parse_sfx_task,
|
|
12
|
+
)
|
|
13
|
+
from sonilo.types import MusicResult, Segment, SfxTask, StreamEvent, Track
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from sonilo._async_client import AsyncSonilo
|
|
17
|
+
from sonilo._client import Sonilo
|
|
18
|
+
|
|
19
|
+
PATH = "/v1/video-to-music"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class VideoToMusic:
|
|
23
|
+
def __init__(self, client: "Sonilo") -> None:
|
|
24
|
+
self._client = client
|
|
25
|
+
|
|
26
|
+
def stream(
|
|
27
|
+
self,
|
|
28
|
+
*,
|
|
29
|
+
video: Any = None,
|
|
30
|
+
video_url: Optional[str] = None,
|
|
31
|
+
prompt: Optional[str] = None,
|
|
32
|
+
segments: Optional[List[Segment]] = None,
|
|
33
|
+
) -> Iterator[StreamEvent]:
|
|
34
|
+
data, files, opened = build_v2m_parts(video, video_url, prompt, segments)
|
|
35
|
+
close_after = files["video"][1] if files is not None and opened else None
|
|
36
|
+
return self._client._stream_events(PATH, data=data, files=files, close_after=close_after)
|
|
37
|
+
|
|
38
|
+
def generate(
|
|
39
|
+
self,
|
|
40
|
+
*,
|
|
41
|
+
video: Any = None,
|
|
42
|
+
video_url: Optional[str] = None,
|
|
43
|
+
prompt: Optional[str] = None,
|
|
44
|
+
segments: Optional[List[Segment]] = None,
|
|
45
|
+
) -> Track:
|
|
46
|
+
return collect_track(
|
|
47
|
+
self.stream(video=video, video_url=video_url, prompt=prompt, segments=segments)
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def submit(
|
|
51
|
+
self,
|
|
52
|
+
*,
|
|
53
|
+
video: Any = None,
|
|
54
|
+
video_url: Optional[str] = None,
|
|
55
|
+
prompt: Optional[str] = None,
|
|
56
|
+
segments: Optional[List[Segment]] = None,
|
|
57
|
+
isolate_vocals: Optional[bool] = None,
|
|
58
|
+
mode: Optional[str] = None,
|
|
59
|
+
) -> SfxTask:
|
|
60
|
+
"""Submit an async video-to-music task and return its ack.
|
|
61
|
+
|
|
62
|
+
isolate_vocals=True requires mode="async" (auto-selected if `mode`
|
|
63
|
+
is omitted); passing an explicit non-async mode alongside
|
|
64
|
+
isolate_vocals raises a SoniloError before any request is made. Poll
|
|
65
|
+
with `client.tasks.wait(task_id, parser=sonilo.resources.tasks.parse_music_result)`
|
|
66
|
+
or use `generate_async()` to submit and wait in one call.
|
|
67
|
+
"""
|
|
68
|
+
data, files, opened = build_v2m_async_parts(
|
|
69
|
+
video, video_url, prompt, segments, mode, isolate_vocals
|
|
70
|
+
)
|
|
71
|
+
close_after = files["video"][1] if files is not None and opened else None
|
|
72
|
+
return parse_sfx_task(
|
|
73
|
+
self._client._post_json(PATH, data=data, files=files, close_after=close_after)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def generate_async(
|
|
77
|
+
self,
|
|
78
|
+
*,
|
|
79
|
+
video: Any = None,
|
|
80
|
+
video_url: Optional[str] = None,
|
|
81
|
+
prompt: Optional[str] = None,
|
|
82
|
+
segments: Optional[List[Segment]] = None,
|
|
83
|
+
isolate_vocals: Optional[bool] = None,
|
|
84
|
+
mode: Optional[str] = None,
|
|
85
|
+
poll_interval: float = DEFAULT_POLL_INTERVAL,
|
|
86
|
+
timeout: float = DEFAULT_WAIT_TIMEOUT,
|
|
87
|
+
) -> MusicResult:
|
|
88
|
+
"""submit() + tasks.wait(), returning the parsed MusicResult."""
|
|
89
|
+
task = self.submit(
|
|
90
|
+
video=video,
|
|
91
|
+
video_url=video_url,
|
|
92
|
+
prompt=prompt,
|
|
93
|
+
segments=segments,
|
|
94
|
+
isolate_vocals=isolate_vocals,
|
|
95
|
+
mode=mode,
|
|
96
|
+
)
|
|
97
|
+
return self._client.tasks.wait(
|
|
98
|
+
task.task_id,
|
|
99
|
+
poll_interval=poll_interval,
|
|
100
|
+
timeout=timeout,
|
|
101
|
+
parser=parse_music_result,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class AsyncVideoToMusic:
|
|
106
|
+
def __init__(self, client: "AsyncSonilo") -> None:
|
|
107
|
+
self._client = client
|
|
108
|
+
|
|
109
|
+
def stream(
|
|
110
|
+
self,
|
|
111
|
+
*,
|
|
112
|
+
video: Any = None,
|
|
113
|
+
video_url: Optional[str] = None,
|
|
114
|
+
prompt: Optional[str] = None,
|
|
115
|
+
segments: Optional[List[Segment]] = None,
|
|
116
|
+
) -> AsyncIterator[StreamEvent]:
|
|
117
|
+
data, files, opened = build_v2m_parts(video, video_url, prompt, segments)
|
|
118
|
+
close_after = files["video"][1] if files is not None and opened else None
|
|
119
|
+
return self._client._stream_events(PATH, data=data, files=files, close_after=close_after)
|
|
120
|
+
|
|
121
|
+
async def generate(
|
|
122
|
+
self,
|
|
123
|
+
*,
|
|
124
|
+
video: Any = None,
|
|
125
|
+
video_url: Optional[str] = None,
|
|
126
|
+
prompt: Optional[str] = None,
|
|
127
|
+
segments: Optional[List[Segment]] = None,
|
|
128
|
+
) -> Track:
|
|
129
|
+
return await acollect_track(
|
|
130
|
+
self.stream(video=video, video_url=video_url, prompt=prompt, segments=segments)
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
async def submit(
|
|
134
|
+
self,
|
|
135
|
+
*,
|
|
136
|
+
video: Any = None,
|
|
137
|
+
video_url: Optional[str] = None,
|
|
138
|
+
prompt: Optional[str] = None,
|
|
139
|
+
segments: Optional[List[Segment]] = None,
|
|
140
|
+
isolate_vocals: Optional[bool] = None,
|
|
141
|
+
mode: Optional[str] = None,
|
|
142
|
+
) -> SfxTask:
|
|
143
|
+
"""Submit an async video-to-music task and return its ack.
|
|
144
|
+
|
|
145
|
+
isolate_vocals=True requires mode="async" (auto-selected if `mode`
|
|
146
|
+
is omitted); passing an explicit non-async mode alongside
|
|
147
|
+
isolate_vocals raises a SoniloError before any request is made.
|
|
148
|
+
"""
|
|
149
|
+
data, files, opened = build_v2m_async_parts(
|
|
150
|
+
video, video_url, prompt, segments, mode, isolate_vocals
|
|
151
|
+
)
|
|
152
|
+
close_after = files["video"][1] if files is not None and opened else None
|
|
153
|
+
return parse_sfx_task(
|
|
154
|
+
await self._client._post_json(
|
|
155
|
+
PATH, data=data, files=files, close_after=close_after
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
async def generate_async(
|
|
160
|
+
self,
|
|
161
|
+
*,
|
|
162
|
+
video: Any = None,
|
|
163
|
+
video_url: Optional[str] = None,
|
|
164
|
+
prompt: Optional[str] = None,
|
|
165
|
+
segments: Optional[List[Segment]] = None,
|
|
166
|
+
isolate_vocals: Optional[bool] = None,
|
|
167
|
+
mode: Optional[str] = None,
|
|
168
|
+
poll_interval: float = DEFAULT_POLL_INTERVAL,
|
|
169
|
+
timeout: float = DEFAULT_WAIT_TIMEOUT,
|
|
170
|
+
) -> MusicResult:
|
|
171
|
+
"""submit() + tasks.wait(), returning the parsed MusicResult."""
|
|
172
|
+
task = await self.submit(
|
|
173
|
+
video=video,
|
|
174
|
+
video_url=video_url,
|
|
175
|
+
prompt=prompt,
|
|
176
|
+
segments=segments,
|
|
177
|
+
isolate_vocals=isolate_vocals,
|
|
178
|
+
mode=mode,
|
|
179
|
+
)
|
|
180
|
+
return await self._client.tasks.wait(
|
|
181
|
+
task.task_id,
|
|
182
|
+
poll_interval=poll_interval,
|
|
183
|
+
timeout=timeout,
|
|
184
|
+
parser=parse_music_result,
|
|
185
|
+
)
|