Metadata-Version: 2.5
Name: sonilo-cli
Version: 0.15.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.17,>=0.16.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 video-analysis --video clip.mp4 --variants 2
    # prints a creative brief as JSON; generates nothing
    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, `--stems`, 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

### Music stems

`--stems` on `text-to-music` and `video-to-music` also splits the generated music into four
stems — drums, bass, vocals, other — saved next to the main output with the stem name spliced
before the extension (`take.m4a` → `take.drums.m4a`, and per variant with `--variants` above 1:
`take.0.drums.m4a`). It is free of charge, and forces the async path. On `video-to-music` it
splits the *generated* music, never the video's own audio.

    sonilo text-to-music --prompt "warm lo-fi piano" --duration 60 --stems --output take.m4a
    # writes take.m4a, take.drums.m4a, take.bass.m4a, take.vocals.m4a, take.other.m4a

- Separation runs **after** generation and typically adds 2-6 minutes to the wait; the CLI
  waits up to 2400 seconds on these runs (covering the separation service's own 30-minute
  ceiling, the way dubbing's `--timeout` covers its backend's). If the wait still times out,
  the task keeps running server-side — resume it with `sonilo tasks wait <task-id>`.
- Separation can also come up short without failing the run: streams that did not separate are
  reported on stderr (the API's `stems_error`), while the stems that did come back are still
  saved — a partial result is not an error exit, and the main output is always written.
- Not the same flag as `--stem` on the sound commands, which saves layers those endpoints
  already return; `--stems` *requests* a separation the API would not otherwise run.

### 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.

### Video analysis

`video-analysis` analyzes a video and prints a **creative brief** for scoring it. It is the one
command that produces no media file — nothing is generated:

    sonilo video-analysis --video clip.mp4 --prompt "focus on the chase" --variants 2

- The brief goes to **stdout as JSON** so it can be piped into another tool: `segments` (a
  time-aligned section plan) and `variations` (one ready-to-use generation prompt each). Pass
  `--output brief.json` to write it to a file instead.
- `--variants` is 1-5 (default 1) and is **billed per brief**.
- Source videos may be at most 360 seconds long, and billing has a 10-second floor.
- Feed a variation's prompt straight into the next command:

      sonilo video-analysis --video clip.mp4 --output brief.json
      sonilo video-to-music --video clip.mp4 --prompt "$(jq -r '.variations[0].prompt' brief.json)"

### 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, pt_br, es, es_419, de, fr, it, ru, th, ar,
  tr, vi, id` (`pt_br` is Brazilian Portuguese and `es_419` Latin American
  Spanish; plain `pt` and `es` stay unqualified, as does `ar`).
- Source videos may be at most 300 seconds long.
- `--no-lipsync` leaves the picture completely untouched. By default the speaker's mouth is
  re-rendered to match the dubbed speech; with this flag the video comes back at its original
  resolution and frame rate and only the audio is replaced, so the mouths keep moving to the
  original language. Use it for footage with no on-camera speaker, or when preserving the exact
  original picture matters more than matching lip movement.
- `--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, video-analysis |
| 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.
