@storyteller-platform/align 0.1.57 → 0.1.58

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/dist/align/align.cjs +38 -19
  2. package/dist/align/align.d.cts +12 -1
  3. package/dist/align/align.d.ts +12 -1
  4. package/dist/align/align.js +38 -19
  5. package/dist/align/ctc/Aligner.cjs +800 -0
  6. package/dist/align/ctc/Aligner.d.cts +56 -0
  7. package/dist/align/ctc/Aligner.d.ts +56 -0
  8. package/dist/align/ctc/Aligner.js +789 -0
  9. package/dist/align/ctc/align.cjs +189 -0
  10. package/dist/align/ctc/align.d.cts +14 -0
  11. package/dist/align/ctc/align.d.ts +14 -0
  12. package/dist/align/ctc/align.js +132 -0
  13. package/dist/{process/mime.cjs → align/ctc/emissions.cjs} +7 -20
  14. package/dist/align/ctc/emissions.d.cts +6 -0
  15. package/dist/align/ctc/emissions.d.ts +6 -0
  16. package/dist/align/ctc/emissions.js +7 -0
  17. package/dist/align/ctc/forcedAlign.cjs +273 -0
  18. package/dist/align/ctc/forcedAlign.d.cts +21 -0
  19. package/dist/align/ctc/forcedAlign.d.ts +21 -0
  20. package/dist/align/ctc/forcedAlign.js +248 -0
  21. package/dist/align/ctc/greedyDecode.cjs +61 -0
  22. package/dist/align/ctc/greedyDecode.d.cts +15 -0
  23. package/dist/align/ctc/greedyDecode.d.ts +15 -0
  24. package/dist/align/ctc/greedyDecode.js +37 -0
  25. package/dist/align/ctc/mediaOverlay.cjs +248 -0
  26. package/dist/align/ctc/mediaOverlay.d.cts +16 -0
  27. package/dist/align/ctc/mediaOverlay.d.ts +16 -0
  28. package/dist/align/ctc/mediaOverlay.js +224 -0
  29. package/dist/align/ctc/report.cjs +66 -0
  30. package/dist/align/ctc/report.d.cts +79 -0
  31. package/dist/align/ctc/report.d.ts +79 -0
  32. package/dist/align/ctc/report.js +42 -0
  33. package/dist/align/ctc/search.cjs +437 -0
  34. package/dist/align/ctc/search.d.cts +46 -0
  35. package/dist/align/ctc/search.d.ts +46 -0
  36. package/dist/align/ctc/search.js +412 -0
  37. package/dist/align/getSentenceRanges.cjs +9 -3
  38. package/dist/align/getSentenceRanges.d.cts +1 -0
  39. package/dist/align/getSentenceRanges.d.ts +1 -0
  40. package/dist/align/getSentenceRanges.js +9 -3
  41. package/dist/align/parse.cjs +17 -4
  42. package/dist/align/parse.d.cts +8 -4
  43. package/dist/align/parse.d.ts +8 -4
  44. package/dist/align/parse.js +19 -4
  45. package/dist/align/search.cjs +76 -23
  46. package/dist/align/search.d.cts +8 -4
  47. package/dist/align/search.d.ts +8 -4
  48. package/dist/align/search.js +76 -24
  49. package/dist/align/slugify.cjs +3 -2
  50. package/dist/align/slugify.d.cts +1 -1
  51. package/dist/align/slugify.d.ts +1 -1
  52. package/dist/align/slugify.js +3 -2
  53. package/dist/cli/bin.cjs +204 -114
  54. package/dist/cli/bin.js +194 -103
  55. package/dist/common/ffmpeg.cjs +10 -5
  56. package/dist/common/ffmpeg.js +10 -5
  57. package/dist/common/logging.cjs +33 -1
  58. package/dist/common/logging.js +33 -1
  59. package/dist/emit/emit.cjs +105 -0
  60. package/dist/emit/emit.d.cts +15 -0
  61. package/dist/emit/emit.d.ts +15 -0
  62. package/dist/emit/emit.js +86 -0
  63. package/dist/emit/fs.cjs +227 -0
  64. package/dist/emit/fs.d.cts +22 -0
  65. package/dist/emit/fs.d.ts +22 -0
  66. package/dist/emit/fs.js +162 -0
  67. package/dist/emit/parse.cjs +68 -0
  68. package/dist/emit/parse.d.cts +26 -0
  69. package/dist/emit/parse.d.ts +26 -0
  70. package/dist/emit/parse.js +58 -0
  71. package/dist/index.d.cts +1 -1
  72. package/dist/index.d.ts +1 -1
  73. package/dist/markup/model.cjs +0 -16
  74. package/dist/markup/model.d.cts +1 -1
  75. package/dist/markup/model.d.ts +1 -1
  76. package/dist/markup/model.js +0 -12
  77. package/dist/markup/parseDom.cjs +13 -14
  78. package/dist/markup/parseDom.d.cts +2 -2
  79. package/dist/markup/parseDom.d.ts +2 -2
  80. package/dist/markup/parseDom.js +13 -18
  81. package/dist/markup/resolvedPos.d.cts +1 -1
  82. package/dist/markup/resolvedPos.d.ts +1 -1
  83. package/dist/markup/segmentation.cjs +3 -1
  84. package/dist/markup/segmentation.js +3 -1
  85. package/dist/markup/serializeDom.cjs +1 -1
  86. package/dist/markup/serializeDom.d.cts +1 -1
  87. package/dist/markup/serializeDom.d.ts +1 -1
  88. package/dist/markup/serializeDom.js +1 -1
  89. package/dist/markup/transform.cjs +5 -2
  90. package/dist/markup/transform.d.cts +1 -1
  91. package/dist/markup/transform.d.ts +1 -1
  92. package/dist/markup/transform.js +5 -4
  93. package/dist/{model-CZ2mMHJO.d.cts → model-CeOMDDgt.d.cts} +3 -11
  94. package/dist/{model-CZ2mMHJO.d.ts → model-CeOMDDgt.d.ts} +3 -11
  95. package/dist/process/processAudiobook.cjs +12 -23
  96. package/dist/process/processAudiobook.js +14 -30
  97. package/dist/process/ranges.cjs +2 -2
  98. package/dist/process/ranges.js +2 -2
  99. package/dist/readium/guidedNavigation.cjs +8 -8
  100. package/dist/readium/guidedNavigation.js +8 -8
  101. package/dist/readium/manifest.cjs +5 -4
  102. package/dist/readium/manifest.js +5 -4
  103. package/dist/snapshot/ctc/snapshot.cjs +261 -0
  104. package/dist/snapshot/ctc/snapshot.d.cts +6 -0
  105. package/dist/snapshot/ctc/snapshot.d.ts +6 -0
  106. package/dist/snapshot/ctc/snapshot.js +200 -0
  107. package/dist/snapshot/parse.cjs +29 -16
  108. package/dist/snapshot/parse.d.cts +14 -10
  109. package/dist/snapshot/parse.d.ts +14 -10
  110. package/dist/snapshot/parse.js +32 -17
  111. package/dist/transcribe/parse.d.cts +6 -6
  112. package/dist/transcribe/parse.d.ts +6 -6
  113. package/dist/transcribe/transcribe.cjs +4 -2
  114. package/dist/transcribe/transcribe.js +4 -2
  115. package/package.json +6 -4
  116. package/dist/process/mime.d.cts +0 -3
  117. package/dist/process/mime.d.ts +0 -3
  118. package/dist/process/mime.js +0 -24
