readalign 1.0.0__tar.gz → 1.1.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 (37) hide show
  1. readalign-1.1.0/CHANGELOG.md +70 -0
  2. {readalign-1.0.0 → readalign-1.1.0}/PKG-INFO +14 -3
  3. {readalign-1.0.0 → readalign-1.1.0}/README.md +13 -2
  4. {readalign-1.0.0 → readalign-1.1.0}/pyproject.toml +1 -1
  5. {readalign-1.0.0 → readalign-1.1.0}/src/readalign/__init__.py +1 -1
  6. {readalign-1.0.0 → readalign-1.1.0}/src/readalign/align.py +13 -1
  7. {readalign-1.0.0 → readalign-1.1.0}/src/readalign/asr.py +284 -277
  8. {readalign-1.0.0 → readalign-1.1.0}/src/readalign/audio.py +30 -12
  9. {readalign-1.0.0 → readalign-1.1.0}/src/readalign/cli.py +547 -495
  10. {readalign-1.0.0 → readalign-1.1.0}/src/readalign/package.py +32 -6
  11. {readalign-1.0.0 → readalign-1.1.0}/src/readalign/sentences.py +3 -1
  12. readalign-1.1.0/src/readalign/verify.py +75 -0
  13. {readalign-1.0.0 → readalign-1.1.0}/tests/test_align.py +28 -0
  14. {readalign-1.0.0 → readalign-1.1.0}/tests/test_asr.py +21 -0
  15. readalign-1.1.0/tests/test_audio.py +40 -0
  16. {readalign-1.0.0 → readalign-1.1.0}/tests/test_cli.py +79 -1
  17. {readalign-1.0.0 → readalign-1.1.0}/tests/test_output.py +50 -0
  18. {readalign-1.0.0 → readalign-1.1.0}/tests/test_text.py +13 -0
  19. readalign-1.1.0/tests/test_verify.py +128 -0
  20. readalign-1.0.0/CHANGELOG.md +0 -34
  21. {readalign-1.0.0 → readalign-1.1.0}/.gitignore +0 -0
  22. {readalign-1.0.0 → readalign-1.1.0}/LICENSE +0 -0
  23. {readalign-1.0.0 → readalign-1.1.0}/scripts/fetch_samples.py +0 -0
  24. {readalign-1.0.0 → readalign-1.1.0}/src/readalign/__main__.py +0 -0
  25. {readalign-1.0.0 → readalign-1.1.0}/src/readalign/clock.py +0 -0
  26. {readalign-1.0.0 → readalign-1.1.0}/src/readalign/epub.py +0 -0
  27. {readalign-1.0.0 → readalign-1.1.0}/src/readalign/errors.py +0 -0
  28. {readalign-1.0.0 → readalign-1.1.0}/src/readalign/fs.py +0 -0
  29. {readalign-1.0.0 → readalign-1.1.0}/src/readalign/members.py +0 -0
  30. {readalign-1.0.0 → readalign-1.1.0}/src/readalign/refine.py +0 -0
  31. {readalign-1.0.0 → readalign-1.1.0}/src/readalign/report.py +0 -0
  32. {readalign-1.0.0 → readalign-1.1.0}/src/readalign/sidecar.py +0 -0
  33. {readalign-1.0.0 → readalign-1.1.0}/src/readalign/smil.py +0 -0
  34. {readalign-1.0.0 → readalign-1.1.0}/src/readalign/textnorm.py +0 -0
  35. {readalign-1.0.0 → readalign-1.1.0}/tests/conftest.py +0 -0
  36. {readalign-1.0.0 → readalign-1.1.0}/tests/test_epub.py +0 -0
  37. {readalign-1.0.0 → readalign-1.1.0}/tests/test_sidecar_report.py +0 -0
