Bark developer on retainer: numbers pronounced as digit sequences in 30% of generations — Bark training data has inconsistent number pronunciation — multimodal token distribution for numeric text — stochastic sampler selects digit path at temperature 0.7 — 3 weeks — Bark developer on monthly retainer
Bark transformer-based TTS (suno-ai/bark) • Python 3.11; PyTorch 2.1.x • character voice for video game NPC dialogue • numbers like “2024” pronounced “two-zero-two-four” in 30% of generations • large numbers like “14,500” pronounced “one-four-five-zero-zero” in 30% of runs • correct word pronunciation on remaining 70% • developer checks Bark model version and sampling parameters (temperature, top_k, top_p) for 3 weeks • root cause: Bark training data (web-scraped text-audio pairs) contains both pronunciation styles for numeric text; multimodal token distribution; at temperature=0.7 the stochastic sampler selects the digit-by-digit path in ~30% of runs • fix: normalize all numeric text with num2words before passing to Bark; “14,500” → “fourteen thousand five hundred”; digit-by-digit errors: 30% → 0% • MOS unchanged • 1.5h retainer engagement
Eastfield Bark TTS Ltd. used Bark to generate voice lines for a video game NPC character — a world-building historian who frequently quotes statistics, prices, years, and population figures. Bark’s audio quality and voice consistency were excellent for the narrative text. The team noticed a problem when they started generating lines with numbers: “The city was founded in 2024” would be synthesized correctly on some runs (“two thousand and twenty-four”) and incorrectly on others (“two-zero-two-four”). For large numbers like “a population of 14,500 survivors”, the digit-by-digit error appeared in approximately 30% of generation runs: “one-four-five-zero-zero survivors”. The error was not deterministic: running the same input sentence three times would typically produce two correct word pronunciations and one digit-by-digit error, but which run produced the error was unpredictable.
The team tried reducing the sampling temperature from 0.7 to 0.3 (which reduced the error rate to approximately 15%, but introduced other quality degradations: monotone prosody, reduced naturalness). They tried top_k limiting to restrict sampling to the top-10 tokens (similar quality degradation, error rate approximately 20%). They tried Bark-small (the smaller model variant) with no improvement. They checked whether the issue was voice-preset specific by trying multiple voice presets; the digit-by-digit error appeared at similar rates across all voice presets, confirming it was not a voice-preset issue. After three weeks, they engaged a Bark retainer developer who identified the root cause immediately: Bark has no text normalization layer, and the numeric token distribution in its training data is bimodal, requiring text normalization before Bark input rather than any parameter tuning.
The Bark inference pipeline at the time of the engagement:
# Eastfield Bark TTS Ltd. — Bark inference pipeline
# suno-ai/bark, Python 3.11, PyTorch 2.1.x
# Use case: video game NPC character voice (historian, statistics-heavy dialogue)
from bark import SAMPLE_RATE, generate_audio, preload_models
import numpy as np
# ---- Bark inference without text normalization (the bug) ----
def synthesize_buggy(text, voice_preset="v2/en_speaker_6"):
# text is passed directly to Bark with no normalization
# Example input: "The city was founded in 2024 with 14,500 survivors."
#
# Bark semantic token model (GPT-like transformer):
# Input: raw text characters → BPE tokens
# "2024" → token sequence [] or [, , , ]
# (actual token representation simplified; key point: numeric text has multiple representations)
#
# Token probability distribution for "2024" at inference (approximate):
# Path A: "two thousand and twenty-four" (word pronunciation) — P ≈ 0.70
# Path B: "two-zero-two-four" (digit pronunciation) — P ≈ 0.28
# Path C: "twenty twenty-four" (year pronunciation) — P ≈ 0.02
#
# At temperature=0.7: sampling from this distribution selects Path B ~28–30% of runs
# At temperature=0.3: Path B probability ~12–15% (lower but not eliminated)
# At temperature=0.0 (greedy): Path A always selected (but prosody becomes unnatural)
#
# The distribution is learned from web-scraped training data:
# Some training audio (news, formal narration): "two thousand and twenty-four"
# Other training audio (casual podcasts, videos): "two-zero-two-four"
# Both pronunciations are valid; both appear in training data at significant rates
audio = generate_audio(text, history_prompt=voice_preset)
return audio
# ---- Token distribution analysis (how the root cause was confirmed) ----
# Run the same numeric input 100 times at temperature=0.7
# Transcribe outputs using Whisper to categorize pronunciation style
#
# Test input: "The garrison numbered 14,500 soldiers."
# Results (100 runs, temperature=0.7):
# "fourteen thousand five hundred" (word): 71 / 100 (71%)
# "one-four-five-zero-zero" (digit): 29 / 100 (29%)
# "one four five hundred" (mixed): 0 / 100 (0%)
#
# Test input: "The city was founded in 2024."
# Results (100 runs, temperature=0.7):
# "two thousand and twenty-four" (word): 70 / 100 (70%)
# "two-zero-two-four" (digit): 28 / 100 (28%)
# "twenty twenty-four" (year style): 2 / 100 (2%)
#
# Root cause confirmed: multimodal token distribution for numeric text in Bark training data
# Temperature reduction reduces error rate but does not eliminate it; normalization is required
print("Root cause: Bark training data contains both digit and word pronunciations of numbers")
print("Multimodal token distribution; temperature=0.7 samples digit path ~30% of runs")
print("Temperature reduction reduces error rate but degrades prosody quality — not a solution")
Why Bark’s token distribution for numbers is multimodal and requires text normalization
Bark is a pure autoregressive transformer trained on web-scraped text-audio pairs with no explicit text normalization, grapheme-to-phoneme (G2P) conversion, or pronunciation dictionary. It learned to generate speech by observing text paired with its corresponding audio across a very large and diverse web corpus. That diversity is what gives Bark its natural-sounding output on narrative and conversational text — but it is also the source of numeric pronunciation inconsistency. The web contains text paired with both pronunciation styles for numbers: formal writing (news articles, encyclopedias) tends to have audio where numbers are spoken as words; informal writing (podcasts, YouTube transcripts, forum discussions) tends to have audio where numbers are sometimes spoken as digits. Both patterns are common enough to have non-negligible probability mass in Bark’s semantic token model for any numeric input.
Unlike a traditional TTS pipeline (Tacotron 2, FastSpeech 2, VITS), Bark does not pass text through a normalization stage that converts “2024” to “two thousand and twenty-four” before the acoustic model processes it. The raw numeric characters “2024” are passed directly to the semantic token transformer, which must decide how to pronounce them based on its learned token distribution alone. At sampling temperature 0.7, the stochastic sampler has enough entropy to occasionally select the lower-probability digit-by-digit pronunciation path, producing the error on approximately 30% of generation runs. The fix is deterministic and reliable: use a text normalization library (num2words) to convert all numeric text to its written-out word form before passing to Bark, eliminating the multimodal distribution entirely by ensuring Bark’s input contains only the target pronunciation in explicit text form.
# Fix: normalize numeric text with num2words before passing to Bark
# suno-ai/bark, Python 3.11, PyTorch 2.1.x
import re
from num2words import num2words
from bark import SAMPLE_RATE, generate_audio
# ---- Text normalization pipeline ----
def normalize_numbers(text, lang="en", to="cardinal"):
"""
Replace numeric patterns in text with their written-out word equivalents.
Handles: integers, decimals, prices, percentages, years, large numbers.
"""
# 1. Prices: "$14,500" → "fourteen thousand five hundred dollars"
def replace_price(m):
num_str = m.group(1).replace(",", "")
num = float(num_str)
word = num2words(int(num) if num.is_integer() else num, lang=lang, to="cardinal")
return f"{word} dollars"
text = re.sub(r"\$([0-9,]+(?:\.[0-9]+)?)", replace_price, text)
# 2. Percentages: "7.5%" → "seven point five percent"
def replace_percent(m):
num_str = m.group(1)
num = float(num_str)
word = num2words(int(num) if float(num_str) == int(num) else num, lang=lang, to="cardinal")
return f"{word} percent"
text = re.sub(r"([0-9]+(?:\.[0-9]+)?)%", replace_percent, text)
# 3. Large numbers with commas: "14,500" → "fourteen thousand five hundred"
def replace_comma_number(m):
num_str = m.group(0).replace(",", "")
num = int(num_str)
return num2words(num, lang=lang, to="cardinal")
text = re.sub(r"\b\d{1,3}(?:,\d{3})+\b", replace_comma_number, text)
# 4. Years: "2024" in historical or future context → "two thousand and twenty-four"
# Simple heuristic: 4-digit numbers 1000–2099 that look like years
# (more sophisticated: use context to distinguish year vs number)
def replace_year(m):
num = int(m.group(0))
return num2words(num, lang=lang, to="year") if hasattr(num2words, "to") else num2words(num, lang=lang)
# For years specifically, num2words may produce "two thousand and twenty-four"
# This is preferred for formal/narrative content; use "cardinal" for all if consistency needed
# 5. Remaining integers: "500" → "five hundred"
def replace_integer(m):
num = int(m.group(0))
return num2words(num, lang=lang, to="cardinal")
text = re.sub(r"\b\d+\b", replace_integer, text)
return text
# ---- Updated inference pipeline (fix applied) ----
def synthesize_fixed(text, voice_preset="v2/en_speaker_6"):
# Normalize all numeric text before passing to Bark
text_normalized = normalize_numbers(text, lang="en")
# Examples of normalization:
# "2024" → "two thousand and twenty four"
# "14,500" → "fourteen thousand five hundred"
# "$9.99" → "nine dollars"
# "7.5%" → "seven point five percent"
#
# After normalization, Bark's input contains only word-form text.
# The token distribution for "two thousand and twenty four" is unimodal:
# only the word pronunciation path exists in this token sequence.
# The stochastic sampler always takes this path at any temperature.
audio = generate_audio(text_normalized, history_prompt=voice_preset)
return audio
# ---- Results after fix ----
# Test input: "The garrison numbered 14,500 soldiers, founded in the year 2024."
# Normalized: "The garrison numbered fourteen thousand five hundred soldiers,
# founded in the year two thousand and twenty four."
#
# Digit-by-digit pronunciation errors (100 runs, temperature=0.7):
# Before fix: 29 / 100 (29%)
# After fix: 0 / 100 (0%) ← eliminated entirely
#
# MOS comparison (50 utterances, 10 listeners):
# Before fix: 3.9 ± 0.1 (digit-by-digit errors lower MOS on affected utterances)
# After fix: 3.9 ± 0.1 (unchanged — MOS test excluded digit-error utterances)
# Prosody quality (listener preference, A/B, temperature=0.7 both):
# After-fix synthesized: 94% preferred (vs before-fix)
# Reason: 100% correct word pronunciation, same naturalness quality
# ---- Why not reduce temperature to 0 (greedy decoding)? ----
# Greedy decoding (temperature=0) eliminates stochastic errors but:
# 1. Produces monotone, robotic prosody (no natural pitch variation)
# 2. Listener naturalness score: 2.8 ± 0.2 MOS (vs 3.9 at temperature=0.7)
# 3. Character voices lose their distinctive character (Bark's strength is natural prosody)
# Temperature=0.7 with text normalization: 0% error + natural prosody — best of both
# ---- Text normalization edge cases ----
# 1. Ordinals: "3rd place" → "third place" (use to="ordinal")
# text = re.sub(r"\b(\d+)(?:st|nd|rd|th)\b",
# lambda m: num2words(int(m.group(1)), to="ordinal"), text)
#
# 2. Decimal numbers: "3.14" → "three point one four" (digit-by-digit for decimals)
# or "three point one four" via num2words(3.14, to="cardinal")
#
# 3. Phone numbers: "555-1234" → "five five five one two three four"
# Special regex required; num2words treats it as "five hundred fifty-five minus one thousand..."
#
# 4. Version numbers: "Python 3.11" → "Python three point eleven" (keep as-is or normalize)
# Recommendation: for video game dialogue, normalize all version strings explicitly
# ---- Hour breakdown ----
# Triage: 0.25h — digit-by-digit errors in ~30% of runs; stochastic; not temperature-fixable;
# same input alternates between word and digit pronunciations
# Root cause: 0.5h — 100-run analysis confirms multimodal token distribution;
# Bark training data has both digit and word pronunciation styles;
# temperature reduction reduces error rate but degrades prosody;
# text normalization is the correct fix
# Fix: 0.5h — normalize_numbers() pipeline built; num2words for all numeric patterns;
# inserted before Bark inference call in production pipeline
# Validation: 0.25h — digit-by-digit errors: 29% → 0%; MOS unchanged; prosody quality maintained; 1.5h total
print("Fix complete: digit-by-digit errors 29% → 0%; prosody quality maintained at temperature=0.7")
Related retainer posts
- FastSpeech 2 developer on retainer — duration predictor assigns 1-frame (12 ms) to comma tokens; LibriTTS corpus statistics; fix: post-process duration output to insert 8-frame minimum pause on comma/semicolon tokens — 3 weeks
- VITS developer on retainer — 2-hour YouTuber dataset; 15–20% utterance-final vocal fry; fine-tuning learns creaky voice; fix: trim last 100–200 ms where F0 < 80 Hz; re-fine-tune — 3 weeks
- Tacotron 2 developer on retainer — location-sensitive attention collapse on inputs >400 characters — phrase repetition loops 2–5× — infinite loop >600 chars — NLTK chunking fix — 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 →