@@ -81,16 +81,16 @@ module.exports = __toCommonJS(align_exports);
81
81
  var import_node_crypto = require("node:crypto");
82
82
  var import_node_fs = require("node:fs");
83
83
  var import_promises = require("node:fs/promises");
84
- var import_node_os = require("node:os");
85
84
  var import_node_path = require("node:path");
86
85
  var import_posix = require("node:path/posix");
87
86
  var import_shared = require("@readium/shared");
88
87
  var import_itertools = require("itertools");
89
88
  var import_memoize = __toESM(require("memoize"), 1);
90
89
  var import_yazl = require("yazl");
91
- var import_audiobook = require("@storyteller-platform/audiobook");
92
90
  var import_epub = require("@storyteller-platform/epub");
91
+ var import_fs = require("@storyteller-platform/fs");
93
92
  var import_ghost_story = require("@storyteller-platform/ghost-story");
93
+ var import_media_types = require("@storyteller-platform/media-types");
94
94
  var import_ffmpeg = require("../common/ffmpeg.cjs");
95
95
  var import_parseDom = require("../markup/parseDom.cjs");
96
96
  var import_segmentation = require("../markup/segmentation.cjs");
@@ -105,8 +105,7 @@ async function align(input, output, transcriptionsDir, audiobookDir, options) {
105
105
  var _stack2 = [];
106
106
  try {
107
107
  const outFormat = options.outFormat ?? "epub";
108
- const epubPath = outFormat === "epub" ? (0, import_node_path.join)(
109
- (0, import_node_os.tmpdir)(),
108
+ const epubPath = outFormat === "epub" ? (0, import_fs.scratchPath)(
110
109
  `storyteller-platform-align-${(0, import_node_crypto.randomUUID)()}`,
111
110
  (0, import_node_path.basename)(output)
112
111
  ) : input;
@@ -124,7 +123,7 @@ async function align(input, output, transcriptionsDir, audiobookDir, options) {
124
123
  await (0, import_promises.copyFile)(input, epubPath);
125
124
  }
126
125
  const audiobookFiles = await (0, import_promises.readdir)(audiobookDir).then(
127
- (filenames) => filenames.filter((f) => (0, import_audiobook.isAudioFile)(f)).map((f) => (0, import_node_path.join)(audiobookDir, f))
126
+ (filenames) => filenames.filter((f) => import_media_types.MediaType.fromPath(f)?.kind === "audio").map((f) => (0, import_node_path.join)(audiobookDir, f))
128
127
  );
129
128
  const epub = __using(_stack2, await import_epub.Epub.from(epubPath));
130
129
  const transcriptions = await (0, import_promises.readdir)(transcriptionsDir).then(
@@ -172,8 +171,7 @@ async function align(input, output, transcriptionsDir, audiobookDir, options) {
172
171
  ),
173
172
  guidedNavigationDocuments
174
173
  );
175
- const tmpArchivePath = (0, import_node_path.join)(
176
- (0, import_node_os.tmpdir)(),
174
+ const tmpArchivePath = (0, import_fs.scratchPath)(
177
175
  `storyteller-platform-epub-${(0, import_node_crypto.randomUUID)()}`
178
176
  );
179
177
  const { promise, resolve } = Promise.withResolvers();
@@ -235,6 +233,9 @@ class Aligner {
235
233
  this.languageOverride = languageOverride;
236
234
  this.logger = logger;
237
235
  this.transcription = concatTranscriptions(transcriptions, audiofiles);
236
+ audiofiles.forEach((audiofile, index) => {
237
+ this.audiofileTranscripts[audiofile] = transcriptions[index]?.transcript ?? "";
238
+ });
238
239
  this.getChapterSentences = (0, import_memoize.default)(this.getChapterSentences.bind(this));
239
240
  this.granularity = granularity ?? "sentence";
240
241
  this.textRef = textRef ?? "id-fragment";
@@ -246,6 +247,7 @@ class Aligner {
246
247
  granularity;
247
248
  textRef;
248
249
  audioFileDurations = {};
250
+ audiofileTranscripts = {};
249
251
  report = {
250
252
  chapters: [],
251
253
  unalignedChapters: [],
@@ -369,7 +371,7 @@ class Aligner {
369
371
  const duration = await (0, import_ffmpeg.getTrackDuration)(audiofile);
370
372
  this.totalDuration += duration;
371
373
  const audio = await (0, import_promises.readFile)(audiofile);
372
- const mediaType = (0, import_audiobook.lookupAudioMime)(base) ?? void 0;
374
+ const mediaType = import_media_types.MediaType.fromPath(base)?.mime;
373
375
  await this.epub.addManifestItem(
374
376
  {
375
377
  id,
@@ -386,7 +388,7 @@ class Aligner {
386
388
  {
387
389
  id: mediaOverlayId,
388
390
  href: `MediaOverlays/${chapterStem}.smil`,
389
- mediaType: "application/smil+xml"
391
+ mediaType: import_media_types.MediaType.SMIL.mime
390
392
  },
391
393
  createMediaOverlay(
392
394
  chapter,
@@ -414,7 +416,7 @@ class Aligner {
414
416
  value: import_epub.Epub.formatSmilDuration(chapterDuration)
415
417
  });
416
418
  }
417
- addChapterReport(chapter, chapterSentences, sentenceRanges, startSentence, endSentence, mapping, transcriptionOffset, endTranscriptionOffset) {
419
+ addChapterReport(spineIndex, chapter, chapterSentences, sentenceRanges, startSentence, endSentence, mapping, transcriptionOffset, endTranscriptionOffset, score) {
418
420
  const audioFiles = sentenceRanges.reduce(
419
421
  (acc, range) => {
420
422
  const existing = acc.find(
@@ -438,6 +440,8 @@ class Aligner {
438
440
  const mappedEndTranscriptionOffset = cursor.map(endTranscriptionOffset);
439
441
  this.report.chapters.push({
440
442
  href: chapter.href,
443
+ score,
444
+ spineIndex,
441
445
  transcriptionOffset: mappedTranscriptionOffset,
442
446
  endTranscriptionOffset: mappedEndTranscriptionOffset,
443
447
  transcriptionContext: {
@@ -505,7 +509,7 @@ class Aligner {
505
509
  }
506
510
  }
507
511
  }
508
- async alignChapter(chapterId, transcriptionText, transcriptionOffset, transcriptionEndOffset, locale, mappedTimeline, mapping) {
512
+ async alignChapter(spineIndex, chapterId, transcriptionText, transcriptionOffset, transcriptionEndOffset, locale, mappedTimeline, mapping) {
509
513
  const timing = (0, import_ghost_story.createTiming)();
510
514
  timing.start("read contents");
511
515
  const manifest = await this.epub.getManifest();
@@ -525,7 +529,8 @@ class Aligner {
525
529
  wordRanges,
526
530
  transcriptionOffset: endTranscriptionOffset,
527
531
  firstFoundSentence,
528
- lastFoundSentence
532
+ lastFoundSentence,
533
+ score
529
534
  } = await (0, import_getSentenceRanges.getSentenceRanges)(
530
535
  transcriptionText,
531
536
  mappedTimeline,
@@ -544,7 +549,7 @@ class Aligner {
544
549
  import_epub.Epub.addLinkToXhtmlHead(chapterXml, {
545
550
  rel: "stylesheet",
546
551
  href: storytellerStylesheetUrl,
547
- type: "text/css"
552
+ type: import_media_types.MediaType.CSS.mime
548
553
  });
549
554
  this.alignedChapters.push({
550
555
  chapter,
@@ -555,6 +560,7 @@ class Aligner {
555
560
  endOffset: endTranscriptionOffset
556
561
  });
557
562
  this.addChapterReport(
563
+ spineIndex,
558
564
  chapter,
559
565
  chapterSentences,
560
566
  sentenceRanges,
@@ -562,7 +568,8 @@ class Aligner {
562
568
  lastFoundSentence,
563
569
  mapping,
564
570
  transcriptionOffset,
565
- endTranscriptionOffset
571
+ endTranscriptionOffset,
572
+ score
566
573
  );
567
574
  return {
568
575
  lastSentenceRange: sentenceRanges.at(-1) ?? null,
@@ -614,7 +621,8 @@ class Aligner {
614
621
  if (manifest[chapterId]?.properties?.includes("nav")) {
615
622
  this.report.unalignedChapters.push({
616
623
  href: spineItem.href,
617
- reason: "is-nav"
624
+ reason: "is-nav",
625
+ spineIndex: index
618
626
  });
619
627
  continue;
620
628
  }
@@ -629,7 +637,8 @@ class Aligner {
629
637
  this.logger?.info(`Chapter #${index} has no text; skipping`);
630
638
  this.report.unalignedChapters.push({
631
639
  href: spineItem.href,
632
- reason: "no-text"
640
+ reason: "no-text",
641
+ spineIndex: index
633
642
  });
634
643
  continue;
635
644
  }
@@ -640,7 +649,8 @@ class Aligner {
640
649
  );
641
650
  this.report.unalignedChapters.push({
642
651
  href: spineItem.href,
643
- reason: "too-short"
652
+ reason: "too-short",
653
+ spineIndex: index
644
654
  });
645
655
  continue;
646
656
  }
@@ -655,6 +665,9 @@ class Aligner {
655
665
  this.report.unalignedChapters.push({
656
666
  href: spineItem.href,
657
667
  reason: "not-found",
668
+ spineIndex: index,
669
+ chapterSentenceCount: chapterSentences.length,
670
+ score: 0,
658
671
  start: chapterSentences.slice(0, 3).map((s) => s.text).join("").slice(0, 80),
659
672
  end: chapterSentences.slice(-3).map((s) => s.text).join("").slice(-80)
660
673
  });
@@ -665,6 +678,7 @@ class Aligner {
665
678
  continue;
666
679
  }
667
680
  const result = await this.alignChapter(
681
+ index,
668
682
  chapterId,
669
683
  transcriptionText,
670
684
  Math.max(start, 0),
@@ -730,7 +744,12 @@ class Aligner {
730
744
  }
731
745
  for (const audiofile of this.audiofiles) {
732
746
  if (!this.report.audioFiles.some(({ filepath }) => filepath === audiofile)) {
733
- this.report.unalignedAudioFiles.push({ filepath: audiofile });
747
+ const transcript = this.audiofileTranscripts[audiofile]?.trim();
748
+ this.report.unalignedAudioFiles.push({
749
+ filepath: audiofile,
750
+ duration: this.audioFileDurations[audiofile] ?? null,
751
+ ...transcript ? { transcription: { text: transcript.slice(0, 2e3) } } : {}
752
+ });
734
753
  }
735
754
  }
736
755
  await this.epub.addMetadata({
@@ -747,7 +766,7 @@ class Aligner {
747
766
  {
748
767
  id: "storyteller_readaloud_styles",
749
768
  href: "Styles/storyteller-readaloud.css",
750
- mediaType: "text/css"
769
+ mediaType: import_media_types.MediaType.CSS.mime
751
770
  },
752
771
  `
753
772
  .-epub-media-overlay-active {
@@ -13,6 +13,8 @@ interface AudioFileContext {
13
13
  }
14
14
  interface ChapterReport {
15
15
  href: string;
16
+ score: number;
17
+ spineIndex: number;
16
18
  transcriptionOffset: number;
17
19
  endTranscriptionOffset: number;
18
20
  transcriptionContext: {
@@ -42,10 +44,14 @@ interface ChapterReport {
42
44
  type UnalignedChapterReason = "too-short" | "not-found" | "is-nav" | "no-text";
43
45
  interface UnalignedChapterReport {
44
46
  href: string;
47
+ spineIndex: number;
45
48
  reason: Exclude<UnalignedChapterReason, "not-found">;
46
49
  }
47
50
  interface UnalignedNotFoundChapterReport {
48
51
  href: string;
52
+ spineIndex: number;
53
+ chapterSentenceCount: number;
54
+ score: number;
49
55
  reason: "not-found";
50
56
  start: string;
51
57
  end: string;
@@ -61,6 +67,10 @@ interface AudioFileReport {
61
67
  }
62
68
  interface UnalignedAudioFileReport {
63
69
  filepath: string;
70
+ duration?: number | null;
71
+ transcription?: {
72
+ text: string;
73
+ };
64
74
  }
65
75
  interface Report {
66
76
  chapters: ChapterReport[];
@@ -90,6 +100,7 @@ declare class Aligner {
90
100
  private granularity;
91
101
  private textRef;
92
102
  private audioFileDurations;
103
+ private audiofileTranscripts;
93
104
  report: Report;
94
105
  constructor(epub: Epub, audiofiles: string[], transcriptions: Pick<RecognitionResult, "transcript" | "timeline">[], granularity: "sentence" | "word" | null | undefined, textRef: "id-fragment" | "text-fragment" | null | undefined, languageOverride?: (Intl.Locale | null) | undefined, logger?: (Logger | null) | undefined);
95
106
  private getChapterSentences;
@@ -107,4 +118,4 @@ declare class Aligner {
107
118
  }
108
119
  declare function concatTranscriptions(transcriptions: Pick<RecognitionResult, "transcript" | "timeline">[], audiofiles: string[]): StorytellerTranscription;
109
120
 
110
- export { type AlignOptions, Aligner, align, concatTranscriptions };
121
+ export { type AlignOptions, Aligner, type AudioFileContext, type AudioFileReport, type ChapterReport, type Report, type UnalignedAudioFileReport, type UnalignedChapterReason, type UnalignedChapterReport, type UnalignedNotFoundChapterReport, align, concatTranscriptions };
@@ -13,6 +13,8 @@ interface AudioFileContext {
13
13
  }
14
14
  interface ChapterReport {
15
15
  href: string;
16
+ score: number;
17
+ spineIndex: number;
16
18
  transcriptionOffset: number;
17
19
  endTranscriptionOffset: number;
18
20
  transcriptionContext: {
@@ -42,10 +44,14 @@ interface ChapterReport {
42
44
  type UnalignedChapterReason = "too-short" | "not-found" | "is-nav" | "no-text";
43
45
  interface UnalignedChapterReport {
44
46
  href: string;
47
+ spineIndex: number;
45
48
  reason: Exclude<UnalignedChapterReason, "not-found">;
46
49
  }
47
50
  interface UnalignedNotFoundChapterReport {
48
51
  href: string;
52
+ spineIndex: number;
53
+ chapterSentenceCount: number;
54
+ score: number;
49
55
  reason: "not-found";
50
56
  start: string;
51
57
  end: string;
@@ -61,6 +67,10 @@ interface AudioFileReport {
61
67
  }
62
68
  interface UnalignedAudioFileReport {
63
69
  filepath: string;
70
+ duration?: number | null;
71
+ transcription?: {
72
+ text: string;
73
+ };
64
74
  }
65
75
  interface Report {
66
76
  chapters: ChapterReport[];
@@ -90,6 +100,7 @@ declare class Aligner {
90
100
  private granularity;
91
101
  private textRef;
92
102
  private audioFileDurations;
103
+ private audiofileTranscripts;
93
104
  report: Report;
94
105
  constructor(epub: Epub, audiofiles: string[], transcriptions: Pick<RecognitionResult, "transcript" | "timeline">[], granularity: "sentence" | "word" | null | undefined, textRef: "id-fragment" | "text-fragment" | null | undefined, languageOverride?: (Intl.Locale | null) | undefined, logger?: (Logger | null) | undefined);
95
106
  private getChapterSentences;
@@ -107,4 +118,4 @@ declare class Aligner {
107
118
  }
108
119
  declare function concatTranscriptions(transcriptions: Pick<RecognitionResult, "transcript" | "timeline">[], audiofiles: string[]): StorytellerTranscription;
109
120
 
110
- export { type AlignOptions, Aligner, align, concatTranscriptions };
121
+ export { type AlignOptions, Aligner, type AudioFileContext, type AudioFileReport, type ChapterReport, type Report, type UnalignedAudioFileReport, type UnalignedChapterReason, type UnalignedChapterReport, type UnalignedNotFoundChapterReport, align, concatTranscriptions };
@@ -13,7 +13,6 @@ import {
13
13
  rm,
14
14
  writeFile
15
15
  } from "node:fs/promises";
16
- import { tmpdir } from "node:os";
17
16
  import {
18
17
  basename as autoBasename,
19
18
  dirname as autoDirname,
@@ -26,14 +25,15 @@ import { LocalizedString } from "@readium/shared";
26
25
  import { enumerate, max } from "itertools";
27
26
  import memoize from "memoize";
28
27
  import { ZipFile } from "yazl";
29
- import { isAudioFile, lookupAudioMime } from "@storyteller-platform/audiobook";
30
28
  import {
31
29
  Epub
32
30
  } from "@storyteller-platform/epub";
31
+ import { scratchPath } from "@storyteller-platform/fs";
33
32
  import {
34
33
  createAggregator,
35
34
  createTiming
36
35
  } from "@storyteller-platform/ghost-story";
36
+ import { MediaType } from "@storyteller-platform/media-types";
37
37
  import { getTrackDuration } from "../common/ffmpeg.js";
38
38
  import { parseDom } from "../markup/parseDom.js";
39
39
  import { segmentChapter } from "../markup/segmentation.js";
@@ -60,8 +60,7 @@ async function align(input, output, transcriptionsDir, audiobookDir, options) {
60
60
  var _stack2 = [];
61
61
  try {
62
62
  const outFormat = options.outFormat ?? "epub";
63
- const epubPath = outFormat === "epub" ? autoJoin(
64
- tmpdir(),
63
+ const epubPath = outFormat === "epub" ? scratchPath(
65
64
  `storyteller-platform-align-${randomUUID()}`,
66
65
  autoBasename(output)
67
66
  ) : input;
@@ -79,7 +78,7 @@ async function align(input, output, transcriptionsDir, audiobookDir, options) {
79
78
  await copyFile(input, epubPath);
80
79
  }
81
80
  const audiobookFiles = await readdir(audiobookDir).then(
82
- (filenames) => filenames.filter((f) => isAudioFile(f)).map((f) => autoJoin(audiobookDir, f))
81
+ (filenames) => filenames.filter((f) => MediaType.fromPath(f)?.kind === "audio").map((f) => autoJoin(audiobookDir, f))
83
82
  );
84
83
  const epub = __using(_stack2, await Epub.from(epubPath));
85
84
  const transcriptions = await readdir(transcriptionsDir).then(
@@ -127,8 +126,7 @@ async function align(input, output, transcriptionsDir, audiobookDir, options) {
127
126
  ),
128
127
  guidedNavigationDocuments
129
128
  );
130
- const tmpArchivePath = autoJoin(
131
- tmpdir(),
129
+ const tmpArchivePath = scratchPath(
132
130
  `storyteller-platform-epub-${randomUUID()}`
133
131
  );
134
132
  const { promise, resolve } = Promise.withResolvers();
@@ -190,6 +188,9 @@ class Aligner {
190
188
  this.languageOverride = languageOverride;
191
189
  this.logger = logger;
192
190
  this.transcription = concatTranscriptions(transcriptions, audiofiles);
191
+ audiofiles.forEach((audiofile, index) => {
192
+ this.audiofileTranscripts[audiofile] = transcriptions[index]?.transcript ?? "";
193
+ });
193
194
  this.getChapterSentences = memoize(this.getChapterSentences.bind(this));
194
195
  this.granularity = granularity ?? "sentence";
195
196
  this.textRef = textRef ?? "id-fragment";
@@ -201,6 +202,7 @@ class Aligner {
201
202
  granularity;
202
203
  textRef;
203
204
  audioFileDurations = {};
205
+ audiofileTranscripts = {};
204
206
  report = {
205
207
  chapters: [],
206
208
  unalignedChapters: [],
@@ -324,7 +326,7 @@ class Aligner {
324
326
  const duration = await getTrackDuration(audiofile);
325
327
  this.totalDuration += duration;
326
328
  const audio = await readFile(audiofile);
327
- const mediaType = lookupAudioMime(base) ?? void 0;
329
+ const mediaType = MediaType.fromPath(base)?.mime;
328
330
  await this.epub.addManifestItem(
329
331
  {
330
332
  id,
@@ -341,7 +343,7 @@ class Aligner {
341
343
  {
342
344
  id: mediaOverlayId,
343
345
  href: `MediaOverlays/${chapterStem}.smil`,
344
- mediaType: "application/smil+xml"
346
+ mediaType: MediaType.SMIL.mime
345
347
  },
346
348
  createMediaOverlay(
347
349
  chapter,
@@ -369,7 +371,7 @@ class Aligner {
369
371
  value: Epub.formatSmilDuration(chapterDuration)
370
372
  });
371
373
  }
372
- addChapterReport(chapter, chapterSentences, sentenceRanges, startSentence, endSentence, mapping, transcriptionOffset, endTranscriptionOffset) {
374
+ addChapterReport(spineIndex, chapter, chapterSentences, sentenceRanges, startSentence, endSentence, mapping, transcriptionOffset, endTranscriptionOffset, score) {
373
375
  const audioFiles = sentenceRanges.reduce(
374
376
  (acc, range) => {
375
377
  const existing = acc.find(
@@ -393,6 +395,8 @@ class Aligner {
393
395
  const mappedEndTranscriptionOffset = cursor.map(endTranscriptionOffset);
394
396
  this.report.chapters.push({
395
397
  href: chapter.href,
398
+ score,
399
+ spineIndex,
396
400
  transcriptionOffset: mappedTranscriptionOffset,
397
401
  endTranscriptionOffset: mappedEndTranscriptionOffset,
398
402
  transcriptionContext: {
@@ -460,7 +464,7 @@ class Aligner {
460
464
  }
461
465
  }
462
466
  }
463
- async alignChapter(chapterId, transcriptionText, transcriptionOffset, transcriptionEndOffset, locale, mappedTimeline, mapping) {
467
+ async alignChapter(spineIndex, chapterId, transcriptionText, transcriptionOffset, transcriptionEndOffset, locale, mappedTimeline, mapping) {
464
468
  const timing = createTiming();
465
469
  timing.start("read contents");
466
470
  const manifest = await this.epub.getManifest();
@@ -480,7 +484,8 @@ class Aligner {
480
484
  wordRanges,
481
485
  transcriptionOffset: endTranscriptionOffset,
482
486
  firstFoundSentence,
483
- lastFoundSentence
487
+ lastFoundSentence,
488
+ score
484
489
  } = await getSentenceRanges(
485
490
  transcriptionText,
486
491
  mappedTimeline,
@@ -499,7 +504,7 @@ class Aligner {
499
504
  Epub.addLinkToXhtmlHead(chapterXml, {
500
505
  rel: "stylesheet",
501
506
  href: storytellerStylesheetUrl,
502
- type: "text/css"
507
+ type: MediaType.CSS.mime
503
508
  });
504
509
  this.alignedChapters.push({
505
510
  chapter,
@@ -510,6 +515,7 @@ class Aligner {
510
515
  endOffset: endTranscriptionOffset
511
516
  });
512
517
  this.addChapterReport(
518
+ spineIndex,
513
519
  chapter,
514
520
  chapterSentences,
515
521
  sentenceRanges,
@@ -517,7 +523,8 @@ class Aligner {
517
523
  lastFoundSentence,
518
524
  mapping,
519
525
  transcriptionOffset,
520
- endTranscriptionOffset
526
+ endTranscriptionOffset,
527
+ score
521
528
  );
522
529
  return {
523
530
  lastSentenceRange: sentenceRanges.at(-1) ?? null,
@@ -569,7 +576,8 @@ class Aligner {
569
576
  if (manifest[chapterId]?.properties?.includes("nav")) {
570
577
  this.report.unalignedChapters.push({
571
578
  href: spineItem.href,
572
- reason: "is-nav"
579
+ reason: "is-nav",
580
+ spineIndex: index
573
581
  });
574
582
  continue;
575
583
  }
@@ -584,7 +592,8 @@ class Aligner {
584
592
  this.logger?.info(`Chapter #${index} has no text; skipping`);
585
593
  this.report.unalignedChapters.push({
586
594
  href: spineItem.href,
587
- reason: "no-text"
595
+ reason: "no-text",
596
+ spineIndex: index
588
597
  });
589
598
  continue;
590
599
  }
@@ -595,7 +604,8 @@ class Aligner {
595
604
  );
596
605
  this.report.unalignedChapters.push({
597
606
  href: spineItem.href,
598
- reason: "too-short"
607
+ reason: "too-short",
608
+ spineIndex: index
599
609
  });
600
610
  continue;
601
611
  }
@@ -610,6 +620,9 @@ class Aligner {
610
620
  this.report.unalignedChapters.push({
611
621
  href: spineItem.href,
612
622
  reason: "not-found",
623
+ spineIndex: index,
624
+ chapterSentenceCount: chapterSentences.length,
625
+ score: 0,
613
626
  start: chapterSentences.slice(0, 3).map((s) => s.text).join("").slice(0, 80),
614
627
  end: chapterSentences.slice(-3).map((s) => s.text).join("").slice(-80)
615
628
  });
@@ -620,6 +633,7 @@ class Aligner {
620
633
  continue;
621
634
  }
622
635
  const result = await this.alignChapter(
636
+ index,
623
637
  chapterId,
624
638
  transcriptionText,
625
639
  Math.max(start, 0),
@@ -685,7 +699,12 @@ class Aligner {
685
699
  }
686
700
  for (const audiofile of this.audiofiles) {
687
701
  if (!this.report.audioFiles.some(({ filepath }) => filepath === audiofile)) {
688
- this.report.unalignedAudioFiles.push({ filepath: audiofile });
702
+ const transcript = this.audiofileTranscripts[audiofile]?.trim();
703
+ this.report.unalignedAudioFiles.push({
704
+ filepath: audiofile,
705
+ duration: this.audioFileDurations[audiofile] ?? null,
706
+ ...transcript ? { transcription: { text: transcript.slice(0, 2e3) } } : {}
707
+ });
689
708
  }
690
709
  }
691
710
  await this.epub.addMetadata({
@@ -702,7 +721,7 @@ class Aligner {
702
721
  {
703
722
  id: "storyteller_readaloud_styles",
704
723
  href: "Styles/storyteller-readaloud.css",
705
- mediaType: "text/css"
724
+ mediaType: MediaType.CSS.mime
706
725
  },
707
726
  `
708
727
  .-epub-media-overlay-active {