Skip to main content

AnkiForgeAI

License: MIT Python 3.11+ CI

AI-powered vocabulary flashcard pipeline for any language with automatic delivery to Anki. Language-agnostic via configurable YAML profiles (languages/{code}/language.yaml).

Built-in languages: 🇳🇴 Norwegian Bokmål (nb), 🇩🇪 German (de), 🇬🇧 English (en), 🇪🇸 Spanish (es).

What it does

  1. Ingest — generates word candidates:
    • by topic via LLM ("20 food-related words, A2")
    • from a URL (web page extraction + LLM parsing)
  2. Enrich — adds grammar forms, translation, example sentences.
  3. Dedupe — checks duplicates against existing Anki notes and staging DB.
  4. Media — generates mp3 audio (edge-tts) and downloads images (pick a provider: Unsplash, Pexels, Pixabay, or key-free Openverse).
  5. Review — interactive review of ambiguous candidates (CLI).
  6. Push — sends approved cards to Anki via AnkiConnect.

Architecture

Decision Why
AnkiConnect, not CSV Two-way sync, dedupe against live Anki deck
SQLite as staging + audit + cache Anki = source of truth, SQLite = local index + history
forms as JSON Different schemas for nouns/verbs/adjectives per language
edge-tts over gTTS Microsoft neural voices, free, per-language quality
Pluggable image provider images.provider: Unsplash, Pexels, Pixabay, or key-free Openverse — legal, free tiers
Selectable transcription transcription: practical (Cyrillic respelling) or ipa — pronunciation hints aren't hardcoded to Russian speakers
Prompts in prompts/*.md Improve card quality without touching code
Any LLM provider OpenRouter, Anthropic Claude, or the local claude CLI (claude_cli — no API key, reuses your claude login; shares that quota with interactive Claude Code use, so better for occasional generation than heavy cron)

Card statuses

pending → review → approved → pushed
              ↓
           skipped / suspended
  • pending — just created, not yet enriched
  • review — potential duplicates found, needs decision
  • approved — ready for push to Anki
  • pushed — already in Anki (anki_note_id set)
  • skipped — discarded (reason in audit_log)
  • suspended — postponed

Supported Languages

Language profiles live in languages/{code}/. Currently supported:

Language Code Status
🇳🇴 Norwegian Bokmål nb ✅ Complete
🇩🇪 German de ✅ Complete
🇬🇧 English en ✅ Complete
🇪🇸 Spanish es ✅ Complete

Select the active language with language: <code> in config.yaml, or pick it interactively via ankiforgeai setup.

Adding Your Language

  1. Create languages/{code}/language.yaml using nb or de as template:
code: xx              # ISO 639-1
name: Language Name
article: true         # whether nouns have articles
pos_labels:           # POS → target language name
  noun: ...
  verb: ...
forms:                # grammar field schema per POS
  noun:
    - {key: gender, label: "Gender"}
    - {key: ...}
tts:                  # edge-tts voices
  voice_female: xx-XX-NameNeural
anki:
  deck_name: MyDeck
back_labels:          # labels in your native language
  translation: "Translation"
  1. Copy prompts: cp prompts/*.md languages/{code}/prompts/
  2. Adapt prompts for the target language
  3. Minimal setup for a working language: language.yaml + topic_words.md

Installation

Requires Python 3.11+ and Anki desktop + the AnkiConnect addon.

As a tool (end users)

uv tool install ankiforgeai   # or: pipx install ankiforgeai / pip install ankiforgeai
ankiforgeai setup             # writes config.yaml in the current directory
ankiforgeai init              # creates the local DB and the Anki Note Type

ankiforgeai setup will ask which LLM provider to use. Pick "Claude Code CLI" if you don't want to set up a separate API key — see No API subscription? below for what that needs and what it trades off.

config.yaml, data/, and media/ are created in whatever directory you run ankiforgeai from — cd into a project folder first (e.g. mkdir ~/ankiforgeai && cd ~/ankiforgeai).

setup also asks for your LLM/image API keys directly (masked input, written to .env) and, if given one, offers to generate and push a small first batch of cards right away — say no and add keys to .env yourself later if you'd rather review what it runs first. It also offers to register the daily-automation cycle (see below) with your OS scheduler on the spot.

init is safe to re-run: if the Note Type already exists in Anki, it pushes the current card templates and CSS to it instead of skipping, so re-run it after pulling an update that changes the card design. This also applies if an update renames the Note Type itself (anki.note_type in languages/{code}/language.yaml) — ankiforgeai push will fail with Note Type '...' не найден в Anki until you re-run init to create it.

From source (contributors)

git clone https://github.com/k0bad/AnkiForgeAi.git
cd AnkiForgeAi
uv venv
source .venv/bin/activate     # Linux/macOS
# .venv\Scripts\activate      # Windows
uv pip install -e ".[dev]"

# Configuration
cp .env.example .env
# edit .env — add your API keys
cp config.yaml.example config.yaml
# edit config.yaml — Anki URL, deck name, language, providers, etc.

# Initialize (DB + Anki Note Type)
ankiforgeai init

No API subscription? Use your Claude Code login

If you already have Claude Code installed and logged in (Pro/Max subscription, or an ANTHROPIC_API_KEY configured at the Claude Code level) you can skip getting a separate OpenRouter/Anthropic key for this project entirely:

claude login          # one-time, skip if already logged in

Then in config.yaml:

llm:
  provider: claude_cli   # instead of openrouter/anthropic
  model: sonnet           # alias — "sonnet" | "opus" | "haiku", not a provider/model string

.env needs no OPENROUTER_API_KEY/ANTHROPIC_API_KEY in this mode — ankiforgeai setup sets this up for you if you pick "Claude Code CLI" at the provider prompt. Under the hood this runs claude -p as a subprocess (see src/ankicards/llm.py::_call_claude_cli).

Trade-off: each call goes through your existing Claude Code session and shares its rate-limit/quota with whatever else you use Claude Code for interactively — it is not a separate billing pool. Fine for occasional/manual generation (ankiforgeai ingest topic ... run by hand); not recommended for the unattended daily cron cycle below if you also do heavy interactive Claude Code work on the same account, since a large batch could eat into quota you'd rather have during the day.

Usage

# Generate 20 food-related words at A2 level
ankiforgeai ingest topic "mat" --count 20 --level A2

# Extract words from a web page
ankiforgeai ingest url "https://example.com/lesson"

# Run interactive review
ankiforgeai review

# Push approved cards to Anki
ankiforgeai push

# Sync Anki → local cache (daily)
ankiforgeai sync

# View stats
ankiforgeai stats

# Consistency check: enrich/images config toggles vs actual card data
ankiforgeai doctor

# Delete cards permanently (frees their id for reuse; irreversible if already pushed)
ankiforgeai delete <id> [<id> ...]

Using with Claude Code

This repo ships two things: the ankiforgeai CLI above (what pip install ankiforgeai gets you), and a Claude Code Agent Skill checked into git alongside it. When Claude Code works inside this directory it auto-loads SKILL.md and drives the same CLI non-interactively — --json output, review accept/skip/suspend/ resume/edit <id> by card id instead of the TTY prompts ankiforgeai review needs a human terminal for. That's what powers the primary workflow: ask in chat ("сгенерируй 20 слов по теме одежда A2"), Claude runs the CLI and reports back. The skill is git-only — it isn't part of the PyPI package.

Automated Daily Cycle

ankiforgeai setup can register this for you (Task Scheduler on Windows, cron elsewhere) — the steps below are for setting it up by hand, or changing a registration setup already made.

# Generate → dedupe (AI-adjudicated)/enrich/media → push (no Telegram notification)
python scripts/daily_topic.py

# Preview what today's topic would be
python scripts/daily_topic.py --dry-run

# Override topic and count
python scripts/daily_topic.py --topic dyr --count 5 --no-push

# Full cycle incl. notifications (config.yaml -> notifications:) — use this for cron
./scripts/daily_topic.sh

daily_topic.py alone does not send notifications — pass --notify (which is what daily_topic.sh does) to fan the report out to every enabled channel in config.yaml -> notifications:. Two backends today, and both can be enabled at once:

  • webhook — POST JSON to any URL (n8n, Zapier, a custom bot gateway); the URL itself is read from NOTIFY_WEBHOOK_URL in .env, overriding notifications[].url in config.yaml when set
  • telegram — direct Telegram Bot API call, no intermediary: set chat_id (and optionally topic_id) in config.yaml, and the bot token via NOTIFY_TELEGRAM_TOKEN in .env — never in config.yaml, which is committed

Either way, keep the real address/token out of config.yaml (it's committed) — with no .env override and an empty config.yaml value, that channel is skipped (logged as notify.no_url / notify.no_token) instead of failing the run. See src/ankicards/notify/. Set up daily_topic.sh as a cron job for hands-free daily vocabulary generation with delivery to your configured channel(s).

Windows

daily_topic.sh needs bash; on Windows use scripts/daily_topic.ps1 (same --notify full cycle) with Task Scheduler instead of cron:

# One-off manual run, same as ./scripts/daily_topic.sh
powershell -File scripts\daily_topic.ps1

# Register a daily trigger (adjust -At to taste)
$action   = New-ScheduledTaskAction -Execute "powershell.exe" `
    -Argument '-NoProfile -ExecutionPolicy Bypass -File "<repo-path>\scripts\daily_topic.ps1"'
$trigger  = New-ScheduledTaskTrigger -Daily -At 8:00am
$settings = New-ScheduledTaskSettingsSet -WakeToRun -StartWhenAvailable
Register-ScheduledTask -TaskName "AnkiForgeAI Daily Words" -Action $action `
    -Trigger $trigger -Settings $settings `
    -Description "ankicards: generate + auto-push + notify daily vocabulary"

-WakeToRun wakes the PC from sleep for the trigger (needs BIOS/power support; won't power on from a full shutdown); -StartWhenAvailable catches up on the next boot if the PC was off at trigger time instead of silently skipping the day.

Project Structure

src/ankicards/
├── models.py              # Card, POS, Status, Decision (Pydantic)
├── config.py              # config.yaml + language profiles
├── db.py                  # SQLite layer
├── cli.py                 # Typer CLI
├── pipeline.py            # Stage orchestration
├── llm.py                 # LLM client (OpenRouter/Anthropic)
├── dedupe.py              # Exact + fuzzy matching (rapidfuzz)
├── doctor.py              # Consistency check: enrich/images config vs card data
├── migrate_ids.py         # One-time UUID → sequential int id migration
├── ingest/
│   ├── url.py             # trafilatura + LLM
│   └── topic.py           # Topic-based generation
├── enrich/
│   ├── grammar.py         # Grammar forms per POS
│   ├── translation.py     # Translations
│   ├── examples.py        # Example sentences
│   └── pronunciation.py   # Pronunciation hints
├── media/
│   ├── tts.py             # edge-tts audio
│   └── images.py          # Image search: unsplash/pexels/pixabay/openverse
├── anki/
│   ├── connect.py         # HTTP client for AnkiConnect
│   ├── sync.py            # Anki → cache sync
│   └── notetype.py        # Note type definition
├── notify/
│   ├── base.py            # Notifier protocol
│   ├── format.py          # Telegram-flavored markdown report renderer (shared)
│   ├── webhook.py         # Generic webhook backend (n8n, Zapier, ...)
│   └── telegram.py        # Direct Telegram Bot API backend
└── review/
    ├── interactive.py     # Rich + questionary UI
    └── actions.py         # Non-interactive accept/skip/suspend/resume/edit/delete

languages/                 # Language profiles (YAML + prompts)
prompts/                   # Default prompts
scripts/                   # daily_topic, run_images
tests/
data/                      # DB, logs (gitignored)
media/                     # Audio, images (gitignored)

Roadmap

  • Multi-language architecture
  • Ingest by topic (LLM)
  • Ingest from URL (trafilatura + LLM)
  • Dedupe (rapidfuzz)
  • Grammar enrichment
  • edge-tts audio
  • Pluggable image providers (Unsplash / Pexels / Pixabay / Openverse)
  • Selectable pronunciation transcription (practical Cyrillic / IPA)
  • AnkiConnect push & sync
  • Interactive review CLI
  • Full auto cycle (cron + dedupe + push + notify)
  • Pluggable notification channels (generic webhook: n8n / Zapier / Hermes / any)
  • AI-adjudicated dedupe — ambiguous fuzzy matches are judged by the LLM (same word vs. coincidentally similar), not blindly auto-accepted or left for a human by default
  • PyPI publication — package and release workflow are ready (see DEVELOPER_GUIDE.md §12), pending one-time trusted-publisher setup on pypi.org

Coming up

  • ankiforgeai config set language.de — CLI command to switch language without hand-editing config.yaml
  • parse_mode: MarkdownV2 support with special-character escaping (notify/webhook.py currently renders for legacy parse_mode: Markdown)
  • Nynorsk as its own language.yaml (nn)

See DEVELOPER_GUIDE.md §10 for the full, actively-maintained dev-facing plan (this list is a snapshot of it).

For Developers

See DEVELOPER_GUIDE.md for the full architecture reference, and CONTRIBUTING.md for contribution guidelines.

Metadata

Release files for ankiforgeai 0.5.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for ankiforgeai 0.5.0
File Size Uploaded
ankiforgeai-0.5.0.tar.gz 101.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for ankiforgeai 0.5.0
File Interpreter ABI Platform
ankiforgeai-0.5.0-py3-none-any.whl Python 3 none any Details

Total release size: 235.0 kB

Release files / ankiforgeai-0.5.0.tar.gz

Download URL ankiforgeai-0.5.0.tar.gz
Size 101.8 kB
Tags Source
SHA-256 checksum
How to use checksums
be64447ed6e529b4d93122d6ef09d8a3b9bafc66f3f3c17405ebb0ae9fdfd031
BLAKE2b-256 checksum
How to use checksums
fcf39e6936a89d104e7059c42843fc2efa9d3323f618248444cc928a483a946b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 18, 2026.

Transparency log

Release files / ankiforgeai-0.5.0-py3-none-any.whl

Download URL ankiforgeai-0.5.0-py3-none-any.whl
Size 133.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e91f65e512ba22a2fcae458d069d50076ea150a98070d4c3c34fd72f66cb22db
BLAKE2b-256 checksum
How to use checksums
e812cbbcfcad6e083e36efa8269427656158e42f82ac576d79012f682cb99703
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 18, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.5.0 This release

2 release files

0.4.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page