Methodology to determine the best amount of time to be spent on a card on average

Is there a framework that allows me to find out how much time I SHOULD or SHOULD NOT be spending per card :question:

For example, I often do myself taking a huge amount of time to recall the answer for a card. I do sometimes end up getting the correct answer but sometimes I do not.

Is there an objective way and addon which help finding the time allowed per card based on difficulty from previous history :question:

This is interesting topic! Here’s my personal opinion.

Is there a framework that allows me to find out how much time I SHOULD or SHOULD NOT be spending per card :question:

My framework is

  • Don’t try to go as fast possible as it’ll lead to inadvertently figuring out some way to go fast at the cost of low quality recall
  • But also don’t allow unlimited time on reviews. Go somewhat fast
  • The important thing is that the recall of information feels high quality

For example, I often do myself taking a huge amount of time to recall the answer for a card. I do sometimes end up getting the correct answer but sometimes I do not.

I sometimes also spend a minute or longer trying to remember something when I feel like it’s right on the tip of my tongue. In contrast, when my brain draws a total blank, I immediately hit Again. I think the memory gets stronger with each successful recall whereas it just gets refreshed without strengthening, if you need be shown the answer. This is a highly unscientific, that’s-how-it-feels-to-me statement.

Is there an objective way and addon which help finding the time allowed per card based on difficulty from previous history :question:

But to answer your question, the card data you’d use to calculate something like this would probably include:

  • average review time per card
    • or maybe averaged over the deck?
  • with the default reviewer card ease
  • or with FSRS, difficulty and stability

A simple approach would be to decide on some base review time using the deck average review time and then increase or decrease it per card using the above data. You could get this information in the custom scheduler javascript, set a timer with setTimeout and maybe play some sound or automatically answer Again with javascript? The deck-level Auto Advance setting should either be off or perhaps set as the maximum allowed time any card should be allowed for review.

1 Like

I meant like as in:

Cards with X difficulty and X lapses should have a Y allocated time for you to try and recall the answer.

My framework is

The thing is, I don’t how fast ist fast and how slow is slow.

I sometimes also spend a minute or longer trying to remember something when I feel like it’s right on the tip of my tongue

This is a major reason why I was looking for this. I am suffering from this problem.

The thing is, I don’t how fast ist fast and how slow is slow .

With vocabulary cards, I’ve heard that 5-10 seconds is good and 60 seconds is definitely too slow. Personally 15 seconds feels acceptable to me.

But general ideas on how to determine this for any type of card could be

  • Look the at review times of cards with high ease and no lapses. Take the average there as your target
  • But also consider your study goals: how much time do you want to study and what level of recall is acceptable? Then adjust the target up or down a little.

This is a major reason why I was looking for this. I am suffering from this problem.

I don’t personally consider it a problem when I spend extra time recalling but I also don’t spend more than a minute on these cases and they aren’t that frequent either. Do you spend more than 1 minute on these cases? Are they really frequent? If your answer to either is “yes”, then putting some strict restrictions on review time make sense.

I have more than 70000 cards in my collection and there is quite a significant share that requires a lot more effort to recall than others. They hamper my rhythm. So yes…

Look the at review times of cards with high ease and no lapses. Take the average there as your target

I have generally held it to be a rule that at the very least: each card during review should not take more time than the maximum review time ever taken before from past passes.

For example, if the maximum for a card was 12 seconds, then an elapsed time of the front and back side of card shown of =<12 is considered to be good.

I don’t know if there is a code that can help achieve this. I think an add-on would enable this to happen.

Otherwise, the problem with just looking at cards with high ease is that I know that the cards in question are already difficult ones, so for them to achieve the same review time as easier ones is a bit difficult.

There is some code that I made as I was trying to make my own custom Auto Advance implementation but gave up after I realized it would be really difficult or maybe impossible to get it to wait for audio to play the same way the native Anki Auto Advance does.

So, note that cards with audio will have problems with this. I also haven’t tested it very much after giving up with it so there might be bugs (possibly on desktop, the timer may still run even after you answer manually)

To work on desktop this addon is also required. I guess this won’t work on AnkiMobile at all.

The code should be added to the card Front template.

