[Feature Request] Add an `api.ankiUnburyDeck()` JS API to AnkiDroid to implement the Playlist Loop feature

Existing Issue

I have been exploring methods to use Anki for foreign language listening practice. In my view, remembering the sound of a foreign language word/sentence is not entirely the same as remembering a mathematical formula.

It is generally believed that humans have two types of memory: procedural memory and declarative memory. Skills like riding a bicycle or swimming are part of procedural memory; whereas the steps of riding a bicycle (“first, hold the handlebars, then step on the bicycle, followed by pedaling” kind of knowledge) and the formula for the area of a circle belong to declarative memory. Remembering the pronunciation of a foreign language word/sentence has its declarative memory part (requiring knowledge of phonetic symbols, pronunciation rules, etc.), but it is more a part of procedural memory. For declarative memory, understanding is key. Anki’s current method of showing a new card once, asking users to understand and try to remember the content, and then using a series of learning steps to check if the user remembers the content, perfectly applies to declarative memory. However, procedural memory relies on multiple stimuli and repeated practice. Anki’s current method of setting a series of learning steps is not quite suitable for tasks such as remembering the pronunciation of a word/sentence. Here’s a detailed explanation:

The formation process of procedural memory for foreign language listening is not instantaneous. It’s akin to a text gradually becoming clearer in your mind: first, it’s blurry or not even there, and it gets clearer and clearer the more you listen to it. Forming procedural memory for the sound of foreign language words/sentences requires dozens or hundreds of stimuli. It is unrealistic to expect users to remember a segment of sound just through one new card display and several learning steps. Forcing this process often leads to being able to actively pronounce a word without the ability to passively recognize it in real scenarios. (people who have learned a foreign language should have this experience). And if you play the audio of a word/sentence dozens of times during each new card display and each learning step, it does not conform to the brain’s learning habits. Listening to a short audio segment repeatedly in one go can be overwhelming. The brain needs repetition, but it also needs novelty. The correct approach should be: after listening to a short audio segment two or three times, even if you don’t fully understand it, you should switch to the next audio segment, and come back later to continue listening to the audio you didn’t fully grasp earlier.

I have a Deck, where each card contains a foreign language word and a sentence mined from that word (with sentence audio). I would like to implement the following learning process with AnkiDroid:

  1. Focus on studying some cards in the morning, answer them to move them into the learning card queue.
  2. Throughout the rest of the day, repeatedly listen to the cards in the learning card queue studied in the morning. I hope AnkiDroid can do this: play the audio from the first card in the learning queue, then continue to the next card. It should not affect the scores or statistics of the cards, until the audio from the last card has been played, then return to the first card and continue playing from the first card, in a loop (or it could randomly play the audio from all cards). So you can listen to these audios on your sofa, in the airplane to your holiday destination, on a train commuting from work, or while jogging. This is like turning AnkiDroid into a player with a “playlist loop” function, where the sentence audios from the cards in the learning queue form a playlist. During the process of repeatedly listening to the entire playlist, if I feel that I can understand the sentence, I will click “good” or “easy” to graduate it from the learning card queue and the “loop list,” and let Anki automatically schedule the next review time, while the remaining cards continue to loop. In this step, mainly use the “good” and “easy” buttons; if you feel “hard” or need “again,” just leave the audio in the playlist to keep looping. And listening to these cards does not require 100% focus, as maintaining focus for a long time is difficult, and any audio missed due to distraction will soon appear in the next loop.
  3. Focus again in the evening on the cards that remain in the learning card queue to see if they can graduate from the loop list. If you still can’t remember by the end of the day, leave it to the next day to loop with the next day’s learning cards (this is somewhat contrary to the principle that the initial learning interval and relearning interval of the FSRF algorithm should be completed on the same day, but I think it’s justifiable).
  4. Treat due review cards as per the normal learning process. If the user wishes, cards in the relearning card queue can also be looped with the learning card queue of the day.

Currently, AnkiDroid has some roundabout methods to achieve step “2”, but none are perfect:

Method 1: Automatically play cards through the “Automatic display answer” feature, and set “Deck options>Auto Advance>Answer action” to “Answer Hard,” and all the learning steps can be completed on the same day, thus forming a “playlist” that loops a series of cards until manually answered to graduate them from the playlist. However, repeatedly rating cards as hard may extremely affect the scheduling algorithm, and will leave a lot of useless data in the card info (if each sentence audio is only a few seconds, it’s easy to listen to it hundreds of times a day). And if you set “Deck options>Auto Advance>Answer action” to “Bury Card,” then the entire learning card queue will only play once, and you need to manually Unbury Deck and re-enter the study interface to continue playing.

Method 2: Execute AnkiDord JS API’s api.ankiSetCardDue(0) after the audio on the card is played, setting the card to be due today. And set “Deck options>Display Order>Review sort order” to “Random.” This somewhat makes the learning card queue a playlist, but it also has problems that may affect the scheduling algorithm and leave a lot of useless data in the card info.

Solution

