Metadata-Version: 2.4
Name: sonilo-video-kit
Version: 0.1.0
Summary: Video helpers for the Sonilo API: generate a soundtrack and mix it into your video with ffmpeg
Project-URL: Repository, https://github.com/sonilo-ai/sonilo-python
Author: Sonilo AI
License-Expression: MIT
License-File: LICENSE
Keywords: ai,ducking,ffmpeg,music,sonilo,soundtrack,video
Requires-Python: >=3.9
Requires-Dist: sonilo<0.4,>=0.3
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: respx>=0.21; extra == 'dev'
Description-Content-Type: text/markdown

# sonilo-video-kit

Video helpers for the [Sonilo](https://sonilo.com) API: generate a soundtrack
for a video and mix it in locally with ffmpeg. Python ≥ 3.9.

Requires `ffmpeg` + `ffprobe` on your PATH (macOS: `brew install ffmpeg`,
Debian/Ubuntu: `apt-get install ffmpeg`) — or pass `ffmpeg_path`/`ffprobe_path`
to any function.

## Installation

```bash
pip install sonilo sonilo-video-kit
```

`sonilo` (the core client) is a required dependency — it is installed
automatically alongside the kit, but is shown here for clarity.

## Quickstart

```python
from sonilo_video_kit import generate_music_for_video, mix_with_video

track = generate_music_for_video("./clip.mp4", prompt="upbeat, energetic")  # uses SONILO_API_KEY

mix_with_video(
    video="./clip.mp4",
    audio=track.audio,
    output="./clip.scored.mp4",
)
```

## Loudness-matched mixing

By default the kit measures the loudness (LUFS) of your video's own audio and
of the generated music, then sits the music 4 LU below the original — so
dialogue stays intelligible without hand-tuning. The final file is normalized
to −14 LUFS (streaming-platform delivery level) with a −1 dBFS peak limiter.
The delivery-normalize boost is capped at +12 dB; attenuation (bringing an
overly loud render down to target) is uncapped.

- `music_volume` (0–1, default 0.5): 0.5 is the matched level; each step of
  0.25 shifts ±6 dB (full range ±12 dB). 0 mutes the music.
- `original_volume` (0–1, default 1): absolute — 1 keeps the original exactly
  as recorded, 0 removes it entirely.
- `loudness_match=False` switches both knobs to plain absolute gains.
- `normalize=False` skips the delivery-loudness pass.

If loudness measurement fails (exotic codecs, unreadable audio), the kit
silently falls back to absolute-gain behavior rather than failing your render.

## Ducking

`mix_with_video` sits the music at a fixed level under the original audio.
`duck_music_under_speech` goes further: it rides the music down whenever
someone speaks and back up in the gaps.

```python
from sonilo_video_kit import duck_music_under_speech

duck_music_under_speech(
    video="./interview.mp4",
    audio=track.audio,
    output="./interview.ducked.mp4",
)
```

Unlike `mix_with_video`, which is entirely local and free, this calls the
Sonilo ducking API and is **billed on your video's duration**. The kit
uploads only the video's extracted audio track — your picture never leaves
the machine and is copied into the result untouched. The API sets the
ducking curve itself (speech gate, duck depth, −14 LUFS delivery, −1 dBTP
ceiling), so there are no volume knobs to pass.

Requirements are enforced locally, before anything is uploaded or charged:
the video must have an audio track and a real picture, it must run no longer
than **360 s**, `output` must carry a file extension and live in a directory
that already exists and is writable, and your picture must be
stream-copyable into `output`'s container. The ducked audio is always written
as **AAC**, so `output` must be a container that can carry AAC — `.webm`
(Vorbis/Opus only) is rejected before the API call whatever your picture
codec. Both the **360 s** limit and the amount billed are measured on the
**picture**, not the container (a video whose audio outlives its picture is
billed for the picture's length). Any failure raises before the API is called;
the kit never quietly falls back to an un-ducked mix. Use `mix_with_video` for
silent or longer videos.

### Nothing you have paid for is thrown away

The API charges when the job is **submitted**, and the task then runs to
completion server-side whatever happens to your process — so every failure
after that point keeps the mix you paid for reachable. Transient failures are
retried with backoff (a 5xx while polling, a dropped connection, a 503 from the
storage host mid-download).

If a failure after submit is **not** locally recoverable — the poll fails
terminally, the download can't complete, or the wait times out — the raised
`VideoKitError` names the **task id** and tells you the charge was already made
and the task is still finishing server-side. **No local rescue file is written
in this case.** Recover by polling `GET /v1/tasks/<task_id>` yourself until it
reports `succeeded`, then download the `output_url` it returns: that re-fetches
the mix you already paid for, instead of calling `duck_music_under_speech`
again and being billed twice.

If instead a final, purely-local step fails *after* the API call — remuxing
the ducked audio onto your picture, or placing the file at `output` (e.g. the
disk fills mid-mux) — the kit saves the downloaded ducked audio to
`<output>.ducked.wav` and raises a `VideoKitError` naming that path, so you can
fix the local problem and finish locally. A rescue never overwrites an earlier
one (`<output>.ducked.1.wav`, …), and `output` is always placed atomically.

## Errors

`VideoKitError` (invalid arguments, unreadable video), `FfmpegNotFoundError`
(ffmpeg/ffprobe missing — message includes install hints), `FfmpegError`
(ffmpeg failed — carries `exit_code` and `stderr_tail`), `DuckingFailedError`
(the ducking API accepted the job but could not finish it — carries `code`
and `refunded`). Errors from the Sonilo API pass through as the `sonilo`
package's typed errors.

`refunded` reports what the server said **at the moment the task was polled**,
not a final verdict: the backend marks a task failed before it reverses the
charge (and retries a failed reversal), so `refunded: False` means the reversal
had not landed yet, not that you were definitely billed.

## License

MIT
