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