@glissade/cli 0.6.0 → 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 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 = Math.round(clip.at * 1e3);
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 { i as render } from "./render.js";
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> seconds to render (default: 0..duration)
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 range;
161
+ let frameRange;
160
162
  const rangeFlag = flags.get("range");
161
- if (rangeFlag) {
162
- const m = /^([\d.]+)\.\.([\d.]+)$/.exec(rangeFlag);
163
- if (!m) fail(`--range must be 'a..b' in seconds, got '${rangeFlag}'`);
164
- range = [parseFloat(m[1]), parseFloat(m[2])];
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
- ...range ? { range } : {},
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 { i as render, n as ffmpegAvailable, r as loadSceneModule, t as SceneModuleError } from "./render.js";
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
- const [from, to] = opts.range ?? [0, duration];
47
- const firstFrame = Math.round(from * fps);
48
- const lastFrame = Math.max(firstFrame, Math.ceil(to * fps) - 1);
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.6.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.6.0",
24
- "@glissade/core": "0.6.0",
25
- "@glissade/interact": "0.6.0",
26
- "@glissade/lottie": "0.6.0",
27
- "@glissade/svg": "0.6.0",
28
- "@glissade/narrate": "0.6.0",
29
- "@glissade/player": "0.6.0",
30
- "@glissade/scene": "0.6.0",
31
- "@glissade/sfx": "0.6.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",