diff --git a/context7.json b/context7.json index 3d0558d..b8d4252 100644 --- a/context7.json +++ b/context7.json @@ -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." ] } diff --git a/sonilo-cli/tests/test_context7.py b/sonilo-cli/tests/test_context7.py index a7968b7..86d81b9 100644 --- a/sonilo-cli/tests/test_context7.py +++ b/sonilo-cli/tests/test_context7.py @@ -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_")