@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,261 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : Symbol.for("Symbol." + name);
|
|
7
|
+
var __typeError = (msg) => {
|
|
8
|
+
throw TypeError(msg);
|
|
9
|
+
};
|
|
10
|
+
var __export = (target, all) => {
|
|
11
|
+
for (var name in all)
|
|
12
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
13
|
+
};
|
|
14
|
+
var __copyProps = (to, from, except, desc) => {
|
|
15
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
16
|
+
for (let key of __getOwnPropNames(from))
|
|
17
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
18
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
19
|
+
}
|
|
20
|
+
return to;
|
|
21
|
+
};
|
|
22
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
23
|
+
var __using = (stack, value, async) => {
|
|
24
|
+
if (value != null) {
|
|
25
|
+
if (typeof value !== "object" && typeof value !== "function") __typeError("Object expected");
|
|
26
|
+
var dispose, inner;
|
|
27
|
+
if (async) dispose = value[__knownSymbol("asyncDispose")];
|
|
28
|
+
if (dispose === void 0) {
|
|
29
|
+
dispose = value[__knownSymbol("dispose")];
|
|
30
|
+
if (async) inner = dispose;
|
|
31
|
+
}
|
|
32
|
+
if (typeof dispose !== "function") __typeError("Object not disposable");
|
|
33
|
+
if (inner) dispose = function() {
|
|
34
|
+
try {
|
|
35
|
+
inner.call(this);
|
|
36
|
+
} catch (e) {
|
|
37
|
+
return Promise.reject(e);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
stack.push([async, dispose, value]);
|
|
41
|
+
} else if (async) {
|
|
42
|
+
stack.push([async]);
|
|
43
|
+
}
|
|
44
|
+
return value;
|
|
45
|
+
};
|
|
46
|
+
var __callDispose = (stack, error, hasError) => {
|
|
47
|
+
var E = typeof SuppressedError === "function" ? SuppressedError : function(e, s, m, _) {
|
|
48
|
+
return _ = Error(m), _.name = "SuppressedError", _.error = e, _.suppressed = s, _;
|
|
49
|
+
};
|
|
50
|
+
var fail = (e) => error = hasError ? new E(e, error, "An error was suppressed during disposal") : (hasError = true, e);
|
|
51
|
+
var next = (it) => {
|
|
52
|
+
while (it = stack.pop()) {
|
|
53
|
+
try {
|
|
54
|
+
var result = it[1] && it[1].call(it[2]);
|
|
55
|
+
if (it[0]) return Promise.resolve(result).then(next, (e) => (fail(e), next()));
|
|
56
|
+
} catch (e) {
|
|
57
|
+
fail(e);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (hasError) throw error;
|
|
61
|
+
};
|
|
62
|
+
return next();
|
|
63
|
+
};
|
|
64
|
+
var snapshot_exports = {};
|
|
65
|
+
__export(snapshot_exports, {
|
|
66
|
+
createAlignmentSnapshot: () => createAlignmentSnapshot,
|
|
67
|
+
snapshotCtcAlignment: () => snapshotCtcAlignment
|
|
68
|
+
});
|
|
69
|
+
module.exports = __toCommonJS(snapshot_exports);
|
|
70
|
+
var import_promises = require("node:fs/promises");
|
|
71
|
+
var import_node_path = require("node:path");
|
|
72
|
+
var import_posix = require("node:path/posix");
|
|
73
|
+
var import_epub = require("@storyteller-platform/epub");
|
|
74
|
+
var import_greedyDecode = require("../../align/ctc/greedyDecode.cjs");
|
|
75
|
+
var import_slugify = require("../../align/slugify.cjs");
|
|
76
|
+
var import_fs = require("../../emit/fs.cjs");
|
|
77
|
+
var import_parseDom = require("../../markup/parseDom.cjs");
|
|
78
|
+
var import_segmentation = require("../../markup/segmentation.cjs");
|
|
79
|
+
var import_transform = require("../../markup/transform.cjs");
|
|
80
|
+
async function snapshotCtcAlignment(epubPath, emissionsPath, outputPath) {
|
|
81
|
+
var _stack = [];
|
|
82
|
+
try {
|
|
83
|
+
const emissionFilepaths = await (0, import_promises.readdir)(emissionsPath).then(
|
|
84
|
+
(filenames) => filenames.filter((f) => f.endsWith(".json")).map((f) => (0, import_node_path.join)(emissionsPath, f))
|
|
85
|
+
);
|
|
86
|
+
const epub = __using(_stack, await import_epub.Epub.from(epubPath));
|
|
87
|
+
const snapshot = await createAlignmentSnapshot(
|
|
88
|
+
epub,
|
|
89
|
+
emissionFilepaths,
|
|
90
|
+
"id-fragment"
|
|
91
|
+
);
|
|
92
|
+
await (0, import_promises.writeFile)(outputPath, snapshot, { encoding: "utf-8" });
|
|
93
|
+
} catch (_) {
|
|
94
|
+
var _error = _, _hasError = true;
|
|
95
|
+
} finally {
|
|
96
|
+
__callDispose(_stack, _error, _hasError);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function secondsToFrame(seconds, timings) {
|
|
100
|
+
const chunk = Math.floor(seconds / timings.secondsPerChunk);
|
|
101
|
+
const localFrame = Math.round(
|
|
102
|
+
(seconds - chunk * timings.secondsPerChunk) / timings.secondsPerFrame
|
|
103
|
+
);
|
|
104
|
+
return chunk * timings.framesPerChunk + Math.min(localFrame, timings.framesPerChunk - 1);
|
|
105
|
+
}
|
|
106
|
+
async function loadDecodedEmissions(filepath) {
|
|
107
|
+
const emissionsReader = await import_fs.EmissionsReader.from(filepath);
|
|
108
|
+
const { text, frames } = await (0, import_greedyDecode.ctcGreedyDecode)(emissionsReader);
|
|
109
|
+
return {
|
|
110
|
+
text,
|
|
111
|
+
frames,
|
|
112
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
113
|
+
...emissionsReader.timings[0]
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
async function createAlignmentSnapshot(epub, emissionFilepaths, textRef, transliterate) {
|
|
117
|
+
let newSnapshot = "";
|
|
118
|
+
const decodedEmissionsCache = /* @__PURE__ */ new Map();
|
|
119
|
+
function getDecodedEmissions(filepath) {
|
|
120
|
+
let cached = decodedEmissionsCache.get(filepath);
|
|
121
|
+
if (!cached) {
|
|
122
|
+
cached = loadDecodedEmissions(filepath);
|
|
123
|
+
decodedEmissionsCache.set(filepath, cached);
|
|
124
|
+
}
|
|
125
|
+
return cached;
|
|
126
|
+
}
|
|
127
|
+
const locale = await epub.getLanguage() ?? new Intl.Locale("en-US");
|
|
128
|
+
const spine = await epub.getSpineItems();
|
|
129
|
+
const manifest = await epub.getManifest();
|
|
130
|
+
const mediaOverlayItems = spine.map((item) => item.mediaOverlay).filter((mediaOverlayId) => !!mediaOverlayId).map((id) => manifest[id]);
|
|
131
|
+
const mediaOverlays = [];
|
|
132
|
+
for (const item of mediaOverlayItems) {
|
|
133
|
+
const contents = await epub.readItemContents(item.id, "utf-8");
|
|
134
|
+
const parsed = import_epub.Epub.xmlParser.parse(contents);
|
|
135
|
+
mediaOverlays.push(parsed);
|
|
136
|
+
const smil = import_epub.Epub.findXmlChildByName("smil", parsed);
|
|
137
|
+
if (!smil) continue;
|
|
138
|
+
const body = import_epub.Epub.findXmlChildByName("body", import_epub.Epub.getXmlChildren(smil));
|
|
139
|
+
if (!body) continue;
|
|
140
|
+
const seq = import_epub.Epub.findXmlChildByName("seq", import_epub.Epub.getXmlChildren(body));
|
|
141
|
+
if (!seq) continue;
|
|
142
|
+
const textref = seq[":@"]?.["@_epub:textref"];
|
|
143
|
+
if (!textref) continue;
|
|
144
|
+
newSnapshot += `// ${(0, import_posix.basename)(textref)}
|
|
145
|
+
|
|
146
|
+
`;
|
|
147
|
+
const chapterContents = await epub.readFileContents(
|
|
148
|
+
textref,
|
|
149
|
+
item.href,
|
|
150
|
+
"utf-8"
|
|
151
|
+
);
|
|
152
|
+
const chapterXml = import_epub.Epub.xhtmlParser.parse(chapterContents);
|
|
153
|
+
const original = (0, import_parseDom.parseDom)(import_epub.Epub.getXhtmlBody(chapterXml));
|
|
154
|
+
const inlined = (0, import_transform.inlineFootnotes)(original);
|
|
155
|
+
const lifted = (0, import_transform.liftText)(inlined.root);
|
|
156
|
+
const segmentation = await (0, import_segmentation.segmentChapter)(lifted.result, {
|
|
157
|
+
primaryLocale: new Intl.Locale("en-US")
|
|
158
|
+
});
|
|
159
|
+
let lastChapterSentence = -1;
|
|
160
|
+
const chapterSentences = segmentation.filter((s) => s.text.match(/\S/));
|
|
161
|
+
const pars = findAll("par", import_epub.Epub.getXmlChildren(seq));
|
|
162
|
+
for (const par of pars) {
|
|
163
|
+
newSnapshot += `
|
|
164
|
+
`;
|
|
165
|
+
const text = import_epub.Epub.findXmlChildByName("text", import_epub.Epub.getXmlChildren(par));
|
|
166
|
+
if (!text) continue;
|
|
167
|
+
const audio = import_epub.Epub.findXmlChildByName("audio", import_epub.Epub.getXmlChildren(par));
|
|
168
|
+
if (!audio) continue;
|
|
169
|
+
const textSrc = text[":@"]?.["@_src"];
|
|
170
|
+
if (!textSrc) continue;
|
|
171
|
+
const result = textRef === "id-fragment" ? getTextSentenceIndexByIdFragment(textSrc) : getTextSentenceIndexByTextFragment(
|
|
172
|
+
textSrc,
|
|
173
|
+
chapterSentences,
|
|
174
|
+
lastChapterSentence
|
|
175
|
+
);
|
|
176
|
+
if (result === null) continue;
|
|
177
|
+
const { fragment, sentenceId } = result;
|
|
178
|
+
const textSentence = chapterSentences[sentenceId]?.text.replace(/\n/, "");
|
|
179
|
+
if (!textSentence) continue;
|
|
180
|
+
lastChapterSentence = sentenceId;
|
|
181
|
+
if (textRef === "text-fragment") {
|
|
182
|
+
newSnapshot += `${fragment}
|
|
183
|
+
`;
|
|
184
|
+
}
|
|
185
|
+
newSnapshot += `Text: ${transliterate ? (await (0, import_slugify.slugify)(textSentence, locale)).result : textSentence}
|
|
186
|
+
`;
|
|
187
|
+
const audioSrc = audio[":@"]?.["@_src"];
|
|
188
|
+
if (!audioSrc) continue;
|
|
189
|
+
const audioStart = audio[":@"]?.["@_clipBegin"];
|
|
190
|
+
const audioEnd = audio[":@"]?.["@_clipEnd"];
|
|
191
|
+
if (!audioStart || !audioEnd) continue;
|
|
192
|
+
const audioStartTime = parseFloat(audioStart.slice(0, -1));
|
|
193
|
+
const audioEndTime = parseFloat(audioEnd.slice(0, -1));
|
|
194
|
+
const audioFilename = (0, import_posix.basename)(audioSrc, (0, import_posix.extname)(audioSrc));
|
|
195
|
+
const emissionFilepath = emissionFilepaths.find((f) => {
|
|
196
|
+
const withoutJson = (0, import_node_path.basename)(f, (0, import_node_path.extname)(f));
|
|
197
|
+
return (0, import_node_path.basename)(withoutJson, (0, import_node_path.extname)(withoutJson)) === audioFilename;
|
|
198
|
+
});
|
|
199
|
+
if (!emissionFilepath) continue;
|
|
200
|
+
const decoded = await getDecodedEmissions(emissionFilepath);
|
|
201
|
+
const startFrame = secondsToFrame(audioStartTime, decoded);
|
|
202
|
+
const endFrame = secondsToFrame(audioEndTime, decoded);
|
|
203
|
+
let decodedSentence = "";
|
|
204
|
+
for (let i = 0; i < decoded.frames.length; i++) {
|
|
205
|
+
const frame = decoded.frames[i];
|
|
206
|
+
if (frame > endFrame) break;
|
|
207
|
+
if (frame >= startFrame) {
|
|
208
|
+
decodedSentence += decoded.text[i];
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
newSnapshot += `Audio: ${decodedSentence}
|
|
212
|
+
`;
|
|
213
|
+
}
|
|
214
|
+
newSnapshot += `
|
|
215
|
+
`;
|
|
216
|
+
}
|
|
217
|
+
return newSnapshot;
|
|
218
|
+
}
|
|
219
|
+
function getTextSentenceIndexByTextFragment(textSrc, chapterSentences, lastChapterSentence) {
|
|
220
|
+
const textSrcMatch = textSrc.match(/#:~:text=(.+)$/);
|
|
221
|
+
if (!textSrcMatch) return null;
|
|
222
|
+
const textFragment = textSrcMatch[1];
|
|
223
|
+
if (textFragment === void 0) return null;
|
|
224
|
+
const textFragmentParts = textFragment.split(",");
|
|
225
|
+
const textFragmentPrefix = textFragmentParts[0]?.endsWith("-") ? decodeURIComponent(textFragmentParts[0]).slice(0, -1) : "";
|
|
226
|
+
const textFragmentStart = decodeURIComponent(
|
|
227
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
228
|
+
textFragmentPrefix ? textFragmentParts[1] : textFragmentParts[0]
|
|
229
|
+
);
|
|
230
|
+
const textSentenceIndex = chapterSentences.slice(lastChapterSentence + 1).findIndex((s, i) => {
|
|
231
|
+
const prev = chapterSentences[lastChapterSentence + i];
|
|
232
|
+
return (!prev || prev.text.replace("\n", " ").toLowerCase().endsWith(textFragmentPrefix)) && s.text.replace("\n", " ").toLowerCase().startsWith(textFragmentStart);
|
|
233
|
+
});
|
|
234
|
+
if (textSentenceIndex === -1) return null;
|
|
235
|
+
return {
|
|
236
|
+
fragment: textSrcMatch[0],
|
|
237
|
+
sentenceId: textSentenceIndex + lastChapterSentence + 1
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
function getTextSentenceIndexByIdFragment(textSrc) {
|
|
241
|
+
const match = textSrc.match(/#.*s([0-9]+)$/);
|
|
242
|
+
if (!match) return null;
|
|
243
|
+
const [fragment, sentenceId] = match;
|
|
244
|
+
return { fragment, sentenceId: parseInt(sentenceId, 10) };
|
|
245
|
+
}
|
|
246
|
+
function findAll(name, xml) {
|
|
247
|
+
const result = [];
|
|
248
|
+
for (const child of xml) {
|
|
249
|
+
if (import_epub.Epub.isXmlTextNode(child)) continue;
|
|
250
|
+
if (import_epub.Epub.getXmlElementName(child) === name) {
|
|
251
|
+
result.push(child);
|
|
252
|
+
}
|
|
253
|
+
result.push(...findAll(name, import_epub.Epub.getXmlChildren(child)));
|
|
254
|
+
}
|
|
255
|
+
return result;
|
|
256
|
+
}
|
|
257
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
258
|
+
0 && (module.exports = {
|
|
259
|
+
createAlignmentSnapshot,
|
|
260
|
+
snapshotCtcAlignment
|
|
261
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Epub } from '@storyteller-platform/epub';
|
|
2
|
+
|
|
3
|
+
declare function snapshotCtcAlignment(epubPath: string, emissionsPath: string, outputPath: string): Promise<void>;
|
|
4
|
+
declare function createAlignmentSnapshot(epub: Epub, emissionFilepaths: string[], textRef: "id-fragment" | "text-fragment", transliterate?: boolean): Promise<string>;
|
|
5
|
+
|
|
6
|
+
export { createAlignmentSnapshot, snapshotCtcAlignment };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Epub } from '@storyteller-platform/epub';
|
|
2
|
+
|
|
3
|
+
declare function snapshotCtcAlignment(epubPath: string, emissionsPath: string, outputPath: string): Promise<void>;
|
|
4
|
+
declare function createAlignmentSnapshot(epub: Epub, emissionFilepaths: string[], textRef: "id-fragment" | "text-fragment", transliterate?: boolean): Promise<string>;
|
|
5
|
+
|
|
6
|
+
export { createAlignmentSnapshot, snapshotCtcAlignment };
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__callDispose,
|
|
3
|
+
__using
|
|
4
|
+
} from "../../chunk-BIEQXUOY.js";
|
|
5
|
+
import { readdir, writeFile } from "node:fs/promises";
|
|
6
|
+
import { basename, extname, join } from "node:path";
|
|
7
|
+
import {
|
|
8
|
+
basename as posixBasename,
|
|
9
|
+
extname as posixExtname
|
|
10
|
+
} from "node:path/posix";
|
|
11
|
+
import {
|
|
12
|
+
Epub
|
|
13
|
+
} from "@storyteller-platform/epub";
|
|
14
|
+
import { ctcGreedyDecode } from "../../align/ctc/greedyDecode.js";
|
|
15
|
+
import { slugify } from "../../align/slugify.js";
|
|
16
|
+
import { EmissionsReader } from "../../emit/fs.js";
|
|
17
|
+
import { parseDom } from "../../markup/parseDom.js";
|
|
18
|
+
import { segmentChapter } from "../../markup/segmentation.js";
|
|
19
|
+
import { inlineFootnotes, liftText } from "../../markup/transform.js";
|
|
20
|
+
async function snapshotCtcAlignment(epubPath, emissionsPath, outputPath) {
|
|
21
|
+
var _stack = [];
|
|
22
|
+
try {
|
|
23
|
+
const emissionFilepaths = await readdir(emissionsPath).then(
|
|
24
|
+
(filenames) => filenames.filter((f) => f.endsWith(".json")).map((f) => join(emissionsPath, f))
|
|
25
|
+
);
|
|
26
|
+
const epub = __using(_stack, await Epub.from(epubPath));
|
|
27
|
+
const snapshot = await createAlignmentSnapshot(
|
|
28
|
+
epub,
|
|
29
|
+
emissionFilepaths,
|
|
30
|
+
"id-fragment"
|
|
31
|
+
);
|
|
32
|
+
await writeFile(outputPath, snapshot, { encoding: "utf-8" });
|
|
33
|
+
} catch (_) {
|
|
34
|
+
var _error = _, _hasError = true;
|
|
35
|
+
} finally {
|
|
36
|
+
__callDispose(_stack, _error, _hasError);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function secondsToFrame(seconds, timings) {
|
|
40
|
+
const chunk = Math.floor(seconds / timings.secondsPerChunk);
|
|
41
|
+
const localFrame = Math.round(
|
|
42
|
+
(seconds - chunk * timings.secondsPerChunk) / timings.secondsPerFrame
|
|
43
|
+
);
|
|
44
|
+
return chunk * timings.framesPerChunk + Math.min(localFrame, timings.framesPerChunk - 1);
|
|
45
|
+
}
|
|
46
|
+
async function loadDecodedEmissions(filepath) {
|
|
47
|
+
const emissionsReader = await EmissionsReader.from(filepath);
|
|
48
|
+
const { text, frames } = await ctcGreedyDecode(emissionsReader);
|
|
49
|
+
return {
|
|
50
|
+
text,
|
|
51
|
+
frames,
|
|
52
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
53
|
+
...emissionsReader.timings[0]
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
async function createAlignmentSnapshot(epub, emissionFilepaths, textRef, transliterate) {
|
|
57
|
+
let newSnapshot = "";
|
|
58
|
+
const decodedEmissionsCache = /* @__PURE__ */ new Map();
|
|
59
|
+
function getDecodedEmissions(filepath) {
|
|
60
|
+
let cached = decodedEmissionsCache.get(filepath);
|
|
61
|
+
if (!cached) {
|
|
62
|
+
cached = loadDecodedEmissions(filepath);
|
|
63
|
+
decodedEmissionsCache.set(filepath, cached);
|
|
64
|
+
}
|
|
65
|
+
return cached;
|
|
66
|
+
}
|
|
67
|
+
const locale = await epub.getLanguage() ?? new Intl.Locale("en-US");
|
|
68
|
+
const spine = await epub.getSpineItems();
|
|
69
|
+
const manifest = await epub.getManifest();
|
|
70
|
+
const mediaOverlayItems = spine.map((item) => item.mediaOverlay).filter((mediaOverlayId) => !!mediaOverlayId).map((id) => manifest[id]);
|
|
71
|
+
const mediaOverlays = [];
|
|
72
|
+
for (const item of mediaOverlayItems) {
|
|
73
|
+
const contents = await epub.readItemContents(item.id, "utf-8");
|
|
74
|
+
const parsed = Epub.xmlParser.parse(contents);
|
|
75
|
+
mediaOverlays.push(parsed);
|
|
76
|
+
const smil = Epub.findXmlChildByName("smil", parsed);
|
|
77
|
+
if (!smil) continue;
|
|
78
|
+
const body = Epub.findXmlChildByName("body", Epub.getXmlChildren(smil));
|
|
79
|
+
if (!body) continue;
|
|
80
|
+
const seq = Epub.findXmlChildByName("seq", Epub.getXmlChildren(body));
|
|
81
|
+
if (!seq) continue;
|
|
82
|
+
const textref = seq[":@"]?.["@_epub:textref"];
|
|
83
|
+
if (!textref) continue;
|
|
84
|
+
newSnapshot += `// ${posixBasename(textref)}
|
|
85
|
+
|
|
86
|
+
`;
|
|
87
|
+
const chapterContents = await epub.readFileContents(
|
|
88
|
+
textref,
|
|
89
|
+
item.href,
|
|
90
|
+
"utf-8"
|
|
91
|
+
);
|
|
92
|
+
const chapterXml = Epub.xhtmlParser.parse(chapterContents);
|
|
93
|
+
const original = parseDom(Epub.getXhtmlBody(chapterXml));
|
|
94
|
+
const inlined = inlineFootnotes(original);
|
|
95
|
+
const lifted = liftText(inlined.root);
|
|
96
|
+
const segmentation = await segmentChapter(lifted.result, {
|
|
97
|
+
primaryLocale: new Intl.Locale("en-US")
|
|
98
|
+
});
|
|
99
|
+
let lastChapterSentence = -1;
|
|
100
|
+
const chapterSentences = segmentation.filter((s) => s.text.match(/\S/));
|
|
101
|
+
const pars = findAll("par", Epub.getXmlChildren(seq));
|
|
102
|
+
for (const par of pars) {
|
|
103
|
+
newSnapshot += `
|
|
104
|
+
`;
|
|
105
|
+
const text = Epub.findXmlChildByName("text", Epub.getXmlChildren(par));
|
|
106
|
+
if (!text) continue;
|
|
107
|
+
const audio = Epub.findXmlChildByName("audio", Epub.getXmlChildren(par));
|
|
108
|
+
if (!audio) continue;
|
|
109
|
+
const textSrc = text[":@"]?.["@_src"];
|
|
110
|
+
if (!textSrc) continue;
|
|
111
|
+
const result = textRef === "id-fragment" ? getTextSentenceIndexByIdFragment(textSrc) : getTextSentenceIndexByTextFragment(
|
|
112
|
+
textSrc,
|
|
113
|
+
chapterSentences,
|
|
114
|
+
lastChapterSentence
|
|
115
|
+
);
|
|
116
|
+
if (result === null) continue;
|
|
117
|
+
const { fragment, sentenceId } = result;
|
|
118
|
+
const textSentence = chapterSentences[sentenceId]?.text.replace(/\n/, "");
|
|
119
|
+
if (!textSentence) continue;
|
|
120
|
+
lastChapterSentence = sentenceId;
|
|
121
|
+
if (textRef === "text-fragment") {
|
|
122
|
+
newSnapshot += `${fragment}
|
|
123
|
+
`;
|
|
124
|
+
}
|
|
125
|
+
newSnapshot += `Text: ${transliterate ? (await slugify(textSentence, locale)).result : textSentence}
|
|
126
|
+
`;
|
|
127
|
+
const audioSrc = audio[":@"]?.["@_src"];
|
|
128
|
+
if (!audioSrc) continue;
|
|
129
|
+
const audioStart = audio[":@"]?.["@_clipBegin"];
|
|
130
|
+
const audioEnd = audio[":@"]?.["@_clipEnd"];
|
|
131
|
+
if (!audioStart || !audioEnd) continue;
|
|
132
|
+
const audioStartTime = parseFloat(audioStart.slice(0, -1));
|
|
133
|
+
const audioEndTime = parseFloat(audioEnd.slice(0, -1));
|
|
134
|
+
const audioFilename = posixBasename(audioSrc, posixExtname(audioSrc));
|
|
135
|
+
const emissionFilepath = emissionFilepaths.find((f) => {
|
|
136
|
+
const withoutJson = basename(f, extname(f));
|
|
137
|
+
return basename(withoutJson, extname(withoutJson)) === audioFilename;
|
|
138
|
+
});
|
|
139
|
+
if (!emissionFilepath) continue;
|
|
140
|
+
const decoded = await getDecodedEmissions(emissionFilepath);
|
|
141
|
+
const startFrame = secondsToFrame(audioStartTime, decoded);
|
|
142
|
+
const endFrame = secondsToFrame(audioEndTime, decoded);
|
|
143
|
+
let decodedSentence = "";
|
|
144
|
+
for (let i = 0; i < decoded.frames.length; i++) {
|
|
145
|
+
const frame = decoded.frames[i];
|
|
146
|
+
if (frame > endFrame) break;
|
|
147
|
+
if (frame >= startFrame) {
|
|
148
|
+
decodedSentence += decoded.text[i];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
newSnapshot += `Audio: ${decodedSentence}
|
|
152
|
+
`;
|
|
153
|
+
}
|
|
154
|
+
newSnapshot += `
|
|
155
|
+
`;
|
|
156
|
+
}
|
|
157
|
+
return newSnapshot;
|
|
158
|
+
}
|
|
159
|
+
function getTextSentenceIndexByTextFragment(textSrc, chapterSentences, lastChapterSentence) {
|
|
160
|
+
const textSrcMatch = textSrc.match(/#:~:text=(.+)$/);
|
|
161
|
+
if (!textSrcMatch) return null;
|
|
162
|
+
const textFragment = textSrcMatch[1];
|
|
163
|
+
if (textFragment === void 0) return null;
|
|
164
|
+
const textFragmentParts = textFragment.split(",");
|
|
165
|
+
const textFragmentPrefix = textFragmentParts[0]?.endsWith("-") ? decodeURIComponent(textFragmentParts[0]).slice(0, -1) : "";
|
|
166
|
+
const textFragmentStart = decodeURIComponent(
|
|
167
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
168
|
+
textFragmentPrefix ? textFragmentParts[1] : textFragmentParts[0]
|
|
169
|
+
);
|
|
170
|
+
const textSentenceIndex = chapterSentences.slice(lastChapterSentence + 1).findIndex((s, i) => {
|
|
171
|
+
const prev = chapterSentences[lastChapterSentence + i];
|
|
172
|
+
return (!prev || prev.text.replace("\n", " ").toLowerCase().endsWith(textFragmentPrefix)) && s.text.replace("\n", " ").toLowerCase().startsWith(textFragmentStart);
|
|
173
|
+
});
|
|
174
|
+
if (textSentenceIndex === -1) return null;
|
|
175
|
+
return {
|
|
176
|
+
fragment: textSrcMatch[0],
|
|
177
|
+
sentenceId: textSentenceIndex + lastChapterSentence + 1
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function getTextSentenceIndexByIdFragment(textSrc) {
|
|
181
|
+
const match = textSrc.match(/#.*s([0-9]+)$/);
|
|
182
|
+
if (!match) return null;
|
|
183
|
+
const [fragment, sentenceId] = match;
|
|
184
|
+
return { fragment, sentenceId: parseInt(sentenceId, 10) };
|
|
185
|
+
}
|
|
186
|
+
function findAll(name, xml) {
|
|
187
|
+
const result = [];
|
|
188
|
+
for (const child of xml) {
|
|
189
|
+
if (Epub.isXmlTextNode(child)) continue;
|
|
190
|
+
if (Epub.getXmlElementName(child) === name) {
|
|
191
|
+
result.push(child);
|
|
192
|
+
}
|
|
193
|
+
result.push(...findAll(name, Epub.getXmlChildren(child)));
|
|
194
|
+
}
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
export {
|
|
198
|
+
createAlignmentSnapshot,
|
|
199
|
+
snapshotCtcAlignment
|
|
200
|
+
};
|
package/dist/snapshot/parse.cjs
CHANGED
|
@@ -25,22 +25,35 @@ module.exports = __toCommonJS(parse_exports);
|
|
|
25
25
|
var import_core = require("@optique/core");
|
|
26
26
|
var import_run = require("@optique/run");
|
|
27
27
|
var import_parse = require("../common/parse.cjs");
|
|
28
|
-
const snapshotParser = (0, import_core.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
28
|
+
const snapshotParser = (0, import_core.merge)(
|
|
29
|
+
(0, import_core.object)("Snapshot", {
|
|
30
|
+
epub: (0, import_core.option)(
|
|
31
|
+
"--epub",
|
|
32
|
+
(0, import_run.path)({ mustExist: true, type: "file", extensions: [".epub"] }),
|
|
33
|
+
{
|
|
34
|
+
description: import_core.message`Path to an EPUB file to snapshot. This EPUB must have Media Overlays and audio files corresponding to the transcription files passed to --transcriptions.`
|
|
35
|
+
}
|
|
36
|
+
),
|
|
37
|
+
output: (0, import_core.argument)((0, import_run.path)({ type: "file", metavar: "OUTPUT_PATH" }), {
|
|
38
|
+
description: import_core.message`Path to save the snapshot.`
|
|
39
|
+
})
|
|
40
|
+
}),
|
|
41
|
+
(0, import_core.or)(
|
|
42
|
+
(0, import_core.object)({
|
|
43
|
+
transcriptions: (0, import_core.option)(
|
|
44
|
+
"--transcriptions",
|
|
45
|
+
(0, import_run.path)({ mustExist: true, type: "directory" })
|
|
46
|
+
)
|
|
47
|
+
}),
|
|
48
|
+
(0, import_core.object)({
|
|
49
|
+
ctc: (0, import_core.flag)("--ctc"),
|
|
50
|
+
emissions: (0, import_core.option)(
|
|
51
|
+
"--emissions",
|
|
52
|
+
(0, import_run.path)({ mustExist: true, type: "directory" })
|
|
53
|
+
)
|
|
54
|
+
})
|
|
55
|
+
)
|
|
56
|
+
);
|
|
44
57
|
const snapshotCommand = (0, import_core.command)(
|
|
45
58
|
"snapshot",
|
|
46
59
|
(0, import_core.merge)(
|
|
@@ -1,24 +1,28 @@
|
|
|
1
1
|
import * as _optique_core from '@optique/core';
|
|
2
2
|
|
|
3
3
|
declare const snapshotParser: _optique_core.Parser<"sync", {
|
|
4
|
-
readonly transcriptions: string;
|
|
5
4
|
readonly epub: string;
|
|
6
5
|
readonly output: string;
|
|
7
|
-
}
|
|
8
|
-
readonly transcriptions:
|
|
9
|
-
|
|
10
|
-
readonly
|
|
11
|
-
|
|
6
|
+
} & ({
|
|
7
|
+
readonly transcriptions: string;
|
|
8
|
+
} | {
|
|
9
|
+
readonly ctc: true;
|
|
10
|
+
readonly emissions: string;
|
|
11
|
+
}), Record<string | symbol, unknown>>;
|
|
12
12
|
declare const snapshotCommand: _optique_core.Parser<"sync", {
|
|
13
13
|
readonly action: "snapshot";
|
|
14
|
-
} & {
|
|
15
|
-
readonly transcriptions: string;
|
|
14
|
+
} & (({
|
|
16
15
|
readonly epub: string;
|
|
17
16
|
readonly output: string;
|
|
18
|
-
} & {
|
|
17
|
+
} & ({
|
|
18
|
+
readonly transcriptions: string;
|
|
19
|
+
} | {
|
|
20
|
+
readonly ctc: true;
|
|
21
|
+
readonly emissions: string;
|
|
22
|
+
})) & {
|
|
19
23
|
readonly noProgress: boolean;
|
|
20
24
|
readonly logLevel: "silent" | "debug" | "info" | "warn" | "error";
|
|
21
25
|
readonly time: boolean;
|
|
22
|
-
}, ["matched", string] | ["parsing", Record<string | symbol, unknown>] | undefined>;
|
|
26
|
+
}), ["matched", string] | ["parsing", Record<string | symbol, unknown>] | undefined>;
|
|
23
27
|
|
|
24
28
|
export { snapshotCommand, snapshotParser };
|
package/dist/snapshot/parse.d.ts
CHANGED
|
@@ -1,24 +1,28 @@
|
|
|
1
1
|
import * as _optique_core from '@optique/core';
|
|
2
2
|
|
|
3
3
|
declare const snapshotParser: _optique_core.Parser<"sync", {
|
|
4
|
-
readonly transcriptions: string;
|
|
5
4
|
readonly epub: string;
|
|
6
5
|
readonly output: string;
|
|
7
|
-
}
|
|
8
|
-
readonly transcriptions:
|
|
9
|
-
|
|
10
|
-
readonly
|
|
11
|
-
|
|
6
|
+
} & ({
|
|
7
|
+
readonly transcriptions: string;
|
|
8
|
+
} | {
|
|
9
|
+
readonly ctc: true;
|
|
10
|
+
readonly emissions: string;
|
|
11
|
+
}), Record<string | symbol, unknown>>;
|
|
12
12
|
declare const snapshotCommand: _optique_core.Parser<"sync", {
|
|
13
13
|
readonly action: "snapshot";
|
|
14
|
-
} & {
|
|
15
|
-
readonly transcriptions: string;
|
|
14
|
+
} & (({
|
|
16
15
|
readonly epub: string;
|
|
17
16
|
readonly output: string;
|
|
18
|
-
} & {
|
|
17
|
+
} & ({
|
|
18
|
+
readonly transcriptions: string;
|
|
19
|
+
} | {
|
|
20
|
+
readonly ctc: true;
|
|
21
|
+
readonly emissions: string;
|
|
22
|
+
})) & {
|
|
19
23
|
readonly noProgress: boolean;
|
|
20
24
|
readonly logLevel: "silent" | "debug" | "info" | "warn" | "error";
|
|
21
25
|
readonly time: boolean;
|
|
22
|
-
}, ["matched", string] | ["parsing", Record<string | symbol, unknown>] | undefined>;
|
|
26
|
+
}), ["matched", string] | ["parsing", Record<string | symbol, unknown>] | undefined>;
|
|
23
27
|
|
|
24
28
|
export { snapshotCommand, snapshotParser };
|
package/dist/snapshot/parse.js
CHANGED
|
@@ -3,29 +3,44 @@ import {
|
|
|
3
3
|
argument,
|
|
4
4
|
command,
|
|
5
5
|
constant,
|
|
6
|
+
flag,
|
|
6
7
|
merge,
|
|
7
8
|
message,
|
|
8
9
|
object,
|
|
9
|
-
option
|
|
10
|
+
option,
|
|
11
|
+
or
|
|
10
12
|
} from "@optique/core";
|
|
11
13
|
import { path } from "@optique/run";
|
|
12
14
|
import { loggingParser } from "../common/parse.js";
|
|
13
|
-
const snapshotParser =
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
15
|
+
const snapshotParser = merge(
|
|
16
|
+
object("Snapshot", {
|
|
17
|
+
epub: option(
|
|
18
|
+
"--epub",
|
|
19
|
+
path({ mustExist: true, type: "file", extensions: [".epub"] }),
|
|
20
|
+
{
|
|
21
|
+
description: message`Path to an EPUB file to snapshot. This EPUB must have Media Overlays and audio files corresponding to the transcription files passed to --transcriptions.`
|
|
22
|
+
}
|
|
23
|
+
),
|
|
24
|
+
output: argument(path({ type: "file", metavar: "OUTPUT_PATH" }), {
|
|
25
|
+
description: message`Path to save the snapshot.`
|
|
26
|
+
})
|
|
27
|
+
}),
|
|
28
|
+
or(
|
|
29
|
+
object({
|
|
30
|
+
transcriptions: option(
|
|
31
|
+
"--transcriptions",
|
|
32
|
+
path({ mustExist: true, type: "directory" })
|
|
33
|
+
)
|
|
34
|
+
}),
|
|
35
|
+
object({
|
|
36
|
+
ctc: flag("--ctc"),
|
|
37
|
+
emissions: option(
|
|
38
|
+
"--emissions",
|
|
39
|
+
path({ mustExist: true, type: "directory" })
|
|
40
|
+
)
|
|
41
|
+
})
|
|
42
|
+
)
|
|
43
|
+
);
|
|
29
44
|
const snapshotCommand = command(
|
|
30
45
|
"snapshot",
|
|
31
46
|
merge(
|