Technical explainer · Web Speech API
Pronouncing basketball names reliably in browser TTS
The browser is only the playback adapter. Reliable pronunciation requires authoritative identity resolution, verified source data, voice-specific rendering, and continuous validation.
Core model
Separate identity, pronunciation, and rendering
Treat pronunciation as a pipeline. A display name is not a pronunciation record. A human phonetic guide is not a machine phoneme instruction. A browser voice is not a stable speech engine contract.
Canonical identity
Resolve the league, season, team context, and player ID before modifying text. Avoid using surnames or nicknames as primary keys.
Verified pronunciation
Prefer player-recorded audio, official league guides, and team media guides. Preserve the source and verification date.
Engine-specific rendering
Tune a spoken alias for the selected browser voice. Do not assume one phonetic spelling will work across iOS, Android, Windows, and macOS.
Failure analysis
What breaks in a naive implementation
The table below maps the most consequential failure modes to their production impact.
| Severity | Failure mode | Why it breaks | Production response |
|---|---|---|---|
| Critical | Guessed pronunciation data | Model-generated syllabification can be plausible and still wrong. | Require official or player-provided provenance. |
| High | Human guide used as TTS input | Uppercase, hyphens, and stress notation are interpreted differently by each engine. | Store a separate voice-tested spoken alias. |
| High | Regex-only replacement | Unicode, possessives, aliases, punctuation, and overlapping names defeat simple boundaries. | Replace by entity spans and player IDs. |
| High | Nickname or surname ambiguity | “Joker,” “Williams,” “Jalen,” and “Murray” are context-dependent. | Resolve against roster, league, season, and game context. |
| High | Assumed browser SSML support | The specification permits SSML, but browser interoperability is unreliable. | Default browser capabilities to no verified phoneme or lexicon support. |
| Medium | Voice selected by localService |
Local delivery says nothing about quality, pronunciation, or deterministic behavior. | Use a validated voice profile and fallback order. |
| Medium | Immediate getVoices() call |
Voice enumeration may initially return an empty list. | Wait for voiceschanged with a bounded timeout. |
| Medium | No queue policy | Repeated taps append stale utterances to the global speech queue. | Define replace, append, or ignore-while-speaking behavior. |
| Medium | Pronunciation injected into accessibility text | Screen-reader output can diverge from Braille and visible content. | Keep visual, accessible, and synthesized text separate. |
| Medium | NBA-only data model | WNBA, NCAA, G League, FIBA, historical players, and coaches use different sources. | Version by organization, league, season, and entity type. |
Reference architecture
Resolve first. Render second. Speak last.
The speech layer should receive already-resolved entity spans and a verified voice-specific rendering decision.
Portable Mermaid architecture source
The visual flow above is rendered in CSS so it survives restricted previews. This equivalent Mermaid source can be reused in architecture documentation.
flowchart LR
A[Content source] --> B[Entity resolver]
B --> C[Pronunciation registry]
C --> D[Voice-specific renderer]
D --> E[Queue controller]
E --> F[SpeechSynthesis]
F --> G[QA and correction telemetry]
G -. regression feedback .-> C
Data design
A pronunciation record needs provenance and runtime overrides
Use a durable identity record. Keep the human guide, source evidence, and voice-specific output separate.
Do not ship a flat Record<string, string>
A plain name-to-string map cannot represent ambiguity, league context, verification status, season changes, or engine-specific output.
type BasketballEntityType =
| "player"
| "coach"
| "official"
| "venue";
type PronunciationRecord = {
entityId: string;
entityType: BasketballEntityType;
league: "NBA" | "WNBA" | "NCAA" | "G_LEAGUE" | "FIBA";
season: string;
displayName: string;
aliases: string[];
source: {
organization: string;
sourceType:
| "self-audio"
| "official-guide"
| "team-media-guide";
sourceUrl?: string;
verifiedAt: string;
};
humanGuide?: string;
confidence: "verified" | "reviewed" | "unverified";
engineOverrides: Record<string, {
voiceURI?: string;
voiceNamePattern?: string;
spokenAlias: string;
testedAt: string;
}>;
};
Use the player ID as the join key
Names are presentation data. The ID links roster data, source evidence, aliases, season history, and engine-specific overrides.
type EntitySpan = {
start: number;
end: number;
entityId: string;
};
function renderSpokenText(
source: string,
spans: EntitySpan[],
aliases: Map<string, string>
): string {
const ordered = [...spans]
.sort((a, b) => a.start - b.start);
let cursor = 0;
let output = "";
for (const span of ordered) {
if (span.start < cursor ||
span.end > source.length) {
continue;
}
output += source.slice(cursor, span.start);
output += aliases.get(span.entityId)
?? source.slice(span.start, span.end);
cursor = span.end;
}
return output + source.slice(cursor);
}
Implementation guidance
Design for inconsistent browser behavior
Browser speech synthesis is stateful, asynchronous, and dependent on the host operating system. Build explicit policies around it.
Load voices defensively
speechSynthesis.getVoices() may be empty during initial page load.
Wait for voiceschanged, but keep the wait bounded.
async function loadVoices(
timeoutMs = 2_000
): Promise<SpeechSynthesisVoice[]> {
const initial = speechSynthesis.getVoices();
if (initial.length > 0) {
return initial;
}
return new Promise(resolve => {
let settled = false;
const finish = () => {
if (settled) return;
settled = true;
speechSynthesis.removeEventListener(
"voiceschanged",
onChange
);
resolve(speechSynthesis.getVoices());
};
const onChange = () => {
if (speechSynthesis.getVoices().length > 0) {
finish();
}
};
speechSynthesis.addEventListener(
"voiceschanged",
onChange,
{ once: true }
);
window.setTimeout(finish, timeoutMs);
});
}
Define an explicit queue policy
Every call to speak() appends to a global queue. Choose whether
new narration replaces, appends, or is ignored while speech is active.
async function speakLatest(
text: string,
voice?: SpeechSynthesisVoice
): Promise<void> {
speechSynthesis.cancel();
const utterance =
new SpeechSynthesisUtterance(text);
if (voice) {
utterance.voice = voice;
utterance.lang = voice.lang;
}
await new Promise<void>((resolve, reject) => {
utterance.onend = () => resolve();
utterance.onerror = event => {
reject(
new Error(
`Speech synthesis failed: ${event.error}`
)
);
};
speechSynthesis.speak(utterance);
});
}
Assume no portable phoneme support
The Web Speech API specification permits SSML documents, but browsers do not provide a dependable capability contract for phonemes or custom lexicons. Treat browser SSML as unavailable until the exact runtime is tested.
type TtsCapabilities = {
engine:
| "web-speech"
| "cloud-ssml"
| "native";
verifiedSsml: boolean;
verifiedPhonemes: boolean;
verifiedLexicons: boolean;
};
const browserCapabilities: TtsCapabilities = {
engine: "web-speech",
verifiedSsml: false,
verifiedPhonemes: false,
verifiedLexicons: false
};
Separate visible, accessible, and synthesized text
Pronunciation hints should not leak into visible content or accessibility labels. Explicit application narration can use a separate speech-only rendering.
type SpeechContent = {
visualText: string;
accessibleText: string;
synthesizedText: string;
};
const example: SpeechContent = {
visualText:
"Nikola Jokić scored 32 points.",
accessibleText:
"Nikola Jokić scored 32 points.",
synthesizedText:
"Nee-koh-lah Yoh-kitch scored thirty-two points."
};
Validation strategy
Test the complete rendering matrix
A pronunciation can pass on one voice and fail on another. Validation must include the player record, operating system, browser, voice, language, and application queue policy.
Required dimensions
- League and season
- Canonical player ID
- Full name, surname, nickname, and possessive forms
- Operating system and browser version
- Selected voice name, URI, and language
- Playback rate and punctuation context
- Queue interruption and repeated-play behavior
- Accessibility and visual-text parity
Regression risk
Browser updates and operating-system voice updates can change pronunciation without application code changes. Preserve tested voice metadata and retest high-value names after platform upgrades.
type PronunciationTestCase = {
entityId: string;
sourceText: string;
expectedEntitySpans: EntitySpan[];
platform: {
os: string;
browser: string;
browserVersion: string;
voiceURI?: string;
voiceName: string;
lang: string;
};
expectedSpokenAlias: string;
reviewer: string;
result: "pass" | "fail" | "needs-review";
testedAt: string;
};
Production readiness
Minimum release gates
These controls are the floor for a production basketball narration system.
Authoritative source
Use official or player-provided pronunciation references.
Versioned identity
Store league, season, entity ID, and verification date.
Separate representations
Keep human guide and engine-specific alias distinct.
Entity spans
Prefer structured player IDs over arbitrary text replacement.
Capability defaults
Assume no browser phoneme, lexicon, or SSML guarantees.
Voice matrix
Test every supported operating system and selected voice.
Lifecycle controls
Handle voice loading, cancellation, errors, and queue policy.
Accessibility separation
Do not inject speech hacks into visible or accessible text.
Correction workflow
Capture human QA and user-reported mispronunciations.
Safe fallback
Prefer the canonical name over an unverified phonetic guess.
Provenance
Source and claim traceability
Generated on July 18, 2026. This explainer synthesizes the prior red-team analysis and preserves the main authoritative references used for the technical claims.
Used for voice enumeration, voiceschanged, queue behavior,
utterance lifecycle, and localService semantics.
Used as the reference model for official, season-specific player pronunciation guidance.
nba.com/news/2025-26-nba-player-pronunciation-guideUsed for browser pronunciation limitations and accessibility separation concerns.
w3c.github.io/pronunciation/gap-analysis_and_use-case/Used to support the caution against treating browser SSML and phoneme behavior as interoperable.
issues.chromium.org/issues/41361142Methodology
Claims were grouped into API behavior, pronunciation-data provenance, entity resolution, accessibility, and runtime validation. The architecture favors deterministic upstream resolution and conservative browser capability defaults.