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
32 changes: 22 additions & 10 deletions context7.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
{
"$schema": "https://context7.com/schema/context7.json",
"url": "https://context7.com/sonilo-ai/sonilo-python",
"public_key": "pk_h4BOrwVwv5CF9NJ5BIwvC",
"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"],
"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.",
"Read the API key from the SONILO_API_KEY environment variable by default; pass api_key= explicitly only when the caller has a reason to override it.",
"text-to-music and video-to-music: use generate() (streaming) for short synchronous tracks. It does NOT support output_format=\"wav\", isolate_vocals, preserve_speech or ducking.",
"For those options use generate_async(), or submit() + client.tasks.wait(parser=parse_music_result). isolate_vocals/preserve_speech/ducking are video-to-music only; text-to-music takes output_format alone.",
"text-to-sfx and video-to-sfx are always async: submit() returns a task and generate() (or tasks.wait()) polls it to completion. Neither has a streaming variant.",
"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 is charged twice.",
"Both sound endpoints are async-only and take identical options: music_prompt and sfx_prompt (NOT a single prompt), preserve_speech, ducking. video_to_sound returns mixed audio; video_to_video_sound returns the video with that audio muxed in.",
"ducking is default-ON server-side and the request builder only sends a boolean when it is not None. Leave it unset to keep the default; pass ducking=False only to opt out. Never forward a default of False — that silently disables it.",
"The same tri-state applies to preserve_speech: None means unset, not False.",
"SoundResult exposes the combined render as the presigned output_url — save it with result.save(path). Individual layers come from result.save_stem(path, which=\"music\"), with stems music, music_processed and sfx.",
"music_processed exists only when preserve_speech or ducking altered the music bed; 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."
"Catch AuthenticationError (401), PaymentRequiredError (402), RateLimitError (429) and TaskFailedError (status \"failed\") separately rather than one generic except — callers usually handle these differently. All extend SoniloError.",
"video / video_url accept exactly one of the two, never both and never neither — 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; 1 each for the video endpoints), then bill normally. A first call succeeding is not evidence that billing is set up."
]
}
37 changes: 37 additions & 0 deletions sonilo-cli/tests/test_context7.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,40 @@ def test_context7_json_is_valid_and_complete():
assert data["$schema"] == "https://context7.com/schema/context7.json"
assert "**/__pycache__/**" in data["excludeFolders"]
assert isinstance(data["rules"], list) and len(data["rules"]) >= 5


# Limits from https://context7.com/schema/context7.json. The parser tolerates
# oversized values, so nothing complains at index time — but the ownership
# claim validates strictly and rejects the whole file, which is how five
# over-long rules went unnoticed until claiming failed.
MAX_RULE_LEN = 255
MAX_RULES = 50
MAX_DESCRIPTION_LEN = 200
MAX_PROJECT_TITLE_LEN = 100

ALLOWED_KEYS = {
"$schema", "projectTitle", "description", "branch", "folders",
"excludeFolders", "excludeFiles", "rules", "disallow", "redirect",
"previousVersions", "url", "public_key",
}


def test_context7_json_respects_schema_limits():
data = json.loads((ROOT / "context7.json").read_text())

unknown = set(data) - ALLOWED_KEYS
assert not unknown, f"unknown top-level keys reject the claim: {unknown}"

assert len(data["rules"]) <= MAX_RULES
too_long = {i: len(r) for i, r in enumerate(data["rules"]) if len(r) > MAX_RULE_LEN}
assert not too_long, f"rules over {MAX_RULE_LEN} chars: {too_long}"

assert len(data["description"]) <= MAX_DESCRIPTION_LEN
assert len(data["projectTitle"]) <= MAX_PROJECT_TITLE_LEN


def test_context7_json_carries_ownership_keys():
"""The claim flow requires both, and the schema makes them interdependent."""
data = json.loads((ROOT / "context7.json").read_text())
assert data["url"] == "https://context7.com/sonilo-ai/sonilo-python"
assert data["public_key"].startswith("pk_")
Loading