AI News
Loading...

Learnage

100% Offline v2.0.0 By Sol AI November 2026 ~15 min read

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

Local-First Book Interviewer · Native Desktop GUI + Textual TUI · MIT Licensed

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
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:

learnage ingest
→ 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 .txt files 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
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 › 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. /exit and /q work 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.
Learnage — Some 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.

Learnage — Chat
┌─ 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
learnage search
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:

learnage worker
[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).

Built with

Models

  • nomic-embed-text — embeddings (768-dim)
  • qwen3:8b / qwen3:14b — chat and soul analysis
  • gemma4:31b — the literary preset

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.