Haeata-Ash/hanky: Tool to interact with Anki Cards. Contribute to Haeata-Ash/hanky development by creating an account

github.com · 2026-07-23 · by Long Nguyen ·github
Tool to interact with Anki Cards. Contribute to Haeata-Ash/hanky development by creating an account on GitHub.

Haeata-Ash/hanky

Tool to interact with Anki Cards. Contribute to Haeata-Ash/hanky development by creating an account on GitHub.

4 stars · 0 forks · Python

README

hanky: A CLI Application For Generating Anki Decks Hanky is an extendable cli tool which reads flash cards from files, transforms them, then adds them to Anki. For example, a single hanky pipeline might:

use computer vision to pull highlighted words from a printed French text, then translate each word and its sentence to English using a translation service, then generate spoken French audio of the sentence with a text-to-speech service,

turning class texts into rich flash cards with native speech and examples. Checkout the computer vision pipeline demo for the complete example. Hanky is not affiliated or associated with the Anki application/org. Table of Contents

Installation Quickstart Configuration Card Processors & Pipelines

The processor contract Pipelines with multiple processors Attaching media

Custom file loaders Loading from non-file sources Examples CLI Reference Contributing Development Publishing

Installation pip install hanky Quickstart You extend hanky in your own python script, my_script.py, by writing functions called card processors. These functions contain the logic to transform cards. Hanky then handles the cli interface, calling your processors, and finally adding the cards to anki. To illustrate what this looks like, we will create a simple hanky script which ensures all the text on a card is lower case. See Card Processors & Pipelines or the demo folder for more complex, real world examples.

my_script.py

from hanky import HankyPipeline

instantiate the hanky app, creating cards with the "basic" model

hanky = HankyPipeline("basic")

@hanky.card_processor(required_fields=[]) def lowercase_card(card: dict): """Lower-case the text on every field of the card.""" return {field: value.lower() for field, value in card.items()}

run the hanky cli application by running this python file, for example:

python3 my_script.py pipe words.csv --into english::vocab

hanky.run() Running your hanky script You run a hanky script like you would any other python script. Given words.csv: Front,Back Serendipity,A Pleasant Surprise Ephemeral,Lasting A VERY Short Time We would run our new script like so python3 my_script.py pipe words.csv --into english::vocab Here we tell hanky to pipe each word in words.csv through our card_processors, transforming each line of the csv with the lowercase_card, before finally adding each card, of note type basic (the model we gave to the pipeline), into the anki deck english::vocab. This would leave us with a english::vocab deck containing the following cards:

Front Back

serendipity a pleasant surprise

ephemeral lasting a very short time

Note: hanky only adds cards, media, and decks. The Anki models/note types you load into must already exist in your collection. Create them in Anki's UI first. See Adding a Note Type.

Configuration Configure Hanky via a configuration file or via a configuration object. You will only need configuration if you do not use the default Anki profile (User 1) or you are on windows 1. Via TOML file at ~/.config/hanky/hanky.toml:

Path to the Anki collection (the sqlite db Anki stores cards in).

Required only if you do NOT use the default profile, "User 1"

or you use Windows.

Defaults:

Linux: ~/.local/share/Anki2/User 1/collection.anki2

macOS: ~/Library/Application Support/Anki2/User 1/collection.anki2

ANKI_DB_PATH = "~/.local/share/Anki2/User 1/collection.anki2"

Check whether another process (e.g. Anki itself) is using the collection

before opening it. Default: true. Disabling this risks database corruption.

DO_SAFETY_CHECK = true

Allow cards whose fields duplicate an existing card. Default: false.

ALLOW_DUPLICATES = false

Where hanky writes backups of the collection.

Default: ~/.local/share/hanky/backups

BACKUP_FOLDER = "~/.local/share/hanky/backups" 2. A Config object passed to HankyPipeline(...) The object takes precedence over the file. from hanky import HankyPipeline, Config