<script>
  function initAutoAdvance(options = {}) {
    const {
      // Time to wait on card front
      goToAnswerSeconds = 5,
      // Time to wait on card back
      answerSeconds = 5,
      // Threshold ease, lower than this will add time, higher will reduce
      easeThreshold = 2.5,
      // Amount of ease per which to add/reduce by 1 second
      easeStep = 0.5,
      // Which answer to do automatically once the timer is up
      answer = 3
    } = options;

    const handlePromise = (val, func) => {
      if (typeof val === "object" && val !== null && !!val.then) {
        val.then((res) => func(res.value));
      } else {
        func(val);
      }
    };

    let goToAnswer;
    let answer1;
    let answer2;
    let answer3;
    let answer4;

    function answerFunc() {
      switch (answer) {
        case 1:
          answer1();
          break;
        case 2:
          answer2();
          break;
        case 3:
          answer3();
          break;
        case 4:
          answer4();
          break;
      }
    }
    function onGoToAnswer(answerWaitTime) {
      goToAnswer();
      // Start timer for answering
      setTimeout(answerFunc, answerWaitTime);
    }
    const startAutoAdvance = (fctVal) => {
      handlePromise(fctVal, (fctNum = 0) => {
        let goToAnswerWaitTime = goToAnswerSeconds * 1000;
        // fct is a number between 1300 and 5000
        // We want a multipler between 1 and 5
        const fctMult = fctNum / 1000 > 1;

        let answerWaitTime = answerSeconds * 1000;
        // If fct is missing, it'll be zero
        if (fctMult > 1 && easeThreshold > 1 && easeStep > 0) {
          // Add extra time if factor is lower than easeMiddle, reduce if higher
          const extraTime = (fctMult - easeThreshold) * easeStep * 1000;
          goToAnswerWaitTime = goToAnswerWaitTime + extraTime;
          answerWaitTime = answerWaitTime + extraTime;
        }
        // Start timer for showing answer
        setTimeout(() => onGoToAnswer(answerWaitTime), goToAnswerWaitTime);
      });
    };
    try {
      const jsApiContract = {
        version: "0.0.3",
        developer: "put_your_own_email_or_github_here"
      };

      const api = new AnkiDroidJS(jsApiContract);

      goToAnswer = showAnswer;
      answer1 = buttonAnswerEase1;
      answer2 = buttonAnswerEase2;
      answer3 = buttonAnswerEase3;
      answer4 = buttonAnswerEase4;
      startAutoAdvance(api.ankiGetCardFactor());
    } catch (e) {
      if (globalThis.ankiPlatform !== "desktop") {
        console.log("AnkiDroid API error", e);
      } else {
        goToAnswer = () => pycmd("ans");
        answer1 = () => pycmd("ease1");
        answer2 = () => pycmd("ease2");
        answer3 = () => pycmd("ease3");
        answer4 = () => pycmd("ease4");
        pycmd("AnkiJS.ankiGetCardFactor()", startAutoAdvance);
      }
    }
  }

  initAutoAdvance({
    // Customize options as you please
    goToAnswerSeconds: 4,
    answerSeconds: 3,
    easeThreshold: 2,
    easeStep: 0.7,
  });
</script>

Edit: Removed my github from the AnkiDroid JS API init, it should be edited to have your own info instead.

1 Like

I will try this now!! Thanks a lot!!

Can you add a visual indicator to signify that time is up (like the Life Drain addon does)


So I tried it and it started advancing through my cards like crazy. And I went ahead to reverse it and it got even faster.

Could you please turn off the auto advance and instead replace it with some sort of visual cue that time is up (based on the average time needed to reveal the answer for that specific card) :question:

That’s somewhat tedious to do, you gotta insert some html elements to the template in a specific position with specific styling and whatnot. Might be easier to play a sound perhaps? You could add a sound file to your collection.media folder, prefix with _ so Anki doesn’t delete and play it once time is up.

The part of the code that needs changing is the answerFunc, that’s what’s called when the timer is up on the card back. With the auto-answer removed, the answer option given to initAutoAdvance is no longer.

    function answerFunc() {
      // Do stuff to indicate time is up
    }

Yeah, that’s probably the timer from the previous card still running when you answer manually. Probably with a visual/audio indicator what will happen is that it’ll get triggered too fast too.

Now it looks like this. And this stopped the advancing and showing the answer automatically. I don’t know if I broke it.

image

As for the audio sounds…well it is a bit stupid to say but I often learn in the library and don’t have headphones :frowning:

Also I don’t have any audio files I could play.

