Documentation

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.

New here? Watch the ~60-second narrated tour on the overview page, then open the live demo and follow A day in the life below.

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.

On first open your browser shows a one-time certificate warning — the demo uses a self-signed certificate on a non-standard port. Click Advanced → Proceed to continue; the connection is still encrypted.
Access PIN
422471
Enter this at the unlock screen. It sets a signed, http-only cookie — no account, no email. The operator can change it with the DEMO_PIN environment variable.
All demo data is synthetic. The three books are pre-analyzed fixtures included so the demo is walkable the moment you sign in. Nothing you see is real customer or reader data.

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

  1. Go to Library → Add a book (or the dashboard button).
  2. Choose a .pdf, .epub, or .txt file (text-based; scanned PDFs are OCR'd when the server has tesseract + poppler).
  3. The file is validated and its page count and title are read; you land on the book's page.
In the demo, three books are already added: The Great Gatsby, Harry Potter, Book 1 (68 characters — the densest graph), and The Little Prince.

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.

The demo books are already fully analyzed, so you can explore immediately. Running a fresh analysis requires a configured provider (see Provider settings).

Graph · timeline · dossiers

A book page has tabs across the top:

TabWhat it shows
Character graphForce-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.
TimelineEvery incident in reading order, anchored to its page, with participants.
SentimentThe book's emotional-tone arc with chapter markers.
DossiersA card per character: role, aliases, description, traits, an importance bar, mention count, and page range.
Journal / DataYour 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.

Prefer a fast, non-reasoning model for demos — reasoning models can spend their whole token budget on hidden reasoning and return terse answers.

A day in the life

Priya hosts a book club. Tonight is The Great Gatsby and she has an hour.

  1. She opens the demo, enters the PIN, and lands on the Dashboard — three books already mapped.
  2. 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.
  3. She clicks Daisy; the panel lists every relationship and she jots a discussion note: "does she ever really choose?"
  4. The Timeline tab gives her the evening's beats in order, each anchored to a page so she can quote directly.
  5. 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.
  6. 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

  1. ingest — normalize PDF/EPUB/TXT to a list of page texts (optional OCR when system binaries are present).
  2. analyzer — split pages into overlapping windows; run a background thread; emit SSE progress.
  3. llm — provider-agnostic client for OpenAI- and Anthropic-shaped endpoints, with tolerant JSON extraction (direct → fenced → brace-match).
  4. 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.

MethodPathPurpose
GET/Dashboard.
GET/libraryLibrary listing.
POST/uploadUpload a book file (multipart); redirects to its page.
GET/books/{id}Book page (graph / timeline / dossiers).
POST/books/{id}/analyzeStart a fresh full analysis.
POST/books/{id}/analyze/stopStop after the current window (keeps partial results).
GET/books/{id}/eventsSSE stream of analysis progress.
GET/books/{id}/graph.jsonThe full character map as JSON.
GET/books/{id}/export.jsonDownload the map (attachment).
POST/books/{id}/askAsk a spoiler-scoped question about the book.
GET/books/{id}/readThe reveal-reader.
GET/crossoversCharacters shared across books.
POST/settingsSave provider configuration.
POST/settings/testTest the provider connection.
GET/healthzLiveness probe (open, no PIN).
GET/gate · POST /gatePIN 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

SymptomFix
Unlock screen keeps returningCookies 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 demoSome extensions can't reach loopback/LAN addresses — open the demo host directly in a normal tab.
Model returns empty answersSwitch to a fast, non-reasoning model; reasoning models can exhaust the token budget on hidden reasoning.