sonilo-cli 0.8.2__tar.gz → 0.9.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sonilo-cli
3
- Version: 0.8.2
3
+ Version: 0.9.0
4
4
  Summary: Command-line interface for the Sonilo API: generate music and sound effects from text or video
5
5
  Project-URL: Repository, https://github.com/sonilo-ai/sonilo-python
6
6
  Author: Sonilo AI
@@ -146,8 +146,10 @@ they differ only in what comes back: `video-to-sound` writes the mixed **audio**
146
146
  - `--segments` places individual effects on the timeline, in the SFX shape `{start, end, prompt}` —
147
147
  see [Segments](#segments).
148
148
  - `--preserve-speech` keeps speech from the source video in the mix.
149
- - **Ducking is on by default** (music dips under speech). Pass `--no-ducking` to opt out omitting
150
- the flag leaves the server default untouched.
149
+ - **Ducking is off by default.** Pass `--ducking` to bring the source video's own speech into the
150
+ mix with the generated bed dipped under it — without it the result is the generated music and
151
+ effects alone. Omitting both flags leaves the server default untouched; `--no-ducking` still
152
+ works and now just states that default explicitly.
151
153
  - `--stem` is repeatable (`music`, `music_processed`, `sfx`) and saves the individual layers next to
152
154
  the combined output, so you can re-balance the mix yourself. With `--output soundtrack.wav`, the
153
155
  music stem lands at `soundtrack.music.m4a`. `music_processed` exists only when `--preserve-speech`
@@ -130,8 +130,10 @@ they differ only in what comes back: `video-to-sound` writes the mixed **audio**
130
130
  - `--segments` places individual effects on the timeline, in the SFX shape `{start, end, prompt}` —
131
131
  see [Segments](#segments).
132
132
  - `--preserve-speech` keeps speech from the source video in the mix.
133
- - **Ducking is on by default** (music dips under speech). Pass `--no-ducking` to opt out omitting
134
- the flag leaves the server default untouched.
133
+ - **Ducking is off by default.** Pass `--ducking` to bring the source video's own speech into the
134
+ mix with the generated bed dipped under it — without it the result is the generated music and
135
+ effects alone. Omitting both flags leaves the server default untouched; `--no-ducking` still
136
+ works and now just states that default explicitly.
135
137
  - `--stem` is repeatable (`music`, `music_processed`, `sfx`) and saves the individual layers next to
136
138
  the combined output, so you can re-balance the mix yourself. With `--output soundtrack.wav`, the
137
139
  music stem lands at `soundtrack.music.m4a`. `music_processed` exists only when `--preserve-speech`
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sonilo-cli"
7
- version = "0.8.2"
7
+ version = "0.9.0"
8
8
  description = "Command-line interface for the Sonilo API: generate music and sound effects from text or video"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1,3 +1,3 @@
1
- __version__ = "0.8.0"
1
+ __version__ = "0.9.0"
2
2
 
3
3
  __all__ = ["__version__"]
@@ -167,6 +167,30 @@ def parse_segments(
167
167
  return value
168
168
 
169
169
 
170
+ def _ducking(args: argparse.Namespace) -> Optional[bool]:
171
+ """Resolve --ducking / --no-ducking, or None to let the server default win.
172
+
173
+ `ducking` used to be default-ON server-side, so the only direction worth
174
+ expressing was turning it off and --no-ducking was the only flag. It is now
175
+ default-OFF, which makes --ducking the useful one. --no-ducking is kept
176
+ because dropping it would turn every script that passes it into a hard
177
+ argparse error; it now sends the explicit False the server would have
178
+ applied anyway.
179
+
180
+ Passing both is a contradiction with no sensible winner, so it exits rather
181
+ than silently picking one.
182
+ """
183
+ on = getattr(args, "ducking", False)
184
+ off = getattr(args, "no_ducking", False)
185
+ if on and off:
186
+ raise SystemExit("pass at most one of --ducking or --no-ducking")
187
+ if on:
188
+ return True
189
+ if off:
190
+ return False
191
+ return None
192
+
193
+
170
194
  def _segments(args: argparse.Namespace) -> Optional[List[Dict[str, Any]]]:
171
195
  """parse_segments() for whichever subcommand is running."""
172
196
  return parse_segments(args.segments, args.segments_shape, args.command)
@@ -345,7 +369,7 @@ def _run_sound(client: Sonilo, args: argparse.Namespace, resource: Any, default_
345
369
  sfx_prompt=args.sfx_prompt,
346
370
  segments=_segments(args),
347
371
  preserve_speech=True if args.preserve_speech else None,
348
- ducking=False if args.no_ducking else None,
372
+ ducking=_ducking(args),
349
373
  variants_num=args.variants,
350
374
  **extra,
351
375
  )
@@ -403,12 +427,11 @@ def cmd_video_to_video_music(client: Sonilo, args: argparse.Namespace) -> None:
403
427
  client.video_to_video_music,
404
428
  prompt=args.prompt,
405
429
  # Unset flags forward None, not False, so the server default stands —
406
- # same reasoning as --no-ducking on the sound commands. The two
407
- # defaults run opposite ways: ducking is default-ON server-side and
408
- # keep_original_sound default-OFF, so each is only ever sent to change
409
- # the default, never to restate it.
430
+ # same reasoning as --ducking on the sound commands. Both ducking and
431
+ # keep_original_sound are default-OFF server-side, so each is only ever
432
+ # sent to change the default, never to restate it.
410
433
  keep_original_sound=True if args.keep_original_sound else None,
411
- ducking=False if args.no_ducking else None,
434
+ ducking=_ducking(args),
412
435
  preserve_speech=True if args.preserve_speech else None,
413
436
  isolate_vocals=True if args.isolate_vocals else None,
414
437
  variants_num=args.variants,
@@ -611,8 +634,14 @@ def build_parser() -> argparse.ArgumentParser:
611
634
  _add_segments(p_v2sd, SFX_SHAPE)
612
635
  p_v2sd.add_argument("--preserve-speech", dest="preserve_speech", action="store_true",
613
636
  help="Keep source speech in the mix.")
637
+ p_v2sd.add_argument("--ducking", dest="ducking", action="store_true",
638
+ help="Bring the source video's own speech into the mix and duck "
639
+ "the generated bed under it. Off by default, so by default "
640
+ "the result is the generated music and effects alone and "
641
+ "there is no music_processed stem.")
614
642
  p_v2sd.add_argument("--no-ducking", dest="no_ducking", action="store_true",
615
- help="Disable automatic ducking (on by default server-side).")
643
+ help="Explicit opt-out. Same as the default; kept so existing "
644
+ "scripts keep working.")
616
645
  p_v2sd.add_argument("--stem", dest="stems", action="append", choices=_SOUND_STEMS,
617
646
  default=None, help="Also save an individual stem. Repeatable.")
618
647
  p_v2sd.add_argument("--output", default=None, help="Where to save the combined audio.")
@@ -631,10 +660,13 @@ def build_parser() -> argparse.ArgumentParser:
631
660
  "generated music under it. Off by default, so by default "
632
661
  "the result's audio is the generated music alone. "
633
662
  "Supersedes --preserve-speech.")
663
+ p_v2vm.add_argument("--ducking", dest="ducking", action="store_true",
664
+ help="Duck the music under the voice instead of mixing it in at "
665
+ "a static level. No effect without --keep-original-sound "
666
+ "or --preserve-speech.")
634
667
  p_v2vm.add_argument("--no-ducking", dest="no_ducking", action="store_true",
635
- help="Combine the voice and the music as a static mix instead "
636
- "of a dynamic duck. No effect without "
637
- "--keep-original-sound or --preserve-speech.")
668
+ help="Explicit opt-out. Same as the default; kept so existing "
669
+ "scripts keep working.")
638
670
  p_v2vm.add_argument("--preserve-speech", dest="preserve_speech", action="store_true",
639
671
  help="Keep only the source's isolated speech in the mix.")
