AnkiWeb
- Rating
- 1 (π 1 Β· π 0)
- Updated
- 2026-09-28
- Anki versions
- 2.1.50~
- Description language
- en
AnkiWeb addon 2021931128
Converts photos of textbook pages, worksheets, and handwritten notes into flashcards using Google's Gemini vision models and a user-supplied API key, with review, duplicate checks, and one-click undo.
Open on AnkiWeb GitHub Ask about alternatives
active
| Min Anki | Max Anki | Updated |
|---|---|---|
| 2.1.50 | + | 2026-09-28 |
Loadingβ¦
Extracts text, KaTeX math, and structured lists from right-clicked images in Anki using Gemini 2.5 Flash, with API key and proxy configuration.
Converts Google Docs, Notion, and Obsidian notes into Anki flashcards using customizable YAML workflows and multiple LLMs via OpenRouter.
Integrates free Google Gemini AI into Anki's editor to generate and customize flashcards from text or images via prompts and shortcuts.
Generates full German Anki flashcards from one word, with translations, sentences, grammar, image, audio, and optional cloze cards using your free API keys.
Adds an AI chat side panel to Anki for card help, note generation, flashcard creation, voice Q&A, and customizable themes, using your own API key privately.
Automatically attaches images and GIFs to Anki vocabulary cards using a 14-group classification pipeline with 21 image sources.
An Anki add-on that turns a photo of a textbook page, worksheet, or handwritten notes into flashcards, using Google's Gemini vision models.
Each user supplies their own API key. No key ships with the add-on, and there is no server in between.
The obvious design β embed one API key in the add-on β fails twice over:
.py files. Obfuscation
doesn't help: the key must exist in memory to be sent, and it goes on the wire
where any proxy tool reads it.Because the free tier is per-user, asking each person for their own key gives every user their own independent quota. That scales without limit, costs the maintainer nothing, and removes the secret entirely.
If you later want a hosted option, core/provider.py already has the seam β see
Adding a backend.
From a release (recommended). Grab photo2cards.ankiaddon from the
latest release and
open it, or use Anki β Tools β Add-ons β Install from fileβ¦. Restart Anki.
From source.
python build.py # -> dist/photo2cards.ankiaddon
A .ankiaddon file is a zip with the add-on's files at the top level of the
archive β not nested in a folder. build.py handles that, and packaging by hand
usually gets it wrong.
For development, point Anki's add-ons folder at your working tree instead, so edits take effect on the next Anki restart with no rebuild. Close Anki first β it scans the folder at startup.
:: Windows. /J makes a junction, which needs no elevated prompt (/D would).
mklink /J "%APPDATA%\Anki2\addons21\photo2cards" "%CD%\src\photo2cards"
# macOS
ln -s "$PWD/src/photo2cards" ~/Library/Application\ Support/Anki2/addons21/photo2cards
# Linux
ln -s "$PWD/src/photo2cards" ~/.local/share/Anki2/addons21/photo2cards
Note that Anki will write meta.json β containing your API key β into the linked
source folder. It's gitignored and excluded from the build, which is why both of
those exist.
Tools β Photo to Flashcards β Settingsβ¦
That verify step is a single ListModels call. It confirms the key works and
discovers which model ids the key can actually reach β so the add-on never
hardcodes a model name that has since been renamed or retired.
| Action | Where |
|---|---|
| From image files | Tools β Photo to Flashcards β From image file(s)β¦ |
| From clipboard | Ctrl+Shift+V, or the menu |
Ctrl+Shift+V is bound on Anki's main window only. Inside the editor (Add, Browse)
the same keys still mean Anki's own "paste without formatting".
Both open a review dialog. Nothing is written to your collection until you approve it there β edit fronts, backs, and tags inline, untick anything you don't want, then choose a deck and note type. Selecting a row shows the verbatim text from the page that the card came from, so you can check the model's work.
Every card is ticked to begin with, so approving the batch is one click. A row whose front or back you empty unticks itself and greys out, rather than silently vanishing when you add. Photos are not attached by default; you can toggle photo attachment for the whole batch or check the "Photo" column for specific cards (such as diagrams). Attached photos are kept inside a collapsible "Source photo" disclosure on the card so they never clutter normal review.
Photographing overlapping pages is normal, and so is running the same page twice, so the review step checks for cards you already have. A card's identity is its front and its back β the source quote and photo are provenance, not content, so the same card re-read off a second photo is still the same card.
| Situation | What happens |
|---|---|
| The same card twice in one batch | Collapsed before you see it, keeping the later quote |
| Same question, different answer, in one batch | Shown side by side and tinted; ticking both asks first |
| Already in the target deck, same answer | Not added again. Its citation is refreshed if yours is newer |
| Already in the target deck, different answer | A Duplicates detected dialog shows both backs and asks which to keep |
Nothing already in your collection is overwritten unless you pick the replacement yourself. The deck check covers the deck you are adding to and its subdecks, for the note type you selected.
Everything a review writes β new cards and updated ones β lands as one undoable
batch (Ctrl+Z).
src/photo2cards/ the add-on β this is what gets zipped
ββ __init__.py menu wiring only
ββ manifest.json package metadata
ββ config.json default settings
ββ core/ NO anki/aqt imports β testable under plain pytest
β ββ gemini.py HTTP client + error mapping
β ββ provider.py backend selection (direct vs proxy)
β ββ prompts.py system prompt + response schema β iterate here
β ββ models.py Card / SourceImage / GenerationResult
β ββ dedupe.py when two cards count as the same card
β ββ render.py composes the note back, and takes it apart again
β ββ imaging.py downscale + JPEG encode (Qt)
β ββ ratelimit.py client-side throttle
β ββ errors.py typed exceptions the UI branches on
ββ ui/ Qt + Anki layer
β ββ setup.py settings / first-run dialog
β ββ capture.py file picker, clipboard
β ββ review.py review-and-edit dialog
β ββ dupes.py "which version do you want" dialog
β ββ ops.py QueryOp / CollectionOp wrappers
β ββ store.py config read/write
tests/ pure-logic tests, no Anki needed
build.py -> dist/photo2cards.ankiaddon
The core/ vs ui/ split is the load-bearing decision. Anki has no real test
harness, so anything that can be verified without launching Anki must live where
pytest can reach it. If a test in tests/ ever needs aqt, logic has leaked into
the wrong layer.
ui/ is still checkable without clicking through Anki: its dialogs can be driven
headlessly against a throwaway collection using Anki's own bundled interpreter.
CLAUDE.md has the recipe, along with the constraints and Anki API
quirks worth knowing before changing anything. Release history is in
CHANGELOG.md.
pip install pytest ruff
python -m pytest
python -m ruff check src tests build.py
CI runs both on every push and PR, plus a packaging check. The test matrix
includes Python 3.9 deliberately: Anki has shipped a 3.9 interpreter across
several release lines, and float | None in a function signature imports fine on
3.13 while raising TypeError on 3.9 β a bug that only appears once a real user
loads the add-on. Ruff's FA rules catch most of that statically; the 3.9 leg
catches the rest.
Two constraints worth remembering when adding code:
requests instead of using the official SDK β the SDK's
dependency tree includes platform-specific wheels.QueryOp, collection writes
through CollectionOp.If you decide to host a service (to remove the key-setup step, or to monetize),
set backend to proxy in config and fill in ProxyProvider.generate_cards in
core/provider.py. Your endpoint takes {image_b64, mime_type, deck_hint} and
returns the same {"cards": [...]} shape. Nothing else changes β not the UI, not
the call sites.
Non-negotiables if you do: per-user auth and quota enforced server-side, input size/MIME validation, and a hard monthly spend ceiling with alerting. An unauthenticated proxy is just a slower way to leak your key. You'd also become a data processor for images of other people's coursework, which wants a privacy policy first.
config.md.MIT β see LICENSE.