@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/README.md +131 -49
- package/dist/api.d.ts +72 -22
- package/dist/api.js +265 -147
- package/dist/api.js.map +1 -1
- package/dist/index.js +580 -367
- package/dist/index.js.map +1 -1
- package/dist/{nativeEncoder-MWHOONST.js → nativeEncoder-2HGWDEZZ.js} +9 -11
- package/dist/nativeEncoder-2HGWDEZZ.js.map +1 -0
- package/dist/{nativeEncoder-EXDP2O5B.js → nativeEncoder-DLLAT2RT.js} +9 -11
- package/dist/nativeEncoder-DLLAT2RT.js.map +1 -0
- package/package.json +9 -7
- package/dist/nativeEncoder-EXDP2O5B.js.map +0 -1
- package/dist/nativeEncoder-MWHOONST.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,18 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
+
import { createRequire } from "module";
|
|
4
5
|
import { Command } from "commander";
|
|
5
6
|
|
|
6
7
|
// src/commands/convert.ts
|
|
7
8
|
import { writeFile, mkdir } from "fs/promises";
|
|
8
|
-
import { dirname, basename, extname as extname2, join as
|
|
9
|
+
import { dirname, basename, extname as extname2, join as join3, resolve } from "path";
|
|
10
|
+
import { BUILTIN_FORMAT_IDS, ConversionError as ConversionError2 } from "@bendyline/squisq-formats";
|
|
11
|
+
|
|
12
|
+
// src/util/domPolyfill.ts
|
|
13
|
+
import { DOMParser as XmldomDOMParser } from "@xmldom/xmldom";
|
|
14
|
+
var globalScope = globalThis;
|
|
15
|
+
if (typeof globalScope.DOMParser === "undefined") {
|
|
16
|
+
globalScope.DOMParser = XmldomDOMParser;
|
|
17
|
+
}
|
|
9
18
|
|
|
10
19
|
// src/util/readInput.ts
|
|
11
20
|
import { readFile, readdir, stat } from "fs/promises";
|
|
12
21
|
import { join, extname } from "path";
|
|
13
|
-
import { parseMarkdown } from "@bendyline/squisq/markdown";
|
|
22
|
+
import { parseMarkdown, stringifyMarkdown } from "@bendyline/squisq/markdown";
|
|
23
|
+
import { markdownToDoc } from "@bendyline/squisq/doc";
|
|
14
24
|
import { MemoryContentContainer } from "@bendyline/squisq/storage";
|
|
15
25
|
import { zipToContainer } from "@bendyline/squisq-formats/container";
|
|
26
|
+
import { defaultRegistry } from "@bendyline/squisq-formats";
|
|
16
27
|
var MIME_TYPES = {
|
|
17
28
|
".md": "text/markdown",
|
|
18
29
|
".txt": "text/plain",
|
|
@@ -29,6 +40,7 @@ var MIME_TYPES = {
|
|
|
29
40
|
".mp4": "video/mp4",
|
|
30
41
|
".webm": "video/webm"
|
|
31
42
|
};
|
|
43
|
+
var IMPORTER_EXTS = [".docx", ".pptx", ".pdf", ".xlsx", ".csv", ".html", ".htm"];
|
|
32
44
|
function mimeFromExt(filePath) {
|
|
33
45
|
return MIME_TYPES[extname(filePath).toLowerCase()] ?? "application/octet-stream";
|
|
34
46
|
}
|
|
@@ -57,41 +69,70 @@ async function readInput(inputPath) {
|
|
|
57
69
|
if (ext === ".json") {
|
|
58
70
|
return readDocJsonFile(inputPath);
|
|
59
71
|
}
|
|
72
|
+
if (IMPORTER_EXTS.includes(ext)) {
|
|
73
|
+
const def = defaultRegistry().byExtension(ext);
|
|
74
|
+
if (def && (def.importContainer || def.importDoc)) {
|
|
75
|
+
return readViaImporter(inputPath, def);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
60
78
|
return readMarkdownFile(inputPath);
|
|
61
79
|
}
|
|
80
|
+
async function readArrayBuffer(filePath) {
|
|
81
|
+
const data = await readFile(filePath);
|
|
82
|
+
return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
83
|
+
}
|
|
62
84
|
async function readMarkdownFile(filePath) {
|
|
63
85
|
const content = await readFile(filePath, "utf-8");
|
|
64
86
|
const container = new MemoryContentContainer();
|
|
65
87
|
await container.writeDocument(content);
|
|
66
88
|
const markdownDoc = parseMarkdown(content);
|
|
67
|
-
return { container, markdownDoc };
|
|
89
|
+
return { doc: markdownToDoc(markdownDoc), container, markdownDoc, sourceFormat: "md" };
|
|
68
90
|
}
|
|
69
91
|
async function readDocJsonFile(filePath) {
|
|
70
92
|
const content = await readFile(filePath, "utf-8");
|
|
71
93
|
const doc = JSON.parse(content);
|
|
72
94
|
const container = new MemoryContentContainer();
|
|
73
|
-
return { container,
|
|
95
|
+
return { doc, container, sourceFormat: "json" };
|
|
96
|
+
}
|
|
97
|
+
async function readViaImporter(filePath, def) {
|
|
98
|
+
const buffer = await readArrayBuffer(filePath);
|
|
99
|
+
let container;
|
|
100
|
+
let markdownDoc;
|
|
101
|
+
if (def.importContainer) {
|
|
102
|
+
container = await def.importContainer(buffer, {});
|
|
103
|
+
const text = await container.readDocument();
|
|
104
|
+
markdownDoc = text ? parseMarkdown(text) : { type: "document", children: [] };
|
|
105
|
+
} else {
|
|
106
|
+
markdownDoc = await def.importDoc(buffer, {});
|
|
107
|
+
const mem = new MemoryContentContainer();
|
|
108
|
+
await mem.writeDocument(stringifyMarkdown(markdownDoc));
|
|
109
|
+
container = mem;
|
|
110
|
+
}
|
|
111
|
+
return { doc: markdownToDoc(markdownDoc), container, markdownDoc, sourceFormat: def.id };
|
|
74
112
|
}
|
|
75
113
|
var DOC_JSON_NAMES = ["doc.json", "story.json"];
|
|
76
|
-
async function
|
|
77
|
-
const data = await readFile(filePath);
|
|
78
|
-
const container = await zipToContainer(
|
|
79
|
-
data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength)
|
|
80
|
-
);
|
|
114
|
+
async function resolveContainer(container, sourceFormat, missingMessage) {
|
|
81
115
|
for (const name of DOC_JSON_NAMES) {
|
|
82
116
|
const jsonData = await container.readFile(name);
|
|
83
117
|
if (jsonData) {
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
return { container, markdownDoc: null, doc };
|
|
118
|
+
const doc = JSON.parse(new TextDecoder().decode(jsonData));
|
|
119
|
+
return { doc, container, sourceFormat };
|
|
87
120
|
}
|
|
88
121
|
}
|
|
89
122
|
const markdown = await container.readDocument();
|
|
90
123
|
if (!markdown) {
|
|
91
|
-
throw new Error(
|
|
124
|
+
throw new Error(missingMessage);
|
|
92
125
|
}
|
|
93
126
|
const markdownDoc = parseMarkdown(markdown);
|
|
94
|
-
return { container, markdownDoc };
|
|
127
|
+
return { doc: markdownToDoc(markdownDoc), container, markdownDoc, sourceFormat };
|
|
128
|
+
}
|
|
129
|
+
async function readContainer(filePath) {
|
|
130
|
+
const container = await zipToContainer(await readArrayBuffer(filePath));
|
|
131
|
+
return resolveContainer(
|
|
132
|
+
container,
|
|
133
|
+
"dbk",
|
|
134
|
+
`No markdown document or doc.json found in container: ${filePath}`
|
|
135
|
+
);
|
|
95
136
|
}
|
|
96
137
|
async function readFolder(dirPath) {
|
|
97
138
|
const container = new MemoryContentContainer();
|
|
@@ -105,270 +146,159 @@ async function readFolder(dirPath) {
|
|
|
105
146
|
mimeFromExt(relPath)
|
|
106
147
|
);
|
|
107
148
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
return { container, markdownDoc: null, doc };
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
const markdown = await container.readDocument();
|
|
117
|
-
if (!markdown) {
|
|
118
|
-
throw new Error(`No markdown document or doc.json found in folder: ${dirPath}`);
|
|
119
|
-
}
|
|
120
|
-
const markdownDoc = parseMarkdown(markdown);
|
|
121
|
-
return { container, markdownDoc };
|
|
149
|
+
return resolveContainer(
|
|
150
|
+
container,
|
|
151
|
+
"folder",
|
|
152
|
+
`No markdown document or doc.json found in folder: ${dirPath}`
|
|
153
|
+
);
|
|
122
154
|
}
|
|
123
155
|
|
|
124
|
-
// src/
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
try {
|
|
150
|
-
await runConvert(inputPath, opts);
|
|
151
|
-
} catch (err) {
|
|
152
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
153
|
-
console.error(`Error: ${message}`);
|
|
154
|
-
process.exitCode = 1;
|
|
155
|
-
}
|
|
156
|
+
// src/registry.ts
|
|
157
|
+
import { randomBytes } from "crypto";
|
|
158
|
+
import { readFile as readFile2, rm } from "fs/promises";
|
|
159
|
+
import { tmpdir } from "os";
|
|
160
|
+
import { join as join2 } from "path";
|
|
161
|
+
import { defaultRegistry as defaultRegistry2 } from "@bendyline/squisq-formats";
|
|
162
|
+
|
|
163
|
+
// src/api.ts
|
|
164
|
+
import { resolveMediaSchedule } from "@bendyline/squisq/schemas";
|
|
165
|
+
import { flattenBlocks } from "@bendyline/squisq/doc";
|
|
166
|
+
import { generateRenderHtml } from "@bendyline/squisq-video";
|
|
167
|
+
import { resolveDimensions } from "@bendyline/squisq-video";
|
|
168
|
+
import { convert as formatsConvert } from "@bendyline/squisq-formats";
|
|
169
|
+
|
|
170
|
+
// src/util/detectFfmpeg.ts
|
|
171
|
+
import { execFile } from "child_process";
|
|
172
|
+
function run(command, args) {
|
|
173
|
+
return new Promise((resolve4) => {
|
|
174
|
+
execFile(command, args, { timeout: 5e3 }, (err, stdout) => {
|
|
175
|
+
if (err || !stdout.trim()) {
|
|
176
|
+
resolve4(null);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
resolve4(stdout.trim());
|
|
180
|
+
});
|
|
156
181
|
});
|
|
157
182
|
}
|
|
158
|
-
async function
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const { getAvailableThemes } = await import("@bendyline/squisq/schemas");
|
|
168
|
-
const themes = getAvailableThemes();
|
|
169
|
-
if (!themes.includes(opts.theme)) {
|
|
170
|
-
throw new Error(`Unknown theme "${opts.theme}". Available: ${themes.join(", ")}`);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
if (opts.transform) {
|
|
174
|
-
const { getTransformStyleIds } = await import("@bendyline/squisq/transform");
|
|
175
|
-
const styles = getTransformStyleIds();
|
|
176
|
-
if (!styles.includes(opts.transform)) {
|
|
183
|
+
async function getFfmpegVersion(path) {
|
|
184
|
+
const out = await run(path, ["-version"]);
|
|
185
|
+
return out ? out.split("\n")[0].trim() : null;
|
|
186
|
+
}
|
|
187
|
+
async function detectFfmpegDetailed() {
|
|
188
|
+
const envPath = process.env.SQUISQ_FFMPEG;
|
|
189
|
+
if (envPath) {
|
|
190
|
+
const version2 = await getFfmpegVersion(envPath);
|
|
191
|
+
if (!version2) {
|
|
177
192
|
throw new Error(
|
|
178
|
-
`
|
|
193
|
+
`SQUISQ_FFMPEG is set to "${envPath}" but running "${envPath} -version" failed. Fix the path or unset SQUISQ_FFMPEG.`
|
|
179
194
|
);
|
|
180
195
|
}
|
|
196
|
+
return { path: envPath, source: "env" };
|
|
181
197
|
}
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
throw new Error(
|
|
187
|
-
"Convert command requires a markdown document. JSON Doc input is not supported for convert \u2014 use the video command instead."
|
|
188
|
-
);
|
|
189
|
-
}
|
|
190
|
-
let exportMarkdownDoc = result.markdownDoc;
|
|
191
|
-
if (opts.transform) {
|
|
192
|
-
exportMarkdownDoc = await applyTransformToMarkdown(
|
|
193
|
-
result.markdownDoc,
|
|
194
|
-
container,
|
|
195
|
-
opts.transform,
|
|
196
|
-
opts.theme
|
|
197
|
-
);
|
|
198
|
-
console.error(` Applied transform: ${opts.transform}`);
|
|
198
|
+
const command = process.platform === "win32" ? "where" : "which";
|
|
199
|
+
const found = await run(command, ["ffmpeg"]);
|
|
200
|
+
if (found) {
|
|
201
|
+
return { path: found.split("\n")[0].trim(), source: "path" };
|
|
199
202
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const buf = await markdownDocToDocx(exportMarkdownDoc, { themeId });
|
|
207
|
-
await writeFile(outPath, Buffer.from(buf));
|
|
208
|
-
break;
|
|
209
|
-
}
|
|
210
|
-
case "pptx": {
|
|
211
|
-
const { markdownDocToPptx } = await import("@bendyline/squisq-formats/pptx");
|
|
212
|
-
const images = await collectContainerImages(container);
|
|
213
|
-
const buf = await markdownDocToPptx(exportMarkdownDoc, { themeId, images });
|
|
214
|
-
await writeFile(outPath, Buffer.from(buf));
|
|
215
|
-
break;
|
|
216
|
-
}
|
|
217
|
-
case "pdf": {
|
|
218
|
-
const { markdownDocToPdf } = await import("@bendyline/squisq-formats/pdf");
|
|
219
|
-
const buf = await markdownDocToPdf(exportMarkdownDoc, { themeId });
|
|
220
|
-
await writeFile(outPath, Buffer.from(buf));
|
|
221
|
-
break;
|
|
222
|
-
}
|
|
223
|
-
case "html": {
|
|
224
|
-
const { markdownToDoc } = await import("@bendyline/squisq/doc");
|
|
225
|
-
const { docToHtml } = await import("@bendyline/squisq-formats/html");
|
|
226
|
-
const { PLAYER_BUNDLE } = await import("@bendyline/squisq-react/standalone-source");
|
|
227
|
-
const doc = markdownToDoc(exportMarkdownDoc);
|
|
228
|
-
const images = await collectImagesForHtml(doc, container);
|
|
229
|
-
const html = docToHtml(doc, {
|
|
230
|
-
playerScript: PLAYER_BUNDLE,
|
|
231
|
-
images,
|
|
232
|
-
title: baseName,
|
|
233
|
-
mode: "static",
|
|
234
|
-
themeId
|
|
235
|
-
});
|
|
236
|
-
await writeFile(outPath, html, "utf-8");
|
|
237
|
-
break;
|
|
238
|
-
}
|
|
239
|
-
case "htmlzip": {
|
|
240
|
-
const { markdownToDoc } = await import("@bendyline/squisq/doc");
|
|
241
|
-
const { docToHtmlZip } = await import("@bendyline/squisq-formats/html");
|
|
242
|
-
const { PLAYER_BUNDLE } = await import("@bendyline/squisq-react/standalone-source");
|
|
243
|
-
const doc = markdownToDoc(exportMarkdownDoc);
|
|
244
|
-
const images = await collectImagesForHtml(doc, container);
|
|
245
|
-
const blob = await docToHtmlZip(doc, {
|
|
246
|
-
playerScript: PLAYER_BUNDLE,
|
|
247
|
-
images,
|
|
248
|
-
title: baseName,
|
|
249
|
-
mode: "static",
|
|
250
|
-
themeId
|
|
251
|
-
});
|
|
252
|
-
const buf = Buffer.from(await blob.arrayBuffer());
|
|
253
|
-
await writeFile(outPath.replace(/\.htmlzip$/, ".html.zip"), buf);
|
|
254
|
-
break;
|
|
255
|
-
}
|
|
256
|
-
case "epub": {
|
|
257
|
-
const { markdownDocToEpub } = await import("@bendyline/squisq-formats/epub");
|
|
258
|
-
const images = await collectContainerImages(container);
|
|
259
|
-
const epubAudio = /* @__PURE__ */ new Map();
|
|
260
|
-
const audioSegments = result.doc?.audio?.segments;
|
|
261
|
-
if (audioSegments?.length) {
|
|
262
|
-
for (const seg of audioSegments) {
|
|
263
|
-
const data = await container.readFile(seg.src);
|
|
264
|
-
if (data) epubAudio.set(seg.src, data);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
const hasNarration = epubAudio.size > 0;
|
|
268
|
-
const buf = await markdownDocToEpub(exportMarkdownDoc, {
|
|
269
|
-
title: baseName,
|
|
270
|
-
themeId,
|
|
271
|
-
images,
|
|
272
|
-
audio: hasNarration ? epubAudio : void 0,
|
|
273
|
-
audioSegments: hasNarration ? audioSegments : void 0,
|
|
274
|
-
totalDuration: hasNarration ? result.doc?.duration : void 0
|
|
275
|
-
});
|
|
276
|
-
await writeFile(outPath, Buffer.from(buf));
|
|
277
|
-
break;
|
|
278
|
-
}
|
|
279
|
-
case "dbk": {
|
|
280
|
-
const { containerToZip } = await import("@bendyline/squisq-formats/container");
|
|
281
|
-
const blob = await containerToZip(container);
|
|
282
|
-
const buf = await blob.arrayBuffer();
|
|
283
|
-
await writeFile(outPath, Buffer.from(buf));
|
|
284
|
-
break;
|
|
285
|
-
}
|
|
203
|
+
try {
|
|
204
|
+
const specifier = "ffmpeg-static";
|
|
205
|
+
const mod = await import(specifier);
|
|
206
|
+
const candidate = typeof mod === "string" ? mod : typeof mod.default === "string" ? mod.default : null;
|
|
207
|
+
if (candidate) {
|
|
208
|
+
return { path: candidate, source: "ffmpeg-static" };
|
|
286
209
|
}
|
|
287
|
-
|
|
210
|
+
} catch {
|
|
288
211
|
}
|
|
289
|
-
|
|
212
|
+
return null;
|
|
290
213
|
}
|
|
291
|
-
async function
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
const doc = markdownToDoc(markdownDoc);
|
|
295
|
-
const images = extractDocImages(doc.blocks);
|
|
296
|
-
const result = applyTransform(doc, transformStyle, {
|
|
297
|
-
themeId,
|
|
298
|
-
images
|
|
299
|
-
});
|
|
300
|
-
return docToMarkdown(result.doc);
|
|
214
|
+
async function detectFfmpeg() {
|
|
215
|
+
const detection = await detectFfmpegDetailed();
|
|
216
|
+
return detection?.path ?? null;
|
|
301
217
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
for (const file of files) {
|
|
320
|
-
const filename = extractFilename(file.path);
|
|
321
|
-
if (needByFilename.has(filename)) {
|
|
322
|
-
const data = await container.readFile(file.path);
|
|
323
|
-
if (data) {
|
|
324
|
-
images.set(filename, data);
|
|
325
|
-
images.set(file.path, data);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
}
|
|
218
|
+
|
|
219
|
+
// src/util/audioMix.ts
|
|
220
|
+
import { computeAudioTimeline } from "@bendyline/squisq-video";
|
|
221
|
+
async function buildMixedAudioTrack(doc, container, ffmpegPath, coverPreRoll) {
|
|
222
|
+
const timeline = computeAudioTimeline(doc, coverPreRoll);
|
|
223
|
+
if (timeline.length === 0) return null;
|
|
224
|
+
const bytesBySrc = /* @__PURE__ */ new Map();
|
|
225
|
+
const readSrc = async (src) => {
|
|
226
|
+
if (bytesBySrc.has(src)) return bytesBySrc.get(src);
|
|
227
|
+
const data = await container.readFile(src);
|
|
228
|
+
bytesBySrc.set(src, data ?? null);
|
|
229
|
+
return data ?? null;
|
|
230
|
+
};
|
|
231
|
+
const usable = [];
|
|
232
|
+
for (const clip of timeline) {
|
|
233
|
+
const buffer = await readSrc(clip.src);
|
|
234
|
+
if (buffer) usable.push({ clip, buffer });
|
|
329
235
|
}
|
|
330
|
-
return
|
|
236
|
+
if (usable.length === 0) return null;
|
|
237
|
+
return mixTimelineClips(ffmpegPath, usable);
|
|
331
238
|
}
|
|
332
|
-
async function
|
|
333
|
-
const
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
239
|
+
async function mixTimelineClips(ffmpegPath, usable) {
|
|
240
|
+
const { writeFile: writeFile2, readFile: readFile3, mkdir: mkdir3, rm: rm2 } = await import("fs/promises");
|
|
241
|
+
const { join: join5 } = await import("path");
|
|
242
|
+
const { tmpdir: tmpdir2 } = await import("os");
|
|
243
|
+
const { randomBytes: randomBytes2 } = await import("crypto");
|
|
244
|
+
const { execFile: execFile2 } = await import("child_process");
|
|
245
|
+
const workDir = join5(tmpdir2(), `squisq-audio-mix-${randomBytes2(8).toString("hex")}`);
|
|
246
|
+
await mkdir3(workDir, { recursive: true });
|
|
247
|
+
const ms = (s) => Math.max(0, Math.round(s * 1e3));
|
|
248
|
+
try {
|
|
249
|
+
const inputs = [];
|
|
250
|
+
const filters = [];
|
|
251
|
+
const labels = [];
|
|
252
|
+
for (const { clip, buffer } of usable) {
|
|
253
|
+
const p = join5(workDir, `clip-${inputs.length}.mp3`);
|
|
254
|
+
await writeFile2(p, new Uint8Array(buffer));
|
|
255
|
+
const i = inputs.push(p) - 1;
|
|
256
|
+
const delayMs = ms(clip.startSec);
|
|
257
|
+
const start = Math.max(0, clip.sourceInSec);
|
|
258
|
+
const end = start + Math.max(0, clip.durationSec);
|
|
259
|
+
filters.push(
|
|
260
|
+
`[${i}:a]atrim=start=${start}:end=${end},asetpts=PTS-STARTPTS,adelay=${delayMs}|${delayMs}[a${i}]`
|
|
261
|
+
);
|
|
262
|
+
labels.push(`[a${i}]`);
|
|
341
263
|
}
|
|
264
|
+
const graph = `${filters.join(";")};${labels.join("")}amix=inputs=${labels.length}:normalize=0:dropout_transition=0[aout]`;
|
|
265
|
+
const outputPath = join5(workDir, "mixed.mp3");
|
|
266
|
+
const args = ["-y"];
|
|
267
|
+
for (const p of inputs) args.push("-i", p);
|
|
268
|
+
args.push(
|
|
269
|
+
"-filter_complex",
|
|
270
|
+
graph,
|
|
271
|
+
"-map",
|
|
272
|
+
"[aout]",
|
|
273
|
+
"-c:a",
|
|
274
|
+
"libmp3lame",
|
|
275
|
+
"-b:a",
|
|
276
|
+
"192k",
|
|
277
|
+
outputPath
|
|
278
|
+
);
|
|
279
|
+
await new Promise((resolve4, reject) => {
|
|
280
|
+
execFile2(ffmpegPath, args, { timeout: 18e4 }, (err) => {
|
|
281
|
+
if (err) reject(new Error(`ffmpeg audio mix failed: ${err.message}`));
|
|
282
|
+
else resolve4();
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
const data = await readFile3(outputPath);
|
|
286
|
+
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
287
|
+
} finally {
|
|
288
|
+
await rm2(workDir, { recursive: true, force: true });
|
|
342
289
|
}
|
|
343
|
-
return images;
|
|
344
290
|
}
|
|
345
291
|
|
|
346
|
-
// src/commands/video.ts
|
|
347
|
-
import { mkdir as mkdir2 } from "fs/promises";
|
|
348
|
-
import { dirname as dirname2, basename as basename2, extname as extname3, resolve as resolve2 } from "path";
|
|
349
|
-
|
|
350
292
|
// src/api.ts
|
|
351
|
-
import {
|
|
352
|
-
import {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
return new Promise((resolve3) => {
|
|
359
|
-
execFile(command, ["ffmpeg"], { timeout: 5e3 }, (err, stdout) => {
|
|
360
|
-
if (err || !stdout.trim()) {
|
|
361
|
-
resolve3(null);
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
const path = stdout.trim().split("\n")[0].trim();
|
|
365
|
-
resolve3(path);
|
|
366
|
-
});
|
|
293
|
+
import { MemoryContentContainer as MemoryContentContainer2 } from "@bendyline/squisq/storage";
|
|
294
|
+
import { ConversionError } from "@bendyline/squisq-formats";
|
|
295
|
+
async function convert(source, to, options = {}) {
|
|
296
|
+
return formatsConvert(source, to, {
|
|
297
|
+
registry: createCliRegistry(),
|
|
298
|
+
resolvePlayerScript: () => import("@bendyline/squisq-react/standalone-source").then((m) => m.PLAYER_BUNDLE),
|
|
299
|
+
...options
|
|
367
300
|
});
|
|
368
301
|
}
|
|
369
|
-
|
|
370
|
-
// src/api.ts
|
|
371
|
-
import { MemoryContentContainer as MemoryContentContainer2 } from "@bendyline/squisq/storage";
|
|
372
302
|
async function renderDocToMp4(doc, container, options) {
|
|
373
303
|
const {
|
|
374
304
|
outputPath,
|
|
@@ -387,7 +317,7 @@ async function renderDocToMp4(doc, container, options) {
|
|
|
387
317
|
const ffmpegPath = await detectFfmpeg();
|
|
388
318
|
if (!ffmpegPath) {
|
|
389
319
|
throw new Error(
|
|
390
|
-
"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"
|
|
320
|
+
"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."
|
|
391
321
|
);
|
|
392
322
|
}
|
|
393
323
|
onProgress?.("collecting media", 0);
|
|
@@ -401,20 +331,25 @@ async function renderDocToMp4(doc, container, options) {
|
|
|
401
331
|
}
|
|
402
332
|
}
|
|
403
333
|
const audio = /* @__PURE__ */ new Map();
|
|
404
|
-
const audioBuffers = [];
|
|
405
334
|
if (doc.audio?.segments?.length) {
|
|
406
335
|
for (const seg of doc.audio.segments) {
|
|
407
336
|
const data = await container.readFile(seg.src);
|
|
408
337
|
if (data) {
|
|
409
338
|
audio.set(seg.src, data);
|
|
410
339
|
audio.set(seg.name, data);
|
|
411
|
-
audioBuffers.push(data);
|
|
412
340
|
}
|
|
413
341
|
}
|
|
414
342
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
343
|
+
const mediaSrcs = new Set(resolveMediaSchedule(doc).map((c) => c.src));
|
|
344
|
+
for (const block of flattenBlocks(doc.blocks)) {
|
|
345
|
+
for (const layer of block.layers ?? []) {
|
|
346
|
+
if (layer.type === "video") mediaSrcs.add(layer.content.src);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
for (const src of mediaSrcs) {
|
|
350
|
+
if (images.has(src)) continue;
|
|
351
|
+
const data = await container.readFile(src);
|
|
352
|
+
if (data) images.set(src, data);
|
|
418
353
|
}
|
|
419
354
|
onProgress?.("generating render HTML", 10);
|
|
420
355
|
const { PLAYER_BUNDLE } = await import("@bendyline/squisq-react/standalone-source");
|
|
@@ -428,7 +363,16 @@ async function renderDocToMp4(doc, container, options) {
|
|
|
428
363
|
});
|
|
429
364
|
onProgress?.("launching browser", 15);
|
|
430
365
|
const { chromium } = await import("playwright-core");
|
|
431
|
-
|
|
366
|
+
let browser;
|
|
367
|
+
try {
|
|
368
|
+
browser = await chromium.launch({ headless: true });
|
|
369
|
+
} catch (err) {
|
|
370
|
+
const detail = err instanceof Error ? err.message.split("\n")[0] : String(err);
|
|
371
|
+
throw new Error(
|
|
372
|
+
`Playwright Chromium is not installed. Run: npx playwright install chromium
|
|
373
|
+
(launch failed: ${detail})`
|
|
374
|
+
);
|
|
375
|
+
}
|
|
432
376
|
const page = await browser.newPage({
|
|
433
377
|
viewport: { width: dimensions.width, height: dimensions.height }
|
|
434
378
|
});
|
|
@@ -446,7 +390,9 @@ async function renderDocToMp4(doc, container, options) {
|
|
|
446
390
|
const errorDetail = pageErrors.length ? `
|
|
447
391
|
Page errors:
|
|
448
392
|
${pageErrors.join("\n ")}` : "\nNo page errors captured \u2014 the player may have failed to mount.";
|
|
449
|
-
throw new Error(
|
|
393
|
+
throw new Error(
|
|
394
|
+
`The standalone player failed to boot in headless Chromium. Render API did not initialize within 15 seconds.${errorDetail}`
|
|
395
|
+
);
|
|
450
396
|
}
|
|
451
397
|
const docDuration = await page.evaluate(() => {
|
|
452
398
|
return window.getDuration();
|
|
@@ -494,11 +440,8 @@ Page errors:
|
|
|
494
440
|
}
|
|
495
441
|
await browser.close();
|
|
496
442
|
onProgress?.("encoding video", 80);
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
encodingAudio = await addAudioDelay(ffmpegPath, concatenatedAudio, coverPreRoll);
|
|
500
|
-
}
|
|
501
|
-
const { framesToMp4Native } = await import("./nativeEncoder-EXDP2O5B.js");
|
|
443
|
+
const encodingAudio = await buildMixedAudioTrack(doc, container, ffmpegPath, coverPreRoll);
|
|
444
|
+
const { framesToMp4Native } = await import("./nativeEncoder-DLLAT2RT.js");
|
|
502
445
|
await framesToMp4Native(ffmpegPath, frames, encodingAudio, outputPath, {
|
|
503
446
|
fps,
|
|
504
447
|
quality,
|
|
@@ -517,101 +460,224 @@ Page errors:
|
|
|
517
460
|
outputPath
|
|
518
461
|
};
|
|
519
462
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
463
|
+
|
|
464
|
+
// src/registry.ts
|
|
465
|
+
var MP4_DEFAULTS = {
|
|
466
|
+
fps: 30,
|
|
467
|
+
quality: "normal",
|
|
468
|
+
orientation: "landscape",
|
|
469
|
+
coverPreRoll: 0
|
|
470
|
+
};
|
|
471
|
+
function mp4Format() {
|
|
472
|
+
return {
|
|
473
|
+
id: "mp4",
|
|
474
|
+
label: "MP4 Video",
|
|
475
|
+
mimeType: "video/mp4",
|
|
476
|
+
extensions: [".mp4"],
|
|
477
|
+
async exportDoc(input, options) {
|
|
478
|
+
const mp4Opts = options.formatOptions?.mp4 ?? {};
|
|
479
|
+
const fps = typeof mp4Opts.fps === "number" ? mp4Opts.fps : MP4_DEFAULTS.fps;
|
|
480
|
+
const quality = typeof mp4Opts.quality === "string" ? mp4Opts.quality : MP4_DEFAULTS.quality;
|
|
481
|
+
const orientation = typeof mp4Opts.orientation === "string" ? mp4Opts.orientation : MP4_DEFAULTS.orientation;
|
|
482
|
+
const coverPreRoll = typeof mp4Opts.coverPreRoll === "number" ? mp4Opts.coverPreRoll : MP4_DEFAULTS.coverPreRoll;
|
|
483
|
+
const outputPath = join2(tmpdir(), `squisq-mp4-${randomBytes(8).toString("hex")}.mp4`);
|
|
484
|
+
try {
|
|
485
|
+
await renderDocToMp4(input.doc, input.container, {
|
|
486
|
+
outputPath,
|
|
487
|
+
fps,
|
|
488
|
+
quality,
|
|
489
|
+
orientation,
|
|
490
|
+
coverPreRoll
|
|
491
|
+
});
|
|
492
|
+
const data = await readFile2(outputPath);
|
|
493
|
+
const bytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
494
|
+
return { bytes, mimeType: "video/mp4", suggestedFilename: "", warnings: [] };
|
|
495
|
+
} finally {
|
|
496
|
+
await rm(outputPath, { force: true });
|
|
497
|
+
}
|
|
550
498
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
});
|
|
566
|
-
const data = await readFile2(outputPath);
|
|
567
|
-
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
568
|
-
} finally {
|
|
569
|
-
await rm(workDir, { recursive: true, force: true });
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
function createCliRegistry() {
|
|
502
|
+
const registry = defaultRegistry2();
|
|
503
|
+
registry.register(mp4Format());
|
|
504
|
+
return registry;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// src/util/applyTransformPipeline.ts
|
|
508
|
+
async function assertValidTransformStyle(style) {
|
|
509
|
+
const { getTransformStyleIds } = await import("@bendyline/squisq/transform");
|
|
510
|
+
const styles = getTransformStyleIds();
|
|
511
|
+
if (!styles.includes(style)) {
|
|
512
|
+
throw new Error(`Unknown transform style "${style}". Available: ${styles.join(", ")}`);
|
|
570
513
|
}
|
|
571
514
|
}
|
|
572
|
-
async function
|
|
573
|
-
const {
|
|
574
|
-
const
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
515
|
+
async function assertValidThemeId(themeId, source) {
|
|
516
|
+
const { getAvailableThemes } = await import("@bendyline/squisq/schemas");
|
|
517
|
+
const builtins = getAvailableThemes();
|
|
518
|
+
let customIds = [];
|
|
519
|
+
if (source.markdownDoc) {
|
|
520
|
+
const { readCustomThemesFromFrontmatter } = await import("@bendyline/squisq/doc");
|
|
521
|
+
customIds = (readCustomThemesFromFrontmatter(source.markdownDoc.frontmatter) ?? []).map(
|
|
522
|
+
(t) => t.id
|
|
523
|
+
);
|
|
524
|
+
} else if (source.doc?.customThemes) {
|
|
525
|
+
customIds = source.doc.customThemes.map((t) => t.id);
|
|
526
|
+
}
|
|
527
|
+
if (!builtins.includes(themeId) && !customIds.includes(themeId)) {
|
|
528
|
+
throw new Error(
|
|
529
|
+
`Unknown theme "${themeId}". Available: ${[...builtins, ...customIds].join(", ")}`
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
async function applyTransformToDoc(doc, transformStyle, themeId) {
|
|
534
|
+
const { applyTransform, extractDocImages } = await import("@bendyline/squisq/transform");
|
|
535
|
+
const images = extractDocImages(doc.blocks);
|
|
536
|
+
const result = applyTransform(doc, transformStyle, {
|
|
537
|
+
themeId,
|
|
538
|
+
images
|
|
539
|
+
});
|
|
540
|
+
return result.doc;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// src/commands/convert.ts
|
|
544
|
+
var VALID_FORMATS = [...BUILTIN_FORMAT_IDS, "mp4"];
|
|
545
|
+
var DEFAULT_FORMATS = [
|
|
546
|
+
"docx",
|
|
547
|
+
"pptx",
|
|
548
|
+
"pdf",
|
|
549
|
+
"html",
|
|
550
|
+
"htmlzip",
|
|
551
|
+
"epub",
|
|
552
|
+
"dbk"
|
|
553
|
+
];
|
|
554
|
+
function isValidFormat(id) {
|
|
555
|
+
return VALID_FORMATS.includes(id);
|
|
556
|
+
}
|
|
557
|
+
function parseFormats(value) {
|
|
558
|
+
const requested = value.split(",").map((s) => s.trim().toLowerCase());
|
|
559
|
+
const valid = [];
|
|
560
|
+
for (const r of requested) {
|
|
561
|
+
if (isValidFormat(r)) {
|
|
562
|
+
valid.push(r);
|
|
563
|
+
} else {
|
|
564
|
+
console.warn(`Unknown format "${r}" \u2014 skipping. Valid: ${VALID_FORMATS.join(", ")}`);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (valid.length === 0) {
|
|
568
|
+
throw new Error(`No valid formats specified. Valid: ${VALID_FORMATS.join(", ")}`);
|
|
569
|
+
}
|
|
570
|
+
return valid;
|
|
571
|
+
}
|
|
572
|
+
function formatFromOutputPath(outputPath) {
|
|
573
|
+
const lower = outputPath.toLowerCase();
|
|
574
|
+
if (lower.endsWith(".html.zip")) return "htmlzip";
|
|
575
|
+
const ext = extname2(lower);
|
|
576
|
+
const def = createCliRegistry().byExtension(ext);
|
|
577
|
+
if (!def) {
|
|
578
|
+
throw new Error(
|
|
579
|
+
`Cannot infer a format from output extension "${ext || "(none)"}". Use --format to specify one. Valid: ${VALID_FORMATS.join(", ")}`
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
return def.id;
|
|
583
|
+
}
|
|
584
|
+
function registerConvertCommand(program2) {
|
|
585
|
+
program2.command("convert").description("Convert a document to DOCX, PPTX, PDF, HTML, EPUB, MP4, and container formats").argument("<input>", "Path to .md/.docx/.pptx/.pdf/.xlsx/.csv/.html file, .zip/.dbk, or folder").option("-o, --output <file>", "Single output file (format inferred from its extension)").option(
|
|
586
|
+
"-d, --output-dir <dir>",
|
|
587
|
+
"Output directory for multi-format export (default: same as input)"
|
|
588
|
+
).option(
|
|
589
|
+
"-f, --formats <list>",
|
|
590
|
+
`Comma-separated formats to produce (default: a standard set). Valid: ${VALID_FORMATS.join(", ")}`
|
|
591
|
+
).option("--format <id>", "Produce a single format (alias for a one-entry --formats)").option("-t, --theme <id>", "Squisq theme ID to apply (e.g., documentary, cinematic, bold)").option(
|
|
592
|
+
"--transform <style>",
|
|
593
|
+
"Transform style to apply before export (e.g., documentary, magazine, minimal)"
|
|
594
|
+
).option(
|
|
595
|
+
"--no-auto-templates",
|
|
596
|
+
"Disable content-aware template auto-picking for unannotated headings"
|
|
597
|
+
).action(async (inputPath, opts) => {
|
|
598
|
+
try {
|
|
599
|
+
await runConvert(inputPath, opts);
|
|
600
|
+
} catch (err) {
|
|
601
|
+
if (err instanceof ConversionError2) {
|
|
602
|
+
console.error(`Error: ${err.message}`);
|
|
603
|
+
if (err.hint) console.error(` ${err.hint}`);
|
|
604
|
+
} else {
|
|
605
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
606
|
+
console.error(`Error: ${message}`);
|
|
607
|
+
}
|
|
608
|
+
process.exitCode = 1;
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
function resolveFormats(opts) {
|
|
613
|
+
const hasOutput = Boolean(opts.output);
|
|
614
|
+
const hasFormatSelection = Boolean(opts.formats || opts.format);
|
|
615
|
+
if (hasOutput && hasFormatSelection) {
|
|
616
|
+
throw new Error(
|
|
617
|
+
"--output is a single-file destination and cannot be combined with --formats/--format."
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
if (hasOutput) {
|
|
621
|
+
return [formatFromOutputPath(opts.output)];
|
|
622
|
+
}
|
|
623
|
+
if (opts.format) {
|
|
624
|
+
if (!isValidFormat(opts.format.toLowerCase())) {
|
|
625
|
+
throw new Error(`Unknown format "${opts.format}". Valid: ${VALID_FORMATS.join(", ")}`);
|
|
626
|
+
}
|
|
627
|
+
return [opts.format.toLowerCase()];
|
|
628
|
+
}
|
|
629
|
+
if (opts.formats) {
|
|
630
|
+
return parseFormats(opts.formats);
|
|
631
|
+
}
|
|
632
|
+
return [...DEFAULT_FORMATS];
|
|
633
|
+
}
|
|
634
|
+
async function runConvert(inputPath, opts) {
|
|
635
|
+
const resolvedInput = resolve(inputPath);
|
|
636
|
+
const formats = resolveFormats(opts);
|
|
637
|
+
const inputBasename = basename(resolvedInput);
|
|
638
|
+
const inputExt = extname2(inputBasename);
|
|
639
|
+
const baseName = inputExt ? inputBasename.slice(0, -inputExt.length) : inputBasename;
|
|
640
|
+
if (opts.transform) {
|
|
641
|
+
await assertValidTransformStyle(opts.transform);
|
|
642
|
+
}
|
|
643
|
+
console.error(`Reading: ${resolvedInput}`);
|
|
644
|
+
const result = await readInput(resolvedInput);
|
|
645
|
+
if (opts.theme) {
|
|
646
|
+
await assertValidThemeId(opts.theme, result);
|
|
647
|
+
}
|
|
648
|
+
const source = result.markdownDoc ? {
|
|
649
|
+
kind: "markdown",
|
|
650
|
+
markdown: result.markdownDoc,
|
|
651
|
+
container: result.container,
|
|
652
|
+
baseName
|
|
653
|
+
} : { kind: "doc", doc: result.doc, container: result.container, baseName };
|
|
654
|
+
if (opts.transform) {
|
|
655
|
+
console.error(` Applying transform: ${opts.transform}`);
|
|
656
|
+
}
|
|
657
|
+
for (const format of formats) {
|
|
658
|
+
const conversion = await convert(source, format, {
|
|
659
|
+
themeId: opts.theme,
|
|
660
|
+
transformStyle: opts.transform,
|
|
661
|
+
autoTemplates: opts.autoTemplates,
|
|
662
|
+
title: baseName
|
|
605
663
|
});
|
|
606
|
-
const
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
664
|
+
for (const warning of conversion.warnings) {
|
|
665
|
+
console.error(` \u26A0 ${warning}`);
|
|
666
|
+
}
|
|
667
|
+
const outPath = opts.output ? resolve(opts.output) : join3(
|
|
668
|
+
opts.outputDir ? resolve(opts.outputDir) : dirname(resolvedInput),
|
|
669
|
+
conversion.suggestedFilename
|
|
670
|
+
);
|
|
671
|
+
await mkdir(dirname(outPath), { recursive: true });
|
|
672
|
+
await writeFile(outPath, Buffer.from(conversion.bytes));
|
|
673
|
+
console.error(` \u2713 ${outPath}`);
|
|
611
674
|
}
|
|
675
|
+
console.error("Done.");
|
|
612
676
|
}
|
|
613
677
|
|
|
614
678
|
// src/commands/video.ts
|
|
679
|
+
import { mkdir as mkdir2 } from "fs/promises";
|
|
680
|
+
import { dirname as dirname2, basename as basename2, extname as extname3, resolve as resolve2 } from "path";
|
|
615
681
|
var VALID_QUALITIES = ["draft", "normal", "high"];
|
|
616
682
|
var VALID_ORIENTATIONS = ["landscape", "portrait"];
|
|
617
683
|
var VALID_CAPTIONS = ["off", "standard", "social"];
|
|
@@ -628,6 +694,16 @@ function registerVideoCommand(program2) {
|
|
|
628
694
|
"--captions <style>",
|
|
629
695
|
`Caption style: ${VALID_CAPTIONS.join(", ")} (default: off)`,
|
|
630
696
|
"off"
|
|
697
|
+
).option(
|
|
698
|
+
"--no-auto-templates",
|
|
699
|
+
"Disable content-aware template auto-picking for unannotated headings"
|
|
700
|
+
).option("-t, --theme <id>", "Squisq theme ID to apply (e.g., documentary, cinematic, bold)").option(
|
|
701
|
+
"--transform <style>",
|
|
702
|
+
"Transform style to apply before rendering (e.g., documentary, magazine, minimal)"
|
|
703
|
+
).option(
|
|
704
|
+
"--cover-preroll <seconds>",
|
|
705
|
+
"Seconds of cover-slide pre-roll before the story starts (default: 2)",
|
|
706
|
+
"2"
|
|
631
707
|
).option("--width <pixels>", "Override video width").option("--height <pixels>", "Override video height").action(async (inputPath, outputArg, opts) => {
|
|
632
708
|
try {
|
|
633
709
|
if (outputArg && !opts.output) {
|
|
@@ -662,6 +738,13 @@ async function runVideo(inputPath, opts) {
|
|
|
662
738
|
throw new Error(`Invalid captions "${captions}". Valid: ${VALID_CAPTIONS.join(", ")}`);
|
|
663
739
|
}
|
|
664
740
|
const captionStyle = captions === "off" ? void 0 : captions;
|
|
741
|
+
const coverPreRoll = parseFloat(opts.coverPreroll ?? "2");
|
|
742
|
+
if (isNaN(coverPreRoll) || coverPreRoll < 0) {
|
|
743
|
+
throw new Error("Cover pre-roll must be a number of seconds >= 0");
|
|
744
|
+
}
|
|
745
|
+
if (opts.transform) {
|
|
746
|
+
await assertValidTransformStyle(opts.transform);
|
|
747
|
+
}
|
|
665
748
|
const inputBasename = basename2(resolvedInput);
|
|
666
749
|
const inputExt = extname3(inputBasename);
|
|
667
750
|
const baseName = inputExt ? inputBasename.slice(0, -inputExt.length) : inputBasename;
|
|
@@ -670,15 +753,24 @@ async function runVideo(inputPath, opts) {
|
|
|
670
753
|
console.error(`Reading: ${resolvedInput}`);
|
|
671
754
|
const result = await readInput(resolvedInput);
|
|
672
755
|
const { container } = result;
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
756
|
+
if (opts.theme) {
|
|
757
|
+
await assertValidThemeId(opts.theme, result);
|
|
758
|
+
}
|
|
759
|
+
let doc = result.doc;
|
|
760
|
+
if (result.markdownDoc) {
|
|
761
|
+
if (opts.autoTemplates === false) {
|
|
762
|
+
const { markdownToDoc: markdownToDoc2 } = await import("@bendyline/squisq/doc");
|
|
763
|
+
doc = markdownToDoc2(result.markdownDoc, { autoTemplates: false });
|
|
764
|
+
}
|
|
680
765
|
} else {
|
|
681
|
-
|
|
766
|
+
console.error("Using pre-built Doc JSON");
|
|
767
|
+
}
|
|
768
|
+
if (opts.transform) {
|
|
769
|
+
doc = await applyTransformToDoc(doc, opts.transform, opts.theme);
|
|
770
|
+
console.error(` Applied transform: ${opts.transform}`);
|
|
771
|
+
}
|
|
772
|
+
if (opts.theme) {
|
|
773
|
+
doc = { ...doc, themeId: opts.theme };
|
|
682
774
|
}
|
|
683
775
|
console.error(
|
|
684
776
|
`Rendering: ${fps} fps, quality: ${quality}, orientation: ${orientation}, captions: ${captions}`
|
|
@@ -691,7 +783,7 @@ async function runVideo(inputPath, opts) {
|
|
|
691
783
|
width: opts.width ? parseInt(opts.width, 10) : void 0,
|
|
692
784
|
height: opts.height ? parseInt(opts.height, 10) : void 0,
|
|
693
785
|
captionStyle,
|
|
694
|
-
coverPreRoll
|
|
786
|
+
coverPreRoll,
|
|
695
787
|
onProgress: (phase, percent) => {
|
|
696
788
|
writeProgress(phase, percent, 100);
|
|
697
789
|
}
|
|
@@ -714,13 +806,134 @@ function clearProgress() {
|
|
|
714
806
|
process.stderr.write("\r" + " ".repeat(80) + "\r");
|
|
715
807
|
}
|
|
716
808
|
|
|
809
|
+
// src/commands/validate.ts
|
|
810
|
+
import { existsSync, statSync } from "fs";
|
|
811
|
+
import { dirname as dirname3, extname as extname4, join as join4, resolve as resolve3 } from "path";
|
|
812
|
+
import { validateMarkdownDoc } from "@bendyline/squisq/doc";
|
|
813
|
+
function registerValidateCommand(program2) {
|
|
814
|
+
program2.command("validate").description("Validate a squisq markdown document and report structural problems").argument("<input>", "Path to .md file, .zip/.dbk container, or folder").option("--json", "Output diagnostics as JSON (for tooling and agents)").option("--strict", "Exit non-zero on warnings as well as errors").action(async (inputPath, opts) => {
|
|
815
|
+
try {
|
|
816
|
+
const exitCode = await runValidate(inputPath, opts);
|
|
817
|
+
process.exitCode = exitCode;
|
|
818
|
+
} catch (err) {
|
|
819
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
820
|
+
console.error(`Error: ${message}`);
|
|
821
|
+
process.exitCode = 2;
|
|
822
|
+
}
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
async function runValidate(inputPath, opts) {
|
|
826
|
+
const resolvedInput = resolve3(inputPath);
|
|
827
|
+
const { container, markdownDoc, doc } = await readInput(resolvedInput);
|
|
828
|
+
if (!markdownDoc) {
|
|
829
|
+
const diagnostics = doc.diagnostics ?? [];
|
|
830
|
+
return report(diagnostics, opts, "doc.json input \u2014 markdown-level checks skipped");
|
|
831
|
+
}
|
|
832
|
+
const entries = await container.listFiles();
|
|
833
|
+
const containerPaths = new Set(entries.map((e) => e.path));
|
|
834
|
+
const isBareMarkdown = statSync(resolvedInput).isFile() && ![".zip", ".dbk"].includes(extname4(resolvedInput).toLowerCase());
|
|
835
|
+
const baseDir = dirname3(resolvedInput);
|
|
836
|
+
const hasAsset = (path) => {
|
|
837
|
+
if (containerPaths.has(path) || containerPaths.has(path.replace(/^\.\//, ""))) return true;
|
|
838
|
+
if (isBareMarkdown) return existsSync(join4(baseDir, path));
|
|
839
|
+
return false;
|
|
840
|
+
};
|
|
841
|
+
const result = validateMarkdownDoc(markdownDoc, { assets: hasAsset });
|
|
842
|
+
return report(result.diagnostics, opts);
|
|
843
|
+
}
|
|
844
|
+
function severityLabel(severity) {
|
|
845
|
+
switch (severity) {
|
|
846
|
+
case "error":
|
|
847
|
+
return "ERROR ";
|
|
848
|
+
case "warning":
|
|
849
|
+
return "warning";
|
|
850
|
+
case "info":
|
|
851
|
+
return "\u2139 info ";
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
function report(diagnostics, opts, note) {
|
|
855
|
+
const errorCount = diagnostics.filter((d) => d.severity === "error").length;
|
|
856
|
+
const warningCount = diagnostics.filter((d) => d.severity === "warning").length;
|
|
857
|
+
const infoCount = diagnostics.filter((d) => d.severity === "info").length;
|
|
858
|
+
if (opts.json) {
|
|
859
|
+
process.stdout.write(
|
|
860
|
+
`${JSON.stringify({ errorCount, warningCount, infoCount, diagnostics }, null, 2)}
|
|
861
|
+
`
|
|
862
|
+
);
|
|
863
|
+
} else {
|
|
864
|
+
if (note) console.error(note);
|
|
865
|
+
for (const d of diagnostics) {
|
|
866
|
+
const location = d.line != null ? `line ${d.line}` : d.blockId ?? "\u2014";
|
|
867
|
+
console.error(`${severityLabel(d.severity)} ${location} [${d.code}] ${d.message}`);
|
|
868
|
+
}
|
|
869
|
+
if (diagnostics.length === 0) {
|
|
870
|
+
console.error("\u2713 No problems found");
|
|
871
|
+
} else {
|
|
872
|
+
console.error(`
|
|
873
|
+
${errorCount} error(s), ${warningCount} warning(s), ${infoCount} info`);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
if (errorCount > 0) return 1;
|
|
877
|
+
if (opts.strict && warningCount > 0) return 1;
|
|
878
|
+
return 0;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// src/commands/doctor.ts
|
|
882
|
+
function registerDoctorCommand(program2) {
|
|
883
|
+
program2.command("doctor").description("Check that ffmpeg and Playwright Chromium are available for video rendering").action(async () => {
|
|
884
|
+
const allPresent = await runDoctor();
|
|
885
|
+
process.exitCode = allPresent ? 0 : 1;
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
async function runDoctor() {
|
|
889
|
+
let allPresent = true;
|
|
890
|
+
console.error(`Node: ${process.version}`);
|
|
891
|
+
try {
|
|
892
|
+
const detection = await detectFfmpegDetailed();
|
|
893
|
+
if (detection) {
|
|
894
|
+
const version2 = await getFfmpegVersion(detection.path);
|
|
895
|
+
console.error(
|
|
896
|
+
`ffmpeg: ${detection.path} (source: ${detection.source}) \u2014 ${version2 ?? "version unknown"}`
|
|
897
|
+
);
|
|
898
|
+
} else {
|
|
899
|
+
allPresent = false;
|
|
900
|
+
console.error("ffmpeg: not found.");
|
|
901
|
+
console.error(" Install it with:");
|
|
902
|
+
console.error(" macOS: brew install ffmpeg");
|
|
903
|
+
console.error(" Ubuntu: sudo apt install ffmpeg");
|
|
904
|
+
console.error(" Windows: winget install ffmpeg");
|
|
905
|
+
console.error(" Or: npm install ffmpeg-static, or set SQUISQ_FFMPEG.");
|
|
906
|
+
}
|
|
907
|
+
} catch (err) {
|
|
908
|
+
allPresent = false;
|
|
909
|
+
console.error(`ffmpeg: ${err instanceof Error ? err.message : String(err)}`);
|
|
910
|
+
}
|
|
911
|
+
try {
|
|
912
|
+
const { chromium } = await import("playwright-core");
|
|
913
|
+
const browser = await chromium.launch({ headless: true });
|
|
914
|
+
await browser.close();
|
|
915
|
+
console.error(`Chromium: available (${chromium.executablePath()})`);
|
|
916
|
+
} catch (err) {
|
|
917
|
+
allPresent = false;
|
|
918
|
+
const detail = err instanceof Error ? err.message.split("\n")[0] : String(err);
|
|
919
|
+
console.error(`Chromium: not available \u2014 ${detail}`);
|
|
920
|
+
console.error(" Run: npx playwright install chromium");
|
|
921
|
+
}
|
|
922
|
+
console.error(allPresent ? "\n\u2713 All checks passed" : "\n\u2717 Some checks failed");
|
|
923
|
+
return allPresent;
|
|
924
|
+
}
|
|
925
|
+
|
|
717
926
|
// src/index.ts
|
|
927
|
+
var require2 = createRequire(import.meta.url);
|
|
928
|
+
var { version } = require2("../package.json");
|
|
718
929
|
console.error(
|
|
719
|
-
|
|
930
|
+
`\x1B[36m{[\x1B[0m \x1B[1msquiggly square\x1B[0m \x1B[2m\u2014\x1B[0m \x1B[1msquisq\x1B[0m \x1B[2m\u2014\x1B[0m \x1B[33mv${version}\x1B[0m \x1B[36m]}\x1B[0m`
|
|
720
931
|
);
|
|
721
932
|
var program = new Command();
|
|
722
|
-
program.name("squisq").description("Squisq CLI \u2014 convert and process markdown-based documents").version(
|
|
933
|
+
program.name("squisq").description("Squisq CLI \u2014 convert and process markdown-based documents").version(version);
|
|
723
934
|
registerConvertCommand(program);
|
|
724
935
|
registerVideoCommand(program);
|
|
936
|
+
registerValidateCommand(program);
|
|
937
|
+
registerDoctorCommand(program);
|
|
725
938
|
program.parse();
|
|
726
939
|
//# sourceMappingURL=index.js.map
|