diff --git a/backend/tests/test_audio_preprocess.py b/backend/tests/test_audio_preprocess.py index 6330a336..e52c44af 100644 --- a/backend/tests/test_audio_preprocess.py +++ b/backend/tests/test_audio_preprocess.py @@ -58,6 +58,15 @@ def test_silence_is_trimmed_with_padding_kept(): assert len(out) >= int(3.0 * SR), "speech body should be preserved" +def test_clean_audio_is_not_padded_past_original_length(): + # Well-recorded audio with no edge silence shouldn't get longer after + # preprocessing — otherwise a 29.9 s upload could be pushed past the + # 30 s max_duration ceiling downstream. + audio = _tone(3.0, amp=0.3) + out = preprocess_reference_audio(audio, SR) + assert len(out) <= len(audio) + + def test_empty_input_returns_empty(): out = preprocess_reference_audio(np.zeros(0, dtype=np.float32), SR) assert out.size == 0 diff --git a/backend/utils/audio.py b/backend/utils/audio.py index 923130b1..7e0fd6fd 100644 --- a/backend/utils/audio.py +++ b/backend/utils/audio.py @@ -203,7 +203,7 @@ def preprocess_reference_audio( audio: np.ndarray, sample_rate: int, peak_target: float = 0.95, - trim_top_db: float = 30.0, + trim_top_db: float = 40.0, edge_padding_ms: int = 100, ) -> np.ndarray: """ @@ -221,9 +221,14 @@ def preprocess_reference_audio( peak_target: Peak amplitude cap in [0, 1]. Applied only if the input peak exceeds this value. trim_top_db: Silence threshold for edge trimming, in dB below peak. - Conservative (30 dB) so soft speech at the edges isn't clipped off. - edge_padding_ms: Milliseconds of padding retained at each edge after - trimming, so TTS engines have a brief silence to anchor on. + 40 dB sits below normal speech dynamic range (≈30 dB) so soft + trailing syllables are preserved, while still catching obvious + leading/trailing silence. Lower values are more aggressive; + librosa's own default is 60. + edge_padding_ms: Milliseconds of padding to add back at each edge + *only if* trimming shortened the waveform, so TTS engines have a + brief silence to anchor on without ever making the output longer + than the input. Returns: Preprocessed audio array (float32). @@ -236,8 +241,13 @@ def preprocess_reference_audio( audio = audio - float(np.mean(audio)) trimmed, _ = librosa.effects.trim(audio, top_db=trim_top_db) - if trimmed.size > 0: - pad = int(sample_rate * edge_padding_ms / 1000) + if 0 < trimmed.size < audio.size: + pad_each = int(sample_rate * edge_padding_ms / 1000) + # Never pad past the original length — for near-max-duration uploads + # an unconditional pad would push them over the 30 s ceiling and + # trigger a spurious "too long" rejection. + headroom = (audio.size - trimmed.size) // 2 + pad = min(pad_each, max(headroom, 0)) if pad > 0: trimmed = np.pad(trimmed, (pad, pad), mode="constant") audio = trimmed