@storyteller-platform/align 0.1.57 → 0.1.58

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/dist/align/align.cjs +38 -19
  2. package/dist/align/align.d.cts +12 -1
  3. package/dist/align/align.d.ts +12 -1
  4. package/dist/align/align.js +38 -19
  5. package/dist/align/ctc/Aligner.cjs +800 -0
  6. package/dist/align/ctc/Aligner.d.cts +56 -0
  7. package/dist/align/ctc/Aligner.d.ts +56 -0
  8. package/dist/align/ctc/Aligner.js +789 -0
  9. package/dist/align/ctc/align.cjs +189 -0
  10. package/dist/align/ctc/align.d.cts +14 -0
  11. package/dist/align/ctc/align.d.ts +14 -0
  12. package/dist/align/ctc/align.js +132 -0
  13. package/dist/{process/mime.cjs → align/ctc/emissions.cjs} +7 -20
  14. package/dist/align/ctc/emissions.d.cts +6 -0
  15. package/dist/align/ctc/emissions.d.ts +6 -0
  16. package/dist/align/ctc/emissions.js +7 -0
  17. package/dist/align/ctc/forcedAlign.cjs +273 -0
  18. package/dist/align/ctc/forcedAlign.d.cts +21 -0
  19. package/dist/align/ctc/forcedAlign.d.ts +21 -0
  20. package/dist/align/ctc/forcedAlign.js +248 -0
  21. package/dist/align/ctc/greedyDecode.cjs +61 -0
  22. package/dist/align/ctc/greedyDecode.d.cts +15 -0
  23. package/dist/align/ctc/greedyDecode.d.ts +15 -0
  24. package/dist/align/ctc/greedyDecode.js +37 -0
  25. package/dist/align/ctc/mediaOverlay.cjs +248 -0
  26. package/dist/align/ctc/mediaOverlay.d.cts +16 -0
  27. package/dist/align/ctc/mediaOverlay.d.ts +16 -0
  28. package/dist/align/ctc/mediaOverlay.js +224 -0
  29. package/dist/align/ctc/report.cjs +66 -0
  30. package/dist/align/ctc/report.d.cts +79 -0
  31. package/dist/align/ctc/report.d.ts +79 -0
  32. package/dist/align/ctc/report.js +42 -0
  33. package/dist/align/ctc/search.cjs +437 -0
  34. package/dist/align/ctc/search.d.cts +46 -0
  35. package/dist/align/ctc/search.d.ts +46 -0
  36. package/dist/align/ctc/search.js +412 -0
  37. package/dist/align/getSentenceRanges.cjs +9 -3
  38. package/dist/align/getSentenceRanges.d.cts +1 -0
  39. package/dist/align/getSentenceRanges.d.ts +1 -0
  40. package/dist/align/getSentenceRanges.js +9 -3
  41. package/dist/align/parse.cjs +17 -4
  42. package/dist/align/parse.d.cts +8 -4
  43. package/dist/align/parse.d.ts +8 -4
  44. package/dist/align/parse.js +19 -4
  45. package/dist/align/search.cjs +76 -23
  46. package/dist/align/search.d.cts +8 -4
  47. package/dist/align/search.d.ts +8 -4
  48. package/dist/align/search.js +76 -24
  49. package/dist/align/slugify.cjs +3 -2
  50. package/dist/align/slugify.d.cts +1 -1
  51. package/dist/align/slugify.d.ts +1 -1
  52. package/dist/align/slugify.js +3 -2
  53. package/dist/cli/bin.cjs +204 -114
  54. package/dist/cli/bin.js +194 -103
  55. package/dist/common/ffmpeg.cjs +10 -5
  56. package/dist/common/ffmpeg.js +10 -5
  57. package/dist/common/logging.cjs +33 -1
  58. package/dist/common/logging.js +33 -1
  59. package/dist/emit/emit.cjs +105 -0
  60. package/dist/emit/emit.d.cts +15 -0
  61. package/dist/emit/emit.d.ts +15 -0
  62. package/dist/emit/emit.js +86 -0
  63. package/dist/emit/fs.cjs +227 -0
  64. package/dist/emit/fs.d.cts +22 -0
  65. package/dist/emit/fs.d.ts +22 -0
  66. package/dist/emit/fs.js +162 -0
  67. package/dist/emit/parse.cjs +68 -0
  68. package/dist/emit/parse.d.cts +26 -0
  69. package/dist/emit/parse.d.ts +26 -0
  70. package/dist/emit/parse.js +58 -0
  71. package/dist/index.d.cts +1 -1
  72. package/dist/index.d.ts +1 -1
  73. package/dist/markup/model.cjs +0 -16
  74. package/dist/markup/model.d.cts +1 -1
  75. package/dist/markup/model.d.ts +1 -1
  76. package/dist/markup/model.js +0 -12
  77. package/dist/markup/parseDom.cjs +13 -14
  78. package/dist/markup/parseDom.d.cts +2 -2
  79. package/dist/markup/parseDom.d.ts +2 -2
  80. package/dist/markup/parseDom.js +13 -18
  81. package/dist/markup/resolvedPos.d.cts +1 -1
  82. package/dist/markup/resolvedPos.d.ts +1 -1
  83. package/dist/markup/segmentation.cjs +3 -1
  84. package/dist/markup/segmentation.js +3 -1
  85. package/dist/markup/serializeDom.cjs +1 -1
  86. package/dist/markup/serializeDom.d.cts +1 -1
  87. package/dist/markup/serializeDom.d.ts +1 -1
  88. package/dist/markup/serializeDom.js +1 -1
  89. package/dist/markup/transform.cjs +5 -2
  90. package/dist/markup/transform.d.cts +1 -1
  91. package/dist/markup/transform.d.ts +1 -1
  92. package/dist/markup/transform.js +5 -4
  93. package/dist/{model-CZ2mMHJO.d.cts → model-CeOMDDgt.d.cts} +3 -11
  94. package/dist/{model-CZ2mMHJO.d.ts → model-CeOMDDgt.d.ts} +3 -11
  95. package/dist/process/processAudiobook.cjs +12 -23
  96. package/dist/process/processAudiobook.js +14 -30
  97. package/dist/process/ranges.cjs +2 -2
  98. package/dist/process/ranges.js +2 -2
  99. package/dist/readium/guidedNavigation.cjs +8 -8
  100. package/dist/readium/guidedNavigation.js +8 -8
  101. package/dist/readium/manifest.cjs +5 -4
  102. package/dist/readium/manifest.js +5 -4
  103. package/dist/snapshot/ctc/snapshot.cjs +261 -0
  104. package/dist/snapshot/ctc/snapshot.d.cts +6 -0
  105. package/dist/snapshot/ctc/snapshot.d.ts +6 -0
  106. package/dist/snapshot/ctc/snapshot.js +200 -0
  107. package/dist/snapshot/parse.cjs +29 -16
  108. package/dist/snapshot/parse.d.cts +14 -10
  109. package/dist/snapshot/parse.d.ts +14 -10
  110. package/dist/snapshot/parse.js +32 -17
  111. package/dist/transcribe/parse.d.cts +6 -6
  112. package/dist/transcribe/parse.d.ts +6 -6
  113. package/dist/transcribe/transcribe.cjs +4 -2
  114. package/dist/transcribe/transcribe.js +4 -2
  115. package/package.json +6 -4
  116. package/dist/process/mime.d.cts +0 -3
  117. package/dist/process/mime.d.ts +0 -3
  118. package/dist/process/mime.js +0 -24