I had some time and interest to tinker on this so I made it show an indicator instead. It should now properly reset the timers when advancing to the next card too.

This goes in the card Front again (edit the JS Api part to have your own email/github this time)

<script>
  var frontTimer;
  var backTimer;
  function initAutoAdvance(options = {}) {
    const {
      goToAnswerSeconds = 5,
      answerSeconds = 5,
      // Threshold when to add extra time for ease
      easeThreshold = 2.5,
      // Amount of ease per which to add/reduce by 1 second
      easeStep = 0.5
    } = options;

    const handlePromise = (val, func) => {
      if (typeof val === "object" && val !== null && !!val.then) {
        val.then((res) => func(res.value));
      } else {
        func(val);
      }
    };

    let goToAnswer;

  function answerFunc() {
    // Add indicator element into a fixed position
    const indicator = document.createElement("div");
    indicator.style.position = "fixed";
    // Positioned at bottom right corner
    indicator.style.bottom = "0";
    indicator.style.right = "0";
    // Size
    indicator.style.padding = "10px";
    // Black with 50% opacity
    indicator.style.background = "rgba(0,0,0,0.5)";
    // Large border to make the element circular
    indicator.style.borderRadius = "99px";

    document.body.appendChild(indicator);
  }
    function onGoToAnswer(answerWaitTime) {
      goToAnswer();
      // Start timer for answering
      backTimer = setTimeout(answerFunc, answerWaitTime);
    }
    const startAutoAdvance = (fctVal) => {
      clearTimeout(frontTimer);
      clearTimeout(backTimer);
      handlePromise(fctVal, (fctNum = 0) => {
        let goToAnswerWaitTime = goToAnswerSeconds * 1000;
        // fct is a number between 1300 and 5000
        // We want a multipler between 1 and 5
        const fctMult = fctNum / 1000 > 1;

        let answerWaitTime = answerSeconds * 1000;
        // If fct is missing, it'll be zero
        if (fctMult > 1 && easeThreshold > 1 && easeStep > 0) {
          // Add extra time if factor is lower than easeMiddle, reduce if higher
          const extraTime = (fctMult - easeThreshold) * easeStep * 1000;
          goToAnswerWaitTime = goToAnswerWaitTime + extraTime;
          answerWaitTime = answerWaitTime + extraTime;
        }
        // Start timer for showing answer
        frontTimer = setTimeout(
          () => onGoToAnswer(answerWaitTime),
          goToAnswerWaitTime
        );
      });
    };
    try {
      const jsApiContract = {
        version: "0.0.3",
        developer: "put_your_own_email_or_github_here"
      };

      const api = new AnkiDroidJS(jsApiContract);

      goToAnswer = showAnswer;
      startAutoAdvance(api.ankiGetCardFactor());
    } catch (e) {
      if (globalThis.ankiPlatform !== "desktop") {
        console.log("AnkiDroid API error", e);
      } else {
        goToAnswer = () => pycmd("ans");
        pycmd("AnkiJS.ankiGetCardFactor()", startAutoAdvance);
      }
    }
  }

    initAutoAdvance({
      // Customize options as you please
      goToAnswerSeconds: 4,
      answerSeconds: 3,
      easeThreshold: 2,
      easeStep: 0.7,
    });
</script>

In case you’re doing {{FrontSide}} in the card back, the javascript would get run again in the backside which is undesired. To fix that you can add a hidden div in the backside like this:

<div id="backside" style="display:none;"></div>

And then modify the front side code like this:

  var isBackside = document.getElementById("backside");
  if (!isBackside) {
    // Only init autoAdvance in the card front
    initAutoAdvance({
      // Customize options as you please
      goToAnswerSeconds: 4,
      answerSeconds: 3,
      easeThreshold: 2,
      easeStep: 0.7,
    });
  }

Edit: removed console.logs

My cards are cloze cards with this field {{edit:cloze:Text}}.

And I don’t want it to auto advance. I just want it to show an indicator for me to advance.

Sorry if I am asking a bit too much :sweat:

Yeah, the new code is doing that now, the function is still just named autoAdvance, you could rename it. If you want to edit the position or styling of the indicator, edit this part in the above code:

