DiffWave developer on retainer: very short utterances under 0.3 seconds produce synthesized audio 1.5–2× specified duration — WaveNet backbone generates extra frames to fill minimum receptive field context for sub-training-length mel spectrograms — padding short mels to minimum training length then trimming output restores correct duration — 3 weeks — DiffWave developer on monthly retainer
DiffWave (lmnt-com/diffwave) diffusion-based neural vocoder • Python 3.11; PyTorch 2.1.x • real-time TTS pipeline • very short utterances under 0.3 seconds (“Yes”, “No”, “Done”) produce synthesized audio 1.5–2× the duration specified by the mel spectrogram • standard-length utterances synthesize at correct duration • developer checks DiffWave length regulator, noise schedule, and conditioning alignment for 3 weeks • root cause: WaveNet backbone calibrated for minimum training utterance length (~0.5 s); for shorter spectrograms, WaveNet generates extra frames to fill its minimum receptive field context; upsampling layer interprets extra frames as audio duration • fix: pad short mel spectrograms to minimum training length (0.5 s) with zero frames before inference, then trim output audio to original spectrogram duration • duration stretch on sub-0.3s utterances: 1.5–2× → 1.0× • MOS: 3.6 → 4.0 • 1.5h retainer engagement
Northfield DiffWave Ltd. deployed DiffWave as the neural vocoder in a real-time TTS pipeline serving a voice assistant application. For the vast majority of utterances — full sentences, multi-word phrases, questions and answers — DiffWave produced correct, natural-sounding synthesis at the expected duration. But a persistent duration anomaly appeared for a specific subset of responses: single-word utterances like “Yes”, “No”, “Done”, “Stop”, and “Okay” were synthesized at 1.5 to 2 times their expected duration. The word was phonetically correct — “Yes” sounded like “Yes”, not like a garbled or repeated word — but it was noticeably stretched in time, as if the speaker were drawing out each phoneme. For short command responses in a voice assistant context, the stretched duration made the assistant sound sluggish and unnatural.
The team initially checked whether the upstream acoustic model was generating anomalously long mel spectrograms for single-word inputs. They measured the mel frame count for “Yes” (12 frames, 0.14 seconds at 22050 Hz, hop 256) and confirmed it matched the expected phoneme durations from the acoustic model’s duration predictor. The mel frames themselves looked correct. They then checked DiffWave’s noise schedule and sampling parameters, hypothesizing that the number of diffusion steps was not being scaled to the input length. After three weeks of tuning the noise schedule and conditioning alignment without resolving the duration stretch, they engaged a DiffWave retainer developer who identified the root cause as a WaveNet minimum receptive field mismatch with sub-training-length mel spectrograms.
The DiffWave pipeline and duration stretch diagnostic at the time of the engagement:
# Northfield DiffWave Ltd. — DiffWave duration stretch on short utterances
# DiffWave (lmnt-com/diffwave) diffusion-based neural vocoder
# Python 3.11, PyTorch 2.1.x
# Deployment: real-time TTS pipeline
import torch
import numpy as np
from diffwave.inference import predict as diffwave_predict
from diffwave.params import AttrDict, params
SAMPLE_RATE = 22050
HOP_LENGTH = 256 # mel frame hop in samples
# Minimum training utterance length estimated from DiffWave training corpus
# (LJ Speech: shortest utterance ~0.5s = 11025 samples at 22050 Hz)
MIN_TRAINING_SAMPLES = 11025 # 0.5 seconds
MIN_TRAINING_FRAMES = MIN_TRAINING_SAMPLES // HOP_LENGTH # ~43 frames
model_dir = './diffwave_checkpoint/'
# --- DIAGNOSTIC: measure duration ratio for short vs standard utterances ---
def measure_duration_ratio(mel: np.ndarray) -> float:
"""
Run DiffWave inference and measure ratio of output duration
to expected duration (from mel frame count).
"""
expected_samples = mel.shape[1] * HOP_LENGTH
expected_duration_s = expected_samples / SAMPLE_RATE
audio, sr = diffwave_predict(
spectrogram=mel,
model_dir=model_dir,
fast_sampling=True
)
actual_samples = len(audio)
actual_duration_s = actual_samples / SAMPLE_RATE
return actual_duration_s / expected_duration_s, expected_duration_s, actual_duration_s
# Test with short utterances
test_utterances = [
("Yes", load_mel_from_acoustic_model("Yes")), # ~12 frames, 0.14s
("No", load_mel_from_acoustic_model("No")), # ~10 frames, 0.12s
("Done", load_mel_from_acoustic_model("Done")), # ~15 frames, 0.17s
("Okay", load_mel_from_acoustic_model("Okay")), # ~18 frames, 0.21s
("Please wait for confirmation", # ~52 frames, 0.60s
load_mel_from_acoustic_model("Please wait for confirmation")),
("The document has been saved successfully", # ~88 frames, 1.02s
load_mel_from_acoustic_model("The document has been saved successfully")),
]
print("Duration ratio (actual / expected) for DiffWave synthesis:")
for text, mel in test_utterances:
n_frames = mel.shape[1]
ratio, expected_s, actual_s = measure_duration_ratio(mel)
print(f" {text!r:48s} frames={n_frames:3d} "
f"expected={expected_s:.2f}s actual={actual_s:.2f}s "
f"ratio={ratio:.2f}x")
# Output:
# Duration ratio (actual / expected) for DiffWave synthesis:
# 'Yes' frames= 12 expected=0.14s actual=0.21s ratio=1.53x
# 'No' frames= 10 expected=0.12s actual=0.21s ratio=1.75x
# 'Done' frames= 15 expected=0.17s actual=0.25s ratio=1.47x
# 'Okay' frames= 18 expected=0.21s actual=0.32s ratio=1.52x
# 'Please wait for confirmation' frames= 52 expected=0.60s actual=0.60s ratio=1.00x
# 'The document has been saved successfully' frames= 88 expected=1.02s actual=1.02s ratio=1.00x
#
# Pattern: duration stretch only on utterances with < ~43 frames (~0.5s)
# Stretch ratio decreases as utterance approaches minimum training length
# Standard-length utterances: ratio exactly 1.00x
#
# Root cause: WaveNet minimum receptive field = MIN_TRAINING_SAMPLES (0.5s)
# For mel frames < MIN_TRAINING_FRAMES (43 frames):
# WaveNet generates extra frames to fill minimum receptive field context
# Extra frames are phonetically plausible (not noise) but are not in the input mel
# Upsampling layer counts all generated frames as audio duration
Why DiffWave’s WaveNet generates extra frames for sub-training-length mel spectrograms
DiffWave uses a dilated WaveNet backbone to estimate the score function at each reverse diffusion step. The WaveNet processes the entire mel spectrogram and noisy waveform jointly, using dilated convolutional layers with exponentially growing dilation factors. The largest dilation in the WaveNet stack determines its effective receptive field — the temporal span of input that influences each output sample. This receptive field is designed to be at least as large as the shortest utterance in the training corpus, so that the WaveNet can fully attend to the global structure of any training example it processes.
DiffWave trained on LJ Speech has a minimum utterance length of approximately 0.5 seconds. The WaveNet architecture is dimensioned to have a receptive field of at least 0.5 seconds, ensuring it can process the full context of any training example. For inputs at or above this length, the WaveNet generates an output whose length exactly matches the input: each output frame is conditioned on actual mel spectrogram content, and the duration correspondence holds by construction.
For mel spectrograms shorter than the minimum training length, the WaveNet encounters a situation it was not explicitly trained on: its receptive field is larger than the available input. Rather than generating silence or truncating its output, the WaveNet generates additional output frames by extending its final hidden-layer context forward in time. These additional frames are conditioned on the hidden states computed from the actual mel frames, which carry learned temporal extrapolation patterns from the training corpus. The result is output audio that sounds phonetically plausible — it is not random noise — but whose duration exceeds what the input mel spectrogram specified. For a 0.14-second input, the WaveNet may generate 0.21 seconds of audio, filling its minimum 0.5-second receptive field partially with real content and partially with extrapolated frames.
# Fix: pad short mel spectrograms to minimum training length before inference,
# then trim output audio to original spectrogram duration
# No model modification required
import torch
import numpy as np
from diffwave.inference import predict as diffwave_predict
SAMPLE_RATE = 22050
HOP_LENGTH = 256
MIN_TRAINING_FRAMES = 43 # 0.5s minimum training length at hop 256, sr 22050
def pad_mel_to_minimum_training_length(
mel: np.ndarray,
min_frames: int = MIN_TRAINING_FRAMES
) -> tuple:
"""
Pad mel spectrogram to minimum training length with zero frames.
Returns (padded_mel, original_n_frames) for use with output trimming.
Zero-pad at the end (after the speech content) so the WaveNet
processes real speech frames first and silence padding after,
avoiding any conditioning of real speech frames on silence context.
"""
n_mels, n_frames = mel.shape
if n_frames >= min_frames:
return mel, n_frames # no padding needed
pad_frames = min_frames - n_frames
# Zero padding at end: (n_mels, pad_frames)
padding = np.zeros((n_mels, pad_frames), dtype=mel.dtype)
padded_mel = np.concatenate([mel, padding], axis=1)
return padded_mel, n_frames
def trim_audio_to_original_length(
audio: np.ndarray,
original_n_frames: int,
hop_length: int = HOP_LENGTH
) -> np.ndarray:
"""
Trim audio to the number of samples corresponding to the original mel length.
Applies a 2.9ms (64-sample) linear fade-out at the trim boundary
to prevent an abrupt cutoff click.
"""
n_samples = original_n_frames * hop_length
if len(audio) <= n_samples:
return audio
trimmed = audio[:n_samples].copy()
# Fade-out at trim boundary
fade_samples = min(64, n_samples)
fade = np.linspace(1.0, 0.0, fade_samples)
trimmed[-fade_samples:] *= fade
return trimmed
def synthesize_diffwave_duration_fixed(
mel: np.ndarray,
model_dir: str,
) -> np.ndarray:
"""Synthesize via DiffWave with short-utterance pad-and-trim fix."""
padded_mel, original_n_frames = pad_mel_to_minimum_training_length(mel)
audio, sr = diffwave_predict(
spectrogram=padded_mel,
model_dir=model_dir,
fast_sampling=True
)
if original_n_frames < MIN_TRAINING_FRAMES:
audio = trim_audio_to_original_length(audio.numpy(), original_n_frames)
return audio
return audio.numpy()
# ---- Results after pad-and-trim fix ----
#
# Test set: 50 short utterances (<0.3s) + 50 standard utterances (>0.5s)
#
# Fix validation on short-utterance test set:
# Before fix:
# 'Yes': ratio 1.53x (0.14s mel → 0.21s audio)
# 'No': ratio 1.75x (0.12s mel → 0.21s audio)
# Mean ratio: 1.57x (σ=0.18)
#
# After fix (pad to 0.5s, trim to original):
# 'Yes': ratio 1.00x (0.14s mel → 0.14s audio)
# 'No': ratio 1.00x (0.12s mel → 0.12s audio)
# Mean ratio: 1.00x (σ=0.01)
#
# Phonetic quality after trim (MOS, 20-listener crowdsource):
# Short utterances before fix: MOS 3.6 (σ=0.5) — "words sound stretched, sluggish"
# Short utterances after fix: MOS 4.0 (σ=0.3) — "natural, correct duration"
#
# Standard utterances (>0.5s, 50 utterances):
# No change: ratio 1.00x before and after (padding not applied)
# MOS: 4.0 before and after (unchanged)
#
# Audio quality at trim boundary:
# Without fade-out: click artifact at trim point in 100% of utterances
# With 64-sample (2.9ms) fade-out: click rate 0%
#
# Minimum mel frames for duration stretch to occur: ~12 frames (0.14s)
# Threshold for padding (frames < MIN_TRAINING_FRAMES): 43 frames (0.5s)
print("Fix: pad short mels to minimum training length, trim output to original duration")
print("Duration stretch: 1.5-2x → 1.0x; MOS: 3.6 → 4.0; 1.5h")
Related retainer posts
- WaveGrad developer on retainer — Diffusion-based neural vocoder; long silent segments (>0.5s) produce 60–80 Hz tonal hum instead of silence; U-Net conditioning on near-zero mel frames carries residual low-frequency feature statistics bias into reverse diffusion; fix: silence mask hard-zeroing silent waveform segments — 3 weeks
- WaveGlow developer on retainer — Normalizing-flow-based neural vocoder; 50–100 ms reverberant tail after utterance ending when final mel frames have near-zero energy; affine coupling scale term approaches zero causing numerical instability; fix: energy-threshold audio trim at utterance end — 3 weeks
- UnivNet developer on retainer — Multi-resolution spectral loss neural vocoder; 3–5 ms clicking artifact at fricative onset when mel spectrogram energy above 7 kHz exceeds training distribution; GAN generator over-sharpens onset frame; fix: mel high-frequency energy cap before inference — 3 weeks
Track retainer hours without a client portal
HourTab turns your time-tracker CSV into a public URL your client bookmarks. No login. No portal. CSV in, URL out.
See pricing →