Skip to content
Draft
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
4 changes: 3 additions & 1 deletion acestep/engine/sa3_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,9 +180,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"],
Expand Down
77 changes: 77 additions & 0 deletions acestep/engine/sa3_denoise_mapping.py
Original file line number Diff line number Diff line change
@@ -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)
9 changes: 5 additions & 4 deletions acestep/streaming/sa3_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,11 @@ def sa3_knob_specs() -> 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(
Expand Down
53 changes: 53 additions & 0 deletions tests/unit/test_sa3_denoise_mapping.py
Original file line number Diff line number Diff line change
@@ -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)