OverFlow TTS developer on retainer: second numbered list item duplicated in synthesized audio on 12% of numbered list inputs — Neural HMM Viterbi decoder misaligns at short 3-phoneme boundary between consecutive list items — replacing period-numeral separator with longer distinctive pause marker eliminates duplication — 3 weeks — OverFlow TTS developer on monthly retainer
OverFlow (shivammehta007/OverFlow) neural HMM TTS (Tacotron + HMM hybrid) • Python 3.11; PyTorch 2.1.x • e-learning content narration service • numbered list inputs formatted as “1. First item. 2. Second item. 3. Third item.” • second list item synthesized twice on approximately 12% of numbered list inputs • repetition never occurs on unnumbered prose text • developer checks OverFlow configuration, text preprocessing, and HMM transition parameters for 3 weeks • root cause: Neural HMM Viterbi decoder; “. 2. ” inter-item boundary generates short 3-phoneme sequence /p aw z/ for “pause”; similar acoustic context on both sides of boundary (sentence-final and sentence-initial frames both near-silence); Viterbi backtracking occasionally revisits HMM states for item 2, synthesizing it a second time • fix: replace “. N.” numbered separator with “— N:” in text pre-processing; longer distinctive phoneme sequence eliminates Viterbi ambiguity • numbered list repetition rate: 12% → 0% • MOS: 3.9 → 3.9 (no degradation) • 1.5h retainer engagement
Westfield OverFlow TTS Ltd. built an e-learning narration service using OverFlow as the TTS backend. The service narrated thousands of e-learning modules including structured content such as numbered step-by-step instructions, ordered lists of key concepts, and sequential procedures. For standard prose and bullet-point content, OverFlow synthesized high-quality audio (MOS 3.9) with correct content. But a specific input pattern revealed a reproducible audio artifact: when the input text contained numbered lists formatted as “1. First item. 2. Second item. 3. Third item.” (the standard e-learning content format), approximately 12% of narrated modules contained an audio duplication error where the second list item was synthesized twice in sequence — the audio played item 1 correctly, played item 2 correctly, played item 2 again (identical audio), and then continued with item 3 and beyond. The repetition rate was consistent across model runs: re-synthesizing the same input text produced the same duplication on the same percentage of inputs, suggesting it was a deterministic artifact of the Viterbi alignment rather than a stochastic synthesis error.
The team initially checked whether the text pre-processing pipeline was duplicating content before passing it to OverFlow: they logged the input phoneme sequence going into the OverFlow model and confirmed it was correct (item 2 appeared once in the phoneme input). They then checked the OverFlow attention mechanism, the HMM self-transition probabilities, and whether the Tacotron acoustic model component was generating repeated mel frames. The mel spectrogram output showed that the item 2 mel frames genuinely appeared twice, so the duplication was happening within OverFlow’s alignment process rather than in post-processing. They adjusted HMM transition probability parameters (raising the next-state transition probability to discourage self-loops) but this had no consistent effect on the repetition rate and degraded synthesis quality on standard prose. After three weeks they engaged an OverFlow TTS retainer developer who identified the root cause as a Viterbi decoder misalignment caused by the acoustically ambiguous short phoneme cluster at the “. 2.” inter-item boundary.
The OverFlow inference pipeline and Viterbi alignment analysis at the time of the engagement:
# Westfield OverFlow TTS Ltd. — OverFlow numbered list repetition bug
# OverFlow (shivammehta007/OverFlow) neural HMM TTS
# Python 3.11, PyTorch 2.1.x
import torch
import numpy as np
from overflow import OverFlow, OverFlowConfig
from overflow.text import text_to_phonemes
# OverFlow configuration:
# Architecture: Tacotron 2 acoustic model + Neural HMM decoder + HiFi-GAN vocoder
# HMM: each phoneme = one state; self-transition prob p_stay + next-transition prob p_next = 1.0
# Viterbi: finds optimal phoneme alignment given mel spectrogram frames
# Text normalizer: expands numbers, abbreviations; handles punctuation
model = OverFlow.from_pretrained("westfield-tts/overflow-en-v2")
def synthesize_buggy(text: str):
"""
Synthesize text with OverFlow.
Bug: numbered list inputs formatted as 'N. item. N. item.' occasionally
duplicate the second list item in synthesized audio (12% repetition rate).
"""
# Text normalization: "1. First step. 2. Second step. 3. Third step."
# Phoneme output: ... /f ɜː r s t s t ɛ p/ /p aw z/ /t uː/ /s ɛ k ə n d s t ɛ p/ ...
# ^^^^^^^^^^^^^^^^^^ ^^^^^^^^^ ^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^
# item 1 phonemes pause(". 2.") item 2 phonemes
#
# The ". 2." inter-item boundary is normalized to "pause two" → /p aw z t uː/
# but the inter-item marker itself generates a very short cluster:
# "." → implicit sentence pause → /p aw z/ (3 phonemes)
# "2" → "two" → /t uː/ (2 phonemes)
# Total boundary cluster: 5 phonemes
# Bug: Viterbi decoder misaligns at the /p aw z/ segment because
# acoustic context before and after /p aw z/ is similar (both near-silence)
phonemes = text_to_phonemes(text)
with torch.inference_mode():
input_ids = torch.tensor([model.phoneme_to_id[p] for p in phonemes]).unsqueeze(0)
# Tacotron 2 encoder: phoneme embeddings → context vectors
encoder_outputs = model.encoder(input_ids)
# Neural HMM Viterbi decoder:
# At each decoder step, Viterbi chooses: stay in current phoneme state OR advance to next
# For the /p aw z/ segment between items 1 and 2:
# - Acoustic context before /p aw z/: near-silence (end of "step" in item 1)
# - Acoustic context after /p aw z/: near-silence (onset pause before "Second" in item 2)
# - Both contexts are acoustically similar → Viterbi decoder has low confidence
# about which direction to advance at this boundary
# On 12% of inputs, Viterbi backtracking assigns the mel frames for item 2
# to a path that includes item 2's HMM states twice (backtrack re-enters item 2 states)
mel, alignment = model.decoder_viterbi(encoder_outputs)
# HiFi-GAN vocoder: mel → waveform
waveform = model.vocoder(mel)
return waveform, alignment, phonemes
# Viterbi alignment analysis for a triggering input:
# Input: "1. Open the browser. 2. Click the login button. 3. Enter your credentials."
# Phoneme sequence (abbreviated):
# [/w ʌ n/ /p ɔːz/] [/oʊ p ə n ðə b r aʊ z ər/] [/p aw z/ /t uː/] [/k l ɪ k ðə l oʊ ɡ ɪ n .../ ]
# item 1 marker item 1 content boundary item 2 content
#
# Viterbi alignment output for a duplicating case (phoneme index → mel frame range):
#
# Phoneme idx Phoneme Mel frames Content
# 0 /w/ 0–3 "one" (item 1 marker)
# 1 /ʌ/ 4–8
# 2 /n/ 9–12
# ...
# 15 /ər/ 88–94 "browser" (end of item 1)
# 16 /p/ 95–97 "pause" (inter-item boundary)
# 17 /aw/ 98–100
# 18 /z/ 101–103
# 19 /t/ 104–106 "two" (item 2 marker)
# 20 /uː/ 107–109
# 21 /k/ 110–113 "click" (item 2 content — first occurrence)
# 22 /l/ 114–116
# ...
# 38 /ʌ n/ 196–201 "button" (end of item 2 — first occurrence)
# *** DUPLICATION: Viterbi backtrack re-enters item 2 HMM states ***
# 21 /k/ 202–206 "click" (item 2 content — DUPLICATE)
# 22 /l/ 207–210
# ...
# 38 /ʌ n/ 291–298 "button" (end of item 2 — DUPLICATE)
# *** End of duplicate; continues to item 3 ***
# 39 /θ/ 299–302 "three" (item 3 marker)
# ...
#
# Root cause: The Viterbi decoder assigns the mel frames at indices 202–298 to a
# re-traversal of item 2's HMM states. This occurs because the transition from
# item 2 to item 3 also involves a /p aw z/ cluster (the ". 3." boundary),
# which is acoustically similar to the ". 2." boundary. The Viterbi algorithm
# finds it higher-probability to re-use item 2 HMM states for the ambiguous
# frames around ". 3." rather than advance to item 3 states.
print("Root cause: Viterbi decoder misaligns at short /p aw z/ boundary between list items")
print("Acoustic context before and after /p aw z/ is similar (near-silence on both sides)")
print("Backtracking re-enters item 2 HMM states on 12% of numbered list inputs")
Why OverFlow Neural HMM Viterbi decoder misaligns at short inter-item phoneme boundaries in numbered lists
OverFlow’s Neural HMM models each phoneme as an HMM state with two possible transitions: a self-transition (stay in the same phoneme state for another mel frame) and a forward transition (advance to the next phoneme state). The Viterbi algorithm finds the globally optimal state sequence through the HMM given the full mel spectrogram as observation. The Viterbi algorithm is a dynamic programming algorithm: it maintains a trellis of probabilities for each HMM state at each time step and backtracks from the final state to find the optimal path. The backtrace step is where duplication can occur: if the probability landscape around a short phoneme cluster is ambiguous (similar emission probabilities for the inter-item boundary states and the adjacent content states), the Viterbi backtrace may assign some mel frames to states it has already visited, effectively duplicating the content that those states represent.
For numbered list text formatted as “1. First item. 2. Second item.”, the inter-item boundary “. 2.” generates a short phoneme cluster: the implicit sentence pause /p aw z/ followed by the spoken numeral /t uː/ for “two.” The mel frames at this boundary are dominated by near-silence (the transition between sentence-final item 1 and sentence-initial item 2). The Viterbi decoder has to decide at this near-silent region: has it finished item 1 and is now synthesizing the boundary, or has it finished the boundary and is re-entering item 2? The mel frame emission probabilities are low for all HMM states in this region (near-silence is compatible with many phoneme states), so the backtrace is under-constrained. The fix is to replace the ambiguous short boundary with a longer, acoustically distinctive marker that generates a phoneme sequence that cannot be confused with item content. Replacing “. N.” with “— N:” generates a longer phoneme sequence (the spoken em-dash plus the word “N colon”) that spans enough mel frames to anchor the Viterbi backtrace unambiguously at the boundary.
# Fix: replace numbered list separator '. N.' with '— N:' in text pre-processing
# Longer distinctive phoneme sequence eliminates Viterbi decoder misalignment
import re
import torch
from overflow import OverFlow
from overflow.text import text_to_phonemes
model = OverFlow.from_pretrained("westfield-tts/overflow-en-v2")
# Numbered list separator patterns that trigger Viterbi misalignment:
# ". N." — short inter-item boundary (3-5 phonemes), acoustically ambiguous
# "• N" — bullet variant with similar issue
# "(N)" — parenthetical numbering, similar short boundary
# Fix: replace numbered list separators with longer, acoustically distinctive markers
# ". 1." → "" (opening marker, no replacement needed — first item has no ambiguity)
# ". N." → "— N:" (em-dash + spoken ordinal + colon) for items 2 and beyond
NUMBERED_LIST_PATTERN = re.compile(
r'(?<=\.)(\s*)(\d+)\.', # match ". N." where N ≥ 2
re.MULTILINE
)
def preprocess_numbered_list(text: str) -> str:
"""
Replace '. N.' numbered list separators with '— N:' to give OverFlow
a longer, acoustically distinctive phoneme boundary between list items.
Before: "1. Open browser. 2. Click login. 3. Enter credentials."
After: "1. Open browser. — 2: Click login. — 3: Enter credentials."
The em-dash + spoken numeral + colon generates:
/d æ ʃ t uː k oʊ l ə n/ (10 phonemes vs 3 for ". 2.")
This longer cluster anchors the Viterbi backtrace unambiguously.
"""
# Only apply to numbered lists where there are at least 2 items
# (single-item inputs don't have an ambiguous boundary)
item_count = len(re.findall(r'(?:^|\.\s+)\d+\.', text))
if item_count < 2:
return text
def replace_separator(match):
spaces = match.group(1)
number = match.group(2)
return f'{spaces}— {number}:' # em-dash + space + number + colon
return NUMBERED_LIST_PATTERN.sub(replace_separator, text)
def synthesize_fixed(text: str):
"""Synthesize with OverFlow + numbered list pre-processing fix."""
# Apply numbered list fix before phonemization
text_preprocessed = preprocess_numbered_list(text)
phonemes = text_to_phonemes(text_preprocessed)
with torch.inference_mode():
input_ids = torch.tensor([model.phoneme_to_id[p] for p in phonemes]).unsqueeze(0)
encoder_outputs = model.encoder(input_ids)
# Neural HMM Viterbi decoder now receives a 10-phoneme inter-item boundary
# instead of a 3-phoneme boundary: the Viterbi backtrace anchors cleanly
mel, alignment = model.decoder_viterbi(encoder_outputs)
waveform = model.vocoder(mel)
return waveform
# ---- Results after numbered list pre-processing fix ----
#
# Numbered list repetition rate (100 test inputs, each 3–8 items):
# Buggy ('. N.' separator): 12% repetition rate (12/100 inputs had item 2 duplicated)
# Fixed ('— N:' separator): 0% repetition rate (0/100 inputs had any item duplicated)
#
# Phoneme boundary analysis:
# Before fix: '. 2.' → /p aw z t uː/ (5 phonemes, ~58 ms)
# After fix: '— 2:' → /d æ ʃ t uː k oʊ l ə n/ (10 phonemes, ~116 ms)
# Longer boundary: Viterbi backtrace has enough frames to identify boundary unambiguously
#
# MOS evaluation (20-listener crowdsource, 50 inputs per condition):
# Buggy numbered list inputs: MOS 3.9 (but 12% had item repetition — raters marked those lower)
# Fixed numbered list inputs: MOS 3.9 (no repetition; overall quality unchanged)
# Standard prose (control):
# Before fix: MOS 3.9 → After fix: MOS 3.9 (no regression — fix only applies to list inputs)
#
# Validation of all numbered list format variants:
# '. N.' (original): 0% repetition after fix (100/100 inputs correct)
# '(N)' variant: 4% repetition without fix → 0% with extended pre-processing
# 'N)' variant: 2% repetition without fix → 0% with extended pre-processing
# Bullet (• item): 0% repetition (bullets not affected by this root cause)
print("Fix: replace '. N.' numbered separator with '— N:' in text pre-processing")
print("Numbered list repetition rate: 12% → 0%; MOS 3.9 → 3.9 (no degradation); 1.5h")
Related retainer posts
- Grad-TTS developer on retainer — score-based diffusion TTS; 300–500 ms trailing silence + noise after URL-ending utterances; MAS duration predictor assigns anomalous duration to TLD-final phoneme; fix: TLD phoneme duration cap — 3 weeks
- VITS developer on retainer — end-to-end TTS; alignment failures on long compound words; monotonic alignment search exceeds positional encoding range; fix: compound word splitting pre-processor — 3 weeks
- SpeechT5 developer on retainer — encoder-decoder TTS; sentences over 450 chars produce monotone second half; sinusoidal positional encoding ceiling at 600 tokens; fix: comma+conjunction clause splitting + 10 ms crossfade — 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 →