hanky = HankyPipeline("basic", config=Config(ALLOW_DUPLICATES=True)) Card Processors & Pipelines A card processor is a Python function that runs on each card before it is added to Anki. You could use one to enrich a card (fetch a translation, generate audio) or transform it (lower-case a field, render LaTeX). Multiple processors can be registered on a HankyPipeline app to create a pipeline. The processor contract A processor's first parameter is always the card; a plain dict representing a card's fields. Any parameters after that are automatically picked up from your function's signature and forwarded from the CLI arguments passed via --args key=value. A parameter with no default is required while one with a default is optional. @hanky.card_processor(required_fields=[...]) def my_processor(card: dict, lang): ... For example, you might have the same pipeline for different languages, so you would pass in --args lang=german or --args lang=french, and hanky calls your processor under the hood like so: my_processor(card, lang="german") required_fields defines which fields must already be present on the card when this processor runs. Hanky checks this and raises an error if one is missing. It lets a processor declare what an earlier step must have produced. A processor must return one of:

card: the (modified) dict, when it adds no media; (card, media): a card plus a single CardMedia; (card, [media, ...]): a card plus a list of media.

Whatever changes a processor makes to a card become visible to every processor that runs after it. Pipelines with multiple processors If you register more than one processor they form a pipeline. They run in the order you registered them, each receiving the card in the state the previous one returned it. This example below builds flash cards for learning french vocabulary as an english speaker. Given a spreadsheet whose only column is word:

Word

thanks

hello

...

goodbye

We want to create flash cards with the french translation/definition, an example sentence (in both french and english), and french speech audio for the example sentence. The two processors in our pipeline will be:

scrape_translation: takes an english word and scrapes a website to produce a translation and an example. add_audio: takes the translation and example from step 1 then produces French audio.

Note that for brevity the scrape_wordreference and generate_neural_speech functions are not included. See the full demo file for the complete code. from hanky import CardMedia, HankyPipeline

def scrape_wordreference(word: str, lang_pair: str) -> tuple[str, str]: ...

def generate_neural_speech(utf_8_str: str, voice: str) -> bytes: ...

hanky = HankyPipeline("lang-vocab")

Stage 1: requires `word` (this comes straight from the csv), produces `translation` + `example`.

@hanky.card_processor(required_fields=["word"]) def scrape_translation(card: dict): translation, example = scrape_wordreference(card["word"], "enfr") card["translation"] = translation card["example"] = example return card

Stage 2: requires `translation` (from stage 1), attaches audio media.

@hanky.card_processor(required_fields=["translation"]) def add_audio(card: dict): speech = generate_neural_speech(card["translation"], voice="Lea") audio = CardMedia(speech, ".mp3") card["translation-audio"] = audio.media_ref return card, [audio]

hanky.run() Then we would run the script like normal. python3 my_script.py pipe words.xlsx --into french::vocab

Note that this pipeline was created with the lang-vocab model rather than basic. That means we are assuming that a model called lang-vocab has already been created in the anki ui. See the Anki documentation for adding a note type to learn how this is done.

Attaching media Anki handles media seperately to flash cards, so Hanky does as well. To add audio (or other media), you create CardMedia objects, add a reference to the media via .media_ref onto your card before finally returning the CardMedia object from the processor. The CardMedia objects are created from raw bytes and a file extension CardMedia(audio_bytes, ".mp3")or from an existing file CardMedia.from_file("my_sound_file.mp3"). audio = CardMedia(mp3_bytes, ".mp3") card["Speech"] = audio.media_ref return card, [audio] media_ref is how anki knows where the media should go, so it must be placed somewhere on the card. Hanky will make sure that the data will end up in Anki's media store. Supported media is currently only audio: .mp3, .oga, .opus, .wav, .weba, .aac. Custom file loaders To load formats beyond CSV/JSON, register a loader against a file extension. A loader takes an open file object and yields one dict per card. import pandas

def excel_loader(f_obj): df = pandas.read_excel(f_obj) for _, row in df.iterrows(): yield row.to_dict()

is_text=False because .xlsx must be opened in binary mode

hanky.register_loader(".xlsx", excel_loader, is_text=False) Loading from non-file sources The CLI loads cards from files, but you can also build cards in your own script and add them by calling import_from_source directly. It takes any iterable of dicts, one dict per card, so the source can be a list, a generator, rows from an API, or anything else. import random

from hanky import HankyPipeline

