@storyteller-platform/align 0.1.19 → 0.1.21
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 +121 -52
- package/dist/align/align.d.cts +2 -0
- package/dist/align/align.d.ts +2 -0
- package/dist/align/align.js +124 -53
- package/dist/align/getSentenceRanges.cjs +116 -68
- package/dist/align/getSentenceRanges.d.cts +35 -5
- package/dist/align/getSentenceRanges.d.ts +35 -5
- package/dist/align/getSentenceRanges.js +113 -67
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/markup/markup.cjs +18 -1
- package/dist/markup/markup.d.cts +1 -1
- package/dist/markup/markup.d.ts +1 -1
- package/dist/markup/markup.js +18 -1
- package/dist/markup/serializeDom.cjs +80 -53
- package/dist/markup/serializeDom.d.cts +3 -4
- package/dist/markup/serializeDom.d.ts +3 -4
- package/dist/markup/serializeDom.js +79 -51
- package/package.json +2 -2
package/dist/align/align.cjs
CHANGED
|
@@ -81,6 +81,7 @@ module.exports = __toCommonJS(align_exports);
|
|
|
81
81
|
var import_promises = require("node:fs/promises");
|
|
82
82
|
var import_node_path = require("node:path");
|
|
83
83
|
var import_posix = require("node:path/posix");
|
|
84
|
+
var import_itertools = require("itertools");
|
|
84
85
|
var import_memoize = __toESM(require("memoize"), 1);
|
|
85
86
|
var import_audiobook = require("@storyteller-platform/audiobook");
|
|
86
87
|
var import_epub = require("@storyteller-platform/epub");
|
|
@@ -173,10 +174,11 @@ class Aligner {
|
|
|
173
174
|
primaryLocale: this.languageOverride ?? await this.epub.getLanguage()
|
|
174
175
|
}
|
|
175
176
|
);
|
|
176
|
-
return segmentation.
|
|
177
|
+
return segmentation.filter((s) => s.text.match(/\S/));
|
|
177
178
|
}
|
|
178
179
|
async writeAlignedChapter(alignedChapter) {
|
|
179
|
-
const { chapter, sentenceRanges, xml } = alignedChapter;
|
|
180
|
+
const { chapter, sentenceRanges, wordRanges, xml } = alignedChapter;
|
|
181
|
+
const wordRangeMap = new Map(wordRanges.map((w) => [w[0].sentenceId, w]));
|
|
180
182
|
const audiofiles = Array.from(
|
|
181
183
|
new Set(sentenceRanges.map(({ audiofile }) => audiofile))
|
|
182
184
|
);
|
|
@@ -209,7 +211,12 @@ class Aligner {
|
|
|
209
211
|
href: `MediaOverlays/${chapterStem}.smil`,
|
|
210
212
|
mediaType: "application/smil+xml"
|
|
211
213
|
},
|
|
212
|
-
createMediaOverlay(
|
|
214
|
+
createMediaOverlay(
|
|
215
|
+
chapter,
|
|
216
|
+
this.granularity,
|
|
217
|
+
sentenceRanges,
|
|
218
|
+
wordRangeMap
|
|
219
|
+
),
|
|
213
220
|
"xml"
|
|
214
221
|
);
|
|
215
222
|
await this.epub.updateManifestItem(chapter.id, {
|
|
@@ -246,17 +253,17 @@ class Aligner {
|
|
|
246
253
|
},
|
|
247
254
|
firstMatchedSentenceId: startSentence,
|
|
248
255
|
firstMatchedSentenceContext: {
|
|
249
|
-
prevSentence: chapterSentences[startSentence - 1] ?? null,
|
|
256
|
+
prevSentence: chapterSentences[startSentence - 1]?.text ?? null,
|
|
250
257
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
251
|
-
matchedSentence: chapterSentences[startSentence],
|
|
252
|
-
nextSentence: chapterSentences[startSentence + 1] ?? null
|
|
258
|
+
matchedSentence: chapterSentences[startSentence].text,
|
|
259
|
+
nextSentence: chapterSentences[startSentence + 1]?.text ?? null
|
|
253
260
|
},
|
|
254
261
|
lastMatchedSentenceId: endSentence,
|
|
255
262
|
lastMatchedSentenceContext: {
|
|
256
|
-
prevSentence: chapterSentences[endSentence - 1] ?? null,
|
|
263
|
+
prevSentence: chapterSentences[endSentence - 1]?.text ?? null,
|
|
257
264
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
258
|
-
matchedSentence: chapterSentences[endSentence],
|
|
259
|
-
nextSentence: chapterSentences[endSentence + 1] ?? null
|
|
265
|
+
matchedSentence: chapterSentences[endSentence].text,
|
|
266
|
+
nextSentence: chapterSentences[endSentence + 1]?.text ?? null
|
|
260
267
|
},
|
|
261
268
|
chapterSentenceCount: chapterSentences.length,
|
|
262
269
|
alignedSentenceCount: sentenceRanges.length,
|
|
@@ -277,7 +284,7 @@ class Aligner {
|
|
|
277
284
|
}, [])
|
|
278
285
|
});
|
|
279
286
|
}
|
|
280
|
-
async alignChapter(chapterId, transcriptionOffset, transcriptionEndOffset, locale,
|
|
287
|
+
async alignChapter(chapterId, transcriptionText, transcriptionOffset, transcriptionEndOffset, locale, mappedTimeline) {
|
|
281
288
|
const timing = (0, import_ghost_story.createTiming)();
|
|
282
289
|
timing.start("read contents");
|
|
283
290
|
const manifest = await this.epub.getManifest();
|
|
@@ -294,14 +301,18 @@ class Aligner {
|
|
|
294
301
|
timing.start("align sentences");
|
|
295
302
|
const {
|
|
296
303
|
sentenceRanges,
|
|
304
|
+
wordRanges,
|
|
297
305
|
transcriptionOffset: endTranscriptionOffset,
|
|
298
306
|
firstFoundSentence,
|
|
299
307
|
lastFoundSentence
|
|
300
308
|
} = await (0, import_getSentenceRanges.getSentenceRanges)(
|
|
301
|
-
|
|
309
|
+
transcriptionText,
|
|
310
|
+
mappedTimeline,
|
|
302
311
|
chapterSentences,
|
|
312
|
+
chapterId,
|
|
303
313
|
transcriptionOffset,
|
|
304
314
|
transcriptionEndOffset,
|
|
315
|
+
this.granularity,
|
|
305
316
|
locale
|
|
306
317
|
);
|
|
307
318
|
timing.end("align sentences");
|
|
@@ -318,8 +329,9 @@ class Aligner {
|
|
|
318
329
|
chapter,
|
|
319
330
|
xml: chapterXml,
|
|
320
331
|
sentenceRanges,
|
|
321
|
-
|
|
322
|
-
|
|
332
|
+
wordRanges,
|
|
333
|
+
startOffset: transcriptionOffset,
|
|
334
|
+
endOffset: endTranscriptionOffset
|
|
323
335
|
});
|
|
324
336
|
this.addChapterReport(
|
|
325
337
|
chapter,
|
|
@@ -336,16 +348,24 @@ class Aligner {
|
|
|
336
348
|
};
|
|
337
349
|
}
|
|
338
350
|
narrowToAvailableBoundary(boundary) {
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
351
|
+
const available = [
|
|
352
|
+
-1,
|
|
353
|
+
...this.alignedChapters.toSorted((a, b) => a.startOffset - b.startOffset).flatMap(({ startOffset, endOffset }) => [startOffset, endOffset]),
|
|
354
|
+
Infinity
|
|
355
|
+
];
|
|
356
|
+
const withinBoundary = [];
|
|
357
|
+
for (let i = 0; i < available.length - 1; i += 2) {
|
|
358
|
+
const [start, end] = [available[i], available[i + 1]];
|
|
359
|
+
if (boundary.start <= start && boundary.end >= start || boundary.start <= end && boundary.end >= end) {
|
|
360
|
+
withinBoundary.push([
|
|
361
|
+
Math.max(boundary.start, start + 1),
|
|
362
|
+
Math.min(boundary.end, end - 1)
|
|
363
|
+
]);
|
|
346
364
|
}
|
|
347
365
|
}
|
|
348
|
-
|
|
366
|
+
const largestBoundary = (0, import_itertools.max)(withinBoundary, ([start, end]) => end - start);
|
|
367
|
+
if (!largestBoundary) return { start: boundary.start, end: boundary.end };
|
|
368
|
+
return { start: largestBoundary[0], end: largestBoundary[1] };
|
|
349
369
|
}
|
|
350
370
|
async alignBook(onProgress) {
|
|
351
371
|
const locale = this.languageOverride ?? await this.epub.getLanguage() ?? new Intl.Locale("en-US");
|
|
@@ -357,6 +377,7 @@ class Aligner {
|
|
|
357
377
|
this.transcription.transcript,
|
|
358
378
|
locale
|
|
359
379
|
);
|
|
380
|
+
const mappedTimeline = (0, import_getSentenceRanges.mapTranscriptionTimeline)(this.transcription, mapping);
|
|
360
381
|
for (let index = 0; index < spine.length; index++) {
|
|
361
382
|
onProgress?.(index / spine.length);
|
|
362
383
|
const spineItem = spine[index];
|
|
@@ -371,7 +392,7 @@ class Aligner {
|
|
|
371
392
|
const slugifiedChapterSentences = [];
|
|
372
393
|
for (const chapterSentence of chapterSentences) {
|
|
373
394
|
slugifiedChapterSentences.push(
|
|
374
|
-
(await (0, import_slugify.slugify)(chapterSentence, locale)).result
|
|
395
|
+
(await (0, import_slugify.slugify)(chapterSentence.text, locale)).result
|
|
375
396
|
);
|
|
376
397
|
}
|
|
377
398
|
if (chapterSentences.length === 0) {
|
|
@@ -379,7 +400,7 @@ class Aligner {
|
|
|
379
400
|
continue;
|
|
380
401
|
}
|
|
381
402
|
if (chapterSentences.length < 2 && // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
382
|
-
chapterSentences[0].
|
|
403
|
+
chapterSentences[0].words.length < 4) {
|
|
383
404
|
this.logger?.info(
|
|
384
405
|
`Chapter #${index} is fewer than four words; skipping`
|
|
385
406
|
);
|
|
@@ -399,14 +420,13 @@ class Aligner {
|
|
|
399
420
|
if (start === end) {
|
|
400
421
|
continue;
|
|
401
422
|
}
|
|
402
|
-
const transcriptionOffset = mapping.invert().map(Math.max(start, 0), -1);
|
|
403
|
-
const endOffset = mapping.invert().map(Math.min(end, transcriptionText.length), 1);
|
|
404
423
|
const result = await this.alignChapter(
|
|
405
424
|
chapterId,
|
|
406
|
-
|
|
407
|
-
|
|
425
|
+
transcriptionText,
|
|
426
|
+
Math.max(start, 0),
|
|
427
|
+
Math.min(end, transcriptionText.length),
|
|
408
428
|
locale,
|
|
409
|
-
|
|
429
|
+
mappedTimeline
|
|
410
430
|
);
|
|
411
431
|
this.timing.add(result.timing.summary());
|
|
412
432
|
}
|
|
@@ -426,16 +446,36 @@ class Aligner {
|
|
|
426
446
|
}
|
|
427
447
|
return firstAudiofileIndexA - firstAudiofileIndexB;
|
|
428
448
|
});
|
|
429
|
-
|
|
449
|
+
const sentenceRanges = [];
|
|
450
|
+
const chapterSentenceCounts = {};
|
|
451
|
+
for (const alignedChapter of audioOrderedChapters) {
|
|
452
|
+
sentenceRanges.push(...alignedChapter.sentenceRanges);
|
|
453
|
+
const sentences = await this.getChapterSentences(
|
|
454
|
+
alignedChapter.chapter.id
|
|
455
|
+
);
|
|
456
|
+
chapterSentenceCounts[alignedChapter.chapter.id] = sentences.length;
|
|
457
|
+
}
|
|
458
|
+
const interpolated = await (0, import_getSentenceRanges.interpolateSentenceRanges)(
|
|
459
|
+
sentenceRanges,
|
|
460
|
+
chapterSentenceCounts
|
|
461
|
+
);
|
|
462
|
+
const expanded = (0, import_getSentenceRanges.expandEmptySentenceRanges)(interpolated);
|
|
463
|
+
const collapsed = await (0, import_getSentenceRanges.collapseSentenceRangeGaps)(expanded);
|
|
464
|
+
let collapsedStart = 0;
|
|
430
465
|
for (const alignedChapter of audioOrderedChapters) {
|
|
431
|
-
const
|
|
432
|
-
alignedChapter.
|
|
433
|
-
lastSentenceRange
|
|
466
|
+
const sentences = await this.getChapterSentences(
|
|
467
|
+
alignedChapter.chapter.id
|
|
434
468
|
);
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
469
|
+
const finalSentenceRanges = collapsed.slice(
|
|
470
|
+
collapsedStart,
|
|
471
|
+
collapsedStart + sentences.length - 1
|
|
472
|
+
);
|
|
473
|
+
alignedChapter.sentenceRanges = finalSentenceRanges;
|
|
474
|
+
for (const [i, wordRanges] of (0, import_itertools.enumerate)(alignedChapter.wordRanges)) {
|
|
475
|
+
alignedChapter.wordRanges[i] = (0, import_getSentenceRanges.expandEmptySentenceRanges)(wordRanges);
|
|
476
|
+
}
|
|
438
477
|
await this.writeAlignedChapter(alignedChapter);
|
|
478
|
+
collapsedStart += sentences.length - 1;
|
|
439
479
|
}
|
|
440
480
|
await this.epub.addMetadata({
|
|
441
481
|
type: "meta",
|
|
@@ -463,7 +503,7 @@ class Aligner {
|
|
|
463
503
|
return this.timing;
|
|
464
504
|
}
|
|
465
505
|
}
|
|
466
|
-
function createMediaOverlay(chapter, sentenceRanges) {
|
|
506
|
+
function createMediaOverlay(chapter, granularity, sentenceRanges, wordRanges) {
|
|
467
507
|
return [
|
|
468
508
|
import_epub.Epub.createXmlElement(
|
|
469
509
|
"smil",
|
|
@@ -481,24 +521,53 @@ function createMediaOverlay(chapter, sentenceRanges) {
|
|
|
481
521
|
"epub:textref": `../${chapter.href}`,
|
|
482
522
|
"epub:type": "chapter"
|
|
483
523
|
},
|
|
484
|
-
sentenceRanges.map(
|
|
485
|
-
(
|
|
486
|
-
|
|
524
|
+
sentenceRanges.map((sentenceRange) => {
|
|
525
|
+
if (granularity === "sentence" || !wordRanges.has(sentenceRange.id)) {
|
|
526
|
+
return import_epub.Epub.createXmlElement(
|
|
527
|
+
"par",
|
|
528
|
+
{
|
|
529
|
+
id: `${chapter.id}-s${sentenceRange.id}`
|
|
530
|
+
},
|
|
531
|
+
[
|
|
532
|
+
import_epub.Epub.createXmlElement("text", {
|
|
533
|
+
src: `../${chapter.href}#${chapter.id}-s${sentenceRange.id}`
|
|
534
|
+
}),
|
|
535
|
+
import_epub.Epub.createXmlElement("audio", {
|
|
536
|
+
src: `../Audio/${(0, import_posix.basename)(sentenceRange.audiofile)}`,
|
|
537
|
+
clipBegin: `${sentenceRange.start.toFixed(3)}s`,
|
|
538
|
+
clipEnd: `${sentenceRange.end.toFixed(3)}s`
|
|
539
|
+
})
|
|
540
|
+
]
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
const words = wordRanges.get(sentenceRange.id);
|
|
544
|
+
return import_epub.Epub.createXmlElement(
|
|
545
|
+
"seq",
|
|
487
546
|
{
|
|
488
|
-
id: `${chapter.id}-s${sentenceRange.id}
|
|
547
|
+
id: `${chapter.id}-s${sentenceRange.id}`,
|
|
548
|
+
"epub:type": "text-range-small",
|
|
549
|
+
"epub:textref": `../${chapter.href}#${chapter.id}-s${sentenceRange.id}`
|
|
489
550
|
},
|
|
490
|
-
|
|
491
|
-
import_epub.Epub.createXmlElement(
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
551
|
+
words.map(
|
|
552
|
+
(word) => import_epub.Epub.createXmlElement(
|
|
553
|
+
"par",
|
|
554
|
+
{
|
|
555
|
+
id: `${chapter.id}-s${sentenceRange.id}-w${word.id}`
|
|
556
|
+
},
|
|
557
|
+
[
|
|
558
|
+
import_epub.Epub.createXmlElement("text", {
|
|
559
|
+
src: `../${chapter.href}#${chapter.id}-s${sentenceRange.id}-w${word.id}`
|
|
560
|
+
}),
|
|
561
|
+
import_epub.Epub.createXmlElement("audio", {
|
|
562
|
+
src: `../Audio/${(0, import_posix.basename)(word.audiofile)}`,
|
|
563
|
+
clipBegin: `${word.start.toFixed(3)}s`,
|
|
564
|
+
clipEnd: `${word.end.toFixed(3)}s`
|
|
565
|
+
})
|
|
566
|
+
]
|
|
567
|
+
)
|
|
568
|
+
)
|
|
569
|
+
);
|
|
570
|
+
})
|
|
502
571
|
)
|
|
503
572
|
])
|
|
504
573
|
]
|
package/dist/align/align.d.cts
CHANGED
|
@@ -3,6 +3,8 @@ import { Logger } from 'pino';
|
|
|
3
3
|
import { Epub } from '@storyteller-platform/epub';
|
|
4
4
|
import { RecognitionResult } from '@storyteller-platform/ghost-story/recognition';
|
|
5
5
|
import { StorytellerTranscription } from './getSentenceRanges.cjs';
|
|
6
|
+
import '@echogarden/text-segmentation';
|
|
7
|
+
import '@storyteller-platform/transliteration';
|
|
6
8
|
|
|
7
9
|
interface AudioFileContext {
|
|
8
10
|
start: number;
|
package/dist/align/align.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { Logger } from 'pino';
|
|
|
3
3
|
import { Epub } from '@storyteller-platform/epub';
|
|
4
4
|
import { RecognitionResult } from '@storyteller-platform/ghost-story/recognition';
|
|
5
5
|
import { StorytellerTranscription } from './getSentenceRanges.js';
|
|
6
|
+
import '@echogarden/text-segmentation';
|
|
7
|
+
import '@storyteller-platform/transliteration';
|
|
6
8
|
|
|
7
9
|
interface AudioFileContext {
|
|
8
10
|
start: number;
|
package/dist/align/align.js
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
import { copyFile, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
6
6
|
import { dirname as autoDirname, join as autoJoin } from "node:path";
|
|
7
7
|
import { basename, dirname, parse, relative } from "node:path/posix";
|
|
8
|
+
import { enumerate, max } from "itertools";
|
|
8
9
|
import memoize from "memoize";
|
|
9
10
|
import { isAudioFile, lookupAudioMime } from "@storyteller-platform/audiobook";
|
|
10
11
|
import {
|
|
@@ -17,10 +18,12 @@ import {
|
|
|
17
18
|
import { getTrackDuration } from "../common/ffmpeg.js";
|
|
18
19
|
import { getXhtmlSegmentation } from "../markup/segmentation.js";
|
|
19
20
|
import {
|
|
21
|
+
collapseSentenceRangeGaps,
|
|
20
22
|
expandEmptySentenceRanges,
|
|
21
23
|
getChapterDuration,
|
|
22
24
|
getSentenceRanges,
|
|
23
|
-
interpolateSentenceRanges
|
|
25
|
+
interpolateSentenceRanges,
|
|
26
|
+
mapTranscriptionTimeline
|
|
24
27
|
} from "./getSentenceRanges.js";
|
|
25
28
|
import { findBoundaries } from "./search.js";
|
|
26
29
|
import { slugify } from "./slugify.js";
|
|
@@ -107,10 +110,11 @@ class Aligner {
|
|
|
107
110
|
primaryLocale: this.languageOverride ?? await this.epub.getLanguage()
|
|
108
111
|
}
|
|
109
112
|
);
|
|
110
|
-
return segmentation.
|
|
113
|
+
return segmentation.filter((s) => s.text.match(/\S/));
|
|
111
114
|
}
|
|
112
115
|
async writeAlignedChapter(alignedChapter) {
|
|
113
|
-
const { chapter, sentenceRanges, xml } = alignedChapter;
|
|
116
|
+
const { chapter, sentenceRanges, wordRanges, xml } = alignedChapter;
|
|
117
|
+
const wordRangeMap = new Map(wordRanges.map((w) => [w[0].sentenceId, w]));
|
|
114
118
|
const audiofiles = Array.from(
|
|
115
119
|
new Set(sentenceRanges.map(({ audiofile }) => audiofile))
|
|
116
120
|
);
|
|
@@ -143,7 +147,12 @@ class Aligner {
|
|
|
143
147
|
href: `MediaOverlays/${chapterStem}.smil`,
|
|
144
148
|
mediaType: "application/smil+xml"
|
|
145
149
|
},
|
|
146
|
-
createMediaOverlay(
|
|
150
|
+
createMediaOverlay(
|
|
151
|
+
chapter,
|
|
152
|
+
this.granularity,
|
|
153
|
+
sentenceRanges,
|
|
154
|
+
wordRangeMap
|
|
155
|
+
),
|
|
147
156
|
"xml"
|
|
148
157
|
);
|
|
149
158
|
await this.epub.updateManifestItem(chapter.id, {
|
|
@@ -180,17 +189,17 @@ class Aligner {
|
|
|
180
189
|
},
|
|
181
190
|
firstMatchedSentenceId: startSentence,
|
|
182
191
|
firstMatchedSentenceContext: {
|
|
183
|
-
prevSentence: chapterSentences[startSentence - 1] ?? null,
|
|
192
|
+
prevSentence: chapterSentences[startSentence - 1]?.text ?? null,
|
|
184
193
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
185
|
-
matchedSentence: chapterSentences[startSentence],
|
|
186
|
-
nextSentence: chapterSentences[startSentence + 1] ?? null
|
|
194
|
+
matchedSentence: chapterSentences[startSentence].text,
|
|
195
|
+
nextSentence: chapterSentences[startSentence + 1]?.text ?? null
|
|
187
196
|
},
|
|
188
197
|
lastMatchedSentenceId: endSentence,
|
|
189
198
|
lastMatchedSentenceContext: {
|
|
190
|
-
prevSentence: chapterSentences[endSentence - 1] ?? null,
|
|
199
|
+
prevSentence: chapterSentences[endSentence - 1]?.text ?? null,
|
|
191
200
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
192
|
-
matchedSentence: chapterSentences[endSentence],
|
|
193
|
-
nextSentence: chapterSentences[endSentence + 1] ?? null
|
|
201
|
+
matchedSentence: chapterSentences[endSentence].text,
|
|
202
|
+
nextSentence: chapterSentences[endSentence + 1]?.text ?? null
|
|
194
203
|
},
|
|
195
204
|
chapterSentenceCount: chapterSentences.length,
|
|
196
205
|
alignedSentenceCount: sentenceRanges.length,
|
|
@@ -211,7 +220,7 @@ class Aligner {
|
|
|
211
220
|
}, [])
|
|
212
221
|
});
|
|
213
222
|
}
|
|
214
|
-
async alignChapter(chapterId, transcriptionOffset, transcriptionEndOffset, locale,
|
|
223
|
+
async alignChapter(chapterId, transcriptionText, transcriptionOffset, transcriptionEndOffset, locale, mappedTimeline) {
|
|
215
224
|
const timing = createTiming();
|
|
216
225
|
timing.start("read contents");
|
|
217
226
|
const manifest = await this.epub.getManifest();
|
|
@@ -228,14 +237,18 @@ class Aligner {
|
|
|
228
237
|
timing.start("align sentences");
|
|
229
238
|
const {
|
|
230
239
|
sentenceRanges,
|
|
240
|
+
wordRanges,
|
|
231
241
|
transcriptionOffset: endTranscriptionOffset,
|
|
232
242
|
firstFoundSentence,
|
|
233
243
|
lastFoundSentence
|
|
234
244
|
} = await getSentenceRanges(
|
|
235
|
-
|
|
245
|
+
transcriptionText,
|
|
246
|
+
mappedTimeline,
|
|
236
247
|
chapterSentences,
|
|
248
|
+
chapterId,
|
|
237
249
|
transcriptionOffset,
|
|
238
250
|
transcriptionEndOffset,
|
|
251
|
+
this.granularity,
|
|
239
252
|
locale
|
|
240
253
|
);
|
|
241
254
|
timing.end("align sentences");
|
|
@@ -252,8 +265,9 @@ class Aligner {
|
|
|
252
265
|
chapter,
|
|
253
266
|
xml: chapterXml,
|
|
254
267
|
sentenceRanges,
|
|
255
|
-
|
|
256
|
-
|
|
268
|
+
wordRanges,
|
|
269
|
+
startOffset: transcriptionOffset,
|
|
270
|
+
endOffset: endTranscriptionOffset
|
|
257
271
|
});
|
|
258
272
|
this.addChapterReport(
|
|
259
273
|
chapter,
|
|
@@ -270,16 +284,24 @@ class Aligner {
|
|
|
270
284
|
};
|
|
271
285
|
}
|
|
272
286
|
narrowToAvailableBoundary(boundary) {
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
287
|
+
const available = [
|
|
288
|
+
-1,
|
|
289
|
+
...this.alignedChapters.toSorted((a, b) => a.startOffset - b.startOffset).flatMap(({ startOffset, endOffset }) => [startOffset, endOffset]),
|
|
290
|
+
Infinity
|
|
291
|
+
];
|
|
292
|
+
const withinBoundary = [];
|
|
293
|
+
for (let i = 0; i < available.length - 1; i += 2) {
|
|
294
|
+
const [start, end] = [available[i], available[i + 1]];
|
|
295
|
+
if (boundary.start <= start && boundary.end >= start || boundary.start <= end && boundary.end >= end) {
|
|
296
|
+
withinBoundary.push([
|
|
297
|
+
Math.max(boundary.start, start + 1),
|
|
298
|
+
Math.min(boundary.end, end - 1)
|
|
299
|
+
]);
|
|
280
300
|
}
|
|
281
301
|
}
|
|
282
|
-
|
|
302
|
+
const largestBoundary = max(withinBoundary, ([start, end]) => end - start);
|
|
303
|
+
if (!largestBoundary) return { start: boundary.start, end: boundary.end };
|
|
304
|
+
return { start: largestBoundary[0], end: largestBoundary[1] };
|
|
283
305
|
}
|
|
284
306
|
async alignBook(onProgress) {
|
|
285
307
|
const locale = this.languageOverride ?? await this.epub.getLanguage() ?? new Intl.Locale("en-US");
|
|
@@ -291,6 +313,7 @@ class Aligner {
|
|
|
291
313
|
this.transcription.transcript,
|
|
292
314
|
locale
|
|
293
315
|
);
|
|
316
|
+
const mappedTimeline = mapTranscriptionTimeline(this.transcription, mapping);
|
|
294
317
|
for (let index = 0; index < spine.length; index++) {
|
|
295
318
|
onProgress?.(index / spine.length);
|
|
296
319
|
const spineItem = spine[index];
|
|
@@ -305,7 +328,7 @@ class Aligner {
|
|
|
305
328
|
const slugifiedChapterSentences = [];
|
|
306
329
|
for (const chapterSentence of chapterSentences) {
|
|
307
330
|
slugifiedChapterSentences.push(
|
|
308
|
-
(await slugify(chapterSentence, locale)).result
|
|
331
|
+
(await slugify(chapterSentence.text, locale)).result
|
|
309
332
|
);
|
|
310
333
|
}
|
|
311
334
|
if (chapterSentences.length === 0) {
|
|
@@ -313,7 +336,7 @@ class Aligner {
|
|
|
313
336
|
continue;
|
|
314
337
|
}
|
|
315
338
|
if (chapterSentences.length < 2 && // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
316
|
-
chapterSentences[0].
|
|
339
|
+
chapterSentences[0].words.length < 4) {
|
|
317
340
|
this.logger?.info(
|
|
318
341
|
`Chapter #${index} is fewer than four words; skipping`
|
|
319
342
|
);
|
|
@@ -333,14 +356,13 @@ class Aligner {
|
|
|
333
356
|
if (start === end) {
|
|
334
357
|
continue;
|
|
335
358
|
}
|
|
336
|
-
const transcriptionOffset = mapping.invert().map(Math.max(start, 0), -1);
|
|
337
|
-
const endOffset = mapping.invert().map(Math.min(end, transcriptionText.length), 1);
|
|
338
359
|
const result = await this.alignChapter(
|
|
339
360
|
chapterId,
|
|
340
|
-
|
|
341
|
-
|
|
361
|
+
transcriptionText,
|
|
362
|
+
Math.max(start, 0),
|
|
363
|
+
Math.min(end, transcriptionText.length),
|
|
342
364
|
locale,
|
|
343
|
-
|
|
365
|
+
mappedTimeline
|
|
344
366
|
);
|
|
345
367
|
this.timing.add(result.timing.summary());
|
|
346
368
|
}
|
|
@@ -360,16 +382,36 @@ class Aligner {
|
|
|
360
382
|
}
|
|
361
383
|
return firstAudiofileIndexA - firstAudiofileIndexB;
|
|
362
384
|
});
|
|
363
|
-
|
|
385
|
+
const sentenceRanges = [];
|
|
386
|
+
const chapterSentenceCounts = {};
|
|
387
|
+
for (const alignedChapter of audioOrderedChapters) {
|
|
388
|
+
sentenceRanges.push(...alignedChapter.sentenceRanges);
|
|
389
|
+
const sentences = await this.getChapterSentences(
|
|
390
|
+
alignedChapter.chapter.id
|
|
391
|
+
);
|
|
392
|
+
chapterSentenceCounts[alignedChapter.chapter.id] = sentences.length;
|
|
393
|
+
}
|
|
394
|
+
const interpolated = await interpolateSentenceRanges(
|
|
395
|
+
sentenceRanges,
|
|
396
|
+
chapterSentenceCounts
|
|
397
|
+
);
|
|
398
|
+
const expanded = expandEmptySentenceRanges(interpolated);
|
|
399
|
+
const collapsed = await collapseSentenceRangeGaps(expanded);
|
|
400
|
+
let collapsedStart = 0;
|
|
364
401
|
for (const alignedChapter of audioOrderedChapters) {
|
|
365
|
-
const
|
|
366
|
-
alignedChapter.
|
|
367
|
-
lastSentenceRange
|
|
402
|
+
const sentences = await this.getChapterSentences(
|
|
403
|
+
alignedChapter.chapter.id
|
|
368
404
|
);
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
405
|
+
const finalSentenceRanges = collapsed.slice(
|
|
406
|
+
collapsedStart,
|
|
407
|
+
collapsedStart + sentences.length - 1
|
|
408
|
+
);
|
|
409
|
+
alignedChapter.sentenceRanges = finalSentenceRanges;
|
|
410
|
+
for (const [i, wordRanges] of enumerate(alignedChapter.wordRanges)) {
|
|
411
|
+
alignedChapter.wordRanges[i] = expandEmptySentenceRanges(wordRanges);
|
|
412
|
+
}
|
|
372
413
|
await this.writeAlignedChapter(alignedChapter);
|
|
414
|
+
collapsedStart += sentences.length - 1;
|
|
373
415
|
}
|
|
374
416
|
await this.epub.addMetadata({
|
|
375
417
|
type: "meta",
|
|
@@ -397,7 +439,7 @@ class Aligner {
|
|
|
397
439
|
return this.timing;
|
|
398
440
|
}
|
|
399
441
|
}
|
|
400
|
-
function createMediaOverlay(chapter, sentenceRanges) {
|
|
442
|
+
function createMediaOverlay(chapter, granularity, sentenceRanges, wordRanges) {
|
|
401
443
|
return [
|
|
402
444
|
Epub.createXmlElement(
|
|
403
445
|
"smil",
|
|
@@ -415,24 +457,53 @@ function createMediaOverlay(chapter, sentenceRanges) {
|
|
|
415
457
|
"epub:textref": `../${chapter.href}`,
|
|
416
458
|
"epub:type": "chapter"
|
|
417
459
|
},
|
|
418
|
-
sentenceRanges.map(
|
|
419
|
-
(
|
|
420
|
-
|
|
460
|
+
sentenceRanges.map((sentenceRange) => {
|
|
461
|
+
if (granularity === "sentence" || !wordRanges.has(sentenceRange.id)) {
|
|
462
|
+
return Epub.createXmlElement(
|
|
463
|
+
"par",
|
|
464
|
+
{
|
|
465
|
+
id: `${chapter.id}-s${sentenceRange.id}`
|
|
466
|
+
},
|
|
467
|
+
[
|
|
468
|
+
Epub.createXmlElement("text", {
|
|
469
|
+
src: `../${chapter.href}#${chapter.id}-s${sentenceRange.id}`
|
|
470
|
+
}),
|
|
471
|
+
Epub.createXmlElement("audio", {
|
|
472
|
+
src: `../Audio/${basename(sentenceRange.audiofile)}`,
|
|
473
|
+
clipBegin: `${sentenceRange.start.toFixed(3)}s`,
|
|
474
|
+
clipEnd: `${sentenceRange.end.toFixed(3)}s`
|
|
475
|
+
})
|
|
476
|
+
]
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
const words = wordRanges.get(sentenceRange.id);
|
|
480
|
+
return Epub.createXmlElement(
|
|
481
|
+
"seq",
|
|
421
482
|
{
|
|
422
|
-
id: `${chapter.id}-s${sentenceRange.id}
|
|
483
|
+
id: `${chapter.id}-s${sentenceRange.id}`,
|
|
484
|
+
"epub:type": "text-range-small",
|
|
485
|
+
"epub:textref": `../${chapter.href}#${chapter.id}-s${sentenceRange.id}`
|
|
423
486
|
},
|
|
424
|
-
|
|
425
|
-
Epub.createXmlElement(
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
487
|
+
words.map(
|
|
488
|
+
(word) => Epub.createXmlElement(
|
|
489
|
+
"par",
|
|
490
|
+
{
|
|
491
|
+
id: `${chapter.id}-s${sentenceRange.id}-w${word.id}`
|
|
492
|
+
},
|
|
493
|
+
[
|
|
494
|
+
Epub.createXmlElement("text", {
|
|
495
|
+
src: `../${chapter.href}#${chapter.id}-s${sentenceRange.id}-w${word.id}`
|
|
496
|
+
}),
|
|
497
|
+
Epub.createXmlElement("audio", {
|
|
498
|
+
src: `../Audio/${basename(word.audiofile)}`,
|
|
499
|
+
clipBegin: `${word.start.toFixed(3)}s`,
|
|
500
|
+
clipEnd: `${word.end.toFixed(3)}s`
|
|
501
|
+
})
|
|
502
|
+
]
|
|
503
|
+
)
|
|
504
|
+
)
|
|
505
|
+
);
|
|
506
|
+
})
|
|
436
507
|
)
|
|
437
508
|
])
|
|
438
509
|
]
|