@glissade/cli 0.12.0 → 0.13.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/cli.js CHANGED
@@ -1,22 +1,27 @@
1
1
  #!/usr/bin/env node
2
2
  import { c as render, o as parseFrameRange } from "./render.js";
3
3
  import { n as parseCaptionsMode } from "./captions.js";
4
- //#region src/cli.ts
4
+ //#region src/args.ts
5
5
  /**
6
- * gs the glissade CLI (DESIGN.md §5.7).
7
- * gs render <scene-module> [--out <dir|file.mp4|file.webm>] [--fps N] [--range a..b]
6
+ * Tiny positional/flag argv parser for the `gs` CLI. Extracted from cli.ts so it
7
+ * is unit-testable without importing the CLI entry point (which runs main() on
8
+ * import).
8
9
  */
9
- function fail(msg) {
10
- console.error(`gs: ${msg}`);
11
- process.exit(1);
12
- }
13
- function parseCaptionsModeOrFail(raw) {
14
- try {
15
- return parseCaptionsMode(raw);
16
- } catch (err) {
17
- fail(err instanceof Error ? err.message : String(err));
18
- }
19
- }
10
+ const KNOWN_BOOLEAN_FLAGS = new Set([
11
+ "record",
12
+ "force",
13
+ "strict",
14
+ "cache",
15
+ "json",
16
+ "fix",
17
+ "no-warnings",
18
+ "lossless-intermediate",
19
+ "allow-gpu-shards",
20
+ "verbose",
21
+ "allow-degraded",
22
+ "bisect",
23
+ "watch"
24
+ ]);
20
25
  function parseArgs(argv) {
21
26
  const positional = [];
22
27
  const flags = /* @__PURE__ */ new Map();
@@ -26,11 +31,12 @@ function parseArgs(argv) {
26
31
  const eq = a.indexOf("=");
27
32
  if (eq >= 0) flags.set(a.slice(2, eq), a.slice(eq + 1));
28
33
  else {
34
+ const name = a.slice(2);
29
35
  const next = argv[i + 1];
30
- if (next !== void 0 && !next.startsWith("--")) {
31
- flags.set(a.slice(2), next);
36
+ if (!KNOWN_BOOLEAN_FLAGS.has(name) && next !== void 0 && !next.startsWith("--")) {
37
+ flags.set(name, next);
32
38
  i++;
33
- } else flags.set(a.slice(2), "");
39
+ } else flags.set(name, "");
34
40
  }
35
41
  } else positional.push(a);
36
42
  }
@@ -39,6 +45,23 @@ function parseArgs(argv) {
39
45
  flags
40
46
  };
41
47
  }
