@glissade/cli 0.6.1 → 0.7.0-pre.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/audioMix.js +2 -2
- package/dist/cli.js +22 -8
- package/dist/index.d.ts +13 -1
- package/dist/index.js +1 -1
- package/dist/render.js +28 -7
- package/package.json +10 -10
package/dist/audioMix.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime.js";
|
|
2
2
|
import { dirname, isAbsolute, resolve } from "node:path";
|
|
3
|
-
import "@glissade/core";
|
|
3
|
+
import { audioOffsetSamples } from "@glissade/core";
|
|
4
4
|
//#region src/audioMix.ts
|
|
5
5
|
/**
|
|
6
6
|
* Audio mixing on the CLI path (DESIGN.md §5.3): the timeline's AudioClip
|
|
@@ -78,7 +78,7 @@ function planAudioMix(clips, modulePath, duration) {
|
|
|
78
78
|
steps.push(`volume='${gainExpression(clip.gain.keys)}':eval=frame`);
|
|
79
79
|
}
|
|
80
80
|
if (clip.at > 0) {
|
|
81
|
-
const ms =
|
|
81
|
+
const ms = audioOffsetSamples(clip.at) / 48e3 * 1e3;
|
|
82
82
|
steps.push(`adelay=${ms}:all=1`);
|
|
83
83
|
}
|
|
84
84
|
steps.push("apad");
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { a as render, i as parseFrameRange } from "./render.js";
|
|
3
3
|
import { n as parseCaptionsMode } from "./captions.js";
|
|
4
4
|
//#region src/cli.ts
|
|
5
5
|
/**
|
|
@@ -50,7 +50,9 @@ const USAGE = `usage:
|
|
|
50
50
|
render options:
|
|
51
51
|
--out <path> output directory for a PNG sequence, or .mp4/.webm (needs ffmpeg). default: ./out
|
|
52
52
|
--fps <n> frames per second (default: timeline fps, else 60)
|
|
53
|
-
--range <a..b>
|
|
53
|
+
--range <a..b> integer FRAME indices to render, inclusive (default: whole timeline)
|
|
54
|
+
--frame <n> render a single frame index (a still, through the same path)
|
|
55
|
+
--format png-seq force a PNG sequence even when --out looks like a video
|
|
54
56
|
--trace <file> replay an InputTrace and bake it (machine scenes, §A.6)
|
|
55
57
|
--state <name> render one machine state's timeline linearly
|
|
56
58
|
--force downgrade a trace hash mismatch to a warning
|
|
@@ -156,13 +158,23 @@ async function main() {
|
|
|
156
158
|
process.stderr.write(`gs dev: http://localhost:${server.port}/${flags.has("record") ? " (recording UI on)" : ""}\n`);
|
|
157
159
|
return;
|
|
158
160
|
}
|
|
159
|
-
let
|
|
161
|
+
let frameRange;
|
|
160
162
|
const rangeFlag = flags.get("range");
|
|
161
|
-
if (rangeFlag) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
163
|
+
if (rangeFlag) try {
|
|
164
|
+
frameRange = parseFrameRange(rangeFlag);
|
|
165
|
+
} catch (err) {
|
|
166
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
167
|
+
}
|
|
168
|
+
const frameFlag = flags.get("frame");
|
|
169
|
+
let frame;
|
|
170
|
+
if (frameFlag !== void 0) {
|
|
171
|
+
if (!/^\d+$/.test(frameFlag)) fail(`--frame must be a non-negative integer frame index, got '${frameFlag}'`);
|
|
172
|
+
frame = Number(frameFlag);
|
|
165
173
|
}
|
|
174
|
+
const formatFlag = flags.get("format");
|
|
175
|
+
if (formatFlag !== void 0 && formatFlag !== "png-seq") fail(`--format must be 'png-seq', got '${formatFlag}'`);
|
|
176
|
+
if (flags.has("workers")) process.stderr.write("note: --workers is accepted but parallel sharding is not yet implemented; rendering single-threaded\n");
|
|
177
|
+
if (flags.has("watch")) process.stderr.write("note: --watch is not yet implemented in this release; rendering once\n");
|
|
166
178
|
const fpsFlag = flags.get("fps");
|
|
167
179
|
const started = performance.now();
|
|
168
180
|
try {
|
|
@@ -170,7 +182,9 @@ async function main() {
|
|
|
170
182
|
modulePath,
|
|
171
183
|
out: flags.get("out") ?? "out",
|
|
172
184
|
...fpsFlag ? { fps: parseInt(fpsFlag, 10) } : {},
|
|
173
|
-
...
|
|
185
|
+
...frame !== void 0 ? { frame } : {},
|
|
186
|
+
...frameRange ? { frameRange } : {},
|
|
187
|
+
...formatFlag === "png-seq" ? { format: "png-seq" } : {},
|
|
174
188
|
...flags.has("trace") ? { trace: flags.get("trace") } : {},
|
|
175
189
|
...flags.has("state") ? { state: flags.get("state") } : {},
|
|
176
190
|
...flags.has("force") ? { force: true } : {},
|
package/dist/index.d.ts
CHANGED
|
@@ -8,8 +8,14 @@ interface RenderOptions {
|
|
|
8
8
|
modulePath: string;
|
|
9
9
|
out: string;
|
|
10
10
|
fps?: number;
|
|
11
|
-
/** seconds; defaults to [0, duration] */
|
|
11
|
+
/** seconds; defaults to [0, duration] (programmatic API — the CLI uses frame indices). */
|
|
12
12
|
range?: [number, number];
|
|
13
|
+
/** inclusive FRAME indices [first, last] (§5: export APIs are frame-indexed). Wins over `range`. */
|
|
14
|
+
frameRange?: [number, number];
|
|
15
|
+
/** a single FRAME index — a still through the same path. Wins over `frameRange`/`range`. */
|
|
16
|
+
frame?: number;
|
|
17
|
+
/** force a PNG sequence even when `out` has a video extension. */
|
|
18
|
+
format?: 'png-seq';
|
|
13
19
|
/** InputTrace file: replay → bake → render (v2 §A.6 route 2). */
|
|
14
20
|
trace?: string;
|
|
15
21
|
/** Render one machine state's timeline linearly (route 3). */
|
|
@@ -29,6 +35,12 @@ interface RenderOptions {
|
|
|
29
35
|
declare class SceneModuleError extends Error {
|
|
30
36
|
constructor(modulePath: string, detail: string);
|
|
31
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Parse the CLI `--range a..b` flag as INCLUSIVE integer FRAME indices (§5:
|
|
40
|
+
* export APIs are frame-indexed; Player APIs are seconds). Decimal or malformed
|
|
41
|
+
* ranges are rejected, since a frame index is an integer.
|
|
42
|
+
*/
|
|
43
|
+
|
|
32
44
|
declare function loadSceneModule(modulePath: string): Promise<SceneModule>;
|
|
33
45
|
declare function ffmpegAvailable(): boolean;
|
|
34
46
|
declare function render(opts: RenderOptions): Promise<{
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as render, n as ffmpegAvailable, r as loadSceneModule, t as SceneModuleError } from "./render.js";
|
|
2
2
|
import { n as VideoProbeError, r as probeVideo, t as FfmpegVideoFrameSource } from "./videoSource.js";
|
|
3
3
|
import { a as planAudioMix, i as gainExpression, n as atempoChain, o as resolveAssetPath, t as AudioMixError } from "./audioMix.js";
|
|
4
4
|
import { a as pickEncoder, i as parseEncoderList, n as availableEncoders, t as NoEncoderError } from "./encoders.js";
|
package/dist/render.js
CHANGED
|
@@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
6
|
import { createJiti } from "jiti";
|
|
7
|
-
import { evaluate } from "@glissade/scene";
|
|
7
|
+
import { evaluate, withDeterminismGuards } from "@glissade/scene";
|
|
8
8
|
import { SkiaBackend } from "@glissade/backend-skia";
|
|
9
9
|
//#region src/render.ts
|
|
10
10
|
/**
|
|
@@ -18,6 +18,19 @@ var SceneModuleError = class extends Error {
|
|
|
18
18
|
this.name = "SceneModuleError";
|
|
19
19
|
}
|
|
20
20
|
};
|
|
21
|
+
/**
|
|
22
|
+
* Parse the CLI `--range a..b` flag as INCLUSIVE integer FRAME indices (§5:
|
|
23
|
+
* export APIs are frame-indexed; Player APIs are seconds). Decimal or malformed
|
|
24
|
+
* ranges are rejected, since a frame index is an integer.
|
|
25
|
+
*/
|
|
26
|
+
function parseFrameRange(flag) {
|
|
27
|
+
const m = /^(\d+)\.\.(\d+)$/.exec(flag.trim());
|
|
28
|
+
if (!m) throw new Error(`--range must be integer frames 'a..b' (e.g. 0..120), got '${flag}'`);
|
|
29
|
+
const a = Number(m[1]);
|
|
30
|
+
const b = Number(m[2]);
|
|
31
|
+
if (b < a) throw new Error(`--range end (${b}) is before start (${a})`);
|
|
32
|
+
return [a, b];
|
|
33
|
+
}
|
|
21
34
|
async function loadSceneModule(modulePath) {
|
|
22
35
|
const abs = isAbsolute(modulePath) ? modulePath : resolve(process.cwd(), modulePath);
|
|
23
36
|
const loaded = await createJiti(pathToFileURL(process.cwd() + "/").href).import(pathToFileURL(abs).href, { default: true });
|
|
@@ -43,11 +56,19 @@ async function render(opts) {
|
|
|
43
56
|
const { compileTimeline } = await import("@glissade/core");
|
|
44
57
|
const compiled = compileTimeline(doc);
|
|
45
58
|
const duration = compiled.duration;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
59
|
+
let firstFrame;
|
|
60
|
+
let lastFrame;
|
|
61
|
+
if (opts.frame !== void 0) firstFrame = lastFrame = opts.frame;
|
|
62
|
+
else if (opts.frameRange) {
|
|
63
|
+
[firstFrame, lastFrame] = opts.frameRange;
|
|
64
|
+
lastFrame = Math.max(firstFrame, lastFrame);
|
|
65
|
+
} else {
|
|
66
|
+
const [from, to] = opts.range ?? [0, duration];
|
|
67
|
+
firstFrame = Math.round(from * fps);
|
|
68
|
+
lastFrame = Math.max(firstFrame, Math.ceil(to * fps) - 1);
|
|
69
|
+
}
|
|
49
70
|
const total = lastFrame - firstFrame + 1;
|
|
50
|
-
const isVideo = /\.(mp4|webm)$/i.test(opts.out);
|
|
71
|
+
const isVideo = opts.format !== "png-seq" && /\.(mp4|webm)$/i.test(opts.out);
|
|
51
72
|
if (isVideo && !ffmpegAvailable()) throw new Error(`'${opts.out}' needs FFmpeg on PATH and none was found. Render a PNG sequence instead (--out <directory>) or install ffmpeg.`);
|
|
52
73
|
const framesDir = isVideo ? mkdtempSync(join(tmpdir(), "glissade-frames-")) : resolve(opts.out);
|
|
53
74
|
mkdirSync(framesDir, { recursive: true });
|
|
@@ -76,7 +97,7 @@ async function render(opts) {
|
|
|
76
97
|
}
|
|
77
98
|
}
|
|
78
99
|
for (let f = firstFrame; f <= lastFrame; f++) {
|
|
79
|
-
backend.render(evaluate(scene, doc, f / fps));
|
|
100
|
+
backend.render(withDeterminismGuards("throw", () => evaluate(scene, doc, f / fps)));
|
|
80
101
|
writeFileSync(join(framesDir, `frame-${String(f).padStart(5, "0")}.png`), backend.encodePng());
|
|
81
102
|
opts.onProgress?.(f - firstFrame + 1, total);
|
|
82
103
|
}
|
|
@@ -214,4 +235,4 @@ async function render(opts) {
|
|
|
214
235
|
};
|
|
215
236
|
}
|
|
216
237
|
//#endregion
|
|
217
|
-
export { render as i, ffmpegAvailable as n, loadSceneModule as r, SceneModuleError as t };
|
|
238
|
+
export { render as a, parseFrameRange as i, ffmpegAvailable as n, loadSceneModule as r, SceneModuleError as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glissade/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0-pre.0",
|
|
4
4
|
"description": "glissade CLI: headless rendering via backend-skia (+ FFmpeg mux).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -20,15 +20,15 @@
|
|
|
20
20
|
"@napi-rs/canvas": "^0.1.65",
|
|
21
21
|
"esbuild": "^0.28.0",
|
|
22
22
|
"jiti": "^2.4.2",
|
|
23
|
-
"@glissade/backend-skia": "0.
|
|
24
|
-
"@glissade/core": "0.
|
|
25
|
-
"@glissade/interact": "0.
|
|
26
|
-
"@glissade/lottie": "0.
|
|
27
|
-
"@glissade/svg": "0.
|
|
28
|
-
"@glissade/narrate": "0.
|
|
29
|
-
"@glissade/player": "0.
|
|
30
|
-
"@glissade/scene": "0.
|
|
31
|
-
"@glissade/sfx": "0.
|
|
23
|
+
"@glissade/backend-skia": "0.7.0-pre.0",
|
|
24
|
+
"@glissade/core": "0.7.0-pre.0",
|
|
25
|
+
"@glissade/interact": "0.7.0-pre.0",
|
|
26
|
+
"@glissade/lottie": "0.7.0-pre.0",
|
|
27
|
+
"@glissade/svg": "0.7.0-pre.0",
|
|
28
|
+
"@glissade/narrate": "0.7.0-pre.0",
|
|
29
|
+
"@glissade/player": "0.7.0-pre.0",
|
|
30
|
+
"@glissade/scene": "0.7.0-pre.0",
|
|
31
|
+
"@glissade/sfx": "0.7.0-pre.0"
|
|
32
32
|
},
|
|
33
33
|
"repository": {
|
|
34
34
|
"type": "git",
|