readalign 1.0.0__tar.gz → 1.2.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.
Files changed (40) hide show
  1. {readalign-1.0.0 → readalign-1.2.0}/.gitignore +20 -20
  2. readalign-1.2.0/CHANGELOG.md +94 -0
  3. {readalign-1.0.0 → readalign-1.2.0}/PKG-INFO +28 -4
  4. {readalign-1.0.0 → readalign-1.2.0}/README.md +27 -3
  5. {readalign-1.0.0 → readalign-1.2.0}/pyproject.toml +1 -1
  6. {readalign-1.0.0 → readalign-1.2.0}/src/readalign/__init__.py +1 -1
  7. {readalign-1.0.0 → readalign-1.2.0}/src/readalign/align.py +13 -1
  8. {readalign-1.0.0 → readalign-1.2.0}/src/readalign/asr.py +303 -277
  9. {readalign-1.0.0 → readalign-1.2.0}/src/readalign/audio.py +39 -15
  10. {readalign-1.0.0 → readalign-1.2.0}/src/readalign/cli.py +628 -495
  11. {readalign-1.0.0 → readalign-1.2.0}/src/readalign/clock.py +15 -9
  12. {readalign-1.0.0 → readalign-1.2.0}/src/readalign/epub.py +674 -671
  13. readalign-1.2.0/src/readalign/fs.py +64 -0
  14. {readalign-1.0.0 → readalign-1.2.0}/src/readalign/members.py +72 -72
  15. {readalign-1.0.0 → readalign-1.2.0}/src/readalign/package.py +91 -46
  16. {readalign-1.0.0 → readalign-1.2.0}/src/readalign/sentences.py +3 -1
  17. {readalign-1.0.0 → readalign-1.2.0}/src/readalign/sidecar.py +10 -5
  18. {readalign-1.0.0 → readalign-1.2.0}/src/readalign/smil.py +4 -3
  19. readalign-1.2.0/src/readalign/verify.py +75 -0
  20. {readalign-1.0.0 → readalign-1.2.0}/tests/test_align.py +28 -0
  21. {readalign-1.0.0 → readalign-1.2.0}/tests/test_asr.py +72 -0
  22. readalign-1.2.0/tests/test_audio.py +77 -0
  23. readalign-1.2.0/tests/test_cli.py +292 -0
  24. readalign-1.2.0/tests/test_fs.py +51 -0
  25. {readalign-1.0.0 → readalign-1.2.0}/tests/test_output.py +160 -0
  26. {readalign-1.0.0 → readalign-1.2.0}/tests/test_sidecar_report.py +33 -0
  27. {readalign-1.0.0 → readalign-1.2.0}/tests/test_text.py +13 -0
  28. readalign-1.2.0/tests/test_verify.py +128 -0
  29. readalign-1.0.0/CHANGELOG.md +0 -34
  30. readalign-1.0.0/src/readalign/fs.py +0 -22
  31. readalign-1.0.0/tests/test_cli.py +0 -100
  32. {readalign-1.0.0 → readalign-1.2.0}/LICENSE +0 -0
  33. {readalign-1.0.0 → readalign-1.2.0}/scripts/fetch_samples.py +0 -0
  34. {readalign-1.0.0 → readalign-1.2.0}/src/readalign/__main__.py +0 -0
  35. {readalign-1.0.0 → readalign-1.2.0}/src/readalign/errors.py +0 -0
  36. {readalign-1.0.0 → readalign-1.2.0}/src/readalign/refine.py +0 -0
  37. {readalign-1.0.0 → readalign-1.2.0}/src/readalign/report.py +0 -0
  38. {readalign-1.0.0 → readalign-1.2.0}/src/readalign/textnorm.py +0 -0
  39. {readalign-1.0.0 → readalign-1.2.0}/tests/conftest.py +0 -0
  40. {readalign-1.0.0 → readalign-1.2.0}/tests/test_epub.py +0 -0
