@storyteller-platform/align 0.1.57 → 0.1.58

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 (118) hide show
  1. package/dist/align/align.cjs +38 -19
  2. package/dist/align/align.d.cts +12 -1
  3. package/dist/align/align.d.ts +12 -1
  4. package/dist/align/align.js +38 -19
  5. package/dist/align/ctc/Aligner.cjs +800 -0
  6. package/dist/align/ctc/Aligner.d.cts +56 -0
  7. package/dist/align/ctc/Aligner.d.ts +56 -0
  8. package/dist/align/ctc/Aligner.js +789 -0
  9. package/dist/align/ctc/align.cjs +189 -0
  10. package/dist/align/ctc/align.d.cts +14 -0
  11. package/dist/align/ctc/align.d.ts +14 -0
  12. package/dist/align/ctc/align.js +132 -0
  13. package/dist/{process/mime.cjs → align/ctc/emissions.cjs} +7 -20
  14. package/dist/align/ctc/emissions.d.cts +6 -0
  15. package/dist/align/ctc/emissions.d.ts +6 -0
  16. package/dist/align/ctc/emissions.js +7 -0
  17. package/dist/align/ctc/forcedAlign.cjs +273 -0
  18. package/dist/align/ctc/forcedAlign.d.cts +21 -0
  19. package/dist/align/ctc/forcedAlign.d.ts +21 -0
  20. package/dist/align/ctc/forcedAlign.js +248 -0
  21. package/dist/align/ctc/greedyDecode.cjs +61 -0
  22. package/dist/align/ctc/greedyDecode.d.cts +15 -0
  23. package/dist/align/ctc/greedyDecode.d.ts +15 -0
  24. package/dist/align/ctc/greedyDecode.js +37 -0
  25. package/dist/align/ctc/mediaOverlay.cjs +248 -0
  26. package/dist/align/ctc/mediaOverlay.d.cts +16 -0
  27. package/dist/align/ctc/mediaOverlay.d.ts +16 -0
  28. package/dist/align/ctc/mediaOverlay.js +224 -0
  29. package/dist/align/ctc/report.cjs +66 -0
  30. package/dist/align/ctc/report.d.cts +79 -0
  31. package/dist/align/ctc/report.d.ts +79 -0
  32. package/dist/align/ctc/report.js +42 -0
  33. package/dist/align/ctc/search.cjs +437 -0
  34. package/dist/align/ctc/search.d.cts +46 -0
  35. package/dist/align/ctc/search.d.ts +46 -0
  36. package/dist/align/ctc/search.js +412 -0
  37. package/dist/align/getSentenceRanges.cjs +9 -3
  38. package/dist/align/getSentenceRanges.d.cts +1 -0
  39. package/dist/align/getSentenceRanges.d.ts +1 -0
  40. package/dist/align/getSentenceRanges.js +9 -3
  41. package/dist/align/parse.cjs +17 -4
  42. package/dist/align/parse.d.cts +8 -4
  43. package/dist/align/parse.d.ts +8 -4
  44. package/dist/align/parse.js +19 -4
  45. package/dist/align/search.cjs +76 -23
  46. package/dist/align/search.d.cts +8 -4
  47. package/dist/align/search.d.ts +8 -4
  48. package/dist/align/search.js +76 -24
  49. package/dist/align/slugify.cjs +3 -2
  50. package/dist/align/slugify.d.cts +1 -1
  51. package/dist/align/slugify.d.ts +1 -1
  52. package/dist/align/slugify.js +3 -2
  53. package/dist/cli/bin.cjs +204 -114
  54. package/dist/cli/bin.js +194 -103
  55. package/dist/common/ffmpeg.cjs +10 -5
  56. package/dist/common/ffmpeg.js +10 -5
  57. package/dist/common/logging.cjs +33 -1
  58. package/dist/common/logging.js +33 -1
  59. package/dist/emit/emit.cjs +105 -0
  60. package/dist/emit/emit.d.cts +15 -0
  61. package/dist/emit/emit.d.ts +15 -0
  62. package/dist/emit/emit.js +86 -0
  63. package/dist/emit/fs.cjs +227 -0
  64. package/dist/emit/fs.d.cts +22 -0
  65. package/dist/emit/fs.d.ts +22 -0
  66. package/dist/emit/fs.js +162 -0
  67. package/dist/emit/parse.cjs +68 -0
  68. package/dist/emit/parse.d.cts +26 -0
  69. package/dist/emit/parse.d.ts +26 -0
  70. package/dist/emit/parse.js +58 -0
  71. package/dist/index.d.cts +1 -1
  72. package/dist/index.d.ts +1 -1
  73. package/dist/markup/model.cjs +0 -16
  74. package/dist/markup/model.d.cts +1 -1
  75. package/dist/markup/model.d.ts +1 -1
  76. package/dist/markup/model.js +0 -12
  77. package/dist/markup/parseDom.cjs +13 -14
  78. package/dist/markup/parseDom.d.cts +2 -2
  79. package/dist/markup/parseDom.d.ts +2 -2
  80. package/dist/markup/parseDom.js +13 -18
  81. package/dist/markup/resolvedPos.d.cts +1 -1
  82. package/dist/markup/resolvedPos.d.ts +1 -1
  83. package/dist/markup/segmentation.cjs +3 -1
  84. package/dist/markup/segmentation.js +3 -1
  85. package/dist/markup/serializeDom.cjs +1 -1
  86. package/dist/markup/serializeDom.d.cts +1 -1
  87. package/dist/markup/serializeDom.d.ts +1 -1
  88. package/dist/markup/serializeDom.js +1 -1
  89. package/dist/markup/transform.cjs +5 -2
  90. package/dist/markup/transform.d.cts +1 -1
  91. package/dist/markup/transform.d.ts +1 -1
  92. package/dist/markup/transform.js +5 -4
  93. package/dist/{model-CZ2mMHJO.d.cts → model-CeOMDDgt.d.cts} +3 -11
  94. package/dist/{model-CZ2mMHJO.d.ts → model-CeOMDDgt.d.ts} +3 -11
  95. package/dist/process/processAudiobook.cjs +12 -23
  96. package/dist/process/processAudiobook.js +14 -30
  97. package/dist/process/ranges.cjs +2 -2
  98. package/dist/process/ranges.js +2 -2
  99. package/dist/readium/guidedNavigation.cjs +8 -8
  100. package/dist/readium/guidedNavigation.js +8 -8
  101. package/dist/readium/manifest.cjs +5 -4
  102. package/dist/readium/manifest.js +5 -4
  103. package/dist/snapshot/ctc/snapshot.cjs +261 -0
  104. package/dist/snapshot/ctc/snapshot.d.cts +6 -0
  105. package/dist/snapshot/ctc/snapshot.d.ts +6 -0
  106. package/dist/snapshot/ctc/snapshot.js +200 -0
  107. package/dist/snapshot/parse.cjs +29 -16
  108. package/dist/snapshot/parse.d.cts +14 -10
  109. package/dist/snapshot/parse.d.ts +14 -10
  110. package/dist/snapshot/parse.js +32 -17
  111. package/dist/transcribe/parse.d.cts +6 -6
  112. package/dist/transcribe/parse.d.ts +6 -6
  113. package/dist/transcribe/transcribe.cjs +4 -2
  114. package/dist/transcribe/transcribe.js +4 -2
  115. package/package.json +6 -4
  116. package/dist/process/mime.d.cts +0 -3
  117. package/dist/process/mime.d.ts +0 -3
  118. package/dist/process/mime.js +0 -24
