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.

1
durable key: the player ID
3
separate text channels
0
portable browser phoneme guarantees
10
minimum production release gates

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.

Mermaid · reference architecture
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.

TypeScript · canonical record
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.

TypeScript · resolved spans
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.

TypeScript · voice loader
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.

TypeScript · replace policy
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.

TypeScript · capability policy
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.

TypeScript · three-channel content
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.

TypeScript · test case shape
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.

1

Authoritative source

Use official or player-provided pronunciation references.

2

Versioned identity

Store league, season, entity ID, and verification date.

3

Separate representations

Keep human guide and engine-specific alias distinct.

4

Entity spans

Prefer structured player IDs over arbitrary text replacement.

5

Capability defaults

Assume no browser phoneme, lexicon, or SSML guarantees.

6

Voice matrix

Test every supported operating system and selected voice.

7

Lifecycle controls

Handle voice loading, cancellation, errors, and queue policy.

8

Accessibility separation

Do not inject speech hacks into visible or accessible text.

9

Correction workflow

Capture human QA and user-reported mispronunciations.

10

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.

Methodology

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.