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.
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.