@storyteller-platform/align 0.1.56 → 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.
- package/dist/align/align.cjs +38 -19
- package/dist/align/align.d.cts +12 -1
- package/dist/align/align.d.ts +12 -1
- package/dist/align/align.js +38 -19
- package/dist/align/ctc/Aligner.cjs +800 -0
- package/dist/align/ctc/Aligner.d.cts +56 -0
- package/dist/align/ctc/Aligner.d.ts +56 -0
- package/dist/align/ctc/Aligner.js +789 -0
- package/dist/align/ctc/align.cjs +189 -0
- package/dist/align/ctc/align.d.cts +14 -0
- package/dist/align/ctc/align.d.ts +14 -0
- package/dist/align/ctc/align.js +132 -0
- package/dist/{process/mime.cjs → align/ctc/emissions.cjs} +7 -20
- package/dist/align/ctc/emissions.d.cts +6 -0
- package/dist/align/ctc/emissions.d.ts +6 -0
- package/dist/align/ctc/emissions.js +7 -0
- package/dist/align/ctc/forcedAlign.cjs +273 -0
- package/dist/align/ctc/forcedAlign.d.cts +21 -0
- package/dist/align/ctc/forcedAlign.d.ts +21 -0
- package/dist/align/ctc/forcedAlign.js +248 -0
- package/dist/align/ctc/greedyDecode.cjs +61 -0
- package/dist/align/ctc/greedyDecode.d.cts +15 -0
- package/dist/align/ctc/greedyDecode.d.ts +15 -0
- package/dist/align/ctc/greedyDecode.js +37 -0
- package/dist/align/ctc/mediaOverlay.cjs +248 -0
- package/dist/align/ctc/mediaOverlay.d.cts +16 -0
- package/dist/align/ctc/mediaOverlay.d.ts +16 -0
- package/dist/align/ctc/mediaOverlay.js +224 -0
- package/dist/align/ctc/report.cjs +66 -0
- package/dist/align/ctc/report.d.cts +79 -0
- package/dist/align/ctc/report.d.ts +79 -0
- package/dist/align/ctc/report.js +42 -0
- package/dist/align/ctc/search.cjs +437 -0
- package/dist/align/ctc/search.d.cts +46 -0
- package/dist/align/ctc/search.d.ts +46 -0
- package/dist/align/ctc/search.js +412 -0
- package/dist/align/getSentenceRanges.cjs +9 -3
- package/dist/align/getSentenceRanges.d.cts +1 -0
- package/dist/align/getSentenceRanges.d.ts +1 -0
- package/dist/align/getSentenceRanges.js +9 -3
- package/dist/align/parse.cjs +17 -4
- package/dist/align/parse.d.cts +8 -4
- package/dist/align/parse.d.ts +8 -4
- package/dist/align/parse.js +19 -4
- package/dist/align/search.cjs +76 -23
- package/dist/align/search.d.cts +8 -4
- package/dist/align/search.d.ts +8 -4
- package/dist/align/search.js +76 -24
- package/dist/align/slugify.cjs +3 -2
- package/dist/align/slugify.d.cts +1 -1
- package/dist/align/slugify.d.ts +1 -1
- package/dist/align/slugify.js +3 -2
- package/dist/cli/bin.cjs +204 -114
- package/dist/cli/bin.js +194 -103
- package/dist/common/ffmpeg.cjs +10 -5
- package/dist/common/ffmpeg.js +10 -5
- package/dist/common/logging.cjs +33 -1
- package/dist/common/logging.js +33 -1
- package/dist/emit/emit.cjs +105 -0
- package/dist/emit/emit.d.cts +15 -0
- package/dist/emit/emit.d.ts +15 -0
- package/dist/emit/emit.js +86 -0
- package/dist/emit/fs.cjs +227 -0
- package/dist/emit/fs.d.cts +22 -0
- package/dist/emit/fs.d.ts +22 -0
- package/dist/emit/fs.js +162 -0
- package/dist/emit/parse.cjs +68 -0
- package/dist/emit/parse.d.cts +26 -0
- package/dist/emit/parse.d.ts +26 -0
- package/dist/emit/parse.js +58 -0
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/markup/model.cjs +0 -16
- package/dist/markup/model.d.cts +1 -1
- package/dist/markup/model.d.ts +1 -1
- package/dist/markup/model.js +0 -12
- package/dist/markup/parseDom.cjs +13 -14
- package/dist/markup/parseDom.d.cts +2 -2
- package/dist/markup/parseDom.d.ts +2 -2
- package/dist/markup/parseDom.js +13 -18
- package/dist/markup/resolvedPos.d.cts +1 -1
- package/dist/markup/resolvedPos.d.ts +1 -1
- package/dist/markup/segmentation.cjs +3 -1
- package/dist/markup/segmentation.js +3 -1
- package/dist/markup/serializeDom.cjs +1 -1
- package/dist/markup/serializeDom.d.cts +1 -1
- package/dist/markup/serializeDom.d.ts +1 -1
- package/dist/markup/serializeDom.js +1 -1
- package/dist/markup/transform.cjs +5 -2
- package/dist/markup/transform.d.cts +1 -1
- package/dist/markup/transform.d.ts +1 -1
- package/dist/markup/transform.js +5 -4
- package/dist/{model-CZ2mMHJO.d.cts → model-CeOMDDgt.d.cts} +3 -11
- package/dist/{model-CZ2mMHJO.d.ts → model-CeOMDDgt.d.ts} +3 -11
- package/dist/process/processAudiobook.cjs +12 -23
- package/dist/process/processAudiobook.js +14 -30
- package/dist/process/ranges.cjs +2 -2
- package/dist/process/ranges.js +2 -2
- package/dist/readium/guidedNavigation.cjs +8 -8
- package/dist/readium/guidedNavigation.js +8 -8
- package/dist/readium/manifest.cjs +5 -4
- package/dist/readium/manifest.js +5 -4
- package/dist/snapshot/ctc/snapshot.cjs +261 -0
- package/dist/snapshot/ctc/snapshot.d.cts +6 -0
- package/dist/snapshot/ctc/snapshot.d.ts +6 -0
- package/dist/snapshot/ctc/snapshot.js +200 -0
- package/dist/snapshot/parse.cjs +29 -16
- package/dist/snapshot/parse.d.cts +14 -10
- package/dist/snapshot/parse.d.ts +14 -10
- package/dist/snapshot/parse.js +32 -17
- package/dist/transcribe/parse.d.cts +6 -6
- package/dist/transcribe/parse.d.ts +6 -6
- package/dist/transcribe/transcribe.cjs +4 -2
- package/dist/transcribe/transcribe.js +4 -2
- package/package.json +6 -4
- package/dist/process/mime.d.cts +0 -3
- package/dist/process/mime.d.ts +0 -3
- 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
|
+
});
|