@@ -0,0 +1,412 @@
1
+ import "../../chunk-BIEQXUOY.js";
2
+ import { enumerate, range } from "itertools";
3
+ import { ctcGreedyDecode } from "./greedyDecode.js";
4
+ const NGRAM_SIZE = 10;
5
+ const RANSAC_ITERATIONS = 500;
6
+ const MIN_INLIERS = 10;
7
+ const MAX_PAIR_SEPARATION_REQUIREMENT = 200;
8
+ const MIN_CONFIDENCE = 0.5;
9
+ const MIN_COVERAGE = 0.3;
10
+ const REFINE_COUNT = 25;
11
+ const ANCHOR_SPACING = 2e3;
12
+ const BOUNDARY_PAD_FRAMES = 500;
13
+ const EVIDENCE_GAP_MULTIPLIER = 3;
14
+ const EVIDENCE_GAP_FLOOR = 500;
15
+ const EVIDENCE_GAP_PERCENTILE = 0.99;
16
+ const MIN_EDGE_CLUSTER_OFFSETS = 10;
17
+ const EDGE_EXTENSION_MAX_GAP = 1e3;
18
+ const EDGE_EXTENSION_WINDOW = 250;
19
+ const EDGE_EXTENSION_PAIR_SEPARATION = 50;
20
+ const ANCHOR_CONTEXT = 40;
21
+ const ANCHOR_AGREEMENT_FRACTION = 0.4;
22
+ const ANCHOR_AGREEMENT_FLOOR = 0.05;
23
+ const ANCHOR_CALIBRATION_MINIMUM = 5;
24
+ const ngramIndexes = /* @__PURE__ */ new WeakMap();
25
+ const decodedEmissions = /* @__PURE__ */ new WeakMap();
26
+ function median(values) {
27
+ const sorted = Array.from(values).sort((a, b) => a - b);
28
+ return sorted[Math.floor(sorted.length / 2)];
29
+ }
30
+ function getNgramIndex(document) {
31
+ const cached = ngramIndexes.get(document);
32
+ if (cached) return cached;
33
+ const { text, positions } = document;
34
+ const index = /* @__PURE__ */ new Map();
35
+ for (const i of range(text.length - NGRAM_SIZE + 1)) {
36
+ const ngram = text.slice(i, i + NGRAM_SIZE);
37
+ const position = positions[i];
38
+ const entries = index.get(ngram);
39
+ if (entries) {
40
+ entries.push(position);
41
+ } else {
42
+ index.set(ngram, [position]);
43
+ }
44
+ }
45
+ ngramIndexes.set(document, index);
46
+ return index;
47
+ }
48
+ function collectMatches(query, document, excludedRanges) {
49
+ const index = getNgramIndex(document);
50
+ const queryNgramOffsets = /* @__PURE__ */ new Map();
51
+ const matches = [];
52
+ for (const i of range(query.length - NGRAM_SIZE + 1)) {
53
+ const queryNgram = query.slice(i, i + NGRAM_SIZE);
54
+ const offsets = queryNgramOffsets.get(queryNgram);
55
+ if (offsets) offsets.push(i);
56
+ else queryNgramOffsets.set(queryNgram, [i]);
57
+ const positions = index.get(queryNgram);
58
+ if (!positions) continue;
59
+ for (const position of positions) {
60
+ if (excludedRanges.some(
61
+ ([start, end]) => start <= position && position < end
62
+ )) {
63
+ continue;
64
+ }
65
+ matches.push({ offset: i, position });
66
+ }
67
+ }
68
+ return { matches, queryNgramOffsets };
69
+ }
70
+ function fitLine(matches, pairSeparation, { minSlope, maxSlope, inlierTolerance }) {
71
+ let seed = 625341585;
72
+ const random = () => {
73
+ seed = Math.imul(seed, 1103515245) + 12345 & 2147483647;
74
+ return seed / 2147483648;
75
+ };
76
+ let best = null;
77
+ for (let i = 0; i < RANSAC_ITERATIONS; i++) {
78
+ const a = matches[Math.floor(random() * matches.length)];
79
+ const b = matches[Math.floor(random() * matches.length)];
80
+ if (Math.abs(b.offset - a.offset) < pairSeparation) continue;
81
+ const slope2 = (b.position - a.position) / (b.offset - a.offset);
82
+ if (slope2 < minSlope || slope2 > maxSlope) continue;
83
+ const intercept2 = a.position - slope2 * a.offset;
84
+ let count = 0;
85
+ for (const m of matches) {
86
+ const predicted = intercept2 + slope2 * m.offset;
87
+ if (Math.abs(m.position - predicted) <= inlierTolerance) count++;
88
+ }
89
+ if (!best || count > best.count) {
90
+ best = { slope: slope2, intercept: intercept2, count };
91
+ }
92
+ }
93
+ if (!best || best.count < MIN_INLIERS) return null;
94
+ const inliers = matches.filter((m) => {
95
+ const predicted = best.intercept + best.slope * m.offset;
96
+ return Math.abs(m.position - predicted) <= inlierTolerance;
97
+ });
98
+ const sorted = inliers.sort((a, b) => a.offset - b.offset);
99
+ const stride = Math.max(Math.floor(sorted.length / 2), 1);
100
+ const slopes = [];
101
+ for (let i = 0; i + stride < sorted.length; i++) {
102
+ const lo = sorted[i];
103
+ const hi = sorted[i + stride];
104
+ if (hi.offset - lo.offset < pairSeparation) continue;
105
+ slopes.push((hi.position - lo.position) / (hi.offset - lo.offset));
106
+ }
107
+ const slope = slopes.length ? median(slopes) : best.slope;
108
+ const intercept = sorted.length ? median(sorted.map((m) => m.position - slope * m.offset)) : best.intercept;
109
+ return {
110
+ slope,
111
+ intercept,
112
+ inliers
113
+ };
114
+ }
115
+ function bigramSimilarity(a, b) {
116
+ if (a.length < 2 || b.length < 2) return 0;
117
+ const bigrams = /* @__PURE__ */ new Map();
118
+ for (const i of range(a.length - 1)) {
119
+ const bigram = a.slice(i, i + 2);
120
+ bigrams.set(bigram, (bigrams.get(bigram) ?? 0) + 1);
121
+ }
122
+ let shared = 0;
123
+ for (const i of range(b.length - 1)) {
124
+ const bigram = b.slice(i, i + 2);
125
+ const remaining = bigrams.get(bigram) ?? 0;
126
+ if (remaining > 0) {
127
+ shared++;
128
+ bigrams.set(bigram, remaining - 1);
129
+ }
130
+ }
131
+ return shared / Math.max(a.length - 1, b.length - 1);
132
+ }
133
+ function positionToDocumentIndex(document, position) {
134
+ const { positions } = document;
135
+ let low = 0;
136
+ let high = positions.length - 1;
137
+ while (low < high) {
138
+ const mid = low + high >> 1;
139
+ if (positions[mid] < position) low = mid + 1;
140
+ else high = mid;
141
+ }
142
+ return low;
143
+ }
144
+ function contextAgreement(match, query, document) {
145
+ const index = positionToDocumentIndex(document, match.position);
146
+ const queryLeft = query.slice(
147
+ Math.max(0, match.offset - ANCHOR_CONTEXT),
148
+ match.offset
149
+ );
150
+ const documentLeft = document.text.slice(
151
+ Math.max(0, index - ANCHOR_CONTEXT),
152
+ index
153
+ );
154
+ const queryRight = query.slice(
155
+ match.offset + NGRAM_SIZE,
156
+ match.offset + NGRAM_SIZE + ANCHOR_CONTEXT
157
+ );
158
+ const documentRight = document.text.slice(
159
+ index + NGRAM_SIZE,
160
+ index + NGRAM_SIZE + ANCHOR_CONTEXT
161
+ );
162
+ return Math.max(
163
+ bigramSimilarity(queryLeft, documentLeft),
164
+ bigramSimilarity(queryRight, documentRight)
165
+ );
166
+ }
167
+ function selectAnchors(query, inliers, queryNgramOffsets, document, slope, intercept) {
168
+ const documentNgramIndex = getNgramIndex(document);
169
+ const agreements = /* @__PURE__ */ new Map();
170
+ const agreement = (match) => {
171
+ let score = agreements.get(match);
172
+ if (score === void 0) {
173
+ score = contextAgreement(match, query, document);
174
+ agreements.set(match, score);
175
+ }
176
+ return score;
177
+ };
178
+ const select = (minAgreement) => {
179
+ const anchors2 = [];
180
+ for (let i = 0; i < query.length; i += ANCHOR_SPACING) {
181
+ const windowStart = Math.max(0, i - ANCHOR_SPACING / 2);
182
+ const windowEnd = Math.min(query.length, i + ANCHOR_SPACING / 2);
183
+ const candidates = inliers.filter(({ offset }) => windowStart <= offset && offset < windowEnd).sort(
184
+ ({ offset: a }, { offset: b }) => Math.abs(a - i) - Math.abs(b - i)
185
+ );
186
+ for (const c of candidates) {
187
+ const gram = query.slice(c.offset, c.offset + NGRAM_SIZE);
188
+ if (documentNgramIndex.get(gram)?.length !== 1) {
189
+ continue;
190
+ }
191
+ if (queryNgramOffsets.get(gram)?.length !== 1) {
192
+ continue;
193
+ }
194
+ if (minAgreement !== null && agreement(c) < minAgreement) {
195
+ continue;
196
+ }
197
+ anchors2.push(c);
198
+ break;
199
+ }
200
+ }
201
+ return anchors2;
202
+ };
203
+ let anchors = select(null);
204
+ if (anchors.length >= ANCHOR_CALIBRATION_MINIMUM) {
205
+ const threshold = Math.max(
206
+ ANCHOR_AGREEMENT_FRACTION * median(anchors.map(agreement)),
207
+ ANCHOR_AGREEMENT_FLOOR
208
+ );
209
+ anchors = select(threshold);
210
+ }
211
+ const monotonicAnchors = [];
212
+ for (const a of anchors) {
213
+ while (true) {
214
+ const b = monotonicAnchors.at(-1);
215
+ if (!b || a.position - b.position >= a.offset - b.offset) {
216
+ monotonicAnchors.push(a);
217
+ break;
218
+ }
219
+ const aDist = Math.abs(a.position - (intercept + slope * a.offset));
220
+ const bDist = Math.abs(b.position - (intercept + slope * b.offset));
221
+ if (aDist > bDist) break;
222
+ else monotonicAnchors.pop();
223
+ }
224
+ }
225
+ return monotonicAnchors;
226
+ }
227
+ function fitLocalLine(points, fallbackSlope) {
228
+ const stride = Math.max(Math.floor(points.length / 2), 1);
229
+ const slopes = [];
230
+ for (const i of range(0, points.length - stride, stride)) {
231
+ const lo = points[i];
232
+ const hi = points[i + stride];
233
+ if (hi.offset - lo.offset < EDGE_EXTENSION_PAIR_SEPARATION) continue;
234
+ slopes.push((hi.position - lo.position) / (hi.offset - lo.offset));
235
+ }
236
+ const slope = slopes.length ? median(slopes) : fallbackSlope;
237
+ const intercept = median(points.map((m) => m.position - slope * m.offset));
238
+ return { slope, intercept };
239
+ }
240
+ function extendEdge(sortedMatches, sortedInliers, globalSlope, inlierTolerance) {
241
+ const lastInlier = sortedInliers.at(-1);
242
+ if (!lastInlier) return [];
243
+ const window = sortedInliers.slice(-EDGE_EXTENSION_WINDOW);
244
+ const accepted = [];
245
+ let lastAcceptedOffset = lastInlier.offset;
246
+ let local = fitLocalLine(window, globalSlope);
247
+ for (const match of sortedMatches) {
248
+ if (match.offset <= lastInlier.offset) continue;
249
+ if (match.offset - lastAcceptedOffset > EDGE_EXTENSION_MAX_GAP) break;
250
+ const predicted = local.intercept + local.slope * match.offset;
251
+ if (Math.abs(match.position - predicted) > inlierTolerance) continue;
252
+ accepted.push(match);
253
+ window.push(match);
254
+ if (window.length > EDGE_EXTENSION_WINDOW) window.shift();
255
+ lastAcceptedOffset = match.offset;
256
+ local = fitLocalLine(window, globalSlope);
257
+ }
258
+ return accepted;
259
+ }
260
+ function extendInlierEdges(matches, sortedInliers, slope, inlierTolerance) {
261
+ const byOffset = matches.toSorted((a, b) => a.offset - b.offset);
262
+ const tail = extendEdge(byOffset, sortedInliers, slope, inlierTolerance);
263
+ function mirror(m) {
264
+ return { offset: -m.offset, position: -m.position };
265
+ }
266
+ const head = extendEdge(
267
+ byOffset.map(mirror).reverse(),
268
+ sortedInliers.map(mirror).reverse(),
269
+ slope,
270
+ inlierTolerance
271
+ ).map(mirror).reverse();
272
+ return [...head, ...sortedInliers, ...tail];
273
+ }
274
+ function computeEvidenceRange(offsets, queryLength) {
275
+ if (offsets.length < 2) {
276
+ return { evidenceRange: [0, queryLength], matchedRange: [0, queryLength] };
277
+ }
278
+ const gaps = [];
279
+ for (let i = 1; i < offsets.length; i++) {
280
+ gaps.push(offsets[i] - offsets[i - 1]);
281
+ }
282
+ const sortedGaps = gaps.toSorted((a, b) => a - b);
283
+ const p99 = sortedGaps[Math.min(
284
+ sortedGaps.length - 1,
285
+ Math.floor(sortedGaps.length * EVIDENCE_GAP_PERCENTILE)
286
+ )];
287
+ const threshold = Math.max(EVIDENCE_GAP_MULTIPLIER * p99, EVIDENCE_GAP_FLOOR);
288
+ const clusters = [];
289
+ let clusterStart = 0;
290
+ for (const [i, gap] of enumerate(gaps, 1)) {
291
+ if (gap <= threshold) continue;
292
+ clusters.push([clusterStart, i - 1]);
293
+ clusterStart = i;
294
+ }
295
+ clusters.push([clusterStart, offsets.length - 1]);
296
+ let firstCluster = 0;
297
+ let lastCluster = clusters.length - 1;
298
+ while (firstCluster < lastCluster && clusters[firstCluster][1] - clusters[firstCluster][0] + 1 < MIN_EDGE_CLUSTER_OFFSETS) {
299
+ firstCluster++;
300
+ }
301
+ while (lastCluster > firstCluster && clusters[lastCluster][1] - clusters[lastCluster][0] + 1 < MIN_EDGE_CLUSTER_OFFSETS) {
302
+ lastCluster--;
303
+ }
304
+ const first = offsets[clusters[firstCluster][0]];
305
+ const last = offsets[clusters[lastCluster][1]] + NGRAM_SIZE;
306
+ return {
307
+ matchedRange: [first, last],
308
+ evidenceRange: [
309
+ first > threshold ? first - threshold : 0,
310
+ queryLength - last > threshold ? last + threshold : queryLength
311
+ ]
312
+ };
313
+ }
314
+ function findBoundariesInDocument(query, document, options) {
315
+ const gramCount = Math.max(query.length - NGRAM_SIZE + 1, 0);
316
+ const pairSeparation = Math.min(
317
+ Math.max(Math.floor(gramCount / 4), 20),
318
+ MAX_PAIR_SEPARATION_REQUIREMENT
319
+ );
320
+ const { matches, queryNgramOffsets } = collectMatches(
321
+ query,
322
+ document,
323
+ options.excludedRanges
324
+ );
325
+ if (matches.length < MIN_INLIERS) return null;
326
+ const line = fitLine(matches, pairSeparation, options);
327
+ if (!line) return null;
328
+ const { slope, intercept, inliers } = line;
329
+ const byOffset = extendInlierEdges(
330
+ matches,
331
+ inliers.sort((a, b) => a.offset - b.offset),
332
+ slope,
333
+ options.inlierTolerance
334
+ );
335
+ const inlierOffsets = Array.from(
336
+ new Set(byOffset.map(({ offset }) => offset))
337
+ );
338
+ const { evidenceRange, matchedRange } = computeEvidenceRange(
339
+ inlierOffsets,
340
+ query.length
341
+ );
342
+ const head = byOffset.slice(0, REFINE_COUNT);
343
+ const tail = byOffset.slice(-REFINE_COUNT);
344
+ const extrapolatedStart = Math.min(
345
+ ...head.map((m) => m.position - slope * m.offset)
346
+ );
347
+ const extrapolatedEnd = Math.max(
348
+ ...tail.map((m) => m.position + slope * (query.length - m.offset))
349
+ );
350
+ const startPad = Math.max(
351
+ BOUNDARY_PAD_FRAMES,
352
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
353
+ 0.25 * (head[0].position - extrapolatedStart)
354
+ );
355
+ const start = extrapolatedStart - startPad;
356
+ const endPad = Math.max(
357
+ BOUNDARY_PAD_FRAMES,
358
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
359
+ 0.25 * (extrapolatedEnd - tail.at(-1).position)
360
+ );
361
+ const end = extrapolatedEnd + endPad;
362
+ if (end <= start) return null;
363
+ const matchedGrams = new Set(matches.map((m) => m.offset)).size;
364
+ const inlierGrams = inlierOffsets.length;
365
+ const confidence = inlierGrams / matchedGrams;
366
+ if (confidence < MIN_CONFIDENCE) return null;
367
+ const first = byOffset[0];
368
+ const last = byOffset[byOffset.length - 1];
369
+ const coverage = (last.offset - first.offset + NGRAM_SIZE) / query.length;
370
+ if (coverage < MIN_COVERAGE) return null;
371
+ const anchors = selectAnchors(
372
+ query,
373
+ inliers,
374
+ queryNgramOffsets,
375
+ document,
376
+ slope,
377
+ intercept
378
+ );
379
+ return {
380
+ start: Math.round(start),
381
+ end: Math.round(end),
382
+ confidence,
383
+ anchors,
384
+ evidenceRange,
385
+ matchedRange
386
+ };
387
+ }
388
+ async function findCtcBoundaries(query, emissionsReader, excludedRanges) {
389
+ let document = decodedEmissions.get(emissionsReader);
390
+ if (!document) {
391
+ const { text, frames } = await ctcGreedyDecode(emissionsReader);
392
+ document = { text, positions: frames };
393
+ decodedEmissions.set(emissionsReader, document);
394
+ }
395
+ const result = findBoundariesInDocument(query, document, {
396
+ excludedRanges,
397
+ minSlope: 2,
398
+ maxSlope: 15,
399
+ inlierTolerance: 2500
400
+ });
401
+ if (!result) return null;
402
+ return {
403
+ ...result,
404
+ start: Math.max(result.start, 0),
405
+ end: Math.min(result.end, emissionsReader.numFrames - 1)
406
+ };
407
+ }
408
+ export {
409
+ computeEvidenceRange,
410
+ findBoundariesInDocument,
411
+ findCtcBoundaries
412
+ };
@@ -74,7 +74,7 @@ function getAlignmentsForSentence(sentence, alignments) {
74
74
  }
