From ca16f4a8dbc1b7b1d4041a12abdd67d9b509e180 Mon Sep 17 00:00:00 2001 From: RyanOnTheInside <7623207+ryanontheinside@users.noreply.github.com> Date: Mon, 17 Aug 2026 11:02:30 -0400 Subject: [PATCH 1/2] fix(sa3): pin the vendored SA3 to the monotonic audio-to-audio schedule Move SA3_VENDOR_SHA to the combined fork pin (current Stability main + the TRT integer-attribute fix + the normalized-then-scaled schedule ordering). The schedule change is host-side Python only; the SAME-L plugin source is unchanged between the previous pin and this one, so SA3_SAME_L_PLUGIN_REVISION stays put and the existing versioned decoder engine is reused. --- acestep/engine/sa3_helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acestep/engine/sa3_helpers.py b/acestep/engine/sa3_helpers.py index c6ada2b3..9687f3b0 100644 --- a/acestep/engine/sa3_helpers.py +++ b/acestep/engine/sa3_helpers.py @@ -25,7 +25,7 @@ # DEMON tracks this fork branch until the SA3 TensorRT/FP8 producer work # merges upstream. The hash is the reproducibility boundary for installs. SA3_VENDOR_URL = "https://github.com/ryanontheinside/stable-audio-3" -SA3_VENDOR_SHA = "c07698548567fe6f163806f692d282bbaa57aba3" +SA3_VENDOR_SHA = "960da1f8cbe205ab3b702edbfabd91113ab22473" # Revision that last changed the source compiled into the SAME-L TensorRT # engine. Keep this stable across vendor bumps that only touch other code. SA3_SAME_L_PLUGIN_REVISION = "c07698548567fe6f163806f692d282bbaa57aba3" From 384ebc8b8c0251b9a7afe0500865232b69d11239 Mon Sep 17 00:00:00 2001 From: RyanOnTheInside <7623207+ryanontheinside@users.noreply.github.com> Date: Tue, 18 Aug 2026 05:55:29 -0400 Subject: [PATCH 2/2] feat(sa3): spread denoise changes across the product dial (#321) * Pin SA3 to current upstream and version SAME-L plugins Move the managed SA3 source from the old FP8 development lineage to current upstream plus the rebased TensorRT integer-attribute fix. The current upstream SAME-L plugin bakes its AOT/JIT implementation into the serialized engine. Include the plugin revision and implementation choice in the engine name and metadata so stale decoders are ignored instead of being paired with incompatible plugin code. Keep the plugin revision stable across unrelated vendor bumps so they do not force a 1.2 GB decoder rebuild. Existing engines remain beside the new artifact for rollback. Also accept git worktrees as DEMON_SA3_SRC overrides. * fix(sa3): pin the vendored SA3 to the monotonic audio-to-audio schedule Move SA3_VENDOR_SHA to the combined fork pin (current Stability main + the TRT integer-attribute fix + the normalized-then-scaled schedule ordering). The schedule change is host-side Python only; the SAME-L plugin source is unchanged between the previous pin and this one, so SA3_SAME_L_PLUGIN_REVISION stays put and the existing versioned decoder engine is reused. * Map the SA3 denoise knob onto measured audio change Keep the upstream-corrected monotonic schedule, but map the product dial to entry sigma using the inverse of the measured change curve. This spreads useful movement across the control instead of concentrating it near the top. The mapping is deliberately presented as a product choice, not another sampler fix: its referee is validated for ordering rather than equal perceptibility, and one global curve moves different material at different rates. DEMON_SA3_DENOISE_MAPPING=identity bypasses it while retaining the upstream schedule correction. --- acestep/engine/sa3_context.py | 4 +- acestep/engine/sa3_denoise_mapping.py | 77 ++++++++++++++++++++++++++ acestep/streaming/sa3_backend.py | 9 +-- tests/unit/test_sa3_denoise_mapping.py | 53 ++++++++++++++++++ 4 files changed, 138 insertions(+), 5 deletions(-) create mode 100644 acestep/engine/sa3_denoise_mapping.py create mode 100644 tests/unit/test_sa3_denoise_mapping.py diff --git a/acestep/engine/sa3_context.py b/acestep/engine/sa3_context.py index e2232c24..3f573a64 100644 --- a/acestep/engine/sa3_context.py +++ b/acestep/engine/sa3_context.py @@ -187,9 +187,11 @@ def make_schedule_builder( def _builder(denoise: float) -> torch.Tensor: import stable_audio_3.inference.sampling as sampling + from .sa3_denoise_mapping import map_denoise_to_entry_sigma + schedule = sampling.build_schedule( steps=int(steps), - sigma_max=float(denoise), + sigma_max=map_denoise_to_entry_sigma(float(denoise)), dist_shift=prepared["dist_shift"], effective_seq_len=prepared["effective_seq_len"], fallback_seq_len=prepared["fallback_seq_len"], diff --git a/acestep/engine/sa3_denoise_mapping.py b/acestep/engine/sa3_denoise_mapping.py new file mode 100644 index 00000000..c0e8cbd6 --- /dev/null +++ b/acestep/engine/sa3_denoise_mapping.py @@ -0,0 +1,77 @@ +"""Optional product mapping for the Stable Audio 3 denoise control. + +The corrected upstream schedule makes ``sigma_max`` mathematically monotonic, +but measured audio change is still concentrated near the top of its range. In +a 41-point sweep over nine clips, less than one fifth of the available change +occurred below sigma 0.70. The knot table below inverts that measured curve so +equal control movements target roughly equal movements in the referee score. + +The referee combines harmonic and rhythmic change and reproduced two separate +listening-order judgments that a loudness-based measure did not. It validates +the ordering and the location of the dead region, not a claim that every step +is equally perceptible. This is also one global mapping: sparse acoustic input +can move faster than dense electronic material. + +Set ``DEMON_SA3_DENOISE_MAPPING=identity`` to bypass the product mapping while +retaining the upstream monotonic-schedule bugfix. +""" + +from __future__ import annotations + +import os +import typing as tp + +__all__ = [ + "denoise_mapping_mode", + "dial_to_entry_sigma", + "map_denoise_to_entry_sigma", +] + + +# Dial position -> entry sigma, obtained by inverting the measured change curve. +# Both endpoints remain exact: zero preserves the source and one starts from +# pure noise. Values between measured knots interpolate linearly. +_CALIBRATION: tp.Sequence[tuple[float, float]] = ( + (0.00, 0.0000), + (0.10, 0.3786), + (0.20, 0.5762), + (0.30, 0.6853), + (0.40, 0.7321), + (0.50, 0.7683), + (0.65, 0.8376), + (0.80, 0.8843), + (1.00, 1.0000), +) + + +def denoise_mapping_mode() -> str: + """Return ``calibrated`` (default) or the rollback mode ``identity``.""" + mode = os.environ.get("DEMON_SA3_DENOISE_MAPPING", "calibrated").strip().lower() + if mode not in ("calibrated", "identity"): + raise ValueError( + "DEMON_SA3_DENOISE_MAPPING must be calibrated|identity, " + f"got {mode!r}" + ) + return mode + + +def dial_to_entry_sigma(dial: float) -> float: + """Interpolate the measured monotonic mapping, clamped to ``[0, 1]``.""" + position = min(max(float(dial), 0.0), 1.0) + if position <= _CALIBRATION[0][0]: + return _CALIBRATION[0][1] + + for (p0, s0), (p1, s1) in zip(_CALIBRATION, _CALIBRATION[1:]): + if position <= p1: + fraction = (position - p0) / (p1 - p0) + return s0 + (s1 - s0) * fraction + + return _CALIBRATION[-1][1] + + +def map_denoise_to_entry_sigma(dial: float) -> float: + """Apply the selected product mapping without changing endpoint semantics.""" + clamped = min(max(float(dial), 0.0), 1.0) + if denoise_mapping_mode() == "identity": + return clamped + return dial_to_entry_sigma(clamped) diff --git a/acestep/streaming/sa3_backend.py b/acestep/streaming/sa3_backend.py index 4c6e0a09..0fd69482 100644 --- a/acestep/streaming/sa3_backend.py +++ b/acestep/streaming/sa3_backend.py @@ -143,10 +143,11 @@ def sa3_knob_specs(loras: tuple | list = ()) -> list: KnobSpec( "sa3_denoise", default=1.0, max_val=1.0, group="sa3", description=( - "SA3 init_noise_level: fresh-noise vs source-anchor mix " - "at slot init (1.0 = generate from pure noise, lower = " - "closer cover of the source). Distinct from ACE's " - "'denoise' (k1 strength), hence the prefix." + "Measured SA3 audio-change amount: 1.0 generates from pure " + "noise, while lower values stay progressively closer to the " + "source. Mapped onto init_noise_level so useful change is " + "spread across the dial. Distinct from ACE's 'denoise' " + "(k1 strength), hence the prefix." ), ), KnobSpec( diff --git a/tests/unit/test_sa3_denoise_mapping.py b/tests/unit/test_sa3_denoise_mapping.py new file mode 100644 index 00000000..7e0db2fa --- /dev/null +++ b/tests/unit/test_sa3_denoise_mapping.py @@ -0,0 +1,53 @@ +import pytest + +from acestep.engine.sa3_denoise_mapping import ( + denoise_mapping_mode, + dial_to_entry_sigma, + map_denoise_to_entry_sigma, +) + + +def test_mapping_is_monotonic_and_preserves_endpoints(): + values = [dial_to_entry_sigma(i / 100) for i in range(101)] + + assert values[0] == 0.0 + assert values[-1] == 1.0 + assert all(a <= b for a, b in zip(values, values[1:])) + + +def test_mapping_matches_measured_knots(): + assert dial_to_entry_sigma(0.1) == pytest.approx(0.3786) + assert dial_to_entry_sigma(0.5) == pytest.approx(0.7683) + assert dial_to_entry_sigma(0.8) == pytest.approx(0.8843) + + +def test_mapping_interpolates_between_knots(): + midpoint = (0.7683 + 0.8376) / 2 + assert dial_to_entry_sigma(0.575) == pytest.approx(midpoint) + + +def test_mapping_clamps_inputs(): + assert dial_to_entry_sigma(-1) == 0.0 + assert dial_to_entry_sigma(2) == 1.0 + + +def test_default_mapping_lifts_the_measured_dead_region(monkeypatch): + monkeypatch.delenv("DEMON_SA3_DENOISE_MAPPING", raising=False) + + assert denoise_mapping_mode() == "calibrated" + assert map_denoise_to_entry_sigma(0.2) > 0.5 + assert map_denoise_to_entry_sigma(0.5) > 0.7 + + +def test_identity_mode_is_a_bugfix_preserving_rollback(monkeypatch): + monkeypatch.setenv("DEMON_SA3_DENOISE_MAPPING", "identity") + + assert map_denoise_to_entry_sigma(0.2) == 0.2 + assert map_denoise_to_entry_sigma(0.5) == 0.5 + + +def test_invalid_mode_fails_loudly(monkeypatch): + monkeypatch.setenv("DEMON_SA3_DENOISE_MAPPING", "mystery") + + with pytest.raises(ValueError, match=r"calibrated\|identity"): + map_denoise_to_entry_sigma(0.5)