48
+ //#endregion
49
+ //#region src/cli.ts
50
+ /**
51
+ * gs — the glissade CLI (DESIGN.md §5.7).
52
+ * gs render <scene-module> [--out <dir|file.mp4|file.webm>] [--fps N] [--range a..b]
53
+ */
54
+ function fail(msg) {
55
+ console.error(`gs: ${msg}`);
56
+ process.exit(1);
57
+ }
58
+ function parseCaptionsModeOrFail(raw) {
59
+ try {
60
+ return parseCaptionsMode(raw);
61
+ } catch (err) {
62
+ fail(err instanceof Error ? err.message : String(err));
63
+ }
64
+ }
42
65
  const USAGE = `usage:
43
66
  gs render <scene-module> [options]
44
67
  gs diff <scene-module> --at <t> --against <baseline.dl.json|.png>
@@ -66,7 +89,7 @@ render options:
66
89
  --lossless-intermediate render shards as FFV1 + one final encode — the guaranteed byte-correct join
67
90
  (auto-enabled when the encoder can't honor precise boundary keyframes, e.g. mpeg4/openh264)
68
91
  --allow-gpu-shards permit sharding a scene with GPU/shader nodes (output is not reproducible across shards; §3.7)
69
- --cache [<dir>] persistent whole-frame raster cache in <dir> (default .gscache; §3.5). OFF by default — opting in
92
+ --cache[=<dir>] persistent whole-frame raster cache in <dir> (default .gscache; §3.5). OFF by default — opting in
70
93
  never changes output, only speed. A hit serves a stored frame byte-identical to a cold render.
71
94
  WINS: repeated renders + the UNCHANGED-PREFIX of a single-segment edit. Does NOT win a re-narrate —
72
95
  that shifts every frame's timing, so every DisplayList changes and every frame MISSES. Shards share
package/dist/index.d.ts CHANGED
@@ -688,6 +688,13 @@ interface LintOptions {
688
688
  caption?: CaptionProbe;
689
689
  /** include Tier-2 (warn-only) diagnostics; default true */
690
690
  warnings?: boolean;
691
+ /**
692
+ * Caller-supplied caption mode (e.g. the render config is burning captions).
693
+ * MAY escalate caption-fit to Tier-1 — but the committed script's own
694
+ * `captionMode` (on the timing manifest) is the authoritative signal and
695
+ * takes precedence. Omit to defer entirely to the manifest.
696
+ */
697
+ captionMode?: 'burn' | 'sidecar';
691
698
  }
692
699
  /**
693
700
  * Lint the committed narration timing. Pure: same inputs (timing + the probe's
package/dist/loudness.js CHANGED
@@ -41,6 +41,7 @@ var loudness_exports = /* @__PURE__ */ __exportAll({
41
41
  PUBLISH_PROFILES: () => PUBLISH_PROFILES,
42
42
  computeGainDb: () => computeGainDb,
43
43
  computeMixHash: () => computeMixHash,
44
+ floorGain2: () => floorGain2,
44
45
  loudnessPathFor: () => loudnessPathFor,
45
46
  measureFile: () => measureFile,
46
47
  measureLoudnessCommand: () => measureLoudnessCommand,
@@ -240,7 +241,7 @@ async function measureLoudnessCommand(opts) {
240
241
  inputI: round2(inputI),
241
242
  inputTp: round2(inputTp),
242
243
  inputLra: round2(inputLra),
243
- gain: round2(gain),
244
+ gain: floorGain2(gain),
244
245
  mixHash
245
246
  };
246
247
  const loudnessPath = loudnessPathFor(opts.modulePath);
@@ -260,5 +261,6 @@ async function measureLoudnessCommand(opts) {
260
261
  }
261
262
  }
262
263
  const round2 = (v) => Math.round(v * 100) / 100;
264
+ const floorGain2 = (v) => Math.floor(v * 100) / 100;
263
265
  //#endregion
264
266
  export { computeGainDb as a, loudness_exports as c, parseLoudnormJson as d, peakClampBinds as f, PUBLISH_PROFILES as i, measureFile as l, resolveProfile as m, LOUDNESS_SCHEMA_VERSION as n, computeMixHash as o, readLoudness as p, LoudnessError as r, loudnessPathFor as s, DEFAULT_PROFILE_ID as t, measureLoudnessCommand as u };
@@ -99,17 +99,24 @@ function lintNarration(timing, opts = {}) {
99
99
  };
100
100
  for (const s of timing.segments) checkBudget(s.id, s.duration, s.maxSec ?? budgets[s.id]);
101
101
  for (const p of timing.pauses ?? []) checkBudget(p.id, p.duration, budgets[p.id]);
102
- if (opts.caption) {
102
+ const burnDeclared = (timing.captionMode ?? opts.captionMode) === "burn";
103
+ const linesDeclared = timing.captionMaxLines !== void 0;
104
+ const captionEscalated = burnDeclared || linesDeclared;
105
+ const captionTier = captionEscalated ? 1 : 2;
106
+ const runCaptionFit = captionEscalated || warnings;
107
+ const nudge = " — caption-fit is warn-only until you declare maxLines or captionMode:\"burn\" in the script";
108
+ const withNudge = (msg) => captionEscalated ? msg : msg + nudge;
109
+ if (opts.caption && runCaptionFit) {
103
110
  const probe = opts.caption;
104
111
  for (const c of cues) {
105
112
  const id = cueId(c, perSeg.get(c.segId) ?? 1);
106
113
  const { lines, bottomY } = probe.measure(c.text);
107
114
  if (lines > probe.maxLines) out.push({
108
115
  rule: "caption-fit",
109
- tier: 1,
110
- severity: "error",
116
+ tier: captionTier,
117
+ severity: captionEscalated ? "error" : "warn",
111
118
  id,
112
- message: `caption wraps to ${lines} lines, over maxLines ${probe.maxLines}`,
119
+ message: withNudge(`caption wraps to ${lines} lines, over maxLines ${probe.maxLines}`),
113
120
  detail: {
114
121
  lines,
115
122
  maxLines: probe.maxLines,
@@ -118,10 +125,10 @@ function lintNarration(timing, opts = {}) {
118
125
  });
119
126
  else if (bottomY > probe.sceneH + 1e-6) out.push({
120
127
  rule: "caption-fit",
121
- tier: 1,
122
- severity: "error",
128
+ tier: captionTier,
129
+ severity: captionEscalated ? "error" : "warn",
123
130
  id,
124
- message: `caption overflows the frame: its lowest line ends at y=${bottomY.toFixed(0)} of ${probe.sceneH}`,
131
+ message: withNudge(`caption overflows the frame: its lowest line ends at y=${bottomY.toFixed(0)} of ${probe.sceneH}`),
125
132
  detail: {
126
133
  bottomY: round(bottomY),
127
134
  sceneH: probe.sceneH,
@@ -129,6 +129,11 @@ function parseManifest(json) {
129
129
  */
130
130
  function compareManifests(a, b) {
131
131
  if (a.backend !== b.backend) throw new VerifyDeterminismError(`cross-backend byte-compare rejected: '${a.backend}' vs '${b.backend}'. Byte-equality is a Skia↔Skia (cross-machine/shard) guarantee only; browser↔Skia is perceptual (SSIM) parity, never byte-identity (§5.5 item 6).`);
132
+ if (a.fps !== b.fps || a.size.w !== b.size.w || a.size.h !== b.size.h) return {
133
+ ok: false,
134
+ compared: 0,
135
+ reason: `incomparable: baseline fps/size differs (a: ${a.fps}fps ${a.size.w}x${a.size.h} vs b: ${b.fps}fps ${b.size.w}x${b.size.h}). Same frame index = different wall-clock time when fps differs; a different size yields an incomparable RGBA hash. Re-render the baseline at the same fps/size.`
136
+ };
132
137
  const byFrameB = new Map(b.frames.map((e) => [e.frame, e]));
133
138
  let compared = 0;
134
139
  let absent = 0;
@@ -144,6 +149,7 @@ function compareManifests(a, b) {
144
149
  let node;
145
150
  let groupFallback;
146
151
  for (const [id, ha] of Object.entries(ea.nodes)) {
152
+ if (!(id in eb.nodes)) continue;
147
153
  if (eb.nodes[id] === ha) continue;
148
154
  if (groups.has(id)) {
149
155
  groupFallback ??= id;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glissade/cli",
3
- "version": "0.12.0",
3
+ "version": "0.13.0-pre.0",
4
4
  "description": "glissade CLI: headless rendering via backend-skia (+ FFmpeg mux).",
5
5
  "license": "Apache-2.0",
6
6
  "engines": {
@@ -23,15 +23,15 @@
23
23
  "@napi-rs/canvas": "^0.1.65",
24
24
  "esbuild": "^0.28.0",
25
25
  "jiti": "^2.4.2",
26
- "@glissade/backend-skia": "0.12.0",
27
- "@glissade/core": "0.12.0",
28
- "@glissade/interact": "0.12.0",
29
- "@glissade/lottie": "0.12.0",
30
- "@glissade/svg": "0.12.0",
31
- "@glissade/narrate": "0.12.0",
32
- "@glissade/player": "0.12.0",
33
- "@glissade/scene": "0.12.0",
34
- "@glissade/sfx": "0.12.0"
26
+ "@glissade/backend-skia": "0.13.0-pre.0",
27
+ "@glissade/core": "0.13.0-pre.0",
28
+ "@glissade/interact": "0.13.0-pre.0",
29
+ "@glissade/lottie": "0.13.0-pre.0",
30
+ "@glissade/svg": "0.13.0-pre.0",
31
+ "@glissade/narrate": "0.13.0-pre.0",
32
+ "@glissade/player": "0.13.0-pre.0",
33
+ "@glissade/scene": "0.13.0-pre.0",
34
+ "@glissade/sfx": "0.13.0-pre.0"
35
35
  },
36
36
  "repository": {
37
37
  "type": "git",