Learnage
Learnage is a local-first book interviewer that remembers what it read. Drop in epub, pdf or docx files; Learnage ingests them, distils each book's soul — its themes, voice, characters, plot shape and key passages — and then lets you interview the book like you'd interview the author. One book at a time, or a whole panel of them at once. Built on Ollama. No cloud, no accounts, no telemetry.
Learnage 2.0.0
What is Learnage?
Most "chat with your documents" tools treat your files as a pile of opaque chunks to be retrieved. They forget the moment you close the tab, they have no sense of the work as a whole, and they can't carry the character of what they read into a conversation.
Learnage does three things differently. First: when it ingests a book it generates a structured soul and keeps it alongside the chunk index — the soul is what the model carries into the conversation, the chunks are just the evidence it pulls on demand. Second: the library is durable — books, souls, embeddings and every conversation are persisted, so the library itself is the product. Third: you can put several books in the room at once and compare them.
🫀 A Soul, Not Snippets
Every ingested book gets a structured soul: summary, voice, themes, characters, plot, key passages, craft notes — plus metadata, concepts, figures and questions.
💾 Durable Library
SQLite for books, chunks, sessions and messages. ChromaDB for vectors. Close the app, come back next month — the model still knows every book.
🎙️ Interview a Book
Ask what the author really thinks, and get an answer grounded in passages you can cite. The model is told to say "the book doesn't address that" when it can't.
🎭 Panel Interviews
Comma-separate titles to put several books in the room. The model attributes claims per book and flags where they disagree.
🖥️ Native Desktop GUI
PySide6 app with cover thumbnails, a Soul view, a Read view, notes, chats, and a ⌘K command palette driven by a real command registry.
⌨️ Textual TUI
Three tabs — Library, Ingest, Interview — with parallel background workers so the UI never blocks while a book is being chewed on.
🔗 Clickable Citations
In the GUI, [pN] markers in the answer become clickable chips that jump straight to the source passage in the book.
🧠 Minds
Scoped chat personas — Library, Reading, plus your own — each with its own system prompt, temperature and retrieval settings.
📥 Background Ingestion
A standalone daemon watches an inbox folder and ingests anything you drop there. Or use the built-in worker pool in the TUI and GUI.
🔒 100% Local
The only network call Learnage ever makes is to your own Ollama at localhost:11434. No cloud, no telemetry, no accounts.
Privacy Promise
Your books never leave your machine. Learnage talks to nothing but your local Ollama daemon — there is no Learnage server, no account, no analytics and no error reporting. The whole source is MIT licensed and lives on GitHub, so you can audit every line of it.
Requirements
Learnage is pure Python, but it leans on two local services/tools: uv for the environment and Ollama for the brains.
| Thing | Version | Why |
|---|---|---|
| Python | 3.11+ | Declared in pyproject.toml as requires-python = ">=3.11". |
| uv | recent | Environment + lockfile in one. uv sync sets everything up. |
| Ollama | running locally | Chat and embedding inference. Defaults to http://localhost:11434. |
| nomic-embed-text | 768-dim | The embedding model, pulled into Ollama. Small and fast. |
| qwen3:14b | default chat model | What the balanced preset uses for chat and soul analysis. |
| Disk | ~5 GB+ | Room for the Ollama models plus your library and its vectors. |
Which model is actually the default?
Learnage's default preset is balanced (presets/balanced.yaml), which sets llm: qwen3:14b and soul_model: qwen3:14b. If you want the lighter touch, the fast preset uses qwen3:8b for both. Either way you'll want that model pulled into Ollama before you start.
Installation
Three steps: clone, sync, pull the models.
Step 1 — Clone and sync
git clone https://github.com/TheSolai/learnage.git
cd learnage
uv sync
uv sync creates the virtualenv and installs the locked dependency set: httpx, pypdf, ebooklib, beautifulsoup4, python-docx, chromadb, pyyaml, rich, textual, pyside6 and pillow.
Step 2 — Make sure Ollama is up
ollama serve # if it isn't already running
ollama pull nomic-embed-text
ollama pull qwen3:14b # default chat + soul model
ollama pull qwen3:8b # optional: for the 'fast' preset
ollama pull gemma4:31b # optional: for the 'literary' preset
Pointing at a different Ollama
Learnage reads LEARNAGE_OLLAMA_URL if it's set, otherwise it uses http://localhost:11434. Handy if you run Ollama on a different box on your LAN.
Step 3 — Sanity check
uv run learnage doctor
The GUI needs a display
uv run learnage gui opens a native PySide6 window — it needs a real desktop session (macOS, Linux with X/Wayland, Windows). On a headless box, stick to the CLI and the TUI.
Quick Start — doctor First
Before anything else, ask Learnage to check its own homework. doctor pings Ollama, verifies the three models it needs (embed, llm, soul), and confirms the library database and vector directory exist.
uv run learnage doctor
───────────────────────────── Ollama ───────────────────────────── ✓ reachable at http://localhost:11434 ✓ embed: nomic-embed-text ✓ llm: qwen3:14b ✓ soul: qwen3:14b ────────────────────────────── Library ───────────────────────────── ✓ db at /Users/you/.learnage/library.db — 0 book(s) ✓ vectors at /Users/you/.learnage/vectors
Every line is a tick or a cross. A red cross on llm or soul means that model isn't in Ollama yet — go pull it. A red cross on the Ollama line means it isn't running, or it's on a different port.
Then open the TUI
uv run learnage tui --workers 8
--workers is the number of parallel ingest worker processes (8 by default). The TUI never blocks: ingestion runs in a process pool while you carry on browsing and interviewing.
Prefer a window?
uv run learnage gui opens the same library in a native PySide6 desktop app, with the same --workers option. Both front-ends read and write the exact same library on disk, so you can mix and match freely.
Ingesting Your First Book
Ingestion is a pipeline: parse → clean → chunk → embed → store → analyse soul. Learnage hashes the file first, so re-ingesting the same book updates it rather than duplicating it.
From the command line
# One book
uv run learnage ingest ~/Books/SomeBook.epub
# Several at once, or a whole directory
uv run learnage ingest ~/Books/One.epub ~/Books/Two.pdf ~/Books/Folder/
# Skip the soul pass — much faster, chunks are still indexed
uv run learnage ingest --no-soul ~/Books/SomeBook.epub
# Force a re-ingest even if the hash matches
uv run learnage ingest --overwrite ~/Books/SomeBook.epub
You'll get a summary table when it's done:
→ parsing SomeBook.epub
epub · 24 chapter(s) · 412,880 chars
611 chunk(s) · ~487,000 tokens
title id chunks soul note
Some Book 7f3c1a92 611 ✓
From the TUI
Open the Ingest tab (ctrl+i), type a path or hit Add dir for a folder. Files land in a queue handled by the --workers subprocesses; the live queue and per-file state show underneath. The UI stays responsive the whole time.
From the GUI
Click Add books in the sidebar, or press ⌘I. You can pick files and folders. Cover thumbnails are extracted automatically — the embedded cover for EPUBs, the first page rendered for PDFs — and cached in ~/.learnage/covers/.
Supported formats
- EPUB — parsed with
ebooklib+BeautifulSoup4, including the Dublin Core title/author metadata. - PDF — parsed with
pypdf. Uses the PDF outline for chapters when there is one, and falls back to a heading heuristic, then to fixed 8-page sections. - DOCX — parsed with
python-docx, headings become chapters. - TXT — parsed too; the background daemon will happily pick up
.txtfiles as well.
How the chunker works
The chunker is sentence-aware: it splits text into sentences, packs them until it hits the target token budget, and keeps the trailing sentences as overlap for the next chunk. The target size comes from your preset — 800 tokens with 150 overlap on balanced, 600/120 on fast, 1000/200 on literary.
The Soul of a Book
The soul is Learnage's core idea. It's a structured literary reading of the book, generated by a separate LLM pass after chunking, then stored as JSON alongside the book's record. It's what the model carries into every conversation about that book.
📝 Summary
One paragraph — three to five sentences capturing the book's essence.
🎙️ Voice
Register, tone, rhythm and diction — how the book sounds.
🧩 Themes
Five to ten themes, each with a one-line gloss.
🎭 Characters
Major characters with their role and a one-line description.
📐 Plot
The shape of the thing, its central tension and its arc.
🔖 Key Passages
Three to ten load-bearing quotations, each with context and a reason why it matters.
🛠️ Craft Notes
Observations about how the author gets their effects — gold for writing manuals.
🏷️ Meta & Concepts
Genre, period, form, language, plus key concepts, named figures and tone words.
❓ Questions
The questions the book answers, framed as if a reader asked them.
Read the soul for yourself
learnage show takes a title (or a substring of one) and prints the soul as JSON:
uv run learnage show "Some Book"
Or check the whole shelf:
uv run learnage library
Your library
title author chunks soul ingested
Some Book Jane Author 611 ✓ 2026-11-04T09:12:03
Another One — 288 ✓ 2026-11-04T09:14:51
No soul on a book?
If a book was ingested with --no-soul, its soul is empty. Re-ingest it (or regenerate the soul from the GUI's ⌘K palette) and the model will have its character back.
Interviewing a Book
Every answer is composed from four parts: the system preamble (Learnage is an interviewer of books), the soul of each selected book, the top retrieved passages with citation metadata, and the recent conversation history. Then your question goes in. The model is instructed to ground every claim in a citable passage, and to say plainly when the books don't address something.
The CLI REPL
uv run learnage read "Some Book"
╭──────────────────────────────────────────────────────────────╮ │ Learnage · interviewing Some Book │ │ session: 8fd2c1a09b34 · model: qwen3:14b · k=8 │ │ type /help for commands │ ╰──────────────────────────────────────────────────────────────╯ learnage › what does the author think about grief? ...answer streams in, token by token... learnage › /soul learnage › /passages learnage › /clear learnage › /quit
REPL commands
/help— show the command list./soul— show the soul of every book in the session./soul <book> <section>— narrow in on one book and print the raw JSON for that part./passages— show the passages used for the last answer, with scores./sources— show the source (book + chunk + chapter) for the last answer./books— list the books in the session./clear— clear the conversation memory (the souls stay)./quit— leave./exitand/qwork too.
Panel interviews — several books at once
Comma-separate the titles:
uv run learnage read "Book A","Book B","Book C"
When more than one book is in the room, the prompt tells the model to attribute claims to specific books and to flag disagreement rather than smoothing it over. Very handy for comparing craft or stress-testing an idea against several influences.
The Textual TUI
uv run learnage tui opens a three-tab interface:
- Library (
ctrl+l) — a sortable table of every ingested book with author, chunk count, soul status and ingest date. Select a row to read its soul underneath. - Ingest (
ctrl+i) — type a path or add a directory; files queue up and get processed by the background worker pool, with live queue state below. - Interview (
ctrl+r) — choose the books you want to talk to, then ask. The answer streams in, grounded in the books you selected.
ctrl+q quits the TUI.
How many passages get retrieved?
Retrieval k comes from the preset: 8 on balanced, 6 on fast, 10 on literary. Override it per run with --k, e.g. uv run learnage --k 12 read "Some Book".
The Desktop GUI
uv run learnage gui opens the native PySide6 app — a dense, native-Mac launcher with a sidebar, a book list, and separate windows for each book you open. Version 2.0 split the old monolith into focused modules under learnage/gui/ and learnage/gui/views/.
uv run learnage gui --workers 8
Sidebar sections
- Library — every book, with cover thumbnails and selection.
- Souls — filters the list down to the books whose soul analysed successfully.
- Chat all — one global conversation across your whole library.
- Ingesting — the live worker queue.
- Recent — the 30 most recently ingested books.
- Add books — the ingest panel.
The book window
Double-click a book and it opens in its own window with four tabs:
- ✦ Read — the book's chunks as navigable sections, with a filter box, font sizing, bookmarks and export.
- ✎ Notes — your own notes, saved per book under
~/.learnage/notes/. - ✺ Soul — the full SoulView: summary, voice, themes, characters, plot, key passages, craft notes.
- ◇ Chat — the interview for this book.
┌─ Sidebar ─────────┐┌─ ✦ Read ✎ Notes ✺ Soul ◇ Chat ─────────┐ │ ◇ Library ││ ┌──────────────────────────────────────────┐ │ │ ✺ Souls ││ │ Chapter 3 — The Weight of Small Things │ │ │ ✦ Chat all ││ │ │ │ │ ⟳ Ingesting ││ │ She had learned to say less. The room │ │ │ ◴ Recent ││ │ was not large, and neither was the │ │ │ + Add books ││ │ trouble... │ │ │ ││ │ ▏ │ │ │ ── Minds ─────────││ │ │ │ │ ✦ Library ││ └──────────────────────────────────────────┘ │ │ ◇ Reading ││ [filter…] [− A +] ⚑ bookmark ⤓ export │ │ ☽ Witchcraft │└──────────────────────────────────────────────┘ │ ✎ Writing │ │ ✧ Study │
⌘K command palette
Every action in the app is a registered Command in learnage/gui/command_registry.py — a key, a label, a glyph, a category and a one-line description. Press ⌘K and the palette opens, fuzzy-matching against that registry and filtering by what's currently selected (a book, a mind, the library).
| Category | Commands |
|---|---|
| Open | Open book… · Search passages… · Open mind… · New mind |
| Ingest | Add books… · Regenerate soul (selected book) · Weed library… · Back up library now |
| Search | Search title or author… |
| Book | Open book in new window · Forget this book · Export book as Markdown… · Categorize… · Bulk categorize… |
| Settings | Toggle dark / light theme · Settings… |
| Action | New chat (active window) · Cancel |
Streaming citations
As the answer streams in, any [pN] marker the model emits is turned into a small clickable chip that resolves to learnage:cite/<book_id>:<chunk_index>. Click the chip and the source passage opens in the book window. Underneath each answer sits a footer listing every citation with its book, chapter and score.
┌─ Chat ────────────────────────────────────────────────┐ │ you: what does the author think about grief? │ │ │ │ assistant: The book treats grief as something you │ │ rehearse rather than something that happens to you │ │ [1] — the narrator only names it in the past tense, │ │ and even then, obliquely [2]. The central tension │ │ seems to be whether naming it would undo it [3]. │ │ │ │ [1] Some Book · ch. 3 [2] Some Book · ch. 7 │ │ [3] Some Book · ch. 11 │ │ │ │ ┌────────────────────────────────────────────────────┐ │ │ │ Ask… ⏎ │ │ │ └────────────────────────────────────────────────────┘ │ └────────────────────────────────────────────────────────┘
Minds — scoped chat personas
A Mind is a chat with its own system prompt, temperature and retrieval settings, scoped to part of your library. Five ship built in:
- Library — every book; the default.
- Reading — the book currently open, side by side.
- Witchcraft — books of magic, ritual and the occult.
- Writing — craft and writing books.
- Study — study and learning material.
You can create your own from New mind in the palette. Built-in minds are re-seeded on every launch, so your edits to them are lost by design; custom minds are kept verbatim in ~/.learnage/minds.json.
Other keyboard shortcuts
⌘K— command palette⌘T— toggle dark / light theme⌘I— jump to Add books⌘,— settings (library path, workers, model)⌘⇧L— focus the library⌘⇧N— new chat session in the active window
Searching Your Library
Want the raw passages rather than an answer? learnage search does a semantic search straight across the vector store and prints what it finds, grouped by book.
# Search every book
uv run learnage search "the sea is a character in itself"
# Restrict to specific books
uv run learnage search "grief" --books "Some Book,Another One"
# Ask for more results per book
uv run learnage search "grief" --k 10
Some Book
Jane Author
[1] The sea was the only thing that answered her, and it
never once asked how she was doing…
— Chapter 3
[2] She had been walking since the harbour, keeping the
water on her left like a promise…
— Chapter 7
4 result(s) across 1 book(s)
In the GUI, the same thing is available as Search passages… in the ⌘K palette, and there's a separate Search title or author… command that just filters the book list.
The Background Ingestion Daemon
If you'd rather not babysit an ingest, run the standalone daemon. It watches a folder and ingests anything that appears, as soon as the file's size stops changing (so half-copied files don't get chewed on).
mkdir -p ~/.learnage/inbox
uv run learnage worker --inbox ~/.learnage/inbox --workers 8
Drop any .epub, .pdf, .docx or .txt into that folder. The daemon prints a line per job as it goes:
[running] SomeBook.epub chunks=611 soul=ok [ done] AnotherOne.pdf chunks=288 soul=ok
Why the daemon instead of the GUI?
Because it runs headless. Point it at a folder on a NAS, a synced folder, or a server, and let it chew through books while you do something else. The TUI and GUI have their own in-process worker pools, so you don't need the daemon unless you want ingestion to happen without a front-end open.
Presets
A preset is a YAML file in the presets/ directory bundling the chat model, the embedding model, the soul model, chunk sizes, retrieval k and the soul input budget. Three ship with Learnage:
| Preset | Models | Chunk | k | Feels like |
|---|---|---|---|---|
| fast | qwen3:8b | 600 / 120 | 6 | Snappy on a laptop. Small model, smaller chunks. |
| balanced (default) | qwen3:14b | 800 / 150 | 8 | Quality + speed on a Mac. |
| literary | gemma4:31b | 1000 / 200 | 10 | Best prose awareness, slower. Fiction & craft. |
uv run learnage --preset fast read "Some Book"
uv run learnage --preset balanced read "Some Book"
uv run learnage --preset literary read "Some Book"
Overriding a preset for one run
# Different chat model
uv run learnage --llm qwen3:14b read "Some Book"
# Different embedding model
uv run learnage --embed nomic-embed-text read "Some Book"
# Different retrieval depth
uv run learnage --k 12 read "Some Book"
One small gotcha
The --preset flag is currently restricted to fast, balanced and literary. The preset loader itself will happily read any presets/<name>.yaml, so if you add your own file you can still select it by setting LEARNAGE_PRESET to its name — the loader reads that variable for the default preset.
Where Your Library Lives
Everything lives under one home directory, ~/.learnage/ by default. The layout is created for you the first time you run any command.
~/.learnage/
├── library.db # SQLite — books, chunk metadata, sessions, messages
├── vectors/ # ChromaDB persistent directory
├── sessions/ # session state
├── notes/ # your per-book notes
├── covers/ # extracted cover thumbnails (PNG cache)
├── minds.json # your custom minds
└── blocklist.txt # ignored titles
Moving or pointing it elsewhere
# Per run
uv run learnage --home /path/to/library read "Some Book"
# For a whole shell session
export LEARNAGE_HOME=/path/to/library
Backups
The GUI's Back up library now command (also in the ⌘K palette) takes a consistent snapshot of library.db using SQLite's online backup API, copies the vector store across, and writes a MANIFEST.txt. Snapshots land in:
~/Documents/learnage-backups/YYYY-MM-DD/HH-MM-SS/
├── library.db
├── vectors/
└── MANIFEST.txt
Old backups are pruned so only the most recent twenty are kept.
Exporting
Export book as Markdown… saves a book's chunks plus its soul as a .md file. Conversations can be exported to Markdown too, with each turn's sources listed underneath.
Forgetting a book
uv run learnage forget "Some Book"
That drops the book's vectors and deletes its record — the file on disk is untouched.
Troubleshooting
"Ollama unreachable at http://localhost:11434"
Ollama isn't running, or it's listening somewhere else. Start it with ollama serve, or point Learnage at it with LEARNAGE_OLLAMA_URL=http://host:11434.
doctor shows a red cross on llm or soul
The model isn't pulled. ollama pull qwen3:14b (and ollama pull nomic-embed-text for embeddings). Note that doctor matches on the model name or its family — qwen3 counts as a match for qwen3:14b.
Ingestion is slow
Three things cost time: embedding every chunk, the soul analysis pass, and a large chat model. Speed it up with --preset fast, or skip the soul entirely with --no-soul (you can regenerate it later).
The GUI won't open
- You're headless — PySide6 needs a real desktop session. Use the CLI or TUI instead.
- Missing Qt platform plugins — reinstall the environment with
uv sync --reinstall-package pyside6. - On macOS, the first launch may hit Gatekeeper if you built from a downloaded archive rather than cloning.
The TUI looks like a mess
Terminal font or colour support is limited. Textual needs a terminal with true colour; try a modern terminal emulator and avoid 8-bit colour modes.
An answer has no citations
Check that the book actually has a soul and chunks — learnage show "Title" and learnage library will show you. A book ingested with --no-soul has chunks but no character, so answers will be thinner.
I'm not sure which preset to use
Start with fast to prove the pipeline works, then switch to balanced for real reading, and try literary for fiction and craft books.
Start completely fresh
# Wipes the library and vectors (NOT your book files)
rm -rf ~/.learnage/
# Recreate on next run
uv run learnage doctor
Credits & Links
Learnage is open source, MIT licensed, by Anne Marie Lee (Sol AI).
- Source: github.com/TheSolai/learnage
- Issues: github.com/TheSolai/learnage/issues
- License: MIT
- Version: 2.0.0
Built with
- Ollama — local chat and embedding inference
- Textual — the terminal UI
- PySide6 — the native desktop GUI
- ChromaDB — persistent vector store
- ebooklib + BeautifulSoup4 — EPUB parsing
- pypdf — PDF parsing
- python-docx — DOCX parsing
- Rich — terminal output, tables and progress bars
Models
nomic-embed-text— embeddings (768-dim)qwen3:8b/qwen3:14b— chat and soul analysisgemma4:31b— theliterarypreset
Go read something
Learnage exists for the moment a book you've been studying finally starts talking back. Drop in a file, give it ten minutes, and then ask it the question you've been too shy to ask the author.
💬 Join the Discussion
Share your thoughts. Requires a free GitHub account.