@@ -1,20 +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
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,94 @@
1
+ # Changelog
2
+
3
+ ## 1.2.0 - 2026-10-22
4
+
5
+ - New: `--report PATH` selects a separate JSON report for each batch build, with guards
6
+ against overwriting input audio, the input/output EPUB or generated sidecars.
7
+ - Fixed: colliding overlay names and URL-special characters in EPUB resource paths.
8
+ Generated audio and CSS also avoid overwriting unrelated existing resources.
9
+ - Fixed: transcript cache identity now hashes prepared audio bytes and includes chapter
10
+ boundaries and the effective language. Malformed cached words are ignored instead of crashing.
11
+ - Fixed: EPUB, report and cache writes replace their destinations only after a complete write.
12
+ - Fixed: work-directory audio aliases, invalid durations, chapter marks and beam sizes are
13
+ rejected; missing epubcheck paths fail before transcription.
14
+ - Fixed: `check` rejects unmanifested audio, overlays pointing outside their associated document,
15
+ empty overlays, duplicate manifest ids and mismatched durations. Corrupt archives produce
16
+ readable errors; audio-probe scratch filenames never come from archive member names.
17
+ - New: `--min-token-match FRACTION` optionally gates builds on the fraction of book tokens
18
+ matched to narration. Values range from 0 to 1; the default 0 preserves existing behaviour.
19
+ A failed gate exits 5 after writing the EPUB, report and enabled sidecars. Its threshold and
20
+ verdict are recorded under `token_match_gate`; drift and epubcheck failures retain precedence.
21
+ - Fixed: chapters with identical basenames in different EPUB directories could overwrite each
22
+ other's WebVTT sidecars. Colliding names now gain a numeric suffix, including case-only
23
+ collisions on Windows, without changing filenames for non-colliding exports.
24
+ - Fixed: the ebook overwrite guard now recognises symbolic links, hard links and path aliases.
25
+ - Fixed: non-finite drift thresholds such as NaN and infinity are rejected before processing.
26
+
27
+ ## 1.1.0 - 2026-10-04
28
+
29
+ - New: `--verify [JAR]` runs epubcheck on the result right after the build, prints its verdict,
30
+ records it under `verify` in the report, and exits 4 when it finds errors. epubcheck needs
31
+ Java, so the flag is opt-in: with a path it uses that jar, without one it searches `PATH`,
32
+ `EPUBCHECK_HOME` and `JAVA_HOME`.
33
+ - New: every unaligned region in the report carries an `excerpt` quoting the book's own words,
34
+ so a stretch that never matched the narration is recognisable without a token diff.
35
+ - Fixed: `--audio` accepted a glob in the documentation but not in the code, so a pattern like
36
+ `book-*.mp3` failed with "audio not found" on Windows (and anywhere the shell was not asked
37
+ to expand it). Patterns are now expanded by readalign itself, in natural order, with the same
38
+ DRM refusal as a directory.
39
+ - Fixed: `readalign check` raised a raw traceback on input that was a valid zip but broken XML:
40
+ an unparseable `META-INF/container.xml` or package document is now one line and exit 1, and
41
+ an unparseable overlay or content document is reported as a finding like any other fault
42
+ instead of crashing the walk.
43
+ - Fixed: rebuilding over an EPUB that already carried overlays kept the previous run's audio
44
+ manifest item, so a re-run whose prepared audio changed name or format shipped the old file
45
+ as dead weight (a full extra audiobook when the suffix drifted). A re-run now drops the
46
+ previous audio and reuses the same manifest ids, the way it already did for overlays and the
47
+ stylesheet.
48
+ - Fixed: manifest items added while writing an output were not registered in the package's
49
+ manifest index, so two builds over one package could hand out the same item id twice.
50
+ - Fixed: the transcript cache key ignored `--beam-size` (and the window plan), so changing the
51
+ beam silently served a transcript made with the old setting. Keys now cover every setting
52
+ that changes the transcription, which invalidates caches written by 1.0.0 once.
53
+ - Fixed: a sentence ending in the word "no" never split from the next one ("He said no. She
54
+ left."), because "no" sat in the abbreviation table. Numbered labels like "No. 5" still stay
55
+ together: the digit after the period is not an uppercase word.
56
+ - Fixed: the warning for a failed cache write bypassed the `--quiet` log and printed straight
57
+ to stderr; it now goes through the log like every other progress line.
58
+ - DRM refusals name the file they refused, which matters when a directory of audio contains
59
+ one protected straggler.
60
+ - `readalign check` says "and N more" after its first twenty warnings, as it already did for
61
+ errors, instead of truncating silently.
62
+
63
+ ## 1.0.0 - 2026-09-18
64
+
65
+ First release.
66
+
67
+ - `readalign build` turns a DRM-free audiobook and the matching DRM-free EPUB into one EPUB 3
68
+ with conformant Media Overlays: SMIL per content document, `media-overlay` on the manifest
69
+ items, `media:duration` per overlay plus a total, active-class metadata and a highlight
70
+ stylesheet. The overlay's `seq` elements mirror the document's nesting, so both skippability
71
+ (footnotes, endnotes, page numbers, sidebars, annotations) and escapability (tables, lists,
72
+ figures) work. The archive is written mimetype-first and uncompressed as OCF requires.
73
+ - `readalign check` validates the overlays in any EPUB: fragment ids resolve, audio is
74
+ manifested, no `clipEnd` runs past the duration ffprobe reports, declared durations match the
75
+ clips. `--json` for scripting.
76
+ - Transcription with faster-whisper, CUDA when available and CPU otherwise, word timestamps,
77
+ and a per-file transcript cache keyed by content and settings. A GPU that reports itself
78
+ present but cannot load its CUDA libraries falls back to the CPU with one warning line
79
+ naming what to install, rather than a traceback.
80
+ - Anchored alignment: unique shared n-grams, longest increasing subsequence, diff only in the
81
+ gaps. No global dynamic-programming matrix, and audio is streamed rather than buffered on the
82
+ way both in and out, so a 7.5 hour book builds in about 130 MB of memory and opening the
83
+ 205 MB result costs 33 MB. Two runs of the same build produce byte-identical archives.
84
+ - Sidecars: one WebVTT per chapter and audio file, plus `align.json`. `readalign-report.json`
85
+ records coverage, drift, per-document statistics and every region that did not match.
86
+ - Handles one file spanning several chapters, several files in one chapter, audio chapter counts
87
+ that disagree with the spine, narrator front and back matter with no text counterpart, notes
88
+ and page-break markers, existing ids, non-UTF8 XHTML, and clip ends past the real duration.
89
+ - DRM-protected input (`.aax`, `.aa`, `.acsm`, an encrypted EPUB) exits 2 and is never opened.
90
+ - Wrong input gets one line and an exit code, never a traceback: a missing file, an unzipped
91
+ book passed as a directory, something that is not a zip, an EPUB 2 package, an empty audio
92
+ directory, or a file ffprobe cannot read.
93
+ - Optional `readalign[refine]` extra adds a CTC forced-alignment pass behind `--refine`. The
94
+ MMS-300m aligner it uses is CC BY-NC 4.0, so it is never downloaded by the default path.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: readalign
3
- Version: 1.0.0
3
+ Version: 1.2.0
4
4
  Summary: Turn an audiobook and the matching ebook you own into one EPUB 3 with Media Overlays.
5
5
  Project-URL: Homepage, https://github.com/Booyaka101/readalign
6
6
  Project-URL: Repository, https://github.com/Booyaka101/readalign
@@ -50,7 +50,8 @@ readalign build --audio audiobook/ --epub book.epub --out read-along.epub
50
50
  `media-overlay` links, `media:duration` metadata and a highlight stylesheet. It passes
51
51
  epubcheck 5.2.1 with zero messages.
52
52
  - **`readalign-report.json`** next to the output: coverage, drift, per-document statistics, which
53
- documents got no overlay and which regions never matched.
53
+ documents got no overlay and which regions never matched, each quoted with a short excerpt of
54
+ the book's own words so a bad match is obvious without opening the JSON.
54
55
  - **sidecars** (`<name>-sidecars/`): one WebVTT file per chapter and audio file, plus `align.json`
55
56
  with every sentence's id, clip times, confidence and text. A rerun clears the `.vtt` files and
56
57
  `align.json` it wrote last time, so nothing stale is left behind. Turn them off with
@@ -93,7 +94,7 @@ Two minutes of audio is not worth spinning up a GPU for, hence `--device cpu`. R
93
94
  from that command, on a cold cache:
94
95
 
95
96
  ```
96
- readalign 1.0.0
97
+ readalign 1.1.0
97
98
  reading examples/aesop/aesop-two-fables.epub
98
99
  2 content documents, 18 sentences
99
100
  preparing 1 audio file(s)
@@ -150,6 +151,12 @@ It is worth running the official validator too:
150
151
  java -jar epubcheck.jar aligned.epub
151
152
  ```
152
153
 
154
+ Or let readalign do it: `--verify` runs epubcheck on the result right after writing it, prints
155
+ its verdict, records it in the report, and exits 4 if it found errors. It needs Java and the
156
+ epubcheck jar, which is why it is opt-in: pass the jar's path (`--verify C:/tools/epubcheck.jar`)
157
+ or leave the value off and readalign will look for one on `PATH`, in `EPUBCHECK_HOME` and in
158
+ `JAVA_HOME`.
159
+
153
160
  ## A full-length run
154
161
 
155
162
  The 25-file LibriVox recording of *Frankenstein* (7 h 26 m) against the Standard Ebooks edition,
@@ -203,13 +210,23 @@ Drift is how far a sentence's start could be wrong, in seconds.
203
210
  `--drift-threshold` (default 2.5s) is a gate on the maximum, not the average. The EPUB is always
204
211
  written; exceeding the threshold only changes the exit code to 3 so a script can catch it.
205
212
 
213
+ For batch builds, `--min-token-match 0.9` also requires at least 90% of the ebook's normalised
214
+ tokens to match the narration. This optional gate defaults to 0 (disabled). A failed gate exits 5
215
+ but keeps the EPUB, report and enabled sidecars for inspection. The report's `token_match_gate`
216
+ records the threshold and verdict using the unrounded match fraction. Unnarrated front matter
217
+ and abridgements lower this score, so choose a threshold appropriate to your books. This is a
218
+ screening aid, not proof that every sentence is correctly timed. Drift and epubcheck failures
219
+ take precedence over this gate when more than one check fails.
220
+
206
221
  ## How it works
207
222
 
208
223
  1. **ffprobe** reads every audio file. FLAC, WAV, ALAC and WMA are transcoded to AAC because
209
224
  EPUB 3 only takes MP3, MP4 audio and Opus; MP3, M4A/M4B and Opus are stream-copied, so no
210
225
  audio is re-encoded unless it has to be.
211
226
  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.
227
+ a streamed SHA-256 of the prepared audio, chapter boundaries and recogniser settings.
228
+ A second run avoids transcription, but still prepares and hashes the audio. Older cache keys
229
+ are invalidated once; malformed entries are ignored and rebuilt.
213
230
  3. Both sides are normalised to comparable tokens: case folded, accents stripped, punctuation
214
231
  dropped, digits and roman numerals spelled out ("Chapter XVIII" and "chapter eighteen" become
215
232
  the same tokens).
@@ -233,9 +250,11 @@ written; exceeding the threshold only changes the exit code to 3 so a script can
233
250
  --audio PATH a file, a directory of numbered files, or a glob
234
251
  --epub PATH the matching DRM-free EPUB
235
252
  --out PATH the EPUB to write
253
+ --report PATH JSON report path (default: readalign-report.json beside --out)
236
254
  --model NAME faster-whisper model (default: large-v3-turbo)
237
255
  --language CODE ISO code of the narration (default: detect)
238
256
  --drift-threshold SECS exit 3 if any sentence drifts more than this (default: 2.5)
257
+ --min-token-match N exit 5 if matched book-token fraction is below N (0 to 1; default: 0)
239
258
  --sidecars/--no-sidecars write WebVTT and align.json next to the output (default: on)
240
259
  --dry-run probe the inputs and print the plan, transcribe nothing
241
260
  --narrator NAME value for the media:narrator metadata
@@ -244,6 +263,9 @@ written; exceeding the threshold only changes the exit code to 3 so a script can
244
263
  --beam-size N whisper beam size (default: 5)
245
264
  --no-vad disable the voice-activity filter
246
265
  --audio-bitrate RATE bitrate when audio has to be transcoded (default: 96k)
266
+ --verify [JAR] validate the result with epubcheck after writing it (needs Java);
267
+ pass its jar path, or leave the value off to search PATH,
268
+ EPUBCHECK_HOME and JAVA_HOME for one
247
269
  --work-dir PATH scratch directory instead of a temporary one
248
270
  --cache-dir PATH transcript cache location
249
271
  --no-cache neither read nor write the transcript cache
@@ -300,6 +322,8 @@ against the reference Media Overlays title from the EPUB test suite at
300
322
  | 1 | bad input, missing file, missing ffmpeg, nothing could be aligned |
301
323
  | 2 | the input is DRM-protected |
302
324
  | 3 | the EPUB was written but max drift exceeded `--drift-threshold` |
325
+ | 4 | the EPUB was written but epubcheck reported problems (only with `--verify`) |
326
+ | 5 | the EPUB was written but token match was below `--min-token-match` |
303
327
  | 130 | interrupted |
304
328
 
305
329
  Failures print one line, not a traceback.
@@ -17,7 +17,8 @@ readalign build --audio audiobook/ --epub book.epub --out read-along.epub
17
17
  `media-overlay` links, `media:duration` metadata and a highlight stylesheet. It passes
18
18
  epubcheck 5.2.1 with zero messages.
19
19
  - **`readalign-report.json`** next to the output: coverage, drift, per-document statistics, which
20
- documents got no overlay and which regions never matched.
20
+ documents got no overlay and which regions never matched, each quoted with a short excerpt of
21
+ the book's own words so a bad match is obvious without opening the JSON.
21
22
  - **sidecars** (`<name>-sidecars/`): one WebVTT file per chapter and audio file, plus `align.json`
22
23
  with every sentence's id, clip times, confidence and text. A rerun clears the `.vtt` files and
23
24
  `align.json` it wrote last time, so nothing stale is left behind. Turn them off with
@@ -60,7 +61,7 @@ Two minutes of audio is not worth spinning up a GPU for, hence `--device cpu`. R
60
61
  from that command, on a cold cache:
61
62
 
62
63
  ```
63
- readalign 1.0.0
64
+ readalign 1.1.0
64
65
  reading examples/aesop/aesop-two-fables.epub
65
66
  2 content documents, 18 sentences
66
67
  preparing 1 audio file(s)
@@ -117,6 +118,12 @@ It is worth running the official validator too:
117
118
  java -jar epubcheck.jar aligned.epub
118
119
  ```
119
120
 
121
+ Or let readalign do it: `--verify` runs epubcheck on the result right after writing it, prints
122
+ its verdict, records it in the report, and exits 4 if it found errors. It needs Java and the
123
+ epubcheck jar, which is why it is opt-in: pass the jar's path (`--verify C:/tools/epubcheck.jar`)
124
+ or leave the value off and readalign will look for one on `PATH`, in `EPUBCHECK_HOME` and in
125
+ `JAVA_HOME`.
126
+
120
127
  ## A full-length run
121
128
 
122
129
  The 25-file LibriVox recording of *Frankenstein* (7 h 26 m) against the Standard Ebooks edition,
@@ -170,13 +177,23 @@ Drift is how far a sentence's start could be wrong, in seconds.
170
177
  `--drift-threshold` (default 2.5s) is a gate on the maximum, not the average. The EPUB is always
171
178
  written; exceeding the threshold only changes the exit code to 3 so a script can catch it.
172
179
 
180
+ For batch builds, `--min-token-match 0.9` also requires at least 90% of the ebook's normalised
181
+ tokens to match the narration. This optional gate defaults to 0 (disabled). A failed gate exits 5
182
+ but keeps the EPUB, report and enabled sidecars for inspection. The report's `token_match_gate`
183
+ records the threshold and verdict using the unrounded match fraction. Unnarrated front matter
184
+ and abridgements lower this score, so choose a threshold appropriate to your books. This is a
185
+ screening aid, not proof that every sentence is correctly timed. Drift and epubcheck failures
186
+ take precedence over this gate when more than one check fails.
187
+
173
188
  ## How it works
174
189
 
175
190
  1. **ffprobe** reads every audio file. FLAC, WAV, ALAC and WMA are transcoded to AAC because
176
191
  EPUB 3 only takes MP3, MP4 audio and Opus; MP3, M4A/M4B and Opus are stream-copied, so no
177
192
  audio is re-encoded unless it has to be.
178
193
  2. **faster-whisper** transcribes with word timestamps. Transcripts are cached per file, keyed by
179
- content and settings, so a second run over the same audiobook costs nothing.
194
+ a streamed SHA-256 of the prepared audio, chapter boundaries and recogniser settings.
195
+ A second run avoids transcription, but still prepares and hashes the audio. Older cache keys
196
+ are invalidated once; malformed entries are ignored and rebuilt.
180
197
  3. Both sides are normalised to comparable tokens: case folded, accents stripped, punctuation
181
198
  dropped, digits and roman numerals spelled out ("Chapter XVIII" and "chapter eighteen" become
182
199
  the same tokens).
@@ -200,9 +217,11 @@ written; exceeding the threshold only changes the exit code to 3 so a script can
200
217
  --audio PATH a file, a directory of numbered files, or a glob
201
218
  --epub PATH the matching DRM-free EPUB
202
219
  --out PATH the EPUB to write
220
+ --report PATH JSON report path (default: readalign-report.json beside --out)
203
221
  --model NAME faster-whisper model (default: large-v3-turbo)
204
222
  --language CODE ISO code of the narration (default: detect)
205
223
  --drift-threshold SECS exit 3 if any sentence drifts more than this (default: 2.5)
224
+ --min-token-match N exit 5 if matched book-token fraction is below N (0 to 1; default: 0)
206
225
  --sidecars/--no-sidecars write WebVTT and align.json next to the output (default: on)
207
226
  --dry-run probe the inputs and print the plan, transcribe nothing
208
227
  --narrator NAME value for the media:narrator metadata
@@ -211,6 +230,9 @@ written; exceeding the threshold only changes the exit code to 3 so a script can
211
230
  --beam-size N whisper beam size (default: 5)
212
231
  --no-vad disable the voice-activity filter
213
232
  --audio-bitrate RATE bitrate when audio has to be transcoded (default: 96k)
233
+ --verify [JAR] validate the result with epubcheck after writing it (needs Java);
234
+ pass its jar path, or leave the value off to search PATH,
235
+ EPUBCHECK_HOME and JAVA_HOME for one
214
236
  --work-dir PATH scratch directory instead of a temporary one
215
237
  --cache-dir PATH transcript cache location
216
238
  --no-cache neither read nor write the transcript cache
@@ -267,6 +289,8 @@ against the reference Media Overlays title from the EPUB test suite at
267
289
  | 1 | bad input, missing file, missing ffmpeg, nothing could be aligned |
268
290
  | 2 | the input is DRM-protected |
269
291
  | 3 | the EPUB was written but max drift exceeded `--drift-threshold` |
292
+ | 4 | the EPUB was written but epubcheck reported problems (only with `--verify`) |
293
+ | 5 | the EPUB was written but token match was below `--min-token-match` |
270
294
  | 130 | interrupted |
271
295
 
272
296
  Failures print one line, not a traceback.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "readalign"
7
- version = "1.0.0"
7
+ version = "1.2.0"
8
8
  description = "Turn an audiobook and the matching ebook you own into one EPUB 3 with Media Overlays."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,3 +1,3 @@
1
1
  """readalign: build an EPUB 3 with Media Overlays from an audiobook and the matching ebook."""
2
2
 
3
- __version__ = "1.0.0"
3
+ __version__ = "1.2.0"
@@ -175,6 +175,14 @@ def _track_for(tracks: list[AudioTrack], moment: float) -> AudioTrack:
175
175
  return tracks[-1]
176
176
 
177
177
 
178
+ def _excerpt(raw: list[str], start: int, end: int, limit: int = 12) -> str:
179
+ """A short human-readable preview of one unaligned stretch, for the report."""
180
+ words = raw[max(0, start): max(0, start, end)]
181
+ if len(words) <= limit:
182
+ return " ".join(words)
183
+ return " ".join(words[:limit]) + " ..."
184
+
185
+
178
186
  def align(
179
187
  sentences: list[Sentence],
180
188
  words: list[Word],
@@ -192,11 +200,14 @@ def align(
192
200
  )
193
201
 
194
202
  book_tokens: list[str] = []
203
+ book_raw: list[str] = []
195
204
  sentence_spans: list[tuple[int, int]] = []
196
205
  for sentence in sentences:
197
- tokens, _ = tokenize_words(sentence.words)
206
+ tokens, origins = tokenize_words(sentence.words)
198
207
  sentence_spans.append((len(book_tokens), len(book_tokens) + len(tokens)))
199
208
  book_tokens.extend(tokens)
209
+ # The raw word behind every token, so the report can quote the book's own spelling.
210
+ book_raw.extend(sentence.words[origin] for origin in origins)
200
211
 
201
212
  asr_tokens, asr_origin = tokenize_words([word.text for word in words])
202
213
  if not book_tokens:
@@ -224,6 +235,7 @@ def align(
224
235
  unaligned_regions=[
225
236
  {
226
237
  "book_tokens": [region[0], region[1]],
238
+ "excerpt": _excerpt(book_raw, region[0], region[1]),
227
239
  "audio_seconds": [
228
240
  round(words[asr_origin[min(region[2], len(asr_origin) - 1)]].start, 2),
229
241
  round(words[asr_origin[min(region[3] - 1, len(asr_origin) - 1)]].end, 2),