speech synthesis voices (host OS speech engine)
Check id speech-voices
What an ordinary browser yields
Contextual only: a voice list is reported if the host speech engine exposes one.
What a detector infers
speechSynthesis.getVoices() enumerates the host operating system's speech engine — a resource stealth stacks essentially never touch, which makes it a useful window onto the real environment underneath a configured browser. The check records how many voices are present, how many distinct languages they span, and which one is marked default. It is deliberately informational and contributes nothing to the score: a non-empty list reports true, and an empty list reports N/A rather than a failure. The reason is that emptiness has too many innocent causes to attribute — Linux hosts without speech-dispatcher, VDI sessions, Windows N editions, offline machines, or simply voices that haven't finished loading at probe time — so treating it as evidence would produce false positives on ordinary desktops.
How to resolve it
Nothing to fix — this cell is contextual and unscored. Read it as a description of the host: the voice set and its languages reflect the real OS speech engine, so compare it against the platform and locale you intend to present. An empty list is not a defect and is not counted against the result.
Read in the wild by
PerimeterX / HUMAN
Enumerates speechSynthesis.getVoices(), re-reading the list via onvoiceschanged when it comes back empty, and hashes each voice's voiceURI, name, lang, localService and default into two separate payload fields: one covering every voice, and a second that excludes voices whose name contains "Google" or "Microsoft" — tagged browser and device respectively, separating browser-bundled voices from the host OS's own. A flag-gated routine additionally cross-checks the list against UA-CH client hints: when the brand list claims Google Chrome and the form factor is desktop but no voice name matches /google/i, it injects a hidden iframe probe pointing at an additional PerimeterX-controlled endpoint.
PerimeterX collector main.min.js (PX_RE corpus, stample/ifood/source/main.min.js; confirmed PX by _pxAppId, _pxJsClientSrc, regex "perimeterx\.net|a\.pxi\.pub", PX##### codes). Obfuscated strings resolve through the bundle's basE91 table (hP, 1152 entries) via `function hQ(t){return void 0===hO[t]?hO[t]=hM(hP[t]):hO[t]}`. COLLECTION: `qI(){try{pn=hS.speechSynthesis.getVoices(),hS[hQ(195)][hQ(196)]=function(){(!pn||pn&&0===pn.length)&&(pn=hS[hQ(195)].getVoices())}}catch(t){}}` with hQ(195)='speechSynthesis', hQ(196)='onvoiceschanged'. HASHING: `DW(){if(!qE())return{browser:iR(pd),device:iR(pd)};for(var t="",n="",e=0;e<pn.length;e++){var r=pn[e];n+=r.voiceURI+r[hQ(158)]+r.lang+r.localService+r[hQ(1009)],r.name&&-1===r.name[hQ(240)](DK)&&-1===r.name.indexOf(DL)&&(t+=r.name)}return{browser:iR(n),device:iR(t)}}` with hQ(158)='name', hQ(1009)='default', hQ(240)='indexOf', DK=jb("R29vZ2xl")='Google', DL=jb("TWljcm9zb2Z0")='Microsoft'; emitted as payload fields `var r=DW();n[hQ(1008)]=r.browser,n["BFAxGkEwMS4="]=r.device` (hQ(1008)='ICxVJmVMVRE='). UA-CH GATE (`Gw()`, invoked flag-gated as `nf(mU[lh])&&Gw()`): `hU[hQ(191)]&&hU.userAgentData[hQ(1066)]&&hS[hQ(195)]&&hS[hQ(195)].getVoices&&hU.userAgentData[hQ(1066)]([hQ(1067),"formFactor",hQ(1068)]).then(...n[4]=n.a&&n.a[hQ(781)]((function(t){return new RegExp(hQ(1069),"i").test(t.brand)})),...n[61]=n[5]&&n[5].some((function(){...return new RegExp("desktop","i")[hQ(236)](t[0])})),n[-34]=function(){...t(hS.speechSynthesis.getVoices().some((function(t){return new RegExp(hQ(1070),"i")[hQ(236)](t.name)})));hS.speechSynthesis.getVoices().length>0?e():hS.speechSynthesis.addEventListener(hQ(1071),e,((n={})[hQ(220)]=!0,n))},n[-34]().then((function(t){if(n[4]&&n[61]&&!t){var e=jb("aHR0cHM6Ly9jbGllbnQud3JhLWFwaS5uZXQ="),r=jb("SFlVMjMyL2lmcmFtZS5odG1s"),...i[hQ(1073)]=h,i.style.display="none"` — hQ(191)='userAgentData', hQ(1066)='getHighEntropyValues', hQ(1067)='brands', hQ(1068)='formFactors', hQ(781)='some' (NOT 'filter'), hQ(1069)='google chrome', hQ(1070)='google', hQ(1071)='voiceschanged', hQ(220)='once', hQ(236)='test', hQ(1073)='src'; base64 endpoints decode to 'https://client.wra-api.net' + 'HYU232/iframe.html'. Same construct in stample/walmart/source/main.min.js (`Am=fT("R29vZ2xl"),An=fT("TWljcm9zb2Z0")`; `nw(){try{mb=eM.speechSynthesis.getVoices(),eM.speechSynthesis.onvoiceschanged=...`) and stample/totalwine/source/main.min.js (`Ed=jw("R29vZ2xl"),Ee=jw("TWljcm9zb2Z0")`).
CreepJS
CreepJS, a research demo rather than a production detector, enumerates speechSynthesis.getVoices() and records the distinct voice languages and the default local voice (filtered on localService), a read on the host OS speech engine.
src/speech/index.ts: `const data = speechSynthesis.getVoices()`, `const languages = [...new Set(dataUnique.map((x) => x.lang))]`, `const defaultLocalVoices = dataUnique.filter((x) => x.default && x.localService)`
Kasada
Enumerates the speech-synthesis voice list.
speech_synthesis_voices
ips.js 427-probe teardown (emro.cat, Apr 2026)
Every attribution traces to a published artifact. See the sources and their limits.
Nearby checks in Environment & locale
- canPlayType answers identically for <video>, <audio> and the prototype
canplaytype-element-agnosticcanPlayType is implemented exactly once, on HTMLMediaElement.prototype, so <video> and <audio> inherit the same C++ function. - canPlayType behaves like a real codec parser (bogus + quoting)
canplaytype-parser-trapsA real canPlayType is an RFC-6381 parser, not a dictionary. This check exercises two properties only a parser has. - AudioContext invariants + a deterministic offline render
audio-context-coherenceThe check constructs a live AudioContext and reads four invariants — sampleRate, baseLatency, the destination node's channelCount against… - the analyser byte spectrum is the defined quantisation of its float spectrum
audio-analyser-float-vs-byteAn AnalyserNode publishes the same spectrum twice. getFloatFrequencyData returns decibels, and getByteFrequencyData returns that spectrum… - the voice list arrives the way Chrome delivers it
speech-voices-async-pathspeech-voices reads WHAT is in the voice list. This reads HOW the list arrived, which is a separate fact and survives a roster whose every… - an OS that ships a speech synthesiser enumerates at least one voice
speech-voices-vs-platformThe plain speech-voices readout above refuses to score an empty list, and as written it is right to: Chrome does not build the voice list in… - a live audio destination corresponds to an audio device that exists
audio-sink-vs-device-listA resource oracle, and deliberately a different resource from the voice list. - WebSQL (openDatabase) is absent for Chrome ≥ 119
opendatabase-removedWebSQL was removed from Chrome in version 119, so window.openDatabase should not exist on any newer Chromium.
Clearcote is a browser built for fingerprint coherence
It is a Chromium fork, maintained by the same people who wrote this reference. It ships as a compiled browser rather than as a stealth script injected into someone else's — which is a description of how it is built, and is not an argument about how it behaves on this check.
This audit takes no position on how Clearcote scores on Environment & locale checks, on this one, or anywhere else. It has no baseline corpus of other people's fingerprints to rank you against and no vendor scoreboard — nearly every check is self-referential, asking one browser the same question through two independent APIs and reporting whether both answers can be true at once. It runs identically on any browser, including ours. Run it on yours and read the result yourself.
See the other checks in Environment & locale — the family speech-voices belongs to.