Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
- run: sudo apt-get update && sudo apt-get install -y ffmpeg
- run: pip install -e ".[dev]" -e "./sonilo-video-kit[dev]"
- run: pip install -e ".[dev]" -e "./sonilo-video-kit[dev]" -e "./sonilo-cli[dev]"
- run: pytest
- run: pytest sonilo-video-kit
- run: pytest sonilo-cli
40 changes: 40 additions & 0 deletions .github/workflows/publish-cli.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Publish sonilo-cli to PyPI

on:
workflow_dispatch:
push:
tags: ["sonilo-cli-v*"]

jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
# PyPI Trusted Publishing (OIDC). No PYPI_TOKEN secret needed.
id-token: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install -e ".[dev]" -e "./sonilo-cli[dev]" build
- run: pytest sonilo-cli
- name: Verify tag matches pyproject version
if: github.ref_type == 'tag'
run: |
TAG_VERSION="${GITHUB_REF_NAME#sonilo-cli-v}"
PKG_VERSION=$(python -c "import tomllib; print(tomllib.load(open('sonilo-cli/pyproject.toml','rb'))['project']['version'])")
if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then
echo "Tag version ($TAG_VERSION) does not match pyproject version ($PKG_VERSION)"
exit 1
fi
- run: python -m build sonilo-cli
- name: Publish to PyPI
# Authenticates via OIDC Trusted Publishing — a pending publisher for
# project `sonilo-cli` (repo sonilo-python, workflow publish-cli.yml)
# must be configured on PyPI before the first tag push. Tag refs only.
if: github.ref_type == 'tag'
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: sonilo-cli/dist
skip-existing: true
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ build/
.pytest_cache/
.DS_Store
/docs/
.superpowers/
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ Python ≥ 3.9. Sync and async clients included.
pip install sonilo
```

## Command-line interface

Prefer a terminal over Python? [`sonilo-cli`](./sonilo-cli/) wraps this client
in a `sonilo` command for music and SFX generation:

```bash
pip install sonilo-cli
sonilo text-to-music --prompt "warm lo-fi piano, rain" --duration 30
```

## Authentication

Create an API key in your [Sonilo dashboard](https://platform.sonilo.com/dashboard/api-keys),
Expand Down
18 changes: 18 additions & 0 deletions context7.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"$schema": "https://context7.com/schema/context7.json",
"projectTitle": "Sonilo",
"description": "Official Python client and CLI for the Sonilo API — generate music, sound effects, and combined soundtracks from text or video.",
"excludeFolders": ["**/dist/**", "**/.venv/**", "**/__pycache__/**", ".superpowers", ".pytest_cache"],
"rules": [
"Read the API key from the SONILO_API_KEY environment variable by default; only pass api_key= explicitly when the caller has a reason to override it.",
"For text-to-music and video-to-music: use client.text_to_music.generate() / client.video_to_music.generate() (streaming) for short synchronous tracks. Switch to generate_async() (or submit() + client.tasks.wait(parser=parse_music_result)) when the caller needs output_format=\"wav\" — and, for video-to-music only, isolate_vocals, preserve_speech, or ducking. The streaming path supports none of these.",
"For text-to-sfx and video-to-sfx, generation is always async: submit() returns a task, and generate() (or tasks.wait()) polls it to completion. There is no streaming variant for these two endpoints.",
"client.video_to_sound and client.video_to_video_sound score one clip with a music bed AND sound effects in a single call — prefer them over chaining video-to-music with video-to-sfx, which costs two charges. Both are async-only and take identical options: music_prompt and sfx_prompt (NOT a single prompt), preserve_speech, and ducking. video_to_sound returns mixed audio (output_type \"audio\"); video_to_video_sound returns the source video with that audio muxed in (output_type \"video\").",
"ducking is default-ON server-side and the request builder only sends a boolean when it is not None. Leave ducking unset (None) to keep the default; pass ducking=False ONLY to explicitly opt out. Never forward a default of False — that silently disables the server default. The same tri-state applies to preserve_speech.",
"A SoundResult exposes the combined render as the bare presigned output_url — save it with result.save(path). The separate layers come back as the music, music_processed, and sfx stems, saved with result.save_stem(path, which=\"music\"). music_processed is present only when preserve_speech or ducking altered the music bed, and save_stem raises SoniloError for an absent stem.",
"Result media (.url) is a short-lived presigned URL, not the API's own domain — download it with the result's .save() helper; do not send the Authorization header to it.",
"Wrap calls in try/except for AuthenticationError (401), PaymentRequiredError (402), RateLimitError (429), and TaskFailedError (task reached status \"failed\") rather than a single generic except — callers usually want to handle these differently.",
"video / video_url parameters accept exactly one of the two, never both and never neither — check for that before constructing a request.",
"Self-serve accounts start with free runs per endpoint (2 each for text-to-music, text-to-sfx and audio-ducking; 1 each for the video endpoints), after which calls bill at the normal rate — so a first call succeeding is not evidence that the account has billing set up."
]
}
21 changes: 21 additions & 0 deletions sonilo-cli/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Sonilo AI

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
71 changes: 71 additions & 0 deletions sonilo-cli/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# 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

## Auth

Set your API key once:

export SONILO_API_KEY=sk-...

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

## Commands

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-sound --video clip.mp4 \
--music-prompt "uplifting orchestral score" --sfx-prompt "match the on-screen action"
sonilo video-to-video-sound --video clip.mp4 --music-prompt "tense synths"
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`,
`--isolate-vocals`, and `--preserve-speech` 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`.

### 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.
- `--preserve-speech` keeps speech from the source video in the mix.
- **Ducking is on by default** (music dips under speech). Pass `--no-ducking` to opt out — omitting
the flag leaves the server default untouched.
- `--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.

## Free trial

Accounts created through self-serve signup start with free runs on every endpoint — 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 |

Once an endpoint's free runs are used up, calls to it bill at the normal rate. `sonilo account`
shows the services available to your key.
29 changes: 29 additions & 0 deletions sonilo-cli/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "sonilo-cli"
version = "0.1.0"
description = "Command-line interface for the Sonilo API: generate music and sound effects from text or video"
readme = "README.md"
license = "MIT"
requires-python = ">=3.9"
authors = [{ name = "Sonilo AI" }]
dependencies = ["sonilo>=0.5,<0.6"]
keywords = ["sonilo", "cli", "music", "sfx", "text-to-music", "video-to-music", "ai"]

[project.urls]
Repository = "https://github.com/sonilo-ai/sonilo-python"

[project.scripts]
sonilo = "sonilo_cli.__main__:main"

[project.optional-dependencies]
dev = ["pytest>=8", "respx>=0.21"]

[tool.hatch.build.targets.wheel]
packages = ["src/sonilo_cli"]

[tool.pytest.ini_options]
testpaths = ["tests"]
3 changes: 3 additions & 0 deletions sonilo-cli/src/sonilo_cli/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
__version__ = "0.1.0"

__all__ = ["__version__"]
Loading
Loading