Metadata-Version: 2.5
Name: sonilo-cli
Version: 0.12.0
Summary: Command-line interface for the Sonilo API: generate music and sound effects from text or video
Project-URL: Repository, https://github.com/sonilo-ai/sonilo-python
Author: Sonilo AI
License-Expression: MIT
License-File: LICENSE
Keywords: ai,cli,music,sfx,sonilo,text-to-music,video-to-music
Requires-Python: >=3.9
Requires-Dist: sonilo<0.14,>=0.13.0
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: respx>=0.21; extra == 'dev'
Description-Content-Type: text/markdown

# sonilo-cli

Command-line interface for the [Sonilo API](https://github.com/sonilo-ai/sonilo-python) — generate music and sound effects from text or video.

## Install

    pip install sonilo-cli

## Signing in

Run this once per machine — there is no key to create, paste, or export:

    sonilo login

It prints a one-time code and opens your browser to platform.sonilo.com. Sign
in, confirm the code matches what the terminal printed, and approve; the CLI is
waiting on that page and continues by itself. Every command works from then on.

Approving mints an ordinary Sonilo API key on your account, named
`cli: <hostname>` and valid for **90 days**, stored in
`~/.config/sonilo/credentials.json` (`$XDG_CONFIG_HOME/sonilo/` when that is
set), owner-readable only. It is visible and revocable at
[the dashboard](https://platform.sonilo.com/dashboard/api-keys) like any other
key.

    sonilo whoami    # which account, key prefix, expiry, and which source is active
    sonilo logout    # revoke the key server-side, then forget it locally

`logout` revokes before forgetting, so a machine that loses the file never
leaves a live key behind. If the revoke cannot reach the API the credential is
deliberately kept and you are pointed at the dashboard, rather than being left
holding a key nothing can revoke.

Signing in again while already signed in reports the existing session; use
`--force` to replace it (that mints a fresh key and revokes the one it
replaces). When a credential has expired you do not need the flag — `sonilo
login` treats an expired sign-in as no sign-in. On a machine with no browser,
add `--no-browser` and approve the printed URL from another device.

The credential file is shared with the JS CLI (`npm install -g sonilo-cli`) and
read by [sonilo-mcp](https://github.com/sonilo-ai/sonilo-mcp) 0.16.0+, so one
sign-in covers all three — an MCP host config then needs no `env` block at all.

## Auth with an API key

Signing in is optional. A key from
[the dashboard](https://platform.sonilo.com/dashboard/api-keys) works exactly as
it always has, which is what you want for CI, containers, and anything
non-interactive — there is no browser there to approve with, and a 90-day expiry
is not something a pipeline should depend on:

    export SONILO_API_KEY=sk-...

or pass `--api-key sk-...` on any command.

Credentials resolve most-explicit-first: `--api-key`, then `SONILO_API_KEY`,
then the stored sign-in. That order is a guarantee, not an accident — an
exported `SONILO_API_KEY` keeps winning after you upgrade, so adding a `sonilo
login` on the same machine cannot quietly move your calls to another account.

Set `SONILO_API_URL` (or pass `--api-base` to `login`) to point at another
environment. Credentials are stored per host, so a staging sign-in and a
production sign-in coexist without overwriting each other.

## Commands

    sonilo login                       # sign in with your browser, no key needed
    sonilo whoami                      # show the active account and credential source
    sonilo logout                      # revoke the stored key and forget it
    sonilo account                     # plan limits and available services
    sonilo usage --days 7              # usage summary
    sonilo text-to-music --prompt "warm lo-fi piano, rain" --duration 30
    sonilo video-to-music --video clip.mp4 --prompt "tense synths" --format wav
    sonilo text-to-sfx --prompt "glass shattering on concrete" --duration 3
    sonilo video-to-sfx --video clip.mp4 --output whoosh.wav
    sonilo video-to-sfx --video clip.mp4 --segments @segments.json
    sonilo video-to-sound --video clip.mp4 \
        --music-prompt "uplifting orchestral score" --sfx-prompt "match the on-screen action"
    sonilo video-to-video-music --video clip.mp4 --prompt "tense synths" --output scored.mp4
    sonilo video-to-video-sfx --video clip.mp4 --segments @segments.json --output scored.mp4
    sonilo video-to-video-sound --video clip.mp4 --music-prompt "tense synths"
    sonilo audio-ducking --voice interview.mp4 --music-url https://example.com/bed.wav
    # ducks the existing music bed under the voice; a video voice comes back
    # as a new .mp4 with the ducked mix muxed in
    sonilo dubbing --video-url https://example.com/clip.mp4 --languages es,fr --output dubbed.mp4
    # writes dubbed.es.mp4 and dubbed.fr.mp4
    sonilo tasks get <task-id>
    sonilo tasks wait <task-id> --poll-interval 2 --timeout 600

### Notes

- `text-to-music` / `video-to-music` stream a short `.m4a` by default. `--format wav`,
  `--preserve-speech`, `--variants` above 1, and the legacy alias `--isolate-vocals` each switch
  to the async submit-and-poll path.
- `text-to-sfx` / `video-to-sfx` are always async; `--format` accepts `wav|mp3|aac|flac`.
- Output defaults to `./output.<ext>`; override with `--output`.

### Segments

`--segments` scores a timeline instead of one whole-clip prompt. It takes a JSON array, in one of
three forms — inline, from a file, or from stdin:

    sonilo text-to-music --prompt "warm lo-fi piano" --duration 30 \
        --segments '[{"start":0,"label":"intro","prompt":"airy pads"}]'
    sonilo video-to-sfx --video clip.mp4 --segments @segments.json
    jq -c '.cues' storyboard.json | sonilo video-to-sfx --video clip.mp4 --segments @-

A value starting with `@` names a source to read the JSON from, and `@-` reads standard input — the
same convention as `curl`, `gh` and `aws`. Anything else is parsed as JSON directly.

The two segment shapes are **not** interchangeable:

| Shape | Commands | Fields |
| --- | --- | --- |
| Music | `text-to-music`, `video-to-music` | `{start, prompt, label?}` |
| SFX | `video-to-sfx`, `video-to-video-sfx`, `video-to-sound`, `video-to-video-sound` | `{start, end, prompt}` |

- `start` / `end` are seconds from the start of the track or clip.
- Passing one shape to a command that takes the other is rejected before any request is made, with
  a message naming the shape that command expects.
- Only the shape is checked locally. Timing rules — the first segment starting at 0, minimum
  spacing between segments, the `label` vocabulary, how many segments are allowed — are enforced by
  the API, which answers with a `422` describing what it rejected.
- Keys the CLI does not recognise are forwarded as-is, so a newly added API field works without
  upgrading the CLI.
- `text-to-sfx` takes no segments (its output is a single effect, not a timeline).
- `video-to-video-music` takes no segments either — the API scores the whole clip in one pass.

### Variants

`--variants N` (1-10, default 1) generates that many distinct variants in one request instead of
one, on `text-to-music`, `video-to-music`, `video-to-video-music`, `video-to-sound`, and
`video-to-video-sound`. Cost scales linearly — `--variants 3` costs three times a single-variant
request — and values above 1 are never covered by the free trial.

    sonilo text-to-music --prompt "warm lo-fi piano" --duration 30 --variants 3 --output take.m4a
    # writes take.0.m4a, take.1.m4a, take.2.m4a

- `--variants` above 1 forces the async submit-and-poll path (see [Notes](#notes) above).
- With `--variants` unset (or `1`), a command writes the single `--output` file exactly as before
  this flag existed. Above 1, it instead writes one file per variant, with the variant index
  spliced before the extension: `take.m4a` becomes `take.0.m4a`, `take.1.m4a`, etc. — the same
  naming `--stem` and dubbing's per-language output already use.
- On `video-to-sound` / `video-to-video-sound`, `--stem` is applied per variant too, e.g.
  `take.0.music.m4a`.

### Prompt influence

`--prompt-influence` (0-1, API default 0.5) sets how strongly the generated music follows the
prompt, on `video-to-music` and `video-to-video-music` only. Lower values let the video lead;
higher values follow the prompt more literally. It is free of charge, and unlike `--format wav` it
does not force the async path — it works on the streaming default too. Left unset, the field is
not sent at all and the API's own 0.5 default applies; `--prompt-influence 0` is a real value
("let the video lead entirely") and is sent. Out-of-range values earn a `422` from the API.

    sonilo video-to-music --video clip.mp4 --prompt "tense synths" --prompt-influence 0.8

### Scored video

`video-to-video-music` and `video-to-video-sfx` are the video-out counterparts of `video-to-music`
and `video-to-sfx`: same generation, but what comes back is the source picture with the new audio
already muxed in, so there is nothing to line up afterwards. Both are async-only and write a single
file (default `output.mp4`):

    sonilo video-to-video-music --video clip.mp4 --prompt "tense synths" --output scored.mp4
    sonilo video-to-video-sfx --video clip.mp4 \
        --segments '[{"start":0,"end":5,"prompt":"footsteps on gravel"}]' --output scored.mp4

- `--prompt` is optional on both; without it the model scores from the picture alone.
- `video-to-video-music` also takes `--preserve-speech`, which keeps source speech in the mix;
  omitting it leaves the server default untouched. `--isolate-vocals` is a legacy alias for the
  same flag — the API ORs the two together, and this endpoint returns one muxed video with no
  separate vocals stem.
- `video-to-video-sfx` takes `--segments` in the SFX shape `{start, end, prompt}` — see
  [Segments](#segments).
- Neither command exposes `--format`: the output is a video, not an audio file.
- For music *and* effects in one call, use `video-to-video-sound` below.
- `video-to-video-music` also takes `--variants` — see [Variants](#variants) above.
  `video-to-video-sfx` does not.
- `video-to-video-music` also takes `--prompt-influence` — see
  [Prompt influence](#prompt-influence) above. `video-to-video-sfx` does not.

### Combined soundtracks

`video-to-sound` and `video-to-video-sound` score a clip with a music bed *and* sound effects in one
call (one charge, instead of chaining two requests). Both are async-only and take the same options —
they differ only in what comes back: `video-to-sound` writes the mixed **audio** (default
`output.wav`), `video-to-video-sound` writes the **source video with that audio muxed in** (default
`output.mp4`).

    sonilo video-to-sound --video clip.mp4 \
        --music-prompt "uplifting orchestral score" \
        --sfx-prompt "match the on-screen action" \
        --output soundtrack.wav --stem music --stem sfx

- `--music-prompt` / `--sfx-prompt` steer the two layers separately; both are optional.
- `--segments` places individual effects on the timeline, in the SFX shape `{start, end, prompt}` —
  see [Segments](#segments).
- `--preserve-speech` keeps speech from the source video in the mix.
- **Ducking is off by default.** Pass `--ducking` to bring the source video's own speech into the
  mix with the generated bed dipped under it — without it the result is the generated music and
  effects alone. Omitting both flags leaves the server default untouched; `--no-ducking` still
  works and now just states that default explicitly.
- `--stem` is repeatable (`music`, `music_processed`, `sfx`) and saves the individual layers next to
  the combined output, so you can re-balance the mix yourself. With `--output soundtrack.wav`, the
  music stem lands at `soundtrack.music.m4a`. `music_processed` exists only when `--preserve-speech`
  or ducking altered the music bed.
- Both also take `--variants` — see [Variants](#variants) above.

### Audio ducking

`audio-ducking` mixes an **existing** music bed under an **existing** voice track — nothing is
generated, so reach for it when the music is fixed or external. (When the music is being generated
for the same clip anyway, `video-to-sound` or `video-to-music` duck internally as part of that one
call instead.)

    sonilo audio-ducking --voice interview.mp4 --music-url https://example.com/bed.wav

- Exactly one of `--voice` / `--voice-url` and one of `--music` / `--music-url`; a local file and a
  URL mix freely across the two inputs.
- The **voice** may be audio or video: a video's own audio track becomes the voice, and the ducked
  mix is muxed back into a new video, so the result is a `.mp4` instead of a `.wav`. The default
  `--output` name follows what came back (`output.wav` or `output.mp4`).
- The **music** must be audio (`wav, mp3, m4a, aac, ogg, flac`). The API does not detect a video
  there, so the CLI rejects a local video file up front rather than let it be mishandled silently.
- Each input is capped at 360 seconds server-side.

### Dubbing

`dubbing` dubs a video into one or more target languages in a single async call:

    sonilo dubbing --video-url https://example.com/clip.mp4 --languages es,fr --output dubbed.mp4
    # writes dubbed.es.mp4 and dubbed.fr.mp4

- `--languages` is comma-separated; omit it to use the server default `zh_cn,es,fr`. Supported
  codes: `en, zh_cn, ja, ko, pt, es, de, fr, it, ru`.
- Source videos may be at most 180 seconds long.
- `--output` is a filename template, not a single destination: a dubbing task returns one video
  per language, so `--output clip.mp4` writes `clip.es.mp4`, `clip.fr.mp4`, etc.
- Billing is per language, and dubbing has **no free trial runs** — see [Free trial](#free-trial)
  below.
- `--timeout` defaults to 7200 seconds, matching the backend's own ceiling for a dubbing job
  (far longer than other commands' default, since dubbing can run well past the usual
  `tasks wait --timeout 600`). If the wait still times out, the task keeps running
  server-side — resume watching it with `sonilo tasks wait <task-id>`.

## Free trial

Accounts created through self-serve signup start with free runs on most endpoints — no card
required:

| Free runs | Endpoints |
| --- | --- |
| 2 each | text-to-music, text-to-sfx, audio-ducking |
| 1 each | video-to-music, video-to-sfx, video-to-video-music, video-to-video-sfx, video-to-sound, video-to-video-sound |
| 0 | dubbing |

Dubbing bills `video duration × number of languages`, so a free run on it would be worth far more
than a free run on any other endpoint — it has no free allowance and bills from the first call.

The table above is the current default. `sonilo account` prints the live numbers: the account JSON
goes to stdout, and when the account has a free-trial allowance one summary line goes to stderr:

    Free trial: text-to-music 1/2 left, video-to-music 0/1 left

Because the summary is on stderr, `sonilo account | jq .trial` still sees clean JSON.

Once an endpoint's free runs are used up, calls to it bill at the normal rate — or, if the account
has never been funded, fail with `HTTP 402: ... (trial_exhausted)` until a payment method is added.
That is the one 402 a retry can never fix.

## Rate limits

Two separate limits return `HTTP 429`, and they want opposite handling. The CLI prints the API's
own sentence, so the wording says which one you hit:

    sonilo: HTTP 429: Rate limit exceeded: your account allows 60 requests per minute. Please retry after 1 minute. To raise your limit, please contact info@sonilo.com. (rate_limit_exceeded)
    sonilo: HTTP 429: Too many concurrent generations: 5 of 5 in progress. Please wait for one to finish before starting another. To raise your limit, please contact info@sonilo.com. (rate_limit_exceeded)

The first means calls are going out too fast. The counter runs on a fixed 60-second window and
rejected calls count toward it too, so wait the window out instead of retrying inside it. The
second means every generation slot is busy — waiting alone frees nothing, a running generation has
to finish first.

`sonilo account` prints the account's own `rpm_limit` and `concurrency_limit`; the numbers above
are the standard-tier defaults. Email info@sonilo.com to raise either.
