[!TIP] Audience: people who have built local processing pipelines and want to see what “adding an interaction layer” really costs. Core goal: why intensive reading needs its own reader, and how forensics, contracts, and gates keep a three-layer structure from falling apart. Problem-to-solution map:

  • Opening a big book freezes → measure first (470ms/419ms), then cache by pack directory
  • Keeping three layers from drifting → one JSON Schema in contracts/, three conformance tests turn red together
  • Mixing estimates and measurements in docs → tag every claim’s confidence, fill estimates with spikes before coding

This is the third local pipeline I’ve built, and it differs from the first two in a fundamental way. Subtitles and comics are done once translated: a player loads the subtitles, an image viewer opens the comic. Intensive reading has no ready-made carrier — once you’ve translated and explained a book, you still need somewhere to read it, listen to it, look up words, and collect vocabulary. So it needs a whole reader and state layer the other two don’t have. That’s the most interesting comparison across these three projects.

1. What It Does

Drop in an EPUB / PDF / TXT, and it automatically runs tokenization → translation → level-appropriate explanation → high-quality speech → word-level timeline, producing a readable “edition”. New vocabulary syncs through a Worker back to the existing extension and phone app for review. Everything is local inference, no API key required.

The reader is “three modes × two rhythms”: display modes are answer-then-check / silent reading / translation bench, rhythms are continuous / sentence-by-sentence shadowing, with word-level karaoke highlight. In answer-then-check mode, you say a sentence aloud first, then check it; sentences already checked collapse into a thin line, remembered across sessions. Vocabulary has one discipline: the AI may complete a dictionary entry, but only your explicit “add” puts a word into the vocabulary list — the machine doesn’t make review decisions for you.

2. Three Layers, and One Contract

The architecture has three parts: a Python sidecar (prep — offline batch processing, run once per book), a Rust/Tauri shell (window / SQLite / child-process governance / sync), and a pure static frontend (stripped from the original extension). The single source of truth for cross-layer fields is the JSON Schema in contracts/ — when you change a field, all three layers’ conformance tests turn red at once. That’s what keeps three layers from drifting apart: not discipline, but “each layer validates against the same schema, drift is exposed on the spot”.

3. Two-Layer Model: Original Book vs Edition

This refactor was driven by real user feedback. The original book is only a source — not directly readable — and a profile / language / LLM / TTS are chosen when you create an edition. Re-importing the same file registers one entry, second time is skipped; re-running with identical parameters overwrites the edition and keeps a stable id, different parameters create multiple editions; deleting the original cascades to all its editions. Every step is locked by Rust tests.

4. Forensics on the Frozen-Big-Book Bug

Users reported three things: imports seeming duplicated / fuzzy “start reading prep” boundary / opening a big book freezes. The approach was to stop guessing and gather evidence first.

Measured on a real book pack of 23.88MB (8007 sentences, 46 chapters): load_bookpack / load_bookpack_chapter read the whole file and parse all JSON on every request — a single open ≈470ms, each chapter switch ≈419ms. Switching through 46 chapters is 46 full parses; page-turning and cross-chapter search jumps stutter badly or look frozen.

The fix was caching parse results by pack directory, invalidated when an edition or original is deleted. A bonus lesson: a failed load must show a readable error + retry + return to the library — a backend failure must never land on a blank page. A sibling issue: the resident dictionary service loads the model once and reuses it, dropping word lookups from 5–8s to ~1s, freeing VRAM when idle.

And failures aren’t only a loading concern: the library card shows “N sentences failed” directly, and the prep station can re-run only the failed sentences, keeping completed results — a user never reaches a sentence and discovers it’s empty.

5. Confidence Levels in Docs

This section is a method for fellow practitioners. Every claim in the design doc carries a confidence tag: ✅ measured fact / 📐 estimate (not verified on this machine) / ❓ unverified assumption / 🎯 confirmed design decision. Mixing the four kinds gets you into trouble — “a book finishes in 40–60 minutes” was an estimate back then, not a measurement, the product of three numbers nobody had measured, so the error could be large. So before writing code, run spikes to replace estimate cells with measured numbers.

The same lesson keeps appearing in all three reference projects: reading the code ≠ verifying runtime behavior. Real-exe cold-start acceptance caught 2 frontend bugs that only real runs expose — one was a new module never registered in the entry page, one was a navigation root node accidentally deleted during refactoring. All tests green ≠ the exe works.

Summary

The gate is one command running 11 checks: 150 Rust tests + 85 frontend tests + DOM smoke + CSS lint + contrast checks. The theme’s 5 color schemes and custom accent colors all pass WCAG AA contrast gate validation, not eyeballing. A self-deprecating note: the project shipped without version control at first — the repo was created after 3 books were delivered, and the first tag is literally v0.1.0-working-3books. That tag name is an honest footnote to the whole story.