From 17d9821922e1024de46063d3de57c5acf5a2a4e1 Mon Sep 17 00:00:00 2001 From: KakaruHayate Date: Wed, 15 Jul 2026 14:42:40 +0800 Subject: [PATCH 1/2] fix: pad mel with spec_min instead of 0.0 in acoustic collater MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Raw log-mel 0.0 is not a neutral padding value: norm_spec maps it to +1.0 — the very top of the normalized range, i.e. maximum loudness. Every batch therefore filled the padded tail of shorter samples with full-loudness garbage. Two consequences: 1. The diffusion backbones (WaveNet / LYNXNet / LYNXNet2) receive no padding mask, so their receptive field (~181 frames for LYNXNet2 with kernel_size=31 x 6 layers) leaks the fake signal into the trailing valid frames. The loss mask (mel2ph > 0) hides this from the loss on padding frames, but the contaminated valid frames near the boundary are fully counted — a systematic bias on utterance tails, exactly where breathy endings and vibrato decay live. 2. The aux decoder loss is not masked at all: with zero-padding the aux decoder was actively trained to predict maximum loudness on padding frames from near-zero condition. Padding with spec_min (-12 by default) maps to -1.0 (silence) and sits next to the mel extractor's true silence floor log(1e-5) = -11.51, so padded regions now look like ordinary trailing silence — consistent with what the model sees at inference time. Note: this changes the training data distribution slightly; models trained before/after this fix are checkpoint-compatible but their padded-region behavior differs. --- training/acoustic_task.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/training/acoustic_task.py b/training/acoustic_task.py index ca6a71c65..f1ee4ab91 100644 --- a/training/acoustic_task.py +++ b/training/acoustic_task.py @@ -45,7 +45,17 @@ def collater(self, samples): tokens = utils.collate_nd([s['tokens'] for s in samples], 0) f0 = utils.collate_nd([s['f0'] for s in samples], 0.0) mel2ph = utils.collate_nd([s['mel2ph'] for s in samples], 0) - mel = utils.collate_nd([s['mel'] for s in samples], 0.0) + # Pad mel with spec_min (silence) instead of 0.0. Raw log-mel 0.0 lies at + # the TOP of the normalized range (norm_spec maps it to +1.0, i.e. maximum + # loudness), so zero-padding used to fill the padded tail of every shorter + # sample with full-loudness garbage. The diffusion loss masks these frames, + # but the backbone has no padding mask and its receptive field (~181 frames + # for LYNXNet2 k=31 x 6 layers) leaks the fake signal into the trailing + # valid frames; the aux decoder loss is not masked at all. spec_min (-12) + # sits right at the true silence floor log(1e-5) = -11.51 of the mel + # extractor, i.e. padding now looks like silence. + mel_pad = float(hparams['spec_min'][0]) if hparams.get('spec_min') else -12.0 + mel = utils.collate_nd([s['mel'] for s in samples], mel_pad) batch.update({ 'tokens': tokens, 'mel2ph': mel2ph, From 5f2a10fa96d3706006f73f3be458b52fc3fce9af Mon Sep 17 00:00:00 2001 From: Kakaru <97896816+KakaruHayate@users.noreply.github.com> Date: Wed, 15 Jul 2026 14:51:12 +0800 Subject: [PATCH 2/2] Update mel padding strategy to use spec_min Replace zero-padding with spec_min for mel padding to avoid full-loudness garbage in shorter samples. --- training/acoustic_task.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/training/acoustic_task.py b/training/acoustic_task.py index f1ee4ab91..a9f514eac 100644 --- a/training/acoustic_task.py +++ b/training/acoustic_task.py @@ -45,15 +45,6 @@ def collater(self, samples): tokens = utils.collate_nd([s['tokens'] for s in samples], 0) f0 = utils.collate_nd([s['f0'] for s in samples], 0.0) mel2ph = utils.collate_nd([s['mel2ph'] for s in samples], 0) - # Pad mel with spec_min (silence) instead of 0.0. Raw log-mel 0.0 lies at - # the TOP of the normalized range (norm_spec maps it to +1.0, i.e. maximum - # loudness), so zero-padding used to fill the padded tail of every shorter - # sample with full-loudness garbage. The diffusion loss masks these frames, - # but the backbone has no padding mask and its receptive field (~181 frames - # for LYNXNet2 k=31 x 6 layers) leaks the fake signal into the trailing - # valid frames; the aux decoder loss is not masked at all. spec_min (-12) - # sits right at the true silence floor log(1e-5) = -11.51 of the mel - # extractor, i.e. padding now looks like silence. mel_pad = float(hparams['spec_min'][0]) if hparams.get('spec_min') else -12.0 mel = utils.collate_nd([s['mel'] for s in samples], mel_pad) batch.update({