@bendyline/squisq-cli 1.1.6 → 1.2.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/dist/api.js CHANGED
@@ -1,32 +1,198 @@
1
1
  // src/api.ts
2
+ import { resolveMediaSchedule } from "@bendyline/squisq/schemas";
3
+ import { flattenBlocks } from "@bendyline/squisq/doc";
2
4
  import { generateRenderHtml } from "@bendyline/squisq-video";
3
5
  import { resolveDimensions } from "@bendyline/squisq-video";
6
+ import { convert as formatsConvert } from "@bendyline/squisq-formats";
4
7
 
5
8
  // src/util/detectFfmpeg.ts
6
9
  import { execFile } from "child_process";
7
- async function detectFfmpeg() {
8
- const command = process.platform === "win32" ? "where" : "which";
10
+ function run(command, args) {
9
11
  return new Promise((resolve) => {
10
- execFile(command, ["ffmpeg"], { timeout: 5e3 }, (err, stdout) => {
12
+ execFile(command, args, { timeout: 5e3 }, (err, stdout) => {
11
13
  if (err || !stdout.trim()) {
12
14
  resolve(null);
13
15
  return;
14
16
  }
15
- const path = stdout.trim().split("\n")[0].trim();
16
- resolve(path);
17
+ resolve(stdout.trim());
17
18
  });
18
19
  });
19
20
  }
21
+ async function getFfmpegVersion(path) {
22
+ const out = await run(path, ["-version"]);
23
+ return out ? out.split("\n")[0].trim() : null;
24
+ }
25
+ async function detectFfmpegDetailed() {
26
+ const envPath = process.env.SQUISQ_FFMPEG;
27
+ if (envPath) {
28
+ const version = await getFfmpegVersion(envPath);
29
+ if (!version) {
30
+ throw new Error(
31
+ `SQUISQ_FFMPEG is set to "${envPath}" but running "${envPath} -version" failed. Fix the path or unset SQUISQ_FFMPEG.`
32
+ );
33
+ }
34
+ return { path: envPath, source: "env" };
35
+ }
36
+ const command = process.platform === "win32" ? "where" : "which";
37
+ const found = await run(command, ["ffmpeg"]);
38
+ if (found) {
39
+ return { path: found.split("\n")[0].trim(), source: "path" };
40
+ }
41
+ try {
42
+ const specifier = "ffmpeg-static";
43
+ const mod = await import(specifier);
44
+ const candidate = typeof mod === "string" ? mod : typeof mod.default === "string" ? mod.default : null;
45
+ if (candidate) {
46
+ return { path: candidate, source: "ffmpeg-static" };
47
+ }
48
+ } catch {
49
+ }
50
+ return null;
51
+ }
52
+ async function detectFfmpeg() {
53
+ const detection = await detectFfmpegDetailed();
54
+ return detection?.path ?? null;
55
+ }
56
+
57
+ // src/util/audioMix.ts
58
+ import { computeAudioTimeline } from "@bendyline/squisq-video";
59
+ async function buildMixedAudioTrack(doc, container, ffmpegPath, coverPreRoll) {
60
+ const timeline = computeAudioTimeline(doc, coverPreRoll);
61
+ if (timeline.length === 0) return null;
62
+ const bytesBySrc = /* @__PURE__ */ new Map();
63
+ const readSrc = async (src) => {
64
+ if (bytesBySrc.has(src)) return bytesBySrc.get(src);
65
+ const data = await container.readFile(src);
66
+ bytesBySrc.set(src, data ?? null);
67
+ return data ?? null;
68
+ };
69
+ const usable = [];
70
+ for (const clip of timeline) {
71
+ const buffer = await readSrc(clip.src);
72
+ if (buffer) usable.push({ clip, buffer });
73
+ }
74
+ if (usable.length === 0) return null;
75
+ return mixTimelineClips(ffmpegPath, usable);
76
+ }
77
+ async function mixTimelineClips(ffmpegPath, usable) {
78
+ const { writeFile, readFile: readFile3, mkdir, rm: rm2 } = await import("fs/promises");
79
+ const { join: join3 } = await import("path");
80
+ const { tmpdir: tmpdir2 } = await import("os");
81
+ const { randomBytes: randomBytes2 } = await import("crypto");
82
+ const { execFile: execFile2 } = await import("child_process");
83
+ const workDir = join3(tmpdir2(), `squisq-audio-mix-${randomBytes2(8).toString("hex")}`);
84
+ await mkdir(workDir, { recursive: true });
85
+ const ms = (s) => Math.max(0, Math.round(s * 1e3));
86
+ try {
87
+ const inputs = [];
88
+ const filters = [];
89
+ const labels = [];
90
+ for (const { clip, buffer } of usable) {
91
+ const p = join3(workDir, `clip-${inputs.length}.mp3`);
92
+ await writeFile(p, new Uint8Array(buffer));
93
+ const i = inputs.push(p) - 1;
94
+ const delayMs = ms(clip.startSec);
95
+ const start = Math.max(0, clip.sourceInSec);
96
+ const end = start + Math.max(0, clip.durationSec);
97
+ filters.push(
98
+ `[${i}:a]atrim=start=${start}:end=${end},asetpts=PTS-STARTPTS,adelay=${delayMs}|${delayMs}[a${i}]`
99
+ );
100
+ labels.push(`[a${i}]`);
101
+ }
102
+ const graph = `${filters.join(";")};${labels.join("")}amix=inputs=${labels.length}:normalize=0:dropout_transition=0[aout]`;
103
+ const outputPath = join3(workDir, "mixed.mp3");
104
+ const args = ["-y"];
105
+ for (const p of inputs) args.push("-i", p);
106
+ args.push(
107
+ "-filter_complex",
108
+ graph,
109
+ "-map",
110
+ "[aout]",
111
+ "-c:a",
112
+ "libmp3lame",
113
+ "-b:a",
114
+ "192k",
115
+ outputPath
116
+ );
117
+ await new Promise((resolve, reject) => {
118
+ execFile2(ffmpegPath, args, { timeout: 18e4 }, (err) => {
119
+ if (err) reject(new Error(`ffmpeg audio mix failed: ${err.message}`));
120
+ else resolve();
121
+ });
122
+ });
123
+ const data = await readFile3(outputPath);
124
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
125
+ } finally {
126
+ await rm2(workDir, { recursive: true, force: true });
127
+ }
128
+ }
129
+
130
+ // src/registry.ts
131
+ import { randomBytes } from "crypto";
132
+ import { readFile, rm } from "fs/promises";
133
+ import { tmpdir } from "os";
134
+ import { join } from "path";
135
+ import { defaultRegistry } from "@bendyline/squisq-formats";
136
+ var MP4_DEFAULTS = {
137
+ fps: 30,
138
+ quality: "normal",
139
+ orientation: "landscape",
140
+ coverPreRoll: 0
141
+ };
142
+ function mp4Format() {
143
+ return {
144
+ id: "mp4",
145
+ label: "MP4 Video",
146
+ mimeType: "video/mp4",
147
+ extensions: [".mp4"],
148
+ async exportDoc(input, options) {
149
+ const mp4Opts = options.formatOptions?.mp4 ?? {};
150
+ const fps = typeof mp4Opts.fps === "number" ? mp4Opts.fps : MP4_DEFAULTS.fps;
151
+ const quality = typeof mp4Opts.quality === "string" ? mp4Opts.quality : MP4_DEFAULTS.quality;
152
+ const orientation = typeof mp4Opts.orientation === "string" ? mp4Opts.orientation : MP4_DEFAULTS.orientation;
153
+ const coverPreRoll = typeof mp4Opts.coverPreRoll === "number" ? mp4Opts.coverPreRoll : MP4_DEFAULTS.coverPreRoll;
154
+ const outputPath = join(tmpdir(), `squisq-mp4-${randomBytes(8).toString("hex")}.mp4`);
155
+ try {
156
+ await renderDocToMp4(input.doc, input.container, {
157
+ outputPath,
158
+ fps,
159
+ quality,
160
+ orientation,
161
+ coverPreRoll
162
+ });
163
+ const data = await readFile(outputPath);
164
+ const bytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
165
+ return { bytes, mimeType: "video/mp4", suggestedFilename: "", warnings: [] };
166
+ } finally {
167
+ await rm(outputPath, { force: true });
168
+ }
169
+ }
170
+ };
171
+ }
172
+ function createCliRegistry() {
173
+ const registry = defaultRegistry();
174
+ registry.register(mp4Format());
175
+ return registry;
176
+ }
20
177
 
21
178
  // src/api.ts
22
179
  import { MemoryContentContainer as MemoryContentContainer2 } from "@bendyline/squisq/storage";
23
180
 
181
+ // src/util/domPolyfill.ts
182
+ import { DOMParser as XmldomDOMParser } from "@xmldom/xmldom";
183
+ var globalScope = globalThis;
184
+ if (typeof globalScope.DOMParser === "undefined") {
185
+ globalScope.DOMParser = XmldomDOMParser;
186
+ }
187
+
24
188
  // src/util/readInput.ts
25
- import { readFile, readdir, stat } from "fs/promises";
26
- import { join, extname } from "path";
27
- import { parseMarkdown } from "@bendyline/squisq/markdown";
189
+ import { readFile as readFile2, readdir, stat } from "fs/promises";
190
+ import { join as join2, extname } from "path";
191
+ import { parseMarkdown, stringifyMarkdown } from "@bendyline/squisq/markdown";
192
+ import { markdownToDoc } from "@bendyline/squisq/doc";
28
193
  import { MemoryContentContainer } from "@bendyline/squisq/storage";
29
194
  import { zipToContainer } from "@bendyline/squisq-formats/container";
195
+ import { defaultRegistry as defaultRegistry2 } from "@bendyline/squisq-formats";
30
196
  var MIME_TYPES = {
31
197
  ".md": "text/markdown",
32
198
  ".txt": "text/plain",
@@ -43,6 +209,7 @@ var MIME_TYPES = {
43
209
  ".mp4": "video/mp4",
44
210
  ".webm": "video/webm"
45
211
  };
212
+ var IMPORTER_EXTS = [".docx", ".pptx", ".pdf", ".xlsx", ".csv", ".html", ".htm"];
46
213
  function mimeFromExt(filePath) {
47
214
  return MIME_TYPES[extname(filePath).toLowerCase()] ?? "application/octet-stream";
48
215
  }
@@ -52,7 +219,7 @@ async function walkDir(root, prefix = "") {
52
219
  for (const entry of entries) {
53
220
  const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
54
221
  if (entry.isDirectory()) {
55
- paths.push(...await walkDir(join(root, entry.name), relPath));
222
+ paths.push(...await walkDir(join2(root, entry.name), relPath));
56
223
  } else if (entry.isFile()) {
57
224
  paths.push(relPath);
58
225
  }
@@ -71,71 +238,99 @@ async function readInput(inputPath) {
71
238
  if (ext === ".json") {
72
239
  return readDocJsonFile(inputPath);
73
240
  }
241
+ if (IMPORTER_EXTS.includes(ext)) {
242
+ const def = defaultRegistry2().byExtension(ext);
243
+ if (def && (def.importContainer || def.importDoc)) {
244
+ return readViaImporter(inputPath, def);
245
+ }
246
+ }
74
247
  return readMarkdownFile(inputPath);
75
248
  }
249
+ async function readArrayBuffer(filePath) {
250
+ const data = await readFile2(filePath);
251
+ return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
252
+ }
76
253
  async function readMarkdownFile(filePath) {
77
- const content = await readFile(filePath, "utf-8");
254
+ const content = await readFile2(filePath, "utf-8");
78
255
  const container = new MemoryContentContainer();
79
256
  await container.writeDocument(content);
80
257
  const markdownDoc = parseMarkdown(content);
81
- return { container, markdownDoc };
258
+ return { doc: markdownToDoc(markdownDoc), container, markdownDoc, sourceFormat: "md" };
82
259
  }
83
260
  async function readDocJsonFile(filePath) {
84
- const content = await readFile(filePath, "utf-8");
261
+ const content = await readFile2(filePath, "utf-8");
85
262
  const doc = JSON.parse(content);
86
263
  const container = new MemoryContentContainer();
87
- return { container, markdownDoc: null, doc };
264
+ return { doc, container, sourceFormat: "json" };
265
+ }
266
+ async function readViaImporter(filePath, def) {
267
+ const buffer = await readArrayBuffer(filePath);
268
+ let container;
269
+ let markdownDoc;
270
+ if (def.importContainer) {
271
+ container = await def.importContainer(buffer, {});
272
+ const text = await container.readDocument();
273
+ markdownDoc = text ? parseMarkdown(text) : { type: "document", children: [] };
274
+ } else {
275
+ markdownDoc = await def.importDoc(buffer, {});
276
+ const mem = new MemoryContentContainer();
277
+ await mem.writeDocument(stringifyMarkdown(markdownDoc));
278
+ container = mem;
279
+ }
280
+ return { doc: markdownToDoc(markdownDoc), container, markdownDoc, sourceFormat: def.id };
88
281
  }
89
282
  var DOC_JSON_NAMES = ["doc.json", "story.json"];
90
- async function readContainer(filePath) {
91
- const data = await readFile(filePath);
92
- const container = await zipToContainer(
93
- data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength)
94
- );
283
+ async function resolveContainer(container, sourceFormat, missingMessage) {
95
284
  for (const name of DOC_JSON_NAMES) {
96
285
  const jsonData = await container.readFile(name);
97
286
  if (jsonData) {
98
- const decoder = new TextDecoder();
99
- const doc = JSON.parse(decoder.decode(jsonData));
100
- return { container, markdownDoc: null, doc };
287
+ const doc = JSON.parse(new TextDecoder().decode(jsonData));
288
+ return { doc, container, sourceFormat };
101
289
  }
102
290
  }
103
291
  const markdown = await container.readDocument();
104
292
  if (!markdown) {
105
- throw new Error(`No markdown document or doc.json found in container: ${filePath}`);
293
+ throw new Error(missingMessage);
106
294
  }
107
295
  const markdownDoc = parseMarkdown(markdown);
108
- return { container, markdownDoc };
296
+ return { doc: markdownToDoc(markdownDoc), container, markdownDoc, sourceFormat };
297
+ }
298
+ async function readContainer(filePath) {
299
+ const container = await zipToContainer(await readArrayBuffer(filePath));
300
+ return resolveContainer(
301
+ container,
302
+ "dbk",
303
+ `No markdown document or doc.json found in container: ${filePath}`
304
+ );
109
305
  }
110
306
  async function readFolder(dirPath) {
111
307
  const container = new MemoryContentContainer();
112
308
  const files = await walkDir(dirPath);
113
309
  for (const relPath of files) {
114
- const absPath = join(dirPath, relPath);
115
- const data = await readFile(absPath);
310
+ const absPath = join2(dirPath, relPath);
311
+ const data = await readFile2(absPath);
116
312
  await container.writeFile(
117
313
  relPath,
118
314
  new Uint8Array(data.buffer, data.byteOffset, data.byteLength),
119
315
  mimeFromExt(relPath)
120
316
  );
121
317
  }
122
- for (const name of DOC_JSON_NAMES) {
123
- const jsonData = await container.readFile(name);
124
- if (jsonData) {
125
- const decoder = new TextDecoder();
126
- const doc = JSON.parse(decoder.decode(jsonData));
127
- return { container, markdownDoc: null, doc };
128
- }
129
- }
130
- const markdown = await container.readDocument();
131
- if (!markdown) {
132
- throw new Error(`No markdown document or doc.json found in folder: ${dirPath}`);
133
- }
134
- const markdownDoc = parseMarkdown(markdown);
135
- return { container, markdownDoc };
318
+ return resolveContainer(
319
+ container,
320
+ "folder",
321
+ `No markdown document or doc.json found in folder: ${dirPath}`
322
+ );
136
323
  }
137
324
 
138
325
  // src/api.ts
326
+ import { ConversionError } from "@bendyline/squisq-formats";
327
+ async function convert(source, to, options = {}) {
328
+ return formatsConvert(source, to, {
329
+ registry: createCliRegistry(),
330
+ resolvePlayerScript: () => import("@bendyline/squisq-react/standalone-source").then((m) => m.PLAYER_BUNDLE),
331
+ ...options
332
+ });
333
+ }
139
334
  async function renderDocToMp4(doc, container, options) {
140
335
  const {
141
336
  outputPath,
@@ -154,7 +349,7 @@ async function renderDocToMp4(doc, container, options) {
154
349
  const ffmpegPath = await detectFfmpeg();
155
350
  if (!ffmpegPath) {
156
351
  throw new Error(
157
- "ffmpeg is required but not found in PATH.\nInstall it with:\n macOS: brew install ffmpeg\n Ubuntu: sudo apt install ffmpeg\n Windows: winget install ffmpeg"
352
+ "ffmpeg is required but not found in PATH.\nInstall it with:\n macOS: brew install ffmpeg\n Ubuntu: sudo apt install ffmpeg\n Windows: winget install ffmpeg\nOr: npm install ffmpeg-static, or set SQUISQ_FFMPEG to an ffmpeg binary."
158
353
  );
159
354
  }
160
355
  onProgress?.("collecting media", 0);
@@ -168,20 +363,25 @@ async function renderDocToMp4(doc, container, options) {
168
363
  }
169
364
  }
170
365
  const audio = /* @__PURE__ */ new Map();
171
- const audioBuffers = [];
172
366
  if (doc.audio?.segments?.length) {
173
367
  for (const seg of doc.audio.segments) {
174
368
  const data = await container.readFile(seg.src);
175
369
  if (data) {
176
370
  audio.set(seg.src, data);
177
371
  audio.set(seg.name, data);
178
- audioBuffers.push(data);
179
372
  }
180
373
  }
181
374
  }
182
- let concatenatedAudio = null;
183
- if (audioBuffers.length > 0) {
184
- concatenatedAudio = await concatenateAudioBuffers(audioBuffers, ffmpegPath);
375
+ const mediaSrcs = new Set(resolveMediaSchedule(doc).map((c) => c.src));
376
+ for (const block of flattenBlocks(doc.blocks)) {
377
+ for (const layer of block.layers ?? []) {
378
+ if (layer.type === "video") mediaSrcs.add(layer.content.src);
379
+ }
380
+ }
381
+ for (const src of mediaSrcs) {
382
+ if (images.has(src)) continue;
383
+ const data = await container.readFile(src);
384
+ if (data) images.set(src, data);
185
385
  }
186
386
  onProgress?.("generating render HTML", 10);
187
387
  const { PLAYER_BUNDLE } = await import("@bendyline/squisq-react/standalone-source");
@@ -195,7 +395,16 @@ async function renderDocToMp4(doc, container, options) {
195
395
  });
196
396
  onProgress?.("launching browser", 15);
197
397
  const { chromium } = await import("playwright-core");
198
- const browser = await chromium.launch({ headless: true });
398
+ let browser;
399
+ try {
400
+ browser = await chromium.launch({ headless: true });
401
+ } catch (err) {
402
+ const detail = err instanceof Error ? err.message.split("\n")[0] : String(err);
403
+ throw new Error(
404
+ `Playwright Chromium is not installed. Run: npx playwright install chromium
405
+ (launch failed: ${detail})`
406
+ );
407
+ }
199
408
  const page = await browser.newPage({
200
409
  viewport: { width: dimensions.width, height: dimensions.height }
201
410
  });
@@ -213,7 +422,9 @@ async function renderDocToMp4(doc, container, options) {
213
422
  const errorDetail = pageErrors.length ? `
214
423
  Page errors:
215
424
  ${pageErrors.join("\n ")}` : "\nNo page errors captured \u2014 the player may have failed to mount.";
216
- throw new Error(`Render API did not initialize within 15 seconds.${errorDetail}`);
425
+ throw new Error(
426
+ `The standalone player failed to boot in headless Chromium. Render API did not initialize within 15 seconds.${errorDetail}`
427
+ );
217
428
  }
218
429
  const docDuration = await page.evaluate(() => {
219
430
  return window.getDuration();
@@ -261,11 +472,8 @@ Page errors:
261
472
  }
262
473
  await browser.close();
263
474
  onProgress?.("encoding video", 80);
264
- let encodingAudio = concatenatedAudio;
265
- if (coverPreRoll > 0 && concatenatedAudio) {
266
- encodingAudio = await addAudioDelay(ffmpegPath, concatenatedAudio, coverPreRoll);
267
- }
268
- const { framesToMp4Native } = await import("./nativeEncoder-MWHOONST.js");
475
+ const encodingAudio = await buildMixedAudioTrack(doc, container, ffmpegPath, coverPreRoll);
476
+ const { framesToMp4Native } = await import("./nativeEncoder-2HGWDEZZ.js");
269
477
  await framesToMp4Native(ffmpegPath, frames, encodingAudio, outputPath, {
270
478
  fps,
271
479
  quality,
@@ -284,103 +492,10 @@ Page errors:
284
492
  outputPath
285
493
  };
286
494
  }
287
- async function concatenateAudioBuffers(buffers, ffmpegPath) {
288
- if (buffers.length === 0) return new Uint8Array(0);
289
- if (buffers.length === 1) return new Uint8Array(buffers[0]);
290
- if (ffmpegPath) {
291
- return concatenateAudioNative(ffmpegPath, buffers);
292
- }
293
- const totalLength = buffers.reduce((sum, b) => sum + b.byteLength, 0);
294
- const result = new Uint8Array(totalLength);
295
- let offset = 0;
296
- for (const buf of buffers) {
297
- result.set(new Uint8Array(buf), offset);
298
- offset += buf.byteLength;
299
- }
300
- return result;
301
- }
302
- async function concatenateAudioNative(ffmpegPath, buffers) {
303
- const { writeFile, readFile: readFile2, mkdir, rm } = await import("fs/promises");
304
- const { join: join2 } = await import("path");
305
- const { tmpdir } = await import("os");
306
- const { randomBytes } = await import("crypto");
307
- const { execFile: execFile2 } = await import("child_process");
308
- const tmpId = randomBytes(8).toString("hex");
309
- const workDir = join2(tmpdir(), `squisq-audio-concat-${tmpId}`);
310
- await mkdir(workDir, { recursive: true });
311
- try {
312
- const segmentPaths = [];
313
- for (let i = 0; i < buffers.length; i++) {
314
- const segPath = join2(workDir, `seg-${i}.mp3`);
315
- await writeFile(segPath, new Uint8Array(buffers[i]));
316
- segmentPaths.push(segPath);
317
- }
318
- const listPath = join2(workDir, "concat-list.txt");
319
- const listContent = segmentPaths.map((p) => `file '${p.replace(/\\/g, "/")}'`).join("\n");
320
- await writeFile(listPath, listContent);
321
- const outputPath = join2(workDir, "combined.mp3");
322
- await new Promise((resolve, reject) => {
323
- execFile2(
324
- ffmpegPath,
325
- ["-y", "-f", "concat", "-safe", "0", "-i", listPath, "-c", "copy", outputPath],
326
- { timeout: 12e4 },
327
- (err) => {
328
- if (err) reject(new Error(`ffmpeg audio concat failed: ${err.message}`));
329
- else resolve();
330
- }
331
- );
332
- });
333
- const data = await readFile2(outputPath);
334
- return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
335
- } finally {
336
- await rm(workDir, { recursive: true, force: true });
337
- }
338
- }
339
- async function addAudioDelay(ffmpegPath, audioData, delaySecs) {
340
- const { writeFile, readFile: readFile2, rm } = await import("fs/promises");
341
- const { join: join2 } = await import("path");
342
- const { tmpdir } = await import("os");
343
- const { randomBytes } = await import("crypto");
344
- const { execFile: execFile2 } = await import("child_process");
345
- const tmpId = randomBytes(8).toString("hex");
346
- const inputPath = join2(tmpdir(), `squisq-audio-delay-in-${tmpId}.mp3`);
347
- const outputPath = join2(tmpdir(), `squisq-audio-delay-out-${tmpId}.mp3`);
348
- try {
349
- await writeFile(inputPath, audioData);
350
- const delayMs = Math.round(delaySecs * 1e3);
351
- await new Promise((resolve, reject) => {
352
- execFile2(
353
- ffmpegPath,
354
- [
355
- "-y",
356
- "-i",
357
- inputPath,
358
- "-af",
359
- `adelay=${delayMs}|${delayMs}`,
360
- "-c:a",
361
- "libmp3lame",
362
- "-b:a",
363
- "128k",
364
- outputPath
365
- ],
366
- { timeout: 6e4 },
367
- (err) => {
368
- if (err) reject(new Error(`ffmpeg audio delay failed: ${err.message}`));
369
- else resolve();
370
- }
371
- );
372
- });
373
- const data = await readFile2(outputPath);
374
- return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
375
- } finally {
376
- await rm(inputPath, { force: true });
377
- await rm(outputPath, { force: true });
378
- }
379
- }
380
495
  async function extractThumbnails(options) {
381
496
  const { videoPath, outputDir, slug, sizes, force } = options;
382
497
  const { existsSync } = await import("fs");
383
- const { join: join2 } = await import("path");
498
+ const { join: join3 } = await import("path");
384
499
  const { execFile: execFile2 } = await import("child_process");
385
500
  const ffmpegPath = await detectFfmpeg();
386
501
  if (!ffmpegPath) {
@@ -389,7 +504,7 @@ async function extractThumbnails(options) {
389
504
  );
390
505
  }
391
506
  for (const thumb of sizes) {
392
- const outputPath = join2(outputDir, `${slug}-${thumb.width}x${thumb.height}.jpg`);
507
+ const outputPath = join3(outputDir, `${slug}-${thumb.width}x${thumb.height}.jpg`);
393
508
  if (!force && existsSync(outputPath)) continue;
394
509
  await new Promise((resolve, reject) => {
395
510
  execFile2(
@@ -405,7 +520,10 @@ async function extractThumbnails(options) {
405
520
  }
406
521
  }
407
522
  export {
523
+ ConversionError,
408
524
  MemoryContentContainer2 as MemoryContentContainer,
525
+ convert,
526
+ createCliRegistry,
409
527
  extractThumbnails,
410
528
  readInput,
411
529
  renderDocToMp4