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
17 changes: 10 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -421,8 +421,8 @@ The background bed is rebuilt either way, so `ducking` behaves the same.

Source videos may be
at most 300 seconds long, and billing is per language: a 3-language call
costs three times as much as one. Dubbing has no free trial allowance — see
[Free trial](#free-trial).
costs three times as much as one. Dubbing's one free run is a 15-second
preview — see [Free trial](#free-trial).

The SDK's default wait is `DEFAULT_WAIT_TIMEOUT` (600 seconds), but the
dubbing pipeline can take much longer than that — especially with several
Expand Down Expand Up @@ -707,11 +707,14 @@ endpoints — no card required:
| --- | --- |
| 2 each | text-to-music, text-to-sfx, audio-ducking, video-analysis, proofread |
| 1 each | video-to-music, video-to-sfx, video-to-video-music, video-to-video-sfx, video-to-sound, video-to-video-sound |
| 0 | dubbing |

Dubbing bills `video duration × number of languages`, so a free run on it
would be worth far more than a free run on any other endpoint — it has no
free allowance and bills from the first call.
| 1, as a 15-second preview | dubbing |

Dubbing bills `video duration × number of languages`, so its free run is a
preview rather than a full call: the first single-language call without
scripts translates only the first 15 seconds of the video, at no charge, and
the result's `trial_preview` says so and quotes what the whole video would
cost (`full_video_cost_usd`). Several languages, scripts, and every call
after that are billed.

Once an endpoint's free runs are used up, calls to it bill at the normal rate.

Expand Down
2 changes: 1 addition & 1 deletion context7.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"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.",
"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 async 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, no free trial.",
"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.",
"A DubbingResult has no audio/video/output_url. Its results live in result.outputs, a map of language code to dubbed .mp4 URL: use result.save(lang, path) or result.save_all(dir). dubbing's video_url must be https.",
"client.video_analysis returns a creative BRIEF, not media: analyze() (not generate()), no save(). Music brief: result.segments (start/end/label/prompt) + result.variations[i].prompt; mode both (default) adds result.sfx_segments + result.sfx_prompt.",
"Pass a video_analysis variation's prompt straight to video_to_music / video_to_sfx / video_to_sound as their prompt. One of video/video_url plus optional prompt, variants_num (1-5, billed per brief), mode (both/music/sfx, same price); max 480s, 10s floor.",
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "sonilo"
version = "0.20.0"
version = "0.21.0"
description = "Official Python client for the Sonilo API"
readme = "README.md"
license = "MIT"
Expand Down
12 changes: 7 additions & 5 deletions sonilo-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -346,8 +346,8 @@ rendered.
delivered audio with your lines kept verbatim. Each one is written beside its video
(`clip.es.mp4` -> `clip.es.srt`), and one status line per language is printed. A language whose
export is blocked still gets its video — only the `.srt` is missing.
- Billing is per language, and dubbing has **no free trial runs** — see [Free trial](#free-trial)
below.
- Billing is per language. The one free run is a **15-second preview** — see
[Free trial](#free-trial) below; the preview's message is printed right under the `Wrote …` line.
- `--timeout` defaults to 7200 seconds, matching the backend's own ceiling for a dubbing job
(far longer than other commands' default, since dubbing can run well past the usual
`tasks wait --timeout 600`). If the wait still times out, the task keeps running
Expand All @@ -362,10 +362,12 @@ required:
| --- | --- |
| 2 each | text-to-music, text-to-sfx, audio-ducking, video-analysis, proofread |
| 1 each | video-to-music, video-to-sfx, video-to-video-music, video-to-video-sfx, video-to-sound, video-to-video-sound |
| 0 | dubbing |
| 1, as a 15-second preview | dubbing |

Dubbing bills `video duration × number of languages`, so a free run on it would be worth far more
than a free run on any other endpoint — it has no free allowance and bills from the first call.
Dubbing bills `video duration × number of languages`, so its free run is a preview rather than a
full call: the first single-language call without scripts translates only the first 15 seconds
of the video, at no charge, and prints what the whole video would cost. Several languages,
scripts, and every call after that are billed.

The table above is the current default. `sonilo account` prints the live numbers: the account JSON
goes to stdout, and when the account has a free-trial allowance one summary line goes to stderr:
Expand Down
4 changes: 2 additions & 2 deletions sonilo-cli/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ build-backend = "hatchling.build"

[project]
name = "sonilo-cli"
version = "0.19.0"
version = "0.20.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.20.0,<0.21"]
dependencies = ["sonilo>=0.21.0,<0.22"]
keywords = ["sonilo", "cli", "music", "sfx", "text-to-music", "video-to-music", "ai"]

[project.urls]
Expand Down
2 changes: 1 addition & 1 deletion sonilo-cli/src/sonilo_cli/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
__version__ = "0.19.0"
__version__ = "0.20.0"

__all__ = ["__version__"]
8 changes: 8 additions & 0 deletions sonilo-cli/src/sonilo_cli/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -717,6 +717,14 @@ def cmd_dubbing(client: Sonilo, args: argparse.Namespace) -> None:
for language in sorted(result.outputs):
path = result.save(language, _language_path(out, language))
_wrote(path, path.stat().st_size)
if result.trial_preview:
# The free preview translated only the first 15 seconds. Say so right
# under the "Wrote …" line, so nobody mistakes the clip for the whole
# video.
print(
result.trial_preview.get("message")
or "Free preview: only the first 15 seconds of the video were translated."
)
if not args.export_srt:
return
# The .srt lands beside its video (clip.es.mp4 -> clip.es.srt). A blocked
Expand Down
2 changes: 1 addition & 1 deletion src/sonilo/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.20.0"
__version__ = "0.21.0"
7 changes: 7 additions & 0 deletions src/sonilo/resources/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,13 @@ def parse_dubbing_result(body: Dict[str, Any]) -> "DubbingResult":
subtitles=_url_map_from(body.get("subtitles")),
subtitle_preflight=_report_map_from(body.get("subtitle_preflight")),
subtitle_export=_report_map_from(body.get("subtitle_export")),
# Passed through whole (its fields are server-owned, like the
# reports'); anything that is not an object reads as "not a
# preview" rather than raising on a paid run.
trial_preview=(
body["trial_preview"]
if isinstance(body.get("trial_preview"), dict) else None
),
duration_seconds=body.get("duration_seconds"),
cost=body.get("cost"),
error=body.get("error"),
Expand Down
9 changes: 9 additions & 0 deletions src/sonilo/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,14 @@ class DubbingResult:
`subtitles` simply lacks that language. The report values come back as
JSON of whatever type the pipeline stored, so a count or a loss may be a
string rather than a number; read them defensively.

`trial_preview` is present, in every task state, when this run was the
account's free preview: a self-serve account's first single-language call
without scripts translates only the first 15 seconds of the video, at no
charge. It carries `preview_seconds`, `source_duration_seconds`,
`trimmed`, `languages`, `full_video_cost_usd` (what translating the whole
video would cost) and a ready-made `message`. `duration_seconds` is then
the preview's length, not the source's. None on every paid run.
"""

task_id: str
Expand All @@ -636,6 +644,7 @@ class DubbingResult:
subtitles: Dict[str, str] = field(default_factory=dict)
subtitle_preflight: Dict[str, Dict[str, Any]] = field(default_factory=dict)
subtitle_export: Dict[str, Dict[str, Any]] = field(default_factory=dict)
trial_preview: Optional[Dict[str, Any]] = None
duration_seconds: Optional[float] = None
cost: Optional[float] = None
error: Optional[Dict[str, Any]] = None
Expand Down
14 changes: 14 additions & 0 deletions tests/test_dubbing.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,20 @@ def test_parse_dubbing_result_defaults_the_subtitle_maps_to_empty():
assert result.subtitle_export == {}


def test_parse_dubbing_result_carries_the_free_preview():
preview = {
"preview_seconds": 15, "source_duration_seconds": 60.0, "trimmed": True,
"languages": 1, "full_video_cost_usd": 3.49, "message": "Free preview: …",
}
result = parse_dubbing_result({**SUCCESS_BODY, "trial_preview": preview})
assert result.trial_preview == preview


def test_parse_dubbing_result_has_no_preview_on_a_paid_run():
assert parse_dubbing_result(SUCCESS_BODY).trial_preview is None
assert parse_dubbing_result({**SUCCESS_BODY, "trial_preview": "nope"}).trial_preview is None


def test_parse_dubbing_result_drops_malformed_report_entries():
result = parse_dubbing_result({
"task_id": "db1", "status": "succeeded",
Expand Down
Loading