• Eric Muyser

    "People buy into the leader before they buy into the vision." - John Maxwell

    Learn Japanese Through Pokemon: Hacking audio into Memrise


    Welcome to another episode in this series. So let’s get started!

    I was up studying late last night and quickly realized one of the major problems with this technique is I get no auditory memorization or help with pronunciation.


    My first thought was to use the ImTranslator service. Which has very good pronunciation. Better than that of Google Translate. My second thought was the site was too cumbersome to use, and that there should be a good API out there. Unfortunately, most people seem to be the Google version. “No problem!” I thought, “I can find a way to inject ImTranslator’s audio into Memrise!” An hour later, I can click on any row of my Japanese definitions in Memrise to play the associated audio. Here’s how it goes:


    1) Find a way to get the audio file from ImTranslator.

    2) Find a way to inject that audio when I click on some Japanese using JavaScript.

    3) Find a way to have this JavaScript always work when I go to Memrise.


    I know I can do the last two with the Spidermonkey extension for Google Chrome, but the first one presents a challenge. Here’s how I tackled that.


    Get the cURL command for the translation out of DevTools:


    Which looked something like this:


    Realize that it’s not simply returning a consumable URL, such as an MP3 file, for the audio, but the entire page HTML. Additionally, it seems to have generated a SWF for the audio on the server side. Okay, that throws a bone into the mix. I know  that means I need to parse out the <embed> URL. I know doing that with Spidermonkey is still going to cause some cross-domain policy issues, and I’d prefer not to burden myself with operating within the cage of a browser extension. Instead, I can just create a Node.js script to do all the heavy lifting. That’s it.


    Create a folder. Create a file “server.js”. In that file, I came up with this (download the Gist here):

    1. var connect = require('connect');

    2. var http = require('http');

    3. var exec = require('child_process').exec;

    4. var url = require('url');

    5. var app = connect()

    6. .use(function(req, res) {

    7. var queryData = url.parse(req.url, true).query;

    8. var callback = queryData.callback;

    9. var text = queryData.text;

    10. var lang = queryData.lang;

    11. var cmd;

    12. if(lang === 'jp') {

    13. cmd = "curl 'http://imtranslator.net/translate-and-speak/sockets/tts.asp?FA=1&dir=ja&speed=0&B=1&ID=719936066&chr=MiaHead&vc=VW%20Misaki' -H 'Pragma: no-cache' -H 'Origin: http://imtranslator.net' -H 'Accept-Encoding: gzip,deflate,sdch' -H 'Accept-Language: en-US,en;q=0.8' -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.131 Safari/537.36' -H 'Content-Type: application/x-www-form-urlencoded' -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,/;q=0.8' -H 'Cache-Control: no-cache' -H 'Referer: http://imtranslator.net/translate-and-speak/sockets/tts.asp?FA=1&dir=ja&speed=0&B=1&ID=719936066&chr=MiaHead&vc=VW%20Misaki' -H 'Cookie: ASPSESSIONIDAQCTDACT=OJEAJOKCCPBDOLCAELKBMBPD; TTSid=719936066; utma=87094784.1264872206.1399156538.1399156538.1399156538.1; utmb=87094784.3.10.1399156538; utmc=87094784; utmz=87094784.1399156538.1.1.utmcsr=google|utmccn=(organic)|utmcmd=organic|utmctr=(not%20provided)' -H 'Connection: keep-alive' --data 'text=" + text + "&chr=MiaHead&speed=0&voice=&dir=ja&B=1' --compressed";

    14. }

    15. else if(lang === 'en') {

    16. text = escape(queryData.text).replace(/%20/gi, '+');

    17. cmd = "curl 'http://imtranslator.net/translate-and-speak/sockets/tts.asp?FA=1&dir=enf&speed=0&B=1&ID=719936066&chr=AnnaHead&vc=VW%20Kate' -H 'Pragma: no-cache' -H 'Origin: http://imtranslator.net' -H 'Accept-Encoding: gzip,deflate,sdch' -H 'Accept-Language: en-US,en;q=0.8' -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.131 Safari/537.36' -H 'Content-Type: application/x-www-form-urlencoded' -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,/;q=0.8' -H 'Cache-Control: no-cache' -H 'Referer: http://imtranslator.net/translate-and-speak/sockets/tts.asp?FA=1&dir=enf&speed=0&B=1&ID=719936066&chr=AnnaHead&vc=VW%20Kate' -H 'Cookie: ASPSESSIONIDAQCTDACT=OJEAJOKCCPBDOLCAELKBMBPD; TTSid=719936066; ASPSESSIONIDASRCABCT=EIPIJFNCMMMBGHGEDBDKMAMI; utma=87094784.1264872206.1399156538.1399156538.1399176286.2; utmb=87094784.14.10.1399176286; utmc=87094784; utmz=87094784.1399156538.1.1.utmcsr=google|utmccn=(organic)|utmcmd=organic|utmctr=(not%20provided)' -H 'Connection: keep-alive' --data 'text=" + text + "&chr=AnnaHead&speed=0&voice=&dir=enf&B=1' --compressed";

    18. }

    19. console.log(cmd);

    20. exec(cmd, function (error, stdout, stderr) {

    21. if (error !== null) {

    22. console.error('exec error: ' + error);

    23. }

    24. var regex = /PARAM ID="MOVIE" NAME="MOVIE" VALUE="([^\"]+)"/gi;

    25. var ttsURL = regex.exec(stdout)[1];

    26. var response = callback + '("' + ttsURL + '")';

    27. res.end(response);

    28. });

    29. });

    30. http.createServer(app).listen(3001);


    Additionally, I threw in the English condition for the girlfriend, who plays with me sometimes.


    In order to run this script, I’m using Node.js v0.10.1 (but you can probably get away with 0.8.19). I also ran these commands in the same folder:

    npm install connect

    npm install http

    npm install url


    My directory looks like this:


    Now I needed to create the Spidermonkey script. That took a little work, because Memrise loads the lesson via AJAX. So, I was lazy and decided to just capture the click event on the TD elements. Here’s a good spot, right on the ‘Move item’ icon. Additionally, I do it for the first 2 columns, and assume the first column is Japanese, and the second column is English.



    Here’s the code for that extension (download the Gist here):

    1. // ==UserScript==

    2. // @name Japanese Text-To-Speech

    3. // @namespace http://use.i.E.your.homepage/

    4. // @version 0.1

    5. // @description enter something useful

    6. // @match http://www.memrise.com/course/*/*/edit/

    7. // @copyright 2012+, You

    8. // ==/UserScript==

    9. unsafeWindow.injectTTS = function(ttsURL) {

    10. var $object = $('<OBJECT \

    11. type="application/x-shockwave-flash"\

    12. CLASSID="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"\

    13. type="application/x-shockwave-flash"\

    14. WIDTH="113"\

    15. HEIGHT="100"\

    16. CODEBASE="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=8"\

    17. ID=flash1>\

    18. <PARAM ID="MOVIE" NAME="MOVIE" VALUE="' + ttsURL + '">\

    19. <PARAM NAME="PLAY" VALUE="true">\

    20. <PARAM NAME="LOOP" VALUE="false">\

    21. <PARAM NAME="QUALITY" VALUE="high">\


    23. \

    24. <EMBED\

    25. type="application/x-shockwave-flash"\

    26. NAME="flash1"\

    27. ID="flash1"\

    28. SRC="' + ttsURL + '"\

    29. WIDTH="113"\

    30. HEIGHT="100"\

    31. PLAY="true"\

    32. LOOP="false"\

    33. QUALITY="high"\

    34. SCALE="SHOWALL"\

    35. swLiveConnect="true"\

    36. PLUGINSPAGE="http://www.macromedia.com/go/flashplayer/" style="height: 0; overflow: hidden">\

    37. </EMBED>\

    38. </OBJECT>');

    39. $('#levels').prepend($object);

    40. };

    41. $('#content').on('click', '.level-things tr.thing .column:nth-child(2) .text', function(e) {

    42. $('<script>').attr('src', 'http://localhost:3001/api/1/tts/?lang=jp&callback=injectTTS&text=' + $(this).text()).appendTo('head');

    43. });

    44. $('#content').on('click', '.level-things tr.thing .column:nth-child(3) .text', function(e) {

    45. $('<script>').attr('src', 'http://localhost:3001/api/1/tts/?lang=en&callback=injectTTS&text=' + $(this).text()).appendTo('head');

    46. });

    47. $('#content').on('click', '.level-things tr.thing td:first-child', function(e) {

    48. $('<script>').attr('src', 'http://localhost:3001/api/1/tts/?lang=jp&callback=injectTTS&text=' + $(this).next().find('.text').text()).appendTo('head');

    49. });


    Well, that’s it. I have more ideas in my head and I can add more professionalism to these solutions, but I’m not at the point it’s beneficial. Still, I want to hack and pan things out as I go along and get feedback or advice if I can. If you have any questions, feel free to shout out @ericmuyser on Twitter!


    Happy studying!