Refactor the existing read_chapter.py application into an installable Python CLI while preserving its current core outcome: a Markdown or text chapter produces a sibling .wav narration and interactive .html reader.
The refactor is not a feature rewrite. Existing text cleaning, device selection, Kokoro generation, timestamp distribution, reader controls, and local-file output behavior must be retained unless this plan explicitly changes them.
The existing piper_env/, kokoro_env/, ignored Piper ONNX model, and untracked matching Piper JSON are user-local artifacts, not application inputs. This Kokoro refactor must not delete, package, alter, or implicitly adopt them. Decide whether the Piper JSON is intentionally versioned or ignored only in a separate, explicitly scoped Piper task.
The installed commands will be:
ireader
interactive-reader
python -m interactive_reader
read_chapter.py will remain as a thin compatibility launcher for one release. It delegates to the new CLI and retains --autoplay as a documented deprecated alias for --browser; it must not contain a second implementation of the pipeline.
interactive-reader/
|-- src/
| `-- interactive_reader/
| |-- __init__.py # public version only
| |-- __main__.py # calls cli.main()
| |-- cli.py # argparse, exit status, launch policy
| |-- generator.py # end-to-end generation coordination
| |-- cleaner.py # Markdown/text normalization
| |-- device.py # XPU, MPS, CUDA/ROCm, CPU selection
| |-- tts.py # Kokoro generation and timestamp helpers
| |-- html_builder.py # safe HTML and reader-document generation
| |-- window.py # lazy pywebview integration
| `-- templates/
| `-- reader.html # packaged reader template
|-- tests/
|-- read_chapter.py # temporary compatibility launcher
|-- pyproject.toml
|-- requirements.txt
|-- README.md
`-- implementation_plan.md
pyproject.toml is the dependency and package-metadata source of truth. It must declare a Python version floor, setuptools src package discovery, both console scripts, and package data for templates/reader.html. The exact script mappings are:
[project.scripts]
ireader = "interactive_reader.cli:main"
interactive-reader = "interactive_reader.cli:main"Template loading must use importlib.resources, not a repository-relative path, so wheels and editable installs behave identically.
The default installation, pip install ., must be capable of CPU narration. Therefore pyproject.toml declares the CPU-compatible PyTorch dependency required by the supported Kokoro release, along with Kokoro, Misaki, NumPy, and SoundFile. The implementer must validate the resolved CPU package set in a clean virtual environment; do not assume Kokoro transitively installs a compatible Torch build.
Hardware-specific accelerator packages are deliberately not resolved by the generic package install because their indexes and platform support differ. The README must provide tested, version-compatible commands for Intel XPU, NVIDIA CUDA, AMD ROCm, and Apple Silicon/MPS, and say that users install the appropriate Torch variant before installing the project or reinstall the matching Torch build afterwards. The code retains its existing runtime detection and CPU fallback.
Desktop support is a named optional extra, pip install ".[window]", containing pywebview. requirements.txt must be generated from the project dependency definition or replaced by a short compatibility file that installs the project and its documented extras; it may not carry a conflicting list of runtime packages. pyproject.toml remains authoritative.
# Generate files only; print their paths and exit 0.
ireader chapter.md
# Generate and launch the local reader in a native desktop window.
ireader chapter.md --window
# Generate and open the local reader in the default browser.
ireader chapter.md --browser
# Select a Kokoro voice and the initial reader playback rate.
ireader chapter.md --voice am_fenrir --speed 1.25input_fileis a required existing regular file.--voice/-vdefaults toam_fenrir.--speed/-sis the reader's initial HTMLaudio.playbackRate, defaults to1.0, and is validated as a positive finite value. It does not change Kokoro synthesis speed in this refactor; doing so would be a separate audio-generation feature.- The generated HTML receives the initial speed as an explicit template value. On load it sets
audio.playbackRateand marks the matching preset button active; if no preset matches, no preset is shown as selected. --window/-wand--browser/-bare an argparse mutually-exclusive group.- No viewing flag means generate-only, but it is not silent: print the WAV and HTML paths just as the current CLI does.
- Invalid arguments, missing input, empty cleaned text, TTS failure, output-transaction failure, and unavailable requested viewer return non-zero with a concise actionable error.
Output paths are resolved before synthesis. The command must reject any invocation in which a final WAV or HTML path equals the input path (notably an .html source), or the two final output paths resolve to the same file. It must never overwrite the source chapter as a side effect of generation.
Browser launching uses the generated HTML file URI and reports failure if the operating system refuses the launch. Use Python APIs that pass paths directly to the operating system; do not construct shell command strings for browser or audio fallback launches. The old raw-audio fallback is removed because the CLI always generates an HTML reader or reports generation failure.
Window launching imports pywebview only after --window is requested. If the optional extra is absent, report the install command and exit non-zero without attempting a browser fallback. If the operating-system webview backend is unavailable, surface its diagnostic and exit non-zero. Document the supported backends: WebView2 on Windows, WebKit on macOS, and a supported pywebview GUI backend on Linux.
The first version uses a standard native-framed, resizable window with a documented initial size and normal close-to-exit lifecycle. Do not claim a frameless UI until custom close/minimize/drag affordances and accessibility behavior are deliberately designed and tested. Closing the window ends the foreground CLI process cleanly.
Preserve the current paragraph presentation and word-level interaction, but make the word-to-timestamp mapping an explicit correctness boundary and treat chapter content and filenames as untrusted input:
- HTML-escape visible chapter text, title, and attribute values before template insertion.
- Serialize timestamp JSON safely for an inline
<script>context (escape<,>, and&, or otherwise prevent a closing-script sequence). - Derive rendered word tokens and timestamp tokens from one canonical normalized tokenization. Before the output pair is staged, validate a one-to-one ordered mapping: every rendered word has exactly one timestamp and no timestamp is unused. If Kokoro returns text that cannot be reconciled with the canonical tokens, fail with a diagnostic; never emit silently unhighlighted or misaligned text.
- Keep the generated reader fully offline except for its adjacent WAV file: remove remote font/style requests from the template and use system font stacks or embed appropriately licensed assets. The reader must make no network requests and must not require a local web server.
- Preserve the template as a package resource and replace only explicit template markers.
WAV and HTML are one logical output pair. Write and fsync both temporary sibling files before changing either final path. If final outputs already exist, retain recoverable same-directory backups. Persist a same-directory transaction marker that records the staged and backup paths before replacements begin. Replace the two final paths only after both staged files are valid; if either replacement fails, restore any replaced final path from its backup and remove staged files. On startup, recover a marker left by an interrupted prior run by restoring the old pair or completing cleanup, then report what was recovered. Remove the marker and backups only after the pair commits successfully. Report a transaction failure rather than leaving a new WAV with an old HTML or timestamp data.
The existing overwrite policy remains in effect: successful generation replaces existing sibling outputs. A future --output-dir, --force, or no-overwrite feature is outside this refactor.
- Add packaging metadata, resource configuration, exact console-script mappings, dependency/extra definitions, and the temporary legacy launcher before moving implementation logic.
- Move pure functions into
cleaner.py,device.py,tts.py, andhtml_builder.py; define and test the canonical rendered-word/timestamp token mapping rather than retaining silent sequential fallback behavior. - Implement
generator.pyas the only coordinator for reading input, generating audio, building HTML, and committing the output pair transaction. - Implement
cli.pyandwindow.pyaround the CLI and viewer contracts above. Keep GUI imports lazy. - Update the HTML template to apply the requested initial playback rate, synchronize the selected speed control, and retain existing controls and word highlighting.
- Update README installation, accelerator policy, migration, offline-reader guarantee, platform-backend, optional-window, and legacy-
--autoplayguidance. Repair all documentation and template text as UTF-8, replacing existing mojibake/control-character artifacts while preserving the intended symbols and keyboard hints.
Add pytest as a development/test dependency and run the following before merge:
- Unit tests for Markdown cleaning, device fallback seams, timestamp distribution, canonical tokenization, and exact one-to-one ordered word/span/timestamp alignment. Include a deliberately mismatched pipeline result and assert failure before staging outputs.
- HTML-builder tests proving special characters in chapter text, title, filename, and timestamp words cannot create tags, attributes, or a script breakout; also verify the initial-speed value, selected-state behavior, absence of remote asset URLs, valid UTF-8 template/document output, and no unintended control characters.
- CLI tests for help, input validation, source/output collision rejection,
--window/--browserexclusivity,--speedvalidation and propagation, deprecated--autoplay, success-path output reporting, and viewer error messages. Mock Kokoro and viewer processes; tests must not download models or open a GUI. - Generation tests using a fake pipeline that verify WAV and HTML creation, correct relative WAV reference, packaged-template loading, and the paired-output transaction. Simulate failure before commit and while replacing the second final file; confirm prior WAV and HTML are both restored. Simulate an interrupted transaction marker and assert deterministic recovery with no temporary, backup, or marker artifacts left behind.
- Build a wheel and test it in clean virtual environments: install core and verify CPU dependencies plus both console commands and
python -m interactive_reader --help; separately install the window extra and verify packaged resources and lazy GUI import behavior. - Manually smoke-test the browser and native-window paths on Windows, including audio play/pause, click-to-seek, highlighting, initial speed and its visible selected state, and close-to-exit behavior. Record Linux/macOS backend testing separately when those platforms are available.
Acceptance requires all automated tests to pass, clean installation from the built wheel, and a successful Windows manual smoke test. A successful import alone does not prove that the packaged template, launch behavior, output-pair integrity, or reader controls work.