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
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,9 +252,13 @@ poll it yourself with `client.tasks.wait(task_id, parser=parse_sound_result)`.
async call. Pass exactly one of `video` / `video_url` (`video_url` must be
**https**), plus optional `languages` — it defaults server-side to
`["zh_cn", "es", "fr"]`; supported codes are `en, zh_cn, ja, ko, pt, es, de,
fr, it, ru`. Source videos may be at most 180 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).
fr, it, ru`. The optional `ducking` boolean (default off, free) ducks the
background music/effects bed under the dubbed voice while it speaks; when off
the bed is kept at a constant level. (Note this default is the opposite of
the music endpoints' `ducking`, which is on by default.) Source videos may be
at most 180 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).

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
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.8.0"
version = "0.9.0"
description = "Official Python client for the Sonilo API"
readme = "README.md"
license = "MIT"
Expand Down
5 changes: 5 additions & 0 deletions src/sonilo/_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ def build_dubbing_parts(
video: Any,
video_url: Optional[str],
languages: Optional[List[str]],
ducking: Optional[bool] = None,
) -> Tuple[Dict[str, str], Optional[Dict[str, tuple]], bool]:
"""Build the multipart parts for POST /v1/dubbing.

Expand Down Expand Up @@ -118,6 +119,10 @@ def build_dubbing_parts(
data["video_url"] = video_url
if languages is not None:
data["languages"] = json.dumps(languages)
# Default-OFF server-side (the opposite of the music endpoints' ducking):
# omitted when unset so the server default applies.
if ducking is not None:
data["ducking"] = "true" if ducking else "false"

# Now open files (only after data is fully assembled)
files: Optional[Dict[str, tuple]] = None
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.8.0"
__version__ = "0.9.0"
21 changes: 16 additions & 5 deletions src/sonilo/resources/dubbing.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@

class Dubbing:
"""Dub one video into several target languages. Async only; the result
carries a language → dubbed-video-URL map under `outputs`."""
carries a language → dubbed-video-URL map under `outputs`.

`ducking` (default off, free) ducks the background music/effects bed
under the dubbed voice while it speaks; when off the bed is kept at a
constant level. The default is the opposite of the music endpoints'
ducking, which is on by default."""

def __init__(self, client: "Sonilo") -> None:
self._client = client
Expand All @@ -31,8 +36,9 @@ def submit(
video: Any = None,
video_url: Optional[str] = None,
languages: Optional[List[str]] = None,
ducking: Optional[bool] = None,
) -> SfxTask:
data, files, opened = build_dubbing_parts(video, video_url, languages)
data, files, opened = build_dubbing_parts(video, video_url, languages, ducking)
close_after = files["video"][1] if files is not None and opened else None
return parse_sfx_task(
self._client._post_json(PATH, data=data, files=files, close_after=close_after)
Expand All @@ -44,10 +50,13 @@ def generate(
video: Any = None,
video_url: Optional[str] = None,
languages: Optional[List[str]] = None,
ducking: Optional[bool] = None,
poll_interval: float = DEFAULT_POLL_INTERVAL,
timeout: float = DEFAULT_WAIT_TIMEOUT,
) -> DubbingResult:
task = self.submit(video=video, video_url=video_url, languages=languages)
task = self.submit(
video=video, video_url=video_url, languages=languages, ducking=ducking
)
return self._client.tasks.wait(
task.task_id,
poll_interval=poll_interval,
Expand All @@ -66,8 +75,9 @@ async def submit(
video: Any = None,
video_url: Optional[str] = None,
languages: Optional[List[str]] = None,
ducking: Optional[bool] = None,
) -> SfxTask:
data, files, opened = build_dubbing_parts(video, video_url, languages)
data, files, opened = build_dubbing_parts(video, video_url, languages, ducking)
close_after = files["video"][1] if files is not None and opened else None
return parse_sfx_task(
await self._client._post_json(
Expand All @@ -81,11 +91,12 @@ async def generate(
video: Any = None,
video_url: Optional[str] = None,
languages: Optional[List[str]] = None,
ducking: Optional[bool] = None,
poll_interval: float = DEFAULT_POLL_INTERVAL,
timeout: float = DEFAULT_WAIT_TIMEOUT,
) -> DubbingResult:
task = await self.submit(
video=video, video_url=video_url, languages=languages
video=video, video_url=video_url, languages=languages, ducking=ducking
)
return await self._client.tasks.wait(
task.task_id,
Expand Down
16 changes: 16 additions & 0 deletions tests/test_dubbing.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,22 @@ def test_submit_posts_to_v1_dubbing():
assert '["es", "fr"]' in sent


@respx.mock
def test_submit_sends_ducking_only_when_set():
route = respx.post("https://api.sonilo.com/v1/dubbing").mock(
return_value=httpx.Response(202, json=ACK)
)
with Sonilo(api_key="sk-test") as client:
client.dubbing.submit(video_url="https://x/v.mp4", ducking=True)
client.dubbing.submit(video_url="https://x/v.mp4", ducking=False)
client.dubbing.submit(video_url="https://x/v.mp4")
bodies = [unquote_plus(c.request.content.decode()) for c in route.calls]
assert "ducking=true" in bodies[0]
assert "ducking=false" in bodies[1]
# Unset → omitted entirely so the server default (off) applies.
assert "ducking" not in bodies[2]


@respx.mock
def test_generate_polls_to_a_dubbing_result():
respx.post("https://api.sonilo.com/v1/dubbing").mock(
Expand Down
Loading