Skip to content

Commit df5352b

Browse files
feat: add sonilo-cli package and context7.json (#10)
* feat(cli): scaffold sonilo-cli package * feat(cli): parser core, client, account and usage commands * feat(cli): text-to-music and video-to-music commands * chore: gitignore .superpowers/ scratch (public repo) * feat(cli): text-to-sfx and video-to-sfx commands * feat(cli): tasks get and tasks wait commands * feat: add context7.json for Context7 library registration * ci(cli): test and publish sonilo-cli; document the package * feat(cli): video-to-sound and video-to-video-sound commands; allow sonilo 0.5.x The 0.5.0 SDK release adds these two endpoints and the CLI's `sonilo>=0.4,<0.5` pin excluded it, making the packages unresolvable together. Widen the pin and wire up both commands, being careful that unset --no-ducking/--preserve-speech forward None (not False) so the server's default-ON ducking isn't silently disabled. * docs(cli): document video-to-sound commands, stems, ducking default, and the free trial
1 parent ef20411 commit df5352b

14 files changed

Lines changed: 935 additions & 1 deletion

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ jobs:
1717
with:
1818
python-version: ${{ matrix.python-version }}
1919
- run: sudo apt-get update && sudo apt-get install -y ffmpeg
20-
- run: pip install -e ".[dev]" -e "./sonilo-video-kit[dev]"
20+
- run: pip install -e ".[dev]" -e "./sonilo-video-kit[dev]" -e "./sonilo-cli[dev]"
2121
- run: pytest
2222
- run: pytest sonilo-video-kit
23+
- run: pytest sonilo-cli

.github/workflows/publish-cli.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: Publish sonilo-cli to PyPI
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
tags: ["sonilo-cli-v*"]
7+
8+
jobs:
9+
publish:
10+
runs-on: ubuntu-latest
11+
permissions:
12+
contents: read
13+
# PyPI Trusted Publishing (OIDC). No PYPI_TOKEN secret needed.
14+
id-token: write
15+
steps:
16+
- uses: actions/checkout@v4
17+
- uses: actions/setup-python@v5
18+
with:
19+
python-version: "3.12"
20+
- run: pip install -e ".[dev]" -e "./sonilo-cli[dev]" build
21+
- run: pytest sonilo-cli
22+
- name: Verify tag matches pyproject version
23+
if: github.ref_type == 'tag'
24+
run: |
25+
TAG_VERSION="${GITHUB_REF_NAME#sonilo-cli-v}"
26+
PKG_VERSION=$(python -c "import tomllib; print(tomllib.load(open('sonilo-cli/pyproject.toml','rb'))['project']['version'])")
27+
if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then
28+
echo "Tag version ($TAG_VERSION) does not match pyproject version ($PKG_VERSION)"
29+
exit 1
30+
fi
31+
- run: python -m build sonilo-cli
32+
- name: Publish to PyPI
33+
# Authenticates via OIDC Trusted Publishing — a pending publisher for
34+
# project `sonilo-cli` (repo sonilo-python, workflow publish-cli.yml)
35+
# must be configured on PyPI before the first tag push. Tag refs only.
36+
if: github.ref_type == 'tag'
37+
uses: pypa/gh-action-pypi-publish@release/v1
38+
with:
39+
packages-dir: sonilo-cli/dist
40+
skip-existing: true

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ build/
66
.pytest_cache/
77
.DS_Store
88
/docs/
9+
.superpowers/

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@ Python ≥ 3.9. Sync and async clients included.
99
pip install sonilo
1010
```
1111

12+
## Command-line interface
13+
14+
Prefer a terminal over Python? [`sonilo-cli`](./sonilo-cli/) wraps this client
15+
in a `sonilo` command for music and SFX generation:
16+
17+
```bash
18+
pip install sonilo-cli
19+
sonilo text-to-music --prompt "warm lo-fi piano, rain" --duration 30
20+
```
21+
1222
## Authentication
1323

1424
Create an API key in your [Sonilo dashboard](https://platform.sonilo.com/dashboard/api-keys),

context7.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"$schema": "https://context7.com/schema/context7.json",
3+
"projectTitle": "Sonilo",
4+
"description": "Official Python client and CLI for the Sonilo API — generate music, sound effects, and combined soundtracks from text or video.",
5+
"excludeFolders": ["**/dist/**", "**/.venv/**", "**/__pycache__/**", ".superpowers", ".pytest_cache"],
6+
"rules": [
7+
"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.",
8+
"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.",
9+
"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.",
10+
"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\").",
11+
"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.",
12+
"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.",
13+
"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.",
14+
"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.",
15+
"video / video_url parameters accept exactly one of the two, never both and never neither — check for that before constructing a request.",
16+
"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."
17+
]
18+
}

sonilo-cli/LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Sonilo AI
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

sonilo-cli/README.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# sonilo-cli
2+
3+
Command-line interface for the [Sonilo API](https://github.com/sonilo-ai/sonilo-python) — generate music and sound effects from text or video.
4+
5+
## Install
6+
7+
pip install sonilo-cli
8+
9+
## Auth
10+
11+
Set your API key once:
12+
13+
export SONILO_API_KEY=sk-...
14+
15+
or pass `--api-key sk-...` on any command.
16+
17+
## Commands
18+
19+
sonilo account # plan limits and available services
20+
sonilo usage --days 7 # usage summary
21+
sonilo text-to-music --prompt "warm lo-fi piano, rain" --duration 30
22+
sonilo video-to-music --video clip.mp4 --prompt "tense synths" --format wav
23+
sonilo text-to-sfx --prompt "glass shattering on concrete" --duration 3
24+
sonilo video-to-sfx --video clip.mp4 --output whoosh.wav
25+
sonilo video-to-sound --video clip.mp4 \
26+
--music-prompt "uplifting orchestral score" --sfx-prompt "match the on-screen action"
27+
sonilo video-to-video-sound --video clip.mp4 --music-prompt "tense synths"
28+
sonilo tasks get <task-id>
29+
sonilo tasks wait <task-id> --poll-interval 2 --timeout 600
30+
31+
### Notes
32+
33+
- `text-to-music` / `video-to-music` stream a short `.m4a` by default. `--format wav`,
34+
`--isolate-vocals`, and `--preserve-speech` each switch to the async submit-and-poll path.
35+
- `text-to-sfx` / `video-to-sfx` are always async; `--format` accepts `wav|mp3|aac|flac`.
36+
- Output defaults to `./output.<ext>`; override with `--output`.
37+
38+
### Combined soundtracks
39+
40+
`video-to-sound` and `video-to-video-sound` score a clip with a music bed *and* sound effects in one
41+
call (one charge, instead of chaining two requests). Both are async-only and take the same options —
42+
they differ only in what comes back: `video-to-sound` writes the mixed **audio** (default
43+
`output.wav`), `video-to-video-sound` writes the **source video with that audio muxed in** (default
44+
`output.mp4`).
45+
46+
sonilo video-to-sound --video clip.mp4 \
47+
--music-prompt "uplifting orchestral score" \
48+
--sfx-prompt "match the on-screen action" \
49+
--output soundtrack.wav --stem music --stem sfx
50+
51+
- `--music-prompt` / `--sfx-prompt` steer the two layers separately; both are optional.
52+
- `--preserve-speech` keeps speech from the source video in the mix.
53+
- **Ducking is on by default** (music dips under speech). Pass `--no-ducking` to opt out — omitting
54+
the flag leaves the server default untouched.
55+
- `--stem` is repeatable (`music`, `music_processed`, `sfx`) and saves the individual layers next to
56+
the combined output, so you can re-balance the mix yourself. With `--output soundtrack.wav`, the
57+
music stem lands at `soundtrack.music.m4a`. `music_processed` exists only when `--preserve-speech`
58+
or ducking altered the music bed.
59+
60+
## Free trial
61+
62+
Accounts created through self-serve signup start with free runs on every endpoint — no card
63+
required:
64+
65+
| Free runs | Endpoints |
66+
| --- | --- |
67+
| 2 each | text-to-music, text-to-sfx, audio-ducking |
68+
| 1 each | video-to-music, video-to-sfx, video-to-video-music, video-to-video-sfx, video-to-sound, video-to-video-sound |
69+
70+
Once an endpoint's free runs are used up, calls to it bill at the normal rate. `sonilo account`
71+
shows the services available to your key.

sonilo-cli/pyproject.toml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
[build-system]
2+
requires = ["hatchling"]
3+
build-backend = "hatchling.build"
4+
5+
[project]
6+
name = "sonilo-cli"
7+
version = "0.1.0"
8+
description = "Command-line interface for the Sonilo API: generate music and sound effects from text or video"
9+
readme = "README.md"
10+
license = "MIT"
11+
requires-python = ">=3.9"
12+
authors = [{ name = "Sonilo AI" }]
13+
dependencies = ["sonilo>=0.5,<0.6"]
14+
keywords = ["sonilo", "cli", "music", "sfx", "text-to-music", "video-to-music", "ai"]
15+
16+
[project.urls]
17+
Repository = "https://github.com/sonilo-ai/sonilo-python"
18+
19+
[project.scripts]
20+
sonilo = "sonilo_cli.__main__:main"
21+
22+
[project.optional-dependencies]
23+
dev = ["pytest>=8", "respx>=0.21"]
24+
25+
[tool.hatch.build.targets.wheel]
26+
packages = ["src/sonilo_cli"]
27+
28+
[tool.pytest.ini_options]
29+
testpaths = ["tests"]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
__version__ = "0.1.0"
2+
3+
__all__ = ["__version__"]

0 commit comments

Comments
 (0)