anki integration

I use beeminder to track my anki reviews. for a long time I manually inputted a +1 when I finished, but I’d always wanted an autodata solution because I find it tedious to keep up with manual data entry for more than a couple goals at a time.

however, none of the existing solutions quite did it for me - the thing I most care about is whether I did all of my reviews, not exactly how many reviews I did (which fluctuates by day) or some other incidental statistic.

I’ve come up with an autodata extension which basically works for me. it’s not at all plug and play, but for anyone else looking to work up an autodata solution for this or some other goal, it might be helpful as simple guidelines.

# ~/.local/share/Anki2/addons21/ankibee/__init__.py

from string import Template
import requests
import os
# from anki.collection import Collection
from aqt import mw
from aqt import gui_hooks

try:
    from dotenv import load_dotenv
    load_dotenv() # loads from .env by default
except ImportError:
    pass

bee_user = os.getenv('BEEMINDER_USER')
bee_auth_token = os.getenv('BEEMINDER_AUTH_TOKEN')


bee_goal = "anki"

# is this a good way of doing this? probably not
# but it sends 0 when I haven't done any reviews, 1 when I've done all of them,
# and something in between when I've done something in between. "good enough"
def get_review_ratio():
    try:
        reviews_due = mw.col.sched.counts()[2]
    except AttributeError:
        return # something isn't set if we haven't done any reviews this session, dirty way out
    review_cards_rated = len(mw.col.find_cards("rated:1 is:review"))
    review_ratio = review_cards_rated / (reviews_due + review_cards_rated)

    return review_ratio


from aqt.utils import showInfo


def send_datapoint(goal, value, comment = 'via ankibee'):
    return requests.post(f'https://www.beeminder.com/api/v1/users/{bee_user}/goals/{goal}/datapoints.json', data = {
        'auth_token': bee_auth_token,
        'value': value,
        'requestid': mw.col.sched.today, # if we submit multiple datapoints on the same day it replaces the earlier datapoint
        'comment': comment
        })


def beemind_review_ratio():
    send_datapoint('anki', get_review_ratio())


def beemind_anki_journal():
    journal_note_count = len(mw.col.find_notes("tag:anki-journal"))

    send_datapoint('anki-journal', journal_note_count)


# TODO we also do things that are *not* beeminding # we actually don't yet ignore this
# repeatedly sending datapoints should be idempotent otherwise 	
# TODO these requests are synchronous, if we make a lot that could get annoying
def beemind_everything():
    beemind_review_ratio()
    beemind_anki_journal()


gui_hooks.profile_will_close.append(beemind_everything)
7 Likes

Thanks for sharing!

If this sends a datapoint every time the desktop anki app syncs, then you’ll also want to change the goal’s aggregation method to be max or nonzero, which you can do on a ‘custom’ goal toward the bottom of the settings tab.

2 Likes

@philip sending a requestid causes beeminder to replace the existing datapoint with that requestid. I set requestid to mw.col.sched.today, so I just get the more recent version of a datapoint recorded in anki (see the comment in send_datapoint).

you could set a goal to work differently, but since I submit datapoints every time anki closes (triggers reliably for me and only a little spammy) I like this approach, rather than than filling beeminder with junk datapoints.

if I add more specific goals (which I intend to in the future) I’ll probably want start making the requests asynchronously to avoid slowing down shutdown too much. it’s also possible to have datapoints send on other triggers but most of those would probably feel bad without async sending.

3 Likes

Anki was driving me nuts - it’s hard to replace Anki and their platform is limited for a reason. To keep it free, they had to make some constraints. Nonetheless I am not able to have plugins for Anki mobile, I can’t spin up my custom sync server and I can’t have any API endpoint to retrieve the data. I can write a bot to login to AnkiWeb and read data, but it isn’t nice solution.

I came up with a bit different one to make sure to sync data with beeminder. It should work on every platform and is quite elastic. You can execute JS in a card template. In your card template, you can add a snippet of code in either “back” or “front”:

<script>
(() => {
  if (typeof window._skcount === "undefined") {
    window._skcount = 0;
  }
  window._skcount += 1;

  const $debug = document.querySelector("#skcount");
  $debug.innerHTML = window._skcount;

  const SYNC_EVERY_X_CARDS = 10;
  const USER = "YOUR_NAME";
  const AUTH_TOKEN = "YOUR_TOKEN";
  const GOAL = "YOUR_GOAL";

  if (window._skcount % SYNC_EVERY_X_CARDS === 0) {
    fetch(`https://www.beeminder.com/api/v1/users/${USER}/goals/${GOAL}/datapoints.json?auth_token=${AUTH_TOKEN}`, {
      headers: {
        "Content-Type": "application/json",
      },
      method: "POST",
      body: JSON.stringify({ value: SYNC_EVERY_X_CARDS, comment: new Date().toString() }),
    })
      .then(() => {
        $debug.innerHTML = "Saved to Beeminder!";
      })
      .catch((error) => ($debug.innerHTML = error.message));
  }
})();
</script>

Please fill the template with your goal data and also place in your card this snippet: <div id="skcount"></div> to see which card is it and see syncing status.

This script will add a beeminder data every X cards you review. It works for me on Anki MacOS, AnkiMobile (iOS) and AnkiWeb. Obviously it has a downside if you spread your efforts thin across different decks (20 decks, 5 cards to review in each).
Similarly, we can use JavaScript tricks to beemind beenary goals (did I review at least 1 card today?) or perhaps create “time spent on Anki” goal.

