Is Voice Out: Text to Speech Voice Reader safe?

Medium risk

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.

Dictanotev1.33Chrome Web Store
45Risk

AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.

Publishers can request a review.

Findings

SeverityMEDIUM
ClassUNWANTED
TypeUnexpected
CWECWE-359
SourceAI SANDBOX

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.

01EvidenceCAUSE EFFECT
What actually happens
You did this

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 did this

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.

02EvidenceFIELD TABLE
Fields built for the Google Translate speech request
FieldValueWhy 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
enThis tells the remote service which language to synthesize for the audio you hear.
Request operation
jQ1olcThis 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.
03EvidenceNETWORK CAPTURE
Captured request
POSThttps://translate.google.com/_/TranslateWebserverUi/data/batchexecute
The code parses the response as JSON and returns base64 MPEG audio for playback.
04EvidenceCODE COMPARE
The code that does this

The shipped code path from active-tab text to Google Translate audio

What it actually does
Readable player2.js request builder and synthesizerplayer2.js
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]
};
Readable player2.js GoogleTranslate provider selectionplayer2.js
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
  }
Readable background.js active-tab handoffbackground.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 functionsjs/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()
    }
  }
05EvidenceTHIRD PARTY LIST
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.

Data recipients

translate.google.comvoiceout.appapp.posthog.com
Updated 17 September 2026jmodgcjbfcmningbahdmedofbabejbba