sonilo-cli 0.8.2__tar.gz → 0.10.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_cli-0.8.2 → sonilo_cli-0.10.0}/PKG-INFO +19 -4
- {sonilo_cli-0.8.2 → sonilo_cli-0.10.0}/README.md +17 -2
- {sonilo_cli-0.8.2 → sonilo_cli-0.10.0}/pyproject.toml +2 -2
- {sonilo_cli-0.8.2 → sonilo_cli-0.10.0}/src/sonilo_cli/__init__.py +1 -1
- {sonilo_cli-0.8.2 → sonilo_cli-0.10.0}/src/sonilo_cli/__main__.py +68 -14
- {sonilo_cli-0.8.2 → sonilo_cli-0.10.0}/tests/test_cli.py +83 -6
- {sonilo_cli-0.8.2 → sonilo_cli-0.10.0}/.gitignore +0 -0
- {sonilo_cli-0.8.2 → sonilo_cli-0.10.0}/LICENSE +0 -0
- {sonilo_cli-0.8.2 → sonilo_cli-0.10.0}/tests/__init__.py +0 -0
- {sonilo_cli-0.8.2 → sonilo_cli-0.10.0}/tests/test_context7.py +0 -0
- {sonilo_cli-0.8.2 → sonilo_cli-0.10.0}/tests/test_smoke.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sonilo-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.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
|
|
@@ -8,7 +8,7 @@ License-Expression: MIT
|
|
|
8
8
|
License-File: LICENSE
|
|
9
9
|
Keywords: ai,cli,music,sfx,sonilo,text-to-music,video-to-music
|
|
10
10
|
Requires-Python: >=3.9
|
|
11
|
-
Requires-Dist: sonilo<0.
|
|
11
|
+
Requires-Dist: sonilo<0.13,>=0.12.0
|
|
12
12
|
Provides-Extra: dev
|
|
13
13
|
Requires-Dist: pytest>=8; extra == 'dev'
|
|
14
14
|
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
@@ -106,6 +106,17 @@ request — and values above 1 are never covered by the free trial.
|
|
|
106
106
|
- On `video-to-sound` / `video-to-video-sound`, `--stem` is applied per variant too, e.g.
|
|
107
107
|
`take.0.music.m4a`.
|
|
108
108
|
|
|
109
|
+
### Prompt influence
|
|
110
|
+
|
|
111
|
+
`--prompt-influence` (0-1, API default 0.5) sets how strongly the generated music follows the
|
|
112
|
+
prompt, on `video-to-music` and `video-to-video-music` only. Lower values let the video lead;
|
|
113
|
+
higher values follow the prompt more literally. It is free of charge, and unlike `--format wav` it
|
|
114
|
+
does not force the async path — it works on the streaming default too. Left unset, the field is
|
|
115
|
+
not sent at all and the API's own 0.5 default applies; `--prompt-influence 0` is a real value
|
|
116
|
+
("let the video lead entirely") and is sent. Out-of-range values earn a `422` from the API.
|
|
117
|
+
|
|
118
|
+
sonilo video-to-music --video clip.mp4 --prompt "tense synths" --prompt-influence 0.8
|
|
119
|
+
|
|
109
120
|
### Scored video
|
|
110
121
|
|
|
111
122
|
`video-to-video-music` and `video-to-video-sfx` are the video-out counterparts of `video-to-music`
|
|
@@ -128,6 +139,8 @@ file (default `output.mp4`):
|
|
|
128
139
|
- For music *and* effects in one call, use `video-to-video-sound` below.
|
|
129
140
|
- `video-to-video-music` also takes `--variants` — see [Variants](#variants) above.
|
|
130
141
|
`video-to-video-sfx` does not.
|
|
142
|
+
- `video-to-video-music` also takes `--prompt-influence` — see
|
|
143
|
+
[Prompt influence](#prompt-influence) above. `video-to-video-sfx` does not.
|
|
131
144
|
|
|
132
145
|
### Combined soundtracks
|
|
133
146
|
|
|
@@ -146,8 +159,10 @@ they differ only in what comes back: `video-to-sound` writes the mixed **audio**
|
|
|
146
159
|
- `--segments` places individual effects on the timeline, in the SFX shape `{start, end, prompt}` —
|
|
147
160
|
see [Segments](#segments).
|
|
148
161
|
- `--preserve-speech` keeps speech from the source video in the mix.
|
|
149
|
-
- **Ducking is
|
|
150
|
-
the
|
|
162
|
+
- **Ducking is off by default.** Pass `--ducking` to bring the source video's own speech into the
|
|
163
|
+
mix with the generated bed dipped under it — without it the result is the generated music and
|
|
164
|
+
effects alone. Omitting both flags leaves the server default untouched; `--no-ducking` still
|
|
165
|
+
works and now just states that default explicitly.
|
|
151
166
|
- `--stem` is repeatable (`music`, `music_processed`, `sfx`) and saves the individual layers next to
|
|
152
167
|
the combined output, so you can re-balance the mix yourself. With `--output soundtrack.wav`, the
|
|
153
168
|
music stem lands at `soundtrack.music.m4a`. `music_processed` exists only when `--preserve-speech`
|
|
@@ -90,6 +90,17 @@ request — and values above 1 are never covered by the free trial.
|
|
|
90
90
|
- On `video-to-sound` / `video-to-video-sound`, `--stem` is applied per variant too, e.g.
|
|
91
91
|
`take.0.music.m4a`.
|
|
92
92
|
|
|
93
|
+
### Prompt influence
|
|
94
|
+
|
|
95
|
+
`--prompt-influence` (0-1, API default 0.5) sets how strongly the generated music follows the
|
|
96
|
+
prompt, on `video-to-music` and `video-to-video-music` only. Lower values let the video lead;
|
|
97
|
+
higher values follow the prompt more literally. It is free of charge, and unlike `--format wav` it
|
|
98
|
+
does not force the async path — it works on the streaming default too. Left unset, the field is
|
|
99
|
+
not sent at all and the API's own 0.5 default applies; `--prompt-influence 0` is a real value
|
|
100
|
+
("let the video lead entirely") and is sent. Out-of-range values earn a `422` from the API.
|
|
101
|
+
|
|
102
|
+
sonilo video-to-music --video clip.mp4 --prompt "tense synths" --prompt-influence 0.8
|
|
103
|
+
|
|
93
104
|
### Scored video
|
|
94
105
|
|
|
95
106
|
`video-to-video-music` and `video-to-video-sfx` are the video-out counterparts of `video-to-music`
|
|
@@ -112,6 +123,8 @@ file (default `output.mp4`):
|
|
|
112
123
|
- For music *and* effects in one call, use `video-to-video-sound` below.
|
|
113
124
|
- `video-to-video-music` also takes `--variants` — see [Variants](#variants) above.
|
|
114
125
|
`video-to-video-sfx` does not.
|
|
126
|
+
- `video-to-video-music` also takes `--prompt-influence` — see
|
|
127
|
+
[Prompt influence](#prompt-influence) above. `video-to-video-sfx` does not.
|
|
115
128
|
|
|
116
129
|
### Combined soundtracks
|
|
117
130
|
|
|
@@ -130,8 +143,10 @@ they differ only in what comes back: `video-to-sound` writes the mixed **audio**
|
|
|
130
143
|
- `--segments` places individual effects on the timeline, in the SFX shape `{start, end, prompt}` —
|
|
131
144
|
see [Segments](#segments).
|
|
132
145
|
- `--preserve-speech` keeps speech from the source video in the mix.
|
|
133
|
-
- **Ducking is
|
|
134
|
-
the
|
|
146
|
+
- **Ducking is off by default.** Pass `--ducking` to bring the source video's own speech into the
|
|
147
|
+
mix with the generated bed dipped under it — without it the result is the generated music and
|
|
148
|
+
effects alone. Omitting both flags leaves the server default untouched; `--no-ducking` still
|
|
149
|
+
works and now just states that default explicitly.
|
|
135
150
|
- `--stem` is repeatable (`music`, `music_processed`, `sfx`) and saves the individual layers next to
|
|
136
151
|
the combined output, so you can re-balance the mix yourself. With `--output soundtrack.wav`, the
|
|
137
152
|
music stem lands at `soundtrack.music.m4a`. `music_processed` exists only when `--preserve-speech`
|
|
@@ -4,13 +4,13 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sonilo-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.10.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"
|
|
11
11
|
requires-python = ">=3.9"
|
|
12
12
|
authors = [{ name = "Sonilo AI" }]
|
|
13
|
-
dependencies = ["sonilo>=0.
|
|
13
|
+
dependencies = ["sonilo>=0.12.0,<0.13"]
|
|
14
14
|
keywords = ["sonilo", "cli", "music", "sfx", "text-to-music", "video-to-music", "ai"]
|
|
15
15
|
|
|
16
16
|
[project.urls]
|
|
@@ -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)
|
|
@@ -285,12 +309,15 @@ def cmd_video_to_music(client: Sonilo, args: argparse.Namespace) -> None:
|
|
|
285
309
|
preserve_speech=args.preserve_speech or None,
|
|
286
310
|
output_format=fmt if fmt != "m4a" else None,
|
|
287
311
|
variants_num=args.variants,
|
|
312
|
+
prompt_influence=args.prompt_influence,
|
|
288
313
|
)
|
|
289
314
|
_save_music_variants(result, out)
|
|
290
315
|
else:
|
|
316
|
+
# prompt_influence rides the streaming path too — it is a generation
|
|
317
|
+
# parameter, not a finalize-time one, so it never forces async.
|
|
291
318
|
track = client.video_to_music.generate(
|
|
292
319
|
video=args.video, video_url=args.video_url, prompt=args.prompt,
|
|
293
|
-
segments=segments,
|
|
320
|
+
segments=segments, prompt_influence=args.prompt_influence,
|
|
294
321
|
)
|
|
295
322
|
path = track.save(out)
|
|
296
323
|
_wrote(path, len(track.audio))
|
|
@@ -345,7 +372,7 @@ def _run_sound(client: Sonilo, args: argparse.Namespace, resource: Any, default_
|
|
|
345
372
|
sfx_prompt=args.sfx_prompt,
|
|
346
373
|
segments=_segments(args),
|
|
347
374
|
preserve_speech=True if args.preserve_speech else None,
|
|
348
|
-
ducking=
|
|
375
|
+
ducking=_ducking(args),
|
|
349
376
|
variants_num=args.variants,
|
|
350
377
|
**extra,
|
|
351
378
|
)
|
|
@@ -403,15 +430,15 @@ def cmd_video_to_video_music(client: Sonilo, args: argparse.Namespace) -> None:
|
|
|
403
430
|
client.video_to_video_music,
|
|
404
431
|
prompt=args.prompt,
|
|
405
432
|
# Unset flags forward None, not False, so the server default stands —
|
|
406
|
-
# same reasoning as --
|
|
407
|
-
#
|
|
408
|
-
#
|
|
409
|
-
# the default, never to restate it.
|
|
433
|
+
# same reasoning as --ducking on the sound commands. Both ducking and
|
|
434
|
+
# keep_original_sound are default-OFF server-side, so each is only ever
|
|
435
|
+
# sent to change the default, never to restate it.
|
|
410
436
|
keep_original_sound=True if args.keep_original_sound else None,
|
|
411
|
-
ducking=
|
|
437
|
+
ducking=_ducking(args),
|
|
412
438
|
preserve_speech=True if args.preserve_speech else None,
|
|
413
439
|
isolate_vocals=True if args.isolate_vocals else None,
|
|
414
440
|
variants_num=args.variants,
|
|
441
|
+
prompt_influence=args.prompt_influence,
|
|
415
442
|
)
|
|
416
443
|
|
|
417
444
|
|
|
@@ -520,6 +547,19 @@ def _add_segments(parser: argparse.ArgumentParser, shape: _SegmentShape) -> None
|
|
|
520
547
|
parser.set_defaults(segments_shape=shape)
|
|
521
548
|
|
|
522
549
|
|
|
550
|
+
def _add_prompt_influence(parser: argparse.ArgumentParser) -> None:
|
|
551
|
+
# Only the two music-from-video commands take this — the API accepts it
|
|
552
|
+
# nowhere else. type=float so 0 arrives as 0.0, a real value ("let the
|
|
553
|
+
# video lead entirely"), distinct from the unset None that keeps the
|
|
554
|
+
# field off the wire and leaves the API its own 0.5 default.
|
|
555
|
+
parser.add_argument(
|
|
556
|
+
"--prompt-influence", dest="prompt_influence", type=float, default=None,
|
|
557
|
+
help="How strongly the generated music follows the prompt, 0-1 "
|
|
558
|
+
"(API default 0.5). Lower values let the video lead; higher "
|
|
559
|
+
"values follow the prompt more literally. Free of charge.",
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
|
|
523
563
|
def _add_variants(parser: argparse.ArgumentParser) -> None:
|
|
524
564
|
parser.add_argument(
|
|
525
565
|
"--variants", type=int, default=None,
|
|
@@ -578,6 +618,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
578
618
|
p_v2m.add_argument("--async", dest="use_async", action="store_true",
|
|
579
619
|
help="Submit and poll instead of streaming.")
|
|
580
620
|
_add_variants(p_v2m)
|
|
621
|
+
_add_prompt_influence(p_v2m)
|
|
581
622
|
p_v2m.set_defaults(func=cmd_video_to_music)
|
|
582
623
|
|
|
583
624
|
p_t2s = sub.add_parser("text-to-sfx", help="Generate a sound effect from a text prompt")
|
|
@@ -611,8 +652,14 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
611
652
|
_add_segments(p_v2sd, SFX_SHAPE)
|
|
612
653
|
p_v2sd.add_argument("--preserve-speech", dest="preserve_speech", action="store_true",
|
|
613
654
|
help="Keep source speech in the mix.")
|
|
655
|
+
p_v2sd.add_argument("--ducking", dest="ducking", action="store_true",
|
|
656
|
+
help="Bring the source video's own speech into the mix and duck "
|
|
657
|
+
"the generated bed under it. Off by default, so by default "
|
|
658
|
+
"the result is the generated music and effects alone and "
|
|
659
|
+
"there is no music_processed stem.")
|
|
614
660
|
p_v2sd.add_argument("--no-ducking", dest="no_ducking", action="store_true",
|
|
615
|
-
help="
|
|
661
|
+
help="Explicit opt-out. Same as the default; kept so existing "
|
|
662
|
+
"scripts keep working.")
|
|
616
663
|
p_v2sd.add_argument("--stem", dest="stems", action="append", choices=_SOUND_STEMS,
|
|
617
664
|
default=None, help="Also save an individual stem. Repeatable.")
|
|
618
665
|
p_v2sd.add_argument("--output", default=None, help="Where to save the combined audio.")
|
|
@@ -631,10 +678,13 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
631
678
|
"generated music under it. Off by default, so by default "
|
|
632
679
|
"the result's audio is the generated music alone. "
|
|
633
680
|
"Supersedes --preserve-speech.")
|
|
681
|
+
p_v2vm.add_argument("--ducking", dest="ducking", action="store_true",
|
|
682
|
+
help="Duck the music under the voice instead of mixing it in at "
|
|
683
|
+
"a static level. No effect without --keep-original-sound "
|
|
684
|
+
"or --preserve-speech.")
|
|
634
685
|
p_v2vm.add_argument("--no-ducking", dest="no_ducking", action="store_true",
|
|
635
|
-
help="
|
|
636
|
-
"
|
|
637
|
-
"--keep-original-sound or --preserve-speech.")
|
|
686
|
+
help="Explicit opt-out. Same as the default; kept so existing "
|
|
687
|
+
"scripts keep working.")
|
|
638
688
|
p_v2vm.add_argument("--preserve-speech", dest="preserve_speech", action="store_true",
|
|
639
689
|
help="Keep only the source's isolated speech in the mix.")
|
|
640
690
|
# Same aliasing as video-to-music, and here the endpoint collapses the two
|
|
@@ -645,6 +695,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
645
695
|
help="Legacy alias for --preserve-speech; no separate stem.")
|
|
646
696
|
p_v2vm.add_argument("--output", default=None, help="Where to save the scored video.")
|
|
647
697
|
_add_variants(p_v2vm)
|
|
698
|
+
_add_prompt_influence(p_v2vm)
|
|
648
699
|
p_v2vm.set_defaults(func=cmd_video_to_video_music)
|
|
649
700
|
|
|
650
701
|
p_v2vfx = sub.add_parser(
|
|
@@ -676,10 +727,13 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
676
727
|
"--preserve-speech. This command only.")
|
|
677
728
|
p_v2vsd.add_argument("--preserve-speech", dest="preserve_speech", action="store_true",
|
|
678
729
|
help="Keep only the source's isolated speech in the mix.")
|
|
679
|
-
p_v2vsd.add_argument("--
|
|
680
|
-
help="
|
|
681
|
-
"
|
|
730
|
+
p_v2vsd.add_argument("--ducking", dest="ducking", action="store_true",
|
|
731
|
+
help="Duck the generated bed under the voice instead of mixing "
|
|
732
|
+
"it in at a static level. No effect without "
|
|
682
733
|
"--keep-original-sound or --preserve-speech.")
|
|
734
|
+
p_v2vsd.add_argument("--no-ducking", dest="no_ducking", action="store_true",
|
|
735
|
+
help="Explicit opt-out. Same as the default; kept so existing "
|
|
736
|
+
"scripts keep working.")
|
|
683
737
|
p_v2vsd.add_argument("--stem", dest="stems", action="append", choices=_SOUND_STEMS,
|
|
684
738
|
default=None, help="Also save an individual stem. Repeatable.")
|
|
685
739
|
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-
|
|
373
|
-
# `None`, not `False
|
|
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
|
-
|
|
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"),
|
|
401
|
+
"--output", str(tmp_path / "s.wav"), flag])
|
|
392
402
|
body = route.calls.last.request.content.decode()
|
|
393
|
-
assert
|
|
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
|
|
@@ -1080,3 +1096,64 @@ def test_video_to_video_commands_are_listed_in_top_level_help(command, capsys):
|
|
|
1080
1096
|
with pytest.raises(SystemExit):
|
|
1081
1097
|
main(["--help"])
|
|
1082
1098
|
assert command in capsys.readouterr().out
|
|
1099
|
+
|
|
1100
|
+
|
|
1101
|
+
# --- --prompt-influence -------------------------------------------------------
|
|
1102
|
+
#
|
|
1103
|
+
# Only video-to-music and video-to-video-music offer the flag — the API
|
|
1104
|
+
# accepts prompt_influence nowhere else. Unset forwards None (field absent,
|
|
1105
|
+
# API default 0.5 stands); 0 is a real value and must be sent as 0.0.
|
|
1106
|
+
|
|
1107
|
+
|
|
1108
|
+
@respx.mock
|
|
1109
|
+
def test_video_to_music_prompt_influence_rides_the_streaming_path(tmp_path):
|
|
1110
|
+
"""prompt_influence is a generation parameter, valid on stream and async
|
|
1111
|
+
alike, so unlike --format wav it must NOT force the async path."""
|
|
1112
|
+
route = respx.post(f"{BASE}/v1/video-to-music").mock(
|
|
1113
|
+
return_value=httpx.Response(200, text=_music_stream_body())
|
|
1114
|
+
)
|
|
1115
|
+
run(["video-to-music", "--video-url", "http://x/y.mp4",
|
|
1116
|
+
"--prompt-influence", "0.8", "--output", str(tmp_path / "song.m4a")])
|
|
1117
|
+
body = route.calls.last.request.content.decode()
|
|
1118
|
+
assert "prompt_influence=0.8" in body
|
|
1119
|
+
|
|
1120
|
+
|
|
1121
|
+
@respx.mock
|
|
1122
|
+
def test_video_to_music_prompt_influence_zero_is_sent(tmp_path):
|
|
1123
|
+
"""0 means "let the video lead entirely" — a real request, distinct from
|
|
1124
|
+
unset, so it goes on the wire (as 0.0: argparse's float parse)."""
|
|
1125
|
+
route = respx.post(f"{BASE}/v1/video-to-music").mock(
|
|
1126
|
+
return_value=httpx.Response(200, text=_music_stream_body())
|
|
1127
|
+
)
|
|
1128
|
+
run(["video-to-music", "--video-url", "http://x/y.mp4",
|
|
1129
|
+
"--prompt-influence", "0", "--output", str(tmp_path / "song.m4a")])
|
|
1130
|
+
body = route.calls.last.request.content.decode()
|
|
1131
|
+
assert "prompt_influence=0.0" in body
|
|
1132
|
+
|
|
1133
|
+
|
|
1134
|
+
@respx.mock
|
|
1135
|
+
def test_video_to_music_omits_prompt_influence_when_unset(tmp_path):
|
|
1136
|
+
route = respx.post(f"{BASE}/v1/video-to-music").mock(
|
|
1137
|
+
return_value=httpx.Response(200, text=_music_stream_body())
|
|
1138
|
+
)
|
|
1139
|
+
run(["video-to-music", "--video-url", "http://x/y.mp4",
|
|
1140
|
+
"--output", str(tmp_path / "song.m4a")])
|
|
1141
|
+
# Absent, not "None" and not an explicit 0.5 pinning the API's default.
|
|
1142
|
+
assert b"prompt_influence" not in route.calls.last.request.content
|
|
1143
|
+
|
|
1144
|
+
|
|
1145
|
+
@respx.mock
|
|
1146
|
+
def test_video_to_video_music_prompt_influence_reaches_the_request_body(tmp_path):
|
|
1147
|
+
route = _mock_video_task("video-to-video-music", "vmpi1", "video_to_video_music")
|
|
1148
|
+
run(["video-to-video-music", "--video-url", "http://x/y.mp4",
|
|
1149
|
+
"--prompt-influence", "0.3", "--output", str(tmp_path / "s.mp4")])
|
|
1150
|
+
body = route.calls.last.request.content.decode()
|
|
1151
|
+
assert "prompt_influence=0.3" in body
|
|
1152
|
+
|
|
1153
|
+
|
|
1154
|
+
@respx.mock
|
|
1155
|
+
def test_video_to_video_music_omits_prompt_influence_when_unset(tmp_path):
|
|
1156
|
+
route = _mock_video_task("video-to-video-music", "vmpi2", "video_to_video_music")
|
|
1157
|
+
run(["video-to-video-music", "--video-url", "http://x/y.mp4",
|
|
1158
|
+
"--output", str(tmp_path / "s.mp4")])
|
|
1159
|
+
assert b"prompt_influence" not in route.calls.last.request.content
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|