@vibeo/cli 0.1.2 → 0.2.0
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/commands/create.d.ts +6 -1
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +18 -107
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/list.d.ts +9 -5
- package/dist/commands/list.d.ts.map +1 -1
- package/dist/commands/list.js +2 -77
- package/dist/commands/list.js.map +1 -1
- package/dist/commands/preview.d.ts +1 -4
- package/dist/commands/preview.d.ts.map +1 -1
- package/dist/commands/preview.js +1 -57
- package/dist/commands/preview.js.map +1 -1
- package/dist/commands/render.d.ts +17 -4
- package/dist/commands/render.d.ts.map +1 -1
- package/dist/commands/render.js +22 -154
- package/dist/commands/render.js.map +1 -1
- package/dist/index.js +103 -57
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
- package/src/commands/create.ts +19 -122
- package/src/commands/list.ts +2 -98
- package/src/commands/preview.ts +1 -66
- package/src/commands/render.ts +26 -149
- package/src/index.ts +104 -57
package/src/commands/render.ts
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
|
-
import {
|
|
3
|
-
import { parseFrameRange } from "@vibeo/renderer";
|
|
4
|
-
import { renderComposition } from "@vibeo/renderer";
|
|
2
|
+
import { parseFrameRange, renderComposition } from "@vibeo/renderer";
|
|
5
3
|
import type { Codec, ImageFormat, RenderProgress } from "@vibeo/renderer";
|
|
6
4
|
|
|
7
|
-
interface
|
|
5
|
+
interface RenderOptions {
|
|
8
6
|
entry: string;
|
|
9
7
|
composition: string;
|
|
10
|
-
output: string |
|
|
8
|
+
output: string | undefined;
|
|
11
9
|
fps: number | null;
|
|
12
10
|
frames: string | null;
|
|
13
11
|
codec: Codec;
|
|
@@ -16,100 +14,6 @@ interface RenderArgs {
|
|
|
16
14
|
quality: number;
|
|
17
15
|
}
|
|
18
16
|
|
|
19
|
-
function parseArgs(args: string[]): RenderArgs {
|
|
20
|
-
const result: RenderArgs = {
|
|
21
|
-
entry: "",
|
|
22
|
-
composition: "",
|
|
23
|
-
output: null,
|
|
24
|
-
fps: null,
|
|
25
|
-
frames: null,
|
|
26
|
-
codec: "h264",
|
|
27
|
-
concurrency: Math.max(1, Math.floor(availableParallelism() / 2)),
|
|
28
|
-
imageFormat: "png",
|
|
29
|
-
quality: 80,
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
for (let i = 0; i < args.length; i++) {
|
|
33
|
-
const arg = args[i]!;
|
|
34
|
-
const next = args[i + 1];
|
|
35
|
-
|
|
36
|
-
if (arg === "--entry" && next) {
|
|
37
|
-
result.entry = next;
|
|
38
|
-
i++;
|
|
39
|
-
} else if (arg.startsWith("--entry=")) {
|
|
40
|
-
result.entry = arg.slice("--entry=".length);
|
|
41
|
-
} else if (arg === "--composition" && next) {
|
|
42
|
-
result.composition = next;
|
|
43
|
-
i++;
|
|
44
|
-
} else if (arg.startsWith("--composition=")) {
|
|
45
|
-
result.composition = arg.slice("--composition=".length);
|
|
46
|
-
} else if (arg === "--output" && next) {
|
|
47
|
-
result.output = next;
|
|
48
|
-
i++;
|
|
49
|
-
} else if (arg.startsWith("--output=")) {
|
|
50
|
-
result.output = arg.slice("--output=".length);
|
|
51
|
-
} else if (arg === "--fps" && next) {
|
|
52
|
-
result.fps = parseInt(next, 10);
|
|
53
|
-
i++;
|
|
54
|
-
} else if (arg.startsWith("--fps=")) {
|
|
55
|
-
result.fps = parseInt(arg.slice("--fps=".length), 10);
|
|
56
|
-
} else if (arg === "--frames" && next) {
|
|
57
|
-
result.frames = next;
|
|
58
|
-
i++;
|
|
59
|
-
} else if (arg.startsWith("--frames=")) {
|
|
60
|
-
result.frames = arg.slice("--frames=".length);
|
|
61
|
-
} else if (arg === "--codec" && next) {
|
|
62
|
-
result.codec = next as Codec;
|
|
63
|
-
i++;
|
|
64
|
-
} else if (arg.startsWith("--codec=")) {
|
|
65
|
-
result.codec = arg.slice("--codec=".length) as Codec;
|
|
66
|
-
} else if (arg === "--concurrency" && next) {
|
|
67
|
-
result.concurrency = parseInt(next, 10);
|
|
68
|
-
i++;
|
|
69
|
-
} else if (arg.startsWith("--concurrency=")) {
|
|
70
|
-
result.concurrency = parseInt(arg.slice("--concurrency=".length), 10);
|
|
71
|
-
} else if (arg === "--image-format" && next) {
|
|
72
|
-
result.imageFormat = next as ImageFormat;
|
|
73
|
-
i++;
|
|
74
|
-
} else if (arg.startsWith("--image-format=")) {
|
|
75
|
-
result.imageFormat = arg.slice("--image-format=".length) as ImageFormat;
|
|
76
|
-
} else if (arg === "--quality" && next) {
|
|
77
|
-
result.quality = parseInt(next, 10);
|
|
78
|
-
i++;
|
|
79
|
-
} else if (arg.startsWith("--quality=")) {
|
|
80
|
-
result.quality = parseInt(arg.slice("--quality=".length), 10);
|
|
81
|
-
} else if (arg === "--help" || arg === "-h") {
|
|
82
|
-
printHelp();
|
|
83
|
-
process.exit(0);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return result;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function printHelp(): void {
|
|
91
|
-
console.log(`
|
|
92
|
-
vibeo render - Render a composition to video
|
|
93
|
-
|
|
94
|
-
Usage:
|
|
95
|
-
vibeo render --entry <path> --composition <id> [options]
|
|
96
|
-
|
|
97
|
-
Required:
|
|
98
|
-
--entry <path> Path to the root file with compositions
|
|
99
|
-
--composition <id> Composition ID to render
|
|
100
|
-
|
|
101
|
-
Options:
|
|
102
|
-
--output <path> Output file path (default: out/<compositionId>.mp4)
|
|
103
|
-
--fps <number> Override fps
|
|
104
|
-
--frames <range> Frame range "start-end" (e.g., "0-100")
|
|
105
|
-
--codec <codec> h264 | h265 | vp9 | prores (default: h264)
|
|
106
|
-
--concurrency <number> Parallel browser tabs (default: cpu count / 2)
|
|
107
|
-
--image-format <format> png | jpeg (default: png)
|
|
108
|
-
--quality <number> 0-100 for jpeg quality / crf (default: 80)
|
|
109
|
-
--help Show this help
|
|
110
|
-
`);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
17
|
function formatTime(ms: number): string {
|
|
114
18
|
const seconds = Math.floor(ms / 1000);
|
|
115
19
|
const minutes = Math.floor(seconds / 60);
|
|
@@ -131,76 +35,49 @@ function renderProgressBar(progress: RenderProgress): void {
|
|
|
131
35
|
);
|
|
132
36
|
}
|
|
133
37
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
console.error("Error: --entry is required");
|
|
142
|
-
printHelp();
|
|
143
|
-
process.exit(1);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (!parsed.composition) {
|
|
147
|
-
console.error("Error: --composition is required");
|
|
148
|
-
printHelp();
|
|
149
|
-
process.exit(1);
|
|
150
|
-
}
|
|
38
|
+
export async function renderVideo(
|
|
39
|
+
opts: RenderOptions,
|
|
40
|
+
): Promise<{ output: string; elapsed: string }> {
|
|
41
|
+
const ext = opts.codec === "vp9" ? "webm" : opts.codec === "prores" ? "mov" : "mp4";
|
|
42
|
+
const output = opts.output
|
|
43
|
+
? resolve(opts.output)
|
|
44
|
+
: resolve(`out/${opts.composition}.${ext}`);
|
|
151
45
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
// TODO: In a full implementation, we would bundle the entry, extract
|
|
156
|
-
// composition metadata, and use it here. For now we require the user
|
|
157
|
-
// to have the composition info available.
|
|
158
|
-
// This is a simplified flow that demonstrates the pipeline.
|
|
159
|
-
|
|
160
|
-
const ext = parsed.codec === "vp9" ? "webm" : parsed.codec === "prores" ? "mov" : "mp4";
|
|
161
|
-
const output = parsed.output
|
|
162
|
-
? resolve(parsed.output)
|
|
163
|
-
: resolve(`out/${compositionId}.${ext}`);
|
|
164
|
-
|
|
165
|
-
console.log(`\nRendering composition "${compositionId}"`);
|
|
166
|
-
console.log(` Entry: ${entry}`);
|
|
46
|
+
console.log(`\nRendering composition "${opts.composition}"`);
|
|
47
|
+
console.log(` Entry: ${opts.entry}`);
|
|
167
48
|
console.log(` Output: ${output}`);
|
|
168
|
-
console.log(` Codec: ${
|
|
169
|
-
console.log(`
|
|
170
|
-
console.log(` Concurrency: ${parsed.concurrency}`);
|
|
49
|
+
console.log(` Codec: ${opts.codec}`);
|
|
50
|
+
console.log(` Concurrency: ${opts.concurrency}`);
|
|
171
51
|
console.log();
|
|
172
52
|
|
|
173
|
-
// For a full render, we need composition info from the bundle.
|
|
174
|
-
// This would normally be extracted by bundling and evaluating the entry.
|
|
175
|
-
// Here we set up the render config and delegate to renderComposition.
|
|
176
53
|
const compositionInfo = {
|
|
177
54
|
width: 1920,
|
|
178
55
|
height: 1080,
|
|
179
|
-
fps:
|
|
56
|
+
fps: opts.fps ?? 30,
|
|
180
57
|
durationInFrames: 300,
|
|
181
58
|
};
|
|
182
59
|
|
|
183
|
-
const frameRange = parseFrameRange(
|
|
184
|
-
|
|
60
|
+
const frameRange = parseFrameRange(opts.frames, compositionInfo.durationInFrames);
|
|
185
61
|
const startTime = Date.now();
|
|
186
62
|
|
|
187
63
|
await renderComposition(
|
|
188
64
|
{
|
|
189
|
-
entry,
|
|
190
|
-
compositionId,
|
|
65
|
+
entry: opts.entry,
|
|
66
|
+
compositionId: opts.composition,
|
|
191
67
|
outputPath: output,
|
|
192
|
-
codec:
|
|
193
|
-
imageFormat:
|
|
194
|
-
quality:
|
|
195
|
-
fps:
|
|
68
|
+
codec: opts.codec,
|
|
69
|
+
imageFormat: opts.imageFormat,
|
|
70
|
+
quality: opts.quality,
|
|
71
|
+
fps: opts.fps,
|
|
196
72
|
frameRange,
|
|
197
|
-
concurrency:
|
|
73
|
+
concurrency: opts.concurrency,
|
|
198
74
|
pixelFormat: "yuv420p",
|
|
199
75
|
onProgress: renderProgressBar,
|
|
200
76
|
},
|
|
201
77
|
compositionInfo,
|
|
202
78
|
);
|
|
203
79
|
|
|
204
|
-
const elapsed = Date.now() - startTime;
|
|
205
|
-
console.log(`\n\nDone in ${
|
|
80
|
+
const elapsed = formatTime(Date.now() - startTime);
|
|
81
|
+
console.log(`\n\nDone in ${elapsed}. Output: ${output}`);
|
|
82
|
+
return { output, elapsed };
|
|
206
83
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,63 +1,110 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { Cli, z } from "incur";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { availableParallelism } from "node:os";
|
|
4
|
+
import { createProject } from "./commands/create.js";
|
|
5
|
+
import { startPreview } from "./commands/preview.js";
|
|
6
|
+
import { listCompositions } from "./commands/list.js";
|
|
7
|
+
import { renderVideo } from "./commands/render.js";
|
|
5
8
|
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
create Create a new project from a template
|
|
18
|
-
render Render a composition to video
|
|
19
|
-
preview Start a dev server with live preview
|
|
20
|
-
list List registered compositions
|
|
21
|
-
|
|
22
|
-
Options:
|
|
23
|
-
--help Show help for a command
|
|
9
|
+
const cli = Cli.create("vibeo", {
|
|
10
|
+
description: "React-based programmatic video framework CLI",
|
|
11
|
+
sync: {
|
|
12
|
+
suggestions: [
|
|
13
|
+
"create a new video project",
|
|
14
|
+
"preview a composition in the browser",
|
|
15
|
+
"render a composition to video",
|
|
16
|
+
"list all registered compositions",
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
});
|
|
24
20
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
21
|
+
cli.command("create", {
|
|
22
|
+
description: "Create a new Vibeo project from a template",
|
|
23
|
+
args: z.object({
|
|
24
|
+
name: z.string().describe("Project directory name"),
|
|
25
|
+
}),
|
|
26
|
+
options: z.object({
|
|
27
|
+
template: z
|
|
28
|
+
.enum(["basic", "audio-reactive", "transitions", "subtitles"])
|
|
29
|
+
.default("basic")
|
|
30
|
+
.describe("Template to scaffold from"),
|
|
31
|
+
}),
|
|
32
|
+
examples: [
|
|
33
|
+
{ args: { name: "my-video" }, description: "Create with basic template" },
|
|
34
|
+
{ args: { name: "viz" }, options: { template: "audio-reactive" }, description: "Create with audio-reactive template" },
|
|
35
|
+
],
|
|
36
|
+
async run(c) {
|
|
37
|
+
return await createProject(c.args.name, c.options.template);
|
|
38
|
+
},
|
|
39
|
+
});
|
|
33
40
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
cli.command("preview", {
|
|
42
|
+
description: "Start a dev server with live preview in the browser",
|
|
43
|
+
options: z.object({
|
|
44
|
+
entry: z.string().describe("Path to the root file with compositions"),
|
|
45
|
+
port: z.number().default(3000).describe("Port for the dev server"),
|
|
46
|
+
}),
|
|
47
|
+
examples: [
|
|
48
|
+
{ options: { entry: "src/index.tsx" }, description: "Preview on default port" },
|
|
49
|
+
],
|
|
50
|
+
async run(c) {
|
|
51
|
+
await startPreview(resolve(c.options.entry), c.options.port);
|
|
52
|
+
},
|
|
53
|
+
});
|
|
39
54
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
cli.command("render", {
|
|
56
|
+
description: "Render a composition to a video file",
|
|
57
|
+
options: z.object({
|
|
58
|
+
entry: z.string().describe("Path to the root file with compositions"),
|
|
59
|
+
composition: z.string().describe("Composition ID to render"),
|
|
60
|
+
output: z.string().optional().describe("Output file path (default: out/<id>.mp4)"),
|
|
61
|
+
fps: z.number().optional().describe("Override frames per second"),
|
|
62
|
+
frames: z.string().optional().describe('Frame range, e.g. "0-100" or "50"'),
|
|
63
|
+
codec: z
|
|
64
|
+
.enum(["h264", "h265", "vp9", "prores"])
|
|
65
|
+
.default("h264")
|
|
66
|
+
.describe("Video codec"),
|
|
67
|
+
concurrency: z
|
|
68
|
+
.number()
|
|
69
|
+
.default(Math.max(1, Math.floor(availableParallelism() / 2)))
|
|
70
|
+
.describe("Number of parallel browser tabs"),
|
|
71
|
+
imageFormat: z
|
|
72
|
+
.enum(["png", "jpeg"])
|
|
73
|
+
.default("png")
|
|
74
|
+
.describe("Intermediate frame image format"),
|
|
75
|
+
quality: z.number().default(80).describe("JPEG quality / CRF value (0-100)"),
|
|
76
|
+
}),
|
|
77
|
+
examples: [
|
|
78
|
+
{ options: { entry: "src/index.tsx", composition: "MyComp" }, description: "Render with defaults" },
|
|
79
|
+
{ options: { entry: "src/index.tsx", composition: "MyComp", codec: "vp9", frames: "0-100" }, description: "Render a frame range as WebM" },
|
|
80
|
+
],
|
|
81
|
+
async run(c) {
|
|
82
|
+
return await renderVideo({
|
|
83
|
+
entry: resolve(c.options.entry),
|
|
84
|
+
composition: c.options.composition,
|
|
85
|
+
output: c.options.output,
|
|
86
|
+
fps: c.options.fps ?? null,
|
|
87
|
+
frames: c.options.frames ?? null,
|
|
88
|
+
codec: c.options.codec,
|
|
89
|
+
concurrency: c.options.concurrency,
|
|
90
|
+
imageFormat: c.options.imageFormat,
|
|
91
|
+
quality: c.options.quality,
|
|
92
|
+
});
|
|
93
|
+
},
|
|
94
|
+
});
|
|
59
95
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
96
|
+
cli.command("list", {
|
|
97
|
+
description: "List registered compositions in an entry file",
|
|
98
|
+
options: z.object({
|
|
99
|
+
entry: z.string().describe("Path to the root file with compositions"),
|
|
100
|
+
}),
|
|
101
|
+
examples: [
|
|
102
|
+
{ options: { entry: "src/index.tsx" }, description: "List all compositions" },
|
|
103
|
+
],
|
|
104
|
+
async run(c) {
|
|
105
|
+
const compositions = await listCompositions(resolve(c.options.entry));
|
|
106
|
+
return { compositions };
|
|
107
|
+
},
|
|
63
108
|
});
|
|
109
|
+
|
|
110
|
+
cli.serve();
|