Skip to content

Commit afaa1c3

Browse files
feat: make duration optional on text-to-music and text-to-sfx (#41)
The API resolves an omitted duration itself, so the client's job is to leave the field out rather than invent a number: * text-to-music infers the length from the prompt, or from `segments` when they are given (the last segment's start plus 30 seconds). * text-to-sfx generates its own default length (8 seconds). Both builders sent `duration` unconditionally, so passing None would have put the literal string "None" on the wire; they now omit the field. text-to-sfx's duration is also a float now, not an int: the API's minimum is 0.5 seconds, and the shortest effects -- a latch, a click, a single footstep -- run well under a second. sonilo 0.17.0 -> 0.18.0, sonilo-cli 0.16.0 -> 0.17.0, with the CLI's narrow core pin widened to match. `--duration` is optional on both CLI commands.
1 parent acd9004 commit afaa1c3

10 files changed

Lines changed: 84 additions & 24 deletions

File tree

‎README.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,16 @@ track.save("output.mp3")
5252
print(track.title)
5353
```
5454

55+
`duration` is optional. Leave it out and the API picks the length from your
56+
prompt — or from `segments` when you pass them, in which case the track runs to
57+
the last segment's start plus 30 seconds. `text_to_sfx` takes an optional
58+
`duration` too (from 0.5 seconds, defaulting to 8), so a sound effect can be
59+
requested with nothing but a prompt:
60+
61+
```python
62+
effect = client.text_to_sfx.generate(prompt="a small metal door latch clicking shut")
63+
```
64+
5565
## Video to music
5666

5767
```python

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "sonilo"
7-
version = "0.17.0"
7+
version = "0.18.0"
88
description = "Official Python client for the Sonilo API"
99
readme = "README.md"
1010
license = "MIT"

‎sonilo-cli/pyproject.toml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ build-backend = "hatchling.build"
44

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

1616
[project.urls]
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
__version__ = "0.16.0"
1+
__version__ = "0.17.0"
22

33
__all__ = ["__version__"]