10 Likes

@skorytnicki that’s brilliant!

2 Likes

Stealing this thread for one more post.

You might find interesting the auto generation of the images attached to an Anki card. I learn foreign languages with Anki, it’s much easier to remember them with some image.

My deck was originally English/Spanish, I translate it to Polish/Spanish as I go. I keep English field however to download an image. Some examples:

My Back template is this:

<div class='content'>
<div class='title'>
<!--- If "Polski" field exists, use it. If not, go with English default --->
{{^Polski}}
    {{Front}}
{{/Polski}}
{{#Polski}}
    {{Polski}}
{{/Polski}}
</div>
<div class='title translation animation'>
{{Back}}
</div>
<div class='desc animation'>
{{Description}}
</div>
<div class='image animation'>
</div>
</div>
<script>
(() => {
const url = "https://api.unsplash.com/search/photos?page=1&orientation=landscape&query="
const front = "{{Front}}"; // Use English field
const accessKey = "ADD YOUR UNSPLASH KEY"; // go to unsplash and get API key.
fetch(url + front, {
headers: {Authorization: `Client-ID ${accessKey}` }
}).then(res => res.json()).then(data => {
// can also insert img tag if you wish
 document.querySelector('.image').style.backgroundImage = `url(${data.results[0].urls.small})`;
});
})();
</script>
4 Likes

I would really appreciate if you could upload this on google drive, I have no idea why its not working for me

2 Likes

In case you ask about my solution. Here’s a sample deck. You have to go to review on your desktop and edit one card to use your beeminder credentials, like that:

Every five cards you’ll get a datapoint in your beeminder goal. In case of errors, you’ll see beeminder message:

Here you can find the token: beeminder

Good luck. Let me know if it works, in case it doesn’t, please drop the error message.

4 Likes

Just to check, is this meant to work for AnkiDroid as well? It has worked beautifully for Anki Desktop but haven’t had the same success with AnkiDroid.

1 Like

I am not sure. I have not tested it, as I use iOS.

1 Like

love the idea. can you explain how you use it? because it is great to keep the habit and do an amount of cards everytime but I would really like it to be able to see how many cards are due today and only if I finish all of them I get the check. I would prefer this rather than just setting the goal to one card per day to really force me to do all of the cards that are due

I use Anki to learn languages. I set my goal to whatever amount of cards makes sense to me.
Right now it’s about 40 cards. Whenever I have less cards to repeat than my daily basis, it means I have to add new words.

My method has two advantages. One technical, one philosophical.

It is possible to do what you want for example with a scraper bot that logs in to ankiweb.
My method is cross device, requires very little time to implement, does not rely on any third party service.

In terms of beeminder science:
I think 0/1 goals are intuitive, but overall not good for your progress. 3 cards a day and 500 cards a day is not the same effort and should not get the same 1 point on your graph. I would rather review 500 cards in a day when I have more energy and skip a few days and not force myself to repeat every day few cards. Nothing forbids me from doing sessions daily or ratcheting my safety buffer.

so I completely get you from the technical site with cross devices. but I feel like the main point of anki is really do to your daily flashcards so that you really use the algorithm and the power of it. Maybe there is a few to get the things I want by just changing your code a little bit.

1 Like

I like the idea of cross-device, because I study cards on iOS but need to sync on Desktop because that’s how this plugin works:

Rather than tracking a number of cards studied, this tracks the number of cards learned (i.e. not due), so a flat bright red line forces me to keep current with what I already know. Setting a slope, it regularly forces me to add more cards.

1 Like

I feel like the main point of anki is really do to your daily flashcards so that you really use the algorithm and the power of it.

To me, the main point of the anki algorithm is to show me the cards that are most effective to study at any given time to learn the material in the exact minimum amount of time, and I’ll happily trade a little efficiency for flexibility. And my experience is that the efficiency doesn’t seem to be much reduced, which makes sense: you don’t forget things because they’re past the due date; you are just slightly more likely to have forgotten them. (The algorithm has even served me well when coming back from long breaks.)

My guess is that the main reason people usually put such a high priority on doing their reviews every day isn’t because of the little bit of efficiency it gains, but because it can be hard to come back from getting behind because the increasing number of due cards every day can make it overwhelming. But that’s not an issue with beeminder; bringing up the rotestock results in progress without overwhelm both for learning new things and for clearing a huge backlog.

Maybe there is a few to get the things I want by just changing your code a little bit.

If you can find a good way to make the addon track whatever metric you would find more valuable instead, I would gladly accept a pull request. Since doing all your reviews every day to maximize efficiency seems important to you, maybe something that reports a single data point iff it detects that your queue is empty? That would also work for people who do reviews on mobile and when beeminder was demanding reviews that hadn’t been synced, they would be reminded to sync on the computer.

ETA: I wasn’t paying attention to what thread I was in. (I got here via a forum notification that triggered when my thread was linked.) You weren’t talking about my code. What I said is all true, but it’s not what you were doing so feel free to ignore it.

3 Likes

Is it somehow possible to hide the count and the “saved to beeminder”?

Hey, you can try this:

<div id="skcount" style="display: none"></div> 

instead of:

<div id="skcount"></div>

I added it to be aware what’s the progress.
Alternatively, you can make it less intrusive, eg place it at the bottom of the card:

<div id="skcount" style="text-align:center; font-size: 12px; opacity: 0.5; margin-top: 20px"></div> 

thank you. I think it is working

1 Like