Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,16 @@ track.save("output.mp3")
print(track.title)
```

`duration` is optional. Leave it out and the API picks the length from your
prompt — or from `segments` when you pass them, in which case the track runs to
the last segment's start plus 30 seconds. `text_to_sfx` takes an optional
`duration` too (from 0.5 seconds, defaulting to 8), so a sound effect can be
requested with nothing but a prompt:

```python
effect = client.text_to_sfx.generate(prompt="a small metal door latch clicking shut")
```

## Video to music

```python
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "sonilo"
version = "0.17.0"
version = "0.18.0"
description = "Official Python client for the Sonilo API"
readme = "README.md"
license = "MIT"
Expand Down
4 changes: 2 additions & 2 deletions sonilo-cli/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ build-backend = "hatchling.build"

[project]
name = "sonilo-cli"
version = "0.16.0"
version = "0.17.0"
description = "Command-line interface for the Sonilo API: generate music and sound effects from text or video"
readme = "README.md"
license = "MIT"
requires-python = ">=3.9"
authors = [{ name = "Sonilo AI" }]
dependencies = ["sonilo>=0.17.0,<0.18"]
dependencies = ["sonilo>=0.18.0,<0.19"]
keywords = ["sonilo", "cli", "music", "sfx", "text-to-music", "video-to-music", "ai"]

[project.urls]
Expand Down
2 changes: 1 addition & 1 deletion sonilo-cli/src/sonilo_cli/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
__version__ = "0.16.0"
__version__ = "0.17.0"