640
672
  # Same aliasing as video-to-music, and here the endpoint collapses the two
@@ -676,10 +708,13 @@ def build_parser() -> argparse.ArgumentParser:
676
708
  "--preserve-speech. This command only.")
677
709
  p_v2vsd.add_argument("--preserve-speech", dest="preserve_speech", action="store_true",
678
710
  help="Keep only the source's isolated speech in the mix.")
679
- p_v2vsd.add_argument("--no-ducking", dest="no_ducking", action="store_true",
680
- help="Combine the voice and the generated bed as a static mix "
681
- "instead of a dynamic duck. No effect without "
711
+ p_v2vsd.add_argument("--ducking", dest="ducking", action="store_true",
712
+ help="Duck the generated bed under the voice instead of mixing "
713
+ "it in at a static level. No effect without "
682
714
  "--keep-original-sound or --preserve-speech.")
715
+ p_v2vsd.add_argument("--no-ducking", dest="no_ducking", action="store_true",
716
+ help="Explicit opt-out. Same as the default; kept so existing "
717
+ "scripts keep working.")
683
718
  p_v2vsd.add_argument("--stem", dest="stems", action="append", choices=_SOUND_STEMS,
684
719
  default=None, help="Also save an individual stem. Repeatable.")
685
720
  p_v2vsd.add_argument("--output", default=None, help="Where to save the combined video.")
@@ -369,15 +369,25 @@ def test_video_to_sound_ducking_absent_omits_field(tmp_path):
369
369
  )
