Export Wanikani SRS progress to Anki [Guide]

Hello, I want to share a bit of how I exported my Wanikani progress to Anki.

Why Anki?

Well, it’s fast and it works offline on my iPhone, no typing and no loading time. I can use it while I’m waiting in line in McDonalds, while walking to groceries, etc.

Prerequisite

  • Some knowledge on JavaScript, SQL (SQLite).
  • Having localhost server.
  • Having SQLite browser, I use this software https://sqlitebrowser.org/.

Notice

  • Please test the result in a sandbox Anki profile to make sure that everything works correctly and does not mess with your main Anki profile.

The Guide

So, here’s how I did it:

  1. Download all JSONs of all levels using Wanikani API.

    https://www.wanikani.com/api/user/YOUR_API_KEY/kanji/1
    https://www.wanikani.com/api/user/YOUR_API_KEY/kanji/2
    etc...
    https://www.wanikani.com/api/user/YOUR_API_KEY/vocabulary/1
    https://www.wanikani.com/api/user/YOUR_API_KEY/vocabulary/2
    etc...
    
  2. Put the files in a localhost server, with the folder structure like this:

    /WK/kanji/1.json
    /WK/kanji/2.json
    /WK/kanji/3.json
    etc...
    /WK/vocab/1.json
    /WK/vocab/2.json
    /WK/vocab/3.json
    etc...
    

  3. Now, we’ll move to the Anki package. Rename your desired Anki package extension to zip. (.apkg to .zip) and extract the contents. I used this deck Jp Yomi - Words by Yomi Frequency w/ Kanji Info, Stats - AnkiWeb.

  4. Open collection.anki2 in the SQLite browser.

  5. Navigate to Browse Data > Table: Col. Save this creation date value (crt column) in a note.

  6. Open http://localhost/ in your web browser. Copy the following script in the console window. Change the deckCreationTime variable to your crt value (in my case, it’s 1536170400) and the maxLevel to your last Wanikani level. And then, run the script. (You might need to adapt other things in the script depending to your Anki deck)

    var maxLevel = 25;
    var deckCreationTime = 1536170400;
    var allSQL = "";
    
    function kanjisql (i) {
        fetch('/WK/kanji/' + i + '.json')
        .then(function (response) {
            return response.json();
        })
        .then(function (response) {
            var SQL = "";
            response.requested_information.forEach(function(el) {
                if(el.user_specific) {
                    reps = 
                        (el.user_specific.meaning_correct + el.user_specific.meaning_incorrect) > (el.user_specific.reading_correct + el.user_specific.reading_incorrect) ? 
                        (el.user_specific.meaning_correct + el.user_specific.meaning_incorrect) : 
                        (el.user_specific.reading_correct + el.user_specific.reading_incorrect);
                    lapses = 
                        el.user_specific.meaning_incorrect > el.user_specific.reading_incorrect ? 
                        el.user_specific.meaning_incorrect : 
                        el.user_specific.reading_incorrect;
    
                    var ivl = 1;
                    switch(el.user_specific.srs_numeric) {
                        case 3:
                            ivl = 1;
                            break;
                        case 4:
                            ivl = 2;
                            break;
                        case 5:
                            ivl = 6;
                            break;
                        case 6:
                            ivl = 17;
                            break;
                        case 7:
                            ivl = 45;
                            break;
                        case 8:
                            ivl = 118;
                            break;
                        case 9:
                            ivl = 306; 
                            break;
                    }
    
                    var due = 1;
                    switch(el.user_specific.srs_numeric) {
                        case 3:
                        case 4:
                        case 5:
                        case 6:
                        case 7:
                        case 8:
                            due = Math.floor((el.user_specific.available_date - deckCreationTime)/86400) ;
                            break;
                        case 9:
                            due = Math.floor((el.user_specific.burned_date + 26438400 - deckCreationTime)/86400);
                            break;
                    }
    
                    SQL += `UPDATE cards SET type = 2, queue = 2, due = ${due}, ivl = ${ivl}, factor = 2500, reps = ${reps}, lapses = ${lapses} WHERE nid IN (SELECT id FROM notes WHERE flds like "%\u001f${el.character}\u001f%");\n`;                    
                }
            });
    
            return SQL;
    
        }).then(function(SQL) {
            allSQL += SQL;
        });
    }
    
    for (var i = 1; i <= maxLevel; i++) {
        var num = i;
        kanjisql(num);
    }
    
  7. Print allSQL variable using console.log(allSQL). It will print the SQL commands. Save it into a txt file.

    UPDATE cards SET type = 2, queue = 2, due = 122, ivl = 306, factor = 2500, reps = 8, lapses = 0 WHERE nid IN (SELECT id FROM notes WHERE flds like "%一%");
    UPDATE cards SET type = 2, queue = 2, due = 122, ivl = 306, factor = 2500, reps = 8, lapses = 0 WHERE nid IN (SELECT id FROM notes WHERE flds like "%二%");
    UPDATE cards SET type = 2, queue = 2, due = 288, ivl = 306, factor = 2500, reps = 12, lapses = 1 WHERE nid IN (SELECT id FROM notes WHERE flds like "%九%");
    UPDATE cards SET type = 2, queue = 2, due = 122, ivl = 306, factor = 2500, reps = 8, lapses = 0 WHERE nid IN (SELECT id FROM notes WHERE flds like "%七%");
    UPDATE cards SET type = 2, queue = 2, due = 122, ivl = 306, factor = 2500, reps = 8, lapses = 0 WHERE nid IN (SELECT id FROM notes WHERE flds like "%人%");
    UPDATE cards SET type = 2, queue = 2, due = 242, ivl = 306, factor = 2500, reps = 31, lapses = 5 WHERE nid IN (SELECT id FROM notes WHERE flds like "%入%");
    UPDATE cards SET type = 2, queue = 2, due = 122, ivl = 306, factor = 2500, reps = 8, lapses = 0 WHERE nid IN (SELECT id FROM notes WHERE flds like "%八%");
    UPDATE cards SET type = 2, queue = 2, due = 213, ivl = 306, factor = 2500, reps = 30, lapses = 5 WHERE nid IN (SELECT id FROM notes WHERE flds like "%力%");
    UPDATE cards SET type = 2, queue = 2, due = 122, ivl = 306, factor = 2500, reps = 8, lapses = 0 WHERE nid IN (SELECT id FROM notes WHERE flds like "%十%");
    UPDATE cards SET type = 2, queue = 2, due = 122, ivl = 306, factor = 2500, reps = 8, lapses = 0 WHERE nid IN (SELECT id FROM notes WHERE flds like "%三%");
    etc...
    
  8. You’ll need to do the same for the vocabularies. In step 6, change the fetch('/WK/kanji/' + i + '.json') part into fetch('/WK/vocab/' + i + '.json') and run again. Save the SQL commands to your txt file.

  9. All SQL commands are ready. Now, we’ll modify the Anki database.

  10. Open collection.anki2 in the SQLite browser, put the SQL commands in the Execute SQL window.

  11. Execute them!

  12. Save the database and quit the SQLite browser.

  13. Compress both collection.anki2 and media into a zip file. Change the extension to .apkg (See Archive.apkg)

  14. Double click it to import to Anki.

  15. Voila! Now the due, interval, lapses, and reviews are imported to Anki :v:

  16. To make sure that everything went correctly, please check the due date of some cards and compare them to the due date in the Wanikani website.

