@bendyline/squisq-cli 1.0.1 → 1.1.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 +50 -7
- package/dist/__tests__/readInput.test.js +38 -1
- package/dist/__tests__/readInput.test.js.map +1 -1
- package/dist/api.d.ts +106 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +324 -0
- package/dist/api.js.map +1 -0
- package/dist/commands/convert.js +33 -4
- package/dist/commands/convert.js.map +1 -1
- package/dist/commands/video.d.ts +2 -5
- package/dist/commands/video.d.ts.map +1 -1
- package/dist/commands/video.js +27 -158
- package/dist/commands/video.js.map +1 -1
- package/dist/nativeEncoder-MWHOONST.js +88 -0
- package/dist/nativeEncoder-MWHOONST.js.map +1 -0
- package/dist/util/readInput.d.ts +18 -6
- package/dist/util/readInput.d.ts.map +1 -1
- package/dist/util/readInput.js +51 -7
- package/dist/util/readInput.js.map +1 -1
- package/package.json +26 -14
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ npm install -g @bendyline/squisq-cli
|
|
|
17
17
|
|
|
18
18
|
### `squisq convert`
|
|
19
19
|
|
|
20
|
-
Convert a Squisq document to DOCX, PPTX, PDF, HTML, or DBK (container ZIP):
|
|
20
|
+
Convert a Squisq document to DOCX, PPTX, PDF, HTML, EPUB, or DBK (container ZIP):
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
23
|
squisq convert input.md --formats docx,pptx,pdf
|
|
@@ -26,12 +26,12 @@ squisq convert ./my-folder --formats pdf --theme documentary
|
|
|
26
26
|
squisq convert input.md --theme cinematic --transform magazine --formats pptx
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
| Option | Description
|
|
30
|
-
| -------------- |
|
|
31
|
-
| `--output-dir` | Output directory
|
|
32
|
-
| `--formats` | Comma-separated list: docx, pptx, pdf, html, dbk | all |
|
|
33
|
-
| `--theme` | Squisq theme ID (e.g., documentary, cinematic)
|
|
34
|
-
| `--transform` | Transform style (e.g., documentary, magazine)
|
|
29
|
+
| Option | Description | Default |
|
|
30
|
+
| -------------- | ------------------------------------------------------ | ----------- |
|
|
31
|
+
| `--output-dir` | Output directory | current dir |
|
|
32
|
+
| `--formats` | Comma-separated list: docx, pptx, pdf, html, epub, dbk | all |
|
|
33
|
+
| `--theme` | Squisq theme ID (e.g., documentary, cinematic) | none |
|
|
34
|
+
| `--transform` | Transform style (e.g., documentary, magazine) | none |
|
|
35
35
|
|
|
36
36
|
### `squisq video`
|
|
37
37
|
|
|
@@ -54,6 +54,49 @@ squisq video ./my-folder --orientation portrait --captions social
|
|
|
54
54
|
|
|
55
55
|
**Requires:** [ffmpeg](https://ffmpeg.org/) installed and available on your PATH.
|
|
56
56
|
|
|
57
|
+
## Programmatic API
|
|
58
|
+
|
|
59
|
+
Use the CLI as a library from Node.js — no shell-out required:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { renderDocToMp4, MemoryContentContainer, readInput } from '@bendyline/squisq-cli/api';
|
|
63
|
+
|
|
64
|
+
// Load a document from disk
|
|
65
|
+
const { doc, container } = await readInput('./my-article.md');
|
|
66
|
+
|
|
67
|
+
// Render to MP4
|
|
68
|
+
const result = await renderDocToMp4(doc, container, {
|
|
69
|
+
outputPath: './output.mp4',
|
|
70
|
+
fps: 30,
|
|
71
|
+
quality: 'high',
|
|
72
|
+
orientation: 'landscape',
|
|
73
|
+
captionStyle: 'social',
|
|
74
|
+
onProgress: (phase, pct) => console.log(`${phase}: ${pct}%`),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
console.log(`Rendered ${result.frameCount} frames (${result.duration}s)`);
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### `extractThumbnails`
|
|
81
|
+
|
|
82
|
+
Extract JPEG thumbnails from the first frame of a rendered video:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import { extractThumbnails } from '@bendyline/squisq-cli/api';
|
|
86
|
+
|
|
87
|
+
await extractThumbnails({
|
|
88
|
+
videoPath: './output.mp4',
|
|
89
|
+
outputDir: './thumbs',
|
|
90
|
+
slug: 'my-article',
|
|
91
|
+
sizes: [
|
|
92
|
+
{ name: 'og', width: 1200, height: 630, filter: 'scale=1200:630' },
|
|
93
|
+
{ name: 'thumb', width: 480, height: 270, filter: 'scale=480:270' },
|
|
94
|
+
],
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
See the full [API Reference](../../docs/API.md#bendylinesquisq-cli) for all types and options.
|
|
99
|
+
|
|
57
100
|
## Input Formats
|
|
58
101
|
|
|
59
102
|
The CLI accepts three types of input:
|
|
@@ -21,6 +21,7 @@ describe('readInput', () => {
|
|
|
21
21
|
});
|
|
22
22
|
it('reads a .md file into a container and MarkdownDocument', async () => {
|
|
23
23
|
const result = await readInput(FIXTURE_MD);
|
|
24
|
+
expect(result.markdownDoc).to.not.equal(null);
|
|
24
25
|
expect(result.markdownDoc).to.have.property('type', 'document');
|
|
25
26
|
expect(result.markdownDoc.children).to.be.an('array');
|
|
26
27
|
expect(result.markdownDoc.children.length).to.be.greaterThan(0);
|
|
@@ -36,6 +37,7 @@ describe('readInput', () => {
|
|
|
36
37
|
const zipPath = join(tempDir, 'test.zip');
|
|
37
38
|
await writeFile(zipPath, buf);
|
|
38
39
|
const result = await readInput(zipPath);
|
|
40
|
+
expect(result.markdownDoc).to.not.equal(null);
|
|
39
41
|
expect(result.markdownDoc.type).to.equal('document');
|
|
40
42
|
const doc = await result.container.readDocument();
|
|
41
43
|
expect(doc).to.include('# From ZIP');
|
|
@@ -48,6 +50,7 @@ describe('readInput', () => {
|
|
|
48
50
|
const dbkPath = join(tempDir, 'test.dbk');
|
|
49
51
|
await writeFile(dbkPath, buf);
|
|
50
52
|
const result = await readInput(dbkPath);
|
|
53
|
+
expect(result.markdownDoc).to.not.equal(null);
|
|
51
54
|
expect(result.markdownDoc.type).to.equal('document');
|
|
52
55
|
const doc = await result.container.readDocument();
|
|
53
56
|
expect(doc).to.include('# From DBK');
|
|
@@ -58,6 +61,7 @@ describe('readInput', () => {
|
|
|
58
61
|
await writeFile(join(folderPath, 'index.md'), '# Folder Doc\n\nContent here.');
|
|
59
62
|
await writeFile(join(folderPath, 'image.txt'), 'placeholder');
|
|
60
63
|
const result = await readInput(folderPath);
|
|
64
|
+
expect(result.markdownDoc).to.not.equal(null);
|
|
61
65
|
expect(result.markdownDoc.type).to.equal('document');
|
|
62
66
|
const doc = await result.container.readDocument();
|
|
63
67
|
expect(doc).to.include('# Folder Doc');
|
|
@@ -76,8 +80,41 @@ describe('readInput', () => {
|
|
|
76
80
|
}
|
|
77
81
|
catch (err) {
|
|
78
82
|
expect(err).to.be.instanceOf(Error);
|
|
79
|
-
expect(err.message).to.include('No markdown document
|
|
83
|
+
expect(err.message).to.include('No markdown document');
|
|
80
84
|
}
|
|
81
85
|
});
|
|
86
|
+
it('reads a .json file as Doc JSON', async () => {
|
|
87
|
+
const doc = {
|
|
88
|
+
articleId: 'test-article',
|
|
89
|
+
duration: 30,
|
|
90
|
+
blocks: [{ id: 'b1', startTime: 0, duration: 30, audioSegment: 0 }],
|
|
91
|
+
audio: { segments: [] },
|
|
92
|
+
};
|
|
93
|
+
const jsonPath = join(tempDir, 'doc.json');
|
|
94
|
+
await writeFile(jsonPath, JSON.stringify(doc));
|
|
95
|
+
const result = await readInput(jsonPath);
|
|
96
|
+
expect(result.markdownDoc).to.equal(null);
|
|
97
|
+
expect(result.doc).to.deep.include({ articleId: 'test-article', duration: 30 });
|
|
98
|
+
expect(result.doc.blocks).to.have.length(1);
|
|
99
|
+
});
|
|
100
|
+
it('reads a folder with doc.json instead of markdown', async () => {
|
|
101
|
+
const folderPath = join(tempDir, 'jsondoc');
|
|
102
|
+
await mkdir(folderPath, { recursive: true });
|
|
103
|
+
const doc = {
|
|
104
|
+
articleId: 'folder-doc',
|
|
105
|
+
duration: 20,
|
|
106
|
+
blocks: [],
|
|
107
|
+
audio: { segments: [] },
|
|
108
|
+
};
|
|
109
|
+
await writeFile(join(folderPath, 'doc.json'), JSON.stringify(doc));
|
|
110
|
+
await writeFile(join(folderPath, 'image.txt'), 'placeholder');
|
|
111
|
+
const result = await readInput(folderPath);
|
|
112
|
+
expect(result.markdownDoc).to.equal(null);
|
|
113
|
+
expect(result.doc).to.deep.include({ articleId: 'folder-doc' });
|
|
114
|
+
// Media files are still in the container
|
|
115
|
+
const files = await result.container.listFiles();
|
|
116
|
+
const paths = files.map((f) => f.path);
|
|
117
|
+
expect(paths).to.include('image.txt');
|
|
118
|
+
});
|
|
82
119
|
});
|
|
83
120
|
//# sourceMappingURL=readInput.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"readInput.test.js","sourceRoot":"","sources":["../../src/__tests__/readInput.test.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAC5D,OAAO,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAC9B,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,cAAc,EAAE,MAAM,qCAAqC,CAAC;AACrE,OAAO,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AAEnE,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,EAAE,SAAS,CAAC,CAAC;AAEpE,QAAQ,CAAC,WAAW,EAAE,GAAG,EAAE;IACzB,IAAI,OAAe,CAAC;IAEpB,UAAU,CAAC,KAAK,IAAI,EAAE;QACpB,OAAO,GAAG,IAAI,CAAC,MAAM,EAAE,EAAE,eAAe,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC7F,MAAM,KAAK,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC5C,CAAC,CAAC,CAAC;IAEH,SAAS,CAAC,KAAK,IAAI,EAAE;QACnB,MAAM,EAAE,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACtD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wDAAwD,EAAE,KAAK,IAAI,EAAE;QACtE,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,UAAU,CAAC,CAAC;QAE3C,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;QAChE,MAAM,CAAC,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"readInput.test.js","sourceRoot":"","sources":["../../src/__tests__/readInput.test.ts"],"names":[],"mappings":"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"}
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Programmatic Video API
|
|
3
|
+
*
|
|
4
|
+
* Provides a library-style entry point for rendering Squisq documents to MP4
|
|
5
|
+
* from Node.js callers (e.g., Qualla's pipeline). This avoids the need to shell
|
|
6
|
+
* out to the `squisq video` CLI and gives callers full control over the Doc,
|
|
7
|
+
* MemoryContentContainer, and encoding options.
|
|
8
|
+
*
|
|
9
|
+
* Orchestrates the full pipeline: Doc → render HTML → Playwright frame capture → FFmpeg encode.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* import { renderDocToMp4 } from '@bendyline/squisq-cli/api';
|
|
13
|
+
*
|
|
14
|
+
* await renderDocToMp4(doc, container, {
|
|
15
|
+
* outputPath: '/tmp/output.mp4',
|
|
16
|
+
* fps: 30,
|
|
17
|
+
* quality: 'normal',
|
|
18
|
+
* orientation: 'landscape',
|
|
19
|
+
* });
|
|
20
|
+
*/
|
|
21
|
+
import type { Doc } from '@bendyline/squisq/schemas';
|
|
22
|
+
import type { MemoryContentContainer } from '@bendyline/squisq/storage';
|
|
23
|
+
import type { VideoQuality, VideoOrientation } from '@bendyline/squisq-video';
|
|
24
|
+
export type { VideoQuality, VideoOrientation } from '@bendyline/squisq-video';
|
|
25
|
+
export { MemoryContentContainer } from '@bendyline/squisq/storage';
|
|
26
|
+
export { readInput } from './util/readInput.js';
|
|
27
|
+
export type { ReadInputResult } from './util/readInput.js';
|
|
28
|
+
/** Options for renderDocToMp4. */
|
|
29
|
+
export interface RenderDocToMp4Options {
|
|
30
|
+
/** Output file path for the MP4. */
|
|
31
|
+
outputPath: string;
|
|
32
|
+
/** Frames per second (default: 30). */
|
|
33
|
+
fps?: number;
|
|
34
|
+
/** Encoding quality preset (default: 'normal'). */
|
|
35
|
+
quality?: VideoQuality;
|
|
36
|
+
/** Video orientation (default: 'landscape'). */
|
|
37
|
+
orientation?: VideoOrientation;
|
|
38
|
+
/** Override video width in pixels. */
|
|
39
|
+
width?: number;
|
|
40
|
+
/** Override video height in pixels. */
|
|
41
|
+
height?: number;
|
|
42
|
+
/** Caption style to bake into the video (default: none). */
|
|
43
|
+
captionStyle?: 'standard' | 'social';
|
|
44
|
+
/** Seconds of cover-slide pre-roll before the story starts (default: 0). */
|
|
45
|
+
coverPreRoll?: number;
|
|
46
|
+
/**
|
|
47
|
+
* Progress callback. Called with a phase name and 0-100 percentage.
|
|
48
|
+
*/
|
|
49
|
+
onProgress?: (phase: string, percent: number) => void;
|
|
50
|
+
}
|
|
51
|
+
/** Result returned by renderDocToMp4. */
|
|
52
|
+
export interface RenderDocToMp4Result {
|
|
53
|
+
/** Duration of the rendered video in seconds (including pre-roll). */
|
|
54
|
+
duration: number;
|
|
55
|
+
/** Number of frames captured. */
|
|
56
|
+
frameCount: number;
|
|
57
|
+
/** Output file path. */
|
|
58
|
+
outputPath: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Render a Doc + media container to an MP4 video file.
|
|
62
|
+
*
|
|
63
|
+
* The container should contain audio and image files referenced by the Doc's
|
|
64
|
+
* audio.segments[].src and block image paths. Files are embedded as base64
|
|
65
|
+
* data URIs in a self-contained render HTML page.
|
|
66
|
+
*
|
|
67
|
+
* Requires:
|
|
68
|
+
* - Playwright (chromium) — for headless frame capture
|
|
69
|
+
* - FFmpeg — for video encoding (must be on PATH)
|
|
70
|
+
*
|
|
71
|
+
* @param doc - The Doc structure to render
|
|
72
|
+
* @param container - MemoryContentContainer with audio/image files
|
|
73
|
+
* @param options - Rendering and encoding options
|
|
74
|
+
* @returns Result with duration and frame count
|
|
75
|
+
*/
|
|
76
|
+
export declare function renderDocToMp4(doc: Doc, container: MemoryContentContainer, options: RenderDocToMp4Options): Promise<RenderDocToMp4Result>;
|
|
77
|
+
/** A thumbnail size specification. */
|
|
78
|
+
export interface ThumbnailSpec {
|
|
79
|
+
/** Label for the thumbnail (used in filename: `{slug}-{width}x{height}.jpg`). */
|
|
80
|
+
name: string;
|
|
81
|
+
/** Output width in pixels. */
|
|
82
|
+
width: number;
|
|
83
|
+
/** Output height in pixels. */
|
|
84
|
+
height: number;
|
|
85
|
+
/** FFmpeg video filter string (e.g., 'scale=1280:720'). */
|
|
86
|
+
filter: string;
|
|
87
|
+
}
|
|
88
|
+
/** Options for extractThumbnails. */
|
|
89
|
+
export interface ExtractThumbnailsOptions {
|
|
90
|
+
/** Path to the source MP4 video. */
|
|
91
|
+
videoPath: string;
|
|
92
|
+
/** Directory to write thumbnails into. */
|
|
93
|
+
outputDir: string;
|
|
94
|
+
/** Base slug for filenames (produces `{slug}-{width}x{height}.jpg`). */
|
|
95
|
+
slug: string;
|
|
96
|
+
/** Thumbnail sizes to generate. */
|
|
97
|
+
sizes: ThumbnailSpec[];
|
|
98
|
+
/** Overwrite existing thumbnails (default: false). */
|
|
99
|
+
force?: boolean;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Extract thumbnail images from the first frame of an MP4 video.
|
|
103
|
+
* Produces JPEG files at each specified size using FFmpeg video filters.
|
|
104
|
+
*/
|
|
105
|
+
export declare function extractThumbnails(options: ExtractThumbnailsOptions): Promise<void>;
|
|
106
|
+
//# sourceMappingURL=api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,2BAA2B,CAAC;AACrD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AACxE,OAAO,KAAK,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAM9E,YAAY,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC9E,OAAO,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AACnE,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAChD,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAE3D,kCAAkC;AAClC,MAAM,WAAW,qBAAqB;IACpC,oCAAoC;IACpC,UAAU,EAAE,MAAM,CAAC;IAEnB,uCAAuC;IACvC,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb,mDAAmD;IACnD,OAAO,CAAC,EAAE,YAAY,CAAC;IAEvB,gDAAgD;IAChD,WAAW,CAAC,EAAE,gBAAgB,CAAC;IAE/B,sCAAsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf,uCAAuC;IACvC,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,4DAA4D;IAC5D,YAAY,CAAC,EAAE,UAAU,GAAG,QAAQ,CAAC;IAErC,4EAA4E;IAC5E,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;OAEG;IACH,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACvD;AAED,yCAAyC;AACzC,MAAM,WAAW,oBAAoB;IACnC,sEAAsE;IACtE,QAAQ,EAAE,MAAM,CAAC;IAEjB,iCAAiC;IACjC,UAAU,EAAE,MAAM,CAAC;IAEnB,wBAAwB;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,cAAc,CAClC,GAAG,EAAE,GAAG,EACR,SAAS,EAAE,sBAAsB,EACjC,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,oBAAoB,CAAC,CA8L/B;AAmID,sCAAsC;AACtC,MAAM,WAAW,aAAa;IAC5B,iFAAiF;IACjF,IAAI,EAAE,MAAM,CAAC;IACb,8BAA8B;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,+BAA+B;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,2DAA2D;IAC3D,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,qCAAqC;AACrC,MAAM,WAAW,wBAAwB;IACvC,oCAAoC;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,0CAA0C;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAC;IACb,mCAAmC;IACnC,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,sDAAsD;IACtD,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiCxF"}
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Programmatic Video API
|
|
3
|
+
*
|
|
4
|
+
* Provides a library-style entry point for rendering Squisq documents to MP4
|
|
5
|
+
* from Node.js callers (e.g., Qualla's pipeline). This avoids the need to shell
|
|
6
|
+
* out to the `squisq video` CLI and gives callers full control over the Doc,
|
|
7
|
+
* MemoryContentContainer, and encoding options.
|
|
8
|
+
*
|
|
9
|
+
* Orchestrates the full pipeline: Doc → render HTML → Playwright frame capture → FFmpeg encode.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* import { renderDocToMp4 } from '@bendyline/squisq-cli/api';
|
|
13
|
+
*
|
|
14
|
+
* await renderDocToMp4(doc, container, {
|
|
15
|
+
* outputPath: '/tmp/output.mp4',
|
|
16
|
+
* fps: 30,
|
|
17
|
+
* quality: 'normal',
|
|
18
|
+
* orientation: 'landscape',
|
|
19
|
+
* });
|
|
20
|
+
*/
|
|
21
|
+
import { generateRenderHtml } from '@bendyline/squisq-video';
|
|
22
|
+
import { resolveDimensions } from '@bendyline/squisq-video';
|
|
23
|
+
import { detectFfmpeg } from './util/detectFfmpeg.js';
|
|
24
|
+
export { MemoryContentContainer } from '@bendyline/squisq/storage';
|
|
25
|
+
export { readInput } from './util/readInput.js';
|
|
26
|
+
/**
|
|
27
|
+
* Render a Doc + media container to an MP4 video file.
|
|
28
|
+
*
|
|
29
|
+
* The container should contain audio and image files referenced by the Doc's
|
|
30
|
+
* audio.segments[].src and block image paths. Files are embedded as base64
|
|
31
|
+
* data URIs in a self-contained render HTML page.
|
|
32
|
+
*
|
|
33
|
+
* Requires:
|
|
34
|
+
* - Playwright (chromium) — for headless frame capture
|
|
35
|
+
* - FFmpeg — for video encoding (must be on PATH)
|
|
36
|
+
*
|
|
37
|
+
* @param doc - The Doc structure to render
|
|
38
|
+
* @param container - MemoryContentContainer with audio/image files
|
|
39
|
+
* @param options - Rendering and encoding options
|
|
40
|
+
* @returns Result with duration and frame count
|
|
41
|
+
*/
|
|
42
|
+
export async function renderDocToMp4(doc, container, options) {
|
|
43
|
+
const { outputPath, fps = 30, quality = 'normal', orientation = 'landscape', captionStyle, coverPreRoll = 0, onProgress, } = options;
|
|
44
|
+
const dimensions = resolveDimensions({
|
|
45
|
+
orientation,
|
|
46
|
+
width: options.width,
|
|
47
|
+
height: options.height,
|
|
48
|
+
});
|
|
49
|
+
// Detect ffmpeg early — needed for audio concat and video encoding
|
|
50
|
+
const ffmpegPath = await detectFfmpeg();
|
|
51
|
+
if (!ffmpegPath) {
|
|
52
|
+
throw new Error('ffmpeg is required but not found in PATH.\n' +
|
|
53
|
+
'Install it with:\n' +
|
|
54
|
+
' macOS: brew install ffmpeg\n' +
|
|
55
|
+
' Ubuntu: sudo apt install ffmpeg\n' +
|
|
56
|
+
' Windows: winget install ffmpeg');
|
|
57
|
+
}
|
|
58
|
+
onProgress?.('collecting media', 0);
|
|
59
|
+
// ── Collect images from container ───────────────────────────────
|
|
60
|
+
const { collectImagePaths } = await import('@bendyline/squisq-formats/html');
|
|
61
|
+
const imagePaths = collectImagePaths(doc);
|
|
62
|
+
const images = new Map();
|
|
63
|
+
for (const imgPath of imagePaths) {
|
|
64
|
+
const data = await container.readFile(imgPath);
|
|
65
|
+
if (data) {
|
|
66
|
+
images.set(imgPath, data);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// ── Collect audio segments ──────────────────────────────────────
|
|
70
|
+
const audio = new Map();
|
|
71
|
+
const audioBuffers = [];
|
|
72
|
+
if (doc.audio?.segments?.length) {
|
|
73
|
+
for (const seg of doc.audio.segments) {
|
|
74
|
+
const data = await container.readFile(seg.src);
|
|
75
|
+
if (data) {
|
|
76
|
+
audio.set(seg.src, data);
|
|
77
|
+
audio.set(seg.name, data);
|
|
78
|
+
audioBuffers.push(data);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Concatenate audio for the MP4's audio track
|
|
83
|
+
let concatenatedAudio = null;
|
|
84
|
+
if (audioBuffers.length > 0) {
|
|
85
|
+
concatenatedAudio = await concatenateAudioBuffers(audioBuffers, ffmpegPath);
|
|
86
|
+
}
|
|
87
|
+
onProgress?.('generating render HTML', 10);
|
|
88
|
+
// ── Generate self-contained render HTML ─────────────────────────
|
|
89
|
+
const { PLAYER_BUNDLE } = await import('@bendyline/squisq-react/standalone-source');
|
|
90
|
+
const renderHtml = generateRenderHtml(doc, {
|
|
91
|
+
playerScript: PLAYER_BUNDLE,
|
|
92
|
+
images,
|
|
93
|
+
audio: audio.size > 0 ? audio : undefined,
|
|
94
|
+
width: dimensions.width,
|
|
95
|
+
height: dimensions.height,
|
|
96
|
+
captionStyle,
|
|
97
|
+
});
|
|
98
|
+
onProgress?.('launching browser', 15);
|
|
99
|
+
// ── Playwright frame capture ────────────────────────────────────
|
|
100
|
+
const { chromium } = await import('playwright-core');
|
|
101
|
+
const browser = await chromium.launch({ headless: true });
|
|
102
|
+
const page = await browser.newPage({
|
|
103
|
+
viewport: { width: dimensions.width, height: dimensions.height },
|
|
104
|
+
});
|
|
105
|
+
const pageErrors = [];
|
|
106
|
+
page.on('pageerror', (err) => pageErrors.push(err.message));
|
|
107
|
+
await page.setContent(renderHtml, { waitUntil: 'load' });
|
|
108
|
+
await page.waitForTimeout(500);
|
|
109
|
+
try {
|
|
110
|
+
await page.waitForFunction(() => typeof window.getDuration === 'function', { timeout: 15000 });
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
await browser.close();
|
|
114
|
+
const errorDetail = pageErrors.length
|
|
115
|
+
? `\nPage errors:\n ${pageErrors.join('\n ')}`
|
|
116
|
+
: '\nNo page errors captured — the player may have failed to mount.';
|
|
117
|
+
throw new Error(`Render API did not initialize within 15 seconds.${errorDetail}`);
|
|
118
|
+
}
|
|
119
|
+
const docDuration = await page.evaluate(() => {
|
|
120
|
+
return window.getDuration();
|
|
121
|
+
});
|
|
122
|
+
if (docDuration <= 0) {
|
|
123
|
+
await browser.close();
|
|
124
|
+
throw new Error('Document has zero duration — nothing to render');
|
|
125
|
+
}
|
|
126
|
+
const storyFrameCount = Math.ceil(docDuration * fps);
|
|
127
|
+
const preRollFrameCount = Math.ceil(coverPreRoll * fps);
|
|
128
|
+
const totalFrames = preRollFrameCount + storyFrameCount;
|
|
129
|
+
const frames = [];
|
|
130
|
+
onProgress?.('capturing frames', 20);
|
|
131
|
+
// Cover slide pre-roll (if requested)
|
|
132
|
+
if (preRollFrameCount > 0) {
|
|
133
|
+
const hasCover = await page.evaluate(() => {
|
|
134
|
+
const w = window;
|
|
135
|
+
return typeof w.hasCoverBlock === 'function' ? w.hasCoverBlock() : false;
|
|
136
|
+
});
|
|
137
|
+
if (hasCover) {
|
|
138
|
+
await page.evaluate(() => {
|
|
139
|
+
window.showCover();
|
|
140
|
+
});
|
|
141
|
+
await page.waitForTimeout(100);
|
|
142
|
+
const coverFrame = new Uint8Array(await page.screenshot({ type: 'png' }));
|
|
143
|
+
for (let i = 0; i < preRollFrameCount; i++) {
|
|
144
|
+
frames.push(coverFrame);
|
|
145
|
+
}
|
|
146
|
+
await page.evaluate(() => {
|
|
147
|
+
window.hideCover();
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// Story frames via seekTo
|
|
152
|
+
const frameInterval = 1 / fps;
|
|
153
|
+
for (let i = 0; i < storyFrameCount; i++) {
|
|
154
|
+
const time = i * frameInterval;
|
|
155
|
+
await page.evaluate((t) => {
|
|
156
|
+
return window.seekTo(t);
|
|
157
|
+
}, time);
|
|
158
|
+
const screenshot = await page.screenshot({ type: 'png' });
|
|
159
|
+
frames.push(new Uint8Array(screenshot));
|
|
160
|
+
// Report progress: frames phase is 20% to 80%
|
|
161
|
+
if (i % Math.max(1, Math.floor(fps / 2)) === 0 || i === storyFrameCount - 1) {
|
|
162
|
+
const pct = 20 + Math.round((frames.length / totalFrames) * 60);
|
|
163
|
+
onProgress?.('capturing frames', pct);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
await browser.close();
|
|
167
|
+
onProgress?.('encoding video', 80);
|
|
168
|
+
// If there's a pre-roll, delay the audio track to match
|
|
169
|
+
let encodingAudio = concatenatedAudio;
|
|
170
|
+
if (coverPreRoll > 0 && concatenatedAudio) {
|
|
171
|
+
// Use FFmpeg to add silence padding at the start (adelay filter)
|
|
172
|
+
encodingAudio = await addAudioDelay(ffmpegPath, concatenatedAudio, coverPreRoll);
|
|
173
|
+
}
|
|
174
|
+
const { framesToMp4Native } = await import('./util/nativeEncoder.js');
|
|
175
|
+
await framesToMp4Native(ffmpegPath, frames, encodingAudio, outputPath, {
|
|
176
|
+
fps,
|
|
177
|
+
quality,
|
|
178
|
+
orientation,
|
|
179
|
+
width: dimensions.width,
|
|
180
|
+
height: dimensions.height,
|
|
181
|
+
onProgress: (percent, phase) => {
|
|
182
|
+
onProgress?.(`encoding: ${phase}`, 80 + Math.round(percent * 0.2));
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
onProgress?.('done', 100);
|
|
186
|
+
const totalDuration = docDuration + coverPreRoll;
|
|
187
|
+
return {
|
|
188
|
+
duration: totalDuration,
|
|
189
|
+
frameCount: frames.length,
|
|
190
|
+
outputPath,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
// ── Audio helpers ─────────────────────────────────────────────────
|
|
194
|
+
/**
|
|
195
|
+
* Concatenate multiple audio buffers into one.
|
|
196
|
+
* Uses native ffmpeg concat when available, falls back to byte concatenation.
|
|
197
|
+
*/
|
|
198
|
+
async function concatenateAudioBuffers(buffers, ffmpegPath) {
|
|
199
|
+
if (buffers.length === 0)
|
|
200
|
+
return new Uint8Array(0);
|
|
201
|
+
if (buffers.length === 1)
|
|
202
|
+
return new Uint8Array(buffers[0]);
|
|
203
|
+
if (ffmpegPath) {
|
|
204
|
+
return concatenateAudioNative(ffmpegPath, buffers);
|
|
205
|
+
}
|
|
206
|
+
// Fallback: naive byte concatenation (works for MP3)
|
|
207
|
+
const totalLength = buffers.reduce((sum, b) => sum + b.byteLength, 0);
|
|
208
|
+
const result = new Uint8Array(totalLength);
|
|
209
|
+
let offset = 0;
|
|
210
|
+
for (const buf of buffers) {
|
|
211
|
+
result.set(new Uint8Array(buf), offset);
|
|
212
|
+
offset += buf.byteLength;
|
|
213
|
+
}
|
|
214
|
+
return result;
|
|
215
|
+
}
|
|
216
|
+
async function concatenateAudioNative(ffmpegPath, buffers) {
|
|
217
|
+
const { writeFile, readFile, mkdir, rm } = await import('node:fs/promises');
|
|
218
|
+
const { join } = await import('node:path');
|
|
219
|
+
const { tmpdir } = await import('node:os');
|
|
220
|
+
const { randomBytes } = await import('node:crypto');
|
|
221
|
+
const { execFile } = await import('node:child_process');
|
|
222
|
+
const tmpId = randomBytes(8).toString('hex');
|
|
223
|
+
const workDir = join(tmpdir(), `squisq-audio-concat-${tmpId}`);
|
|
224
|
+
await mkdir(workDir, { recursive: true });
|
|
225
|
+
try {
|
|
226
|
+
const segmentPaths = [];
|
|
227
|
+
for (let i = 0; i < buffers.length; i++) {
|
|
228
|
+
const segPath = join(workDir, `seg-${i}.mp3`);
|
|
229
|
+
await writeFile(segPath, new Uint8Array(buffers[i]));
|
|
230
|
+
segmentPaths.push(segPath);
|
|
231
|
+
}
|
|
232
|
+
const listPath = join(workDir, 'concat-list.txt');
|
|
233
|
+
const listContent = segmentPaths.map((p) => `file '${p.replace(/\\/g, '/')}'`).join('\n');
|
|
234
|
+
await writeFile(listPath, listContent);
|
|
235
|
+
const outputPath = join(workDir, 'combined.mp3');
|
|
236
|
+
await new Promise((resolve, reject) => {
|
|
237
|
+
execFile(ffmpegPath, ['-y', '-f', 'concat', '-safe', '0', '-i', listPath, '-c', 'copy', outputPath], { timeout: 120000 }, (err) => {
|
|
238
|
+
if (err)
|
|
239
|
+
reject(new Error(`ffmpeg audio concat failed: ${err.message}`));
|
|
240
|
+
else
|
|
241
|
+
resolve();
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
const data = await readFile(outputPath);
|
|
245
|
+
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
246
|
+
}
|
|
247
|
+
finally {
|
|
248
|
+
await rm(workDir, { recursive: true, force: true });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Add silence at the start of an audio track by re-encoding with adelay filter.
|
|
253
|
+
*/
|
|
254
|
+
async function addAudioDelay(ffmpegPath, audioData, delaySecs) {
|
|
255
|
+
const { writeFile, readFile, rm } = await import('node:fs/promises');
|
|
256
|
+
const { join } = await import('node:path');
|
|
257
|
+
const { tmpdir } = await import('node:os');
|
|
258
|
+
const { randomBytes } = await import('node:crypto');
|
|
259
|
+
const { execFile } = await import('node:child_process');
|
|
260
|
+
const tmpId = randomBytes(8).toString('hex');
|
|
261
|
+
const inputPath = join(tmpdir(), `squisq-audio-delay-in-${tmpId}.mp3`);
|
|
262
|
+
const outputPath = join(tmpdir(), `squisq-audio-delay-out-${tmpId}.mp3`);
|
|
263
|
+
try {
|
|
264
|
+
await writeFile(inputPath, audioData);
|
|
265
|
+
const delayMs = Math.round(delaySecs * 1000);
|
|
266
|
+
await new Promise((resolve, reject) => {
|
|
267
|
+
execFile(ffmpegPath, [
|
|
268
|
+
'-y',
|
|
269
|
+
'-i',
|
|
270
|
+
inputPath,
|
|
271
|
+
'-af',
|
|
272
|
+
`adelay=${delayMs}|${delayMs}`,
|
|
273
|
+
'-c:a',
|
|
274
|
+
'libmp3lame',
|
|
275
|
+
'-b:a',
|
|
276
|
+
'128k',
|
|
277
|
+
outputPath,
|
|
278
|
+
], { timeout: 60000 }, (err) => {
|
|
279
|
+
if (err)
|
|
280
|
+
reject(new Error(`ffmpeg audio delay failed: ${err.message}`));
|
|
281
|
+
else
|
|
282
|
+
resolve();
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
const data = await readFile(outputPath);
|
|
286
|
+
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
287
|
+
}
|
|
288
|
+
finally {
|
|
289
|
+
await rm(inputPath, { force: true });
|
|
290
|
+
await rm(outputPath, { force: true });
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Extract thumbnail images from the first frame of an MP4 video.
|
|
295
|
+
* Produces JPEG files at each specified size using FFmpeg video filters.
|
|
296
|
+
*/
|
|
297
|
+
export async function extractThumbnails(options) {
|
|
298
|
+
const { videoPath, outputDir, slug, sizes, force } = options;
|
|
299
|
+
const { existsSync } = await import('node:fs');
|
|
300
|
+
const { join } = await import('node:path');
|
|
301
|
+
const { execFile } = await import('node:child_process');
|
|
302
|
+
const ffmpegPath = await detectFfmpeg();
|
|
303
|
+
if (!ffmpegPath) {
|
|
304
|
+
throw new Error('ffmpeg is required for thumbnail extraction but not found in PATH.\n' +
|
|
305
|
+
'Install it with:\n' +
|
|
306
|
+
' macOS: brew install ffmpeg\n' +
|
|
307
|
+
' Ubuntu: sudo apt install ffmpeg\n' +
|
|
308
|
+
' Windows: winget install ffmpeg');
|
|
309
|
+
}
|
|
310
|
+
for (const thumb of sizes) {
|
|
311
|
+
const outputPath = join(outputDir, `${slug}-${thumb.width}x${thumb.height}.jpg`);
|
|
312
|
+
if (!force && existsSync(outputPath))
|
|
313
|
+
continue;
|
|
314
|
+
await new Promise((resolve, reject) => {
|
|
315
|
+
execFile(ffmpegPath, ['-y', '-i', videoPath, '-vf', thumb.filter, '-frames:v', '1', '-q:v', '2', outputPath], { timeout: 30000 }, (err) => {
|
|
316
|
+
if (err)
|
|
317
|
+
reject(new Error(`Thumbnail extraction failed (${thumb.name}): ${err.message}`));
|
|
318
|
+
else
|
|
319
|
+
resolve();
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
//# sourceMappingURL=api.js.map
|
package/dist/api.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
|