I have recently looked at some of AnkiDroid’s source code and found it quite easy to add an API to AnkiDroid’s JS API that calls the Scheduler class’s sched.unburyDeck() method. With the new api.ankiUnburyDeck() AnkiDroid JS API and the existing api.ankiBuryCard() API, the learning process mentioned above can be perfectly implemented. Users can automatically bury a card after the audio playback ends on each card through JavaScript. When only one card is left in the entire deck, the script will unbury the whole deck to achieve a loop playback effect, without affecting the scores or statistics of the cards. Therefore, this feature request is to add an api.ankiUnburyDeck() AnkiDroid JS API.

To more clearly illustrate the use case of this API, I have roughly implemented this API locally and recompiled an AnkiDroid, and wrote a sample deck. The video demonstration is as follows:

https://github.com/ankidroid/Anki-Android/assets/29732324/736513a5-7128-402b-a018-3f1da9628831

The front code of the sample Deck card is as follows:

<!--Front Template-->
<!--1 is the normal mode, 2 is the playlist loop mode -->
<div id="isPlayListLoopMode" style="display:none">2</div>

{{Sentence Number}}<br><br>

{{cloze:Example Sentence}}<br><br>
{{Example Sentence Meaning}}<br><br>

<audio controls id="audio" preload="auto">
    <source src="{{Example Sentence Audio for HTML}}" type="audio/mpeg">
    Your browser does not support audio elements.
</audio>

<script>
    var UA = navigator.userAgent;
    var isMobile = /Android/i.test(UA);
    var isAndroidWebview = /wv/i.test(UA);

    var isPlayListLoopMode;

    if (isMobile && isAndroidWebview) {
        // Retrieve the mode from the DOM
        isPlayListLoopMode = document.getElementById("isPlayListLoopMode").textContent;

        // onUpdateHook fires after the new card has been placed in the DOM, but before it is shown.
        onUpdateHook.push(function () {
            if (isPlayListLoopMode === "2") {
                // Automatic show answer for playlist loop mode
                showAnswer();
            }
        });
    }
</script>

The back code is as follows:

<!--Back Template-->
<!--1 is the normal mode, 2 is the playlist loop mode --->
<div id="isPlayListLoopMode" style="display:none">2</div>
<!-- audio repeat times -->
<div id="repeatTimes" style="display:none">2</div>

{{Sentence Number}}<br><br>

{{cloze:Example Sentence}}<br><br>
{{Example Sentence Meaning}}<br><br>

<audio controls id="audio" preload="auto">
    <source src="{{Example Sentence Audio for HTML}}" type="audio/mpeg">
    Your browser does not support audio elements.
</audio>

<script>

    var UA = navigator.userAgent;

    var isMobile = /Android/i.test(UA);
    var isAndroidWebview = /wv/i.test(UA);

    if (isMobile && isAndroidWebview) {

        var isPlayListLoopMode = document.getElementById("isPlayListLoopMode").textContent;
        var repeatTimes = Number(document.getElementById("repeatTimes").textContent);

        if (isPlayListLoopMode === "2") {

            // init AnkiDroidJS API
            if (typeof AnkiDroidJS !== "undefined") {

                var audio = document.getElementById("audio");
                var jsApiContract = { "version": "0.0.3", "developer": "example@example.com" };
                var api = new AnkiDroidJS(jsApiContract);

                var playCount = 1;
                audio.addEventListener("ended", function () {

                    playCount++;

                    if (playCount <= repeatTimes) {

                        // api.ankiShowToast("Play count: " + playCount);
                        audio.play();
                    } else {

                        (async function() {
                            try {
                                // // Call three APIs simultaneously and wait for all of them to complete
                                let [newCardCount, lrnCardCount, revCardCount] = await Promise.all([
                                    api.ankiGetNewCardCount().then(response => response.value),
                                    api.ankiGetLrnCardCount().then(response => response.value),
                                    api.ankiGetRevCardCount().then(response => response.value)
                                ]);

                                // Add the returned values together
                                let totalCount = newCardCount + lrnCardCount + revCardCount;

                                if (totalCount <= 1) {
                                    api.ankiShowToast("unbury whole deck");
                                    // This step is very important! Do not wait for the task to complete here,
                                    // proceed to the next step immediately, otherwise AnkiDroid will exit the Reviewer
                                    api.ankiBuryCard();
                                    await api.ankiUnburyDeck();

                                } else {
                                    await api.ankiBuryCard();
                                }

                            } catch (error) {
                                console.error('An error occurred:', error);
                            }
                        })();

                    }
                });

                onUpdateHook.push(function () {
                    if (isPlayListLoopMode === "2") {
                        // api.ankiShowToast("Play count: " + playCount);
                        audio.play();
                    }
                })

            }
        }
    }

</script>