75
75
  return {
76
76
  alignments: result,
77
- score: result.some((a) => a.opType === "MATCH") ? score : -1
77
+ score: result.some((a) => a.opType === "MATCH") ? score / sentence.length : -1
78
78
  };
79
79
  }
80
80
  function errorAlignWithNarrowing(refSentences, hyp, narrowStart, narrowEnd) {
@@ -160,6 +160,8 @@ async function getSentenceRanges(transcriptionText, mappedTimeline, sentences, c
160
160
  const { result } = await (0, import_slugify.slugify)(s.text, locale);
161
161
  slugifiedChapterSentences.push(result);
162
162
  }
163
+ let totalScore = 0;
164
+ let totalSentences = 0;
163
165
  let firstFoundSentence = 0;
164
166
  let lastFoundSentence = sentences.length - 1;
165
167
  let chapterTranscriptEndIndex = chapterOffset;
@@ -225,6 +227,8 @@ async function getSentenceRanges(transcriptionText, mappedTimeline, sentences, c
225
227
  slugifiedSentence,
226
228
  alignments.slice(alignmentIndex)
227
229
  );
230
+ totalScore += score;
231
+ totalSentences += 1;
228
232
  const sentenceLengthInSlugifiedTranscript = sentenceAlignments.filter((a) => a.opType !== "DELETE").map((a) => a.hyp).join("-").length;
229
233
  if (score > 0) {
230
234
  lastGoodSentenceIndex = j + slice[0];
@@ -316,7 +320,8 @@ async function getSentenceRanges(transcriptionText, mappedTimeline, sentences, c
316
320
  lastFoundSentence: Math.max(
317
321
  firstFoundSentence,
318
322
  chapterSentenceIndex - 1
319
- )
323
+ ),
324
+ score: totalScore / totalSentences * 100 + 50
320
325
  };
321
326
  }
