24

Google implemented a captcha to block people from accessing the TTS translate API https://translate.google.com/translate_tts?ie=UTF-8&q=test&tl=zh-TW. I was using it in my mobile application. Now, it is not returning anything. How do I get around the captcha?

ginsengtang
  • 751
  • 1
  • 7
  • 17

5 Answers5

28

Add the qualifier '&client=tw-ob' to the end of your query. https://translate.google.com/translate_tts?ie=UTF-8&q=test&tl=zh-TW&client=tw-ob

This answer no longer works consistently. Your ip address will be blocked by google temporarily if you abuse this too much.

ginsengtang
  • 751
  • 1
  • 7
  • 17
  • 1
    the page is redirected to a captcha if you just do &client – ginsengtang Aug 18 '15 at 14:04
  • Not just "&client" try this "https://translate.google.com/translate_tts?ie=UTF-8&q=test&tl=zh-TW&client=" nothing after the "client=" is important – Sunil Sunny Aug 19 '15 at 07:15
  • 1
    I think you mean you can put anything in the &client='anything', but it has to be something. I was not aware of this; this is great! – ginsengtang Aug 19 '15 at 13:46
  • That did not help me for ajax requests. 404 is being returned. However it works when I open this in browser directly. – AlexHalkin Oct 10 '15 at 22:39
  • @ginsengtang what do you mean with "if you abuse this"? For me I see no problem. Is there anything that I abuse if I use your solution? – Cinzel Jun 27 '16 at 17:48
  • I had my ip address blocked temporarily if I use this workaround too much. If it works for you, then that's great! Try refreshing a lot and eventually you will see the captcha. – ginsengtang Jun 27 '16 at 17:58
  • ok @ginsengtang If you entered / filled out the captcha and continued the sound was working? – Cinzel Jun 27 '16 at 18:15
  • yes, this question is about getting around the captcha though – ginsengtang Jun 27 '16 at 23:40
15

there are 3 main issues:

  1. you must include "client" in your query string (client=t seems to work).
  2. (in case you are trying to retrieve it using AJAX) the Referer of the HTTP request must be https://translate.google.com/
  3. "tk" field changes for every query, and it must be populated with a matching hash: tk = hash(q, TKK), where q is the text to be TTSed, and TKK is a var in the global scope when you load translate.google.com: (type 'window.TKK' in the console). see the hash function at the bottom of this reply (calcHash).

to summarize:

function generateGoogleTTSLink(q, tl, tkk) {
    var tk = calcHash(q, tkk);
    return `https://translate.google.com/translate_tts?ie=UTF-8&total=1&idx=0&client=t&ttsspeed=1&tl=${tl}&tk=${tk}&q=${q}&textlen=${q.length}`;
}

generateGoogleTTSLink('ciao', 'it', '410353.1336369826');
// see definition of "calcHash" in the bottom of this comment.

=> to get your hands on a TKK, you can open Google Translate website, then type "TKK" in developer tools' console (e.g.: "410353.1336369826").

NOTE that TKK value changes every hour, and so, old TKKs might get blocked at some point, and refreshing it may be necessary (although so far it seems like old keys can work for a LONG time).

if you DO wish to periodically refresh TKK, it can be automated pretty easily, but not if you're running your code from the browser.

you can find a full NodeJS implementation here: https://github.com/guyrotem/google-translate-server. it exposes a minimal TTS API (query, language), and is deployed to a free Heroku server, so you can test it online if you like.

function shiftLeftOrRightThenSumOrXor(num, opArray) {
 return opArray.reduce((acc, opString) => {
  var op1 = opString[1]; // '+' | '-' ~ SUM | XOR
  var op2 = opString[0]; // '+' | '^' ~ SLL | SRL
  var xd = opString[2]; // [0-9a-f]

  var shiftAmount = hexCharAsNumber(xd);
  var mask = (op1 == '+') ? acc >>> shiftAmount : acc << shiftAmount;
  return (op2 == '+') ? (acc + mask & 0xffffffff) : (acc ^ mask);
 }, num);
}

function hexCharAsNumber(xd) {
 return (xd >= 'a') ? xd.charCodeAt(0) - 87 : Number(xd);
}

function transformQuery(query) {
 for (var e = [], f = 0, g = 0; g < query.length; g++) {
   var l = query.charCodeAt(g);
   if (l < 128) {
    e[f++] = l;     // 0{l[6-0]}
   } else if (l < 2048) {
    e[f++] = l >> 6 | 0xC0;  // 110{l[10-6]}
    e[f++] = l & 0x3F | 0x80; // 10{l[5-0]}
   } else if (0xD800 == (l & 0xFC00) && g + 1 < query.length && 0xDC00 == (query.charCodeAt(g + 1) & 0xFC00)) {
    // that's pretty rare... (avoid ovf?)
    l = (1 << 16) + ((l & 0x03FF) << 10) + (query.charCodeAt(++g) & 0x03FF);
    e[f++] = l >> 18 | 0xF0;  // 111100{l[9-8*]}
    e[f++] = l >> 12 & 0x3F | 0x80; // 10{l[7*-2]}
    e[f++] = l & 0x3F | 0x80;  // 10{(l+1)[5-0]}
   } else {
  e[f++] = l >> 12 | 0xE0;  // 1110{l[15-12]}
  e[f++] = l >> 6 & 0x3F | 0x80; // 10{l[11-6]}
  e[f++] = l & 0x3F | 0x80;  // 10{l[5-0]}
   }
 }
 return e;
}