@@ -0,0 +1,70 @@
1
+ # Changelog
2
+
3
+ ## 1.1.0 - 2026-10-04
4
+
5
+ - New: `--verify [JAR]` runs epubcheck on the result right after the build, prints its verdict,
6
+ records it under `verify` in the report, and exits 4 when it finds errors. epubcheck needs
7
+ Java, so the flag is opt-in: with a path it uses that jar, without one it searches `PATH`,
8
+ `EPUBCHECK_HOME` and `JAVA_HOME`.
9
+ - New: every unaligned region in the report carries an `excerpt` quoting the book's own words,
10
+ so a stretch that never matched the narration is recognisable without a token diff.
11
+ - Fixed: `--audio` accepted a glob in the documentation but not in the code, so a pattern like
12
+ `book-*.mp3` failed with "audio not found" on Windows (and anywhere the shell was not asked
13
+ to expand it). Patterns are now expanded by readalign itself, in natural order, with the same
14
+ DRM refusal as a directory.
15
+ - Fixed: `readalign check` raised a raw traceback on input that was a valid zip but broken XML:
16
+ an unparseable `META-INF/container.xml` or package document is now one line and exit 1, and
17
+ an unparseable overlay or content document is reported as a finding like any other fault
18
+ instead of crashing the walk.
19
+ - Fixed: rebuilding over an EPUB that already carried overlays kept the previous run's audio
20
+ manifest item, so a re-run whose prepared audio changed name or format shipped the old file
21
+ as dead weight (a full extra audiobook when the suffix drifted). A re-run now drops the
22
+ previous audio and reuses the same manifest ids, the way it already did for overlays and the
23
+ stylesheet.
24
+ - Fixed: manifest items added while writing an output were not registered in the package's
25
+ manifest index, so two builds over one package could hand out the same item id twice.
26
+ - Fixed: the transcript cache key ignored `--beam-size` (and the window plan), so changing the
27
+ beam silently served a transcript made with the old setting. Keys now cover every setting
28
+ that changes the transcription, which invalidates caches written by 1.0.0 once.
29
+ - Fixed: a sentence ending in the word "no" never split from the next one ("He said no. She
30
+ left."), because "no" sat in the abbreviation table. Numbered labels like "No. 5" still stay
31
+ together: the digit after the period is not an uppercase word.
32
+ - Fixed: the warning for a failed cache write bypassed the `--quiet` log and printed straight
33
+ to stderr; it now goes through the log like every other progress line.
34
+ - DRM refusals name the file they refused, which matters when a directory of audio contains
35
+ one protected straggler.
36
+ - `readalign check` says "and N more" after its first twenty warnings, as it already did for
37
+ errors, instead of truncating silently.
38
+
39
+ ## 1.0.0 - 2026-09-18
40
+
41
+ First release.
42
+
43
+ - `readalign build` turns a DRM-free audiobook and the matching DRM-free EPUB into one EPUB 3
44
+ with conformant Media Overlays: SMIL per content document, `media-overlay` on the manifest
45
+ items, `media:duration` per overlay plus a total, active-class metadata and a highlight
46
+ stylesheet. The overlay's `seq` elements mirror the document's nesting, so both skippability
47
+ (footnotes, endnotes, page numbers, sidebars, annotations) and escapability (tables, lists,
48
+ figures) work. The archive is written mimetype-first and uncompressed as OCF requires.
49
+ - `readalign check` validates the overlays in any EPUB: fragment ids resolve, audio is
50
+ manifested, no `clipEnd` runs past the duration ffprobe reports, declared durations match the
51
+ clips. `--json` for scripting.
52
+ - Transcription with faster-whisper, CUDA when available and CPU otherwise, word timestamps,
53
+ and a per-file transcript cache keyed by content and settings. A GPU that reports itself
54
+ present but cannot load its CUDA libraries falls back to the CPU with one warning line
55
+ naming what to install, rather than a traceback.
56
+ - Anchored alignment: unique shared n-grams, longest increasing subsequence, diff only in the
57
+ gaps. No global dynamic-programming matrix, and audio is streamed rather than buffered on the
58
+ way both in and out, so a 7.5 hour book builds in about 130 MB of memory and opening the
59
+ 205 MB result costs 33 MB. Two runs of the same build produce byte-identical archives.
60
+ - Sidecars: one WebVTT per chapter and audio file, plus `align.json`. `readalign-report.json`
61
+ records coverage, drift, per-document statistics and every region that did not match.
62
+ - Handles one file spanning several chapters, several files in one chapter, audio chapter counts
63
+ that disagree with the spine, narrator front and back matter with no text counterpart, notes
64
+ and page-break markers, existing ids, non-UTF8 XHTML, and clip ends past the real duration.
65
+ - DRM-protected input (`.aax`, `.aa`, `.acsm`, an encrypted EPUB) exits 2 and is never opened.
66
+ - Wrong input gets one line and an exit code, never a traceback: a missing file, an unzipped
67
+ book passed as a directory, something that is not a zip, an EPUB 2 package, an empty audio
68
+ directory, or a file ffprobe cannot read.
69
+ - Optional `readalign[refine]` extra adds a CTC forced-alignment pass behind `--refine`. The
70
+ 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.1.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,
@@ -244,6 +251,9 @@ written; exceeding the threshold only changes the exit code to 3 so a script can
244
251
  --beam-size N whisper beam size (default: 5)
245
252
  --no-vad disable the voice-activity filter
246
253
  --audio-bitrate RATE bitrate when audio has to be transcoded (default: 96k)
254
+ --verify [JAR] validate the result with epubcheck after writing it (needs Java);
255
+ pass its jar path, or leave the value off to search PATH,
256
+ EPUBCHECK_HOME and JAVA_HOME for one
247
257
  --work-dir PATH scratch directory instead of a temporary one
248
258
  --cache-dir PATH transcript cache location
249
259
  --no-cache neither read nor write the transcript cache
@@ -300,6 +310,7 @@ against the reference Media Overlays title from the EPUB test suite at
300
310
  | 1 | bad input, missing file, missing ffmpeg, nothing could be aligned |
301
311
  | 2 | the input is DRM-protected |
302
312
  | 3 | the EPUB was written but max drift exceeded `--drift-threshold` |
313
+ | 4 | the EPUB was written but epubcheck reported problems (only with `--verify`) |
303
314
  | 130 | interrupted |
304
315
 
305
316
  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,
@@ -211,6 +218,9 @@ written; exceeding the threshold only changes the exit code to 3 so a script can
211
218
  --beam-size N whisper beam size (default: 5)
212
219
  --no-vad disable the voice-activity filter
213
220
  --audio-bitrate RATE bitrate when audio has to be transcoded (default: 96k)
221
+ --verify [JAR] validate the result with epubcheck after writing it (needs Java);
222
+ pass its jar path, or leave the value off to search PATH,
223
+ EPUBCHECK_HOME and JAVA_HOME for one
214
224
  --work-dir PATH scratch directory instead of a temporary one
215
225
  --cache-dir PATH transcript cache location
216
226
  --no-cache neither read nor write the transcript cache
@@ -267,6 +277,7 @@ against the reference Media Overlays title from the EPUB test suite at
267
277
  | 1 | bad input, missing file, missing ffmpeg, nothing could be aligned |
268
278
  | 2 | the input is DRM-protected |
269
279
  | 3 | the EPUB was written but max drift exceeded `--drift-threshold` |
280
+ | 4 | the EPUB was written but epubcheck reported problems (only with `--verify`) |
270
281
  | 130 | interrupted |
271
282
 
272
283
  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.1.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.1.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),