sonilo-cli 0.4.0__tar.gz → 0.5.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.4.0 → sonilo_cli-0.5.0}/PKG-INFO +28 -3
- {sonilo_cli-0.4.0 → sonilo_cli-0.5.0}/README.md +27 -2
- {sonilo_cli-0.4.0 → sonilo_cli-0.5.0}/pyproject.toml +1 -1
- {sonilo_cli-0.4.0 → sonilo_cli-0.5.0}/src/sonilo_cli/__init__.py +1 -1
- {sonilo_cli-0.4.0 → sonilo_cli-0.5.0}/src/sonilo_cli/__main__.py +67 -2
- {sonilo_cli-0.4.0 → sonilo_cli-0.5.0}/tests/test_cli.py +186 -4
- {sonilo_cli-0.4.0 → sonilo_cli-0.5.0}/.gitignore +0 -0
- {sonilo_cli-0.4.0 → sonilo_cli-0.5.0}/LICENSE +0 -0
- {sonilo_cli-0.4.0 → sonilo_cli-0.5.0}/tests/__init__.py +0 -0
- {sonilo_cli-0.4.0 → sonilo_cli-0.5.0}/tests/test_context7.py +0 -0
- {sonilo_cli-0.4.0 → sonilo_cli-0.5.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.5.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
|
|
@@ -41,6 +41,8 @@ or pass `--api-key sk-...` on any command.
|
|
|
41
41
|
sonilo video-to-sfx --video clip.mp4 --segments @segments.json
|
|
42
42
|
sonilo video-to-sound --video clip.mp4 \
|
|
43
43
|
--music-prompt "uplifting orchestral score" --sfx-prompt "match the on-screen action"
|
|
44
|
+
sonilo video-to-video-music --video clip.mp4 --prompt "tense synths" --output scored.mp4
|
|
45
|
+
sonilo video-to-video-sfx --video clip.mp4 --segments @segments.json --output scored.mp4
|
|
44
46
|
sonilo video-to-video-sound --video clip.mp4 --music-prompt "tense synths"
|
|
45
47
|
sonilo dubbing --video-url https://example.com/clip.mp4 --languages es,fr --output dubbed.mp4
|
|
46
48
|
# writes dubbed.es.mp4 and dubbed.fr.mp4
|
|
@@ -50,7 +52,8 @@ or pass `--api-key sk-...` on any command.
|
|
|
50
52
|
### Notes
|
|
51
53
|
|
|
52
54
|
- `text-to-music` / `video-to-music` stream a short `.m4a` by default. `--format wav`,
|
|
53
|
-
`--
|
|
55
|
+
`--preserve-speech`, and its legacy alias `--isolate-vocals` each switch to the async
|
|
56
|
+
submit-and-poll path.
|
|
54
57
|
- `text-to-sfx` / `video-to-sfx` are always async; `--format` accepts `wav|mp3|aac|flac`.
|
|
55
58
|
- Output defaults to `./output.<ext>`; override with `--output`.
|
|
56
59
|
|
|
@@ -72,7 +75,7 @@ The two segment shapes are **not** interchangeable:
|
|
|
72
75
|
| Shape | Commands | Fields |
|
|
73
76
|
| --- | --- | --- |
|
|
74
77
|
| Music | `text-to-music`, `video-to-music` | `{start, prompt, label?}` |
|
|
75
|
-
| SFX | `video-to-sfx`, `video-to-sound`, `video-to-video-sound` | `{start, end, prompt}` |
|
|
78
|
+
| SFX | `video-to-sfx`, `video-to-video-sfx`, `video-to-sound`, `video-to-video-sound` | `{start, end, prompt}` |
|
|
76
79
|
|
|
77
80
|
- `start` / `end` are seconds from the start of the track or clip.
|
|
78
81
|
- Passing one shape to a command that takes the other is rejected before any request is made, with
|
|
@@ -83,6 +86,28 @@ The two segment shapes are **not** interchangeable:
|
|
|
83
86
|
- Keys the CLI does not recognise are forwarded as-is, so a newly added API field works without
|
|
84
87
|
upgrading the CLI.
|
|
85
88
|
- `text-to-sfx` takes no segments (its output is a single effect, not a timeline).
|
|
89
|
+
- `video-to-video-music` takes no segments either — the API scores the whole clip in one pass.
|
|
90
|
+
|
|
91
|
+
### Scored video
|
|
92
|
+
|
|
93
|
+
`video-to-video-music` and `video-to-video-sfx` are the video-out counterparts of `video-to-music`
|
|
94
|
+
and `video-to-sfx`: same generation, but what comes back is the source picture with the new audio
|
|
95
|
+
already muxed in, so there is nothing to line up afterwards. Both are async-only and write a single
|
|
96
|
+
file (default `output.mp4`):
|
|
97
|
+
|
|
98
|
+
sonilo video-to-video-music --video clip.mp4 --prompt "tense synths" --output scored.mp4
|
|
99
|
+
sonilo video-to-video-sfx --video clip.mp4 \
|
|
100
|
+
--segments '[{"start":0,"end":5,"prompt":"footsteps on gravel"}]' --output scored.mp4
|
|
101
|
+
|
|
102
|
+
- `--prompt` is optional on both; without it the model scores from the picture alone.
|
|
103
|
+
- `video-to-video-music` also takes `--preserve-speech`, which keeps source speech in the mix;
|
|
104
|
+
omitting it leaves the server default untouched. `--isolate-vocals` is a legacy alias for the
|
|
105
|
+
same flag — the API ORs the two together, and this endpoint returns one muxed video with no
|
|
106
|
+
separate vocals stem.
|
|
107
|
+
- `video-to-video-sfx` takes `--segments` in the SFX shape `{start, end, prompt}` — see
|
|
108
|
+
[Segments](#segments).
|
|
109
|
+
- Neither command exposes `--format`: the output is a video, not an audio file.
|
|
110
|
+
- For music *and* effects in one call, use `video-to-video-sound` below.
|
|
86
111
|
|
|
87
112
|
### Combined soundtracks
|
|
88
113
|
|
|
@@ -25,6 +25,8 @@ or pass `--api-key sk-...` on any command.
|
|
|
25
25
|
sonilo video-to-sfx --video clip.mp4 --segments @segments.json
|
|
26
26
|
sonilo video-to-sound --video clip.mp4 \
|
|
27
27
|
--music-prompt "uplifting orchestral score" --sfx-prompt "match the on-screen action"
|
|
28
|
+
sonilo video-to-video-music --video clip.mp4 --prompt "tense synths" --output scored.mp4
|
|
29
|
+
sonilo video-to-video-sfx --video clip.mp4 --segments @segments.json --output scored.mp4
|
|
28
30
|
sonilo video-to-video-sound --video clip.mp4 --music-prompt "tense synths"
|
|
29
31
|
sonilo dubbing --video-url https://example.com/clip.mp4 --languages es,fr --output dubbed.mp4
|
|
30
32
|
# writes dubbed.es.mp4 and dubbed.fr.mp4
|
|
@@ -34,7 +36,8 @@ or pass `--api-key sk-...` on any command.
|
|
|
34
36
|
### Notes
|
|
35
37
|
|
|
36
38
|
- `text-to-music` / `video-to-music` stream a short `.m4a` by default. `--format wav`,
|
|
37
|
-
`--
|
|
39
|
+
`--preserve-speech`, and its legacy alias `--isolate-vocals` each switch to the async
|
|
40
|
+
submit-and-poll path.
|
|
38
41
|
- `text-to-sfx` / `video-to-sfx` are always async; `--format` accepts `wav|mp3|aac|flac`.
|
|
39
42
|
- Output defaults to `./output.<ext>`; override with `--output`.
|
|
40
43
|
|
|
@@ -56,7 +59,7 @@ The two segment shapes are **not** interchangeable:
|
|
|
56
59
|
| Shape | Commands | Fields |
|
|
57
60
|
| --- | --- | --- |
|
|
58
61
|
| Music | `text-to-music`, `video-to-music` | `{start, prompt, label?}` |
|
|
59
|
-
| SFX | `video-to-sfx`, `video-to-sound`, `video-to-video-sound` | `{start, end, prompt}` |
|
|
62
|
+
| SFX | `video-to-sfx`, `video-to-video-sfx`, `video-to-sound`, `video-to-video-sound` | `{start, end, prompt}` |
|
|
60
63
|
|
|
61
64
|
- `start` / `end` are seconds from the start of the track or clip.
|
|
62
65
|
- Passing one shape to a command that takes the other is rejected before any request is made, with
|
|
@@ -67,6 +70,28 @@ The two segment shapes are **not** interchangeable:
|
|
|
67
70
|
- Keys the CLI does not recognise are forwarded as-is, so a newly added API field works without
|
|
68
71
|
upgrading the CLI.
|
|
69
72
|
- `text-to-sfx` takes no segments (its output is a single effect, not a timeline).
|
|
73
|
+
- `video-to-video-music` takes no segments either — the API scores the whole clip in one pass.
|
|
74
|
+
|
|
75
|
+
### Scored video
|
|
76
|
+
|
|
77
|
+
`video-to-video-music` and `video-to-video-sfx` are the video-out counterparts of `video-to-music`
|
|
78
|
+
and `video-to-sfx`: same generation, but what comes back is the source picture with the new audio
|
|
79
|
+
already muxed in, so there is nothing to line up afterwards. Both are async-only and write a single
|
|
80
|
+
file (default `output.mp4`):
|
|
81
|
+
|
|
82
|
+
sonilo video-to-video-music --video clip.mp4 --prompt "tense synths" --output scored.mp4
|
|
83
|
+
sonilo video-to-video-sfx --video clip.mp4 \
|
|
84
|
+
--segments '[{"start":0,"end":5,"prompt":"footsteps on gravel"}]' --output scored.mp4
|
|
85
|
+
|
|
86
|
+
- `--prompt` is optional on both; without it the model scores from the picture alone.
|
|
87
|
+
- `video-to-video-music` also takes `--preserve-speech`, which keeps source speech in the mix;
|
|
88
|
+
omitting it leaves the server default untouched. `--isolate-vocals` is a legacy alias for the
|
|
89
|
+
same flag — the API ORs the two together, and this endpoint returns one muxed video with no
|
|
90
|
+
separate vocals stem.
|
|
91
|
+
- `video-to-video-sfx` takes `--segments` in the SFX shape `{start, end, prompt}` — see
|
|
92
|
+
[Segments](#segments).
|
|
93
|
+
- Neither command exposes `--format`: the output is a video, not an audio file.
|
|
94
|
+
- For music *and* effects in one call, use `video-to-video-sound` below.
|
|
70
95
|
|
|
71
96
|
### Combined soundtracks
|
|
72
97
|
|
|
@@ -329,6 +329,40 @@ def cmd_video_to_video_sound(client: Sonilo, args: argparse.Namespace) -> None:
|
|
|
329
329
|
_run_sound(client, args, client.video_to_video_sound, "mp4")
|
|
330
330
|
|
|
331
331
|
|
|
332
|
+
def _run_video(args: argparse.Namespace, resource: Any, **params: Any) -> None:
|
|
333
|
+
"""Run one of the video-returning endpoints and save the single result.
|
|
334
|
+
|
|
335
|
+
These return the source picture with the generated audio muxed in — one
|
|
336
|
+
file, no stems — so the default destination is an `.mp4`, matching
|
|
337
|
+
video-to-video-sound.
|
|
338
|
+
"""
|
|
339
|
+
out = args.output if args.output is not None else "output.mp4"
|
|
340
|
+
result = resource.generate(video=args.video, video_url=args.video_url, **params)
|
|
341
|
+
path = result.save(out)
|
|
342
|
+
_wrote(path, path.stat().st_size)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def cmd_video_to_video_music(client: Sonilo, args: argparse.Namespace) -> None:
|
|
346
|
+
_run_video(
|
|
347
|
+
args,
|
|
348
|
+
client.video_to_video_music,
|
|
349
|
+
prompt=args.prompt,
|
|
350
|
+
# Unset flags forward None, not False, so the server default stands —
|
|
351
|
+
# same reasoning as --no-ducking on the sound commands.
|
|
352
|
+
preserve_speech=True if args.preserve_speech else None,
|
|
353
|
+
isolate_vocals=True if args.isolate_vocals else None,
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def cmd_video_to_video_sfx(client: Sonilo, args: argparse.Namespace) -> None:
|
|
358
|
+
_run_video(
|
|
359
|
+
args,
|
|
360
|
+
client.video_to_video_sfx,
|
|
361
|
+
prompt=args.prompt,
|
|
362
|
+
segments=_segments(args),
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
|
|
332
366
|
# Matched to the dubbing backend's own ceiling: it polls its pipeline for up
|
|
333
367
|
# to 7200s (2 hours), so anything shorter abandons a job the user has already
|
|
334
368
|
# been charged for. The SDK's generic DEFAULT_WAIT_TIMEOUT of 600s is far too
|
|
@@ -460,10 +494,14 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
460
494
|
p_v2m.add_argument("--output", default=None, help="Where to save the audio.")
|
|
461
495
|
p_v2m.add_argument("--format", choices=["m4a", "wav"], default="m4a",
|
|
462
496
|
help="Output container. wav forces async.")
|
|
463
|
-
p_v2m.add_argument("--isolate-vocals", dest="isolate_vocals", action="store_true",
|
|
464
|
-
help="Split out a vocals-only stem. Forces async.")
|
|
465
497
|
p_v2m.add_argument("--preserve-speech", dest="preserve_speech", action="store_true",
|
|
466
498
|
help="Keep source speech in the mix. Forces async.")
|
|
499
|
+
# The API ORs isolate_vocals into preserve_speech (video_to_music.py:
|
|
500
|
+
# `isolate_vocals = bool(preserve_speech) or bool(isolate_vocals)`), so
|
|
501
|
+
# the two flags are one feature under two names, not two behaviours.
|
|
502
|
+
# isolate_vocals is the legacy name kept for existing callers.
|
|
503
|
+
p_v2m.add_argument("--isolate-vocals", dest="isolate_vocals", action="store_true",
|
|
504
|
+
help="Legacy alias for --preserve-speech. Forces async.")
|
|
467
505
|
p_v2m.add_argument("--async", dest="use_async", action="store_true",
|
|
468
506
|
help="Submit and poll instead of streaming.")
|
|
469
507
|
p_v2m.set_defaults(func=cmd_video_to_music)
|
|
@@ -506,6 +544,33 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
506
544
|
p_v2sd.add_argument("--output", default=None, help="Where to save the combined audio.")
|
|
507
545
|
p_v2sd.set_defaults(func=cmd_video_to_sound)
|
|
508
546
|
|
|
547
|
+
p_v2vm = sub.add_parser(
|
|
548
|
+
"video-to-video-music", help="Generate music muxed into the source video"
|
|
549
|
+
)
|
|
550
|
+
_add_global(p_v2vm)
|
|
551
|
+
_add_video_source(p_v2vm)
|
|
552
|
+
p_v2vm.add_argument("--prompt", default=None, help="Optional creative direction.")
|
|
553
|
+
p_v2vm.add_argument("--preserve-speech", dest="preserve_speech", action="store_true",
|
|
554
|
+
help="Keep source speech in the mix.")
|
|
555
|
+
# Same aliasing as video-to-music, and here the endpoint collapses the two
|
|
556
|
+
# into a single boolean before it reaches the model (video_to_video.py:
|
|
557
|
+
# `keep_speech = bool(preserve_speech) or bool(isolate_vocals)`), with no
|
|
558
|
+
# vocals stem in the result — the output is one muxed video.
|
|
559
|
+
p_v2vm.add_argument("--isolate-vocals", dest="isolate_vocals", action="store_true",
|
|
560
|
+
help="Legacy alias for --preserve-speech; no separate stem.")
|
|
561
|
+
p_v2vm.add_argument("--output", default=None, help="Where to save the scored video.")
|
|
562
|
+
p_v2vm.set_defaults(func=cmd_video_to_video_music)
|
|
563
|
+
|
|
564
|
+
p_v2vfx = sub.add_parser(
|
|
565
|
+
"video-to-video-sfx", help="Generate sound effects muxed into the source video"
|
|
566
|
+
)
|
|
567
|
+
_add_global(p_v2vfx)
|
|
568
|
+
_add_video_source(p_v2vfx)
|
|
569
|
+
p_v2vfx.add_argument("--prompt", default=None, help="Optional creative direction.")
|
|
570
|
+
_add_segments(p_v2vfx, SFX_SHAPE)
|
|
571
|
+
p_v2vfx.add_argument("--output", default=None, help="Where to save the scored video.")
|
|
572
|
+
p_v2vfx.set_defaults(func=cmd_video_to_video_sfx)
|
|
573
|
+
|
|
509
574
|
p_v2vsd = sub.add_parser(
|
|
510
575
|
"video-to-video-sound", help="Generate matched music+sfx muxed into the source video"
|
|
511
576
|
)
|
|
@@ -764,7 +764,7 @@ def test_segments_semantic_rules_are_left_to_the_server():
|
|
|
764
764
|
|
|
765
765
|
@pytest.mark.parametrize(
|
|
766
766
|
"command",
|
|
767
|
-
["text-to-music", "video-to-music", "video-to-sfx",
|
|
767
|
+
["text-to-music", "video-to-music", "video-to-sfx", "video-to-video-sfx",
|
|
768
768
|
"video-to-sound", "video-to-video-sound"],
|
|
769
769
|
)
|
|
770
770
|
def test_segments_help_shows_all_three_value_forms(command, capsys):
|
|
@@ -778,10 +778,192 @@ def test_segments_help_shows_all_three_value_forms(command, capsys):
|
|
|
778
778
|
|
|
779
779
|
@pytest.mark.parametrize("command", ["text-to-sfx"])
|
|
780
780
|
def test_commands_without_segments_reject_the_flag(command, capsys):
|
|
781
|
-
"""text-to-sfx takes no segments in the SDK, so the CLI must not offer it
|
|
782
|
-
|
|
783
|
-
|
|
781
|
+
"""text-to-sfx takes no segments in the SDK, so the CLI must not offer it.
|
|
782
|
+
The other segment-less endpoint, video-to-video-music, is covered by
|
|
783
|
+
test_video_to_video_music_rejects_segments (it takes no --duration)."""
|
|
784
784
|
with pytest.raises(SystemExit) as exc:
|
|
785
785
|
run([command, "--prompt", "x", "--duration", "3", "--segments", "[]"])
|
|
786
786
|
assert exc.value.code == 1
|
|
787
787
|
assert "unrecognized arguments" in capsys.readouterr().err
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
# --- video-to-video-music / video-to-video-sfx ---------------------------
|
|
791
|
+
#
|
|
792
|
+
# Both return the source picture with the generated audio muxed in, so the
|
|
793
|
+
# task body carries a single `video` object rather than `audio`/`output_url`
|
|
794
|
+
# (shape confirmed against tests/test_video_to_video.py in the SDK repo).
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
def _video_body(task_id, task_type):
|
|
798
|
+
return {
|
|
799
|
+
"task_id": task_id,
|
|
800
|
+
"type": task_type,
|
|
801
|
+
"status": "succeeded",
|
|
802
|
+
"video": {"url": f"https://r2.example.com/{task_id}.mp4",
|
|
803
|
+
"content_type": "video/mp4", "file_size": 7},
|
|
804
|
+
"duration_seconds": 4.0,
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
def _mock_video_task(endpoint, task_id, task_type, content=b"MP4DATA"):
|
|
809
|
+
"""Wire up submit + poll + download for one video-returning endpoint."""
|
|
810
|
+
route = respx.post(f"{BASE}/v1/{endpoint}").mock(
|
|
811
|
+
return_value=httpx.Response(202, json={"task_id": task_id, "status": "processing"})
|
|
812
|
+
)
|
|
813
|
+
respx.get(f"{BASE}/v1/tasks/{task_id}").mock(
|
|
814
|
+
return_value=httpx.Response(200, json=_video_body(task_id, task_type))
|
|
815
|
+
)
|
|
816
|
+
respx.get(f"https://r2.example.com/{task_id}.mp4").mock(
|
|
817
|
+
return_value=httpx.Response(200, content=content)
|
|
818
|
+
)
|
|
819
|
+
return route
|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
@respx.mock
|
|
823
|
+
def test_video_to_video_music_saves_the_scored_video(tmp_path):
|
|
824
|
+
route = _mock_video_task("video-to-video-music", "vm1", "video_to_video_music")
|
|
825
|
+
out = tmp_path / "scored.mp4"
|
|
826
|
+
run(["video-to-video-music", "--video-url", "http://x/y.mp4",
|
|
827
|
+
"--prompt", "tense synths", "--output", str(out)])
|
|
828
|
+
assert route.called
|
|
829
|
+
assert out.read_bytes() == b"MP4DATA"
|
|
830
|
+
body = unquote_plus(route.calls.last.request.content.decode())
|
|
831
|
+
assert "video_url=http://x/y.mp4" in body
|
|
832
|
+
assert "prompt=tense synths" in body
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
@respx.mock
|
|
836
|
+
def test_video_to_video_music_defaults_to_mp4(tmp_path, monkeypatch):
|
|
837
|
+
monkeypatch.chdir(tmp_path)
|
|
838
|
+
_mock_video_task("video-to-video-music", "vm2", "video_to_video_music")
|
|
839
|
+
run(["video-to-video-music", "--video-url", "http://x/y.mp4"])
|
|
840
|
+
assert (tmp_path / "output.mp4").read_bytes() == b"MP4DATA"
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
@respx.mock
|
|
844
|
+
def test_video_to_video_music_flags_reach_the_request_body(tmp_path):
|
|
845
|
+
route = _mock_video_task("video-to-video-music", "vm3", "video_to_video_music")
|
|
846
|
+
run(["video-to-video-music", "--video-url", "http://x/y.mp4",
|
|
847
|
+
"--preserve-speech", "--isolate-vocals", "--output", str(tmp_path / "s.mp4")])
|
|
848
|
+
body = route.calls.last.request.content.decode()
|
|
849
|
+
assert "preserve_speech=true" in body
|
|
850
|
+
assert "isolate_vocals=true" in body
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
@respx.mock
|
|
854
|
+
def test_video_to_video_music_unset_flags_are_omitted(tmp_path):
|
|
855
|
+
route = _mock_video_task("video-to-video-music", "vm4", "video_to_video_music")
|
|
856
|
+
run(["video-to-video-music", "--video-url", "http://x/y.mp4",
|
|
857
|
+
"--output", str(tmp_path / "s.mp4")])
|
|
858
|
+
# Unset switches must forward None, not False, so the server default
|
|
859
|
+
# stands — same rule as --no-ducking on the sound commands.
|
|
860
|
+
body = route.calls.last.request.content.decode()
|
|
861
|
+
assert "preserve_speech=" not in body
|
|
862
|
+
assert "isolate_vocals=" not in body
|
|
863
|
+
assert "prompt=" not in body
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
def test_video_to_video_music_requires_a_video_source():
|
|
867
|
+
with pytest.raises(SystemExit) as exc:
|
|
868
|
+
run(["video-to-video-music", "--prompt", "x"])
|
|
869
|
+
assert exc.value.code == 1
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
def test_video_to_video_music_rejects_both_sources():
|
|
873
|
+
with pytest.raises(SystemExit) as exc:
|
|
874
|
+
run(["video-to-video-music", "--video", "a.mp4", "--video-url", "http://x/y.mp4"])
|
|
875
|
+
assert exc.value.code == 1
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
def test_video_to_video_music_rejects_segments(capsys):
|
|
879
|
+
"""The endpoint scores the whole clip in one pass — the SDK resource takes
|
|
880
|
+
no `segments`, so the CLI must not offer the flag."""
|
|
881
|
+
with pytest.raises(SystemExit) as exc:
|
|
882
|
+
run(["video-to-video-music", "--video-url", "http://x/y.mp4", "--segments", "[]"])
|
|
883
|
+
assert exc.value.code == 1
|
|
884
|
+
assert "unrecognized arguments" in capsys.readouterr().err
|
|
885
|
+
|
|
886
|
+
|
|
887
|
+
@respx.mock
|
|
888
|
+
def test_video_to_video_sfx_saves_the_scored_video(tmp_path):
|
|
889
|
+
route = _mock_video_task("video-to-video-sfx", "vf1", "video_to_video_sfx")
|
|
890
|
+
out = tmp_path / "scored.mp4"
|
|
891
|
+
run(["video-to-video-sfx", "--video-url", "http://x/y.mp4",
|
|
892
|
+
"--prompt", "footsteps", "--output", str(out)])
|
|
893
|
+
assert route.called
|
|
894
|
+
assert out.read_bytes() == b"MP4DATA"
|
|
895
|
+
body = unquote_plus(route.calls.last.request.content.decode())
|
|
896
|
+
assert "video_url=http://x/y.mp4" in body
|
|
897
|
+
assert "prompt=footsteps" in body
|
|
898
|
+
|
|
899
|
+
|
|
900
|
+
@respx.mock
|
|
901
|
+
def test_video_to_video_sfx_defaults_to_mp4(tmp_path, monkeypatch):
|
|
902
|
+
monkeypatch.chdir(tmp_path)
|
|
903
|
+
_mock_video_task("video-to-video-sfx", "vf2", "video_to_video_sfx")
|
|
904
|
+
run(["video-to-video-sfx", "--video-url", "http://x/y.mp4"])
|
|
905
|
+
assert (tmp_path / "output.mp4").read_bytes() == b"MP4DATA"
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
@respx.mock
|
|
909
|
+
def test_video_to_video_sfx_segments_reach_the_request_body(tmp_path):
|
|
910
|
+
route = _mock_video_task("video-to-video-sfx", "vf3", "video_to_video_sfx")
|
|
911
|
+
run(["video-to-video-sfx", "--video-url", "http://x/y.mp4",
|
|
912
|
+
"--segments", json.dumps(SFX_SEGMENTS), "--output", str(tmp_path / "s.mp4")])
|
|
913
|
+
assert _sent_segments(route) == SFX_SEGMENTS
|
|
914
|
+
|
|
915
|
+
|
|
916
|
+
@respx.mock
|
|
917
|
+
def test_video_to_video_sfx_omitting_segments_sends_no_field(tmp_path):
|
|
918
|
+
route = _mock_video_task("video-to-video-sfx", "vf4", "video_to_video_sfx")
|
|
919
|
+
run(["video-to-video-sfx", "--video-url", "http://x/y.mp4",
|
|
920
|
+
"--output", str(tmp_path / "s.mp4")])
|
|
921
|
+
assert b"segments" not in route.calls.last.request.content
|
|
922
|
+
|
|
923
|
+
|
|
924
|
+
def test_video_to_video_sfx_rejects_music_shaped_segments(capsys):
|
|
925
|
+
with pytest.raises(SystemExit) as exc:
|
|
926
|
+
run(["video-to-video-sfx", "--video-url", "http://x/y.mp4",
|
|
927
|
+
"--segments", json.dumps(MUSIC_SEGMENTS)])
|
|
928
|
+
assert exc.value.code == 1
|
|
929
|
+
err = capsys.readouterr().err
|
|
930
|
+
assert "video-to-video-sfx segments take {start, end, prompt}" in err
|
|
931
|
+
assert "got an object with keys start, label, prompt" in err
|
|
932
|
+
|
|
933
|
+
|
|
934
|
+
def test_video_to_video_sfx_requires_a_video_source():
|
|
935
|
+
with pytest.raises(SystemExit) as exc:
|
|
936
|
+
run(["video-to-video-sfx", "--prompt", "x"])
|
|
937
|
+
assert exc.value.code == 1
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
def test_video_to_video_sfx_rejects_both_sources():
|
|
941
|
+
with pytest.raises(SystemExit) as exc:
|
|
942
|
+
run(["video-to-video-sfx", "--video", "a.mp4", "--video-url", "http://x/y.mp4"])
|
|
943
|
+
assert exc.value.code == 1
|
|
944
|
+
|
|
945
|
+
|
|
946
|
+
@pytest.mark.parametrize("command", ["video-to-music", "video-to-video-music"])
|
|
947
|
+
def test_isolate_vocals_is_documented_as_an_alias(command, capsys):
|
|
948
|
+
"""Both endpoints OR the two fields into one behaviour server-side
|
|
949
|
+
(video_to_music.py: `isolate_vocals = bool(preserve_speech) or
|
|
950
|
+
bool(isolate_vocals)`; video_to_video.py: `keep_speech = bool(...) or
|
|
951
|
+
bool(...)`), so the help must not present --isolate-vocals as a separate
|
|
952
|
+
feature. On video-to-video-music there is no stem at all — the result is
|
|
953
|
+
a single muxed video."""
|
|
954
|
+
with pytest.raises(SystemExit):
|
|
955
|
+
main([command, "--help"])
|
|
956
|
+
help_text = capsys.readouterr().out
|
|
957
|
+
assert "Legacy alias for --preserve-speech" in help_text
|
|
958
|
+
assert "vocals-only stem" not in help_text
|
|
959
|
+
|
|
960
|
+
|
|
961
|
+
@pytest.mark.parametrize(
|
|
962
|
+
"command", ["video-to-video-music", "video-to-video-sfx"]
|
|
963
|
+
)
|
|
964
|
+
def test_video_to_video_commands_are_listed_in_top_level_help(command, capsys):
|
|
965
|
+
"""Both are documented publicly as `sonilo <command>`, so they have to be
|
|
966
|
+
discoverable from `sonilo --help`, not just by knowing the name."""
|
|
967
|
+
with pytest.raises(SystemExit):
|
|
968
|
+
main(["--help"])
|
|
969
|
+
assert command in capsys.readouterr().out
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|