Extract Notability Learn quizzes, summaries, and note text; export to Anki / JSON / Markdown with a PySide6 GUI
Project description
notability-extractor
Extract Notability Learn content (AI-generated quizzes, summaries, and OCR'd
note text) and export it as an Anki .apkg deck, JSON, or Markdown for review.
How it works
Notability Learn generates quizzes via Claude Haiku and summaries via Gemini
2.5 Flash, server-side. Both get cached locally in the app's HTTP cache.
Handwriting OCR and PDF text live inside .nbn note bundles.
The tool runs in two phases:
- Extract (macOS only): walks the iCloud Drive
.nbnbundles and the HTTP cache (Cache.db+fsCachedData/), writes a normalized export directory at~/notability_export/. - Build (any OS): reads the export directory, merges flashcards into a
persistent JSONL archive at
~/.notability_extractor/cards.jsonl, and emits outputs:.apkgfor Anki,.jsonfor programmatic review,.mdfor human reading.
Linux and Windows machines can skip phase 1 by pointing --input-dir at a
directory produced on a Mac. Useful if you want to do the Anki packaging on a
different machine than the one Notability runs on.
The JSONL archive is the source of truth for flashcards. Both the CLI and the
GUI read from and write to it, and the build always reconstructs .apkg from
the archive (not the input dir) so edits and tags persist across rebuilds.
Install
End users (recommended)
Install from PyPI with pip. You do NOT need to clone this repo and you do NOT
need uv or make.
Linux:
pip install --user notability-extractor
macOS:
# macOS only ships `pip3`, not `pip`. Use this:
pip3 install --user notability-extractor
# or equivalently:
python3 -m pip install --user notability-extractor
This installs two console scripts to ~/.local/bin/:
notability-extractor- the CLInotability-extractor-gui- the desktop GUI
Make sure ~/.local/bin is on your PATH. On macOS that usually means adding
export PATH="$HOME/.local/bin:$PATH" to ~/.zshrc.
The first time you launch notability-extractor-gui, it auto-installs a
desktop shortcut (a .desktop entry on Linux, a .app bundle in
~/Applications/ on macOS) so you can launch it from your app menu next time.
If you'd rather skip that step, run notability-extractor --remove-shortcut.
Developers
This part only matters if you want to hack on the code:
git clone https://github.com/mdeguzis/notability-extractor.git
cd notability-extractor
make install-dev # requires `uv` - see https://docs.astral.sh/uv/
make install-dev uses uv to set up .venv/
for dev work and drops the console scripts into ~/.local/bin/. uv is a
dev-only tool here, end users don't need it. (make install is intentionally
not the dev target - it just prints these instructions, so people who try the
obvious thing don't accidentally pull in the dev toolchain.)
Usage
# macOS: auto-extract and build all outputs in the current dir
notability-extractor
# Anywhere: build from a pre-extracted directory
notability-extractor --input-dir ~/notability_export
# Custom output directory
notability-extractor --input-dir ~/notability_export --out-dir ./decks
# macOS: just run phase 1 (produce export dir, no .apkg/json/md)
notability-extractor --extract-only
# Custom Anki deck name (only affects what shows up inside Anki)
notability-extractor --deck-name "Biology 101"
Output filenames are fixed:
| File | Contents |
|---|---|
notability_flashcards.apkg |
Anki deck (quiz questions only) |
notability_flashcards.json |
Full structured dump for programmatic review |
notability_flashcards.md |
Human-readable flashcards |
notability_notes.{json,md} |
Note transcripts |
notability_summaries.{json,md} |
Generated summaries |
Archive management CLI flags
# Open a prompt-driven editor against the archive
notability-extractor --edit-flashcards
# One-shot interactive add
notability-extractor --add-card
# Print archive contents (optionally filter by tag)
notability-extractor --list-cards
notability-extractor --list-cards --tag biology
# Backup / restore round-trip
notability-extractor --backup
notability-extractor --export ~/cards-backup.jsonl
notability-extractor --import ~/cards-backup.jsonl --mode merge
notability-extractor --import ~/cards-backup.jsonl --mode replace
# Launch the GUI
notability-extractor --gui
GUI
A PySide6 desktop companion ships in the same package. After install:
notability-extractor-gui
Pages: Library (browse / edit / add / delete cards with tag filter), Notes (read-only), Summaries (read-only), Build (export apkg / json / md), Settings (theme, paths, backups, schedule).
On Wayland with no DISPLAY set, the GUI auto-applies
QT_QPA_PLATFORM=wayland so SSH/login sessions don't need a manual export.
Backups
The archive at ~/.notability_extractor/cards.jsonl is snapshotted on every
save to ~/.notability_extractor/backups/cards-YYYYMMDD-HHMMSS.jsonl,
hash-deduped so unchanged saves don't make redundant copies. Default retention
is the last 10 snapshots (configurable in Settings).
For a scheduled backup when the GUI is closed, run from cron:
0 * * * * notability-extractor --backup
The Settings page surfaces this exact line for convenience.
Importing into Anki
- Open Anki on your desktop.
File > Importand select the generated.apkgfile.- The deck appears as "Notability Flashcards" (or whatever you passed to
--deck-name).
Caveats
- The Learn cache only contains content from sessions you've actively opened in Notability. If a note has never had Learn run on it, no quiz is cached.
- Notability does not provide a stable export API. The tool reads on-disk formats that could change between app versions. If extraction breaks after a Notability update, open an issue.
- iPadOS-only setups need iCloud Drive sync enabled so the
.nbnbundles and cache files are present on a Mac. Without sync, you'd need physical access to the iPad's sandbox (not currently supported).
Releasing
Releases are semi-automated via GitHub Actions. Publishing to PyPI is gated
on a maintainer explicitly publishing a GitHub Release - normal commits to
main never push to PyPI.
Releases are semi-automated. CI proves the code is shippable (tests + builds + draft GitHub Release with the wheel attached). The maintainer drives the actual ship via two explicit local commands. Nothing publishes automatically from CI.
To cut a new release:
- Bump the
version = "X.Y.Z"line inpyproject.toml - Commit and push to
main - CI runs on the push: tests pass -> autotag creates
vX.Y.Ztag -> build produces wheel and sdist -> a draft GitHub Release is created with the wheel attached - (optional)
make smoke-testto verify the wheel pip-installs cleanly in a throwaway venv make github-release— seeds release notes fromgit logsince the last tag, opens them in$EDITOR(vim default) so you can tweak, then on save+quit publishes the GitHub Release (writes notes + flips draft to published). This does NOT push to PyPI.make pypi-release— confirms the version, checks PyPI doesn't already have it, then builds + uploads the wheel viaupload-to-pypi.sh(twine +~/.pypircAPI token).
Run steps 5 and 6 in either order, or run only one of them. If you want a
release-notes-only update on a previously-published version, run just
make github-release (which fails clean if the release isn't a draft).
No manual tag step. No release notes to hand-write. No CI auto-publish.
Testing pip install before pushing to PyPI
To verify a pre-release wheel installs cleanly the same way pip3 install notability-extractor would for end users, run:
make smoke-test
This is a self-contained PASS/FAIL check:
- cleans
dist/, builds a fresh wheel - creates a throwaway venv at
.venv-smoke/ pip install dist/*.whl(real pip, mimics PyPI)- runs
notability-extractor --versionand checks the GUI binary is there pip uninstallthen nukes the venv
If anything in that chain breaks, you find out before pushing to PyPI.
To separately exercise the GUI's first-launch desktop-shortcut auto-install:
make reset-shortcut # clear any existing launcher + first-launch marker
notability-extractor-gui # should auto-install the shortcut on first launch
ls ~/.local/share/applications/notability-extractor.desktop # (Linux) verify
ls ~/Applications/'Notability Extractor.app' # (macOS) verify
If smoke-test ever gets interrupted halfway and leaves state behind:
make smoke-clean # remove the .venv-smoke dir + any leftover shortcut
One-time PyPI setup
make pypi-release uses ~/.pypirc + twine to upload (not OIDC), so the
only setup is:
-
Get a PyPI API token: https://pypi.org/manage/account/token/
-
Drop it into
~/.pypirc:[distutils] index-servers = pypi testpypi [pypi] username = __token__ password = pypi-<your-token> [testpypi] repository = https://test.pypi.org/legacy/ username = __token__ password = pypi-<your-test-token>
That's it. No trusted-publisher config, no GitHub pypi environment, no OIDC
exchange. The maintainer's local machine is the trust anchor.
Manual ad-hoc upload
Only needed if CI is broken:
./upload-to-pypi.sh --test # TestPyPI dry-run first
./upload-to-pypi.sh # then prod PyPI
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file notability_extractor-0.5.0.tar.gz.
File metadata
- Download URL: notability_extractor-0.5.0.tar.gz
- Upload date:
- Size: 140.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
27b316ec2638feb1bda87d5336aa4710aa604fd9f3ba183efaec82df6e9de79a
|
|
| MD5 |
f915a1ccd11b7237b5404b82d8c61c32
|
|
| BLAKE2b-256 |
95646137a45c45bfbd0d3938f003c52e7229ff91a66cce4c8f23c789257ebe93
|
File details
Details for the file notability_extractor-0.5.0-py3-none-any.whl.
File metadata
- Download URL: notability_extractor-0.5.0-py3-none-any.whl
- Upload date:
- Size: 68.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
52b14ef19bee548a241c4a2e51bcea714869653c574ed9abbf67d35ccff07609
|
|
| MD5 |
c23d66f6174a56c9edf54431e408de9e
|
|
| BLAKE2b-256 |
d3d853cc2df5acaef5561c0e44741f798a6e08f411c4dc7ab2df390707f07cb9
|