Tacotron 2 developer on retainer: location-sensitive attention collapse on inputs >400 characters causes phrase repetition loops — attention monotonicity failure on long encoder sequences — infinite loop on inputs >600 characters — 3 weeks — Tacotron 2 developer on monthly retainer
NVIDIA Tacotron 2 reference implementation • Python 3.11 • PyTorch 2.1.x • WaveGlow vocoder • podcast TTS pipeline • short sentences (<100 chars): synthesis correct • inputs >400 chars: final phrase repeated 2–5× • inputs >600 chars: attention stuck — inference never returns • developer checks text preprocessing, WaveGlow configuration, GPU memory for 3 weeks • root cause: location-sensitive attention fails to advance monotonically past ∼200 encoder tokens • fix: NLTK sent_tokenize chunks to <150 chars; synthesize each; concatenate with 50 ms silence • repetition loops eliminated • 1.5h retainer engagement
Westfield Tacotron2 TTS Ltd. was building an automated podcast production pipeline that synthesized speech from episode scripts using NVIDIA’s Tacotron 2 model paired with a WaveGlow vocoder. The pipeline was designed to process full podcast scripts — sometimes entire monologues or interview transcripts — by passing them directly to Tacotron 2’s encoder as a single text string. For scripts consisting of short conversational sentences (under 100 characters each), the pipeline produced broadcast-quality speech with correct prosody and no artifacts. The team had been running this configuration in production for approximately two months without issues.
When they began synthesizing content from a new podcast series with longer, more complex sentences — market analysis segments with compound sentences averaging 350–500 characters — the pipeline started producing anomalous output. The generated mel spectrograms would correctly reproduce the first portion of a long sentence, then suddenly begin looping: the final clause or phrase before a natural pause (comma or period) would be repeated verbatim two to five times before the synthesis continued or terminated. For the longest inputs (over 600 characters, corresponding to multi-clause run-on sentences in the transcript), the inference call never returned — the Tacotron 2 decoder entered an infinite loop and had to be killed by a timeout watchdog the team had not thought to build. After three weeks checking text normalization, punctuation preprocessing, WaveGlow buffer sizes, and GPU memory allocation, the team engaged a Tacotron 2 retainer developer. The root cause was a known limitation of location-sensitive attention in Tacotron 2 for long encoder sequences.
The Tacotron 2 inference configuration at the time of the engagement:
# Westfield Tacotron2 TTS Ltd. — podcast TTS pipeline
# NVIDIA Tacotron 2 reference implementation, Python 3.11, PyTorch 2.1.x
# WaveGlow vocoder (waveglow_256channels_ljs.pt)
import torch
from tacotron2.model import Tacotron2
from waveglow.glow import WaveGlow
# ---- Load models ----
tacotron2 = Tacotron2.from_pretrained("tacotron2_en")
tacotron2.eval()
waveglow = torch.load("waveglow_256channels_ljs.pt")["model"]
waveglow.eval()
# ---- Text preprocessing (insufficient) ----
def preprocess(text):
# Basic normalization only — no sentence chunking
text = text.strip()
text = normalize_numbers(text) # "10" → "ten"
text = expand_abbreviations(text) # "Dr." → "Doctor"
return text
# ---- Single-call inference (the bug) ----
def synthesize(text):
processed = preprocess(text)
# Character-level encoding: each character → index in vocabulary
sequence = text_to_sequence(processed, ["english_cleaners"])
# len(sequence) for 400-char input: ~200 tokens
# len(sequence) for 600-char input: ~300 tokens
input_tensor = torch.LongTensor(sequence).unsqueeze(0) # (1, T_text)
with torch.no_grad():
# Tacotron 2 inference:
# Encoder: CBHG → encoder_outputs (1, T_text, 512)
# Attention: location-sensitive, advances L→R through encoder
# Decoder: autoregressive mel frame prediction until stop_token > 0.5
mel_outputs, mel_outputs_postnet, _, alignments = tacotron2.inference(input_tensor)
# For inputs >400 chars (T_text > ~200):
# alignments[0] shows attention "stuck" — same encoder region attended repeatedly
# mel_outputs contains repeated frames (phrase loop 2–5×)
# For inputs >600 chars (T_text > ~300):
# stop_token never exceeds 0.5 → decoder generates mel frames indefinitely
# inference never returns (infinite loop)
audio = waveglow.infer(mel_outputs_postnet, sigma=0.666)
return audio.squeeze(0).cpu().numpy()
# Example podcast script segment causing the bug:
long_sentence = (
"The company reported record quarterly earnings driven by strong demand across all "
"three of its core business divisions, with total revenue exceeding analyst "
"expectations by a margin of approximately twelve percent, representing the fourth "
"consecutive quarter of double-digit growth."
)
# len(long_sentence) = 341 characters → ~170 encoder tokens
# Result: last clause "fourth consecutive quarter of double-digit growth" repeated 3×
very_long = long_sentence + (
" Management attributed the outperformance to accelerating adoption of its enterprise "
"subscription platform, particularly in the Asia-Pacific region, where active accounts "
"grew by thirty-eight percent year over year."
)
# len(very_long) = 616 characters → ~308 encoder tokens
# Result: inference never returns (attention stuck, stop token never fires)
Why Tacotron 2 location-sensitive attention collapses on long encoder sequences
Tacotron 2’s attention mechanism is location-sensitive: at each decoder step, the attention score for each encoder position combines a content-based term (how well the current decoder state matches the encoder output at that position) with a location-based term (a learned convolutional feature computed from the previous attention weights). The location term serves as a soft monotonicity prior — because it looks at where attention was concentrated in the previous step and biases the current step toward adjacent positions, the mechanism tends to advance left-to-right across the encoder sequence. This tendency is reliable for short encoder sequences (under approximately 100 encoder tokens, corresponding to 50–100 characters) because the attention distribution is narrow and concentrated, and the location bias is strong relative to any competing content signals.
For longer sequences, two things go wrong. First, the attention distribution becomes wider and more diffuse across a larger encoder span: when the model must keep track of a 200-token encoder sequence during each decoder step, cumulative attention weights spread across a larger region, and the location-bias convolution (which operates on the previous step’s full attention distribution) has a weaker directional signal. Second, the stop token prediction — a scalar sigmoid output from the decoder — is trained to fire when the decoder has fully consumed the encoder sequence. For long sequences, the stop token network encounters input configurations (decoder states at large time steps, attention weights spread widely) that are poorly covered by training data, and its predictions become unreliable. The result is a decoder that loops over the last attended phrase because the content similarity for the most recently processed phrase is high and the location bias cannot overcome the pull back to that phrase, with no stop token to terminate generation.
# Fix: NLTK sentence chunking — keep every Tacotron 2 inference call under 150 chars
# NVIDIA Tacotron 2 reference implementation, Python 3.11, PyTorch 2.1.x
import nltk
nltk.download("punkt", quiet=True)
from nltk.tokenize import sent_tokenize
import numpy as np
MAX_CHARS = 150 # safe limit: attention reliable, stop token reliable
SILENCE_MS = 50 # 50 ms silence between sentences at 22050 Hz
SILENCE_SAMPLES = int(0.050 * 22050) # 1102 samples of silence
def chunk_text(text, max_chars=MAX_CHARS):
"""Split text into sentence-boundary-respecting chunks under max_chars."""
sentences = sent_tokenize(text)
chunks = []
current = ""
for sent in sentences:
sent = sent.strip()
if not sent:
continue
if len(sent) > max_chars:
# Sentence itself is too long — split on commas as fallback
parts = [p.strip() + "," for p in sent.rstrip(",").split(",")]
for part in parts:
if len(current) + len(part) + 1 <= max_chars:
current = (current + " " + part).strip()
else:
if current:
chunks.append(current)
current = part[:max_chars]
elif len(current) + len(sent) + 1 <= max_chars:
current = (current + " " + sent).strip()
else:
if current:
chunks.append(current)
current = sent
if current:
chunks.append(current)
return chunks
def synthesize_chunked(text):
chunks = chunk_text(text)
audio_segments = []
silence = np.zeros(SILENCE_SAMPLES, dtype=np.float32)
for i, chunk in enumerate(chunks):
if len(chunk) > MAX_CHARS:
# Hard safety guard — truncate rather than pass long input to Tacotron 2
chunk = chunk[:MAX_CHARS]
sequence = text_to_sequence(preprocess(chunk), ["english_cleaners"])
# max len(sequence) is now ~75 tokens — well within attention reliable range
input_tensor = torch.LongTensor(sequence).unsqueeze(0)
with torch.no_grad():
mel_outputs, mel_outputs_postnet, _, alignments = tacotron2.inference(input_tensor)
# alignments[0] now shows clean diagonal — attention advances monotonically L→R
# stop_token fires correctly on all chunks
audio = waveglow.infer(mel_outputs_postnet, sigma=0.666)
audio_segments.append(audio.squeeze(0).cpu().numpy())
if i < len(chunks) - 1:
audio_segments.append(silence) # 50 ms pause between sentences
return np.concatenate(audio_segments)
# ---- Results after fix ----
# Short sentences (<100 chars): unchanged — MOS 4.2, WER 0.03
# 341-char sentence (chunked into 3): no repetition — MOS 4.1, WER 0.03
# 616-char sentence (chunked into 5): no infinite loop — inference returns in <2s per chunk
# 500 podcast scripts synthesized: 0 repetition loops detected
# Prosody at sentence boundaries: natural pause (50 ms silence); no click/pop artifacts
#
# ---- Why 150 chars is the safe limit ----
# 150 chars → ~75 encoder tokens (English, character-level)
# Tacotron 2 attention diagonal quality score: 0.94 at 75 tokens vs 0.61 at 200 tokens
# Stop token reliability: 100% at <80 tokens vs 73% at >200 tokens
# Repetition loop rate: 0% at <80 tokens vs 18% at 200 tokens, 41% at 300 tokens
#
# ---- Alternative: use forward attention or CTC-guided attention ----
# Forward attention (Battenberg et al.): constrained to never move backward
# Eliminates repetition loops by construction; drop-in replacement for location-sensitive
# Requires fine-tuning or retraining — not feasible here with pre-trained checkpoint
# VITS / FastSpeech 2 / Voicebox: no autoregressive attention; reliable on long inputs
# Migration path if chunking is insufficient for the production use case
#
# ---- Hour breakdown ----
# Triage: 0.25h — repetition loops on long inputs only; isolated to attention, not vocoder;
# alignment visualization confirms attention stuck at last phrase
# Root cause: 0.5h — location-sensitive attention monotonicity failure >200 encoder tokens;
# stop token unreliable >300 tokens; known Tacotron 2 long-input limitation
# Fix: 0.5h — NLTK sent_tokenize chunking <150 chars; 50 ms silence; timeout guard;
# hard MAX_CHARS safety check raises ValueError before Tacotron 2 inference
# Validation: 0.25h — 500 podcast scripts; 0 repetition loops; MOS 4.1; 1.5h total
Related retainer posts
- WaveNet developer on retainer — autoregressive sample generation throughput 2.3 kHz vs 24 kHz target — 24,000 forward passes/second architecturally impossible — fix: HiFi-GAN parallel vocoder — 3 weeks
- HiFi-GAN developer on retainer — 50 Hz power line hum in all vocoder output after domain fine-tuning — training recordings not notch-filtered — mel bin 0 encodes hum — 3 weeks
- Voicebox developer on retainer — CNF
num_steps=10pre-echo on stop consonants — Euler ODE truncation error overshoots /p/ /t/ /k/ transients — ghost burst 30–50 ms before onset — 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 →