Skip to content

Instantly share code, notes, and snippets.

@branneman
Last active June 7, 2025 14:14
Show Gist options
  • Save branneman/c683d476bb75cf528da2114d0ffe2bda to your computer and use it in GitHub Desktop.
Save branneman/c683d476bb75cf528da2114d0ffe2bda to your computer and use it in GitHub Desktop.
Generating bidirectional Anki decks with Python from a CSV file
  1. 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
  2. Activate venv (for every new terminal window/session or OS reboot):

    source venv/bin/activate
    
  3. 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?
    
  4. Generate a unique ID's for your deck:

    python3 -c "import random; print(random.randrange(1 << 30, 1 << 31))"
  5. Replace the 1337 deck ID with your generated ID:

    < genanki.Deck(1337, ...)
    ---
    > genanki.Deck(1749144902, ...)
  6. Run script, specify CSV file, this creates a .apkg file:

    python3 app.py deck.csv
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