function normalizeHash(encondindRound2) {
 if (encondindRound2 < 0) {
  encondindRound2 = (encondindRound2 & 0x7fffffff) + 0x80000000;
 }
 return encondindRound2 % 1E6;
}

function calcHash(query, windowTkk) {
 // STEP 1: spread the the query char codes on a byte-array, 1-3 bytes per char
 var bytesArray = transformQuery(query);

 // STEP 2: starting with TKK index, add the array from last step one-by-one, and do 2 rounds of shift+add/xor
 var d = windowTkk.split('.');
 var tkkIndex = Number(d[0]) || 0;
 var tkkKey = Number(d[1]) || 0;

 var encondingRound1 = bytesArray.reduce((acc, current) => {
  acc += current;
  return shiftLeftOrRightThenSumOrXor(acc, ['+-a', '^+6'])
 }, tkkIndex);

 // STEP 3: apply 3 rounds of shift+add/xor and XOR with they TKK key
 var encondingRound2 = shiftLeftOrRightThenSumOrXor(encondingRound1, ['+-3', '^+b', '+-f']) ^ tkkKey;

 // STEP 4: Normalize to 2s complement & format
 var normalizedResult = normalizeHash(encondingRound2);

 return normalizedResult.toString() + "." + (normalizedResult ^ tkkIndex)
}

// usage example:
var tk = calcHash('hola', '409837.2120040981');
console.log('tk=' + tk);
 // OUTPUT: 'tk=70528.480109'
Guy Rotem
  • 151
  • 2
  • 6
  • Hi Guy did u find any solution? – diank Feb 22 '17 at 08:26
  • 1
    if you combine the 2 code snippets, given _query_, _target language_ and _TKK_ (e.g.: 413273.3992056628), you can generate a valid link for the TTS audio. are you having any problems with it? – Guy Rotem Feb 22 '17 at 17:27
  • I tried to do `TKK` in `translate.google.com` console lately, but it doesn't seem to be defined ; ( Any solution? – parsecer Feb 19 '22 at 15:58
  • @GuyRotem is there a name for this kind of technique involving the TKK? – fersarr Sep 27 '22 at 09:18
5

You can also try this format :

  1. pass q= urlencode format of your language (In JavaScript you can use the encodeURI() function & PHP has the rawurlencode() function)

  2. pass tl = language short name (suppose bangla = bn)

Now try this :

https://translate.google.com.vn/translate_tts?ie=UTF-8&q=%E0%A6%A2%E0%A6%BE%E0%A6%95%E0%A6%BE+&tl=bn&client=tw-ob

3

First, to avoid captcha, you have to set a proper user-agent like:
"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:46.0) Gecko/20100101 Firefox/46.0"

Then to not being blocked you must provide a proper token ("tk" get parameter) for each single request.
On the web you can find many different kind of scripts that try to calculate the token after a lot of reverse engineering...but every time the big G change the algorithm you're stuck again, so it's much easier to retrieve your token just observing in deep similar requests to translate page (with your text in the url).
You can read the token time by time grepping "tk=" from the output of this simple code with phantomjs:

"use strict";
var page = require('webpage').create();
var system = require('system');
var args = system.args;
if (args.length != 2) { console.log("usage: "+args[0]+" text");  phantom.exit(1); }
page.onConsoleMessage = function(msg) {     console.log(msg); };
page.onResourceRequested = function(request) {   console.log('Request ' + JSON.stringify(request, undefined, 4)); };
page.open("https://translate.google.it/?hl=it&tab=wT#fr/it/"+args[1],     function(status) {
if (status === "success")    {             phantom.exit(0);           } 
else {      phantom.exit(1);    }
});

so in the end you can get your speech with something like:
wget -U "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:46.0) Gecko/20100101 Firefox/46.0" "http://translate.google.com/translate_tts?ie=UTF-8&tl=it&tk=52269.458629&q=ciao&client=t" -O ciao.mp3
(token are probably time based so this link may not work tomorrow)

  • I managed to get things working by using this tip. I ended up using casperjs instead of phantomjs. With casperjs it was easy to use the casper.download(downloadURL, targetFile); after you have managed to construct the URL with the current tk. You can use the resource.received event in casperjs to listen to the requests. – DianeH May 27 '16 at 10:00
  • `casper.on('resource.received', function(resource) { var string = JSON.stringify(resource); if (string && (string.indexOf("tk=") >= 0 || string.indexOf("tts") >= 0)) { var regex = /tk=(.+)&/ var result = string.match(regex); tk = result[1]; casper.echo('TK found: ' + tk); } });` – DianeH May 27 '16 at 10:03
1

I rewrote Guy Rotem's answer in Java, so if you prefer Java over Javascript, feel free to use:

public class Hasher {
    public long shiftLeftOrRightThenSumOrXor(long num, String[] opArray) {
        long result = num;

        int current = 0;
        while (current < opArray.length)  {
            char op1 = opArray[current].charAt(1);  //  '+' | '-' ~ SUM | XOR
            char op2 = opArray[current].charAt(0);  //  '+' | '^' ~ SLL | SRL
            char xd = opArray[current].charAt(2);   //  [0-9a-f]

            assertError(op1 == '+'
                    || op1 == '-', "Invalid OP: " + op1);
            assertError(op2 == '+'
                    || op2 == '^', "Invalid OP: " + op2);
            assertError(('0' <= xd && xd <= '9')
                    || ('a' <= xd && xd <='f'), "Not an 0x? value: " + xd);

            int shiftAmount = hexCharAsNumber(xd);
            int mask = (op1 == '+') ? ((int) result) >>> shiftAmount : ((int) result) << shiftAmount;

            long subresult = (op2 == '+') ? (((int) result) + ((int) mask) & 0xffffffff)
                    : (((int) result) ^ mask);
            result = subresult;
            current++;
        }

        return result;
    }

    public void assertError(boolean cond, String e) {
        if (!cond) {
            System.err.println();
        }
    }

    public int hexCharAsNumber(char xd) {
        return (xd >= 'a') ? xd - 87 : Character.getNumericValue(xd);
    }

    public int[] transformQuery(String query) {
        int[] e = new int[1000];
        int resultSize = 1000;

        for (int f = 0, g = 0; g < query.length(); g++) {
            int l = query.charAt(g);
            if (l < 128) {
                e[f++] = l;                 //  0{l[6-0]}
            } else if (l < 2048) {
                e[f++] = l >> 6 | 0xC0;     //  110{l[10-6]}
                e[f++] = l & 0x3F | 0x80;   //  10{l[5-0]}
            } else if (0xD800 == (l & 0xFC00) &&
                    g + 1 < query.length() && 0xDC00 == (query.charAt(g + 1) & 0xFC00)) {
                //  that's pretty rare... (avoid ovf?)
                l = (1 << 16) + ((l & 0x03FF) << 10) + (query.charAt(++g) & 0x03FF);
                e[f++] = l >> 18 | 0xF0;        //  111100{l[9-8*]}
                e[f++] = l >> 12 & 0x3F | 0x80; //  10{l[7*-2]}
                e[f++] = l & 0x3F | 0x80;       //  10{(l+1)[5-0]}
            } else {
                e[f++] = l >> 12 | 0xE0;        //  1110{l[15-12]}
                e[f++] = l >> 6 & 0x3F | 0x80;  //  10{l[11-6]}
                e[f++] = l & 0x3F | 0x80;       //  10{l[5-0]}
            }

            resultSize = f;
        }

        return Arrays.copyOf(e, resultSize);
    }

    public long normalizeHash(long encondindRound2) {
        if (encondindRound2 < 0) {
            encondindRound2 = (encondindRound2 & 0x7fffffff) + 0x80000000L;
        }
        return (encondindRound2) % 1_000_000;
    }

    /*
    /   EXAMPLE:
    /
    /   INPUT: query: 'hola', windowTkk: '409837.2120040981'
    /   OUTPUT: '70528.480109'
    /
    */
    public String calcHash(String query, String windowTkk) {
        //  STEP 1: spread the the query char codes on a byte-array, 1-3 bytes per char
        int[] bytesArray = transformQuery(query);

        //  STEP 2: starting with TKK index,
        // add the array from last step one-by-one, and do 2 rounds of shift+add/xor
        String[] d = windowTkk.split("\\.");
        int tkkIndex = 0;
        try  {
            tkkIndex = Integer.valueOf(d[0]);
        }
        catch (Exception e)  {
            e.printStackTrace();
        }

        long tkkKey = 0;
        try  {
            tkkKey = Long.valueOf(d[1]);
        }
        catch (Exception e)  {
            e.printStackTrace();
        }

        int current = 0;
        long result = tkkIndex;
        while (current < bytesArray.length)  {
            result += bytesArray[current];
            long subresult = shiftLeftOrRightThenSumOrXor(result,
                    new String[] {"+-a", "^+6"});
            result = subresult;
            current++;
        }
        long encondingRound1 = result;
        //System.out.println("encodingRound1: " + encondingRound1);

        //  STEP 3: apply 3 rounds of shift+add/xor and XOR with they TKK key
        long encondingRound2 = ((int) shiftLeftOrRightThenSumOrXor(encondingRound1,
                new String[] {"+-3", "^+b", "+-f"})) ^ ((int) tkkKey);
        //System.out.println("encodingRound2: " + encondingRound2);

        //  STEP 4: Normalize to 2s complement & format
        long normalizedResult = normalizeHash(encondingRound2);
        //System.out.println("normalizedResult: " +  normalizedResult);

        return String.valueOf(normalizedResult) + "."
                + (((int) normalizedResult) ^ (tkkIndex));
    }
}
parsecer
  • 4,758
  • 13
  • 71
  • 140