From 420cf605fd8514b2ad2de2e268bdd3faca2c30c1 Mon Sep 17 00:00:00 2001 From: Spencer Qian Date: Sat, 1 Aug 2026 04:16:34 -0700 Subject: [PATCH 1/2] feat: keep_original_sound on video_to_video_music and video_to_video_sound (0.11.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The server flipped both endpoints' defaults, so a request that does not set keep_original_sound now returns the generated audio alone. This adds the parameter that opts back in. keep_original_sound picks the voice source and supersedes preserve_speech; ducking independently picks how that voice is combined with the generated audio. Both are emitted only when explicitly passed, so each server default stands on its own — and they run in opposite directions: ducking is default-ON, keep_original_sound default-OFF. The field is video-only, following the pattern already used for output_format in reverse: keyword-only with a None default on the shared build_v2s_parts, and the resource that must not send it simply never passes it. Tests assert that on the public signatures, so adding it to VideoToSound by reflex fails. Also corrects two README claims and two SoundOutput/SoundResult docstrings that described the old default and the old music_processed condition. --- README.md | 24 +++- pyproject.toml | 2 +- src/sonilo/_requests.py | 36 ++++-- src/sonilo/_version.py | 2 +- src/sonilo/resources/video_to_video_music.py | 8 ++ src/sonilo/resources/video_to_video_sound.py | 8 ++ src/sonilo/types.py | 15 ++- tests/test_keep_original_sound.py | 115 +++++++++++++++++++ 8 files changed, 187 insertions(+), 23 deletions(-) create mode 100644 tests/test_keep_original_sound.py diff --git a/README.md b/README.md index 506fe9a..9fed599 100644 --- a/README.md +++ b/README.md @@ -154,12 +154,15 @@ audio muxed in — not just an audio file. Both endpoints are async; `generate() submits and polls to a `VideoResult`: ```python -# By default the returned video keeps the source's speech with the music -# ducked under it — pass ducking=False for music-only audio. +# By default the returned video's audio is the generated music ALONE — the +# source's own audio is removed. keep_original_sound=True keeps the whole +# source track with the music under it; preserve_speech=True keeps only the +# isolated speech. Add ducking=False to either for a static mix instead of a +# dynamic duck. music = client.video_to_video_music.generate( video="my_video.mp4", # path, bytes, open file, or use video_url= prompt="cinematic orchestral swell", - preserve_speech=True, + keep_original_sound=True, # segments=[{"start": 0, "prompt": "sparse pads"}, # {"start": 30, "prompt": "add drums"}], ) @@ -227,8 +230,19 @@ result.save_stem("music.m4a", which="music") result.save_stem("sfx.wav", which="sfx") ``` -`preserve_speech=True` keeps the speech from the source video, and `ducking` -(on by default) dips the music under it — pass `ducking=False` to opt out. +Two independent knobs decide what happens to the source's own audio. +**`keep_original_sound`** picks the voice source: pass `True` to keep the whole +source track, or `preserve_speech=True` to keep only the isolated speech. Both +default to off, so `video_to_video_sound` by default returns the generated +music and effects **alone** — and with no voice source there is no processed +track, so the default result carries no `music_processed` stem. +**`ducking`** (on by default) picks how that voice and the generated bed are +combined: leave it for the dynamic duck, or pass `ducking=False` for a static +voice-forward mix. It has no effect when neither voice flag is set. +`keep_original_sound` supersedes `preserve_speech`, and is accepted only by +`video_to_video_sound` — `video_to_sound` returns generated audio, so there is +no source picture whose audio could be preserved. + `segments` takes the same `{"start", "end", "prompt"}` list as `video_to_sfx`. Input videos may be at most 180 seconds long. diff --git a/pyproject.toml b/pyproject.toml index e4a8cf8..ca2d1ad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "sonilo" -version = "0.10.0" +version = "0.11.0" description = "Official Python client for the Sonilo API" readme = "README.md" license = "MIT" diff --git a/src/sonilo/_requests.py b/src/sonilo/_requests.py index b1ffd20..b639298 100644 --- a/src/sonilo/_requests.py +++ b/src/sonilo/_requests.py @@ -209,13 +209,19 @@ def build_v2v_music_parts( variants_num: Optional[int] = None, segments: Optional[List[Segment]] = None, ducking: Optional[bool] = None, + keep_original_sound: Optional[bool] = None, ) -> Tuple[Dict[str, str], Optional[Dict[str, tuple]], bool]: # video-to-video-music is 202/async-only by design (there is no streaming # mode to fall back to), so variants_num travels straight through with no # mode guard — unlike text-to-music/video-to-music. data, files, opened = build_v2m_parts(video, video_url, prompt, segments) - # Only emitted when explicitly passed: `ducking` is default-ON - # server-side, so an unset value must not go out as "false". + # Every boolean is emitted only when explicitly passed, so each server + # default stands on its own — and they point in opposite directions: + # `ducking` is default-ON so an unset value must not go out as "false", + # while `keep_original_sound` is default-OFF so an unset value must not go + # out as "true". + if keep_original_sound is not None: + data["keep_original_sound"] = "true" if keep_original_sound else "false" if ducking is not None: data["ducking"] = "true" if ducking else "false" if preserve_speech is not None: @@ -247,27 +253,37 @@ def build_v2s_parts( ducking: Optional[bool], variants_num: Optional[int] = None, output_format: Optional[str] = None, + keep_original_sound: Optional[bool] = None, ) -> Tuple[Dict[str, str], Optional[Dict[str, tuple]], bool]: """Multipart parts shared by /v1/video-to-sound and /v1/video-to-video-sound. - `output_format` is the one field they do not share: only the audio - endpoint accepts it, since video-to-video-sound always muxes the mix into - an mp4. It is keyword-only with a None default here, and the - VideoToVideoSound resource simply never passes it. + Two fields they do NOT share, one in each direction. Both are keyword-only + with a None default here, and the resource that must not send one simply + never passes it: + + * `output_format` is audio-only — video-to-video-sound always muxes the mix + into an mp4 — so VideoToVideoSound never passes it. + * `keep_original_sound` is video-only — it only means something when the + deliverable is a video whose own audio could be preserved — so + VideoToSound never passes it. These endpoints take `music_prompt`/`sfx_prompt` instead of a single `prompt`, so build_v2m_parts is called with prompt=None. Booleans are only - emitted when explicitly passed: `ducking` is default-ON server-side, so an - unset value must not go out as "false". Both endpoints are async-only - (202 + poll), so — like video-to-video-music — variants_num travels - straight through with no mode guard. + emitted when explicitly passed, and the two defaults run opposite ways: + `ducking` is default-ON server-side so an unset value must not go out as + "false", while `keep_original_sound` is default-OFF so an unset value must + not go out as "true". Both endpoints are async-only (202 + poll), so — like + video-to-video-music — variants_num travels straight through with no mode + guard. """ data, files, opened = build_v2m_parts(video, video_url, None, segments) if music_prompt is not None: data["music_prompt"] = music_prompt if sfx_prompt is not None: data["sfx_prompt"] = sfx_prompt + if keep_original_sound is not None: + data["keep_original_sound"] = "true" if keep_original_sound else "false" if preserve_speech is not None: data["preserve_speech"] = "true" if preserve_speech else "false" if ducking is not None: diff --git a/src/sonilo/_version.py b/src/sonilo/_version.py index 61fb31c..ae6db5f 100644 --- a/src/sonilo/_version.py +++ b/src/sonilo/_version.py @@ -1 +1 @@ -__version__ = "0.10.0" +__version__ = "0.11.0" diff --git a/src/sonilo/resources/video_to_video_music.py b/src/sonilo/resources/video_to_video_music.py index c0b053b..c8e7cc5 100644 --- a/src/sonilo/resources/video_to_video_music.py +++ b/src/sonilo/resources/video_to_video_music.py @@ -29,6 +29,7 @@ def submit( video_url: Optional[str] = None, prompt: Optional[str] = None, segments: Optional[List[Segment]] = None, + keep_original_sound: Optional[bool] = None, ducking: Optional[bool] = None, preserve_speech: Optional[bool] = None, isolate_vocals: Optional[bool] = None, @@ -49,6 +50,7 @@ def submit( variants_num, segments=segments, ducking=ducking, + keep_original_sound=keep_original_sound, ) close_after = files["video"][1] if files is not None and opened else None return parse_sfx_task( @@ -62,6 +64,7 @@ def generate( video_url: Optional[str] = None, prompt: Optional[str] = None, segments: Optional[List[Segment]] = None, + keep_original_sound: Optional[bool] = None, ducking: Optional[bool] = None, preserve_speech: Optional[bool] = None, isolate_vocals: Optional[bool] = None, @@ -75,6 +78,7 @@ def generate( prompt=prompt, segments=segments, ducking=ducking, + keep_original_sound=keep_original_sound, preserve_speech=preserve_speech, isolate_vocals=isolate_vocals, variants_num=variants_num, @@ -98,6 +102,7 @@ async def submit( video_url: Optional[str] = None, prompt: Optional[str] = None, segments: Optional[List[Segment]] = None, + keep_original_sound: Optional[bool] = None, ducking: Optional[bool] = None, preserve_speech: Optional[bool] = None, isolate_vocals: Optional[bool] = None, @@ -112,6 +117,7 @@ async def submit( variants_num, segments=segments, ducking=ducking, + keep_original_sound=keep_original_sound, ) close_after = files["video"][1] if files is not None and opened else None return parse_sfx_task( @@ -127,6 +133,7 @@ async def generate( video_url: Optional[str] = None, prompt: Optional[str] = None, segments: Optional[List[Segment]] = None, + keep_original_sound: Optional[bool] = None, ducking: Optional[bool] = None, preserve_speech: Optional[bool] = None, isolate_vocals: Optional[bool] = None, @@ -140,6 +147,7 @@ async def generate( prompt=prompt, segments=segments, ducking=ducking, + keep_original_sound=keep_original_sound, preserve_speech=preserve_speech, isolate_vocals=isolate_vocals, variants_num=variants_num, diff --git a/src/sonilo/resources/video_to_video_sound.py b/src/sonilo/resources/video_to_video_sound.py index f60e41a..b476502 100644 --- a/src/sonilo/resources/video_to_video_sound.py +++ b/src/sonilo/resources/video_to_video_sound.py @@ -30,6 +30,7 @@ def submit( music_prompt: Optional[str] = None, sfx_prompt: Optional[str] = None, segments: Optional[List[SfxSegment]] = None, + keep_original_sound: Optional[bool] = None, preserve_speech: Optional[bool] = None, ducking: Optional[bool] = None, variants_num: Optional[int] = None, @@ -44,6 +45,7 @@ def submit( data, files, opened = build_v2s_parts( video, video_url, music_prompt, sfx_prompt, segments, preserve_speech, ducking, variants_num, + keep_original_sound=keep_original_sound, ) close_after = files["video"][1] if files is not None and opened else None return parse_sfx_task( @@ -58,6 +60,7 @@ def generate( music_prompt: Optional[str] = None, sfx_prompt: Optional[str] = None, segments: Optional[List[SfxSegment]] = None, + keep_original_sound: Optional[bool] = None, preserve_speech: Optional[bool] = None, ducking: Optional[bool] = None, variants_num: Optional[int] = None, @@ -70,6 +73,7 @@ def generate( music_prompt=music_prompt, sfx_prompt=sfx_prompt, segments=segments, + keep_original_sound=keep_original_sound, preserve_speech=preserve_speech, ducking=ducking, variants_num=variants_num, @@ -94,6 +98,7 @@ async def submit( music_prompt: Optional[str] = None, sfx_prompt: Optional[str] = None, segments: Optional[List[SfxSegment]] = None, + keep_original_sound: Optional[bool] = None, preserve_speech: Optional[bool] = None, ducking: Optional[bool] = None, variants_num: Optional[int] = None, @@ -101,6 +106,7 @@ async def submit( data, files, opened = build_v2s_parts( video, video_url, music_prompt, sfx_prompt, segments, preserve_speech, ducking, variants_num, + keep_original_sound=keep_original_sound, ) close_after = files["video"][1] if files is not None and opened else None return parse_sfx_task( @@ -117,6 +123,7 @@ async def generate( music_prompt: Optional[str] = None, sfx_prompt: Optional[str] = None, segments: Optional[List[SfxSegment]] = None, + keep_original_sound: Optional[bool] = None, preserve_speech: Optional[bool] = None, ducking: Optional[bool] = None, variants_num: Optional[int] = None, @@ -129,6 +136,7 @@ async def generate( music_prompt=music_prompt, sfx_prompt=sfx_prompt, segments=segments, + keep_original_sound=keep_original_sound, preserve_speech=preserve_speech, ducking=ducking, variants_num=variants_num, diff --git a/src/sonilo/types.py b/src/sonilo/types.py index 085cf59..2262af2 100644 --- a/src/sonilo/types.py +++ b/src/sonilo/types.py @@ -365,8 +365,9 @@ async def _adownload_to(url: str, path: Union[str, Path], timeout: float) -> Pat class SoundOutput: """One entry of a video-to-sound / video-to-video-sound task's `outputs` array — one per variant (`variants_num`), sorted by `variant_index`. - `music_processed` is present only when preserve_speech or ducking - altered that variant's music bed.""" + `music_processed` is present only when a voice source was kept, i.e. with + keep_original_sound or preserve_speech. On video-to-video-sound neither is + on by default, so the default result carries no such stem.""" variant_index: int output_url: str @@ -386,10 +387,12 @@ class SoundResult: than a media object, because these endpoints render exactly one artifact whose kind is announced by `output_type` ("audio" for /v1/video-to-sound, "video" for /v1/video-to-video-sound). `music`, `music_processed` and `sfx` - are the individual stems; `music_processed` is present only when - preserve_speech or ducking altered the music bed. All of the above stay - aliases for `outputs[0]`'s corresponding fields, even when - `variants_num > 1` produced several variants. + are the individual stems; `music_processed` is present only when a voice + source was kept, i.e. with keep_original_sound or preserve_speech — on + video-to-video-sound neither is on by default, so the default result + carries no such stem. All of the above stay aliases for `outputs[0]`'s + corresponding fields, even when `variants_num > 1` produced several + variants. """ task_id: str diff --git a/tests/test_keep_original_sound.py b/tests/test_keep_original_sound.py new file mode 100644 index 0000000..5905209 --- /dev/null +++ b/tests/test_keep_original_sound.py @@ -0,0 +1,115 @@ +"""Covers `keep_original_sound` on the two video-output endpoints. + +The server flipped both endpoints' defaults: a request that does not set this +flag now returns the generated audio alone, where it previously returned the +source video's speech with the generated music ducked underneath. These tests +pin the two things that keep the SDK honest about that — the flag is omitted +from the wire unless explicitly passed (so an unset value never becomes an +explicit "true"), and it never reaches the audio-only endpoint at all. +""" +import inspect + +from sonilo._requests import build_v2s_parts, build_v2v_music_parts +from sonilo.resources.video_to_sound import AsyncVideoToSound, VideoToSound +from sonilo.resources.video_to_video_music import ( + AsyncVideoToVideoMusic, + VideoToVideoMusic, +) +from sonilo.resources.video_to_video_sound import ( + AsyncVideoToVideoSound, + VideoToVideoSound, +) + + +# --- video-to-video-music ---------------------------------------------------- + +def test_v2v_music_omits_keep_original_sound_when_none(): + """Default-OFF server-side, so an unset value must not go out as "true" — + and must not go out as "false" either, which would pin a default the server + owns.""" + data, _, _ = build_v2v_music_parts(None, "https://x/v.mp4", None, None, None) + assert "keep_original_sound" not in data + + +def test_v2v_music_sends_keep_original_sound(): + data, _, _ = build_v2v_music_parts( + None, "https://x/v.mp4", None, None, None, + keep_original_sound=True, + ) + assert data["keep_original_sound"] == "true" + + +def test_v2v_music_static_mix_row(): + """`keep_original_sound` + `ducking=False` is the new static-mix row: the + whole original track, mixed at a fixed offset instead of ducked.""" + data, _, _ = build_v2v_music_parts( + None, "https://x/v.mp4", None, None, None, + keep_original_sound=True, ducking=False, + ) + assert data["keep_original_sound"] == "true" + assert data["ducking"] == "false" + + +def test_v2v_music_sends_both_flags_leaving_precedence_to_the_server(): + """Deliberately not resolved client-side: the server supersedes + preserve_speech with keep_original_sound and logs that it did. Resolving it + here would hide the override and desync from the other SDKs.""" + data, _, _ = build_v2v_music_parts( + None, "https://x/v.mp4", None, True, None, + keep_original_sound=True, + ) + assert data["keep_original_sound"] == "true" + assert data["preserve_speech"] == "true" + + +def test_v2v_music_explicit_false_is_sent(): + """An explicit False is a real request ("do not keep it"), distinct from + unset, so it does go on the wire.""" + data, _, _ = build_v2v_music_parts( + None, "https://x/v.mp4", None, None, None, + keep_original_sound=False, + ) + assert data["keep_original_sound"] == "false" + + +# --- video-to-video-sound ---------------------------------------------------- + +def test_v2s_omits_keep_original_sound_when_none(): + data, _, _ = build_v2s_parts( + None, "https://x/v.mp4", None, None, None, None, None, + ) + assert "keep_original_sound" not in data + + +def test_v2s_sends_keep_original_sound(): + data, _, _ = build_v2s_parts( + None, "https://x/v.mp4", None, None, None, None, None, + keep_original_sound=True, + ) + assert data["keep_original_sound"] == "true" + + +# --- the audio endpoint must never expose or send it ------------------------- + +def test_audio_endpoint_does_not_expose_keep_original_sound(): + """`keep_original_sound` is video-only: it only means something when the + deliverable is a video whose own audio could be preserved. The audio + resource simply never passes it to the shared builder — the mirror of how + `output_format` is kept off video-to-video-sound. Asserted on the public + signatures so adding it by reflex would fail here.""" + for cls in (VideoToSound, AsyncVideoToSound): + for method in ("submit", "generate"): + params = inspect.signature(getattr(cls, method)).parameters + assert "keep_original_sound" not in params, f"{cls.__name__}.{method}" + + +def test_video_endpoints_do_expose_keep_original_sound(): + for cls in ( + VideoToVideoMusic, + AsyncVideoToVideoMusic, + VideoToVideoSound, + AsyncVideoToVideoSound, + ): + for method in ("submit", "generate"): + params = inspect.signature(getattr(cls, method)).parameters + assert "keep_original_sound" in params, f"{cls.__name__}.{method}" From 83fceafc13a028e0106f2211b85bbf25be859b4b Mon Sep 17 00:00:00 2001 From: Spencer Qian Date: Sat, 1 Aug 2026 04:27:34 -0700 Subject: [PATCH 2/2] fix: bump sonilo-cli for the 0.11 core, and add --keep-original-sound to it CI caught that this repo ships three packages, not one: sonilo-cli pinned sonilo>=0.10.0,<0.11, so bumping the core to 0.11.0 made the editable install unresolvable. Widened to >=0.11.0,<0.12 and bumped sonilo-cli to 0.8.0. sonilo-video-kit's >=0.3,<1.0 already covers it. That also surfaced that the Python CLI needed the same treatment as the JS one: --keep-original-sound on both video-to-video-music and video-to-video-sound, plus --no-ducking on video-to-video-music, which the SDK supported but the CLI never exposed -- without it the static-mix combination was unreachable. _run_sound is shared by video-to-sound and video-to-video-sound, so the flag is forwarded only when the parser actually defined it: video_to_sound.generate() does not accept the keyword at all, so it must be omitted rather than passed as None. --- sonilo-cli/pyproject.toml | 4 +-- sonilo-cli/src/sonilo_cli/__init__.py | 2 +- sonilo-cli/src/sonilo_cli/__main__.py | 40 ++++++++++++++++++++++++--- 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/sonilo-cli/pyproject.toml b/sonilo-cli/pyproject.toml index 6755a3e..9e6c5c6 100644 --- a/sonilo-cli/pyproject.toml +++ b/sonilo-cli/pyproject.toml @@ -4,13 +4,13 @@ build-backend = "hatchling.build" [project] name = "sonilo-cli" -version = "0.7.0" +version = "0.8.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.10.0,<0.11"] +dependencies = ["sonilo>=0.11.0,<0.12"] keywords = ["sonilo", "cli", "music", "sfx", "text-to-music", "video-to-music", "ai"] [project.urls] diff --git a/sonilo-cli/src/sonilo_cli/__init__.py b/sonilo-cli/src/sonilo_cli/__init__.py index 47b9ec7..32480aa 100644 --- a/sonilo-cli/src/sonilo_cli/__init__.py +++ b/sonilo-cli/src/sonilo_cli/__init__.py @@ -1,3 +1,3 @@ -__version__ = "0.7.0" +__version__ = "0.8.0" __all__ = ["__version__"] diff --git a/sonilo-cli/src/sonilo_cli/__main__.py b/sonilo-cli/src/sonilo_cli/__main__.py index e53098a..e9efbed 100644 --- a/sonilo-cli/src/sonilo_cli/__main__.py +++ b/sonilo-cli/src/sonilo_cli/__main__.py @@ -331,6 +331,13 @@ def _stem_path(out: str, stem: str, media: Any) -> str: def _run_sound(client: Sonilo, args: argparse.Namespace, resource: Any, default_ext: str) -> None: out = args.output if args.output is not None else f"output.{default_ext}" + # video-to-video-sound only. Its parser is the only one that defines the + # flag, and video_to_sound.generate() does not accept the keyword at all, + # so it has to be omitted here rather than forwarded as None — the same + # split the SDK enforces by never passing it from the audio resource. + extra: Dict[str, Any] = {} + if getattr(args, "keep_original_sound", False): + extra["keep_original_sound"] = True result = resource.generate( video=args.video, video_url=args.video_url, @@ -340,6 +347,7 @@ def _run_sound(client: Sonilo, args: argparse.Namespace, resource: Any, default_ preserve_speech=True if args.preserve_speech else None, ducking=False if args.no_ducking else None, variants_num=args.variants, + **extra, ) multi = args.variants is not None and args.variants > 1 and len(result.outputs) > 1 if not multi: @@ -395,7 +403,12 @@ def cmd_video_to_video_music(client: Sonilo, args: argparse.Namespace) -> None: client.video_to_video_music, prompt=args.prompt, # Unset flags forward None, not False, so the server default stands — - # same reasoning as --no-ducking on the sound commands. + # same reasoning as --no-ducking on the sound commands. The two + # defaults run opposite ways: ducking is default-ON server-side and + # keep_original_sound default-OFF, so each is only ever sent to change + # the default, never to restate it. + keep_original_sound=True if args.keep_original_sound else None, + ducking=False if args.no_ducking else None, preserve_speech=True if args.preserve_speech else None, isolate_vocals=True if args.isolate_vocals else None, variants_num=args.variants, @@ -612,8 +625,18 @@ def build_parser() -> argparse.ArgumentParser: _add_global(p_v2vm) _add_video_source(p_v2vm) p_v2vm.add_argument("--prompt", default=None, help="Optional creative direction.") + p_v2vm.add_argument("--keep-original-sound", dest="keep_original_sound", + action="store_true", + help="Keep the source video's whole original audio, with the " + "generated music under it. Off by default, so by default " + "the result's audio is the generated music alone. " + "Supersedes --preserve-speech.") + p_v2vm.add_argument("--no-ducking", dest="no_ducking", action="store_true", + help="Combine the voice and the music as a static mix instead " + "of a dynamic duck. No effect without " + "--keep-original-sound or --preserve-speech.") p_v2vm.add_argument("--preserve-speech", dest="preserve_speech", action="store_true", - help="Keep source speech in the mix.") + help="Keep only the source's isolated speech in the mix.") # Same aliasing as video-to-music, and here the endpoint collapses the two # into a single boolean before it reaches the model (video_to_video.py: # `keep_speech = bool(preserve_speech) or bool(isolate_vocals)`), with no @@ -644,10 +667,19 @@ def build_parser() -> argparse.ArgumentParser: p_v2vsd.add_argument("--sfx-prompt", dest="sfx_prompt", default=None, help="Optional creative direction for the sound effects.") _add_segments(p_v2vsd, SFX_SHAPE) + p_v2vsd.add_argument("--keep-original-sound", dest="keep_original_sound", + action="store_true", + help="Keep the source video's whole original audio, with the " + "generated music and effects under it. Off by default, so " + "by default the result's audio is the generated audio " + "alone and there is no music_processed stem. Supersedes " + "--preserve-speech. This command only.") p_v2vsd.add_argument("--preserve-speech", dest="preserve_speech", action="store_true", - help="Keep source speech in the mix.") + help="Keep only the source's isolated speech in the mix.") p_v2vsd.add_argument("--no-ducking", dest="no_ducking", action="store_true", - help="Disable automatic ducking (on by default server-side).") + help="Combine the voice and the generated bed as a static mix " + "instead of a dynamic duck. No effect without " + "--keep-original-sound or --preserve-speech.") p_v2vsd.add_argument("--stem", dest="stems", action="append", choices=_SOUND_STEMS, default=None, help="Also save an individual stem. Repeatable.") p_v2vsd.add_argument("--output", default=None, help="Where to save the combined video.")