@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 +41 -18
- package/dist/index.d.ts +7 -0
- package/dist/loudness.js +3 -1
- package/dist/narrationLintCommand.js +14 -7
- package/dist/verifyDeterminism.js +6 -0
- package/package.json +10 -10
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/
|
|
4
|
+
//#region src/args.ts
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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(
|
|
36
|
+
if (!KNOWN_BOOLEAN_FLAGS.has(name) && next !== void 0 && !next.startsWith("--")) {
|
|
37
|
+
flags.set(name, next);
|
|
32
38
|
i++;
|
|
33
|
-
} else flags.set(
|
|
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
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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.
|
|
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.
|
|
27
|
-
"@glissade/core": "0.
|
|
28
|
-
"@glissade/interact": "0.
|
|
29
|
-
"@glissade/lottie": "0.
|
|
30
|
-
"@glissade/svg": "0.
|
|
31
|
-
"@glissade/narrate": "0.
|
|
32
|
-
"@glissade/player": "0.
|
|
33
|
-
"@glissade/scene": "0.
|
|
34
|
-
"@glissade/sfx": "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",
|