sonilo-video-kit 0.1.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.
@@ -0,0 +1,8 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .pytest_cache/
7
+ .DS_Store
8
+ /docs/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sonilo AI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,144 @@
1
+ Metadata-Version: 2.4
2
+ Name: sonilo-video-kit
3
+ Version: 0.1.0
4
+ Summary: Video helpers for the Sonilo API: generate a soundtrack and mix it into your video with ffmpeg
5
+ Project-URL: Repository, https://github.com/sonilo-ai/sonilo-python
6
+ Author: Sonilo AI
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Keywords: ai,ducking,ffmpeg,music,sonilo,soundtrack,video
10
+ Requires-Python: >=3.9
11
+ Requires-Dist: sonilo<0.4,>=0.3
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest>=8; extra == 'dev'
14
+ Requires-Dist: respx>=0.21; extra == 'dev'
15
+ Description-Content-Type: text/markdown
16
+
17
+ # sonilo-video-kit
18
+
19
+ Video helpers for the [Sonilo](https://sonilo.com) API: generate a soundtrack
20
+ for a video and mix it in locally with ffmpeg. Python ≥ 3.9.
21
+
22
+ Requires `ffmpeg` + `ffprobe` on your PATH (macOS: `brew install ffmpeg`,
23
+ Debian/Ubuntu: `apt-get install ffmpeg`) — or pass `ffmpeg_path`/`ffprobe_path`
24
+ to any function.
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ pip install sonilo sonilo-video-kit
30
+ ```
31
+
32
+ `sonilo` (the core client) is a required dependency — it is installed
33
+ automatically alongside the kit, but is shown here for clarity.
34
+
35
+ ## Quickstart
36
+
37
+ ```python
38
+ from sonilo_video_kit import generate_music_for_video, mix_with_video
39
+
40
+ track = generate_music_for_video("./clip.mp4", prompt="upbeat, energetic") # uses SONILO_API_KEY
41
+
42
+ mix_with_video(
43
+ video="./clip.mp4",
44
+ audio=track.audio,
45
+ output="./clip.scored.mp4",
46
+ )
47
+ ```
48
+
49
+ ## Loudness-matched mixing
50
+
51
+ By default the kit measures the loudness (LUFS) of your video's own audio and
52
+ of the generated music, then sits the music 4 LU below the original — so
53
+ dialogue stays intelligible without hand-tuning. The final file is normalized
54
+ to −14 LUFS (streaming-platform delivery level) with a −1 dBFS peak limiter.
55
+ The delivery-normalize boost is capped at +12 dB; attenuation (bringing an
56
+ overly loud render down to target) is uncapped.
57
+
58
+ - `music_volume` (0–1, default 0.5): 0.5 is the matched level; each step of
59
+ 0.25 shifts ±6 dB (full range ±12 dB). 0 mutes the music.
60
+ - `original_volume` (0–1, default 1): absolute — 1 keeps the original exactly
61
+ as recorded, 0 removes it entirely.
62
+ - `loudness_match=False` switches both knobs to plain absolute gains.
63
+ - `normalize=False` skips the delivery-loudness pass.
64
+
65
+ If loudness measurement fails (exotic codecs, unreadable audio), the kit
66
+ silently falls back to absolute-gain behavior rather than failing your render.
67
+
68
+ ## Ducking
69
+
70
+ `mix_with_video` sits the music at a fixed level under the original audio.
71
+ `duck_music_under_speech` goes further: it rides the music down whenever
72
+ someone speaks and back up in the gaps.
73
+
74
+ ```python
75
+ from sonilo_video_kit import duck_music_under_speech
76
+
77
+ duck_music_under_speech(
78
+ video="./interview.mp4",
79
+ audio=track.audio,
80
+ output="./interview.ducked.mp4",
81
+ )
82
+ ```
83
+
84
+ Unlike `mix_with_video`, which is entirely local and free, this calls the
85
+ Sonilo ducking API and is **billed on your video's duration**. The kit
86
+ uploads only the video's extracted audio track — your picture never leaves
87
+ the machine and is copied into the result untouched. The API sets the
88
+ ducking curve itself (speech gate, duck depth, −14 LUFS delivery, −1 dBTP
89
+ ceiling), so there are no volume knobs to pass.
90
+
91
+ Requirements are enforced locally, before anything is uploaded or charged:
92
+ the video must have an audio track and a real picture, it must run no longer
93
+ than **360 s**, `output` must carry a file extension and live in a directory
94
+ that already exists and is writable, and your picture must be
95
+ stream-copyable into `output`'s container. The ducked audio is always written
96
+ as **AAC**, so `output` must be a container that can carry AAC — `.webm`
97
+ (Vorbis/Opus only) is rejected before the API call whatever your picture
98
+ codec. Both the **360 s** limit and the amount billed are measured on the
99
+ **picture**, not the container (a video whose audio outlives its picture is
100
+ billed for the picture's length). Any failure raises before the API is called;
101
+ the kit never quietly falls back to an un-ducked mix. Use `mix_with_video` for
102
+ silent or longer videos.
103
+
104
+ ### Nothing you have paid for is thrown away
105
+
106
+ The API charges when the job is **submitted**, and the task then runs to
107
+ completion server-side whatever happens to your process — so every failure
108
+ after that point keeps the mix you paid for reachable. Transient failures are
109
+ retried with backoff (a 5xx while polling, a dropped connection, a 503 from the
110
+ storage host mid-download).
111
+
112
+ If a failure after submit is **not** locally recoverable — the poll fails
113
+ terminally, the download can't complete, or the wait times out — the raised
114
+ `VideoKitError` names the **task id** and tells you the charge was already made
115
+ and the task is still finishing server-side. **No local rescue file is written
116
+ in this case.** Recover by polling `GET /v1/tasks/<task_id>` yourself until it
117
+ reports `succeeded`, then download the `output_url` it returns: that re-fetches
118
+ the mix you already paid for, instead of calling `duck_music_under_speech`
119
+ again and being billed twice.
120
+
121
+ If instead a final, purely-local step fails *after* the API call — remuxing
122
+ the ducked audio onto your picture, or placing the file at `output` (e.g. the
123
+ disk fills mid-mux) — the kit saves the downloaded ducked audio to
124
+ `<output>.ducked.wav` and raises a `VideoKitError` naming that path, so you can
125
+ fix the local problem and finish locally. A rescue never overwrites an earlier
126
+ one (`<output>.ducked.1.wav`, …), and `output` is always placed atomically.
127
+
128
+ ## Errors
129
+
130
+ `VideoKitError` (invalid arguments, unreadable video), `FfmpegNotFoundError`
131
+ (ffmpeg/ffprobe missing — message includes install hints), `FfmpegError`
132
+ (ffmpeg failed — carries `exit_code` and `stderr_tail`), `DuckingFailedError`
133
+ (the ducking API accepted the job but could not finish it — carries `code`
134
+ and `refunded`). Errors from the Sonilo API pass through as the `sonilo`
135
+ package's typed errors.
136
+
137
+ `refunded` reports what the server said **at the moment the task was polled**,
138
+ not a final verdict: the backend marks a task failed before it reverses the
139
+ charge (and retries a failed reversal), so `refunded: False` means the reversal
140
+ had not landed yet, not that you were definitely billed.
141
+
142
+ ## License
143
+
144
+ MIT
@@ -0,0 +1,128 @@
1
+ # sonilo-video-kit
2
+
3
+ Video helpers for the [Sonilo](https://sonilo.com) API: generate a soundtrack
4
+ for a video and mix it in locally with ffmpeg. Python ≥ 3.9.
5
+
6
+ Requires `ffmpeg` + `ffprobe` on your PATH (macOS: `brew install ffmpeg`,
7
+ Debian/Ubuntu: `apt-get install ffmpeg`) — or pass `ffmpeg_path`/`ffprobe_path`
8
+ to any function.
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ pip install sonilo sonilo-video-kit
14
+ ```
15
+
16
+ `sonilo` (the core client) is a required dependency — it is installed
17
+ automatically alongside the kit, but is shown here for clarity.
18
+
19
+ ## Quickstart
20
+
21
+ ```python
22
+ from sonilo_video_kit import generate_music_for_video, mix_with_video
23
+
24
+ track = generate_music_for_video("./clip.mp4", prompt="upbeat, energetic") # uses SONILO_API_KEY
25
+
26
+ mix_with_video(
27
+ video="./clip.mp4",
28
+ audio=track.audio,
29
+ output="./clip.scored.mp4",
30
+ )
31
+ ```
32
+
33
+ ## Loudness-matched mixing
34
+
35
+ By default the kit measures the loudness (LUFS) of your video's own audio and
36
+ of the generated music, then sits the music 4 LU below the original — so
37
+ dialogue stays intelligible without hand-tuning. The final file is normalized
38
+ to −14 LUFS (streaming-platform delivery level) with a −1 dBFS peak limiter.
39
+ The delivery-normalize boost is capped at +12 dB; attenuation (bringing an
40
+ overly loud render down to target) is uncapped.
41
+
42
+ - `music_volume` (0–1, default 0.5): 0.5 is the matched level; each step of
43
+ 0.25 shifts ±6 dB (full range ±12 dB). 0 mutes the music.
44
+ - `original_volume` (0–1, default 1): absolute — 1 keeps the original exactly
45
+ as recorded, 0 removes it entirely.
46
+ - `loudness_match=False` switches both knobs to plain absolute gains.
47
+ - `normalize=False` skips the delivery-loudness pass.
48
+
49
+ If loudness measurement fails (exotic codecs, unreadable audio), the kit
50
+ silently falls back to absolute-gain behavior rather than failing your render.
51
+
52
+ ## Ducking
53
+
54
+ `mix_with_video` sits the music at a fixed level under the original audio.
55
+ `duck_music_under_speech` goes further: it rides the music down whenever
56
+ someone speaks and back up in the gaps.
57
+
58
+ ```python
59
+ from sonilo_video_kit import duck_music_under_speech
60
+
61
+ duck_music_under_speech(
62
+ video="./interview.mp4",
63
+ audio=track.audio,
64
+ output="./interview.ducked.mp4",
65
+ )
66
+ ```
67
+
68
+ Unlike `mix_with_video`, which is entirely local and free, this calls the
69
+ Sonilo ducking API and is **billed on your video's duration**. The kit
70
+ uploads only the video's extracted audio track — your picture never leaves
71
+ the machine and is copied into the result untouched. The API sets the
72
+ ducking curve itself (speech gate, duck depth, −14 LUFS delivery, −1 dBTP
73
+ ceiling), so there are no volume knobs to pass.
74
+
75
+ Requirements are enforced locally, before anything is uploaded or charged:
76
+ the video must have an audio track and a real picture, it must run no longer
77
+ than **360 s**, `output` must carry a file extension and live in a directory
78
+ that already exists and is writable, and your picture must be
79
+ stream-copyable into `output`'s container. The ducked audio is always written
80
+ as **AAC**, so `output` must be a container that can carry AAC — `.webm`
81
+ (Vorbis/Opus only) is rejected before the API call whatever your picture
82
+ codec. Both the **360 s** limit and the amount billed are measured on the
83
+ **picture**, not the container (a video whose audio outlives its picture is
84
+ billed for the picture's length). Any failure raises before the API is called;
85
+ the kit never quietly falls back to an un-ducked mix. Use `mix_with_video` for
86
+ silent or longer videos.
87
+
88
+ ### Nothing you have paid for is thrown away
89
+
90
+ The API charges when the job is **submitted**, and the task then runs to
91
+ completion server-side whatever happens to your process — so every failure
92
+ after that point keeps the mix you paid for reachable. Transient failures are
93
+ retried with backoff (a 5xx while polling, a dropped connection, a 503 from the
94
+ storage host mid-download).
95
+
96
+ If a failure after submit is **not** locally recoverable — the poll fails
97
+ terminally, the download can't complete, or the wait times out — the raised
98
+ `VideoKitError` names the **task id** and tells you the charge was already made
99
+ and the task is still finishing server-side. **No local rescue file is written
100
+ in this case.** Recover by polling `GET /v1/tasks/<task_id>` yourself until it
101
+ reports `succeeded`, then download the `output_url` it returns: that re-fetches
102
+ the mix you already paid for, instead of calling `duck_music_under_speech`
103
+ again and being billed twice.
104
+
105
+ If instead a final, purely-local step fails *after* the API call — remuxing
106
+ the ducked audio onto your picture, or placing the file at `output` (e.g. the
107
+ disk fills mid-mux) — the kit saves the downloaded ducked audio to
108
+ `<output>.ducked.wav` and raises a `VideoKitError` naming that path, so you can
109
+ fix the local problem and finish locally. A rescue never overwrites an earlier
110
+ one (`<output>.ducked.1.wav`, …), and `output` is always placed atomically.
111
+
112
+ ## Errors
113
+
114
+ `VideoKitError` (invalid arguments, unreadable video), `FfmpegNotFoundError`
115
+ (ffmpeg/ffprobe missing — message includes install hints), `FfmpegError`
116
+ (ffmpeg failed — carries `exit_code` and `stderr_tail`), `DuckingFailedError`
117
+ (the ducking API accepted the job but could not finish it — carries `code`
118
+ and `refunded`). Errors from the Sonilo API pass through as the `sonilo`
119
+ package's typed errors.
120
+
121
+ `refunded` reports what the server said **at the moment the task was polled**,
122
+ not a final verdict: the backend marks a task failed before it reverses the
123
+ charge (and retries a failed reversal), so `refunded: False` means the reversal
124
+ had not landed yet, not that you were definitely billed.
125
+
126
+ ## License
127
+
128
+ MIT
@@ -0,0 +1,19 @@
1
+ """Usage: SONILO_API_KEY=sk_... python examples/score_video.py clip.mp4 "upbeat, energetic"
2
+
3
+ Generates a soundtrack for a video and mixes it in locally with ffmpeg.
4
+ Requires ffmpeg + ffprobe on PATH.
5
+ """
6
+ import sys
7
+
8
+ from sonilo_video_kit import generate_music_for_video, mix_with_video
9
+
10
+ video = sys.argv[1] if len(sys.argv) > 1 else "clip.mp4"
11
+ prompt = sys.argv[2] if len(sys.argv) > 2 else "cinematic orchestral score"
12
+ output = "clip.scored.mp4"
13
+
14
+ track = generate_music_for_video(video, prompt=prompt)
15
+
16
+ out = mix_with_video(video=video, audio=track.audio, output=output)
17
+
18
+ title = f' — "{track.title}"' if track.title else ""
19
+ print(f"Saved {out}{title}")
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "sonilo-video-kit"
7
+ version = "0.1.0"
8
+ description = "Video helpers for the Sonilo API: generate a soundtrack and mix it into your video with ffmpeg"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.9"
12
+ authors = [{ name = "Sonilo AI" }]
13
+ dependencies = ["sonilo>=0.3,<0.4"]
14
+ keywords = ["sonilo", "video", "music", "ffmpeg", "soundtrack", "ducking", "ai"]
15
+
16
+ [project.urls]
17
+ Repository = "https://github.com/sonilo-ai/sonilo-python"
18
+
19
+ [project.optional-dependencies]
20
+ dev = ["pytest>=8", "respx>=0.21"]
21
+
22
+ [tool.hatch.build.targets.wheel]
23
+ packages = ["src/sonilo_video_kit"]
24
+
25
+ [tool.pytest.ini_options]
26
+ testpaths = ["tests"]
@@ -0,0 +1,20 @@
1
+ """Video helpers for the Sonilo API (ffmpeg-based)."""
2
+ from .duck import (
3
+ MAX_DUCKED_MIX_BYTES, MAX_DUCKING_DURATION_SECONDS, duck_music_under_speech,
4
+ )
5
+ from .errors import (
6
+ DuckingFailedError, FfmpegError, FfmpegNotFoundError, VideoKitError,
7
+ )
8
+ from .generate import VideoMusicClient, generate_music_for_video
9
+ from .loudness import (
10
+ DELIVERY_TARGET_LUFS, FALLBACK_MUSIC_LUFS, GAP_BELOW_VOICE_LU, OUTPUT_CEILING_DBFS,
11
+ )
12
+ from .mix import mix_with_video
13
+
14
+ __all__ = sorted([
15
+ "DELIVERY_TARGET_LUFS", "DuckingFailedError", "FALLBACK_MUSIC_LUFS", "FfmpegError",
16
+ "FfmpegNotFoundError", "GAP_BELOW_VOICE_LU", "MAX_DUCKED_MIX_BYTES",
17
+ "MAX_DUCKING_DURATION_SECONDS", "OUTPUT_CEILING_DBFS", "VideoKitError",
18
+ "VideoMusicClient", "duck_music_under_speech", "generate_music_for_video",
19
+ "mix_with_video",
20
+ ])