‎sonilo-cli/src/sonilo_cli/__main__.py‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -856,7 +856,11 @@ def build_parser() -> argparse.ArgumentParser:
856856
p_t2m = sub.add_parser("text-to-music", help="Generate music from a text prompt")
857857
_add_global(p_t2m)
858858
p_t2m.add_argument("--prompt", required=True, help="What the music should sound like.")
859-
p_t2m.add_argument("--duration", type=int, required=True, help="Track length in seconds.")
859+
p_t2m.add_argument(
860+
"--duration", type=int, default=None,
861+
help="Track length in seconds. Omit to let the length follow the prompt "
862+
"(or --segments).",
863+
)
860864
_add_segments(p_t2m, MUSIC_SHAPE)
861865
p_t2m.add_argument("--output", default=None, help="Where to save the audio.")
862866
p_t2m.add_argument("--format", choices=["m4a", "wav", "mp3"], default="m4a",
@@ -893,7 +897,10 @@ def build_parser() -> argparse.ArgumentParser:
893897
p_t2s = sub.add_parser("text-to-sfx", help="Generate a sound effect from a text prompt")
894898
_add_global(p_t2s)
895899
p_t2s.add_argument("--prompt", required=True, help="What the sound effect should be.")
896-
p_t2s.add_argument("--duration", type=int, required=True, help="Effect length in seconds.")
900+
p_t2s.add_argument(
901+
"--duration", type=float, default=None,
902+
help="Effect length in seconds, from 0.5. Omit for the default length.",
903+
)
897904
p_t2s.add_argument("--output", default=None, help="Where to save the audio.")
898905
p_t2s.add_argument("--format", choices=_SFX_FORMATS, default="wav",
899906
help="Output format. Default: wav")

‎src/sonilo/_requests.py‎

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,23 @@ def close(self) -> None:
2626

2727

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

3642

3743
def build_t2m_async_data(
3844
prompt: str,
39-
duration: int,
45+
duration: Optional[int],
4046
segments: Optional[List[Segment]],
4147
mode: Optional[str],
4248
output_format: Optional[str],
@@ -509,9 +515,14 @@ def build_v2s_parts(
509515

510516

511517
def build_sfx_t2s_data(
512-
prompt: str, duration: int, audio_format: Optional[str]
518+
prompt: str, duration: Optional[float], audio_format: Optional[str]
513519
) -> Dict[str, str]:
514-
data = {"prompt": prompt, "duration": str(duration)}
520+
"""`duration` is optional (the API generates its own default length when
521+
it is absent) and fractional -- the shortest effects run well under a
522+
second, so this is a number, not an integer."""
523+
data = {"prompt": prompt}
524+
if duration is not None:
525+
data["duration"] = str(duration)
515526
if audio_format is not None:
516527
data["audio_format"] = audio_format
517528
return data

‎src/sonilo/_version.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.17.0"
1+
__version__ = "0.18.0"

‎src/sonilo/resources/text_to_music.py‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def stream(
2727
self,
2828
*,
2929
prompt: str,
30-
duration: int,
30+
duration: Optional[int] = None,
3131
segments: Optional[List[Segment]] = None,
3232
) -> Iterator[StreamEvent]:
3333
data = build_t2m_data(prompt, duration, segments)
@@ -37,7 +37,7 @@ def generate(
3737
self,
3838
*,
3939
prompt: str,
40-
duration: int,
40+
duration: Optional[int] = None,
4141
segments: Optional[List[Segment]] = None,
4242
) -> Track:
4343
return collect_track(self.stream(prompt=prompt, duration=duration, segments=segments))
@@ -46,7 +46,7 @@ def submit(
4646
self,
4747
*,
4848
prompt: str,
49-
duration: int,
49+
duration: Optional[int] = None,
5050
segments: Optional[List[Segment]] = None,
5151
mode: Optional[str] = None,
5252
output_format: Optional[str] = None,
@@ -79,7 +79,7 @@ def generate_async(
7979
self,
8080
*,
8181
prompt: str,
82-
duration: int,
82+
duration: Optional[int] = None,
8383
segments: Optional[List[Segment]] = None,
8484
mode: Optional[str] = None,
8585
output_format: Optional[str] = None,
@@ -113,7 +113,7 @@ def stream(
113113
self,
114114
*,
115115
prompt: str,
116-
duration: int,
116+
duration: Optional[int] = None,
117117
segments: Optional[List[Segment]] = None,
118118
) -> AsyncIterator[StreamEvent]:
119119
data = build_t2m_data(prompt, duration, segments)
@@ -123,7 +123,7 @@ async def generate(
123123
self,
124124
*,
125125
prompt: str,
126-
duration: int,
126+
duration: Optional[int] = None,
127127
segments: Optional[List[Segment]] = None,
128128
) -> Track:
129129
return await acollect_track(
@@ -134,7 +134,7 @@ async def submit(
134134
self,
135135
*,
136136
prompt: str,
137-
duration: int,
137+
duration: Optional[int] = None,
138138
segments: Optional[List[Segment]] = None,
139139
mode: Optional[str] = None,
140140
output_format: Optional[str] = None,
@@ -158,7 +158,7 @@ async def generate_async(
158158
self,
159159
*,
160160
prompt: str,
161-
duration: int,
161+
duration: Optional[int] = None,
162162
segments: Optional[List[Segment]] = None,
163163
mode: Optional[str] = None,
164164
output_format: Optional[str] = None,

‎src/sonilo/resources/text_to_sfx.py‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ def __init__(self, client: "Sonilo") -> None:
1818
self._client = client
1919

2020
def submit(
21-
self, *, prompt: str, duration: int, audio_format: Optional[str] = None
21+
self, *, prompt: str, duration: Optional[float] = None,
22+
audio_format: Optional[str] = None
2223
) -> SfxTask:
2324
data = build_sfx_t2s_data(prompt, duration, audio_format)
2425
return parse_sfx_task(self._client._post_json(PATH, data=data))
@@ -27,7 +28,7 @@ def generate(
2728
self,
2829
*,
2930
prompt: str,
30-
duration: int,
31+
duration: Optional[float] = None,
3132
audio_format: Optional[str] = None,
3233
poll_interval: float = DEFAULT_POLL_INTERVAL,
3334
timeout: float = DEFAULT_WAIT_TIMEOUT,
@@ -43,7 +44,8 @@ def __init__(self, client: "AsyncSonilo") -> None:
4344
self._client = client
4445

4546
async def submit(
46-
self, *, prompt: str, duration: int, audio_format: Optional[str] = None
47+
self, *, prompt: str, duration: Optional[float] = None,
48+
audio_format: Optional[str] = None
4749
) -> SfxTask:
4850
data = build_sfx_t2s_data(prompt, duration, audio_format)
4951
return parse_sfx_task(await self._client._post_json(PATH, data=data))
@@ -52,7 +54,7 @@ async def generate(
5254
self,
5355
*,
5456
prompt: str,
55-
duration: int,
57+
duration: Optional[float] = None,
5658
audio_format: Optional[str] = None,
5759
poll_interval: float = DEFAULT_POLL_INTERVAL,
5860
timeout: float = DEFAULT_WAIT_TIMEOUT,

‎tests/test_requests.py‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,3 +323,33 @@ def test_build_dubbing_parts_sends_export_srt_with_subtitles():
323323
)
324324
assert data["export_srt"] == "true"
325325

326+
327+
328+
# --- optional duration ------------------------------------------------------
329+
# The API resolves an omitted duration itself: text-to-music infers a length
330+
# from the prompt (or the caller's segments), text-to-sfx uses its own default.
331+
# The client's job is only to leave the field out rather than invent a number.
332+
333+
def test_build_t2m_data_omits_an_absent_duration():
334+
assert "duration" not in build_t2m_data("lofi beat", None, None)
335+
336+
337+
def test_build_t2m_async_data_omits_an_absent_duration():
338+
from sonilo._requests import build_t2m_async_data
339+
340+
data = build_t2m_async_data("lofi beat", None, None, "async", None, None, None)
341+
assert "duration" not in data
342+
343+
344+
def test_build_sfx_t2s_data_omits_an_absent_duration():
345+
from sonilo._requests import build_sfx_t2s_data
346+
347+
assert "duration" not in build_sfx_t2s_data("a door latch", None, None)
348+
349+
350+
def test_build_sfx_t2s_data_keeps_a_fractional_duration():
351+
"""The shortest effects are well under a second, so duration is a number,
352+
not an integer — 0.5 must not be rounded or stringified as an int."""
353+
from sonilo._requests import build_sfx_t2s_data
354+
355+
assert build_sfx_t2s_data("a door latch", 0.5, None)["duration"] == "0.5"

0 commit comments

Comments
 (0)