322
327
  chapterSentenceIndex += lastGoodSentenceIndex + 1;
@@ -330,7 +335,8 @@ async function getSentenceRanges(transcriptionText, mappedTimeline, sentences, c
330
335
  wordRanges,
331
336
  transcriptionOffset: chapterTranscriptEndIndex,
332
337
  firstFoundSentence,
333
- lastFoundSentence
338
+ lastFoundSentence,
339
+ score: totalScore / totalSentences * 100 + 50
334
340
  };
335
341
  }
336
342
  function expandEmptySentenceRanges(sentenceRanges) {
@@ -53,6 +53,7 @@ declare function getSentenceRanges(transcriptionText: string, mappedTimeline: Ma
53
53
  transcriptionOffset: number;
54
54
  firstFoundSentence: number;
55
55
  lastFoundSentence: number;
56
+ score: number;
56
57
  }>;
57
58
  /**
58
59
  * Whisper sometimes provides words with no time information,
@@ -53,6 +53,7 @@ declare function getSentenceRanges(transcriptionText: string, mappedTimeline: Ma
53
53
  transcriptionOffset: number;
54
54
  firstFoundSentence: number;
55
55
  lastFoundSentence: number;
56
+ score: number;
56
57
  }>;
57
58
  /**
58
59
  * Whisper sometimes provides words with no time information,
@@ -47,7 +47,7 @@ function getAlignmentsForSentence(sentence, alignments) {
47
47
  }
48
48
  return {
49
49
  alignments: result,
50
- score: result.some((a) => a.opType === "MATCH") ? score : -1
50
+ score: result.some((a) => a.opType === "MATCH") ? score / sentence.length : -1
51
51
  };
52
52
  }
53
53
  function errorAlignWithNarrowing(refSentences, hyp, narrowStart, narrowEnd) {
@@ -133,6 +133,8 @@ async function getSentenceRanges(transcriptionText, mappedTimeline, sentences, c
133
133
  const { result } = await slugify(s.text, locale);
134
134
  slugifiedChapterSentences.push(result);
135
135
  }
136
+ let totalScore = 0;
137
+ let totalSentences = 0;
136
138
  let firstFoundSentence = 0;
137
139
  let lastFoundSentence = sentences.length - 1;
138
140
  let chapterTranscriptEndIndex = chapterOffset;
@@ -198,6 +200,8 @@ async function getSentenceRanges(transcriptionText, mappedTimeline, sentences, c
198
200
  slugifiedSentence,
199
201
  alignments.slice(alignmentIndex)
200
202
  );
203
+ totalScore += score;
204
+ totalSentences += 1;
201
205
  const sentenceLengthInSlugifiedTranscript = sentenceAlignments.filter((a) => a.opType !== "DELETE").map((a) => a.hyp).join("-").length;
202
206
  if (score > 0) {
203
207
  lastGoodSentenceIndex = j + slice[0];
@@ -289,7 +293,8 @@ async function getSentenceRanges(transcriptionText, mappedTimeline, sentences, c
289
293
  lastFoundSentence: Math.max(
290
294
  firstFoundSentence,
291
295
  chapterSentenceIndex - 1
292
- )
296
+ ),
297
+ score: totalScore / totalSentences * 100 + 50
293
298
  };
294
299
  }
295
300
  chapterSentenceIndex += lastGoodSentenceIndex + 1;
@@ -303,7 +308,8 @@ async function getSentenceRanges(transcriptionText, mappedTimeline, sentences, c
303
308
  wordRanges,
304
309
  transcriptionOffset: chapterTranscriptEndIndex,
305
310
  firstFoundSentence,
306
- lastFoundSentence
311
+ lastFoundSentence,
312
+ score: totalScore / totalSentences * 100 + 50
307
313
  };
308
314
  }
309
315
  function expandEmptySentenceRanges(sentenceRanges) {
@@ -55,10 +55,6 @@ const alignCommand = (0, import_core.command)(
55
55
  (0, import_core.merge)(
56
56
  (0, import_core.object)({
57
57
  action: (0, import_core.constant)("align"),
58
- transcriptions: (0, import_core.option)(
59
- "--transcriptions",
60
- (0, import_valueparser.path)({ mustExist: true, type: "directory" })
61
- ),
62
58
  output: (0, import_core.option)(
63
59
  "--output",
64
60
  // TODO: I think it should be possible to pick the correct
@@ -66,6 +62,23 @@ const alignCommand = (0, import_core.command)(
66
62
  (0, import_valueparser.path)({ type: "file", extensions: [".epub", ".gnp"] })
67
63
  )
68
64
  }),
65
+ (0, import_core.or)(
66
+ (0, import_core.object)({
67
+ ctc: (0, import_core.flag)("--ctc", {
68
+ description: import_core.message`Use CTC emissions instead of transcriptions for alignment`
69
+ }),
70
+ emissions: (0, import_core.option)(
71
+ "--emissions",
72
+ (0, import_valueparser.path)({ mustExist: true, type: "directory" })
73
+ )
74
+ }),
75
+ (0, import_core.object)({
76
+ transcriptions: (0, import_core.option)(
77
+ "--transcriptions",
78
+ (0, import_valueparser.path)({ mustExist: true, type: "directory" })
79
+ )
80
+ })
81
+ ),
69
82
  alignParser,
70
83
  import_parse.autoUpgradeParser,
71
84
  import_parse.removeNcxParser,
@@ -13,11 +13,15 @@ declare const alignParser: _optique_core.Parser<"sync", {
13
13
  readonly reports: [_optique_core.ValueParserResult<string> | undefined] | undefined;
14
14
  readonly outFormat: [_optique_core.ValueParserResult<"epub" | "gnp"> | undefined] | undefined;
15
15
  }>;
16
- declare const alignCommand: _optique_core.Parser<"sync", {
16
+ declare const alignCommand: _optique_core.Parser<"sync", (({
17
17
  readonly action: "align";
18
- readonly transcriptions: string;
19
18
  readonly output: string;
20
- } & {
19
+ } & ({
20
+ readonly ctc: true;
21
+ readonly emissions: string;
22
+ } | {
23
+ readonly transcriptions: string;
24
+ })) & {
21
25
  readonly audiobook: string;
22
26
  readonly epub: string;
23
27
  readonly textRef: "id-fragment" | "text-fragment";
@@ -25,7 +29,7 @@ declare const alignCommand: _optique_core.Parser<"sync", {
25
29
  readonly outFormat: "epub" | "gnp";
26
30
  } & {
27
31
  readonly autoupgrade: boolean;
28
- } & {
32
+ }) & {
29
33
  readonly removeNcx: boolean;
30
34
  } & {
31
35
  readonly noProgress: boolean;
@@ -13,11 +13,15 @@ declare const alignParser: _optique_core.Parser<"sync", {
13
13
  readonly reports: [_optique_core.ValueParserResult<string> | undefined] | undefined;
14
14
  readonly outFormat: [_optique_core.ValueParserResult<"epub" | "gnp"> | undefined] | undefined;
15
15
  }>;
16
- declare const alignCommand: _optique_core.Parser<"sync", {
16
+ declare const alignCommand: _optique_core.Parser<"sync", (({
17
17
  readonly action: "align";
18
- readonly transcriptions: string;
19
18
  readonly output: string;
20
- } & {
19
+ } & ({
20
+ readonly ctc: true;
21
+ readonly emissions: string;
22
+ } | {
23
+ readonly transcriptions: string;
24
+ })) & {
21
25
  readonly audiobook: string;
22
26
  readonly epub: string;
23
27
  readonly textRef: "id-fragment" | "text-fragment";
@@ -25,7 +29,7 @@ declare const alignCommand: _optique_core.Parser<"sync", {
25
29
  readonly outFormat: "epub" | "gnp";
26
30
  } & {
27
31
  readonly autoupgrade: boolean;
28
- } & {
32
+ }) & {
29
33
  readonly removeNcx: boolean;
30
34
  } & {
31
35
  readonly noProgress: boolean;
@@ -3,11 +3,13 @@ import {
3
3
  choice,
4
4
  command,
5
5
  constant,
6
+ flag,
6
7
  merge,
7
8
  message,
8
9
  object,
9
10
  option,
10
11
  optional,
12
+ or,
11
13
  withDefault
12
14
  } from "@optique/core";
13
15
  import { path } from "@optique/run/valueparser";
@@ -48,10 +50,6 @@ const alignCommand = command(
48
50
  merge(
49
51
  object({
50
52
  action: constant("align"),
51
- transcriptions: option(
52
- "--transcriptions",
53
- path({ mustExist: true, type: "directory" })
54
- ),
55
53
  output: option(
56
54
  "--output",
57
55
  // TODO: I think it should be possible to pick the correct
@@ -59,6 +57,23 @@ const alignCommand = command(
59
57
  path({ type: "file", extensions: [".epub", ".gnp"] })
60
58
  )
61
59
  }),
60
+ or(
61
+ object({
62
+ ctc: flag("--ctc", {
63
+ description: message`Use CTC emissions instead of transcriptions for alignment`
64
+ }),
65
+ emissions: option(
66
+ "--emissions",
67
+ path({ mustExist: true, type: "directory" })
68
+ )
69
+ }),
70
+ object({
71
+ transcriptions: option(
72
+ "--transcriptions",
73
+ path({ mustExist: true, type: "directory" })
74
+ )
75
+ })
76
+ ),
62
77
  alignParser,
63
78
  autoUpgradeParser,
64
79
  removeNcxParser,