The Scheduler class’s sched.unburyDeck() is not written to be called in the Reviewer, so it cannot synchronously update the Screen Count in the Reviewer. Adding an AnkiDroid JS API to call the Scheduler class’s sched.unburyDeck() is a straightforward task, which is a Good First Issue, so I completed this recompiled version of AnkiDroid. But a good API should also synchronously update the Screen Count in the top left corner of the Reviewer, and I have studied for a long time and still do not know how to implement it, so I regret that I cannot submit a PR for this API myself. All I’ve done for my version of AnkiDroid that added the api.ankiUnburyDeck() API is merely adding a total of 5 lines of code shown in the two images below (Based on AnkiDroid v2.18.3.):

Using the AnkiDroid API to fully automatically control the looping of cards, while manually rating a card, may also encounter the following problems:

  1. https://github.com/ankidroid/Anki-Android/issues/15936. This is the specific implementation of “Method 2” in the “roundabout methods” mentioned above. This issue may indicate that the AnkiDroid JS API was not designed for such use cases, using it in this way somewhat goes against its original design intention. So I also want to know if using the AnkiDroid JS API in this way will be supported? If I inadvertently violated any taboos, please let me know in time.
  2. Automatically use api.ankiUnburyDeck() to unbury the entire Deck in the code without any prompt may mess up the user’s data. Adding a prompt for user confirmation might be better, but that would go against the original intention of “automatic looping playback without manual intervention.”

Other Considered Solutions

Other solution one: Add an api.ankiDelayCardBySeconds() AnkiDroid JS API. It accepts a parameter x in seconds, setting the card to be due after x seconds, just like a card placed in the learning queue waiting for x min before studying again, but this API does not affect the scores or statistics of the cards. Users can specify the next appearance time in seconds for each card in JavaScript: if the next appearance time for all cards is the same, then loop all cards; If each card waits randomly for x seconds, then randomly play all current cards. You can also control the waiting seconds to make different cards appear at different frequencies. I am not familiar with the internal implementation of the Anki back end, so I don’t know if such a function can be implemented.

Other solution two: Or we can add an api.ankiInsertCardAtReviewQueuePosition() API. Users can insert the card into a specific position in the review queue to a specific position after the audio on the current card is played, but it does not affect the scores or statistics of the cards. This also allows users to implement playlist loop and random playback function through JavaScript. But I also don’t know if such a function can be implemented.

Other solution three: Add “Listening Practice Mode-Playlist Loop” and “Listening Practice Mode-Random Playback” options to “Deck options>Auto Advance>Answer action.” After turning on one of these two modes, during review, AnkiDroid will loop/randomly play the audio from all current cards according to the user’s set mode. After the audio is played, if the user does not answer, switch directly to the next card and play the audio from the next card, do not affect the scores or statistics of the cards.

Other solution four: Set up a special filtered deck dedicated to listening practice (or add a special “listening practice mode” to the filtered deck), and cards that enter this filtered deck for review will be looped until the user answers the card to return it to the original deck.

Among these solutions, I still think the solution proposed in the “Solution” section to add an api.ankiUnburyDeck() JS API is the most suitable. This solution requires very little code change and has no impact on existing users. You can first provide this AnkiDroid JS API to allow users to implement the looping playback function, and then decide whether to support functions like “Playlist Loop/Random Playback” in the future.

Other Comments

Language learning is a major application direction for Anki, and listening practice is a key part of language learning. There are already many practices using Anki series software for listening practice, such as Subs2SRS, etc. But without exception, they all learn a long audio segment on one card. The goal is not to use Anki for learning every minor aspect of the language, but rather to focus on the areas that pose challenges. Therefore, atomizing the audio to help you focus most of your study time on these problem spots is very important. And for atomized audio, the learning method of “playing a series of cards and excluding a card from the playlist after understanding it” is better than “continuously listening to a short audio segment and changing to the next one after completely memorizing it.” So, I believe that adding an api.ankiUnburyDeck() AnkiDroid JS API at this stage is of great significance.

Currently, users can completely control the Reviewer through AnkiDroid JS API, and completely control the next review interval of the card through custom scheduling, but the extraction order of the cards of the day can only rely on the existing options (Ascending intervals, Ascending ease, etc.). With the upcoming release of the AnkiDroid javascript market, we can use the features of JavaScript to develop almost any function player in Anki, and audio-type cards will be better supported (in the scenario of listening practice, the mobile version of Anki naturally has more advantages than the desktop version). And if in the future, users can customize the extraction order of the cards of the day like custom scheduling, making AnkiDroid a programmable audio player, it will greatly improve the applicability of using AnkiDroid for listening practice, facilitate the implementation of functions such as “Playlist Loop/Random Playback”, and may even give birth to some interesting use cases. For example, only store one word in the card without the sentence, before reviewing, first submit all the words due today to an AI system, ask AI to generate a story with these words, and the review process is to read/listen to a story, adjust the extraction order of the cards to match the order in which they appear in the story, when reading/playing to the word if the user does not pause it is considered that the user answered “good” to the word (of course this is just a very immature idea. For listening practice, the “Playlist Loop/Random Playback” two modes should be enough)

In any case, thank you for your contribution to the AnkiDroid community! Your efforts to enhance the learning experience for all users are sincerely appreciated. You’re doing a great job and making a great tool!