@storyteller-platform/align 0.1.47 → 0.1.48
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/common/ffmpeg.cjs +72 -19
- package/dist/common/ffmpeg.d.cts +17 -1
- package/dist/common/ffmpeg.d.ts +17 -1
- package/dist/common/ffmpeg.js +69 -19
- package/dist/process/processAudiobook.cjs +36 -2
- package/dist/process/processAudiobook.js +42 -3
- package/package.json +2 -2
package/dist/common/ffmpeg.cjs
CHANGED
|
@@ -28,8 +28,11 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
28
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
29
|
var ffmpeg_exports = {};
|
|
30
30
|
__export(ffmpeg_exports, {
|
|
31
|
+
MP3_CBR_BITRATES: () => MP3_CBR_BITRATES,
|
|
31
32
|
getTrackDuration: () => getTrackDuration,
|
|
32
33
|
getTrackInfo: () => getTrackInfo,
|
|
34
|
+
isVbrMp3: () => isVbrMp3,
|
|
35
|
+
selectCbrBitrate: () => selectCbrBitrate,
|
|
33
36
|
splitFile: () => splitFile,
|
|
34
37
|
transcodeFile: () => transcodeFile
|
|
35
38
|
});
|
|
@@ -74,6 +77,52 @@ async function getTrackDuration(path, logger) {
|
|
|
74
77
|
const info = await getTrackInfo(path, logger);
|
|
75
78
|
return info["duration"];
|
|
76
79
|
}
|
|
80
|
+
const MP3_CBR_BITRATES = [
|
|
81
|
+
64e3,
|
|
82
|
+
8e4,
|
|
83
|
+
96e3,
|
|
84
|
+
112e3,
|
|
85
|
+
128e3,
|
|
86
|
+
16e4,
|
|
87
|
+
192e3,
|
|
88
|
+
224e3,
|
|
89
|
+
256e3,
|
|
90
|
+
32e4
|
|
91
|
+
];
|
|
92
|
+
const VBR_PROBE_PACKET_COUNT = 50;
|
|
93
|
+
const MP3_CBR_MAX_DISTINCT_SIZES = 2;
|
|
94
|
+
const VBR_PROBE_MIN_SEEKABLE_SECONDS = 180;
|
|
95
|
+
async function probeAudioDuration(path) {
|
|
96
|
+
const stdout = await execCmd(
|
|
97
|
+
`ffprobe -i ${(0, import_shell.quotePath)(path)} -v error -show_entries format=duration -output_format json`
|
|
98
|
+
);
|
|
99
|
+
const { format } = JSON.parse(stdout);
|
|
100
|
+
const duration = Number(format?.duration);
|
|
101
|
+
return Number.isFinite(duration) && duration > 0 ? duration : null;
|
|
102
|
+
}
|
|
103
|
+
async function probePacketSizes(path, startSeconds) {
|
|
104
|
+
const interval = startSeconds > 0 ? `${startSeconds}%+#${VBR_PROBE_PACKET_COUNT}` : `%+#${VBR_PROBE_PACKET_COUNT}`;
|
|
105
|
+
const stdout = await execCmd(
|
|
106
|
+
`ffprobe -i ${(0, import_shell.quotePath)(path)} -v error -select_streams a:0 -read_intervals "${interval}" -show_entries packet=size -output_format json`
|
|
107
|
+
);
|
|
108
|
+
const { packets } = JSON.parse(stdout);
|
|
109
|
+
return (packets ?? []).map((packet) => Number(packet.size)).filter((size) => Number.isFinite(size) && size > 0);
|
|
110
|
+
}
|
|
111
|
+
async function isVbrMp3(path) {
|
|
112
|
+
if ((0, import_node_path.extname)(path).toLowerCase() !== ".mp3") return false;
|
|
113
|
+
const duration = await probeAudioDuration(path);
|
|
114
|
+
const startSeconds = duration && duration > VBR_PROBE_MIN_SEEKABLE_SECONDS ? Math.floor(duration / 3) : 0;
|
|
115
|
+
let sizes = await probePacketSizes(path, startSeconds);
|
|
116
|
+
if (sizes.length === 0 && startSeconds > 0) {
|
|
117
|
+
sizes = await probePacketSizes(path, 0);
|
|
118
|
+
}
|
|
119
|
+
if (sizes.length === 0) return false;
|
|
120
|
+
const distinctSizes = new Set(sizes).size;
|
|
121
|
+
return distinctSizes > MP3_CBR_MAX_DISTINCT_SIZES;
|
|
122
|
+
}
|
|
123
|
+
function selectCbrBitrate(averageBitrate) {
|
|
124
|
+
return MP3_CBR_BITRATES.find((tier) => tier >= averageBitrate) ?? MP3_CBR_BITRATES.at(-1) ?? MP3_CBR_BITRATES[0];
|
|
125
|
+
}
|
|
77
126
|
function parseTrackInfo(format) {
|
|
78
127
|
return {
|
|
79
128
|
filename: format.filename,
|
|
@@ -136,15 +185,16 @@ async function constructExtractCoverArtCommand(source, destExtension) {
|
|
|
136
185
|
];
|
|
137
186
|
return `${command} ${args.join(" ")} | `;
|
|
138
187
|
}
|
|
139
|
-
function commonFfmpegArguments(
|
|
188
|
+
function commonFfmpegArguments(options) {
|
|
189
|
+
const { sourceExtension, destExtension, codec, bitrate } = options;
|
|
140
190
|
const args = ["-vn"];
|
|
141
191
|
if (codec) {
|
|
142
|
-
args.push(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
192
|
+
args.push("-c:a", codec);
|
|
193
|
+
if (codec === "libopus") {
|
|
194
|
+
args.push("-b:a", bitrate && /^\d+[kK]$/i.test(bitrate) ? bitrate : "32K");
|
|
195
|
+
} else if (codec === "libmp3lame" && bitrate) {
|
|
196
|
+
args.push("-b:a", bitrate);
|
|
197
|
+
}
|
|
148
198
|
} else if ((0, import_mime.areSameType)(sourceExtension, destExtension) || destExtension == ".mp4") {
|
|
149
199
|
args.push("-c:a", "copy");
|
|
150
200
|
}
|
|
@@ -168,12 +218,12 @@ async function splitFile(input, output, start, end, encoding, signal, logger) {
|
|
|
168
218
|
end,
|
|
169
219
|
"-i",
|
|
170
220
|
(0, import_shell.quotePath)(input),
|
|
171
|
-
...commonFfmpegArguments(
|
|
172
|
-
(0, import_node_path.extname)(input),
|
|
173
|
-
(0, import_node_path.extname)(output),
|
|
174
|
-
encoding?.codec ?? null,
|
|
175
|
-
encoding?.bitrate ?? null
|
|
176
|
-
),
|
|
221
|
+
...commonFfmpegArguments({
|
|
222
|
+
sourceExtension: (0, import_node_path.extname)(input),
|
|
223
|
+
destExtension: (0, import_node_path.extname)(output),
|
|
224
|
+
codec: encoding?.codec ?? null,
|
|
225
|
+
bitrate: encoding?.bitrate ?? null
|
|
226
|
+
}),
|
|
177
227
|
(0, import_shell.quotePath)(output)
|
|
178
228
|
];
|
|
179
229
|
const coverArtCommand = await constructExtractCoverArtCommand(
|
|
@@ -203,12 +253,12 @@ async function transcodeFile(input, output, encoding, signal, logger) {
|
|
|
203
253
|
"-nostdin",
|
|
204
254
|
"-i",
|
|
205
255
|
(0, import_shell.quotePath)(input),
|
|
206
|
-
...commonFfmpegArguments(
|
|
207
|
-
(0, import_node_path.extname)(input),
|
|
208
|
-
(0, import_node_path.extname)(output),
|
|
209
|
-
encoding?.codec ?? null,
|
|
210
|
-
encoding?.bitrate ?? null
|
|
211
|
-
),
|
|
256
|
+
...commonFfmpegArguments({
|
|
257
|
+
sourceExtension: (0, import_node_path.extname)(input),
|
|
258
|
+
destExtension: (0, import_node_path.extname)(output),
|
|
259
|
+
codec: encoding?.codec ?? null,
|
|
260
|
+
bitrate: encoding?.bitrate ?? null
|
|
261
|
+
}),
|
|
212
262
|
(0, import_shell.quotePath)(output)
|
|
213
263
|
];
|
|
214
264
|
const coverArtCommand = await constructExtractCoverArtCommand(
|
|
@@ -224,8 +274,11 @@ async function transcodeFile(input, output, encoding, signal, logger) {
|
|
|
224
274
|
}
|
|
225
275
|
// Annotate the CommonJS export names for ESM import in node:
|
|
226
276
|
0 && (module.exports = {
|
|
277
|
+
MP3_CBR_BITRATES,
|
|
227
278
|
getTrackDuration,
|
|
228
279
|
getTrackInfo,
|
|
280
|
+
isVbrMp3,
|
|
281
|
+
selectCbrBitrate,
|
|
229
282
|
splitFile,
|
|
230
283
|
transcodeFile
|
|
231
284
|
});
|
package/dist/common/ffmpeg.d.cts
CHANGED
|
@@ -3,6 +3,22 @@ import { AudioEncoding } from '../process/AudioEncoding.cjs';
|
|
|
3
3
|
|
|
4
4
|
declare const getTrackInfo: (path: string, logger?: Logger) => Promise<TrackInfo>;
|
|
5
5
|
declare function getTrackDuration(path: string, logger?: Logger): Promise<number>;
|
|
6
|
+
/**
|
|
7
|
+
* CBR bitrates (bps) offered for MP3 output, roughly matching LAME -V9..-V0
|
|
8
|
+
*/
|
|
9
|
+
declare const MP3_CBR_BITRATES: readonly [64000, 80000, 96000, 112000, 128000, 160000, 192000, 224000, 256000, 320000];
|
|
10
|
+
/**
|
|
11
|
+
* Detect whether an MP3 file uses a variable bitrate
|
|
12
|
+
* Does this by sampling the first few packets and checking if the sizes are different
|
|
13
|
+
* CBR MP3 files will have the same packet size for the entire file
|
|
14
|
+
*
|
|
15
|
+
* Can't really trust the reported bitrate to tell CBR from VBR
|
|
16
|
+
* LAME writes a Xing header carrying the *average* bitrate,
|
|
17
|
+
* which ffprobe surfaces as a normal per-stream `bit_rate`,
|
|
18
|
+
* so a VBR file looks identical to a CBR one by that measure.
|
|
19
|
+
*/
|
|
20
|
+
declare function isVbrMp3(path: string): Promise<boolean>;
|
|
21
|
+
declare function selectCbrBitrate(averageBitrate: number): number;
|
|
6
22
|
type TrackInfo = {
|
|
7
23
|
filename: string;
|
|
8
24
|
nbStreams: number;
|
|
@@ -30,4 +46,4 @@ type TrackInfo = {
|
|
|
30
46
|
declare function splitFile(input: string, output: string, start: number, end: number, encoding?: AudioEncoding | null, signal?: AbortSignal | null, logger?: Logger | null): Promise<boolean>;
|
|
31
47
|
declare function transcodeFile(input: string, output: string, encoding?: AudioEncoding | null, signal?: AbortSignal | null, logger?: Logger | null): Promise<true | undefined>;
|
|
32
48
|
|
|
33
|
-
export { getTrackDuration, getTrackInfo, splitFile, transcodeFile };
|
|
49
|
+
export { MP3_CBR_BITRATES, getTrackDuration, getTrackInfo, isVbrMp3, selectCbrBitrate, splitFile, transcodeFile };
|
package/dist/common/ffmpeg.d.ts
CHANGED
|
@@ -3,6 +3,22 @@ import { AudioEncoding } from '../process/AudioEncoding.js';
|
|
|
3
3
|
|
|
4
4
|
declare const getTrackInfo: (path: string, logger?: Logger) => Promise<TrackInfo>;
|
|
5
5
|
declare function getTrackDuration(path: string, logger?: Logger): Promise<number>;
|
|
6
|
+
/**
|
|
7
|
+
* CBR bitrates (bps) offered for MP3 output, roughly matching LAME -V9..-V0
|
|
8
|
+
*/
|
|
9
|
+
declare const MP3_CBR_BITRATES: readonly [64000, 80000, 96000, 112000, 128000, 160000, 192000, 224000, 256000, 320000];
|
|
10
|
+
/**
|
|
11
|
+
* Detect whether an MP3 file uses a variable bitrate
|
|
12
|
+
* Does this by sampling the first few packets and checking if the sizes are different
|
|
13
|
+
* CBR MP3 files will have the same packet size for the entire file
|
|
14
|
+
*
|
|
15
|
+
* Can't really trust the reported bitrate to tell CBR from VBR
|
|
16
|
+
* LAME writes a Xing header carrying the *average* bitrate,
|
|
17
|
+
* which ffprobe surfaces as a normal per-stream `bit_rate`,
|
|
18
|
+
* so a VBR file looks identical to a CBR one by that measure.
|
|
19
|
+
*/
|
|
20
|
+
declare function isVbrMp3(path: string): Promise<boolean>;
|
|
21
|
+
declare function selectCbrBitrate(averageBitrate: number): number;
|
|
6
22
|
type TrackInfo = {
|
|
7
23
|
filename: string;
|
|
8
24
|
nbStreams: number;
|
|
@@ -30,4 +46,4 @@ type TrackInfo = {
|
|
|
30
46
|
declare function splitFile(input: string, output: string, start: number, end: number, encoding?: AudioEncoding | null, signal?: AbortSignal | null, logger?: Logger | null): Promise<boolean>;
|
|
31
47
|
declare function transcodeFile(input: string, output: string, encoding?: AudioEncoding | null, signal?: AbortSignal | null, logger?: Logger | null): Promise<true | undefined>;
|
|
32
48
|
|
|
33
|
-
export { getTrackDuration, getTrackInfo, splitFile, transcodeFile };
|
|
49
|
+
export { MP3_CBR_BITRATES, getTrackDuration, getTrackInfo, isVbrMp3, selectCbrBitrate, splitFile, transcodeFile };
|
package/dist/common/ffmpeg.js
CHANGED
|
@@ -39,6 +39,52 @@ async function getTrackDuration(path, logger) {
|
|
|
39
39
|
const info = await getTrackInfo(path, logger);
|
|
40
40
|
return info["duration"];
|
|
41
41
|
}
|
|
42
|
+
const MP3_CBR_BITRATES = [
|
|
43
|
+
64e3,
|
|
44
|
+
8e4,
|
|
45
|
+
96e3,
|
|
46
|
+
112e3,
|
|
47
|
+
128e3,
|
|
48
|
+
16e4,
|
|
49
|
+
192e3,
|
|
50
|
+
224e3,
|
|
51
|
+
256e3,
|
|
52
|
+
32e4
|
|
53
|
+
];
|
|
54
|
+
const VBR_PROBE_PACKET_COUNT = 50;
|
|
55
|
+
const MP3_CBR_MAX_DISTINCT_SIZES = 2;
|
|
56
|
+
const VBR_PROBE_MIN_SEEKABLE_SECONDS = 180;
|
|
57
|
+
async function probeAudioDuration(path) {
|
|
58
|
+
const stdout = await execCmd(
|
|
59
|
+
`ffprobe -i ${quotePath(path)} -v error -show_entries format=duration -output_format json`
|
|
60
|
+
);
|
|
61
|
+
const { format } = JSON.parse(stdout);
|
|
62
|
+
const duration = Number(format?.duration);
|
|
63
|
+
return Number.isFinite(duration) && duration > 0 ? duration : null;
|
|
64
|
+
}
|
|
65
|
+
async function probePacketSizes(path, startSeconds) {
|
|
66
|
+
const interval = startSeconds > 0 ? `${startSeconds}%+#${VBR_PROBE_PACKET_COUNT}` : `%+#${VBR_PROBE_PACKET_COUNT}`;
|
|
67
|
+
const stdout = await execCmd(
|
|
68
|
+
`ffprobe -i ${quotePath(path)} -v error -select_streams a:0 -read_intervals "${interval}" -show_entries packet=size -output_format json`
|
|
69
|
+
);
|
|
70
|
+
const { packets } = JSON.parse(stdout);
|
|
71
|
+
return (packets ?? []).map((packet) => Number(packet.size)).filter((size) => Number.isFinite(size) && size > 0);
|
|
72
|
+
}
|
|
73
|
+
async function isVbrMp3(path) {
|
|
74
|
+
if (extname(path).toLowerCase() !== ".mp3") return false;
|
|
75
|
+
const duration = await probeAudioDuration(path);
|
|
76
|
+
const startSeconds = duration && duration > VBR_PROBE_MIN_SEEKABLE_SECONDS ? Math.floor(duration / 3) : 0;
|
|
77
|
+
let sizes = await probePacketSizes(path, startSeconds);
|
|
78
|
+
if (sizes.length === 0 && startSeconds > 0) {
|
|
79
|
+
sizes = await probePacketSizes(path, 0);
|
|
80
|
+
}
|
|
81
|
+
if (sizes.length === 0) return false;
|
|
82
|
+
const distinctSizes = new Set(sizes).size;
|
|
83
|
+
return distinctSizes > MP3_CBR_MAX_DISTINCT_SIZES;
|
|
84
|
+
}
|
|
85
|
+
function selectCbrBitrate(averageBitrate) {
|
|
86
|
+
return MP3_CBR_BITRATES.find((tier) => tier >= averageBitrate) ?? MP3_CBR_BITRATES.at(-1) ?? MP3_CBR_BITRATES[0];
|
|
87
|
+
}
|
|
42
88
|
function parseTrackInfo(format) {
|
|
43
89
|
return {
|
|
44
90
|
filename: format.filename,
|
|
@@ -101,15 +147,16 @@ async function constructExtractCoverArtCommand(source, destExtension) {
|
|
|
101
147
|
];
|
|
102
148
|
return `${command} ${args.join(" ")} | `;
|
|
103
149
|
}
|
|
104
|
-
function commonFfmpegArguments(
|
|
150
|
+
function commonFfmpegArguments(options) {
|
|
151
|
+
const { sourceExtension, destExtension, codec, bitrate } = options;
|
|
105
152
|
const args = ["-vn"];
|
|
106
153
|
if (codec) {
|
|
107
|
-
args.push(
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
154
|
+
args.push("-c:a", codec);
|
|
155
|
+
if (codec === "libopus") {
|
|
156
|
+
args.push("-b:a", bitrate && /^\d+[kK]$/i.test(bitrate) ? bitrate : "32K");
|
|
157
|
+
} else if (codec === "libmp3lame" && bitrate) {
|
|
158
|
+
args.push("-b:a", bitrate);
|
|
159
|
+
}
|
|
113
160
|
} else if (areSameType(sourceExtension, destExtension) || destExtension == ".mp4") {
|
|
114
161
|
args.push("-c:a", "copy");
|
|
115
162
|
}
|
|
@@ -133,12 +180,12 @@ async function splitFile(input, output, start, end, encoding, signal, logger) {
|
|
|
133
180
|
end,
|
|
134
181
|
"-i",
|
|
135
182
|
quotePath(input),
|
|
136
|
-
...commonFfmpegArguments(
|
|
137
|
-
extname(input),
|
|
138
|
-
extname(output),
|
|
139
|
-
encoding?.codec ?? null,
|
|
140
|
-
encoding?.bitrate ?? null
|
|
141
|
-
),
|
|
183
|
+
...commonFfmpegArguments({
|
|
184
|
+
sourceExtension: extname(input),
|
|
185
|
+
destExtension: extname(output),
|
|
186
|
+
codec: encoding?.codec ?? null,
|
|
187
|
+
bitrate: encoding?.bitrate ?? null
|
|
188
|
+
}),
|
|
142
189
|
quotePath(output)
|
|
143
190
|
];
|
|
144
191
|
const coverArtCommand = await constructExtractCoverArtCommand(
|
|
@@ -168,12 +215,12 @@ async function transcodeFile(input, output, encoding, signal, logger) {
|
|
|
168
215
|
"-nostdin",
|
|
169
216
|
"-i",
|
|
170
217
|
quotePath(input),
|
|
171
|
-
...commonFfmpegArguments(
|
|
172
|
-
extname(input),
|
|
173
|
-
extname(output),
|
|
174
|
-
encoding?.codec ?? null,
|
|
175
|
-
encoding?.bitrate ?? null
|
|
176
|
-
),
|
|
218
|
+
...commonFfmpegArguments({
|
|
219
|
+
sourceExtension: extname(input),
|
|
220
|
+
destExtension: extname(output),
|
|
221
|
+
codec: encoding?.codec ?? null,
|
|
222
|
+
bitrate: encoding?.bitrate ?? null
|
|
223
|
+
}),
|
|
177
224
|
quotePath(output)
|
|
178
225
|
];
|
|
179
226
|
const coverArtCommand = await constructExtractCoverArtCommand(
|
|
@@ -188,8 +235,11 @@ async function transcodeFile(input, output, encoding, signal, logger) {
|
|
|
188
235
|
return true;
|
|
189
236
|
}
|
|
190
237
|
export {
|
|
238
|
+
MP3_CBR_BITRATES,
|
|
191
239
|
getTrackDuration,
|
|
192
240
|
getTrackInfo,
|
|
241
|
+
isVbrMp3,
|
|
242
|
+
selectCbrBitrate,
|
|
193
243
|
splitFile,
|
|
194
244
|
transcodeFile
|
|
195
245
|
};
|
|
@@ -126,6 +126,27 @@ async function processAudiobook(input, output, options) {
|
|
|
126
126
|
);
|
|
127
127
|
return timing;
|
|
128
128
|
}
|
|
129
|
+
async function resolveVbrEncoding(filepath, userEncoding, logger) {
|
|
130
|
+
if (userEncoding?.codec && userEncoding.codec !== "libmp3lame") {
|
|
131
|
+
return userEncoding;
|
|
132
|
+
}
|
|
133
|
+
const sourceIsMp3 = (0, import_node_path.extname)(filepath).toLowerCase() === ".mp3";
|
|
134
|
+
if (!userEncoding?.codec && !sourceIsMp3) {
|
|
135
|
+
return userEncoding;
|
|
136
|
+
}
|
|
137
|
+
if (!userEncoding?.codec && !await (0, import_ffmpeg.isVbrMp3)(filepath)) {
|
|
138
|
+
return userEncoding;
|
|
139
|
+
}
|
|
140
|
+
const trackInfo = await (0, import_ffmpeg.getTrackInfo)(filepath, logger ?? void 0);
|
|
141
|
+
const targetBitrate = (0, import_ffmpeg.selectCbrBitrate)(trackInfo.bitRate);
|
|
142
|
+
logger?.info(
|
|
143
|
+
`Forcing CBR MP3 for ${filepath} (avg ${trackInfo.bitRate}bps) at ${targetBitrate / 1e3}k`
|
|
144
|
+
);
|
|
145
|
+
return {
|
|
146
|
+
codec: "libmp3lame",
|
|
147
|
+
bitrate: `${targetBitrate / 1e3}k`
|
|
148
|
+
};
|
|
149
|
+
}
|
|
129
150
|
async function processFile(input, output, prefix, options) {
|
|
130
151
|
var _stack = [];
|
|
131
152
|
try {
|
|
@@ -144,13 +165,26 @@ async function processFile(input, output, prefix, options) {
|
|
|
144
165
|
options.signal,
|
|
145
166
|
options.logger
|
|
146
167
|
);
|
|
168
|
+
const vbrEncodings = /* @__PURE__ */ new Map();
|
|
169
|
+
const uniqueFilepaths = [...new Set(ranges.map((r) => r.filepath))];
|
|
170
|
+
await Promise.all(
|
|
171
|
+
uniqueFilepaths.map(async (filepath) => {
|
|
172
|
+
const result = await resolveVbrEncoding(
|
|
173
|
+
filepath,
|
|
174
|
+
options.encoding,
|
|
175
|
+
options.logger
|
|
176
|
+
);
|
|
177
|
+
vbrEncodings.set(filepath, result);
|
|
178
|
+
})
|
|
179
|
+
);
|
|
147
180
|
await Promise.all(
|
|
148
181
|
ranges.map(async (range, index) => {
|
|
149
182
|
var _stack2 = [];
|
|
150
183
|
try {
|
|
184
|
+
const effectiveEncoding = vbrEncodings.has(range.filepath) ? vbrEncodings.get(range.filepath) : options.encoding;
|
|
151
185
|
const outputExtension = determineExtension(
|
|
152
186
|
range.filepath,
|
|
153
|
-
|
|
187
|
+
effectiveEncoding?.codec
|
|
154
188
|
);
|
|
155
189
|
const outputFilename = `${prefix}${(index + 1).toString().padStart(5, "0")}${outputExtension}`;
|
|
156
190
|
const outputFilepath = (0, import_node_path.join)(output, outputFilename);
|
|
@@ -168,7 +202,7 @@ async function processFile(input, output, prefix, options) {
|
|
|
168
202
|
outputFilepath,
|
|
169
203
|
range.start,
|
|
170
204
|
range.end,
|
|
171
|
-
|
|
205
|
+
effectiveEncoding,
|
|
172
206
|
options.signal,
|
|
173
207
|
options.logger
|
|
174
208
|
);
|
|
@@ -19,7 +19,12 @@ import {
|
|
|
19
19
|
createAggregator,
|
|
20
20
|
createTiming
|
|
21
21
|
} from "@storyteller-platform/ghost-story";
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
getTrackInfo,
|
|
24
|
+
isVbrMp3,
|
|
25
|
+
selectCbrBitrate,
|
|
26
|
+
splitFile
|
|
27
|
+
} from "../common/ffmpeg.js";
|
|
23
28
|
import { getSafeChapterRanges } from "./ranges.js";
|
|
24
29
|
async function processAudiobook(input, output, options) {
|
|
25
30
|
const timing = createAggregator();
|
|
@@ -73,6 +78,27 @@ async function processAudiobook(input, output, options) {
|
|
|
73
78
|
);
|
|
74
79
|
return timing;
|
|
75
80
|
}
|
|
81
|
+
async function resolveVbrEncoding(filepath, userEncoding, logger) {
|
|
82
|
+
if (userEncoding?.codec && userEncoding.codec !== "libmp3lame") {
|
|
83
|
+
return userEncoding;
|
|
84
|
+
}
|
|
85
|
+
const sourceIsMp3 = extname(filepath).toLowerCase() === ".mp3";
|
|
86
|
+
if (!userEncoding?.codec && !sourceIsMp3) {
|
|
87
|
+
return userEncoding;
|
|
88
|
+
}
|
|
89
|
+
if (!userEncoding?.codec && !await isVbrMp3(filepath)) {
|
|
90
|
+
return userEncoding;
|
|
91
|
+
}
|
|
92
|
+
const trackInfo = await getTrackInfo(filepath, logger ?? void 0);
|
|
93
|
+
const targetBitrate = selectCbrBitrate(trackInfo.bitRate);
|
|
94
|
+
logger?.info(
|
|
95
|
+
`Forcing CBR MP3 for ${filepath} (avg ${trackInfo.bitRate}bps) at ${targetBitrate / 1e3}k`
|
|
96
|
+
);
|
|
97
|
+
return {
|
|
98
|
+
codec: "libmp3lame",
|
|
99
|
+
bitrate: `${targetBitrate / 1e3}k`
|
|
100
|
+
};
|
|
101
|
+
}
|
|
76
102
|
async function processFile(input, output, prefix, options) {
|
|
77
103
|
var _stack = [];
|
|
78
104
|
try {
|
|
@@ -91,13 +117,26 @@ async function processFile(input, output, prefix, options) {
|
|
|
91
117
|
options.signal,
|
|
92
118
|
options.logger
|
|
93
119
|
);
|
|
120
|
+
const vbrEncodings = /* @__PURE__ */ new Map();
|
|
121
|
+
const uniqueFilepaths = [...new Set(ranges.map((r) => r.filepath))];
|
|
122
|
+
await Promise.all(
|
|
123
|
+
uniqueFilepaths.map(async (filepath) => {
|
|
124
|
+
const result = await resolveVbrEncoding(
|
|
125
|
+
filepath,
|
|
126
|
+
options.encoding,
|
|
127
|
+
options.logger
|
|
128
|
+
);
|
|
129
|
+
vbrEncodings.set(filepath, result);
|
|
130
|
+
})
|
|
131
|
+
);
|
|
94
132
|
await Promise.all(
|
|
95
133
|
ranges.map(async (range, index) => {
|
|
96
134
|
var _stack2 = [];
|
|
97
135
|
try {
|
|
136
|
+
const effectiveEncoding = vbrEncodings.has(range.filepath) ? vbrEncodings.get(range.filepath) : options.encoding;
|
|
98
137
|
const outputExtension = determineExtension(
|
|
99
138
|
range.filepath,
|
|
100
|
-
|
|
139
|
+
effectiveEncoding?.codec
|
|
101
140
|
);
|
|
102
141
|
const outputFilename = `${prefix}${(index + 1).toString().padStart(5, "0")}${outputExtension}`;
|
|
103
142
|
const outputFilepath = join(output, outputFilename);
|
|
@@ -115,7 +154,7 @@ async function processFile(input, output, prefix, options) {
|
|
|
115
154
|
outputFilepath,
|
|
116
155
|
range.start,
|
|
117
156
|
range.end,
|
|
118
|
-
|
|
157
|
+
effectiveEncoding,
|
|
119
158
|
options.signal,
|
|
120
159
|
options.logger
|
|
121
160
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@storyteller-platform/align",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.48",
|
|
4
4
|
"description": "A library and CLI for automatically aligning audiobooks and EPUBs to produce Media Overlays",
|
|
5
5
|
"author": "Shane Friedman",
|
|
6
6
|
"license": "MIT",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"@optique/run": "^0.10.7",
|
|
72
72
|
"@readium/shared": "^2.2.0",
|
|
73
73
|
"@storyteller-platform/audiobook": "^0.4.1",
|
|
74
|
-
"@storyteller-platform/epub": "^0.6.
|
|
74
|
+
"@storyteller-platform/epub": "^0.6.2",
|
|
75
75
|
"@storyteller-platform/ghost-story": "^0.1.11",
|
|
76
76
|
"@storyteller-platform/transliteration": "^3.1.2",
|
|
77
77
|
"chalk": "^5.4.1",
|