Command-line interface for the Sonilo API — generate music and sound effects from text or video.
pip install sonilo-cli
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 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 0.16.0+, so one
sign-in covers all three — an MCP host config then needs no env block at all.
Signing in is optional. A key from the dashboard 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.
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
text-to-music/video-to-musicstream a short.m4aby default.--format wav,--preserve-speech,--variantsabove 1,--stems, and the legacy alias--isolate-vocalseach switch to the async submit-and-poll path.text-to-sfx/video-to-sfxare always async;--formatacceptswav|mp3|aac|flac.- Output defaults to
./output.<ext>; override with--output.
--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/endare 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
labelvocabulary, how many segments are allowed — are enforced by the API, which answers with a422describing 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-sfxtakes no segments (its output is a single effect, not a timeline).video-to-video-musictakes no segments either — the API scores the whole clip in one pass.
--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
--variantsabove 1 forces the async submit-and-poll path (see Notes above).- With
--variantsunset (or1), a command writes the single--outputfile exactly as before this flag existed. Above 1, it instead writes one file per variant, with the variant index spliced before the extension:take.m4abecomestake.0.m4a,take.1.m4a, etc. — the same naming--stemand dubbing's per-language output already use. - On
video-to-sound/video-to-video-sound,--stemis applied per variant too, e.g.take.0.music.m4a.
--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
--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
--timeoutcovers its backend's). If the wait still times out, the task keeps running server-side — resume it withsonilo 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
--stemon the sound commands, which saves layers those endpoints already return;--stemsrequests a separation the API would not otherwise run.
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
--promptis optional on both; without it the model scores from the picture alone.video-to-video-musicalso takes--preserve-speech, which keeps source speech in the mix; omitting it leaves the server default untouched.--isolate-vocalsis 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-sfxtakes--segmentsin the SFX shape{start, end, prompt}— see 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-soundbelow. video-to-video-musicalso takes--variants— see Variants above.video-to-video-sfxdoes not.video-to-video-musicalso takes--prompt-influence— see Prompt influence above.video-to-video-sfxdoes not.
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-promptsteer the two layers separately; both are optional.--segmentsplaces individual effects on the timeline, in the SFX shape{start, end, prompt}— see Segments.--preserve-speechkeeps speech from the source video in the mix.- Ducking is off by default. Pass
--duckingto 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-duckingstill works and now just states that default explicitly. --stemis 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 atsoundtrack.music.m4a.music_processedexists only when--preserve-speechor ducking altered the music bed.- Both also take
--variants— see Variants above.
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-urland 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
.mp4instead of a.wav. The default--outputname follows what came back (output.wavoroutput.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 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) andvariations(one ready-to-use generation prompt each). Pass--output brief.jsonto write it to a file instead. -
--variantsis 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 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
--languagesis comma-separated; omit it to use the server defaultzh_cn,es,fr. Supported codes:en, zh_cn, ja, ko, pt, es, de, fr, it, ru.- Source videos may be at most 300 seconds long.
--outputis a filename template, not a single destination: a dubbing task returns one video per language, so--output clip.mp4writesclip.es.mp4,clip.fr.mp4, etc.- Billing is per language, and dubbing has no free trial runs — see Free trial below.
--timeoutdefaults 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 usualtasks wait --timeout 600). If the wait still times out, the task keeps running server-side — resume watching it withsonilo tasks wait <task-id>.
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.
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.