Metadata-Version: 2.5
Name: sonilo-cli
Version: 0.19.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.21,>=0.20.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 proofread --video clip.mp4 --languages ja,zh_cn --output scripts/clip.srt
    # writes scripts/clip.<lang>.srt, including the detected source language
    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**.
- `--mode` is `both` (default), `music` or `sfx`. `both` adds a sound-design brief to the JSON:
  `sfx_segments` (shot-sized sections) and `sfx_prompt` (one string). `music` reproduces the
  previous output shape. Same price for all three.
- Source videos may be at most 480 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)"

### Proofread

`proofread` transcribes a video and translates the transcript into editable `.srt` files — one per
language, plus the source-language transcript. The video must have an audio track. Nothing is
dubbed: this is the step **before** `dubbing`, so the wording can be corrected before any voice is
rendered.

    sonilo proofread --video clip.mp4 --languages ja,zh_cn --output scripts/clip.srt
    # writes scripts/clip.en.srt, scripts/clip.ja.srt, scripts/clip.zh_cn.srt

- `--languages` is comma-separated and takes the same codes as `dubbing` (see [Dubbing](#dubbing)
  below for the list), so a proofread script can go straight into a dub. Omit it for the
  source-language transcript alone.
- `--source-language` tells transcription which language to expect, which helps on short, noisy or
  mixed-language audio. Omit it to have the language detected; either way the detected code is
  printed and names the source-language file.
- `--output` is a filename template, not a single destination, exactly as it is for `dubbing`: one
  `.srt` is written per language with the code inserted before the extension, so
  `--output scripts/clip.srt` writes `scripts/clip.en.srt`, `scripts/clip.fr.srt`, etc. Missing
  directories are created. Default: `proofread.srt`. Every language is always written — the URLs on
  the result are presigned and expire, and the files are the point.
- The source language is **always** returned alongside the requested targets, so a one-language
  request writes two files.
- After the files, the command prints the detected source language, the cue count, and one line per
  non-blocking warning (`Warning fr: high_text_speed (warning) at cue 33 — ...`). A warning never
  withholds a file.
- Source videos may be at most 300 seconds long and 300 MB. Billing is per second of video
  multiplied by the number of target languages at $0.001/second, a transcript-only request counts as
  one, and billing has a 10-second floor; there are 2 free runs — see [Free trial](#free-trial)
  below.
- `--timeout` defaults to 600 seconds, the usual default: a proofread job typically finishes in well
  under a minute. If the wait does time out, the task keeps running server-side — resume it with
  `sonilo tasks wait <task-id>`.
- Edit the files, then feed them straight into `dubbing`, which makes the dub speak your exact
  wording (drop the source-language file: `--subtitle` must match `--languages`):

      sonilo proofread --video clip.mp4 --languages es,fr --output scripts/clip.srt
      # ... correct scripts/clip.es.srt and scripts/clip.fr.srt ...
      sonilo dubbing --video clip.mp4 --languages es,fr \
        --subtitle es=scripts/clip.es.srt --subtitle fr=scripts/clip.fr.srt

### 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, ta, ml, kn, gu, pa_in, sd_in, hi` (`pt_br` is Brazilian Portuguese
  and `es_419` Latin American Spanish; plain `pt` and `es` stay unqualified, as
  does `ar`; `pa_in` and `sd_in` are Punjabi and Sindhi as spoken in India, the
  only variants available; `hi` is Hindi).
- 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.
- `--ducking` ducks the background music/effects bed under the dubbed voice while it speaks;
  off by default, so the bed otherwise stays at a static level. `--no-ducking` states that
  default explicitly.
- `--subtitle <language>=<path-or-url>` gives one language the script to speak, as an `.srt`/`.vtt`
  file or an https URL. Repeat it once per language; the set must match `--languages` exactly.
  The scripts are in the **target** language, not the source's:

      sonilo dubbing --video clip.mp4 --languages es,fr \
        --subtitle es=spanish.srt --subtitle fr=https://example.com/french.vtt --export-srt

- `--export-srt` (requires `--subtitle`) returns a re-timed `.srt` per language, aligned to the
  delivered audio with your lines kept verbatim. Each one is written beside its video
  (`clip.es.mp4` -> `clip.es.srt`), and one status line per language is printed. A language whose
  export is blocked still gets its video — only the `.srt` is missing.
- 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, proofread |
| 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.