function answerFunc() {
    // Add indicator element into a fixed position
    const indicator = document.createElement("div");
    indicator.style.position = "fixed";
    // Positioned at bottom right corner
    indicator.style.bottom = "0";
    indicator.style.right = "0";
    // Size
    indicator.style.padding = "10px";
    // Black with 50% opacity
    indicator.style.background = "rgba(0,0,0,0.5)";
    // Large border to make the element circular
    indicator.style.borderRadius = "99px";

    document.body.appendChild(indicator);
  }

Or wait, did you mean you don’t want it to advance to the card back at all? Just stay in the front?

Yeah.

And also the indicator is shown now on the bottom right. How can I make it larger :question:

I have also wanted to ask, does this code get the average amount of time spent on the card shown and uses that average as the threshold :question:

No, that data is not available in the card template using javascript. This code is setting the base wait time the same for every card, and then increasing or decreasing it based on the card ease.

So this code only shows the indicator without advancing. Including it in the card back too isn’t as harmful as before but you could still use the isBackside trick from before, if needed.

<script>
  var frontTimer;
  function initIndicatorTimer(options = {}) {
    const {
      // Time to wait before showing indicator
      waitSeconds = 5,
      // Threshold when to add extra time for ease
      easeThreshold = 2.5,
      // Amount of ease per which to add/reduce by 1 second
      easeStep = 0.5,
    } = options;

    const handlePromise = (val, func) => {
      if (typeof val === 'object' && val !== null && !!val.then) {
        val.then(res => func(res.value));
      } else {
        func(val);
      }
    }

  function onTimerEnd() {
    // Add indicator element into a fixed position
    const indicator = document.createElement("div");
    indicator.style.position = "fixed";
    // Positioned at bottom right corner
    indicator.style.bottom = "0";
    indicator.style.right = "0";
    // Size
    indicator.style.padding = "20px";
    // Black with 50% opacity
    indicator.style.background = "rgba(0,0,0,0.5)";
    // Large border to make the element circular
    indicator.style.borderRadius = "99px";

    document.body.appendChild(indicator);
  }
    const startTimer = (fctVal) => {
      clearTimeout(frontTimer);
      handlePromise(fctVal, (fctNum = 0) => {
        let waitTime = waitSeconds * 1000;
        // fct is a number between 1300 and 5000
        // We want a multipler between 1 and 5
        const fctMult = fctNum / 1000 > 1

        // If fct is missing, it'll be zero
        if (fctMult > 1 && easeThreshold > 1 && easeStep > 0) {
          // Add extra time if factor is lower than easeMiddle, reduce if higher
          const extraTime = (fctMult - easeThreshold) * easeStep * 1000
          waitTime = waitTime + extraTime;
        }
        // Start timer for showing answer
        frontTimer = setTimeout(onTimerEnd, waitTime);
      });
    };
    try {
      const jsApiContract = { version: "0.0.3", developer: "https://github.com/jhhr" };

      const api = new AnkiDroidJS(jsApiContract);

      startTimer(api.ankiGetCardFactor());
    } catch (e) {
      if (globalThis.ankiPlatform !== "desktop") {
        console.log("AnkiDroid API error", e);
      } else {
        pycmd("AnkiJS.ankiGetCardFactor()", startTimer);
      }
    }
  }

    initIndicatorTimer({
      // Customize options as you please
      waitSeconds: 5,
      easeThreshold: 2,
      easeStep: 0.7,
    });
</script>

And also the indicator is shown now on the bottom right. How can I make it larger :question:

Refer to this: Methodology to determine the best amount of time to be spent on a card on average - #14 by jhhr

I increased the padding from 10px to 20px in the latest version already.

To get the card average review time, you probably need to write into some card field like CardAvgReviewTime and the include that in the code like this. The problem there is then to manually edit in that data into all your cards.

    initIndicatorTimer({
      // Customize options as you please
      waitSeconds: parseFloat({{CardAvgReviewTime}}),
      easeThreshold: 2,
      easeStep: 0.7,
    });

You could get that data using the {{{info-TimeAvg:}} from Additional card fields addon but of course that means it only works in desktop.

    initIndicatorTimer({
      // Customize options as you please
      // Use avg time from info-TimeAvg field from addon, if available
      // otherwise use static base wait time of 5 seconds
      waitSeconds: {{#info-TimeAvg:}}parseFloat({{info-TimeAvg:}}){{\info-TimeAvg:}}{{^info-TimeAvg:}}5{{\info-TimeAvg:}},
      easeThreshold: 2,
      easeStep: 0.7,
    });