@storyteller-platform/align 0.0.1
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/LICENSE.txt +21 -0
- package/README.md +3 -0
- package/dist/align/align.cjs +525 -0
- package/dist/align/align.d.cts +58 -0
- package/dist/align/align.d.ts +58 -0
- package/dist/align/align.js +458 -0
- package/dist/align/fuzzy.cjs +164 -0
- package/dist/align/fuzzy.d.cts +6 -0
- package/dist/align/fuzzy.d.ts +6 -0
- package/dist/align/fuzzy.js +141 -0
- package/dist/align/getSentenceRanges.cjs +304 -0
- package/dist/align/getSentenceRanges.d.cts +31 -0
- package/dist/align/getSentenceRanges.d.ts +31 -0
- package/dist/align/getSentenceRanges.js +277 -0
- package/dist/align/parse.cjs +63 -0
- package/dist/align/parse.d.cts +30 -0
- package/dist/align/parse.d.ts +30 -0
- package/dist/align/parse.js +51 -0
- package/dist/chunk-BIEQXUOY.js +50 -0
- package/dist/cli/bin.cjs +368 -0
- package/dist/cli/bin.d.cts +1 -0
- package/dist/cli/bin.d.ts +1 -0
- package/dist/cli/bin.js +319 -0
- package/dist/common/ffmpeg.cjs +232 -0
- package/dist/common/ffmpeg.d.cts +33 -0
- package/dist/common/ffmpeg.d.ts +33 -0
- package/dist/common/ffmpeg.js +196 -0
- package/dist/common/logging.cjs +45 -0
- package/dist/common/logging.d.cts +5 -0
- package/dist/common/logging.d.ts +5 -0
- package/dist/common/logging.js +12 -0
- package/dist/common/parse.cjs +73 -0
- package/dist/common/parse.d.cts +28 -0
- package/dist/common/parse.d.ts +28 -0
- package/dist/common/parse.js +56 -0
- package/dist/common/shell.cjs +30 -0
- package/dist/common/shell.d.cts +3 -0
- package/dist/common/shell.d.ts +3 -0
- package/dist/common/shell.js +7 -0
- package/dist/index.cjs +37 -0
- package/dist/index.d.cts +12 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +11 -0
- package/dist/markup/__tests__/markup.test.cjs +464 -0
- package/dist/markup/__tests__/markup.test.d.cts +2 -0
- package/dist/markup/__tests__/markup.test.d.ts +2 -0
- package/dist/markup/__tests__/markup.test.js +441 -0
- package/dist/markup/markup.cjs +316 -0
- package/dist/markup/markup.d.cts +24 -0
- package/dist/markup/markup.d.ts +24 -0
- package/dist/markup/markup.js +254 -0
- package/dist/markup/parse.cjs +55 -0
- package/dist/markup/parse.d.cts +17 -0
- package/dist/markup/parse.d.ts +17 -0
- package/dist/markup/parse.js +43 -0
- package/dist/markup/segmentation.cjs +87 -0
- package/dist/markup/segmentation.d.cts +8 -0
- package/dist/markup/segmentation.d.ts +8 -0
- package/dist/markup/segmentation.js +67 -0
- package/dist/markup/semantics.cjs +79 -0
- package/dist/markup/semantics.d.cts +6 -0
- package/dist/markup/semantics.d.ts +6 -0
- package/dist/markup/semantics.js +53 -0
- package/dist/process/AudioEncoding.cjs +16 -0
- package/dist/process/AudioEncoding.d.cts +8 -0
- package/dist/process/AudioEncoding.d.ts +8 -0
- package/dist/process/AudioEncoding.js +0 -0
- package/dist/process/__tests__/processAudiobook.test.cjs +232 -0
- package/dist/process/__tests__/processAudiobook.test.d.cts +2 -0
- package/dist/process/__tests__/processAudiobook.test.d.ts +2 -0
- package/dist/process/__tests__/processAudiobook.test.js +209 -0
- package/dist/process/mime.cjs +43 -0
- package/dist/process/mime.d.cts +3 -0
- package/dist/process/mime.d.ts +3 -0
- package/dist/process/mime.js +24 -0
- package/dist/process/parse.cjs +84 -0
- package/dist/process/parse.d.cts +28 -0
- package/dist/process/parse.d.ts +28 -0
- package/dist/process/parse.js +73 -0
- package/dist/process/processAudiobook.cjs +220 -0
- package/dist/process/processAudiobook.d.cts +24 -0
- package/dist/process/processAudiobook.d.ts +24 -0
- package/dist/process/processAudiobook.js +166 -0
- package/dist/process/ranges.cjs +203 -0
- package/dist/process/ranges.d.cts +15 -0
- package/dist/process/ranges.d.ts +15 -0
- package/dist/process/ranges.js +137 -0
- package/dist/transcribe/parse.cjs +149 -0
- package/dist/transcribe/parse.d.cts +114 -0
- package/dist/transcribe/parse.d.ts +114 -0
- package/dist/transcribe/parse.js +143 -0
- package/dist/transcribe/transcribe.cjs +400 -0
- package/dist/transcribe/transcribe.d.cts +41 -0
- package/dist/transcribe/transcribe.d.ts +41 -0
- package/dist/transcribe/transcribe.js +330 -0
- package/package.json +96 -0
|
@@ -0,0 +1,220 @@
|
|
|
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 processAudiobook_exports = {};
|
|
65
|
+
__export(processAudiobook_exports, {
|
|
66
|
+
processAudiobook: () => processAudiobook,
|
|
67
|
+
processFile: () => processFile
|
|
68
|
+
});
|
|
69
|
+
module.exports = __toCommonJS(processAudiobook_exports);
|
|
70
|
+
var import_promises = require("node:fs/promises");
|
|
71
|
+
var import_node_path = require("node:path");
|
|
72
|
+
var import_async_semaphore = require("@esfx/async-semaphore");
|
|
73
|
+
var import_audiobook = require("@storyteller-platform/audiobook");
|
|
74
|
+
var import_ghost_story = require("@storyteller-platform/ghost-story");
|
|
75
|
+
var import_ffmpeg = require("../common/ffmpeg.cjs");
|
|
76
|
+
var import_ranges = require("./ranges.cjs");
|
|
77
|
+
async function processAudiobook(input, output, options) {
|
|
78
|
+
var _a, _b, _c;
|
|
79
|
+
const timing = (0, import_ghost_story.createAggregator)();
|
|
80
|
+
timing.setMetadata("codec", ((_a = options.encoding) == null ? void 0 : _a.codec) ?? "unspecified");
|
|
81
|
+
timing.setMetadata("bitrate", ((_b = options.encoding) == null ? void 0 : _b.bitrate) ?? "unspecified");
|
|
82
|
+
timing.setMetadata("max-length", `${(options.maxLength ?? 2) * 60} minutes`);
|
|
83
|
+
await (0, import_promises.mkdir)(output, { recursive: true });
|
|
84
|
+
const allFiles = await (0, import_promises.readdir)(input, { recursive: true });
|
|
85
|
+
const filenames = allFiles.filter((f) => (0, import_audiobook.isAudioFile)(f) || (0, import_audiobook.isZipArchive)(f));
|
|
86
|
+
(_c = options.logger) == null ? void 0 : _c.debug(`Found ${filenames.length} files to process`);
|
|
87
|
+
const outputFiles = [];
|
|
88
|
+
const controller = new AbortController();
|
|
89
|
+
const signal = AbortSignal.any([
|
|
90
|
+
options.signal ?? new AbortSignal(),
|
|
91
|
+
controller.signal
|
|
92
|
+
]);
|
|
93
|
+
const semaphore = new import_async_semaphore.AsyncSemaphore(options.parallelism ?? 1);
|
|
94
|
+
const perFileProgress = /* @__PURE__ */ new Map();
|
|
95
|
+
await Promise.all(
|
|
96
|
+
filenames.map(async (filename, index) => {
|
|
97
|
+
if (signal.aborted) throw new Error("Aborted");
|
|
98
|
+
const filepath = (0, import_node_path.join)(input, filename);
|
|
99
|
+
function onFileProgress(progress) {
|
|
100
|
+
var _a2, _b2;
|
|
101
|
+
perFileProgress.set(index, progress);
|
|
102
|
+
const updatedProgress = Array.from(perFileProgress.values()).reduce(
|
|
103
|
+
(acc, p) => acc + p
|
|
104
|
+
);
|
|
105
|
+
(_a2 = options.logger) == null ? void 0 : _a2.info(
|
|
106
|
+
`Progress: ${Math.floor(updatedProgress * 100)}%`
|
|
107
|
+
);
|
|
108
|
+
(_b2 = options.onProgress) == null ? void 0 : _b2.call(options, updatedProgress);
|
|
109
|
+
}
|
|
110
|
+
const fileOptions = {
|
|
111
|
+
...options,
|
|
112
|
+
signal,
|
|
113
|
+
lock: semaphore,
|
|
114
|
+
onProgress: onFileProgress
|
|
115
|
+
};
|
|
116
|
+
const { processedFiles, timing: fileTiming } = await processFile(
|
|
117
|
+
filepath,
|
|
118
|
+
output,
|
|
119
|
+
(index + 1).toString().padStart(5, "0") + "-",
|
|
120
|
+
fileOptions
|
|
121
|
+
);
|
|
122
|
+
timing.add(fileTiming.summary());
|
|
123
|
+
outputFiles.push(...processedFiles);
|
|
124
|
+
}).map(
|
|
125
|
+
(p) => p.catch((e) => {
|
|
126
|
+
controller.abort(e);
|
|
127
|
+
throw e;
|
|
128
|
+
})
|
|
129
|
+
)
|
|
130
|
+
);
|
|
131
|
+
return timing;
|
|
132
|
+
}
|
|
133
|
+
async function processFile(input, output, prefix, options) {
|
|
134
|
+
var _stack = [];
|
|
135
|
+
try {
|
|
136
|
+
const timing = (0, import_ghost_story.createTiming)();
|
|
137
|
+
const outputFiles = [];
|
|
138
|
+
const audiobook = __using(_stack, await import_audiobook.Audiobook.from(input));
|
|
139
|
+
const maxHours = options.maxLength ?? 2;
|
|
140
|
+
const maxSeconds = 60 * 60 * maxHours;
|
|
141
|
+
const duration = await audiobook.getDuration();
|
|
142
|
+
const chapters = await audiobook.getChapters();
|
|
143
|
+
const ranges = await (0, import_ranges.getSafeChapterRanges)(
|
|
144
|
+
input,
|
|
145
|
+
duration,
|
|
146
|
+
chapters,
|
|
147
|
+
maxSeconds,
|
|
148
|
+
options.signal,
|
|
149
|
+
options.logger
|
|
150
|
+
);
|
|
151
|
+
await Promise.all(
|
|
152
|
+
ranges.map(async (range, index) => {
|
|
153
|
+
var _a, _b;
|
|
154
|
+
var _stack2 = [];
|
|
155
|
+
try {
|
|
156
|
+
const outputExtension = determineExtension(
|
|
157
|
+
range.filepath,
|
|
158
|
+
(_a = options.encoding) == null ? void 0 : _a.codec
|
|
159
|
+
);
|
|
160
|
+
const outputFilename = `${prefix}${(index + 1).toString().padStart(5, "0")}${outputExtension}`;
|
|
161
|
+
const outputFilepath = (0, import_node_path.join)(output, outputFilename);
|
|
162
|
+
const stack = __using(_stack2, new DisposableStack());
|
|
163
|
+
stack.defer(() => {
|
|
164
|
+
options.lock.release();
|
|
165
|
+
});
|
|
166
|
+
await options.lock.wait();
|
|
167
|
+
if ((_b = options.signal) == null ? void 0 : _b.aborted) throw new Error("Aborted");
|
|
168
|
+
await timing.timeAsync(
|
|
169
|
+
`${range.filepath},${range.start}:${range.end}`,
|
|
170
|
+
async () => {
|
|
171
|
+
var _a2;
|
|
172
|
+
const wasSplit = await (0, import_ffmpeg.splitFile)(
|
|
173
|
+
range.filepath,
|
|
174
|
+
outputFilepath,
|
|
175
|
+
range.start,
|
|
176
|
+
range.end,
|
|
177
|
+
options.encoding,
|
|
178
|
+
options.signal,
|
|
179
|
+
options.logger
|
|
180
|
+
);
|
|
181
|
+
if (wasSplit) {
|
|
182
|
+
outputFiles.push(outputFilename);
|
|
183
|
+
(_a2 = options.onProgress) == null ? void 0 : _a2.call(options, (outputFiles.length + 1) / ranges.length);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
);
|
|
187
|
+
} catch (_2) {
|
|
188
|
+
var _error2 = _2, _hasError2 = true;
|
|
189
|
+
} finally {
|
|
190
|
+
__callDispose(_stack2, _error2, _hasError2);
|
|
191
|
+
}
|
|
192
|
+
})
|
|
193
|
+
);
|
|
194
|
+
return { processedFiles: outputFiles, timing };
|
|
195
|
+
} catch (_) {
|
|
196
|
+
var _error = _, _hasError = true;
|
|
197
|
+
} finally {
|
|
198
|
+
__callDispose(_stack, _error, _hasError);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function determineExtension(input, codec) {
|
|
202
|
+
if (codec === "libmp3lame") {
|
|
203
|
+
return ".mp3";
|
|
204
|
+
}
|
|
205
|
+
if (codec === "aac" || codec === "libopus") {
|
|
206
|
+
return ".mp4";
|
|
207
|
+
}
|
|
208
|
+
if (import_audiobook.MP3_FILE_EXTENSIONS.some((ext) => input.endsWith(ext))) {
|
|
209
|
+
return ".mp3";
|
|
210
|
+
}
|
|
211
|
+
if (import_audiobook.MPEG4_FILE_EXTENSIONS.some((ext) => input.endsWith(ext)) || import_audiobook.OGG_FILE_EXTENSIONS.some((ext) => input.endsWith(ext)) || import_audiobook.OPUS_FILE_EXTENSIONS.some((ext) => input.endsWith(ext)) || import_audiobook.AAC_FILE_EXTENSIONS.some((ext) => input.endsWith(ext))) {
|
|
212
|
+
return ".mp4";
|
|
213
|
+
}
|
|
214
|
+
return (0, import_node_path.extname)(input);
|
|
215
|
+
}
|
|
216
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
217
|
+
0 && (module.exports = {
|
|
218
|
+
processAudiobook,
|
|
219
|
+
processFile
|
|
220
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as _storyteller_platform_ghost_story from '@storyteller-platform/ghost-story';
|
|
2
|
+
import { TimingAggregator } from '@storyteller-platform/ghost-story';
|
|
3
|
+
import { AsyncSemaphore } from '@esfx/async-semaphore';
|
|
4
|
+
import { Logger } from 'pino';
|
|
5
|
+
import { AudioEncoding } from './AudioEncoding.cjs';
|
|
6
|
+
|
|
7
|
+
interface ProcessOptions {
|
|
8
|
+
maxLength?: number | null | undefined;
|
|
9
|
+
encoding?: AudioEncoding | null | undefined;
|
|
10
|
+
parallelism?: number | null | undefined;
|
|
11
|
+
signal?: AbortSignal | null | undefined;
|
|
12
|
+
onProgress?: ((progress: number) => void) | null | undefined;
|
|
13
|
+
logger?: Logger | null | undefined;
|
|
14
|
+
}
|
|
15
|
+
declare function processAudiobook(input: string, output: string, options: ProcessOptions): Promise<TimingAggregator>;
|
|
16
|
+
interface ProcessFileOptions extends Omit<ProcessOptions, "parallelism"> {
|
|
17
|
+
lock: AsyncSemaphore;
|
|
18
|
+
}
|
|
19
|
+
declare function processFile(input: string, output: string, prefix: string, options: ProcessFileOptions): Promise<{
|
|
20
|
+
processedFiles: string[];
|
|
21
|
+
timing: _storyteller_platform_ghost_story.Timing;
|
|
22
|
+
}>;
|
|
23
|
+
|
|
24
|
+
export { type ProcessOptions, processAudiobook, processFile };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as _storyteller_platform_ghost_story from '@storyteller-platform/ghost-story';
|
|
2
|
+
import { TimingAggregator } from '@storyteller-platform/ghost-story';
|
|
3
|
+
import { AsyncSemaphore } from '@esfx/async-semaphore';
|
|
4
|
+
import { Logger } from 'pino';
|
|
5
|
+
import { AudioEncoding } from './AudioEncoding.js';
|
|
6
|
+
|
|
7
|
+
interface ProcessOptions {
|
|
8
|
+
maxLength?: number | null | undefined;
|
|
9
|
+
encoding?: AudioEncoding | null | undefined;
|
|
10
|
+
parallelism?: number | null | undefined;
|
|
11
|
+
signal?: AbortSignal | null | undefined;
|
|
12
|
+
onProgress?: ((progress: number) => void) | null | undefined;
|
|
13
|
+
logger?: Logger | null | undefined;
|
|
14
|
+
}
|
|
15
|
+
declare function processAudiobook(input: string, output: string, options: ProcessOptions): Promise<TimingAggregator>;
|
|
16
|
+
interface ProcessFileOptions extends Omit<ProcessOptions, "parallelism"> {
|
|
17
|
+
lock: AsyncSemaphore;
|
|
18
|
+
}
|
|
19
|
+
declare function processFile(input: string, output: string, prefix: string, options: ProcessFileOptions): Promise<{
|
|
20
|
+
processedFiles: string[];
|
|
21
|
+
timing: _storyteller_platform_ghost_story.Timing;
|
|
22
|
+
}>;
|
|
23
|
+
|
|
24
|
+
export { type ProcessOptions, processAudiobook, processFile };
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__callDispose,
|
|
3
|
+
__using
|
|
4
|
+
} from "../chunk-BIEQXUOY.js";
|
|
5
|
+
import { mkdir, readdir } from "node:fs/promises";
|
|
6
|
+
import { extname, join } from "node:path";
|
|
7
|
+
import { AsyncSemaphore } from "@esfx/async-semaphore";
|
|
8
|
+
import {
|
|
9
|
+
AAC_FILE_EXTENSIONS,
|
|
10
|
+
Audiobook,
|
|
11
|
+
MP3_FILE_EXTENSIONS,
|
|
12
|
+
MPEG4_FILE_EXTENSIONS,
|
|
13
|
+
OGG_FILE_EXTENSIONS,
|
|
14
|
+
OPUS_FILE_EXTENSIONS,
|
|
15
|
+
isAudioFile,
|
|
16
|
+
isZipArchive
|
|
17
|
+
} from "@storyteller-platform/audiobook";
|
|
18
|
+
import {
|
|
19
|
+
createAggregator,
|
|
20
|
+
createTiming
|
|
21
|
+
} from "@storyteller-platform/ghost-story";
|
|
22
|
+
import { splitFile } from "../common/ffmpeg.js";
|
|
23
|
+
import { getSafeChapterRanges } from "./ranges.js";
|
|
24
|
+
async function processAudiobook(input, output, options) {
|
|
25
|
+
var _a, _b, _c;
|
|
26
|
+
const timing = createAggregator();
|
|
27
|
+
timing.setMetadata("codec", ((_a = options.encoding) == null ? void 0 : _a.codec) ?? "unspecified");
|
|
28
|
+
timing.setMetadata("bitrate", ((_b = options.encoding) == null ? void 0 : _b.bitrate) ?? "unspecified");
|
|
29
|
+
timing.setMetadata("max-length", `${(options.maxLength ?? 2) * 60} minutes`);
|
|
30
|
+
await mkdir(output, { recursive: true });
|
|
31
|
+
const allFiles = await readdir(input, { recursive: true });
|
|
32
|
+
const filenames = allFiles.filter((f) => isAudioFile(f) || isZipArchive(f));
|
|
33
|
+
(_c = options.logger) == null ? void 0 : _c.debug(`Found ${filenames.length} files to process`);
|
|
34
|
+
const outputFiles = [];
|
|
35
|
+
const controller = new AbortController();
|
|
36
|
+
const signal = AbortSignal.any([
|
|
37
|
+
options.signal ?? new AbortSignal(),
|
|
38
|
+
controller.signal
|
|
39
|
+
]);
|
|
40
|
+
const semaphore = new AsyncSemaphore(options.parallelism ?? 1);
|
|
41
|
+
const perFileProgress = /* @__PURE__ */ new Map();
|
|
42
|
+
await Promise.all(
|
|
43
|
+
filenames.map(async (filename, index) => {
|
|
44
|
+
if (signal.aborted) throw new Error("Aborted");
|
|
45
|
+
const filepath = join(input, filename);
|
|
46
|
+
function onFileProgress(progress) {
|
|
47
|
+
var _a2, _b2;
|
|
48
|
+
perFileProgress.set(index, progress);
|
|
49
|
+
const updatedProgress = Array.from(perFileProgress.values()).reduce(
|
|
50
|
+
(acc, p) => acc + p
|
|
51
|
+
);
|
|
52
|
+
(_a2 = options.logger) == null ? void 0 : _a2.info(
|
|
53
|
+
`Progress: ${Math.floor(updatedProgress * 100)}%`
|
|
54
|
+
);
|
|
55
|
+
(_b2 = options.onProgress) == null ? void 0 : _b2.call(options, updatedProgress);
|
|
56
|
+
}
|
|
57
|
+
const fileOptions = {
|
|
58
|
+
...options,
|
|
59
|
+
signal,
|
|
60
|
+
lock: semaphore,
|
|
61
|
+
onProgress: onFileProgress
|
|
62
|
+
};
|
|
63
|
+
const { processedFiles, timing: fileTiming } = await processFile(
|
|
64
|
+
filepath,
|
|
65
|
+
output,
|
|
66
|
+
(index + 1).toString().padStart(5, "0") + "-",
|
|
67
|
+
fileOptions
|
|
68
|
+
);
|
|
69
|
+
timing.add(fileTiming.summary());
|
|
70
|
+
outputFiles.push(...processedFiles);
|
|
71
|
+
}).map(
|
|
72
|
+
(p) => p.catch((e) => {
|
|
73
|
+
controller.abort(e);
|
|
74
|
+
throw e;
|
|
75
|
+
})
|
|
76
|
+
)
|
|
77
|
+
);
|
|
78
|
+
return timing;
|
|
79
|
+
}
|
|
80
|
+
async function processFile(input, output, prefix, options) {
|
|
81
|
+
var _stack = [];
|
|
82
|
+
try {
|
|
83
|
+
const timing = createTiming();
|
|
84
|
+
const outputFiles = [];
|
|
85
|
+
const audiobook = __using(_stack, await Audiobook.from(input));
|
|
86
|
+
const maxHours = options.maxLength ?? 2;
|
|
87
|
+
const maxSeconds = 60 * 60 * maxHours;
|
|
88
|
+
const duration = await audiobook.getDuration();
|
|
89
|
+
const chapters = await audiobook.getChapters();
|
|
90
|
+
const ranges = await getSafeChapterRanges(
|
|
91
|
+
input,
|
|
92
|
+
duration,
|
|
93
|
+
chapters,
|
|
94
|
+
maxSeconds,
|
|
95
|
+
options.signal,
|
|
96
|
+
options.logger
|
|
97
|
+
);
|
|
98
|
+
await Promise.all(
|
|
99
|
+
ranges.map(async (range, index) => {
|
|
100
|
+
var _a, _b;
|
|
101
|
+
var _stack2 = [];
|
|
102
|
+
try {
|
|
103
|
+
const outputExtension = determineExtension(
|
|
104
|
+
range.filepath,
|
|
105
|
+
(_a = options.encoding) == null ? void 0 : _a.codec
|
|
106
|
+
);
|
|
107
|
+
const outputFilename = `${prefix}${(index + 1).toString().padStart(5, "0")}${outputExtension}`;
|
|
108
|
+
const outputFilepath = join(output, outputFilename);
|
|
109
|
+
const stack = __using(_stack2, new DisposableStack());
|
|
110
|
+
stack.defer(() => {
|
|
111
|
+
options.lock.release();
|
|
112
|
+
});
|
|
113
|
+
await options.lock.wait();
|
|
114
|
+
if ((_b = options.signal) == null ? void 0 : _b.aborted) throw new Error("Aborted");
|
|
115
|
+
await timing.timeAsync(
|
|
116
|
+
`${range.filepath},${range.start}:${range.end}`,
|
|
117
|
+
async () => {
|
|
118
|
+
var _a2;
|
|
119
|
+
const wasSplit = await splitFile(
|
|
120
|
+
range.filepath,
|
|
121
|
+
outputFilepath,
|
|
122
|
+
range.start,
|
|
123
|
+
range.end,
|
|
124
|
+
options.encoding,
|
|
125
|
+
options.signal,
|
|
126
|
+
options.logger
|
|
127
|
+
);
|
|
128
|
+
if (wasSplit) {
|
|
129
|
+
outputFiles.push(outputFilename);
|
|
130
|
+
(_a2 = options.onProgress) == null ? void 0 : _a2.call(options, (outputFiles.length + 1) / ranges.length);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
);
|
|
134
|
+
} catch (_2) {
|
|
135
|
+
var _error2 = _2, _hasError2 = true;
|
|
136
|
+
} finally {
|
|
137
|
+
__callDispose(_stack2, _error2, _hasError2);
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
);
|
|
141
|
+
return { processedFiles: outputFiles, timing };
|
|
142
|
+
} catch (_) {
|
|
143
|
+
var _error = _, _hasError = true;
|
|
144
|
+
} finally {
|
|
145
|
+
__callDispose(_stack, _error, _hasError);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function determineExtension(input, codec) {
|
|
149
|
+
if (codec === "libmp3lame") {
|
|
150
|
+
return ".mp3";
|
|
151
|
+
}
|
|
152
|
+
if (codec === "aac" || codec === "libopus") {
|
|
153
|
+
return ".mp4";
|
|
154
|
+
}
|
|
155
|
+
if (MP3_FILE_EXTENSIONS.some((ext) => input.endsWith(ext))) {
|
|
156
|
+
return ".mp3";
|
|
157
|
+
}
|
|
158
|
+
if (MPEG4_FILE_EXTENSIONS.some((ext) => input.endsWith(ext)) || OGG_FILE_EXTENSIONS.some((ext) => input.endsWith(ext)) || OPUS_FILE_EXTENSIONS.some((ext) => input.endsWith(ext)) || AAC_FILE_EXTENSIONS.some((ext) => input.endsWith(ext))) {
|
|
159
|
+
return ".mp4";
|
|
160
|
+
}
|
|
161
|
+
return extname(input);
|
|
162
|
+
}
|
|
163
|
+
export {
|
|
164
|
+
processAudiobook,
|
|
165
|
+
processFile
|
|
166
|
+
};
|
|
@@ -0,0 +1,203 @@
|
|
|
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 ranges_exports = {};
|
|
65
|
+
__export(ranges_exports, {
|
|
66
|
+
getSafeChapterRanges: () => getSafeChapterRanges,
|
|
67
|
+
getSafeRanges: () => getSafeRanges
|
|
68
|
+
});
|
|
69
|
+
module.exports = __toCommonJS(ranges_exports);
|
|
70
|
+
var import_node_crypto = require("node:crypto");
|
|
71
|
+
var import_promises = require("node:fs/promises");
|
|
72
|
+
var import_node_os = require("node:os");
|
|
73
|
+
var import_node_path = require("node:path");
|
|
74
|
+
var import_vad = require("@storyteller-platform/ghost-story/vad");
|
|
75
|
+
var import_ffmpeg = require("../common/ffmpeg.cjs");
|
|
76
|
+
async function getSafeChapterRanges(input, duration, chapters, maxSeconds, signal, logger) {
|
|
77
|
+
if (!chapters.length) {
|
|
78
|
+
logger == null ? void 0 : logger.info(
|
|
79
|
+
`Track is longer than ${maxSeconds / 60} minutes (${duration / 60}m); using VAD to determine safe split points.`
|
|
80
|
+
);
|
|
81
|
+
const ranges2 = await getSafeRanges(input, duration, maxSeconds, 0, signal);
|
|
82
|
+
return ranges2.map((r) => ({ filepath: input, ...r }));
|
|
83
|
+
}
|
|
84
|
+
const initialRanges = chapters.map((chapter, index) => {
|
|
85
|
+
const next = chapters[index + 1];
|
|
86
|
+
if (!next)
|
|
87
|
+
return {
|
|
88
|
+
filepath: chapter.filename,
|
|
89
|
+
start: chapter.start ?? 0,
|
|
90
|
+
end: duration
|
|
91
|
+
};
|
|
92
|
+
const end = next.filename === chapter.filename ? next.start ?? duration : duration;
|
|
93
|
+
return {
|
|
94
|
+
filepath: chapter.filename,
|
|
95
|
+
start: chapter.start ?? 0,
|
|
96
|
+
end
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
const ranges = [];
|
|
100
|
+
for (const range of initialRanges) {
|
|
101
|
+
const chapterDuration = range.end - range.start;
|
|
102
|
+
if (chapterDuration <= maxSeconds) {
|
|
103
|
+
ranges.push(range);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
logger == null ? void 0 : logger.info(
|
|
107
|
+
`Chapter is longer than ${maxSeconds / 60} minutes (${duration / 60}m); using VAD to determine safe split points.`
|
|
108
|
+
);
|
|
109
|
+
const chapterRanges = await getSafeRanges(
|
|
110
|
+
range.filepath,
|
|
111
|
+
chapterDuration,
|
|
112
|
+
maxSeconds,
|
|
113
|
+
range.start,
|
|
114
|
+
signal,
|
|
115
|
+
logger
|
|
116
|
+
);
|
|
117
|
+
ranges.push(
|
|
118
|
+
...chapterRanges.map((r) => ({ filepath: range.filepath, ...r }))
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return ranges;
|
|
122
|
+
}
|
|
123
|
+
async function getSafeRanges(input, duration, maxSeconds, start = 0, signal, logger) {
|
|
124
|
+
var _stack = [];
|
|
125
|
+
try {
|
|
126
|
+
const ext = (0, import_node_path.extname)(input);
|
|
127
|
+
const rawFilename = (0, import_node_path.basename)(input, ext);
|
|
128
|
+
const tmpDir = (0, import_node_path.join)((0, import_node_os.tmpdir)(), `storyteller-align-silence-${(0, import_node_crypto.randomUUID)()}`);
|
|
129
|
+
const stack = __using(_stack, new AsyncDisposableStack(), true);
|
|
130
|
+
stack.defer(async () => {
|
|
131
|
+
await (0, import_promises.rm)(tmpDir, { recursive: true, force: true });
|
|
132
|
+
});
|
|
133
|
+
const ranges = [{ start, end: duration + start }];
|
|
134
|
+
for (let i = 0; i + 1 < duration / maxSeconds; i++) {
|
|
135
|
+
if (signal == null ? void 0 : signal.aborted) throw new Error("Aborted");
|
|
136
|
+
const tmpFilepath = (0, import_node_path.join)(tmpDir, i.toString(), `${rawFilename}.wav`);
|
|
137
|
+
await (0, import_promises.mkdir)((0, import_node_path.dirname)(tmpFilepath), { recursive: true });
|
|
138
|
+
const approxCutPoint = start + maxSeconds * (i + 1);
|
|
139
|
+
const searchStart = approxCutPoint - 120;
|
|
140
|
+
const searchEnd = approxCutPoint;
|
|
141
|
+
await (0, import_ffmpeg.splitFile)(
|
|
142
|
+
input,
|
|
143
|
+
tmpFilepath,
|
|
144
|
+
searchStart,
|
|
145
|
+
searchEnd,
|
|
146
|
+
{},
|
|
147
|
+
signal,
|
|
148
|
+
logger
|
|
149
|
+
);
|
|
150
|
+
const vadTimeline = await (0, import_vad.detectVoiceActivity)(tmpFilepath, {
|
|
151
|
+
engine: "active-gate-og"
|
|
152
|
+
});
|
|
153
|
+
if (vadTimeline.length === 0) {
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
const silenceTimeline = vadTimeline.reduce((acc, entry) => {
|
|
157
|
+
const lastEntry = acc.at(-1);
|
|
158
|
+
if (!lastEntry) {
|
|
159
|
+
return [
|
|
160
|
+
...acc,
|
|
161
|
+
{
|
|
162
|
+
start: entry.endTime + searchStart,
|
|
163
|
+
end: entry.endTime + searchStart
|
|
164
|
+
}
|
|
165
|
+
];
|
|
166
|
+
}
|
|
167
|
+
return [
|
|
168
|
+
...acc.slice(0, -1),
|
|
169
|
+
{
|
|
170
|
+
...lastEntry,
|
|
171
|
+
end: entry.startTime + searchStart
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
start: entry.endTime + searchStart,
|
|
175
|
+
end: entry.endTime + searchStart
|
|
176
|
+
}
|
|
177
|
+
];
|
|
178
|
+
}, []);
|
|
179
|
+
const nearestLikelySentenceBreak = silenceTimeline.reduce((acc, entry) => {
|
|
180
|
+
const currLength = acc.end - acc.start;
|
|
181
|
+
const entryLength = entry.end - entry.start;
|
|
182
|
+
return currLength > entryLength ? acc : entry;
|
|
183
|
+
});
|
|
184
|
+
const lastRange = ranges.at(-1);
|
|
185
|
+
lastRange.end = nearestLikelySentenceBreak.start;
|
|
186
|
+
ranges.push({
|
|
187
|
+
start: nearestLikelySentenceBreak.end,
|
|
188
|
+
end: duration + start
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
return ranges;
|
|
192
|
+
} catch (_) {
|
|
193
|
+
var _error = _, _hasError = true;
|
|
194
|
+
} finally {
|
|
195
|
+
var _promise = __callDispose(_stack, _error, _hasError);
|
|
196
|
+
_promise && await _promise;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
200
|
+
0 && (module.exports = {
|
|
201
|
+
getSafeChapterRanges,
|
|
202
|
+
getSafeRanges
|
|
203
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Logger } from 'pino';
|
|
2
|
+
import { AudiobookChapter } from '@storyteller-platform/audiobook';
|
|
3
|
+
|
|
4
|
+
declare function getSafeChapterRanges(input: string, duration: number, chapters: AudiobookChapter[], maxSeconds: number, signal?: AbortSignal | null, logger?: Logger | null): Promise<{
|
|
5
|
+
start: number;
|
|
6
|
+
end: number;
|
|
7
|
+
filepath: string;
|
|
8
|
+
}[]>;
|
|
9
|
+
interface Range {
|
|
10
|
+
start: number;
|
|
11
|
+
end: number;
|
|
12
|
+
}
|
|
13
|
+
declare function getSafeRanges(input: string, duration: number, maxSeconds: number, start?: number, signal?: AbortSignal | null, logger?: Logger | null): Promise<Range[]>;
|
|
14
|
+
|
|
15
|
+
export { getSafeChapterRanges, getSafeRanges };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Logger } from 'pino';
|
|
2
|
+
import { AudiobookChapter } from '@storyteller-platform/audiobook';
|
|
3
|
+
|
|
4
|
+
declare function getSafeChapterRanges(input: string, duration: number, chapters: AudiobookChapter[], maxSeconds: number, signal?: AbortSignal | null, logger?: Logger | null): Promise<{
|
|
5
|
+
start: number;
|
|
6
|
+
end: number;
|
|
7
|
+
filepath: string;
|
|
8
|
+
}[]>;
|
|
9
|
+
interface Range {
|
|
10
|
+
start: number;
|
|
11
|
+
end: number;
|
|
12
|
+
}
|
|
13
|
+
declare function getSafeRanges(input: string, duration: number, maxSeconds: number, start?: number, signal?: AbortSignal | null, logger?: Logger | null): Promise<Range[]>;
|
|
14
|
+
|
|
15
|
+
export { getSafeChapterRanges, getSafeRanges };
|