# speechSynthesis.getVoices() fingerprint: 199 vs 180

> speechSynthesis.getVoices() lists 199 voices in Google Chrome on our Mac and 180 in any unbranded Chromium. A page reads the difference in one call.

Source: https://mosaicbrowser.com/blog/chromium-speech-voices

## 199 voices: how a speech list tells Chromium from Chrome

speechSynthesis.getVoices() lists 199 voices in Google Chrome on our Mac and 180 in any unbranded Chromium. A page reads the difference in one call.

![199: Nineteen voices only Google's own build ships, beside a screenshot: mosaic settings showing the installed Tesseract Chrome engine and its version](https://mosaicbrowser.com/covers/chromium-speech-voices.webp)

`speechSynthesis.getVoices()` returned 199 voices in Google Chrome 153 on our Mac and 180 in every unbranded Chromium build of the same version, ours included. The 180 are macOS's own voices and matched exactly. The other 19 are Google's network voices, `Google US English`, `Google Deutsch`, `Google 日本語` and sixteen more, and only Google's branded build has them. A page reads the whole list in one call with no permission, so a browser that says it is Chrome and lists 180 voices has told the page what it really is.

## Where the nineteen come from

The extra voices are not in Chromium's source as voices. They arrive from a component extension that is compiled in only when Chromium is built as Google Chrome, under the `is_chrome_branded` build flag. Every build without that flag, which means every Chromium anyone else compiles, lists only what the operating system provides.

On a Mac those 180 voices are Apple's: Albert, Alice, Alva, Amélie and the rest. Chrome adds its own after them: `Google UK English Female`, `Google UK English Male`, `Google español`, `Google français`, `Google italiano`, `Google polski`, `Google русский`, `Google 한국의` and others. They are network voices, so their `localService` reads `false`, which no Apple voice does.

The list also has a timing quirk that matters for anyone measuring it. Chrome fills it in asynchronously. A call made as the page loads can return an empty array, and the list appears after the `voiceschanged` event fires.

Mosaic is a desktop app

Email yourself the link and open it on your computer.

[How it works: Machines that exist](https://mosaicbrowser.com/features/real-fingerprints)

## How it stayed hidden

We were chasing a verdict at the time. IPHey scored our profiles Unreliable, 90 out of 100, with one signal, an inconsistent browser fingerprint, while a branded Chrome on the same Mac and the same exit, minutes apart, scored Trustworthy at 100. So we compared our engine with that Chrome, surface by surface.

Almost everything matched. Twenty-two fields that depend on branding, including Widevine robustness levels, proprietary codecs, `window.chrome`, plugins and brands, were identical once two launch switches were in place. Twenty-eight deeper fields were identical in both the page and an isolated world: canvas, audio, WebGL parameters and extensions, font metrics, `getClientRects` to twelve decimals, `Intl`, property order, descriptors, native function sources, media queries, scrollbar width. All fourteen request headers matched, in order and case. The TLS ClientHello and the HTTP/2 settings had no differing fields.

The voice list was in that comparison, and that is the uncomfortable part. Our script read it synchronously, before it filled in. It got 0 from Chrome and 0 from our engine, compared 0 with 0, and then carried an exemption for the row: "loads asynchronously; races the probe". Measured, found equal and excused, three ways of not looking at one field. The probe now waits for `voiceschanged`, the exemption is gone, and the first honest read gave 199 against 180.

## The control that should have come first

Before the voices turned up we had spent four days assuming something in our patch series caused the IPHey signal. Google's own unpatched Chrome for Testing 153.0.8010.36 earns the identical verdict. One ninety-second run rules out every patch at once. We now ask whether a stock Chromium already does something before we look for the cause in our own work.

That control also says something about the voices. Chrome for Testing is unbranded, so it lists 180 too. The voice gap is not about our engine at all; it is about Chromium against Chrome, and any browser built from Chromium source that claims to be Chrome carries it unless someone puts the voices back. An [anti-detect browser](https://mosaicbrowser.com/guides/what-is-an-anti-detect-browser) built on Chromium has the same gap until it restores them.

## Putting the voices back

Patch `0145-branded-tts-voices` adds the 19 voices to Tesseract Chrome, our engine, at the point where the branded extension's own voices would arrive: after the platform's voices are collected and before Chrome sorts the list. So Chrome's own rules apply to them unchanged. They are ordered the way Chrome orders them, only the first voice is marked default, and Chrome's gate that hides remote voices on a cellular connection still applies. Names and languages were recorded from the branded Chrome on the same machine, in its order. We did not invent any of them, because an invented voice would be a new tell rather than a removed one.

It fixed the list. It did not, on its own, move IPHey's verdict, whose cause turned out to be somewhere else. We kept the patch anyway: a voice list that disagrees with the browser's name is a difference any page can read in one call, whatever one detector weighs. Why owning the engine is the only place a fix like this can go is in [why we built our own Chromium](https://mosaicbrowser.com/blog/why-we-built-our-own-chromium), and how the rest of a profile is drawn is on [real fingerprints](https://mosaicbrowser.com/features/real-fingerprints).

## Questions

### Why does speechSynthesis.getVoices() return a different number in Chrome and Chromium?

Google Chrome adds network voices such as Google US English from a component that is only built into Google's branded Chrome. On our Mac in September 2026 that was 199 voices for Chrome 153 and 180 for an unbranded build of the same version.

### Why does getVoices() return an empty list?

In Chrome the list fills in asynchronously, so a call made as the page loads can return nothing. Wait for the voiceschanged event and call it again. A fingerprinting script does exactly that.

### Can a page read the voice list without asking permission?

Yes. getVoices() needs no prompt and no click, and the script never has to speak, so the person on the page hears nothing.

### What does Mosaic's engine report?

The machine's own system voices plus the nineteen Google network voices, in Chrome's order. The Google entries were recorded from a branded Chrome on the same machine, and nothing in the list is invented.

## Related

[Why we built our own Chromium instead of injecting JavaScript](https://mosaicbrowser.com/blog/why-we-built-our-own-chromium): Mosaic changes fingerprint values in C++ inside its own engine. Three measured differences between Chrome and plain Chromium explain why that matters.

[Machines that exist: fingerprints drawn from real hardware](https://mosaicbrowser.com/features/real-fingerprints): Mosaic draws each profile's GPU, screen, CPU, memory and fonts from one real device, so the parts of a fingerprint agree with each other and with the host.

[What is an anti-detect browser?](https://mosaicbrowser.com/guides/what-is-an-anti-detect-browser): An anti-detect browser gives each profile its own fingerprint, proxy and cookies, so sites see separate visitors. What it changes, how, and its limits.











## Mosaic, on your own computer

The browser this blog is about: its own Chromium, fingerprints from real machines and a proxy exit per profile. Free holds 3 profiles. Pro is $49 a month for any number.

Mosaic is a desktop app

Email yourself the link and open it on your computer.
