@storyteller-platform/align 0.1.18 → 0.1.20
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 +107 -45
- package/dist/align/align.d.cts +3 -0
- package/dist/align/align.d.ts +3 -0
- package/dist/align/align.js +110 -46
- 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/dist/markup/transform.cjs +6 -1
- package/dist/markup/transform.js +6 -1
- package/package.json +3 -3
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,19 +253,20 @@ 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
|
-
chapterSentenceCount:
|
|
268
|
+
chapterSentenceCount: chapterSentences.length,
|
|
269
|
+
alignedSentenceCount: sentenceRanges.length,
|
|
262
270
|
audioFiles: sentenceRanges.reduce((acc, range) => {
|
|
263
271
|
const existing = acc.find(
|
|
264
272
|
(context) => context.filepath === range.audiofile
|
|
@@ -276,7 +284,7 @@ class Aligner {
|
|
|
276
284
|
}, [])
|
|
277
285
|
});
|
|
278
286
|
}
|
|
279
|
-
async alignChapter(chapterId, transcriptionOffset, transcriptionEndOffset, locale,
|
|
287
|
+
async alignChapter(chapterId, transcriptionText, transcriptionOffset, transcriptionEndOffset, locale, mappedTimeline) {
|
|
280
288
|
const timing = (0, import_ghost_story.createTiming)();
|
|
281
289
|
timing.start("read contents");
|
|
282
290
|
const manifest = await this.epub.getManifest();
|
|
@@ -293,14 +301,18 @@ class Aligner {
|
|
|
293
301
|
timing.start("align sentences");
|
|
294
302
|
const {
|
|
295
303
|
sentenceRanges,
|
|
304
|
+
wordRanges,
|
|
296
305
|
transcriptionOffset: endTranscriptionOffset,
|
|
297
306
|
firstFoundSentence,
|
|
298
307
|
lastFoundSentence
|
|
299
308
|
} = await (0, import_getSentenceRanges.getSentenceRanges)(
|
|
300
|
-
|
|
309
|
+
transcriptionText,
|
|
310
|
+
mappedTimeline,
|
|
301
311
|
chapterSentences,
|
|
312
|
+
chapterId,
|
|
302
313
|
transcriptionOffset,
|
|
303
314
|
transcriptionEndOffset,
|
|
315
|
+
this.granularity,
|
|
304
316
|
locale
|
|
305
317
|
);
|
|
306
318
|
timing.end("align sentences");
|
|
@@ -317,8 +329,9 @@ class Aligner {
|
|
|
317
329
|
chapter,
|
|
318
330
|
xml: chapterXml,
|
|
319
331
|
sentenceRanges,
|
|
320
|
-
|
|
321
|
-
|
|
332
|
+
wordRanges,
|
|
333
|
+
startOffset: transcriptionOffset,
|
|
334
|
+
endOffset: endTranscriptionOffset
|
|
322
335
|
});
|
|
323
336
|
this.addChapterReport(
|
|
324
337
|
chapter,
|
|
@@ -356,6 +369,7 @@ class Aligner {
|
|
|
356
369
|
this.transcription.transcript,
|
|
357
370
|
locale
|
|
358
371
|
);
|
|
372
|
+
const mappedTimeline = (0, import_getSentenceRanges.mapTranscriptionTimeline)(this.transcription, mapping);
|
|
359
373
|
for (let index = 0; index < spine.length; index++) {
|
|
360
374
|
onProgress?.(index / spine.length);
|
|
361
375
|
const spineItem = spine[index];
|
|
@@ -370,7 +384,7 @@ class Aligner {
|
|
|
370
384
|
const slugifiedChapterSentences = [];
|
|
371
385
|
for (const chapterSentence of chapterSentences) {
|
|
372
386
|
slugifiedChapterSentences.push(
|
|
373
|
-
(await (0, import_slugify.slugify)(chapterSentence, locale)).result
|
|
387
|
+
(await (0, import_slugify.slugify)(chapterSentence.text, locale)).result
|
|
374
388
|
);
|
|
375
389
|
}
|
|
376
390
|
if (chapterSentences.length === 0) {
|
|
@@ -378,7 +392,7 @@ class Aligner {
|
|
|
378
392
|
continue;
|
|
379
393
|
}
|
|
380
394
|
if (chapterSentences.length < 2 && // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
381
|
-
chapterSentences[0].
|
|
395
|
+
chapterSentences[0].words.length < 4) {
|
|
382
396
|
this.logger?.info(
|
|
383
397
|
`Chapter #${index} is fewer than four words; skipping`
|
|
384
398
|
);
|
|
@@ -398,14 +412,13 @@ class Aligner {
|
|
|
398
412
|
if (start === end) {
|
|
399
413
|
continue;
|
|
400
414
|
}
|
|
401
|
-
const transcriptionOffset = mapping.invert().map(Math.max(start, 0), -1);
|
|
402
|
-
const endOffset = mapping.invert().map(Math.min(end, transcriptionText.length), 1);
|
|
403
415
|
const result = await this.alignChapter(
|
|
404
416
|
chapterId,
|
|
405
|
-
|
|
406
|
-
|
|
417
|
+
transcriptionText,
|
|
418
|
+
Math.max(start, 0),
|
|
419
|
+
Math.min(end, transcriptionText.length),
|
|
407
420
|
locale,
|
|
408
|
-
|
|
421
|
+
mappedTimeline
|
|
409
422
|
);
|
|
410
423
|
this.timing.add(result.timing.summary());
|
|
411
424
|
}
|
|
@@ -425,16 +438,36 @@ class Aligner {
|
|
|
425
438
|
}
|
|
426
439
|
return firstAudiofileIndexA - firstAudiofileIndexB;
|
|
427
440
|
});
|
|
428
|
-
|
|
441
|
+
const sentenceRanges = [];
|
|
442
|
+
const chapterSentenceCounts = {};
|
|
429
443
|
for (const alignedChapter of audioOrderedChapters) {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
444
|
+
sentenceRanges.push(...alignedChapter.sentenceRanges);
|
|
445
|
+
const sentences = await this.getChapterSentences(
|
|
446
|
+
alignedChapter.chapter.id
|
|
433
447
|
);
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
448
|
+
chapterSentenceCounts[alignedChapter.chapter.id] = sentences.length;
|
|
449
|
+
}
|
|
450
|
+
const interpolated = await (0, import_getSentenceRanges.interpolateSentenceRanges)(
|
|
451
|
+
sentenceRanges,
|
|
452
|
+
chapterSentenceCounts
|
|
453
|
+
);
|
|
454
|
+
const expanded = (0, import_getSentenceRanges.expandEmptySentenceRanges)(interpolated);
|
|
455
|
+
const collapsed = await (0, import_getSentenceRanges.collapseSentenceRangeGaps)(expanded);
|
|
456
|
+
let collapsedStart = 0;
|
|
457
|
+
for (const alignedChapter of audioOrderedChapters) {
|
|
458
|
+
const sentences = await this.getChapterSentences(
|
|
459
|
+
alignedChapter.chapter.id
|
|
460
|
+
);
|
|
461
|
+
const finalSentenceRanges = collapsed.slice(
|
|
462
|
+
collapsedStart,
|
|
463
|
+
collapsedStart + sentences.length - 1
|
|
464
|
+
);
|
|
465
|
+
alignedChapter.sentenceRanges = finalSentenceRanges;
|
|
466
|
+
for (const [i, wordRanges] of (0, import_itertools.enumerate)(alignedChapter.wordRanges)) {
|
|
467
|
+
alignedChapter.wordRanges[i] = (0, import_getSentenceRanges.expandEmptySentenceRanges)(wordRanges);
|
|
468
|
+
}
|
|
437
469
|
await this.writeAlignedChapter(alignedChapter);
|
|
470
|
+
collapsedStart += sentences.length - 1;
|
|
438
471
|
}
|
|
439
472
|
await this.epub.addMetadata({
|
|
440
473
|
type: "meta",
|
|
@@ -462,7 +495,7 @@ class Aligner {
|
|
|
462
495
|
return this.timing;
|
|
463
496
|
}
|
|
464
497
|
}
|
|
465
|
-
function createMediaOverlay(chapter, sentenceRanges) {
|
|
498
|
+
function createMediaOverlay(chapter, granularity, sentenceRanges, wordRanges) {
|
|
466
499
|
return [
|
|
467
500
|
import_epub.Epub.createXmlElement(
|
|
468
501
|
"smil",
|
|
@@ -480,24 +513,53 @@ function createMediaOverlay(chapter, sentenceRanges) {
|
|
|
480
513
|
"epub:textref": `../${chapter.href}`,
|
|
481
514
|
"epub:type": "chapter"
|
|
482
515
|
},
|
|
483
|
-
sentenceRanges.map(
|
|
484
|
-
(
|
|
485
|
-
|
|
516
|
+
sentenceRanges.map((sentenceRange) => {
|
|
517
|
+
if (granularity === "sentence" || !wordRanges.has(sentenceRange.id)) {
|
|
518
|
+
return import_epub.Epub.createXmlElement(
|
|
519
|
+
"par",
|
|
520
|
+
{
|
|
521
|
+
id: `${chapter.id}-s${sentenceRange.id}`
|
|
522
|
+
},
|
|
523
|
+
[
|
|
524
|
+
import_epub.Epub.createXmlElement("text", {
|
|
525
|
+
src: `../${chapter.href}#${chapter.id}-s${sentenceRange.id}`
|
|
526
|
+
}),
|
|
527
|
+
import_epub.Epub.createXmlElement("audio", {
|
|
528
|
+
src: `../Audio/${(0, import_posix.basename)(sentenceRange.audiofile)}`,
|
|
529
|
+
clipBegin: `${sentenceRange.start.toFixed(3)}s`,
|
|
530
|
+
clipEnd: `${sentenceRange.end.toFixed(3)}s`
|
|
531
|
+
})
|
|
532
|
+
]
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
const words = wordRanges.get(sentenceRange.id);
|
|
536
|
+
return import_epub.Epub.createXmlElement(
|
|
537
|
+
"seq",
|
|
486
538
|
{
|
|
487
|
-
id: `${chapter.id}-s${sentenceRange.id}
|
|
539
|
+
id: `${chapter.id}-s${sentenceRange.id}`,
|
|
540
|
+
"epub:type": "text-range-small",
|
|
541
|
+
"epub:textref": `../${chapter.href}#${chapter.id}-s${sentenceRange.id}`
|
|
488
542
|
},
|
|
489
|
-
|
|
490
|
-
import_epub.Epub.createXmlElement(
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
543
|
+
words.map(
|
|
544
|
+
(word) => import_epub.Epub.createXmlElement(
|
|
545
|
+
"par",
|
|
546
|
+
{
|
|
547
|
+
id: `${chapter.id}-s${sentenceRange.id}-w${word.id}`
|
|
548
|
+
},
|
|
549
|
+
[
|
|
550
|
+
import_epub.Epub.createXmlElement("text", {
|
|
551
|
+
src: `../${chapter.href}#${chapter.id}-s${sentenceRange.id}-w${word.id}`
|
|
552
|
+
}),
|
|
553
|
+
import_epub.Epub.createXmlElement("audio", {
|
|
554
|
+
src: `../Audio/${(0, import_posix.basename)(word.audiofile)}`,
|
|
555
|
+
clipBegin: `${word.start.toFixed(3)}s`,
|
|
556
|
+
clipEnd: `${word.end.toFixed(3)}s`
|
|
557
|
+
})
|
|
558
|
+
]
|
|
559
|
+
)
|
|
560
|
+
)
|
|
561
|
+
);
|
|
562
|
+
})
|
|
501
563
|
)
|
|
502
564
|
])
|
|
503
565
|
]
|
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;
|
|
@@ -29,6 +31,7 @@ interface ChapterReport {
|
|
|
29
31
|
nextSentence: string | null;
|
|
30
32
|
};
|
|
31
33
|
chapterSentenceCount: number;
|
|
34
|
+
alignedSentenceCount: number;
|
|
32
35
|
audioFiles: AudioFileContext[];
|
|
33
36
|
}
|
|
34
37
|
interface Report {
|
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;
|
|
@@ -29,6 +31,7 @@ interface ChapterReport {
|
|
|
29
31
|
nextSentence: string | null;
|
|
30
32
|
};
|
|
31
33
|
chapterSentenceCount: number;
|
|
34
|
+
alignedSentenceCount: number;
|
|
32
35
|
audioFiles: AudioFileContext[];
|
|
33
36
|
}
|
|
34
37
|
interface Report {
|
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 } 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,19 +189,20 @@ 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
|
-
chapterSentenceCount:
|
|
204
|
+
chapterSentenceCount: chapterSentences.length,
|
|
205
|
+
alignedSentenceCount: sentenceRanges.length,
|
|
196
206
|
audioFiles: sentenceRanges.reduce((acc, range) => {
|
|
197
207
|
const existing = acc.find(
|
|
198
208
|
(context) => context.filepath === range.audiofile
|
|
@@ -210,7 +220,7 @@ class Aligner {
|
|
|
210
220
|
}, [])
|
|
211
221
|
});
|
|
212
222
|
}
|
|
213
|
-
async alignChapter(chapterId, transcriptionOffset, transcriptionEndOffset, locale,
|
|
223
|
+
async alignChapter(chapterId, transcriptionText, transcriptionOffset, transcriptionEndOffset, locale, mappedTimeline) {
|
|
214
224
|
const timing = createTiming();
|
|
215
225
|
timing.start("read contents");
|
|
216
226
|
const manifest = await this.epub.getManifest();
|
|
@@ -227,14 +237,18 @@ class Aligner {
|
|
|
227
237
|
timing.start("align sentences");
|
|
228
238
|
const {
|
|
229
239
|
sentenceRanges,
|
|
240
|
+
wordRanges,
|
|
230
241
|
transcriptionOffset: endTranscriptionOffset,
|
|
231
242
|
firstFoundSentence,
|
|
232
243
|
lastFoundSentence
|
|
233
244
|
} = await getSentenceRanges(
|
|
234
|
-
|
|
245
|
+
transcriptionText,
|
|
246
|
+
mappedTimeline,
|
|
235
247
|
chapterSentences,
|
|
248
|
+
chapterId,
|
|
236
249
|
transcriptionOffset,
|
|
237
250
|
transcriptionEndOffset,
|
|
251
|
+
this.granularity,
|
|
238
252
|
locale
|
|
239
253
|
);
|
|
240
254
|
timing.end("align sentences");
|
|
@@ -251,8 +265,9 @@ class Aligner {
|
|
|
251
265
|
chapter,
|
|
252
266
|
xml: chapterXml,
|
|
253
267
|
sentenceRanges,
|
|
254
|
-
|
|
255
|
-
|
|
268
|
+
wordRanges,
|
|
269
|
+
startOffset: transcriptionOffset,
|
|
270
|
+
endOffset: endTranscriptionOffset
|
|
256
271
|
});
|
|
257
272
|
this.addChapterReport(
|
|
258
273
|
chapter,
|
|
@@ -290,6 +305,7 @@ class Aligner {
|
|
|
290
305
|
this.transcription.transcript,
|
|
291
306
|
locale
|
|
292
307
|
);
|
|
308
|
+
const mappedTimeline = mapTranscriptionTimeline(this.transcription, mapping);
|
|
293
309
|
for (let index = 0; index < spine.length; index++) {
|
|
294
310
|
onProgress?.(index / spine.length);
|
|
295
311
|
const spineItem = spine[index];
|
|
@@ -304,7 +320,7 @@ class Aligner {
|
|
|
304
320
|
const slugifiedChapterSentences = [];
|
|
305
321
|
for (const chapterSentence of chapterSentences) {
|
|
306
322
|
slugifiedChapterSentences.push(
|
|
307
|
-
(await slugify(chapterSentence, locale)).result
|
|
323
|
+
(await slugify(chapterSentence.text, locale)).result
|
|
308
324
|
);
|
|
309
325
|
}
|
|
310
326
|
if (chapterSentences.length === 0) {
|
|
@@ -312,7 +328,7 @@ class Aligner {
|
|
|
312
328
|
continue;
|
|
313
329
|
}
|
|
314
330
|
if (chapterSentences.length < 2 && // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
315
|
-
chapterSentences[0].
|
|
331
|
+
chapterSentences[0].words.length < 4) {
|
|
316
332
|
this.logger?.info(
|
|
317
333
|
`Chapter #${index} is fewer than four words; skipping`
|
|
318
334
|
);
|
|
@@ -332,14 +348,13 @@ class Aligner {
|
|
|
332
348
|
if (start === end) {
|
|
333
349
|
continue;
|
|
334
350
|
}
|
|
335
|
-
const transcriptionOffset = mapping.invert().map(Math.max(start, 0), -1);
|
|
336
|
-
const endOffset = mapping.invert().map(Math.min(end, transcriptionText.length), 1);
|
|
337
351
|
const result = await this.alignChapter(
|
|
338
352
|
chapterId,
|
|
339
|
-
|
|
340
|
-
|
|
353
|
+
transcriptionText,
|
|
354
|
+
Math.max(start, 0),
|
|
355
|
+
Math.min(end, transcriptionText.length),
|
|
341
356
|
locale,
|
|
342
|
-
|
|
357
|
+
mappedTimeline
|
|
343
358
|
);
|
|
344
359
|
this.timing.add(result.timing.summary());
|
|
345
360
|
}
|
|
@@ -359,16 +374,36 @@ class Aligner {
|
|
|
359
374
|
}
|
|
360
375
|
return firstAudiofileIndexA - firstAudiofileIndexB;
|
|
361
376
|
});
|
|
362
|
-
|
|
377
|
+
const sentenceRanges = [];
|
|
378
|
+
const chapterSentenceCounts = {};
|
|
363
379
|
for (const alignedChapter of audioOrderedChapters) {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
380
|
+
sentenceRanges.push(...alignedChapter.sentenceRanges);
|
|
381
|
+
const sentences = await this.getChapterSentences(
|
|
382
|
+
alignedChapter.chapter.id
|
|
367
383
|
);
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
384
|
+
chapterSentenceCounts[alignedChapter.chapter.id] = sentences.length;
|
|
385
|
+
}
|
|
386
|
+
const interpolated = await interpolateSentenceRanges(
|
|
387
|
+
sentenceRanges,
|
|
388
|
+
chapterSentenceCounts
|
|
389
|
+
);
|
|
390
|
+
const expanded = expandEmptySentenceRanges(interpolated);
|
|
391
|
+
const collapsed = await collapseSentenceRangeGaps(expanded);
|
|
392
|
+
let collapsedStart = 0;
|
|
393
|
+
for (const alignedChapter of audioOrderedChapters) {
|
|
394
|
+
const sentences = await this.getChapterSentences(
|
|
395
|
+
alignedChapter.chapter.id
|
|
396
|
+
);
|
|
397
|
+
const finalSentenceRanges = collapsed.slice(
|
|
398
|
+
collapsedStart,
|
|
399
|
+
collapsedStart + sentences.length - 1
|
|
400
|
+
);
|
|
401
|
+
alignedChapter.sentenceRanges = finalSentenceRanges;
|
|
402
|
+
for (const [i, wordRanges] of enumerate(alignedChapter.wordRanges)) {
|
|
403
|
+
alignedChapter.wordRanges[i] = expandEmptySentenceRanges(wordRanges);
|
|
404
|
+
}
|
|
371
405
|
await this.writeAlignedChapter(alignedChapter);
|
|
406
|
+
collapsedStart += sentences.length - 1;
|
|
372
407
|
}
|
|
373
408
|
await this.epub.addMetadata({
|
|
374
409
|
type: "meta",
|
|
@@ -396,7 +431,7 @@ class Aligner {
|
|
|
396
431
|
return this.timing;
|
|
397
432
|
}
|
|
398
433
|
}
|
|
399
|
-
function createMediaOverlay(chapter, sentenceRanges) {
|
|
434
|
+
function createMediaOverlay(chapter, granularity, sentenceRanges, wordRanges) {
|
|
400
435
|
return [
|
|
401
436
|
Epub.createXmlElement(
|
|
402
437
|
"smil",
|
|
@@ -414,24 +449,53 @@ function createMediaOverlay(chapter, sentenceRanges) {
|
|
|
414
449
|
"epub:textref": `../${chapter.href}`,
|
|
415
450
|
"epub:type": "chapter"
|
|
416
451
|
},
|
|
417
|
-
sentenceRanges.map(
|
|
418
|
-
(
|
|
419
|
-
|
|
452
|
+
sentenceRanges.map((sentenceRange) => {
|
|
453
|
+
if (granularity === "sentence" || !wordRanges.has(sentenceRange.id)) {
|
|
454
|
+
return Epub.createXmlElement(
|
|
455
|
+
"par",
|
|
456
|
+
{
|
|
457
|
+
id: `${chapter.id}-s${sentenceRange.id}`
|
|
458
|
+
},
|
|
459
|
+
[
|
|
460
|
+
Epub.createXmlElement("text", {
|
|
461
|
+
src: `../${chapter.href}#${chapter.id}-s${sentenceRange.id}`
|
|
462
|
+
}),
|
|
463
|
+
Epub.createXmlElement("audio", {
|
|
464
|
+
src: `../Audio/${basename(sentenceRange.audiofile)}`,
|
|
465
|
+
clipBegin: `${sentenceRange.start.toFixed(3)}s`,
|
|
466
|
+
clipEnd: `${sentenceRange.end.toFixed(3)}s`
|
|
467
|
+
})
|
|
468
|
+
]
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
const words = wordRanges.get(sentenceRange.id);
|
|
472
|
+
return Epub.createXmlElement(
|
|
473
|
+
"seq",
|
|
420
474
|
{
|
|
421
|
-
id: `${chapter.id}-s${sentenceRange.id}
|
|
475
|
+
id: `${chapter.id}-s${sentenceRange.id}`,
|
|
476
|
+
"epub:type": "text-range-small",
|
|
477
|
+
"epub:textref": `../${chapter.href}#${chapter.id}-s${sentenceRange.id}`
|
|
422
478
|
},
|
|
423
|
-
|
|
424
|
-
Epub.createXmlElement(
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
479
|
+
words.map(
|
|
480
|
+
(word) => Epub.createXmlElement(
|
|
481
|
+
"par",
|
|
482
|
+
{
|
|
483
|
+
id: `${chapter.id}-s${sentenceRange.id}-w${word.id}`
|
|
484
|
+
},
|
|
485
|
+
[
|
|
486
|
+
Epub.createXmlElement("text", {
|
|
487
|
+
src: `../${chapter.href}#${chapter.id}-s${sentenceRange.id}-w${word.id}`
|
|
488
|
+
}),
|
|
489
|
+
Epub.createXmlElement("audio", {
|
|
490
|
+
src: `../Audio/${basename(word.audiofile)}`,
|
|
491
|
+
clipBegin: `${word.start.toFixed(3)}s`,
|
|
492
|
+
clipEnd: `${word.end.toFixed(3)}s`
|
|
493
|
+
})
|
|
494
|
+
]
|
|
495
|
+
)
|
|
496
|
+
)
|
|
497
|
+
);
|
|
498
|
+
})
|
|
435
499
|
)
|
|
436
500
|
])
|
|
437
501
|
]
|