From 3d9468da35b361995e56922053e7a55e2304e4d7 Mon Sep 17 00:00:00 2001 From: shawnsong530 Date: Tue, 13 Oct 2026 00:28:45 -0700 Subject: [PATCH 1/2] docs: AGENTS.md for agents working in this repo Covers the three packages, layout, the CI commands, conventions for changes and new endpoints, the tag-driven releases, SDK and CLI entry points, length caps and the error classes. CLAUDE.md imports it. --- AGENTS.md | 142 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ CLAUDE.md | 1 + 2 files changed, 143 insertions(+) create mode 100644 AGENTS.md create mode 100644 CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..9a3d21a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,142 @@ +# AGENTS.md + +Notes for coding agents working in this repo, and for agents writing code +against the Sonilo Python packages. + +## What this repo is + +Three PyPI packages in one repo: + +| Package | Path | What it is | +|---|---|---| +| `sonilo` | `.` (`src/sonilo`) | Python client for the Sonilo API. Sync (`Sonilo`) and async (`AsyncSonilo`) clients, Python 3.9+, only depends on `httpx` | +| `sonilo-cli` | `sonilo-cli/` | `sonilo` command line tool, built on `sonilo` | +| `sonilo-video-kit` | `sonilo-video-kit/` | Helpers that generate a soundtrack, duck it under speech and mix it into a video with local ffmpeg | + +The TypeScript counterparts live in +[sonilo-js](https://github.com/sonilo-ai/sonilo-js). + +## Layout + +``` +src/sonilo/_client.py Sonilo: auth, base URL, timeout, transport +src/sonilo/_async_client.py AsyncSonilo, same surface with async methods +src/sonilo/resources/*.py one module per endpoint (text_to_music, video_to_sfx, dubbing, ...) +src/sonilo/resources/tasks.py tasks.get / tasks.wait and the parse_*_result parsers +src/sonilo/errors.py error classes, mapped from HTTP status and the API `code` +src/sonilo/types.py result dataclasses and TypedDicts +src/sonilo/_version.py __version__, sent in X-Sonilo-Client-Version +sonilo-cli/src/sonilo_cli/ __main__.py (every command), login.py, credentials.py +sonilo-video-kit/src/sonilo_video_kit/ generate, duck, mix, loudness, ffmpeg wrappers +context7.json rules Context7 serves to coding assistants (checked by sonilo-cli/tests/test_context7.py) +examples/ runnable scripts +``` + +## Commands + +CI (`.github/workflows/ci.yml`) runs on Python 3.9 and 3.12 with ffmpeg +installed: + +```bash +pip install -e ".[dev]" -e "./sonilo-video-kit[dev]" -e "./sonilo-cli[dev]" +pytest # sonilo +pytest sonilo-video-kit # needs ffmpeg and ffprobe on PATH +pytest sonilo-cli +``` + +Tests mock HTTP with `respx`; none of them call the real API. + +## Making changes + +- Keep `sonilo` on Python 3.9 syntax (`Optional[...]`, `from __future__ import annotations`) + and on `httpx` as its only dependency. +- Every resource has a sync class and an async class in the same module. Change + both, and cover both in tests. +- New endpoint: add a module under `src/sonilo/resources/`, attach it in both + `_client.py` and `_async_client.py`, export new result types from + `src/sonilo/__init__.py`, add tests, document it in `README.md`, then add the + CLI command in `sonilo-cli/src/sonilo_cli/__main__.py` if a user would run it + from a terminal. +- Use the API's field names (`video_url`, `variants_num`, `output_format`) as + the Python keyword arguments. +- Tri-state options (`ducking`, `preserve_speech`) are sent only when not + `None`, so the server default applies when they are left unset. +- Branch on the API's error `code`, never on message text. +- Wrappers pass `client_name` / `client_version` (`cli-python`, + `videokit-python`) so their traffic is reported in the `X-Sonilo-Client` + header; direct SDK use leaves them unset. +- `context7.json` rules are limited to 255 characters each and 50 in total; + `sonilo-cli/tests/test_context7.py` enforces it. + +## Releases + +Each package publishes to PyPI through OIDC Trusted Publishing when its tag is +pushed. The workflow fails if the tag does not match the version in the +package's `pyproject.toml`. + +| Package | Bump | Tag | Workflow | +|---|---|---|---| +| `sonilo` | `pyproject.toml` and `src/sonilo/_version.py` | `vX.Y.Z` | `publish.yml` | +| `sonilo-cli` | `sonilo-cli/pyproject.toml` and `sonilo_cli/__init__.py` | `sonilo-cli-vX.Y.Z` | `publish-cli.yml` | +| `sonilo-video-kit` | `sonilo-video-kit/pyproject.toml` and `sonilo_video_kit/_version.py` | `sonilo-video-kit-vX.Y.Z` | `publish-video-kit.yml` | + +`sonilo-cli` pins `sonilo>=X.Y, Date: Tue, 13 Oct 2026 00:29:08 -0700 Subject: [PATCH 2/2] README and examples: keys start with sk-, music saves as m4a; context7 dubbing has a 15 s free preview --- README.md | 6 +++--- context7.json | 2 +- examples/generate.py | 4 ++-- examples/sfx.py | 2 +- sonilo-video-kit/examples/score_video.py | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 558f584..e9d8ad0 100644 --- a/README.md +++ b/README.md @@ -26,12 +26,12 @@ then give it to the client either as an environment variable (recommended) or inline: ```bash -export SONILO_API_KEY=sk_... +export SONILO_API_KEY=sk-... ``` ```python client = Sonilo() # reads SONILO_API_KEY -client = Sonilo(api_key="sk_...") # or pass it directly +client = Sonilo(api_key="sk-...") # or pass it directly ``` Keep your key secret — use it only server-side, never commit it, and prefer the @@ -48,7 +48,7 @@ track = client.text_to_music.generate( prompt="cinematic orchestral score", duration=60, ) -track.save("output.mp3") +track.save("output.m4a") print(track.title) ``` diff --git a/context7.json b/context7.json index 7686de1..dd165bf 100644 --- a/context7.json +++ b/context7.json @@ -27,7 +27,7 @@ "Result media (.url) is a short-lived presigned URL, not the API's own domain \u2014 download it with the result's .save() helper; do not send the Authorization header to it.", "Catch AuthenticationError (401), PaymentRequiredError (402), RateLimitError (429) and TaskFailedError (status \"failed\") separately rather than one generic except \u2014 callers usually handle these differently. All extend SoniloError.", "video / video_url accept exactly one of the two, never both and never neither \u2014 validate before constructing a request.", - "Self-serve accounts start with free runs per endpoint (2 each for text-to-music, text-to-sfx, audio-ducking, video-analysis, proofread; 1 each for other video endpoints; none for dubbing). A first call succeeding is not proof billing works.", + "Self-serve accounts start with free runs per endpoint (2 each for text-to-music, text-to-sfx, audio-ducking, video-analysis, proofread; 1 each for other video endpoints; dubbing: one free 15 s preview). A first call succeeding is not proof billing works.", "Before a paid call, read client.account.services().get(\"trial\", {}) and degrade gracefully when a service's remaining is 0: that call raises TrialExhaustedError (402 trial_exhausted), which no retry fixes \u2014 ask for a payment method. trial may be absent.", "client.audio_ducking ducks an EXISTING music bed under an EXISTING voice track; nothing is generated. One of voice/voice_url, one of music/music_url. Voice may be audio or video (video returns a .mp4); music must be audio. Result: output_url, no stems.", "client.dubbing dubs one video into many languages in one call; languages: 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; default zh_cn,es,fr. Billed per language; 1 free 15 s preview.", diff --git a/examples/generate.py b/examples/generate.py index ba9712f..92baec0 100644 --- a/examples/generate.py +++ b/examples/generate.py @@ -1,4 +1,4 @@ -"""Usage: SONILO_API_KEY=sk_... python examples/generate.py "lofi beat" 30""" +"""Usage: SONILO_API_KEY=sk-... python examples/generate.py "lofi beat" 30""" import sys from sonilo import Sonilo @@ -9,6 +9,6 @@ with Sonilo() as client: track = client.text_to_music.generate(prompt=prompt, duration=duration) -out = track.save("output.mp3") +out = track.save("output.m4a") title = f' — "{track.title}"' if track.title else "" print(f"Saved {out} ({len(track.audio)} bytes){title}") diff --git a/examples/sfx.py b/examples/sfx.py index ab808c7..7045f9a 100644 --- a/examples/sfx.py +++ b/examples/sfx.py @@ -1,6 +1,6 @@ """Generate a sound effect from text and save it locally. -Usage: SONILO_API_KEY=sk_... python examples/sfx.py +Usage: SONILO_API_KEY=sk-... python examples/sfx.py """ from sonilo import Sonilo diff --git a/sonilo-video-kit/examples/score_video.py b/sonilo-video-kit/examples/score_video.py index 9df406e..0f23960 100644 --- a/sonilo-video-kit/examples/score_video.py +++ b/sonilo-video-kit/examples/score_video.py @@ -1,4 +1,4 @@ -"""Usage: SONILO_API_KEY=sk_... python examples/score_video.py clip.mp4 "upbeat, energetic" +"""Usage: SONILO_API_KEY=sk-... python examples/score_video.py clip.mp4 "upbeat, energetic" Generates a soundtrack for a video and mixes it in locally with ffmpeg. Requires ffmpeg + ffprobe on PATH.