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.
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.
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.
| Field | Value | Why it matters | |
|---|---|---|---|
Text to read aloud | Q3 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 language | en | This tells the remote service which language to synthesize for the audio you hear. | |
Request operation | jQ1olc | This identifies the Google Translate web operation used to turn the requested text into audio. | |
Google web token | SNlM0e=<redacted> | This value is scraped from Google Translate so the internal web request is accepted. |
The shipped code path from active-tab text to Google Translate audio
function 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]
};function $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
} 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
})
} 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()
}
}- translate.google.com
Receives the requested page or selected text through the Google Translate web speech request and returns audio data.