__all__ = ["__version__"]
11 changes: 9 additions & 2 deletions sonilo-cli/src/sonilo_cli/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -856,7 +856,11 @@ def build_parser() -> argparse.ArgumentParser:
p_t2m = sub.add_parser("text-to-music", help="Generate music from a text prompt")
_add_global(p_t2m)
p_t2m.add_argument("--prompt", required=True, help="What the music should sound like.")
p_t2m.add_argument("--duration", type=int, required=True, help="Track length in seconds.")
p_t2m.add_argument(
"--duration", type=int, default=None,
help="Track length in seconds. Omit to let the length follow the prompt "
"(or --segments).",
)
_add_segments(p_t2m, MUSIC_SHAPE)
p_t2m.add_argument("--output", default=None, help="Where to save the audio.")
p_t2m.add_argument("--format", choices=["m4a", "wav", "mp3"], default="m4a",
Expand Down Expand Up @@ -893,7 +897,10 @@ def build_parser() -> argparse.ArgumentParser:
p_t2s = sub.add_parser("text-to-sfx", help="Generate a sound effect from a text prompt")
_add_global(p_t2s)
p_t2s.add_argument("--prompt", required=True, help="What the sound effect should be.")
p_t2s.add_argument("--duration", type=int, required=True, help="Effect length in seconds.")
p_t2s.add_argument(
"--duration", type=float, default=None,
help="Effect length in seconds, from 0.5. Omit for the default length.",
)
p_t2s.add_argument("--output", default=None, help="Where to save the audio.")
p_t2s.add_argument("--format", choices=_SFX_FORMATS, default="wav",
help="Output format. Default: wav")
Expand Down
21 changes: 16 additions & 5 deletions src/sonilo/_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,23 @@ def close(self) -> None:


def build_t2m_data(
prompt: str, duration: int, segments: Optional[List[Segment]]
prompt: str, duration: Optional[int], segments: Optional[List[Segment]]
) -> Dict[str, str]:
data = {"prompt": prompt, "duration": str(duration)}
"""`duration` is optional: left out, the API resolves the length itself --
from the caller's segments when there are any, otherwise from the prompt.
Omit the field rather than sending a stand-in, which is the only way to
ask for that."""
data = {"prompt": prompt}
if duration is not None:
data["duration"] = str(duration)
if segments is not None:
data["segments"] = json.dumps(segments)
return data


def build_t2m_async_data(
prompt: str,
duration: int,
duration: Optional[int],
segments: Optional[List[Segment]],
mode: Optional[str],
output_format: Optional[str],
Expand Down Expand Up @@ -509,9 +515,14 @@ def build_v2s_parts(


def build_sfx_t2s_data(
prompt: str, duration: int, audio_format: Optional[str]
prompt: str, duration: Optional[float], audio_format: Optional[str]
) -> Dict[str, str]:
data = {"prompt": prompt, "duration": str(duration)}
"""`duration` is optional (the API generates its own default length when
it is absent) and fractional -- the shortest effects run well under a
second, so this is a number, not an integer."""
data = {"prompt": prompt}
if duration is not None:
data["duration"] = str(duration)
if audio_format is not None:
data["audio_format"] = audio_format
return data
Expand Down
2 changes: 1 addition & 1 deletion src/sonilo/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.17.0"
__version__ = "0.18.0"
16 changes: 8 additions & 8 deletions src/sonilo/resources/text_to_music.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def stream(
self,
*,
prompt: str,
duration: int,
duration: Optional[int] = None,
segments: Optional[List[Segment]] = None,
) -> Iterator[StreamEvent]:
data = build_t2m_data(prompt, duration, segments)
Expand All @@ -37,7 +37,7 @@ def generate(
self,
*,
prompt: str,
duration: int,
duration: Optional[int] = None,
segments: Optional[List[Segment]] = None,
) -> Track:
return collect_track(self.stream(prompt=prompt, duration=duration, segments=segments))
Expand All @@ -46,7 +46,7 @@ def submit(
self,
*,
prompt: str,
duration: int,
duration: Optional[int] = None,
segments: Optional[List[Segment]] = None,
mode: Optional[str] = None,
output_format: Optional[str] = None,
Expand Down Expand Up @@ -79,7 +79,7 @@ def generate_async(
self,
*,
prompt: str,
duration: int,
duration: Optional[int] = None,
segments: Optional[List[Segment]] = None,
mode: Optional[str] = None,
output_format: Optional[str] = None,
Expand Down Expand Up @@ -113,7 +113,7 @@ def stream(
self,
*,
prompt: str,
duration: int,
duration: Optional[int] = None,
segments: Optional[List[Segment]] = None,
) -> AsyncIterator[StreamEvent]:
data = build_t2m_data(prompt, duration, segments)
Expand All @@ -123,7 +123,7 @@ async def generate(
self,
*,
prompt: str,
duration: int,
duration: Optional[int] = None,
segments: Optional[List[Segment]] = None,
) -> Track:
return await acollect_track(
Expand All @@ -134,7 +134,7 @@ async def submit(
self,
*,
prompt: str,
duration: int,
duration: Optional[int] = None,
segments: Optional[List[Segment]] = None,
mode: Optional[str] = None,
output_format: Optional[str] = None,
Expand All @@ -158,7 +158,7 @@ async def generate_async(
self,
*,
prompt: str,
duration: int,
duration: Optional[int] = None,
segments: Optional[List[Segment]] = None,
mode: Optional[str] = None,
output_format: Optional[str] = None,
Expand Down
10 changes: 6 additions & 4 deletions src/sonilo/resources/text_to_sfx.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ def __init__(self, client: "Sonilo") -> None:
self._client = client

def submit(
self, *, prompt: str, duration: int, audio_format: Optional[str] = None
self, *, prompt: str, duration: Optional[float] = None,
audio_format: Optional[str] = None
) -> SfxTask:
data = build_sfx_t2s_data(prompt, duration, audio_format)
return parse_sfx_task(self._client._post_json(PATH, data=data))
Expand All @@ -27,7 +28,7 @@ def generate(
self,
*,
prompt: str,
duration: int,
duration: Optional[float] = None,
audio_format: Optional[str] = None,
poll_interval: float = DEFAULT_POLL_INTERVAL,
timeout: float = DEFAULT_WAIT_TIMEOUT,
Expand All @@ -43,7 +44,8 @@ def __init__(self, client: "AsyncSonilo") -> None:
self._client = client

async def submit(
self, *, prompt: str, duration: int, audio_format: Optional[str] = None
self, *, prompt: str, duration: Optional[float] = None,
audio_format: Optional[str] = None
) -> SfxTask:
data = build_sfx_t2s_data(prompt, duration, audio_format)
return parse_sfx_task(await self._client._post_json(PATH, data=data))
Expand All @@ -52,7 +54,7 @@ async def generate(
self,
*,
prompt: str,
duration: int,
duration: Optional[float] = None,
audio_format: Optional[str] = None,
poll_interval: float = DEFAULT_POLL_INTERVAL,
timeout: float = DEFAULT_WAIT_TIMEOUT,
Expand Down
30 changes: 30 additions & 0 deletions tests/test_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,3 +323,33 @@ def test_build_dubbing_parts_sends_export_srt_with_subtitles():
)
assert data["export_srt"] == "true"



# --- optional duration ------------------------------------------------------
# The API resolves an omitted duration itself: text-to-music infers a length
# from the prompt (or the caller's segments), text-to-sfx uses its own default.
# The client's job is only to leave the field out rather than invent a number.

def test_build_t2m_data_omits_an_absent_duration():
assert "duration" not in build_t2m_data("lofi beat", None, None)


def test_build_t2m_async_data_omits_an_absent_duration():
from sonilo._requests import build_t2m_async_data

data = build_t2m_async_data("lofi beat", None, None, "async", None, None, None)
assert "duration" not in data


def test_build_sfx_t2s_data_omits_an_absent_duration():
from sonilo._requests import build_sfx_t2s_data

assert "duration" not in build_sfx_t2s_data("a door latch", None, None)


def test_build_sfx_t2s_data_keeps_a_fractional_duration():
"""The shortest effects are well under a second, so duration is a number,
not an integer — 0.5 must not be rounded or stringified as an int."""
from sonilo._requests import build_sfx_t2s_data

assert build_sfx_t2s_data("a door latch", 0.5, None)["duration"] == "0.5"
Loading