Notes

  • The deck doesn’t have to be the same as Wanikani deck. I use this gigantic 25k+ cards deck.
  • There are some words in Wanikani that might not exist in the Anki deck. So, the progress of those words will be discarded.
  • This is a one-way conversion. Once you move to Anki and make more progress on Anki, I guess there’s no way back to move your progress from Anki to Wanikani.
  • The main downside is, my Wanikani level will be stuck in level 26 forever, haha.
2 Likes

https://wanikanitoanki.com

1 Like

It doesn’t export the due date, wrong answers, corrrect answers, etc. I need those values to export my progress to Anki.

1 Like

How cool!! So it actually transfer the progress (specially due dates) to Anki?
I wish had that a few months ago; I kinda did the whole migration manually. Now only reviewing vocab in Anki, exactly to avoid overlapping and overtesting (and basically wasting time doing vocab in 2 different apps).

You could add it to the resources post, it’s deffinitely a great addition. :ok_hand:

1 Like

Of course, that’s the main goal of this guide. By the way, it also revives your burned items. So, the interval will go further (306 days, 765 days, 1912 days, etc).

Wanikani’s ease factor is around 260% (1d, 2.6d, 6.76d, 17.576d, 45.6976d, 118.81376d). But in my script, I set it as 250%. You can change it in the SQL string to any ease factor you like.

1 Like

Ah, doing more than just simple export.
Guess I jumped the gun a bit replying while you were writing the whole post.

1 Like