TIL: How to include media in Anki cards using Genaki

Welcome to the first TIL ✌🏻, where I share small lessons I’ve learnt, these generally aren’t substantial enough to be their own fully featured blog post but are worth sharing, just in case someone else is looking at the same problem.

[A poem is written outside and right justified along the left edge of the panel to the right.] Never have I felt so close to another soul / And yet so helplessly alone / As when I Google an error / And there's one result / A thread by someone with the same problem / And no answer / Last posted to in 2003. [Cueball stands in front of his desk, having risen so the chair has moved away behind him. He is holding on to his computer's screen, looking at it while visibly shaking the screen and shouting at it.] Cueball: Who were you, DenverCoder9? Cueball: What did you see?!

Genaki is a Python package that allows you to programmatically generate decks for Anki, a popular spaced-repetition flashcard program. Recently, I have been working on refactoring a Python application that generates a deck based on a list of words given.

Part of this work involved storing all the image and sound resources to a tmp/ directory so at the end cleaning temporary resources was easy. Another important note about this new implementation is it is OO. So I have two key classes that are of importance to this TIL: AnkiCard and AnkiDeck.

After downloading the image we store it:

# AnkiCard.py
def __download_image(self, url: str):
    img_data = requests.get(url, headers={'User-Agent': 'Mozilla/5.0'})
    img_data.raise_for_status()

    # Identify MIME type and then set the extension type
    img_mime_type = mimetypes.guess_type(url.split("?")[0], strict=True)
    img_extension = mimetypes.guess_extension(img_mime_type[0], strict=True)

    # OUTPUT_DIRECTORY = Path("./tmp").absolute()
    img_path = self.OUTPUT_DIRECTORY / "images"
    img_name = f"{slugify(self.translation_dict['de'], separator='_')}{img_extension}"

    img_file = img_path / img_name

    if not img_path.exists():
        os.makedirs(img_path)

    with open(img_file, "wb") as f:
        logging.info(f"Downloading: {img_name}")
        f.write(img_data.content)
        self.__resize_image(img_file)

    return img_file

Once an image is saved, for example: /tmp/images/die_tante.png we can continue to do the other important things and finally create an Anki Note:

# AnkiDeck.py
def create_notes(self, model: genanki.Model):
    notes = list()
    for card in self.anki_cards:
        notes.append(genanki.Note(
            model=model,
            fields=[card.translation_dict["de"],
                    card.translation_dict["en"],
                    f'<img src="{card.image.name}">',
                    f"[sound:{card.audio.name}]"]))

    return notes

What you might have noticed is when creating the note we don’t use the full path to the media just the media name. What I learnt after a fair bit of debugging is that if you use the full path for the media, it won’t load (even if the media exists at the provided absolute path). You just need to provide the media name. An important note though is that when creating the media library you do need to provide the full path to the media:

# AnkiDeck.py
def __build_media_lib(self):
    media = list()
    for card in self.anki_cards:
        media.append(card.image)
        media.append(card.audio)

This happens as, when your deck is made, the media library (__build_media_lib(self)) is used to copy the resources over to your Anki package, and the card field:

fields=[card.translation_dict["de"],
                    card.translation_dict["en"],
                    f'<img src="{card.image.name}">',
                    f"[sound:{card.audio.name}]"]))

Is simply used to reference the media path in the package. You can observe this yourself by renaming an .apkg to .zip and unzip the file, you will observe files named after numberes, e.g. 5988, 5989 - these are the media files that the cards use. You’ll then also note a file called media which contains a JSON file containing mappings between images and their original names:

{"5988": "sapi5-c11ad087-de1c62eb-797700be-a324a996-51fe3627.mp3",
    "5989": "sapi5com-1bd3bb72-9bc0090b-9216f53b-d44a2ebe-b8bb1fe8.mp3"}

So, in this example, if you located file 5989 and rename it with the file extension .mp3 you’ll have a working audio file.

But to the point, as you can see, when these files are saved in the root directory of the .apkg and not embedded in the card itself using the absolute path, so it makes sense that when adding the card field why you only need to use the name and not the path.