* fix(audio): stop near-silent renders becoming "blank noise" + guard archetypes Root cause of the blank/hiss voices: normalize_audio peak-normalized to -2 dBFS whenever max(|audio|) > 0. When the model emits a near-silent clip (peak at the noise floor, e.g. 1e-4), that applies thousands of × of gain and lifts the noise floor to full scale — silence turned into loud hiss. This affected every generation path (clone/dub/design/archetypes), which is why "some voices" came out as blank noise. - services/audio_dsp.py: normalize_audio gains a -50 dBFS silence floor. At or below it the audio is left untouched (stays inaudible) instead of being amplified. Real speech — even a whisper — peaks well above the floor, so normal output is unchanged. - api/routers/archetypes.py: after rendering, _is_blank_audio() detects a dead clip (empty / non-finite / peak < 0.02 — a real normalized clip peaks ~0.79). The render retries once with a different seed, then fails loudly (503 via the existing handlers) so a blank preview or voice profile is never cached/saved. Also extracts the script with a non-empty fallback. - core/archetypes.py: _build never falls back to an empty script (empty text synthesizes to silence). Tests (tests/, runs in CI): normalize_audio doesn't amplify silence but still normalizes real audio to target; _is_blank_audio flags dead renders and passes real audio; every archetype carries a non-empty sample script. Verified: full tests/ suite 601 passed incl. 8 new (the 2 test_supertonic3 failures are pre-existing on main — local .venv engine/license state, green in CI — and unrelated to this diff). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(gallery): static log message in blank-render retry (clears py/clear-text-logging) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
43 lines
1.6 KiB
Python
43 lines
1.6 KiB
Python
"""normalize_audio must never amplify a near-silent render into hiss.
|
||
|
||
Regression for the "blank noise" some generated voices exhibited: the model
|
||
occasionally emits near-silence, and peak-normalizing that to -2 dBFS applies
|
||
thousands of × of gain, lifting the noise floor to full scale. The silence
|
||
floor in normalize_audio prevents that while leaving real audio untouched.
|
||
"""
|
||
import torch
|
||
|
||
from services.audio_dsp import normalize_audio
|
||
|
||
|
||
def test_silence_is_not_amplified():
|
||
# A dead render sitting at ~-80 dBFS must stay inaudible, not get scaled up.
|
||
quiet = torch.full((1, 16000), 1e-4, dtype=torch.float32)
|
||
out = normalize_audio(quiet, target_dBFS=-2.0)
|
||
assert out.abs().max().item() < 0.01, "near-silent input must not be amplified to hiss"
|
||
|
||
|
||
def test_all_zeros_stays_zero():
|
||
out = normalize_audio(torch.zeros(1, 8000, dtype=torch.float32))
|
||
assert out.abs().max().item() == 0.0
|
||
|
||
|
||
def test_real_audio_is_normalized_to_target():
|
||
sig = torch.zeros(1, 16000, dtype=torch.float32)
|
||
sig[0, ::100] = 0.1 # real signal peaking well above the silence floor
|
||
out = normalize_audio(sig, target_dBFS=-2.0)
|
||
target = 10 ** (-2.0 / 20.0) # ~0.794
|
||
assert abs(out.abs().max().item() - target) < 0.02
|
||
|
||
|
||
def test_just_above_floor_is_normalized():
|
||
# 0.01 (-40 dBFS) is above the -50 dBFS floor → should still be normalized.
|
||
sig = torch.zeros(1, 16000, dtype=torch.float32)
|
||
sig[0, 0] = 0.01
|
||
out = normalize_audio(sig, target_dBFS=-2.0)
|
||
assert out.abs().max().item() > 0.5
|
||
|
||
|
||
def test_empty_passthrough():
|
||
assert normalize_audio(torch.zeros(0)).numel() == 0
|