Is Voice Out: Text to Speech Voice Reader safe?
Voice Out reads the page text or selection you ask it to speak and, on the default free voice, sends it to Google Translate's internal RPC.
When you trigger read-aloud, the extension extracts the active page's body text or your current selection and routes it to whichever voice provider you've selected. The default 'free' voice POSTs that text to Google Translate's undocumented internal batchexecute endpoint (not the documented Cloud TTS API) to retrieve the spoken audio. Pages containing math formulae have their MathML sent to the vendor's own server (voiceout.app/read-aloud/mathml), and usage events tagged with a persistent per-install device ID are sent to PostHog.
Who publishes itDictanote - 4 other listings from the same operator, 1 of them carrying a finding
Dictanote - 4 other listings from the same operator, 1 of them carrying a finding
What this publisher told the store about itself, and the other listings that told it the same thing.
Same store account
4 other listings published from this account, 781k+ users between them. 1 of them carries a finding.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Free TTS sends requested page text to Google Translate
Pressing Play on the GoogleTranslate free voice path makes the extension read the selected or page text and post it to Translate's batchexecute endpoint for audio.
No live body was recorded in testing; POST, URL, f.req shown in code.
What actually happens
You press Play to have the extension read selected text or the active page.
The code waits for this playback action before following the GoogleTranslate free voice path.
The extension reads that text from the active tab and submits it to Google Translate for audio.
The request goes to Google Translate's internal batchexecute web endpoint rather than a documented Cloud Text-to-Speech API.
Fields built for the Google Translate speech request
- Text to read aloudQ3 roadmap: partner beta opens July 22 after legal review. (illustrative)
This is the page text or selection you asked the extension to read, and it is included in the speech request.
- Speech languageen
This tells the remote service which language to synthesize for the audio you hear.
- Request operationjQ1olc
This identifies the Google Translate web operation used to turn the requested text into audio.
- Google web tokenSNlM0e=<redacted>
This value is scraped from Google Translate so the internal web request is accepted.
Captured request
The code parses the response as JSON and returns base64 MPEG audio for playback.
The code that does this
The shipped code path from active-tab text to Google Translate audio
Readable player2.js request builder and synthesizer
player2.jsfunction je(t, e, r) { r || (r = {}), r.tld || (r.tld = "com"), r.tokensTTL || (r.tokensTTL = 3600); var o = "https://translate.google." + r.tld; return Promise.resolve(xe.get("wiz")).then(function(s) { return s && s.timestamp + r.tokensTTL * 1e3 > Date.now() ? s : Ut(o).then(function(a) { return a.timestamp = Date.now(), xe.set("wiz", a), a }) }).then(function(s) { return Nt(s, t, e) }).then(function(s) { if (!r.validateOnly) return s.body.at || delete s.body.at, Ve.post(o + "/_/TranslateWebserverUi/data/batchexecute", { searchParams: s.query, form: s.body, responseType: "text" }).then(function(a) { var n = a.body.match(/\d+/); return a.body.substr(n.index + n[0].length, Number(n[0])) }).then(JSON.parse).then(function(a) { var n = a[0][2]; return JSON.parse(n) }) })}function Ut(t) { var e = { "f.sid": /"FdrFJe":"(.*?)"/, bl: /"cfb2h":"(.*?)"/, at: /"SNlM0e":"(.*?)"/ }; return Ve.get(t).then(function(r) { var o = r.body.indexOf("WIZ_global_data = {"); if (o == -1) throw new Error("Wiz not found"); var s = r.body.indexOf("<\/script>", o); return r.body.substring(o, s) }).then(function(r) { var o = {}; for (var s in e) { var a = e[s].exec(r); a ? o[s] = a[1] : console.warn("Wiz property not found '" + s + "'") } return o })}function Nt(t, e, r) { if (!Array.isArray(r)) throw new Error("Payload must be an array"); return { query: { rpcids: e, "f.sid": t["f.sid"], bl: t.bl, hl: "en", "soc-app": 1, "soc-platform": 1, "soc-device": 1, _reqid: ++Lt * 1e5 + Math.floor(1e3 + Math.random() * 9e3), rt: "c" }, body: { "f.req": JSON.stringify([ [ [e, JSON.stringify(r), null, "generic"] ] ]), at: t.at } }}const Dt = async function() { const t = await ze(); if (t.isDenied()) throw t.renewDenial(), new Error("Service unavailable"); return je("jQ1olc", [], { validateOnly: !0 })}, Vt = async function(t, e) { const r = await ze(); if (r.isDenied()) throw new Error("Server returns 429"); r.use(), console.log("synthesizing", t.slice(0, 25) + "…", e); const o = await je("jQ1olc", [t, e, null]); if (!o) throw new Error("Failed to synthesize text '" + t.slice(0, 25) + "…' in language " + e); return console.log("synthesized", t.slice(0, 25) + "…", e), "data:audio/mpeg;base64," + o[0]};Readable player2.js GoogleTranslate provider selection
player2.jsfunction $t() { var t, e = !1, r; this.ready = function() { return Dt() }, this.speak = function(s, a, n) { a.rateAdjust = 1.1; const i = Promise.resolve().then(function() { return t && t[0] == s && t[1] == a ? t[2] : o(s, a.voice.lang) }); r = W(i, a), r.startPromise.then(() => { n({ type: "start", charIndex: 0 }), e = !0 }).catch(function(c) { n({ type: "error", error: c }) }), r.endPromise.then(() => n({ type: "end", charIndex: s.length }), c => n({ type: "error", error: c })).finally(() => e = !1) }, this.isSpeaking = function(s) { s(e) }, this.pause = this.stop = function() { r && r.pause() }, this.resume = function() { return r ? r.resume() : Promise.resolve() }, this.prefetch = function(s, a) { o(s, a.voice.lang).then(function(n) { t = [s, a, n] }).catch(console.error) }, this.clearPrefetch = function() { z(t), t = null }, this.setNextStartTime = function() {}, this.getVoices = It; function o(s, a) { return H(s && a), Vt(s, a) }} function g() { return tt(e.voice) ? qe : rt(e.voice) ? _e : Ne(e.voice) ? Pe : nt(e.voice) ? He : st(e.voice) ? we : me(e.voice) && !/\s(Hebrew|Telugu)$/.test(e.voice.voiceName) ? We : it(e.voice) ? Me : at(e.voice) ? Fe : ot(e.voice) ? Ge : ye(e.voice) ? we : pe(e.voice) ? new qt(ve, 16 * 1e3) : ve }Readable background.js active-tab handoff
background.js async function U(e) { const t = e ? await M(e) : await k(); if (!t) throw new Error(JSON.stringify({ code: "error_page_unreadable" })); const r = S.begin(); try { const i = ue.find(s => s.match(t.url || "", t.title)); if (i.validate && await i.validate(t), i.getSourceUri) await T("sourceUri", i.getSourceUri(t)); else { const s = i.getFrameId && await ie(t.id).then(o => i.getFrameId(o)); await X(t, s) || await Ee(t, s, i.extraScripts), await T("sourceUri", "contentscript:" + t.id) } } finally { r.end() } await p().then(i => i == !0, i => !1) || await A(t), await l({ method: "playTab" }) } async function Ee(e, t, r) { await chrome.scripting.executeScript({ target: { tabId: e.id, frameIds: t ? [t] : void 0 }, files: ["/js/libs/jquery-3.1.1.min.js", "js/content/defaults.js", "js/content/messaging.js", "js/content/content.js"] }); const n = r || await chrome.tabs.sendMessage(e.id, { dest: "contentScript", method: "getRequireJs" }); await chrome.scripting.executeScript({ target: { tabId: e.id, frameIds: t ? [t] : void 0 }, files: n }) }Readable content extraction functions
js/content/content.js; js/content/html-doc.js async function getTexts(index, quietly) { if (index < 0) { if (index == -100) return (await getSelectedText()).split(paragraphSplitter); else return null; } else { return Promise.resolve(readAloudDoc.getTexts(index, quietly)) .then(function(texts) { if (texts && Array.isArray(texts)) { if (!quietly) consoleLog(texts.join("\n\n")); } return texts; }) } } function getSelectedText() { if (readAloudDoc.getSelectedText) return readAloudDoc.getSelectedText() return window.getSelection().toString().trim(); } this.getTexts = async function(index) { if (index == 0) { const math = await getMath() try { if (math) math.show() return parse() } finally { if (math) math.hide() } } else return null; } this.getSelectedText = async function() { const math = await getMath() try { if (math) math.show() return window.getSelection().toString().trim() } finally { if (math) math.hide() } }Remote service receiving the requested text
- translate.google.com
Receives the requested page or selected text through the Google Translate web speech request and returns audio data.
Where it sends data
Destinations our analysis observed Voice Out contacting. Sending data somewhere is not a finding on its own - an extension that syncs your settings has to talk to its own server - but it is where your data can go, and who else it goes to.
- translate.google.comwidely used
Voice Out sends data to translate.google.com. A widely used service: 52 other extensions we have analyzed send data here.
- app.posthog.com
Voice Out sends data to app.posthog.com. 6 other extensions we have analyzed send data here.
- voiceout.app
Voice Out sends data to voiceout.app. No other extension we have analyzed sends data here.