A self-contained 3D e-book reader — a web application that simulates a physical book in three-dimensional space. No build step, no Node.js, no npm — just a browser and Python.
🐳 Docker:
hiddenath/bookhaven— one command to run🌐 Live demo: 129.151.144.75:8080
⚠️ The media below is large — click to expand (high traffic, ~9 MB total).
- 📕 3D book spread with page-flip animation (CSS 3D)
- 📱 Single-page mode — for tall/portrait screens only the right page is shown automatically
- 🖱️ Interactive page turning — grab the page corner with the mouse or your finger
- 👆 Touch controls — swipe left/right and wide tap zones on the screen edges
- 🔊 Volume keys — page turning inside an Android WebView wrapper (in a regular browser the OS intercepts them)
- ⌨️ Navigation: arrows, spacebar, Home/End, click zones on the sides
- 📄 Dynamic layout — text is split into pages according to screen size, font and margins
- 📍 Position saving — reopening a book returns you to where you left off (anchored to text, not page numbers)
- 🗣️ Text-to-speech — the narrator reads the book aloud using the built-in browser voices (Web Speech API, works offline)
- 📖 Reads both pages of the spread, then flips the page with animation and sound — just like manual reading
- 🎚️ Stress marks — a 3.2M-word Russian stress dictionary (RUAccent) marks the stressed vowel of every word, so the voice puts accents correctly («за́мок», not «замо́к»)
- 🎭 Human-like pacing — separate pauses after sentences, paragraphs, chapter titles and poem stanzas; headings are read slower, like an announcer
- 🧹 Clean speech — quotes and markup symbols are not read aloud, dashes become natural pauses
- ⏯️ One button — the FAB button starts/stops narration when a book is open (in the library it still adds books)
- 📄 Auto-flip toggle — optionally stop at the end of each page instead of turning automatically
- 🔊 Paper rustle on every page flip — a random sound from the
sounds/folder (no repeats in a row) - ➕ Drop-in sounds — add more MP3/OGG/WAV files into
sounds/, they are picked up automatically, no code changes - 🔈 Toggle in settings — sound can be turned off alongside the animation toggle
- 📚 Card grid with covers, title, author and reading progress
- ➕ Adding books: button, Drag & Drop
- 📂 Formats: TXT, EPUB, FB2
- 🗑️ Deleting books with confirmation
- 💾 Server-side storage — books live in the
books/folder, metadata inmeta.json
- 🔖 Add/remove bookmarks with text preview
- 📑 Side panel with the list, click to jump to the location
- 🎯 Marker on the book edge — visual bookmark indicator
- 📃 Chapter navigation — jump to any chapter from the TOC panel
- 🔤 Font size, line height, horizontal & vertical margins
- 🎨 7 themes: Paper, Sepia, Night, Forest, Sky, Ember (OLED), Amber (OLED)
- 🌙 Immersive mode — clicking the center hides all panels
- 🔤 Hyphenation toggle
- 🎞️ Page-flip animation and flip sound toggles
- 📄 Auto-flip during narration toggle
- Tap the screen edges — right/left edges flip forward/back (zones widen automatically on touch screens)
- Swipe — swipe left to go forward, right to go back
- Volume keys — in a regular browser the OS intercepts them, so flipping works inside an Android WebView wrapper. The wrapper can control the reader in three ways (implementation:
js/reader.js,_bindVolumeKeys):
| Way | How to do it in the wrapper |
|---|---|
| JS bridge | Callwindow.BookHavenNative.volumeUp() / .volumeDown() (e.g. via evaluateJavascript in onKeyDown) |
| Event | Dispatchwindow.dispatchEvent(new Event('volumeup')) / ('volumedown') |
| Key | Forward akeydown with keyCode 175 (louder) / 174 (quieter) to the WebView |
# Start the server (static + API in one file, one port)
python3 server.py
# Open in browser
http://127.0.0.1:8080One port serves everything: static files (HTML/CSS/JS) and the API (state, books, sounds, stress marks) — the frontend talks to the same origin, no cross-origin requests.
Options:
python3 server.py # 8080; if busy — 8081…8099 (auto)
python3 server.py --port 9000 # exact port (busy = error)
python3 server.py --port 0 # any free port, picked by the OS
python3 server.py --host 127.0.0.1 # localhost only (for desktop wrappers)
python3 server.py --quiet # no request logging🐳 Docker / any external port: the image pins 8080 inside the container (
Dockerfile), but you can expose it on any host port —docker run -p 9000:8080 ...just works: the frontend uses relative paths and doesn't care which port it's served from.
book_reader/
├── index.html # Entry point, UI markup
├── book.svg # Logo (library header + favicon)
├── server.py # Single server: static + API
├── css/
│ ├── main.css # Global styles, themes (CSS Custom Properties)
│ ├── reader.css # 3D reader scene, bookmarks
│ └── library.css # Library (card grid)
├── js/
│ ├── main.js # Initialization, pagination, state, autosave
│ ├── reader.js # Reader: flip animation, drag, keyboard, volume keys
│ ├── narrator.js # Narrator: speech engines, pacing, stress marks
│ ├── sounds.js # Page-flip sounds (random pick from sounds/)
│ ├── library.js # Library: cards, import, deletion
│ ├── bookmarks.js # Bookmarks (side panel, marker on the book edge)
│ ├── notes.js # Footnotes: tooltip, jump to note, "back" button
│ ├── parsers.js # TXT/EPUB parsers (FB2 — server-side)
│ ├── position.js # Reading anchors (data-block-id)
│ ├── toc.js # Table of contents
│ └── storage.js # localStorage + server-side saving
├── lib/
│ ├── epub.js # epub.js (downloaded, BSD)
│ ├── jszip.min.js # JSZip — epub.js dependency
│ └── stress_dict.json.gz # Russian stress dictionary (3.2M word forms, RUAccent)
├── sounds/ # Page-flip sounds (MP3/OGG/WAV, picked up automatically)
├── assets/ # Screenshots and GIF animations for the README
├── books/ # Book catalog (created automatically)
│ ├── <name>.fb2 / .txt / .epub # Book file with its original name
│ └── <name>.meta.json # Metadata next to the book file
└── data/
└── state.json # Global settings (created automatically)
Text is split into pages in the browser: a hidden measurer computes the height of each paragraph under the current font settings, then paragraphs are laid out into pages. Each paragraph gets a stable identifier (data-block-id) tied to the book and its sequential number — so the reading position does not "drift" when settings change.
Books are stored as plain files in books/ — the original filename is kept, no subfolders. Metadata (progress, bookmarks, covers) is stored in <name>.meta.json next to the book.
| What | Where |
|---|---|
| Books (originals) | books/<name>.fb2 / .txt / .epub |
| Book metadata (progress, bookmarks) | books/<name>.meta.json |
| Global settings | localStorage + data/state.json |
| Method | Path | Description |
|---|---|---|
GET |
/state |
Global state (settings, library snapshot) |
POST |
/state |
Save global state |
GET |
/books |
List of books (metadata without text) |
POST |
/books |
Save a book (copies intobooks/) |
GET |
/books/<id>/text |
Book text (FB2 parsed on the fly) |
GET |
/books/<id>/meta |
Book metadata |
POST |
/books/<id>/meta |
Update progress/bookmarks |
GET |
/books/<id>/cover |
Book cover (from FB2<coverpage>) |
GET |
/books/<id>/image/<name> |
Image from the book body (FB2<binary>) |
DELETE |
/books/<id> |
Delete a book |
GET |
/sounds |
List of page-flip sound files |
POST |
/tts/stress |
Mark up text with stress accents (U+0301) for the narrator |
- Frontend: vanilla ES2022 JavaScript, CSS3 Custom Properties, CSS 3D transforms
- Backend: Python
http.server(no dependencies) - Libraries: epub.js + JSZip (bundled in
/lib/) - TTS: Web Speech API (browser voices) + RUAccent stress dictionary (3.2M word forms)
- Icons: inline SVG (Material Design paths)
Dual licensing — see LICENSE.md:
- 🆓 Personal use — free: read, install on your devices, modify for yourself, share with others for personal use.
- 💼 Commercial use — paid license: selling, embedding into commercial products/services, monetized hosting, using the code in commercial projects — requires purchasing a license from the author.
Applies to the repository version dated September 1, 2026 and later.
Third-party libraries: epub.js (BSD-3-Clause), JSZip (MIT/GPLv3).
📚 About the books in the screenshots: the demo uses free preview versions of books from LitRes (a Russian e-book store) and a public-domain art book. They are shown for demonstration purposes only and are not distributed with the project.















