@scenar/cli 0.1.6 → 0.1.8
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/package.json +22 -6
- package/src/__tests__/load-bundle.test.ts +56 -0
- package/src/__tests__/render-command.test.ts +77 -0
- package/src/commands/render.d.ts +3 -0
- package/src/commands/render.d.ts.map +1 -0
- package/src/commands/render.js +169 -0
- package/src/commands/render.js.map +1 -0
- package/src/commands/render.ts +225 -0
- package/src/index.d.ts.map +1 -1
- package/src/index.js +3 -1
- package/src/index.js.map +1 -1
- package/src/index.ts +3 -1
- package/src/util/load-bundle.d.ts +27 -0
- package/src/util/load-bundle.d.ts.map +1 -0
- package/src/util/load-bundle.js +30 -0
- package/src/util/load-bundle.js.map +1 -0
- package/src/util/load-bundle.ts +45 -0
- package/tsconfig.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scenar/cli",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Scenar CLI — validate
|
|
3
|
+
"version": "0.1.8",
|
|
4
|
+
"description": "Scenar CLI — validate scenarios, generate narration, and render videos.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"repository": {
|
|
@@ -16,7 +16,10 @@
|
|
|
16
16
|
"narration",
|
|
17
17
|
"tts",
|
|
18
18
|
"validate",
|
|
19
|
-
"yaml"
|
|
19
|
+
"yaml",
|
|
20
|
+
"video",
|
|
21
|
+
"render",
|
|
22
|
+
"remotion"
|
|
20
23
|
],
|
|
21
24
|
"bin": {
|
|
22
25
|
"scenar": "./bin/scenar.js"
|
|
@@ -24,14 +27,18 @@
|
|
|
24
27
|
"main": "./src/index.js",
|
|
25
28
|
"types": "./src/index.d.ts",
|
|
26
29
|
"dependencies": {
|
|
27
|
-
"@scenar/
|
|
28
|
-
"@scenar/
|
|
30
|
+
"@scenar/core": "0.1.8",
|
|
31
|
+
"@scenar/preview": "0.1.8",
|
|
32
|
+
"@scenar/sdk": "0.1.8",
|
|
29
33
|
"commander": "^13.0.0",
|
|
30
34
|
"yaml": "^2.7.0"
|
|
31
35
|
},
|
|
32
36
|
"peerDependencies": {
|
|
33
37
|
"echogarden": ">=2.0.0",
|
|
34
|
-
"edge-tts-universal": ">=1.0.0"
|
|
38
|
+
"edge-tts-universal": ">=1.0.0",
|
|
39
|
+
"remotion": "4.0.448",
|
|
40
|
+
"@remotion/renderer": "4.0.448",
|
|
41
|
+
"@remotion/bundler": "4.0.448"
|
|
35
42
|
},
|
|
36
43
|
"peerDependenciesMeta": {
|
|
37
44
|
"echogarden": {
|
|
@@ -39,6 +46,15 @@
|
|
|
39
46
|
},
|
|
40
47
|
"edge-tts-universal": {
|
|
41
48
|
"optional": true
|
|
49
|
+
},
|
|
50
|
+
"remotion": {
|
|
51
|
+
"optional": true
|
|
52
|
+
},
|
|
53
|
+
"@remotion/renderer": {
|
|
54
|
+
"optional": true
|
|
55
|
+
},
|
|
56
|
+
"@remotion/bundler": {
|
|
57
|
+
"optional": true
|
|
42
58
|
}
|
|
43
59
|
}
|
|
44
60
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { join, basename } from "node:path";
|
|
3
|
+
import { loadBundle } from "../util/load-bundle.js";
|
|
4
|
+
import * as loadTs from "../util/load-ts.js";
|
|
5
|
+
import * as fs from "node:fs/promises";
|
|
6
|
+
|
|
7
|
+
vi.mock("../util/load-ts.js");
|
|
8
|
+
vi.mock("node:fs/promises");
|
|
9
|
+
|
|
10
|
+
describe("loadBundle", () => {
|
|
11
|
+
const mockSteps = [
|
|
12
|
+
{ delayMs: 0, narration: "Step one narration" },
|
|
13
|
+
{ delayMs: 2000, narration: "Step two narration" },
|
|
14
|
+
{ delayMs: 1500 },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const mockManifest = {
|
|
18
|
+
steps: [
|
|
19
|
+
{ src: "/audio/step-0.mp3", durationMs: 3000 },
|
|
20
|
+
{ src: "/audio/step-1.mp3", durationMs: 2500 },
|
|
21
|
+
null,
|
|
22
|
+
],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
it("loads steps and narration manifest from a directory", async () => {
|
|
26
|
+
vi.mocked(loadTs.loadStepsFromTs).mockResolvedValue(mockSteps);
|
|
27
|
+
vi.mocked(fs.access).mockResolvedValue(undefined);
|
|
28
|
+
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockManifest) as never);
|
|
29
|
+
|
|
30
|
+
const bundle = await loadBundle("/fake/scenarios/quickstart-tour");
|
|
31
|
+
|
|
32
|
+
expect(bundle.id).toBe("quickstart-tour");
|
|
33
|
+
expect(bundle.steps).toHaveLength(3);
|
|
34
|
+
expect(bundle.steps[0]!.delayMs).toBe(0);
|
|
35
|
+
expect(bundle.narrationManifest).toBeDefined();
|
|
36
|
+
expect(bundle.narrationManifest!.steps).toHaveLength(3);
|
|
37
|
+
expect(bundle.narrationManifest!.steps[2]).toBeNull();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("uses the directory basename as the bundle id", async () => {
|
|
41
|
+
vi.mocked(loadTs.loadStepsFromTs).mockResolvedValue(mockSteps);
|
|
42
|
+
vi.mocked(fs.access).mockResolvedValue(undefined);
|
|
43
|
+
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockManifest) as never);
|
|
44
|
+
|
|
45
|
+
const bundle = await loadBundle("/some/path/my-demo");
|
|
46
|
+
expect(bundle.id).toBe("my-demo");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("returns undefined manifest when no manifest.json exists", async () => {
|
|
50
|
+
vi.mocked(loadTs.loadStepsFromTs).mockResolvedValue(mockSteps);
|
|
51
|
+
vi.mocked(fs.access).mockRejectedValue(new Error("ENOENT"));
|
|
52
|
+
|
|
53
|
+
const bundle = await loadBundle("/fake/scenarios/no-audio-demo");
|
|
54
|
+
expect(bundle.narrationManifest).toBeUndefined();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { createProgram } from "../index.js";
|
|
3
|
+
|
|
4
|
+
describe("scenar render", () => {
|
|
5
|
+
let stderrOutput: string;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
stderrOutput = "";
|
|
9
|
+
vi.spyOn(process.stderr, "write").mockImplementation((chunk) => {
|
|
10
|
+
stderrOutput += String(chunk);
|
|
11
|
+
return true;
|
|
12
|
+
});
|
|
13
|
+
process.exitCode = undefined;
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("registers the render command with correct description", () => {
|
|
17
|
+
const program = createProgram();
|
|
18
|
+
const renderCmd = program.commands.find((c) => c.name() === "render");
|
|
19
|
+
expect(renderCmd).toBeDefined();
|
|
20
|
+
expect(renderCmd!.description()).toContain("MP4 video");
|
|
21
|
+
expect(renderCmd!.description()).toContain("Remotion");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("accepts a directory argument", () => {
|
|
25
|
+
const program = createProgram();
|
|
26
|
+
const renderCmd = program.commands.find((c) => c.name() === "render");
|
|
27
|
+
expect(renderCmd).toBeDefined();
|
|
28
|
+
expect(renderCmd!.registeredArguments).toHaveLength(1);
|
|
29
|
+
expect(renderCmd!.registeredArguments[0]!.name()).toBe("dir");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("exposes all required options", () => {
|
|
33
|
+
const program = createProgram();
|
|
34
|
+
const renderCmd = program.commands.find((c) => c.name() === "render");
|
|
35
|
+
expect(renderCmd).toBeDefined();
|
|
36
|
+
|
|
37
|
+
const optionNames = renderCmd!.options.map((o) => o.long);
|
|
38
|
+
expect(optionNames).toContain("--out");
|
|
39
|
+
expect(optionNames).toContain("--fps");
|
|
40
|
+
expect(optionNames).toContain("--width");
|
|
41
|
+
expect(optionNames).toContain("--height");
|
|
42
|
+
expect(optionNames).toContain("--entry");
|
|
43
|
+
expect(optionNames).toContain("--composition-id");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("defaults fps to 30, width to 1920, height to 1080", () => {
|
|
47
|
+
const program = createProgram();
|
|
48
|
+
const renderCmd = program.commands.find((c) => c.name() === "render");
|
|
49
|
+
expect(renderCmd).toBeDefined();
|
|
50
|
+
|
|
51
|
+
const getDefault = (long: string) =>
|
|
52
|
+
renderCmd!.options.find((o) => o.long === long)?.defaultValue;
|
|
53
|
+
|
|
54
|
+
expect(getDefault("--fps")).toBe("30");
|
|
55
|
+
expect(getDefault("--width")).toBe("1920");
|
|
56
|
+
expect(getDefault("--height")).toBe("1080");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("sets exit code 1 when given a nonexistent path", async () => {
|
|
60
|
+
const program = createProgram();
|
|
61
|
+
program.exitOverride();
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
await program.parseAsync([
|
|
65
|
+
"node",
|
|
66
|
+
"scenar",
|
|
67
|
+
"render",
|
|
68
|
+
"/tmp/__nonexistent_scenar_test_path__",
|
|
69
|
+
]);
|
|
70
|
+
} catch {
|
|
71
|
+
// Commander may throw on exitOverride.
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
expect(process.exitCode).toBe(1);
|
|
75
|
+
expect(stderrOutput).toContain("Error");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../../src/commands/render.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAYpC,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA0E5D"}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { resolve, join } from "node:path";
|
|
2
|
+
import { stat, mkdir, access } from "node:fs/promises";
|
|
3
|
+
import { loadBundle } from "../util/load-bundle.js";
|
|
4
|
+
export function registerRenderCommand(program) {
|
|
5
|
+
program
|
|
6
|
+
.command("render")
|
|
7
|
+
.description("Render a scenario as an MP4 video using Remotion.\n\n" +
|
|
8
|
+
"Accepts a scenario directory containing steps.ts and an optional\n" +
|
|
9
|
+
"narration/ subfolder with manifest.json + audio clips.\n\n" +
|
|
10
|
+
"Output defaults to ./<scenario-id>.mp4 in the current working\n" +
|
|
11
|
+
"directory. Use --out to write to a different path.")
|
|
12
|
+
.argument("<dir>", "path to a scenario directory (must contain steps.ts)")
|
|
13
|
+
.option("--out <path>", "output file path or directory for the MP4")
|
|
14
|
+
.option("--fps <number>", "frames per second (default: 30)", "30")
|
|
15
|
+
.option("--width <number>", "video width in pixels (default: 1920)", "1920")
|
|
16
|
+
.option("--height <number>", "video height in pixels (default: 1080)", "1080")
|
|
17
|
+
.option("--composition-id <id>", "Remotion composition ID to render")
|
|
18
|
+
.option("--entry <path>", "path to a Remotion entry file (Root.tsx)")
|
|
19
|
+
.action(async (dir, options) => {
|
|
20
|
+
const resolved = resolve(dir);
|
|
21
|
+
let info;
|
|
22
|
+
try {
|
|
23
|
+
info = await stat(resolved);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
process.stderr.write(`\x1b[31mError:\x1b[0m ${dir} does not exist.\n`);
|
|
27
|
+
process.exitCode = 1;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (!info.isDirectory()) {
|
|
31
|
+
process.stderr.write(`\x1b[31mError:\x1b[0m ${dir} is not a directory.\n` +
|
|
32
|
+
"The render command requires a scenario directory (with steps.ts).\n");
|
|
33
|
+
process.exitCode = 1;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const fps = Number(options.fps) || 30;
|
|
37
|
+
const width = Number(options.width) || 1920;
|
|
38
|
+
const height = Number(options.height) || 1080;
|
|
39
|
+
try {
|
|
40
|
+
const bundle = await loadBundle(resolved);
|
|
41
|
+
const scenarioId = bundle.id;
|
|
42
|
+
const outputPath = resolveOutputPath(options.out, scenarioId);
|
|
43
|
+
const entryPoint = await resolveEntryPoint(options.entry);
|
|
44
|
+
process.stderr.write(`Scenario: ${scenarioId}\n`);
|
|
45
|
+
process.stderr.write(`Steps: ${bundle.steps.length}\n`);
|
|
46
|
+
process.stderr.write(`Audio: ${bundle.narrationManifest ? "yes (manifest found)" : "none"}\n`);
|
|
47
|
+
process.stderr.write(`Entry: ${entryPoint}\n`);
|
|
48
|
+
process.stderr.write(`Output: ${outputPath}\n`);
|
|
49
|
+
process.stderr.write(`Config: ${width}x${height} @ ${fps}fps\n\n`);
|
|
50
|
+
await renderScenario({
|
|
51
|
+
bundle,
|
|
52
|
+
entryPoint,
|
|
53
|
+
compositionId: options.compositionId ?? scenarioId,
|
|
54
|
+
outputPath,
|
|
55
|
+
fps,
|
|
56
|
+
width,
|
|
57
|
+
height,
|
|
58
|
+
});
|
|
59
|
+
process.stderr.write(`\n\x1b[32m✓\x1b[0m Video saved to ${outputPath}\n`);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
63
|
+
process.stderr.write(`\x1b[31mError:\x1b[0m ${msg}\n`);
|
|
64
|
+
process.exitCode = 1;
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Output path resolution
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
function resolveOutputPath(outOption, scenarioId) {
|
|
72
|
+
if (!outOption) {
|
|
73
|
+
return resolve(`./${scenarioId}.mp4`);
|
|
74
|
+
}
|
|
75
|
+
if (outOption.endsWith(".mp4")) {
|
|
76
|
+
return resolve(outOption);
|
|
77
|
+
}
|
|
78
|
+
return resolve(join(outOption, `${scenarioId}.mp4`));
|
|
79
|
+
}
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Entry point resolution
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
async function resolveEntryPoint(entryOption) {
|
|
84
|
+
if (entryOption) {
|
|
85
|
+
const resolved = resolve(entryOption);
|
|
86
|
+
try {
|
|
87
|
+
await access(resolved);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
throw new Error(`Remotion entry point not found: ${entryOption}`);
|
|
91
|
+
}
|
|
92
|
+
return resolved;
|
|
93
|
+
}
|
|
94
|
+
const candidates = [
|
|
95
|
+
resolve("remotion/index.tsx"),
|
|
96
|
+
resolve("remotion/index.ts"),
|
|
97
|
+
resolve("src/remotion/index.tsx"),
|
|
98
|
+
resolve("src/remotion/index.ts"),
|
|
99
|
+
];
|
|
100
|
+
for (const candidate of candidates) {
|
|
101
|
+
try {
|
|
102
|
+
await access(candidate);
|
|
103
|
+
return candidate;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// Try next.
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
throw new Error("No Remotion entry point found.\n\n" +
|
|
110
|
+
"Create a remotion/index.tsx that exports your Remotion Root component,\n" +
|
|
111
|
+
"or use --entry to specify a custom path.\n\n" +
|
|
112
|
+
"Example remotion/index.tsx:\n\n" +
|
|
113
|
+
' import { Composition } from "remotion";\n' +
|
|
114
|
+
' import { ScenarioComposition, calculateScenarioTimeline } from "@scenar/remotion";\n' +
|
|
115
|
+
" import { myBundle } from \"../scenarios/my-demo/bundle\";\n\n" +
|
|
116
|
+
" export const RemotionRoot = () => (\n" +
|
|
117
|
+
" <Composition\n" +
|
|
118
|
+
' id="my-demo"\n' +
|
|
119
|
+
" component={() => (\n" +
|
|
120
|
+
" <ScenarioComposition bundle={myBundle}>\n" +
|
|
121
|
+
" {(data) => <MyView data={data} />}\n" +
|
|
122
|
+
" </ScenarioComposition>\n" +
|
|
123
|
+
" )}\n" +
|
|
124
|
+
" fps={30}\n" +
|
|
125
|
+
" width={1920}\n" +
|
|
126
|
+
" height={1080}\n" +
|
|
127
|
+
" durationInFrames={\n" +
|
|
128
|
+
" calculateScenarioTimeline(myBundle.steps, myBundle.narrationManifest, 30)\n" +
|
|
129
|
+
" .durationInFrames\n" +
|
|
130
|
+
" }\n" +
|
|
131
|
+
" />\n" +
|
|
132
|
+
" );\n");
|
|
133
|
+
}
|
|
134
|
+
async function renderScenario(config) {
|
|
135
|
+
// Dynamic imports — these are optional peer deps.
|
|
136
|
+
const bundler = await import("@remotion/bundler").catch(() => {
|
|
137
|
+
throw new Error("Could not load @remotion/bundler.\n" +
|
|
138
|
+
"Install it: pnpm add @remotion/bundler@4.0.448");
|
|
139
|
+
});
|
|
140
|
+
const renderer = await import("@remotion/renderer").catch(() => {
|
|
141
|
+
throw new Error("Could not load @remotion/renderer.\n" +
|
|
142
|
+
"Install it: pnpm add @remotion/renderer@4.0.448");
|
|
143
|
+
});
|
|
144
|
+
process.stderr.write("Bundling Remotion project...\n");
|
|
145
|
+
const serveUrl = await bundler.bundle({ entryPoint: config.entryPoint });
|
|
146
|
+
process.stderr.write(`Selecting composition: ${config.compositionId}\n`);
|
|
147
|
+
const composition = await renderer.selectComposition({
|
|
148
|
+
serveUrl,
|
|
149
|
+
id: config.compositionId,
|
|
150
|
+
});
|
|
151
|
+
process.stderr.write(`Composition: ${composition.width}x${composition.height} @ ${composition.fps}fps, ` +
|
|
152
|
+
`${composition.durationInFrames} frames ` +
|
|
153
|
+
`(${(composition.durationInFrames / composition.fps).toFixed(1)}s)\n`);
|
|
154
|
+
const outDir = resolve(config.outputPath, "..");
|
|
155
|
+
await mkdir(outDir, { recursive: true });
|
|
156
|
+
process.stderr.write("Rendering...\n");
|
|
157
|
+
await renderer.renderMedia({
|
|
158
|
+
composition,
|
|
159
|
+
serveUrl,
|
|
160
|
+
codec: "h264",
|
|
161
|
+
outputLocation: config.outputPath,
|
|
162
|
+
onProgress: ({ progress }) => {
|
|
163
|
+
const pct = Math.round(progress * 100);
|
|
164
|
+
process.stderr.write(`\r Progress: ${pct}%`);
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
process.stderr.write("\n");
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=render.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.js","sourceRoot":"","sources":["../../../src/commands/render.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { resolve, join } from "node:path";
|
|
2
|
+
import { stat, mkdir, access } from "node:fs/promises";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { loadBundle, type CliBundle } from "../util/load-bundle.js";
|
|
5
|
+
|
|
6
|
+
interface RenderOptions {
|
|
7
|
+
out?: string;
|
|
8
|
+
fps?: string;
|
|
9
|
+
width?: string;
|
|
10
|
+
height?: string;
|
|
11
|
+
compositionId?: string;
|
|
12
|
+
entry?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function registerRenderCommand(program: Command): void {
|
|
16
|
+
program
|
|
17
|
+
.command("render")
|
|
18
|
+
.description(
|
|
19
|
+
"Render a scenario as an MP4 video using Remotion.\n\n" +
|
|
20
|
+
"Accepts a scenario directory containing steps.ts and an optional\n" +
|
|
21
|
+
"narration/ subfolder with manifest.json + audio clips.\n\n" +
|
|
22
|
+
"Output defaults to ./<scenario-id>.mp4 in the current working\n" +
|
|
23
|
+
"directory. Use --out to write to a different path.",
|
|
24
|
+
)
|
|
25
|
+
.argument("<dir>", "path to a scenario directory (must contain steps.ts)")
|
|
26
|
+
.option("--out <path>", "output file path or directory for the MP4")
|
|
27
|
+
.option("--fps <number>", "frames per second (default: 30)", "30")
|
|
28
|
+
.option("--width <number>", "video width in pixels (default: 1920)", "1920")
|
|
29
|
+
.option("--height <number>", "video height in pixels (default: 1080)", "1080")
|
|
30
|
+
.option("--composition-id <id>", "Remotion composition ID to render")
|
|
31
|
+
.option("--entry <path>", "path to a Remotion entry file (Root.tsx)")
|
|
32
|
+
.action(async (dir: string, options: RenderOptions) => {
|
|
33
|
+
const resolved = resolve(dir);
|
|
34
|
+
|
|
35
|
+
let info;
|
|
36
|
+
try {
|
|
37
|
+
info = await stat(resolved);
|
|
38
|
+
} catch {
|
|
39
|
+
process.stderr.write(`\x1b[31mError:\x1b[0m ${dir} does not exist.\n`);
|
|
40
|
+
process.exitCode = 1;
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!info.isDirectory()) {
|
|
45
|
+
process.stderr.write(
|
|
46
|
+
`\x1b[31mError:\x1b[0m ${dir} is not a directory.\n` +
|
|
47
|
+
"The render command requires a scenario directory (with steps.ts).\n",
|
|
48
|
+
);
|
|
49
|
+
process.exitCode = 1;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const fps = Number(options.fps) || 30;
|
|
54
|
+
const width = Number(options.width) || 1920;
|
|
55
|
+
const height = Number(options.height) || 1080;
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const bundle = await loadBundle(resolved);
|
|
59
|
+
const scenarioId = bundle.id;
|
|
60
|
+
const outputPath = resolveOutputPath(options.out, scenarioId);
|
|
61
|
+
const entryPoint = await resolveEntryPoint(options.entry);
|
|
62
|
+
|
|
63
|
+
process.stderr.write(`Scenario: ${scenarioId}\n`);
|
|
64
|
+
process.stderr.write(`Steps: ${bundle.steps.length}\n`);
|
|
65
|
+
process.stderr.write(
|
|
66
|
+
`Audio: ${bundle.narrationManifest ? "yes (manifest found)" : "none"}\n`,
|
|
67
|
+
);
|
|
68
|
+
process.stderr.write(`Entry: ${entryPoint}\n`);
|
|
69
|
+
process.stderr.write(`Output: ${outputPath}\n`);
|
|
70
|
+
process.stderr.write(`Config: ${width}x${height} @ ${fps}fps\n\n`);
|
|
71
|
+
|
|
72
|
+
await renderScenario({
|
|
73
|
+
bundle,
|
|
74
|
+
entryPoint,
|
|
75
|
+
compositionId: options.compositionId ?? scenarioId,
|
|
76
|
+
outputPath,
|
|
77
|
+
fps,
|
|
78
|
+
width,
|
|
79
|
+
height,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
process.stderr.write(`\n\x1b[32m✓\x1b[0m Video saved to ${outputPath}\n`);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
85
|
+
process.stderr.write(`\x1b[31mError:\x1b[0m ${msg}\n`);
|
|
86
|
+
process.exitCode = 1;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Output path resolution
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
function resolveOutputPath(outOption: string | undefined, scenarioId: string): string {
|
|
96
|
+
if (!outOption) {
|
|
97
|
+
return resolve(`./${scenarioId}.mp4`);
|
|
98
|
+
}
|
|
99
|
+
if (outOption.endsWith(".mp4")) {
|
|
100
|
+
return resolve(outOption);
|
|
101
|
+
}
|
|
102
|
+
return resolve(join(outOption, `${scenarioId}.mp4`));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// Entry point resolution
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
async function resolveEntryPoint(entryOption: string | undefined): Promise<string> {
|
|
110
|
+
if (entryOption) {
|
|
111
|
+
const resolved = resolve(entryOption);
|
|
112
|
+
try {
|
|
113
|
+
await access(resolved);
|
|
114
|
+
} catch {
|
|
115
|
+
throw new Error(`Remotion entry point not found: ${entryOption}`);
|
|
116
|
+
}
|
|
117
|
+
return resolved;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const candidates = [
|
|
121
|
+
resolve("remotion/index.tsx"),
|
|
122
|
+
resolve("remotion/index.ts"),
|
|
123
|
+
resolve("src/remotion/index.tsx"),
|
|
124
|
+
resolve("src/remotion/index.ts"),
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
for (const candidate of candidates) {
|
|
128
|
+
try {
|
|
129
|
+
await access(candidate);
|
|
130
|
+
return candidate;
|
|
131
|
+
} catch {
|
|
132
|
+
// Try next.
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
throw new Error(
|
|
137
|
+
"No Remotion entry point found.\n\n" +
|
|
138
|
+
"Create a remotion/index.tsx that exports your Remotion Root component,\n" +
|
|
139
|
+
"or use --entry to specify a custom path.\n\n" +
|
|
140
|
+
"Example remotion/index.tsx:\n\n" +
|
|
141
|
+
' import { Composition } from "remotion";\n' +
|
|
142
|
+
' import { ScenarioComposition, calculateScenarioTimeline } from "@scenar/remotion";\n' +
|
|
143
|
+
" import { myBundle } from \"../scenarios/my-demo/bundle\";\n\n" +
|
|
144
|
+
" export const RemotionRoot = () => (\n" +
|
|
145
|
+
" <Composition\n" +
|
|
146
|
+
' id="my-demo"\n' +
|
|
147
|
+
" component={() => (\n" +
|
|
148
|
+
" <ScenarioComposition bundle={myBundle}>\n" +
|
|
149
|
+
" {(data) => <MyView data={data} />}\n" +
|
|
150
|
+
" </ScenarioComposition>\n" +
|
|
151
|
+
" )}\n" +
|
|
152
|
+
" fps={30}\n" +
|
|
153
|
+
" width={1920}\n" +
|
|
154
|
+
" height={1080}\n" +
|
|
155
|
+
" durationInFrames={\n" +
|
|
156
|
+
" calculateScenarioTimeline(myBundle.steps, myBundle.narrationManifest, 30)\n" +
|
|
157
|
+
" .durationInFrames\n" +
|
|
158
|
+
" }\n" +
|
|
159
|
+
" />\n" +
|
|
160
|
+
" );\n",
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
// Render orchestration
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
interface RenderConfig {
|
|
169
|
+
bundle: CliBundle;
|
|
170
|
+
entryPoint: string;
|
|
171
|
+
compositionId: string;
|
|
172
|
+
outputPath: string;
|
|
173
|
+
fps: number;
|
|
174
|
+
width: number;
|
|
175
|
+
height: number;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function renderScenario(config: RenderConfig): Promise<void> {
|
|
179
|
+
// Dynamic imports — these are optional peer deps.
|
|
180
|
+
const bundler = await import("@remotion/bundler").catch(() => {
|
|
181
|
+
throw new Error(
|
|
182
|
+
"Could not load @remotion/bundler.\n" +
|
|
183
|
+
"Install it: pnpm add @remotion/bundler@4.0.448",
|
|
184
|
+
);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const renderer = await import("@remotion/renderer").catch(() => {
|
|
188
|
+
throw new Error(
|
|
189
|
+
"Could not load @remotion/renderer.\n" +
|
|
190
|
+
"Install it: pnpm add @remotion/renderer@4.0.448",
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
process.stderr.write("Bundling Remotion project...\n");
|
|
195
|
+
const serveUrl = await bundler.bundle({ entryPoint: config.entryPoint });
|
|
196
|
+
|
|
197
|
+
process.stderr.write(`Selecting composition: ${config.compositionId}\n`);
|
|
198
|
+
const composition = await renderer.selectComposition({
|
|
199
|
+
serveUrl,
|
|
200
|
+
id: config.compositionId,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
process.stderr.write(
|
|
204
|
+
`Composition: ${composition.width}x${composition.height} @ ${composition.fps}fps, ` +
|
|
205
|
+
`${composition.durationInFrames} frames ` +
|
|
206
|
+
`(${(composition.durationInFrames / composition.fps).toFixed(1)}s)\n`,
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
const outDir = resolve(config.outputPath, "..");
|
|
210
|
+
await mkdir(outDir, { recursive: true });
|
|
211
|
+
|
|
212
|
+
process.stderr.write("Rendering...\n");
|
|
213
|
+
await renderer.renderMedia({
|
|
214
|
+
composition,
|
|
215
|
+
serveUrl,
|
|
216
|
+
codec: "h264" as const,
|
|
217
|
+
outputLocation: config.outputPath,
|
|
218
|
+
onProgress: ({ progress }) => {
|
|
219
|
+
const pct = Math.round(progress * 100);
|
|
220
|
+
process.stderr.write(`\r Progress: ${pct}%`);
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
process.stderr.write("\n");
|
|
225
|
+
}
|
package/src/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAMpC,wBAAgB,aAAa,IAAI,OAAO,CAcvC;AAED,wBAAgB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CAGxC"}
|
package/src/index.js
CHANGED
|
@@ -2,14 +2,16 @@ import { Command } from "commander";
|
|
|
2
2
|
import { registerValidateCommand } from "./commands/validate.js";
|
|
3
3
|
import { registerNarrateCommand } from "./commands/narrate.js";
|
|
4
4
|
import { registerPreviewCommand } from "./commands/preview.js";
|
|
5
|
+
import { registerRenderCommand } from "./commands/render.js";
|
|
5
6
|
export function createProgram() {
|
|
6
7
|
const program = new Command();
|
|
7
8
|
program
|
|
8
9
|
.name("scenar")
|
|
9
|
-
.description("Scenar CLI — validate scenarios, generate narration, and manage previews.")
|
|
10
|
+
.description("Scenar CLI — validate scenarios, generate narration, render videos, and manage previews.")
|
|
10
11
|
.version("0.0.1");
|
|
11
12
|
registerValidateCommand(program);
|
|
12
13
|
registerNarrateCommand(program);
|
|
14
|
+
registerRenderCommand(program);
|
|
13
15
|
registerPreviewCommand(program);
|
|
14
16
|
return program;
|
|
15
17
|
}
|
package/src/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAE7D,MAAM,UAAU,aAAa;IAC3B,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;IAE9B,OAAO;SACJ,IAAI,CAAC,QAAQ,CAAC;SACd,WAAW,CAAC,0FAA0F,CAAC;SACvG,OAAO,CAAC,OAAO,CAAC,CAAC;IAEpB,uBAAuB,CAAC,OAAO,CAAC,CAAC;IACjC,sBAAsB,CAAC,OAAO,CAAC,CAAC;IAChC,qBAAqB,CAAC,OAAO,CAAC,CAAC;IAC/B,sBAAsB,CAAC,OAAO,CAAC,CAAC;IAEhC,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,GAAG,CAAC,IAAc;IAChC,MAAM,OAAO,GAAG,aAAa,EAAE,CAAC;IAChC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;AACtB,CAAC"}
|
package/src/index.ts
CHANGED
|
@@ -2,17 +2,19 @@ import { Command } from "commander";
|
|
|
2
2
|
import { registerValidateCommand } from "./commands/validate.js";
|
|
3
3
|
import { registerNarrateCommand } from "./commands/narrate.js";
|
|
4
4
|
import { registerPreviewCommand } from "./commands/preview.js";
|
|
5
|
+
import { registerRenderCommand } from "./commands/render.js";
|
|
5
6
|
|
|
6
7
|
export function createProgram(): Command {
|
|
7
8
|
const program = new Command();
|
|
8
9
|
|
|
9
10
|
program
|
|
10
11
|
.name("scenar")
|
|
11
|
-
.description("Scenar CLI — validate scenarios, generate narration, and manage previews.")
|
|
12
|
+
.description("Scenar CLI — validate scenarios, generate narration, render videos, and manage previews.")
|
|
12
13
|
.version("0.0.1");
|
|
13
14
|
|
|
14
15
|
registerValidateCommand(program);
|
|
15
16
|
registerNarrateCommand(program);
|
|
17
|
+
registerRenderCommand(program);
|
|
16
18
|
registerPreviewCommand(program);
|
|
17
19
|
|
|
18
20
|
return program;
|