dreamvoice / audio_postprocess.py
sush0401's picture
DreamVoice: ZeroGPU app
d30938f verified
Raw
History Blame Contribute Delete
4.22 kB
"""Audio post-processing: silence trimming, normalization, quality checks."""
from __future__ import annotations
import io
import os
import tempfile
import numpy as np
def trim_silence(
audio: np.ndarray,
sample_rate: int,
threshold_db: float = -35.0,
min_silence_sec: float = 0.12,
max_silence_sec: float = 0.20,
keep_start: float = 0.03,
keep_end: float = 0.05,
) -> np.ndarray:
"""Trim leading/trailing silence and compress long pauses between speech segments.
Args:
audio: 1-D float32 waveform.
sample_rate: Samples per second.
threshold_db: Silence threshold in dB (below this = silence).
min_silence_sec: Minimum pause to keep (natural breathing room).
max_silence_sec: Maximum pause allowed — anything longer gets capped here.
keep_start: Seconds of silence to keep at the start.
keep_end: Seconds of silence to keep at the end.
Returns:
Cleaned waveform (float32).
"""
if audio.size == 0:
return audio
threshold = 10 ** (threshold_db / 20.0)
sr = sample_rate
# --- find voiced regions ---
is_voiced = np.abs(audio) > threshold
voiced_indices = np.where(is_voiced)[0]
if voiced_indices.size == 0:
return audio[: int(sr * 0.1)]
# keep a small buffer before first voice and after last voice
start = max(0, int(voiced_indices[0] - sr * keep_start))
end = min(len(audio), int(voiced_indices[-1] + sr * keep_end))
trimmed = audio[start:end].copy()
# --- compress long inter-voice pauses ---
target_pause = int(min_silence_sec * sr)
max_pause = int(max_silence_sec * sr)
result_parts: list[np.ndarray] = []
fade_len = int(0.005 * sr) # 5ms crossfade to avoid clicks
i = 0
n = len(trimmed)
while i < n:
if np.abs(trimmed[i]) <= threshold:
silence_end = i
while silence_end < n and np.abs(trimmed[silence_end]) <= threshold:
silence_end += 1
silence_len = silence_end - i
if silence_len > max_pause:
result_parts.append(trimmed[i: i + target_pause])
elif silence_len > target_pause:
result_parts.append(trimmed[i: i + target_pause])
else:
result_parts.append(trimmed[i: silence_end])
i = silence_end
else:
voice_end = i
while voice_end < n and np.abs(trimmed[voice_end]) > threshold:
voice_end += 1
result_parts.append(trimmed[i: voice_end])
i = voice_end
if not result_parts:
return trimmed
out = np.concatenate(result_parts).astype(np.float32)
# fade-in / fade-out to avoid clicks at trim points
if len(out) > fade_len * 2:
fade_in = np.linspace(0, 1, fade_len, dtype=np.float32)
fade_out = np.linspace(1, 0, fade_len, dtype=np.float32)
out[:fade_len] *= fade_in
out[-fade_len:] *= fade_out
return out
def normalize_audio(
audio: np.ndarray,
target_peak_db: float = -1.0,
) -> np.ndarray:
"""Peak-normalize audio to a target level."""
if audio.size == 0:
return audio
peak = float(np.max(np.abs(audio)))
if peak < 1e-6:
return audio
target = 10 ** (target_peak_db / 20.0)
return (audio * (target / peak)).astype(np.float32)
def postprocess(
audio: np.ndarray,
sample_rate: int,
trim: bool = True,
normalize: bool = True,
) -> np.ndarray:
"""Full post-processing pipeline: trim silence + normalize."""
if audio.size == 0:
return audio
if trim:
audio = trim_silence(audio, sample_rate)
if normalize:
audio = normalize_audio(audio)
return audio
def postprocess_wav_bytes(
wav_bytes: bytes,
sample_rate: int,
trim: bool = True,
normalize: bool = True,
) -> bytes:
"""Post-process WAV bytes (used on Modal GPU before sending back)."""
import soundfile as sf
audio, sr = sf.read(io.BytesIO(wav_bytes), dtype="float32")
audio = postprocess(audio, sr, trim=trim, normalize=normalize)
buf = io.BytesIO()
sf.write(buf, audio, sr, format="WAV")
return buf.getvalue()