@@ -0,0 +1,800 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var Aligner_exports = {};
30
+ __export(Aligner_exports, {
31
+ Aligner: () => Aligner
32
+ });
33
+ module.exports = __toCommonJS(Aligner_exports);
34
+ var import_promises = require("node:fs/promises");
35
+ var import_posix = require("node:path/posix");
36
+ var import_itertools = require("itertools");
37
+ var import_memoize = __toESM(require("memoize"), 1);
38
+ var import_epub = require("@storyteller-platform/epub");
39
+ var import_ghost_story = require("@storyteller-platform/ghost-story");
40
+ var import_media_types = require("@storyteller-platform/media-types");
41
+ var import_parseDom = require("../../markup/parseDom.cjs");
42
+ var import_segmentation = require("../../markup/segmentation.cjs");
43
+ var import_transform = require("../../markup/transform.cjs");
44
+ var import_getSentenceRanges = require("../getSentenceRanges.cjs");
45
+ var import_interpolateSentenceRanges = require("../interpolateSentenceRanges.cjs");
46
+ var import_slugify = require("../slugify.cjs");
47
+ var import_emissions = require("./emissions.cjs");
48
+ var import_forcedAlign = require("./forcedAlign.cjs");
49
+ var import_mediaOverlay = require("./mediaOverlay.cjs");
50
+ var import_report = require("./report.cjs");
51
+ var import_search = require("./search.cjs");
52
+ const MAX_SEARCH_LENGTH = 4e4;
53
+ class Aligner {
54
+ constructor(epub, audiofiles, emissionsReader, granularity, textRef, languageOverride, logger) {
55
+ this.epub = epub;
56
+ this.audiofiles = audiofiles;
57
+ this.emissionsReader = emissionsReader;
58
+ this.languageOverride = languageOverride;
59
+ this.logger = logger;
60
+ this.audiofileFrames = /* @__PURE__ */ new Map();
61
+ this.audiofileEmissionTimings = /* @__PURE__ */ new Map();
62
+ for (const [i, audiofile] of (0, import_itertools.enumerate)(audiofiles)) {
63
+ const [, framesStart, framesEnd] = emissionsReader.frameMap[i];
64
+ const timings = emissionsReader.timings[i];
65
+ const length = framesEnd - framesStart;
66
+ this.audiofileFrames.set(audiofile, [framesStart, framesEnd]);
67
+ this.audioFileDurations[audiofile] = (0, import_emissions.framesToSeconds)(length, timings);
68
+ this.audiofileEmissionTimings.set(audiofile, timings);
69
+ }
70
+ this.getChapterSentences = (0, import_memoize.default)(this.getChapterSentences.bind(this));
71
+ this.granularity = granularity ?? "sentence";
72
+ this.textRef = textRef ?? "id-fragment";
73
+ }
74
+ audiofileFrames;
75
+ audiofileEmissionTimings;
76
+ totalDuration = 0;
77
+ alignedChapters = [];
78
+ timing = (0, import_ghost_story.createAggregator)();
79
+ granularity;
80
+ textRef;
81
+ audioFileDurations = {};
82
+ report = {
83
+ chapters: [],
84
+ unalignedChapters: [],
85
+ audioFiles: [],
86
+ unalignedAudioFiles: []
87
+ };
88
+ async getChapterSentences(chapterId) {
89
+ const chapterXml = await this.epub.readXhtmlItemContents(chapterId);
90
+ const original = (0, import_parseDom.parseDom)(import_epub.Epub.getXhtmlBody(chapterXml));
91
+ const inlined = (0, import_transform.inlineFootnotes)(original);
92
+ const lifted = (0, import_transform.liftText)(inlined.root);
93
+ const segmentation = await (0, import_segmentation.segmentChapter)(lifted.result, {
94
+ primaryLocale: this.languageOverride ?? await this.epub.getLanguage()
95
+ });
96
+ return segmentation.filter((s) => s.text.match(/\S/));
97
+ }
98
+ async writeAlignedChapter(alignedChapter) {
99
+ const locale = this.languageOverride ?? await this.epub.getLanguage() ?? new Intl.Locale("en-US");
100
+ const { chapter, sentenceRanges, wordRanges, xml } = alignedChapter;
101
+ const sentences = await this.getChapterSentences(chapter.id);
102
+ const {
103
+ sentenceIdToBlockFragment,
104
+ sentenceIdToFragment,
105
+ wordIdToFragment,
106
+ wordRangeMap
107
+ } = (0, import_mediaOverlay.buildFragmentMaps)(
108
+ sentenceRanges,
109
+ wordRanges,
110
+ sentences,
111
+ locale,
112
+ this.textRef,
113
+ this.granularity
114
+ );
115
+ await this.addChapterAudio(sentenceRanges);
116
+ const mediaOverlayId = await this.addMediaOverlay(
117
+ chapter,
118
+ sentenceRanges,
119
+ wordRangeMap,
120
+ sentenceIdToBlockFragment,
121
+ sentenceIdToFragment,
122
+ wordIdToFragment
123
+ );
124
+ await this.epub.updateManifestItem(chapter.id, {
125
+ ...chapter,
126
+ mediaOverlay: mediaOverlayId
127
+ });
128
+ await this.epub.writeXhtmlItemContents(chapter.id, xml);
129
+ const chapterDuration = (0, import_getSentenceRanges.getChapterDuration)(sentenceRanges);
130
+ await this.epub.addMetadata({
131
+ type: "meta",
132
+ properties: {
133
+ property: "media:duration",
134
+ refines: `#${mediaOverlayId}`
135
+ },
136
+ value: import_epub.Epub.formatSmilDuration(chapterDuration)
137
+ });
138
+ }
139
+ async addChapterAudio(sentenceRanges) {
140
+ const audiofiles = Array.from(
141
+ new Set(sentenceRanges.map(({ audiofile }) => audiofile))
142
+ );
143
+ await Promise.all(
144
+ audiofiles.map(async (audiofile) => {
145
+ const { name, base } = (0, import_posix.parse)(audiofile);
146
+ const id = `audio_${name}`;
147
+ const manifest = await this.epub.getManifest();
148
+ if (id in manifest) return;
149
+ const epubAudioFilename = `Audio/${base}`;
150
+ const duration = this.audioFileDurations[audiofile];
151
+ this.totalDuration += duration;
152
+ const audio = await (0, import_promises.readFile)(audiofile);
153
+ const mediaType = import_media_types.MediaType.fromPath(base)?.mime;
154
+ await this.epub.addManifestItem(
155
+ {
156
+ id,
157
+ href: epubAudioFilename,
158
+ mediaType
159
+ },
160
+ audio
161
+ );
162
+ })
163
+ );
164
+ }
165
+ async addMediaOverlay(chapter, sentenceRanges, wordRangeMap, sentenceIdToBlockFragment, sentenceIdToFragment, wordIdToFragment) {
166
+ const { name: chapterStem } = (0, import_posix.parse)(chapter.href);
167
+ const mediaOverlayId = `${chapter.id}_overlay`;
168
+ await this.epub.addManifestItem(
169
+ {
170
+ id: mediaOverlayId,
171
+ href: `MediaOverlays/${chapterStem}.smil`,
172
+ mediaType: "application/smil+xml"
173
+ },
174
+ (0, import_mediaOverlay.createMediaOverlay)(
175
+ chapter,
176
+ this.granularity,
177
+ sentenceRanges,
178
+ wordRangeMap,
179
+ sentenceIdToBlockFragment,
180
+ sentenceIdToFragment,
181
+ wordIdToFragment
182
+ ),
183
+ "xml"
184
+ );
185
+ return mediaOverlayId;
186
+ }
187
+ addChapterReport(spineIndex, chapter, chunk, score, chapterSentences, sentenceCount, sentenceRanges, startSentence, endSentence, alignedEmissionsStart, alignedEmissionsEnd) {
188
+ const audioFiles = (0, import_report.buildAudioFileContexts)(sentenceRanges);
189
+ this.report.chapters.push({
190
+ href: chapter.href,
191
+ spineIndex,
192
+ chunk,
193
+ score,
194
+ transcriptionOffset: alignedEmissionsStart,
195
+ endTranscriptionOffset: alignedEmissionsEnd,
196
+ firstMatchedSentenceId: startSentence,
197
+ firstMatchedSentenceContext: {
198
+ prevSentence: chapterSentences[startSentence - 1]?.text ?? null,
199
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
200
+ matchedSentence: chapterSentences[startSentence].text,
201
+ nextSentence: chapterSentences[startSentence + 1]?.text ?? null
202
+ },
203
+ lastMatchedSentenceId: endSentence,
204
+ lastMatchedSentenceContext: {
205
+ prevSentence: chapterSentences[endSentence - 1]?.text ?? null,
206
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
207
+ matchedSentence: chapterSentences[endSentence].text,
208
+ nextSentence: chapterSentences[endSentence + 1]?.text ?? null
209
+ },
210
+ chapterSentenceCount: sentenceCount,
211
+ alignedSentenceCount: sentenceRanges.length,
212
+ audioFiles
213
+ });
214
+ (0, import_report.updateAudioFileReports)(this.report, audioFiles, this.audioFileDurations);
215
+ }
216
+ async getSentenceRanges(chapterId, slugifiedChapter, sentenceStarts, wordStarts, sentenceIndexOffset, emissionsStart, emissionsEnd, anchors, paddedChapterBoundaries, unpaddedChapterBoundaries) {
217
+ const SENTENCE_PAD = 25;
218
+ const WORD_PAD = 8;
219
+ const windowAnchors = anchors.filter(
220
+ (a) => a.position > emissionsStart && a.position < emissionsEnd && a.position - emissionsStart >= a.offset && emissionsEnd - a.position >= slugifiedChapter.length - a.offset
221
+ ).map((a) => ({ ...a, position: a.position - emissionsStart }));
222
+ const alignments = (0, import_forcedAlign.ctcForcedAlign)(
223
+ slugifiedChapter,
224
+ await this.emissionsReader.readFrames(emissionsStart, emissionsEnd),
225
+ windowAnchors,
226
+ this.emissionsReader.vocab,
227
+ this.emissionsReader.blankId,
228
+ (labelOffset, alignments2) => sentenceStarts.filter(
229
+ ([s, e]) => s >= labelOffset && e < labelOffset + alignments2.length && (0, import_forcedAlign.detectDroppedLabels)(alignments2, s - labelOffset, e - labelOffset)
230
+ )
231
+ );
232
+ const finiteScores = alignments.filter(({ score: score2 }) => score2 !== -Infinity);
233
+ const unscaledScore = finiteScores.reduce((acc, { score: score2 }) => acc + score2, 0) / finiteScores.length;
234
+ const score = finiteScores.length > 0 ? Math.max((unscaledScore + 10) * 10, 0) : 0;
235
+ const {
236
+ sentenceRanges,
237
+ skippedSentences,
238
+ firstFoundSentence,
239
+ lastFoundSentence,
240
+ firstConfirmedSentence,
241
+ lastConfirmedSentence
242
+ } = this.buildSentenceRanges(
243
+ alignments,
244
+ sentenceStarts,
245
+ sentenceIndexOffset,
246
+ emissionsStart,
247
+ SENTENCE_PAD,
248
+ chapterId,
249
+ paddedChapterBoundaries,
250
+ unpaddedChapterBoundaries
251
+ );
252
+ const wordRanges = this.buildWordRanges(
253
+ alignments,
254
+ wordStarts,
255
+ sentenceIndexOffset,
256
+ emissionsStart,
257
+ WORD_PAD,
258
+ skippedSentences,
259
+ chapterId
260
+ );
261
+ if (firstFoundSentence === null || firstConfirmedSentence === null) {
262
+ return {
263
+ score,
264
+ sentenceRanges: [],
265
+ wordRanges: [],
266
+ alignedEmissionsStart: emissionsStart,
267
+ alignedEmissionsEnd: emissionsStart,
268
+ firstFoundSentence: sentenceIndexOffset,
269
+ lastFoundSentence: sentenceIndexOffset
270
+ };
271
+ }
272
+ return {
273
+ score,
274
+ sentenceRanges,
275
+ wordRanges,
276
+ alignedEmissionsStart: alignments[sentenceStarts[firstConfirmedSentence][0]].startFrame + emissionsStart,
277
+ alignedEmissionsEnd: alignments[sentenceStarts[lastConfirmedSentence][1]].endFrame + emissionsStart,
278
+ firstFoundSentence: sentenceIndexOffset + firstFoundSentence,
279
+ lastFoundSentence: sentenceIndexOffset + lastFoundSentence
280
+ };
281
+ }
282
+ buildSentenceRanges(alignments, sentenceStarts, sentenceIndexOffset, emissionsStart, padFrames, chapterId, paddedChapterBoundaries, unpaddedChapterBoundaries) {
283
+ const sentenceRanges = [];
284
+ const skippedSentences = /* @__PURE__ */ new Set();
285
+ let firstFoundSentence = null;
286
+ let lastFoundSentence = 0;
287
+ let lastFoundSentenceEnd = 0;
288
+ let firstConfirmedSentence = null;
289
+ let lastConfirmedSentence = 0;
290
+ for (const [i, [startOffset, endOffset]] of (0, import_itertools.enumerate)(sentenceStarts)) {
291
+ if (endOffset < startOffset) {
292
+ skippedSentences.add(i);
293
+ continue;
294
+ }
295
+ if (endOffset < paddedChapterBoundaries[0] || startOffset >= paddedChapterBoundaries[1]) {
296
+ skippedSentences.add(i);
297
+ continue;
298
+ }
299
+ const goodChars = alignments.slice(startOffset, endOffset + 1).reduce((acc, a) => acc + (a.score > -4 ? 1 : 0), 0);
300
+ if (goodChars / (endOffset + 1 - startOffset) < 0.5) {
301
+ skippedSentences.add(i);
302
+ continue;
303
+ }
304
+ const sentenceLength = endOffset + 1 - startOffset;
305
+ const framesPerChar = (alignments[endOffset].endFrame - alignments[startOffset].startFrame) / sentenceLength;
306
+ if (sentenceLength > 8 && (framesPerChar < 2 || framesPerChar > 15)) {
307
+ skippedSentences.add(i);
308
+ continue;
309
+ }
310
+ const startFrame = this.refineStartFrame(
311
+ alignments[startOffset].startFrame,
312
+ lastFoundSentenceEnd,
313
+ padFrames,
314
+ emissionsStart
315
+ );
316
+ firstFoundSentence ??= i;
317
+ lastFoundSentence = i;
318
+ lastFoundSentenceEnd = alignments[endOffset].endFrame;
319
+ const { position: start, audiofile } = this.frameToAudioPosition(
320
+ startFrame,
321
+ emissionsStart
322
+ );
323
+ const { position: end } = this.frameToAudioPosition(
324
+ alignments[endOffset].endFrame,
325
+ emissionsStart
326
+ );
327
+ sentenceRanges.push({
328
+ id: sentenceIndexOffset + i,
329
+ chapterId,
330
+ start,
331
+ end,
332
+ audiofile
333
+ });
334
+ if (startOffset <= unpaddedChapterBoundaries[1] && endOffset >= unpaddedChapterBoundaries[0]) {
335
+ firstConfirmedSentence ??= i;
336
+ lastConfirmedSentence = i;
337
+ }
338
+ }
339
+ return {
340
+ sentenceRanges,
341
+ skippedSentences,
342
+ firstFoundSentence,
343
+ lastFoundSentence,
344
+ lastFoundSentenceEnd,
345
+ firstConfirmedSentence,
346
+ lastConfirmedSentence
347
+ };
348
+ }
349
+ buildWordRanges(alignments, wordStarts, sentenceIndexOffset, emissionsStart, padFrames, skippedSentences, chapterId) {
350
+ const wordRanges = [];
351
+ let lastFoundWordEnd = 0;
352
+ for (const [i, ws] of (0, import_itertools.enumerate)(wordStarts)) {
353
+ if (skippedSentences.has(i)) continue;
354
+ const ranges = [];
355
+ for (const [j, [startOffset, endOffset]] of (0, import_itertools.enumerate)(ws)) {
356
+ if (endOffset < startOffset) continue;
357
+ const goodChars = alignments.slice(startOffset, endOffset + 1).reduce((acc, a) => acc + (a.score > -4 ? 1 : 0), 0);
358
+ if (goodChars / (endOffset + 1 - startOffset) < 0.5) {
359
+ continue;
360
+ }
361
+ const startFrame = this.refineStartFrame(
362
+ alignments[startOffset].startFrame,
363
+ lastFoundWordEnd,
364
+ padFrames,
365
+ emissionsStart
366
+ );
367
+ lastFoundWordEnd = alignments[endOffset].endFrame;
368
+ const { position: start, audiofile } = this.frameToAudioPosition(
369
+ startFrame,
370
+ emissionsStart
371
+ );
372
+ const { position: end } = this.frameToAudioPosition(
373
+ alignments[endOffset].endFrame,
374
+ emissionsStart
375
+ );
376
+ ranges.push({
377
+ id: j,
378
+ sentenceId: sentenceIndexOffset + i,
379
+ chapterId,
380
+ start,
381
+ end,
382
+ audiofile
383
+ });
384
+ }
385
+ if (ranges.length) wordRanges.push(ranges);
386
+ }
387
+ return wordRanges;
388
+ }
389
+ frameToAudioPosition(frame, emissionsStart) {
390
+ const globalFrame = frame + emissionsStart;
391
+ for (const [audiofile, [fileStart, fileEnd]] of this.audiofileFrames) {
392
+ if (globalFrame < fileEnd) {
393
+ return {
394
+ position: (0, import_emissions.framesToSeconds)(
395
+ globalFrame - fileStart,
396
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
397
+ this.audiofileEmissionTimings.get(audiofile)
398
+ ),
399
+ audiofile
400
+ };
401
+ }
402
+ }
403
+ throw new Error(`Frame ${globalFrame} is beyond the end of the emissions`);
404
+ }
405
+ refineStartFrame(startFrame, prevEndFrame, padFrames, emissionsStart) {
406
+ const { audiofile } = this.frameToAudioPosition(startFrame, emissionsStart);
407
+ const [start] = this.audiofileFrames.get(audiofile);
408
+ const windowFloor = start - emissionsStart;
409
+ return Math.max(prevEndFrame + 1, startFrame - padFrames, windowFloor);
410
+ }
411
+ async alignChapter(spineIndex, chapterId, chunk, sentenceIndexOffset, chapterSentences, slugifiedChapter, sentenceStarts, wordStarts, emissionsStart, emissionsEnd, anchors, paddedChapterBoundaries, unpaddedChapterBoundaries) {
412
+ const timing = (0, import_ghost_story.createTiming)();
413
+ timing.start("read contents");
414
+ const manifest = await this.epub.getManifest();
415
+ const chapter = manifest[chapterId];
416
+ if (!chapter)
417
+ throw new Error(
418
+ `Failed to align chapter: could not find chapter with id ${chapterId} in manifest`
419
+ );
420
+ const chapterXml = await this.epub.readXhtmlItemContents(chapterId);
421
+ timing.start("align sentences");
422
+ const {
423
+ score,
424
+ sentenceRanges,
425
+ wordRanges,
426
+ alignedEmissionsStart,
427
+ alignedEmissionsEnd,
428
+ firstFoundSentence,
429
+ lastFoundSentence
430
+ } = await this.getSentenceRanges(
431
+ chapterId,
432
+ slugifiedChapter,
433
+ sentenceStarts,
434
+ wordStarts,
435
+ sentenceIndexOffset,
436
+ emissionsStart,
437
+ emissionsEnd,
438
+ anchors,
439
+ paddedChapterBoundaries,
440
+ unpaddedChapterBoundaries
441
+ );
442
+ timing.end("align sentences");
443
+ const storytellerStylesheetUrl = (0, import_posix.relative)(
444
+ (0, import_posix.dirname)(chapter.href),
445
+ "Styles/storyteller-readaloud.css"
446
+ );
447
+ import_epub.Epub.addLinkToXhtmlHead(chapterXml, {
448
+ rel: "stylesheet",
449
+ href: storytellerStylesheetUrl,
450
+ type: "text/css"
451
+ });
452
+ this.alignedChapters.push({
453
+ chapter,
454
+ chunk,
455
+ xml: chapterXml,
456
+ sentenceRanges,
457
+ wordRanges,
458
+ startOffset: alignedEmissionsStart,
459
+ endOffset: alignedEmissionsEnd
460
+ });
461
+ this.addChapterReport(
462
+ spineIndex,
463
+ chapter,
464
+ chunk,
465
+ score,
466
+ chapterSentences,
467
+ sentenceStarts.length,
468
+ sentenceRanges,
469
+ firstFoundSentence,
470
+ lastFoundSentence,
471
+ alignedEmissionsStart,
472
+ alignedEmissionsEnd
473
+ );
474
+ return {
475
+ lastSentenceRange: sentenceRanges.at(-1) ?? null,
476
+ alignedEmissionsEnd,
477
+ timing
478
+ };
479
+ }
480
+ getMatchedBoundaries() {
481
+ return this.alignedChapters.toSorted((a, b) => a.startOffset - b.startOffset).map(({ startOffset, endOffset }) => [startOffset, endOffset]);
482
+ }
483
+ narrowToAvailableBoundary(boundary) {
484
+ const available = [
485
+ -1,
486
+ ...this.getMatchedBoundaries().flat(),
487
+ Infinity
488
+ ];
489
+ const withinBoundary = [];
490
+ for (let i = 0; i < available.length - 1; i += 2) {
491
+ const [start, end] = [available[i], available[i + 1]];
492
+ if (boundary.start <= start && boundary.end >= start || boundary.start <= end && boundary.end >= end) {
493
+ withinBoundary.push([
494
+ Math.max(boundary.start, start + 1),
495
+ Math.min(boundary.end, end - 1)
496
+ ]);
497
+ }
498
+ }
499
+ const largestBoundary = (0, import_itertools.max)(withinBoundary, ([start, end]) => end - start);
500
+ if (!largestBoundary) return { start: boundary.start, end: boundary.end };
501
+ return { start: largestBoundary[0], end: largestBoundary[1] };
502
+ }
503
+ async alignBook(onProgress) {
504
+ const { locale } = await this.prepareAlignment();
505
+ await this.processSpine(onProgress, locale);
506
+ await this.finalizeAlignment();
507
+ return this.timing;
508
+ }
509
+ async prepareAlignment() {
510
+ const locale = this.languageOverride ?? await this.epub.getLanguage() ?? new Intl.Locale("en-US");
511
+ this.timing.setMetadata("language", locale.toString());
512
+ this.timing.setMetadata("granularity", this.granularity);
513
+ return { locale };
514
+ }
515
+ async processSpine(onProgress, locale) {
516
+ const spine = await this.epub.getSpineItems();
517
+ const manifest = await this.epub.getManifest();
518
+ for (let index = 0; index < spine.length; index++) {
519
+ onProgress?.(index / spine.length);
520
+ const spineItem = spine[index];
521
+ this.logger?.info(
522
+ `Aligning epub item #${index} : ${(0, import_posix.basename)(spineItem.href)}`
523
+ );
524
+ const results = await this.processSpineItem(
525
+ index,
526
+ spineItem,
527
+ manifest,
528
+ locale
529
+ );
530
+ results.forEach((result) => this.timing.add(result.timing.summary()));
531
+ }
532
+ }
533
+ async processSpineItem(index, spineItem, manifest, locale) {
534
+ const chapterId = spineItem.id;
535
+ if (manifest[chapterId]?.properties?.includes("nav")) {
536
+ this.report.unalignedChapters.push({
537
+ href: spineItem.href,
538
+ reason: "is-nav"
539
+ });
540
+ return [];
541
+ }
542
+ const chapterSentences = await this.getChapterSentences(chapterId);
543
+ const slugifiedChapterSentences = [];
544
+ const chapterMappings = [];
545
+ let slugifiedChapterLength = 0;
546
+ for (const chapterSentence of chapterSentences) {
547
+ const { result, mapping } = await (0, import_slugify.slugify)(
548
+ chapterSentence.text,
549
+ locale,
550
+ ""
551
+ );
552
+ slugifiedChapterSentences.push(result);
553
+ slugifiedChapterLength += result.length;
554
+ chapterMappings.push(mapping);
555
+ }
556
+ if (chapterSentences.length === 0 || slugifiedChapterLength === 0) {
557
+ this.logger?.info(`Chapter #${index} has no text; skipping`);
558
+ this.report.unalignedChapters.push({
559
+ href: spineItem.href,
560
+ reason: "no-text"
561
+ });
562
+ return [];
563
+ }
564
+ if (chapterSentences.length < 2 && // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
565
+ chapterSentences[0].words.length < 4) {
566
+ this.logger?.info(`Chapter #${index} is fewer than four words; skipping`);
567
+ this.report.unalignedChapters.push({
568
+ href: spineItem.href,
569
+ reason: "too-short"
570
+ });
571
+ return [];
572
+ }
573
+ const boundaries = await (0, import_search.findCtcBoundaries)(
574
+ slugifiedChapterSentences.join(""),
575
+ this.emissionsReader,
576
+ this.getMatchedBoundaries()
577
+ );
578
+ const evidenceRange = boundaries?.evidenceRange;
579
+ const matchedRange = boundaries?.matchedRange;
580
+ const chunkCount = Math.ceil(slugifiedChapterLength / MAX_SEARCH_LENGTH);
581
+ const chunkLength = Math.ceil(slugifiedChapterLength / chunkCount);
582
+ const chunks = [
583
+ { firstSentence: 0, sentences: [] }
584
+ ];
585
+ let c = 0;
586
+ for (const [i, s] of (0, import_itertools.enumerate)(slugifiedChapterSentences)) {
587
+ if (c >= chunkLength * chunks.length && // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
588
+ chunks.at(-1).sentences.length > 0) {
589
+ chunks.push({ firstSentence: i, sentences: [] });
590
+ }
591
+ chunks.at(-1).sentences.push(s);
592
+ c += s.length;
593
+ }
594
+ if (chunks.length > 1) {
595
+ this.logger?.info(
596
+ `Chapter #${index} is ${slugifiedChapterLength} chars; aligning in ${chunks.length} chunks`
597
+ );
598
+ }
599
+ const results = [];
600
+ const firstAlignedChapterIndex = this.alignedChapters.length;
601
+ for (const [
602
+ chunkIndex,
603
+ { firstSentence, sentences: chunkSentences }
604
+ ] of (0, import_itertools.enumerate)(chunks)) {
605
+ let pos = 0;
606
+ const slugifiedChapterRanges = [];
607
+ for (const sent of chunkSentences) {
608
+ slugifiedChapterRanges.push([pos, pos + sent.length - 1]);
609
+ pos += sent.length;
610
+ }
611
+ const slugifiedWordStarts = chunkSentences.map((_, i) => {
612
+ const sent = chapterSentences[firstSentence + i];
613
+ const cursor = chapterMappings[firstSentence + i].cursor();
614
+ return sent.words.entries.filter((word) => !word.isPunctuation).map((word) => [
615
+ cursor.map(word.startOffset - sent.charRange.start) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
616
+ slugifiedChapterRanges[i][0],
617
+ cursor.map(word.endOffset - 1 - sent.charRange.start) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
618
+ slugifiedChapterRanges[i][0]
619
+ ]);
620
+ });
621
+ const chunkString = chunkSentences.join("");
622
+ const boundaries2 = await (0, import_search.findCtcBoundaries)(
623
+ chunkString,
624
+ this.emissionsReader,
625
+ this.getMatchedBoundaries()
626
+ );
627
+ if (!boundaries2) {
628
+ this.logger?.info(
629
+ `Could not find chapter #${index} in the transcripton`
630
+ );
631
+ this.report.unalignedChapters.push({
632
+ href: spineItem.href,
633
+ reason: "not-found",
634
+ spineIndex: index,
635
+ score: 0,
636
+ chapterSentenceCount: chunkSentences.length,
637
+ start: chapterSentences.slice(firstSentence, firstSentence + 3).map((s) => s.text).join("").slice(0, 80),
638
+ end: chapterSentences.slice(
639
+ Math.max(
640
+ firstSentence,
641
+ firstSentence + chunkSentences.length - 3
642
+ ),
643
+ firstSentence + chunkSentences.length
644
+ ).map((s) => s.text).join("").slice(-80)
645
+ });
646
+ continue;
647
+ }
648
+ const { start, end } = this.narrowToAvailableBoundary(boundaries2);
649
+ if (start === end) {
650
+ this.logger?.info(
651
+ `Could not find chapter #${index} in the transcripton`
652
+ );
653
+ this.report.unalignedChapters.push({
654
+ href: spineItem.href,
655
+ reason: "not-found",
656
+ spineIndex: index,
657
+ score: 0,
658
+ chapterSentenceCount: chunkSentences.length,
659
+ start: chapterSentences.slice(firstSentence, firstSentence + 3).map((s) => s.text).join("").slice(0, 80),
660
+ end: chapterSentences.slice(
661
+ Math.max(
662
+ firstSentence,
663
+ firstSentence + chunkSentences.length - 3
664
+ ),
665
+ firstSentence + chunkSentences.length
666
+ ).map((s) => s.text).join("").slice(-80)
667
+ });
668
+ continue;
669
+ }
670
+ const lastChunkSlugStart = slugifiedChapterLength - chunkString.length;
671
+ const chunkEvidenceHead = chunkIndex === 0 ? evidenceRange?.[0] ?? boundaries2.evidenceRange[0] : boundaries2.evidenceRange[0];
672
+ const chunkEvidenceTail = chunkIndex === chunks.length - 1 && evidenceRange ? evidenceRange[1] - lastChunkSlugStart : boundaries2.evidenceRange[1];
673
+ const chunkMatchedHead = chunkIndex === 0 ? matchedRange?.[0] ?? boundaries2.matchedRange[0] : boundaries2.matchedRange[0];
674
+ const chunkMatchedTail = chunkIndex === chunks.length - 1 && matchedRange ? matchedRange[1] - lastChunkSlugStart : boundaries2.matchedRange[1];
675
+ const result = await this.alignChapter(
676
+ index,
677
+ chapterId,
678
+ chunkIndex,
679
+ firstSentence,
680
+ chapterSentences,
681
+ chunkString,
682
+ slugifiedChapterRanges,
683
+ slugifiedWordStarts,
684
+ start,
685
+ end,
686
+ boundaries2.anchors,
687
+ [chunkEvidenceHead, chunkEvidenceTail],
688
+ [chunkMatchedHead, chunkMatchedTail]
689
+ );
690
+ results.push(result);
691
+ }
692
+ const chunkEntries = this.alignedChapters.slice(firstAlignedChapterIndex);
693
+ if (chunkEntries.length > 1) {
694
+ const claimed = chunkEntries.filter((e) => e.sentenceRanges.length > 0);
695
+ const offsetSources = claimed.length > 0 ? claimed : chunkEntries;
696
+ const first = chunkEntries[0];
697
+ this.alignedChapters.splice(
698
+ firstAlignedChapterIndex,
699
+ chunkEntries.length,
700
+ {
701
+ chapter: first.chapter,
702
+ chunk: 0,
703
+ xml: first.xml,
704
+ sentenceRanges: chunkEntries.flatMap((e) => e.sentenceRanges),
705
+ wordRanges: chunkEntries.flatMap((e) => e.wordRanges),
706
+ startOffset: Math.min(...offsetSources.map((e) => e.startOffset)),
707
+ endOffset: Math.max(...offsetSources.map((e) => e.endOffset))
708
+ }
709
+ );
710
+ }
711
+ return results;
712
+ }
713
+ async finalizeAlignment() {
714
+ const audioOrderedChapters = this.alignedChapters.toSorted((a, b) => {
715
+ const firstRangeA = a.sentenceRanges[0];
716
+ const firstRangeB = b.sentenceRanges[0];
717
+ if (!firstRangeA) return 1;
718
+ if (!firstRangeB) return -1;
719
+ const firstAudiofileIndexA = this.audiofiles.indexOf(
720
+ firstRangeA.audiofile
721
+ );
722
+ const firstAudiofileIndexB = this.audiofiles.indexOf(
723
+ firstRangeB.audiofile
724
+ );
725
+ if (firstAudiofileIndexA === firstAudiofileIndexB) {
726
+ return firstRangeA.start - firstRangeB.start;
727
+ }
728
+ return firstAudiofileIndexA - firstAudiofileIndexB;
729
+ });
730
+ const sentenceRanges = [];
731
+ const chapterSentenceCounts = {};
732
+ const sentenceLengths = {};
733
+ for (const alignedChapter of audioOrderedChapters) {
734
+ sentenceRanges.push(...alignedChapter.sentenceRanges);
735
+ const sentences = await this.getChapterSentences(
736
+ alignedChapter.chapter.id
737
+ );
738
+ chapterSentenceCounts[alignedChapter.chapter.id] = sentences.length;
739
+ for (const [id, sentence] of (0, import_itertools.enumerate)(sentences)) {
740
+ sentenceLengths[(0, import_interpolateSentenceRanges.slotKey)({ chapterId: alignedChapter.chapter.id, id })] = sentence.text.length;
741
+ }
742
+ }
743
+ const interpolated = (0, import_interpolateSentenceRanges.interpolateSentenceRanges)(
744
+ sentenceRanges,
745
+ chapterSentenceCounts,
746
+ this.audioFileDurations,
747
+ sentenceLengths
748
+ );
749
+ const expanded = (0, import_getSentenceRanges.expandEmptySentenceRanges)(interpolated);
750
+ const collapsed = await (0, import_getSentenceRanges.collapseSentenceRangeGaps)(expanded);
751
+ let collapsedStart = 0;
752
+ for (const alignedChapter of audioOrderedChapters) {
753
+ const sentences = await this.getChapterSentences(
754
+ alignedChapter.chapter.id
755
+ );
756
+ const finalSentenceRanges = collapsed.slice(
757
+ collapsedStart,
758
+ collapsedStart + sentences.length
759
+ );
760
+ alignedChapter.sentenceRanges = finalSentenceRanges;
761
+ for (const [i, wordRanges] of (0, import_itertools.enumerate)(alignedChapter.wordRanges)) {
762
+ alignedChapter.wordRanges[i] = (0, import_getSentenceRanges.expandEmptySentenceRanges)(wordRanges);
763
+ }
764
+ await this.writeAlignedChapter(alignedChapter);
765
+ collapsedStart += sentences.length;
766
+ }
767
+ for (const audiofile of this.audiofiles) {
768
+ if (!this.report.audioFiles.some(({ filepath }) => filepath === audiofile)) {
769
+ this.report.unalignedAudioFiles.push({ filepath: audiofile });
770
+ }
771
+ }
772
+ await this.epub.addMetadata({
773
+ type: "meta",
774
+ properties: { property: "media:duration" },
775
+ value: import_epub.Epub.formatSmilDuration(this.totalDuration)
776
+ });
777
+ await this.epub.addMetadata({
778
+ type: "meta",
779
+ properties: { property: "media:active-class" },
780
+ value: "-epub-media-overlay-active"
781
+ });
782
+ await this.epub.addManifestItem(
783
+ {
784
+ id: "storyteller_readaloud_styles",
785
+ href: "Styles/storyteller-readaloud.css",
786
+ mediaType: "text/css"
787
+ },
788
+ `
789
+ .-epub-media-overlay-active {
790
+ background-color: #ffb;
791
+ }
792
+ `,
793
+ "utf-8"
794
+ );
795
+ }
796
+ }
797
+ // Annotate the CommonJS export names for ESM import in node:
798
+ 0 && (module.exports = {
799
+ Aligner
800
+ });