readalign 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .eggs/
5
+ build/
6
+ dist/
7
+ .venv/
8
+ venv/
9
+ .pytest_cache/
10
+ .ruff_cache/
11
+ .coverage
12
+ htmlcov/
13
+ *.epub
14
+ !examples/aesop/*.epub
15
+ readalign-report.json
16
+ *-sidecars/
17
+ .readalign-cache/
18
+ examples/output/
19
+ .DS_Store
20
+ Thumbs.db
@@ -0,0 +1,34 @@
1
+ # Changelog
2
+
3
+ ## 1.0.0 - 2026-09-18
4
+
5
+ First release.
6
+
7
+ - `readalign build` turns a DRM-free audiobook and the matching DRM-free EPUB into one EPUB 3
8
+ with conformant Media Overlays: SMIL per content document, `media-overlay` on the manifest
9
+ items, `media:duration` per overlay plus a total, active-class metadata and a highlight
10
+ stylesheet. The overlay's `seq` elements mirror the document's nesting, so both skippability
11
+ (footnotes, endnotes, page numbers, sidebars, annotations) and escapability (tables, lists,
12
+ figures) work. The archive is written mimetype-first and uncompressed as OCF requires.
13
+ - `readalign check` validates the overlays in any EPUB: fragment ids resolve, audio is
14
+ manifested, no `clipEnd` runs past the duration ffprobe reports, declared durations match the
15
+ clips. `--json` for scripting.
16
+ - Transcription with faster-whisper, CUDA when available and CPU otherwise, word timestamps,
17
+ and a per-file transcript cache keyed by content and settings. A GPU that reports itself
18
+ present but cannot load its CUDA libraries falls back to the CPU with one warning line
19
+ naming what to install, rather than a traceback.
20
+ - Anchored alignment: unique shared n-grams, longest increasing subsequence, diff only in the
21
+ gaps. No global dynamic-programming matrix, and audio is streamed rather than buffered on the
22
+ way both in and out, so a 7.5 hour book builds in about 130 MB of memory and opening the
23
+ 205 MB result costs 33 MB. Two runs of the same build produce byte-identical archives.
24
+ - Sidecars: one WebVTT per chapter and audio file, plus `align.json`. `readalign-report.json`
25
+ records coverage, drift, per-document statistics and every region that did not match.
26
+ - Handles one file spanning several chapters, several files in one chapter, audio chapter counts
27
+ that disagree with the spine, narrator front and back matter with no text counterpart, notes
28
+ and page-break markers, existing ids, non-UTF8 XHTML, and clip ends past the real duration.
29
+ - DRM-protected input (`.aax`, `.aa`, `.acsm`, an encrypted EPUB) exits 2 and is never opened.
30
+ - Wrong input gets one line and an exit code, never a traceback: a missing file, an unzipped
31
+ book passed as a directory, something that is not a zip, an EPUB 2 package, an empty audio
32
+ directory, or a file ffprobe cannot read.
33
+ - Optional `readalign[refine]` extra adds a CTC forced-alignment pass behind `--refine`. The
34
+ MMS-300m aligner it uses is CC BY-NC 4.0, so it is never downloaded by the default path.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Christopher Bosch
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,332 @@
1
+ Metadata-Version: 2.5
2
+ Name: readalign
3
+ Version: 1.0.0
4
+ Summary: Turn an audiobook and the matching ebook you own into one EPUB 3 with Media Overlays.
5
+ Project-URL: Homepage, https://github.com/Booyaka101/readalign
6
+ Project-URL: Repository, https://github.com/Booyaka101/readalign
7
+ Project-URL: Issues, https://github.com/Booyaka101/readalign/issues
8
+ Project-URL: Changelog, https://github.com/Booyaka101/readalign/blob/main/CHANGELOG.md
9
+ Author-email: Christopher Bosch <cbosch101@gmail.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: accessibility,audiobook,epub,epub3,forced-alignment,media-overlays,read-along,smil,whisper
13
+ Classifier: Development Status :: 5 - Production/Stable
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: End Users/Desktop
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
21
+ Classifier: Topic :: Text Processing :: Markup :: XML
22
+ Requires-Python: >=3.11
23
+ Requires-Dist: faster-whisper>=1.1
24
+ Requires-Dist: lxml>=5.0
25
+ Requires-Dist: numpy>=1.24
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=8.0; extra == 'dev'
28
+ Requires-Dist: ruff>=0.16; extra == 'dev'
29
+ Provides-Extra: refine
30
+ Requires-Dist: torch>=2.1; extra == 'refine'
31
+ Requires-Dist: torchaudio>=2.1; extra == 'refine'
32
+ Description-Content-Type: text/markdown
33
+
34
+ # readalign
35
+
36
+ Turn an audiobook you own and the matching ebook you own into a single EPUB 3 with conformant
37
+ Media Overlays: the words highlight as the narrator reads them, in any reading system that
38
+ supports EPUB 3 read-aloud.
39
+
40
+ Everything runs on your machine. There is no account, no API key and no upload. The only network
41
+ access is the one-time download of the speech model.
42
+
43
+ ```
44
+ readalign build --audio audiobook/ --epub book.epub --out read-along.epub
45
+ ```
46
+
47
+ ## What it produces
48
+
49
+ - **an EPUB 3.3 file** containing your ebook, the audio, one SMIL overlay per chapter, the
50
+ `media-overlay` links, `media:duration` metadata and a highlight stylesheet. It passes
51
+ epubcheck 5.2.1 with zero messages.
52
+ - **`readalign-report.json`** next to the output: coverage, drift, per-document statistics, which
53
+ documents got no overlay and which regions never matched.
54
+ - **sidecars** (`<name>-sidecars/`): one WebVTT file per chapter and audio file, plus `align.json`
55
+ with every sentence's id, clip times, confidence and text. A rerun clears the `.vtt` files and
56
+ `align.json` it wrote last time, so nothing stale is left behind. Turn them off with
57
+ `--no-sidecars`.
58
+
59
+ ## Requirements
60
+
61
+ - Python 3.11 or newer
62
+ - **ffmpeg and ffprobe on `PATH`** (readalign shells out to them; it does not vendor a copy).
63
+ Get them from <https://ffmpeg.org/download.html>.
64
+ - An NVIDIA GPU is optional. With one, a 7 hour book transcribes in about 12 minutes. On CPU the
65
+ same book takes hours, so use a smaller `--model` there. If a GPU is present but its CUDA
66
+ libraries will not load, readalign says so and carries on on the CPU.
67
+
68
+ ## Install
69
+
70
+ ```
71
+ pip install readalign
72
+ ```
73
+
74
+ or, without installing anything permanently:
75
+
76
+ ```
77
+ uvx readalign build --audio ... --epub ... --out ...
78
+ ```
79
+
80
+ ## Quick start
81
+
82
+ A public-domain pair is bundled in `examples/aesop`: two minutes of a LibriVox recording and the
83
+ matching Project Gutenberg text. From a checkout:
84
+
85
+ ```
86
+ readalign build \
87
+ --audio examples/aesop/aesop-section-37.mp3 \
88
+ --epub examples/aesop/aesop-two-fables.epub \
89
+ --out aligned.epub --model tiny.en --language en --device cpu
90
+ ```
91
+
92
+ Two minutes of audio is not worth spinning up a GPU for, hence `--device cpu`. Real output
93
+ from that command, on a cold cache:
94
+
95
+ ```
96
+ readalign 1.0.0
97
+ reading examples/aesop/aesop-two-fables.epub
98
+ 2 content documents, 18 sentences
99
+ preparing 1 audio file(s)
100
+ [1/1] aesop-section-37.mp3 -> stream copy
101
+ transcribing
102
+ loading whisper model 'tiny.en' on cpu (int8)
103
+ [1/1] aesop-section-37.mp3: transcribing 1:58
104
+ 298 words so far, 1:58/1:58 audio in 0:03 (35.8x realtime)
105
+ 298 words in 3.3s on cpu
106
+ aligning
107
+ aligning 245 book tokens against 300 spoken tokens
108
+ wrote D:\Repos\ideas\readalign\aligned.epub (0.9 MB, 2 overlays, 0:01:11.640)
109
+ wrote 3 sidecar file(s) to D:\Repos\ideas\readalign\aligned-sidecars
110
+ sentences 18/18 timed (100.0%)
111
+ interpolated 0 low confidence 0
112
+ token match 95.1%
113
+ drift max 0.00s, p95 0.00s, threshold 2.50s
114
+ report D:\Repos\ideas\readalign\readalign-report.json
115
+ ```
116
+
117
+ Run it again and the transcribing lines become `298 words from cache`.
118
+
119
+ The recording opens and closes with the LibriVox announcement, which has no counterpart in the
120
+ text. readalign trims it: the first overlay starts at 20.5 seconds.
121
+
122
+ To fetch the sample yourself, or a full-length pair to try:
123
+
124
+ ```
125
+ python scripts/fetch_samples.py short # rebuilds examples/aesop
126
+ python scripts/fetch_samples.py long # Frankenstein, 7.5 hours over 25 files
127
+ ```
128
+
129
+ ## Checking the result
130
+
131
+ ```
132
+ $ readalign check aligned.epub
133
+ checking D:\Repos\ideas\readalign\aligned.epub
134
+ overlays 2
135
+ sentences 18
136
+ clip time 0.02 hours
137
+ media:duration 0.02 hours declared
138
+ result ok
139
+ ```
140
+
141
+ `check` walks the package document, opens every overlay, and verifies that each `text` element
142
+ points at a fragment that exists in a manifested content document, that each `audio` element
143
+ points at a manifested audio file, that `clipEnd` never runs past the real duration ffprobe
144
+ reports for that file, and that the declared `media:duration` values match the clips. It exits
145
+ non-zero if anything fails, and takes `--json` if you want to script it.
146
+
147
+ It is worth running the official validator too:
148
+
149
+ ```
150
+ java -jar epubcheck.jar aligned.epub
151
+ ```
152
+
153
+ ## A full-length run
154
+
155
+ The 25-file LibriVox recording of *Frankenstein* (7 h 26 m) against the Standard Ebooks edition,
156
+ on an RTX 4090:
157
+
158
+ ```
159
+ reading frankenstein-se.epub
160
+ 38 content documents, 3403 sentences
161
+ preparing 25 audio file(s)
162
+ transcribing
163
+ loading whisper model 'large-v3-turbo' on cuda (float16)
164
+ [1/25] frankenstein_00_shelley_64kb.mp3: transcribing 32:56
165
+ ...
166
+ 75799 words in 718.6s on cuda
167
+ aligning
168
+ aligning 78488 book tokens against 75827 spoken tokens
169
+ wrote frankenstein.epub (205.0 MB, 30 overlays, 6:59:25.256)
170
+ wrote 44 sidecar file(s) to frankenstein-sidecars
171
+ sentences 3250/3403 timed (95.5%)
172
+ interpolated 9 low confidence 10
173
+ token match 94.0%
174
+ drift max 3.98s, p95 0.00s, threshold 2.50s
175
+ untimed matter 128 sentences before the narration, 25 after
176
+ no overlay 8 document(s): text/titlepage.xhtml, text/imprint.xhtml, ...
177
+ ```
178
+
179
+ Twelve minutes of transcription, about ninety seconds for everything else. The result passes
180
+ epubcheck with no messages and `readalign check` reports the same 3250 sentences the build wrote.
181
+
182
+ That run exits 3, not 0, and the reason is worth understanding. The 8 documents with no overlay
183
+ are the title page, the imprint, Shelley's 1831 introduction, the preface, the dedication, the
184
+ epigraph, the colophon and the copyright page. The narrator reads none of them, so readalign
185
+ times none of them. The 153 untimed sentences are the same material. The max drift of 3.98s comes
186
+ from two sentences in the endnotes, which the narrator also never reads: one endnote matched a
187
+ phrase in the closing credits, so the endnote heading next to it got placed inside a 4 second
188
+ window instead of being left alone. Everything the narrator actually reads has a drift of zero.
189
+ `--drift-threshold 4` makes that run exit 0; the report tells you exactly which sentences moved
190
+ the number, so you can decide rather than guess.
191
+
192
+ ## Drift, and what the number means
193
+
194
+ Drift is how far a sentence's start could be wrong, in seconds.
195
+
196
+ - A sentence anchored on its own narrated words has drift **0**. Its start is a word timestamp
197
+ from the recogniser, accurate to a few tens of milliseconds.
198
+ - A sentence the narrator skipped, or garbled past recognition, is placed proportionally in the
199
+ gap between its neighbours. Its drift is the width of that gap: the worst it can be out by.
200
+ - A sentence whose start had to be pushed forward to keep the overlay in order carries the
201
+ distance it was pushed. That is the signal for text matched to the wrong occurrence of itself.
202
+
203
+ `--drift-threshold` (default 2.5s) is a gate on the maximum, not the average. The EPUB is always
204
+ written; exceeding the threshold only changes the exit code to 3 so a script can catch it.
205
+
206
+ ## How it works
207
+
208
+ 1. **ffprobe** reads every audio file. FLAC, WAV, ALAC and WMA are transcoded to AAC because
209
+ EPUB 3 only takes MP3, MP4 audio and Opus; MP3, M4A/M4B and Opus are stream-copied, so no
210
+ audio is re-encoded unless it has to be.
211
+ 2. **faster-whisper** transcribes with word timestamps. Transcripts are cached per file, keyed by
212
+ content and settings, so a second run over the same audiobook costs nothing.
213
+ 3. Both sides are normalised to comparable tokens: case folded, accents stripped, punctuation
214
+ dropped, digits and roman numerals spelled out ("Chapter XVIII" and "chapter eighteen" become
215
+ the same tokens).
216
+ 4. **Alignment** pins the two streams together with n-grams that occur exactly once on each side,
217
+ keeps the longest increasing chain of them, and runs a real diff only in the small regions
218
+ between anchors. That is what lets a 20 hour book align in a few hundred megabytes rather than
219
+ needing a 200k x 200k dynamic programming matrix.
220
+ 5. The XHTML is rewritten so each sentence is wrapped in `<span id="ra-1-7">`. Existing ids are
221
+ never touched, and sentence boundaries that fall inside `<em>` or `<a>` are split without
222
+ changing a character of the text.
223
+ 6. The overlay's `seq` elements mirror the document's own nesting. Footnotes, endnotes, page
224
+ numbers, sidebars and annotations carry their `epub:type`, so a reading system that offers
225
+ "skip footnotes" can, and tables, lists and figures nest the same way, so "escape this table"
226
+ works from three levels in.
227
+ 7. Overlays, manifest items and metadata go into the package document, and the archive is
228
+ written mimetype-first and uncompressed as OCF requires.
229
+
230
+ ## Options
231
+
232
+ ```
233
+ --audio PATH a file, a directory of numbered files, or a glob
234
+ --epub PATH the matching DRM-free EPUB
235
+ --out PATH the EPUB to write
236
+ --model NAME faster-whisper model (default: large-v3-turbo)
237
+ --language CODE ISO code of the narration (default: detect)
238
+ --drift-threshold SECS exit 3 if any sentence drifts more than this (default: 2.5)
239
+ --sidecars/--no-sidecars write WebVTT and align.json next to the output (default: on)
240
+ --dry-run probe the inputs and print the plan, transcribe nothing
241
+ --narrator NAME value for the media:narrator metadata
242
+ --refine sharpen boundaries with the MMS CTC aligner (see below)
243
+ --device auto|cuda|cpu where to run the recogniser (default: auto)
244
+ --beam-size N whisper beam size (default: 5)
245
+ --no-vad disable the voice-activity filter
246
+ --audio-bitrate RATE bitrate when audio has to be transcoded (default: 96k)
247
+ --work-dir PATH scratch directory instead of a temporary one
248
+ --cache-dir PATH transcript cache location
249
+ --no-cache neither read nor write the transcript cache
250
+ --quiet only print errors and the summary
251
+ ```
252
+
253
+ Multiple files are ordered by natural sort, so `part2.mp3` comes before `part10.mp3`. A single
254
+ file covering the whole book is fine, and so is a chapter split over several files; readalign
255
+ works from a continuous timeline and does not assume one file per chapter.
256
+
257
+ ### The optional refine pass
258
+
259
+ `pip install 'readalign[refine]'` adds torch and torchaudio, and `--refine` then runs a CTC
260
+ forced-alignment pass over each sentence boundary to sharpen it.
261
+
262
+ **The MMS-300m aligner that pass uses is licensed CC BY-NC 4.0, which means non-commercial use
263
+ only.** It is never downloaded unless you pass `--refine`, nothing in the default path touches
264
+ it, and any run that uses it records the restriction in the report's warnings.
265
+
266
+ ## Limitations
267
+
268
+ - **No DRM, ever.** An `.aax`, `.aa` or `.acsm` file, or an EPUB with an `META-INF/encryption.xml`,
269
+ exits 2 with `readalign does not handle DRM-protected files`. readalign will not strip
270
+ protection and has no plans to.
271
+ - Output is only as good as the recognition. Heavy accents, music beds, dramatised multi-voice
272
+ productions and poetry read with long pauses all lower the token match rate; watch that number
273
+ in the report.
274
+ - Abridged recordings work, in the sense that the parts that are read get timed and the rest is
275
+ reported as untimed. They will never reach full coverage.
276
+ - Audio is embedded in the EPUB, so the file is as large as the audiobook. Frankenstein at 64 kbps
277
+ is 205 MB. There is no streaming or external-audio option in this version.
278
+ - The audio timeline is shared across files. A sentence never spans two audio files; if one would,
279
+ it is clipped at the file boundary.
280
+ - Sentence splitting is tuned for English-style punctuation. Other languages align fine but may
281
+ produce longer or shorter units than a native reader would choose.
282
+
283
+ ## Reader support
284
+
285
+ Media Overlays are part of EPUB 3, but support is uneven. Readium-based readers (Thorium Reader
286
+ on desktop, Colibrio-based web readers) and Apple Books implement them. Kindle does not read EPUB
287
+ overlays at all, and several popular mobile readers ignore the `media-overlay` attribute and just
288
+ show the book. If a reader shows the text but plays no audio, it does not support overlays; the
289
+ file is still a valid EPUB and still readable.
290
+
291
+ readalign's output is validated against epubcheck 5.2.1, and `readalign check` is validated
292
+ against the reference Media Overlays title from the EPUB test suite at
293
+ <https://epubtest.org/test-books>.
294
+
295
+ ## Exit codes
296
+
297
+ | code | meaning |
298
+ | ---- | ------- |
299
+ | 0 | success |
300
+ | 1 | bad input, missing file, missing ffmpeg, nothing could be aligned |
301
+ | 2 | the input is DRM-protected |
302
+ | 3 | the EPUB was written but max drift exceeded `--drift-threshold` |
303
+ | 130 | interrupted |
304
+
305
+ Failures print one line, not a traceback.
306
+
307
+ ## Development
308
+
309
+ ```
310
+ git clone https://github.com/Booyaka101/readalign
311
+ cd readalign
312
+ pip install -e '.[dev]'
313
+ python -m pytest tests -q
314
+ python -m ruff check .
315
+ ```
316
+
317
+ The suite runs in about a second and needs neither a GPU nor network access; it builds its own
318
+ EPUBs and synthesises word timings. A few cases need ffmpeg and skip without it, and one is
319
+ skipped unless `READALIGN_MO_TESTBOOK` points at a copy of the epubtest.org Media Overlays book.
320
+
321
+ ## Distribution
322
+
323
+ The first distribution step is PyPI: `python -m build` then `twine upload dist/*`, which makes
324
+ `pip install readalign` and `uvx readalign` work for everyone. Nothing else in the project depends
325
+ on a hosted service.
326
+
327
+ ## Licence
328
+
329
+ MIT. See [LICENSE](LICENSE).
330
+
331
+ The bundled sample in `examples/aesop` is public domain: the recording from LibriVox, the text
332
+ from Project Gutenberg. See `examples/aesop/README.md` for the sources.