370
370
  run(["video-to-sound", "--video-url", "http://x/y.mp4",
371
371
  "--output", str(tmp_path / "s.wav")])
372
- # ducking is default-ON server-side: an unset --no-ducking must forward
373
- # `None`, not `False`, so the field must be entirely absent from the
374
- # form-encoded body (per build_v2s_parts).
372
+ # ducking is default-OFF server-side, and neither flag was passed, so the
373
+ # CLI must forward `None`, not `False` the field has to be absent from
374
+ # the form-encoded body (per build_v2s_parts) and let the server decide.
375
375
  body = route.calls.last.request.content.decode()
376
376
  assert "ducking=" not in body
377
377
 
378
378
 
379
379
  @respx.mock
380
- def test_video_to_sound_no_ducking_sets_false(tmp_path):
380
+ @pytest.mark.parametrize(
381
+ "flag, wire",
382
+ [
383
+ # --ducking is the direction that does something now that the server
384
+ # default is off; --no-ducking predates the flip, still parses so
385
+ # existing scripts do not break, and states the default explicitly.
386
+ ("--ducking", "ducking=true"),
387
+ ("--no-ducking", "ducking=false"),
388
+ ],
389
+ )
390
+ def test_video_to_sound_ducking_flags(tmp_path, flag, wire):
381
391
  route = respx.post(f"{BASE}/v1/video-to-sound").mock(
382
392
  return_value=httpx.Response(200, json={"task_id": "sd4", "status": "processing"})
383
393
  )
@@ -388,9 +398,15 @@ def test_video_to_sound_no_ducking_sets_false(tmp_path):
388
398
  return_value=httpx.Response(200, content=b"MIXED")
389
399
  )
390
400
  run(["video-to-sound", "--video-url", "http://x/y.mp4",
391
- "--output", str(tmp_path / "s.wav"), "--no-ducking"])
401
+ "--output", str(tmp_path / "s.wav"), flag])
392
402
  body = route.calls.last.request.content.decode()
393
- assert "ducking=false" in body
403
+ assert wire in body
404
+
405
+
406
+ def test_video_to_sound_rejects_both_ducking_flags(tmp_path):
407
+ with pytest.raises(SystemExit):
408
+ run(["video-to-sound", "--video-url", "http://x/y.mp4",
409
+ "--output", str(tmp_path / "s.wav"), "--ducking", "--no-ducking"])
394
410
 
395
411
 
396
412
  @respx.mock
File without changes
File without changes
File without changes