Story Cartographer
Upload a book, and a language model you choose reads it page by page into an interactive character graph, an incident timeline, and a dossier for every character. This manual covers the gated live demo, then the architecture and API underneath it.
Demo access & PIN
The live demo is the real application, wrapped in a PIN gate so it can be shared safely. Open it, enter the PIN, and you are in for 12 hours on that browser.
DEMO_PIN environment variable.Library & upload
The Dashboard is your reading room — library stats, what you're currently reading, and recent activity. Library lists every book with its cover and status.
Add a book
- Go to Library → Add a book (or the dashboard button).
- Choose a
.pdf,.epub, or.txtfile (text-based; scanned PDFs are OCR'd when the server hastesseract+poppler). - The file is validated and its page count and title are read; you land on the book's page.
Analyzing a book
On a book's page, click Analyze book. The app extracts text per page and reads the book in small overlapping windows (default 6 pages, 1 overlap). Each window goes to your model with the running cast so recurring characters resolve to one node. The map persists after every window and a live progress bar streams over SSE.
Set Visualization to Full to analyze everything now, As I read to catch up to your current reading page, or Off.
Graph · timeline · dossiers
A book page has tabs across the top:
| Tab | What it shows |
|---|---|
| Character graph | Force-directed graph. Nodes sized by mentions, coloured by role; edges typed and weighted. Drag a node, scroll to zoom, click a character to focus its connections and open a detail panel where you can add private notes. Toggle Relationships / Incidents / Both, and Fit / Reheat the layout. |
| Timeline | Every incident in reading order, anchored to its page, with participants. |
| Sentiment | The book's emotional-tone arc with chapter markers. |
| Dossiers | A card per character: role, aliases, description, traits, an importance bar, mention count, and page range. |
| Journal / Data | Your reading journal, and the raw structured JSON. |
The reveal-reader
Click Read book to open the book itself. As you turn pages, the map reveals spoiler-safe — only characters and incidents up to your current page appear, and newly revealed names pulse. Click a highlighted name in the prose to focus that character on the map. You can highlight passages and keep annotations as you go.
Crossovers & export
Crossovers lists characters that appear by name across two or more analyzed books on your shelf — useful for series and shared universes.
Export the full map as JSON from a book page (Export JSON), or the graph itself as SVG or a 2× PNG from the graph toolbar.
Provider settings
Under Settings, choose a wire format and point the app at any compatible endpoint:
- OpenAI-compatible (
/chat/completions) — OpenAI, Azure, Groq, Together, OpenRouter, vLLM, LM Studio, Ollama… - Anthropic-compatible (
/messages).
Set the base URL, API key, and model, then Test connection for a cheap round-trip. Tune pages/window, overlap, and a max windows cap to bound cost. The key is stored server-side, masked in the UI, and sent only to the endpoint you configure.
A day in the life
Priya hosts a book club. Tonight is The Great Gatsby and she has an hour.
- She opens the demo, enters the PIN, and lands on the Dashboard — three books already mapped.
- She opens The Great Gatsby. The character graph shows Nick at the centre, Gatsby and Daisy a heavy edge apart, Tom pulling away — she can see the love triangle.
- She clicks Daisy; the panel lists every relationship and she jots a discussion note: "does she ever really choose?"
- The Timeline tab gives her the evening's beats in order, each anchored to a page so she can quote directly.
- She switches to Harry Potter to show the group what a 68-character graph looks like, then opens Crossovers — nothing shared tonight, but the group gets the idea for their series read.
- She hits Export JSON to keep the map, and closes the laptop. Total setup: minutes, no re-reading.
Technical architecture
A single FastAPI + HTMX application. No database, no build step, no client framework — server-rendered Jinja templates with HTMX for interactivity and a self-contained SVG graph renderer.
The reading pipeline
- ingest — normalize PDF/EPUB/TXT to a list of page texts (optional OCR when system binaries are present).
- analyzer — split pages into overlapping windows; run a background thread; emit SSE progress.
- llm — provider-agnostic client for OpenAI- and Anthropic-shaped endpoints, with tolerant JSON extraction (direct → fenced → brace-match).
- store — merge each window into one map (alias / partial-name resolution, dedup), persist a partial result every window as JSON on disk.
Data model — one JSON blob per book
characters[]: {name, aliases[], role, description, traits[],
mentions, first_page, last_page, importance}
relationships[]: {source, target, type, description, weight} # endpoints = names
incidents[]: {title, summary, page, characters[]}
stats: {characters, relationships, incidents, windows}
Resilience
- One bad window never aborts a run — it's logged as a warning and the run continues.
- Malformed model output is tolerated by the JSON extractor.
- Provider / network errors are surfaced to the user, not swallowed.
- Demo lane: a PIN gate (signed-cookie middleware) wraps every route; enabled with
DEMO_GATE=1. The app is otherwise unchanged.
API reference
All routes below require the PIN cookie (except /healthz,
/gate, and /static). Book pages return HTML; the
JSON and event routes are what a client would script against.
| Method | Path | Purpose |
|---|---|---|
| GET | / | Dashboard. |
| GET | /library | Library listing. |
| POST | /upload | Upload a book file (multipart); redirects to its page. |
| GET | /books/{id} | Book page (graph / timeline / dossiers). |
| POST | /books/{id}/analyze | Start a fresh full analysis. |
| POST | /books/{id}/analyze/stop | Stop after the current window (keeps partial results). |
| GET | /books/{id}/events | SSE stream of analysis progress. |
| GET | /books/{id}/graph.json | The full character map as JSON. |
| GET | /books/{id}/export.json | Download the map (attachment). |
| POST | /books/{id}/ask | Ask a spoiler-scoped question about the book. |
| GET | /books/{id}/read | The reveal-reader. |
| GET | /crossovers | Characters shared across books. |
| POST | /settings | Save provider configuration. |
| POST | /settings/test | Test the provider connection. |
| GET | /healthz | Liveness probe (open, no PIN). |
| GET | /gate · POST /gate | PIN unlock screen and submission. |
Example — fetch a map
# unlock, keeping the cookie curl -c jar -d "pin=422471&next=/" http://<host>:8000/gate # then read the Harry Potter map curl -b jar http://<host>:8000/books/69f3ff276ed1/graph.json
Troubleshooting
| Symptom | Fix |
|---|---|
| Unlock screen keeps returning | Cookies are blocked, or the PIN is wrong. Allow cookies for the demo host and re-enter 422471. |
| "Configure a provider in Settings first" | Fresh analysis and "Ask the book" need a configured LLM endpoint. The pre-analyzed demo books are still fully explorable without one. |
| A browser extension can't reach the demo | Some extensions can't reach loopback/LAN addresses — open the demo host directly in a normal tab. |
| Model returns empty answers | Switch to a fast, non-reasoning model; reasoning models can exhaust the token budget on hidden reasoning. |