-
One-time setup local dev env:
git clone [email protected]:c683d476bb75cf528da2114d0ffe2bda.git csv-to-anki-deck cd csv-to-anki-deck python3 -m venv venv source venv/bin/activate python3 -m pip install genanki gtts mkdir audio echo -e ".gitignore\nvenv\naudio\ndeck.csv\ndeck.apkg" > .gitignore
-
Activate venv (for every new terminal window/session or OS reboot):
source venv/bin/activate
-
Put a CSV file (e.g.
deck.csv
) in the root folder of this repo:front_word,front_sentence,back_word,back_sentence Hallo,"Hallo, ich bin Nora.",Hallo,"Hallo, ik ben Nora." wer,Wer bist du?,wie,Wie ben je?
-
Generate a unique ID's for your deck:
python3 -c "import random; print(random.randrange(1 << 30, 1 << 31))"
-
Replace the
1337
deck ID with your generated ID:< genanki.Deck(1337, ...) --- > genanki.Deck(1749144902, ...)
-
Run script, specify CSV file, this creates a
.apkg
file:python3 app.py deck.csv
Last active
June 7, 2025 14:14
-
-
Save branneman/c683d476bb75cf528da2114d0ffe2bda to your computer and use it in GitHub Desktop.
Generating bidirectional Anki decks with Python from a CSV file
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import csv | |
import genanki | |
from gtts import gTTS | |
import unicodedata | |
import re | |
import os | |
import sys | |
from pathlib import Path | |
if len(sys.argv) < 2: | |
print('Usage: python3 app.py <filename>') | |
sys.exit(1) | |
csv_filename = sys.argv[1] | |
csv_basename = Path(csv_filename).stem | |
media_files = [] | |
def safe_filename(text): | |
text = unicodedata.normalize('NFKD', text).encode('ascii', 'ignore').decode('ascii') | |
text = re.sub(r'[^\w\s-]', '', text).strip().lower() | |
return re.sub(r'[-\s]+', '-', text) | |
model = genanki.Model( | |
1929277496, # Unique ID for each model | |
'Bidirectional word+sentence', | |
fields = [ | |
{ 'name': 'front_word' }, | |
{ 'name': 'front_sentence' }, | |
{ 'name': 'back_word' }, | |
{ 'name': 'back_sentence' }, | |
{ 'name': 'front_audio' }, | |
], | |
templates = [ | |
{ | |
'name': 'Front to Back', | |
'qfmt': '<strong>{{front_word}}</strong><br><br>{{front_sentence}}<br><br{{front_audio}}', | |
'afmt': '<strong>{{front_word}}</strong><br><br>{{front_sentence}}<br><br><hr id="answer"><br><strong>{{back_word}}</strong><br><br>{{back_sentence}}', | |
}, | |
{ | |
'name': 'Back to Front', | |
'qfmt': '<strong>{{back_word}}</strong><br><br>{{back_sentence}}', | |
'afmt': '<strong>{{back_word}}</strong><br><br>{{back_sentence}}<br><br><hr id="answer"><br><strong>{{front_word}}</strong><br><br>{{front_sentence}}<br><br{{front_audio}}', | |
}, | |
] | |
) | |
deck = genanki.Deck( | |
1337, # Unique ID for each deck | |
'Deck' # Name for your deck | |
) | |
with open(csv_filename) as csvfile: | |
reader = csv.DictReader(csvfile) | |
for row in reader: | |
fields = [ | |
row['front_word'], | |
row['front_sentence'], | |
row['back_word'], | |
row['back_sentence'], | |
] | |
if row['front_sentence']: | |
audio_filename = safe_filename(row['front_sentence']) + '.mp3' | |
if not os.path.exists('./audio/' + audio_filename): | |
tts = gTTS(text = row['front_sentence'], lang = 'de') | |
tts.save('./audio/' + audio_filename) | |
print('Generated audio: ' + audio_filename) | |
media_files.append('./audio/' + audio_filename) | |
fields.append(f'[sound:{audio_filename}]') | |
else: | |
fields.append('') | |
deck.add_note(genanki.Note( | |
model = model, | |
fields = fields | |
)) | |
genanki.Package(deck, media_files = media_files).write_to_file('./' + csv_basename + '.apkg') |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment