Add smoothness training pipeline with STFT/adversarial/vocoder-consistency losses

- inflect_nano/train_smooth.py: enhanced training with multi-res STFT loss,
  adversarial mel discriminator, vocoder consistency loss, deeper residual
  postnet, and cosine LR schedule
- preprocess_dataset.py: convert HF datasets, local dirs, or LJSpeech CSVs
  to the durations.jsonl format needed by training
- inference.py: add --smooth-prosody, --mel-smooth-sigma, --lowpass-hz flags
  for zero-cost inference-time quality improvements
- test_inference.py: smoke tests for model loading and synthesis
- colab_smooth_finetune.ipynb: Colab notebook for T4 GPU fine-tuning
- requirements.txt: add numba, scipy, datasets
This commit is contained in:
Michael Treadgold
2026-06-18 19:46:40 +12:00
parent 1a75163b18
commit 6e1da4ddfb
7 changed files with 1478 additions and 1 deletions
+44 -1
View File
@@ -78,6 +78,31 @@ def normalize_audio(audio: np.ndarray, target_rms_db: float = -20.0, peak_db: fl
return np.clip(audio, -1.0, 1.0)
def smooth_mel(mel: torch.Tensor, sigma: float = 1.0) -> torch.Tensor:
"""Gaussian temporal smoothing on mel frames to reduce frame-to-frame jitter."""
if sigma <= 0:
return mel
kernel_size = int(2 * math.ceil(2 * sigma) + 1)
if kernel_size < 3:
return mel
kernel = torch.exp(-0.5 * (torch.arange(kernel_size, device=mel.device, dtype=mel.dtype) - kernel_size // 2) ** 2 / sigma**2)
kernel = kernel / kernel.sum()
# [B, n_mels, T] -> pad last dim (time), then conv1d over time
pad = kernel_size // 2
mel_padded = torch.nn.functional.pad(mel, (pad, pad), mode="replicate")
kernel_expanded = kernel.view(1, 1, -1).expand(mel.shape[1], 1, -1)
return torch.nn.functional.conv1d(mel_padded, kernel_expanded, groups=mel.shape[1])
def apply_lowpass(wav: np.ndarray, cutoff_hz: float, sample_rate: int = 24000) -> np.ndarray:
"""Simple low-pass filter to reduce vocoder buzz above cutoff."""
if cutoff_hz <= 0 or cutoff_hz >= sample_rate / 2:
return wav
from scipy import signal
sos = signal.butter(4, cutoff_hz, btype="low", fs=sample_rate, output="sos")
return signal.sosfiltfilt(sos, wav).astype(np.float32)
@torch.inference_mode()
def synthesize(
text: str,
@@ -88,6 +113,9 @@ def synthesize(
length_scale: float = 1.0,
pitch_scale: float = 1.0,
energy_scale: float = 1.0,
smooth_prosody: bool = False,
mel_smooth_sigma: float = 0.0,
lowpass_hz: float = 0.0,
) -> np.ndarray:
phone, tone, lang = text_to_tokens(text)
phone = phone.unsqueeze(0).to(device)
@@ -102,9 +130,15 @@ def synthesize(
length_scale=float(length_scale),
pitch_scale=float(pitch_scale),
energy_scale=float(energy_scale),
smooth_predictors=smooth_prosody,
)
if mel_smooth_sigma > 0:
mel = smooth_mel(mel, mel_smooth_sigma)
wav = vocoder(mel).squeeze().detach().cpu().numpy()
return normalize_audio(wav)
wav = normalize_audio(wav)
if lowpass_hz > 0:
wav = apply_lowpass(wav, lowpass_hz)
return wav
def main() -> None:
@@ -117,6 +151,12 @@ def main() -> None:
ap.add_argument("--length-scale", type=float, default=1.0)
ap.add_argument("--pitch-scale", type=float, default=1.0)
ap.add_argument("--energy-scale", type=float, default=1.0)
ap.add_argument("--smooth-prosody", action="store_true",
help="Apply 3-frame averaging to pitch/energy/brightness (reduces jitter)")
ap.add_argument("--mel-smooth-sigma", type=float, default=0.0,
help="Gaussian temporal smooth on mel frames (0.5-1.5 reduces buzzing)")
ap.add_argument("--lowpass-hz", type=float, default=0.0,
help="Low-pass cutoff in Hz (e.g. 8000-11000 reduces vocoder buzz)")
args = ap.parse_args()
device = torch.device(args.device)
@@ -131,6 +171,9 @@ def main() -> None:
length_scale=args.length_scale,
pitch_scale=args.pitch_scale,
energy_scale=args.energy_scale,
smooth_prosody=args.smooth_prosody,
mel_smooth_sigma=args.mel_smooth_sigma,
lowpass_hz=args.lowpass_hz,
)
args.out.parent.mkdir(parents=True, exist_ok=True)
sf.write(str(args.out), audio, 24000, subtype="PCM_16")