DICTIONARY = { "ephemeral": "lasting for a very short time", "serendipity": "the occurrence of happy events by chance", "petrichor": "the smell of rain falling on dry ground", "lucid": "expressed clearly and easy to understand", "gregarious": "fond of the company of others; sociable", # ... }

hanky = HankyPipeline("basic")

def random_word_cards(n): for word in random.sample(list(DICTIONARY), n): yield {"Front": word, "Back": DICTIONARY[word]}

add 20 cards straight from the generator, with no file involved

report = hanky.import_from_source(random_word_cards(20), "english::vocab") print(f"Added {report.added}, skipped {report.skipped}, failed {report.failed}.") import_from_source runs the same processor pipeline as the CLI and returns a LoadReport with counts of the cards that were added, skipped, and failed. Examples Complete, runnable scripts live in the demo/ directory, ordered here from simplest to most involved. Each demo lives in its own folder with a requirements.txt listing just its dependencies; install them with, e.g., pip install -r demo/scrape/requirements.txt.

demo_lowercase.py: The minimal example: a single, dependency-free processor that lower-cases every field on a card. demo_random_words.py: A non-file source: builds cards from an in-script word list and adds them with import_from_source. demo_define.py: A single processor that fills the back of each card with a dictionary definition of the word on its front, looked up offline via WordNet (NLTK). demo_audio.py: Registers a custom .xlsx loader and uses AWS Polly to attach text-to-speech audio, choosing the language from a CLI argument (--args lang=french). demo_scrape.py: A two-stage English→French pipeline that scrapes a translation and example sentence from WordReference, then voices the translation with AWS Polly. demo_example_sentences.py: A three-stage English→French pipeline that scrapes a translation from WordReference, asks Claude for an example sentence at a given CEFR level, then adds French audio for both the word and the sentence with AWS Polly. demo_highlight_words.py: A computer-vision based pipeline which loads a photograph of printed French text you've marked with a highlighter pen, and turns every highlighted word or phrase into a flash card with the sentence it appeared in as context.

CLI Reference Both the hanky command and your own scripts share the same interface: [hanky | python3 my_script.py] <operation> <file|dir> [pattern] [options]

Note that cards are created with the model set on the pipeline (HankyPipeline("my-model")). Since standalone hanky command has no script of its own, it uses Anki's built-in Basic model unless you override it. pipe: pipe cards from a single file into a deck: hanky pipe [-h] [-m MODEL] [--into DECK] [--fail-fast] [--dry-run] [-v] [--args K=V ...] file file File to load from (.csv, .json, or a registered extension). -m, --model Override the Anki model/note-type name to create cards with. --into Destination deck. Defaults to the filename without extension. --fail-fast Stop and raise on the first card that can't be added, instead of skipping it and reporting it at the end. --dry-run Run card processors and report what would happen, without touching your Anki collection (no deck, media, cards, or backup added). -v, --verbose Print each card's dictionary after all processors have been run alongside any errors in the final report. --args key=value args forwarded to your card processors (scripts only).

pipe-dir: pipe many files from a directory, deriving deck names from paths: hanky pipe-dir [-h] [-m MODEL] [-r] [--fail-fast] [--dry-run] [-v] [--args K=V ...] dir pattern dir Directory to load from. pattern Glob selecting files, e.g. "*.csv". -m, --model Override the Anki model/note-type name to create cards with. -r, --recursive Also descend into sub-directories. --fail-fast Stop and raise on the first card that can't be added, instead of skipping it and reporting it at the end. --dry-run Run card processors and report what would happen, without touching your Anki collection (no deck, media, cards, or backup added). -v, --verbose Print each card's dictionary after all processors have been run alongside any errors in the final report. --args key=value args forwarded to your card processors (scripts only).

For example, to load every CSV under french/ while building deck names from the folder structure: hanky pipe-dir "french/" "*.csv" -r french/ decks created ├── animals.csv -> french::animals ├── bodies.csv -> french::bodies └── grammar/ └── passe_compose.csv -> french::grammar::passe_compose

CSV (.csv) and JSON (.json) files work with no setup. The column/key names in your file must match the field names of the Anki model you target. Contributing Pull requests are welcome. For major changes, open an issue first to discuss what you'd like to change. Please make sure tests pass (uv run pytest) before submitting. Development uv sync # install dependencies (incl. dev group) uv run pytest # run tests (CI runs Python 3.11, 3.12, 3.13) uv run ruff format . # format uv run ruff check . # lint uv run mypy src/hanky # type check Publishing (For Me) If you are not me, please ignore. Versions come from git tags via hatch-vcs.

Tag and push: git tag -a 0.3.1 -m "hanky 0.3.1" git push origin 0.3.1

CI builds and publishes to PyPI automatically via trusted publishing.

source

github.com · 108 links

Comments (0)

No comments yet