@glissade/cli 0.28.0-pre.1 → 0.29.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/build.js ADDED
@@ -0,0 +1,280 @@
1
+ import { t as __exportAll } from "./rolldown-runtime.js";
2
+ import { t as glissadeVersion } from "./version.js";
3
+ import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
4
+ import { basename, dirname, join, relative, resolve } from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+ import { createHash } from "node:crypto";
7
+ //#region src/buildPlan.ts
8
+ /** The canonical order; a scene runs the subset that applies to it. */
9
+ const PIPELINE = [
10
+ "narrate",
11
+ "sfx",
12
+ "measure-loudness",
13
+ "render"
14
+ ];
15
+ /**
16
+ * Plan one scene's steps. `steps` is the applicable subset in PIPELINE order (a
17
+ * scene with no narration omits 'narrate', etc.). Returns a run/skip decision per
18
+ * step; the first step that runs forces every later step to run (propagation).
19
+ */
20
+ function planScene(scene, steps, probe) {
21
+ const plans = [];
22
+ let upstreamRan = false;
23
+ for (const step of steps) {
24
+ let action;
25
+ let reason;
26
+ if (upstreamRan) {
27
+ action = "run";
28
+ reason = "upstream re-ran";
29
+ } else if (!probe.outputExists(step)) {
30
+ action = "run";
31
+ reason = "output missing";
32
+ } else {
33
+ const rec = probe.recordedHash(step);
34
+ if (rec === void 0) {
35
+ action = "run";
36
+ reason = "never built";
37
+ } else if (rec !== probe.currentHash(step)) {
38
+ action = "run";
39
+ reason = "inputs changed";
40
+ } else {
41
+ action = "skip";
42
+ reason = "fresh";
43
+ }
44
+ }
45
+ if (action === "run") upstreamRan = true;
46
+ plans.push({
47
+ scene,
48
+ step,
49
+ action,
50
+ reason
51
+ });
52
+ }
53
+ return plans;
54
+ }
55
+ //#endregion
56
+ //#region src/build.ts
57
+ /**
58
+ * `gs build` (0.29): the content-graph DAG runner. A `glissade.config.ts` lists the
59
+ * project's scenes; `gs build` derives each scene's narrate → sfx → measure-loudness
60
+ * → render pipeline, content-hashes every step's inputs (source + upstream outputs +
61
+ * glissade version), and runs ONLY the stale subtree. A one-segment re-narration
62
+ * re-narrates that asset, re-syncs ITS sfx, re-measures ITS loudness, re-renders it —
63
+ * and touches nothing else. `--explain` prints the plan without running anything.
64
+ *
65
+ * Step execution is injectable (`deps.runStep`) so the orchestration — staleness
66
+ * across builds, propagation, the manifest — is unit-testable without a TTS venv or
67
+ * ffmpeg; the default `runStep` delegates to the shipped narrate/sfx/loudness/render.
68
+ */
69
+ var build_exports = /* @__PURE__ */ __exportAll({
70
+ applicableSteps: () => applicableSteps,
71
+ buildCommand: () => buildCommand,
72
+ defineProject: () => defineProject,
73
+ hashInputs: () => hashInputs,
74
+ loadConfig: () => loadConfig,
75
+ outputVideo: () => outputVideo,
76
+ resolveScenes: () => resolveScenes,
77
+ stepInputs: () => stepInputs,
78
+ stepOutput: () => stepOutput
79
+ });
80
+ /** Identity helper for a typed `glissade.config.ts` default export. */
81
+ function defineProject(config) {
82
+ return config;
83
+ }
84
+ async function loadConfig(configPath) {
85
+ const { createJiti } = await import("jiti");
86
+ const cfg = await createJiti(pathToFileURL(`${process.cwd()}/`).href).import(pathToFileURL(resolve(configPath)).href, { default: true });
87
+ if (!cfg || !Array.isArray(cfg.scenes)) throw new Error(`${configPath}: config must default-export { scenes: string[] } — use defineProject({ scenes: [...] })`);
88
+ return cfg;
89
+ }
90
+ /** Expand scene patterns (explicit paths + `dir/**\/*.ts` / `dir/*.ts` globs) to concrete files under `root`. */
91
+ function resolveScenes(patterns, root) {
92
+ const found = /* @__PURE__ */ new Set();
93
+ for (const pat of patterns) {
94
+ if (!pat.includes("*")) {
95
+ found.add(resolve(root, pat));
96
+ continue;
97
+ }
98
+ const starAt = pat.indexOf("*");
99
+ const prefix = pat.slice(0, starAt);
100
+ const baseDir = resolve(root, prefix.includes("/") ? prefix.slice(0, prefix.lastIndexOf("/")) : ".");
101
+ const recursive = pat.includes("**");
102
+ const tail = pat.slice(pat.lastIndexOf("/") + 1);
103
+ const re = new RegExp(`^${tail.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*")}$`);
104
+ walk(baseDir, recursive, (file) => {
105
+ if (re.test(basename(file))) found.add(file);
106
+ });
107
+ }
108
+ return [...found].sort();
109
+ }
110
+ function walk(dir, recursive, onFile) {
111
+ let entries;
112
+ try {
113
+ entries = readdirSync(dir);
114
+ } catch {
115
+ return;
116
+ }
117
+ for (const e of entries) {
118
+ if (e === "node_modules" || e.startsWith(".")) continue;
119
+ const full = join(dir, e);
120
+ let s;
121
+ try {
122
+ s = statSync(full);
123
+ } catch {
124
+ continue;
125
+ }
126
+ if (s.isDirectory()) {
127
+ if (recursive) walk(full, recursive, onFile);
128
+ } else onFile(full);
129
+ }
130
+ }
131
+ const stripExt = (scene) => scene.replace(/\.[jt]sx?$/, "");
132
+ const sib = (scene, ext) => `${stripExt(scene)}${ext}`;
133
+ /** Where a scene's rendered video lands (config `out` dir, else alongside the source). */
134
+ function outputVideo(scene, cfg, root) {
135
+ return cfg.out ? join(resolve(root, cfg.out), `${basename(stripExt(scene))}.mp4`) : `${stripExt(scene)}.mp4`;
136
+ }
137
+ /** The input files whose bytes decide a step's freshness (upstream outputs included). */
138
+ function stepInputs(scene, step) {
139
+ switch (step) {
140
+ case "narrate": return [scene, sib(scene, ".narration.json")];
141
+ case "sfx": return [sib(scene, ".sfx.json"), sib(scene, ".narration.timing.json")];
142
+ case "measure-loudness": return [
143
+ sib(scene, ".narration.timing.json"),
144
+ sib(scene, ".music.timing.json"),
145
+ sib(scene, ".sfx.timing.json")
146
+ ];
147
+ case "render": return [
148
+ scene,
149
+ sib(scene, ".narration.timing.json"),
150
+ sib(scene, ".music.timing.json"),
151
+ sib(scene, ".sfx.timing.json"),
152
+ sib(scene, ".loudness.json")
153
+ ];
154
+ }
155
+ }
156
+ /** The committed output whose existence gates a step (missing → must run). */
157
+ function stepOutput(scene, step, videoPath) {
158
+ switch (step) {
159
+ case "narrate": return sib(scene, ".narration.timing.json");
160
+ case "sfx": return sib(scene, ".sfx.timing.json");
161
+ case "measure-loudness": return sib(scene, ".loudness.json");
162
+ case "render": return videoPath;
163
+ }
164
+ }
165
+ /** Which pipeline steps apply to a scene: narrate/sfx only if their source sidecar exists;
166
+ * measure-loudness only if any audio timing is present; render always. */
167
+ function applicableSteps(scene) {
168
+ const steps = [];
169
+ if (existsSync(sib(scene, ".narration.json"))) steps.push("narrate");
170
+ if (existsSync(sib(scene, ".sfx.json"))) steps.push("sfx");
171
+ if ([
172
+ ".narration.json",
173
+ ".music.timing.json",
174
+ ".sfx.json"
175
+ ].some((e) => existsSync(sib(scene, e)))) steps.push("measure-loudness");
176
+ steps.push("render");
177
+ return PIPELINE.filter((s) => steps.includes(s));
178
+ }
179
+ function hashInputs(paths, salt) {
180
+ const h = createHash("sha256").update(salt).update("\0");
181
+ for (const p of paths) {
182
+ h.update(p).update("\0");
183
+ h.update(existsSync(p) ? readFileSync(p) : Buffer.from("\0ABSENT\0"));
184
+ h.update("\0");
185
+ }
186
+ return `sha256:${h.digest("hex")}`;
187
+ }
188
+ const manifestPath = (root) => join(root, ".gsbuild.json");
189
+ function loadManifest(root) {
190
+ const p = manifestPath(root);
191
+ if (existsSync(p)) try {
192
+ const m = JSON.parse(readFileSync(p, "utf8"));
193
+ if (m.version === 1 && m.scenes) return m;
194
+ } catch {}
195
+ return {
196
+ version: 1,
197
+ scenes: {}
198
+ };
199
+ }
200
+ function saveManifest(root, m) {
201
+ writeFileSync(manifestPath(root), JSON.stringify(m, null, 2));
202
+ }
203
+ async function buildCommand(opts, deps = { runStep: defaultRunStep }) {
204
+ const cfg = await loadConfig(opts.config);
205
+ const root = dirname(resolve(opts.config));
206
+ let scenes = resolveScenes(cfg.scenes, root);
207
+ if (opts.only?.length) scenes = scenes.filter((s) => opts.only.some((o) => s.includes(o)));
208
+ const version = glissadeVersion();
209
+ const manifest = loadManifest(root);
210
+ const log = opts.onLog ?? (() => {});
211
+ const allPlans = [];
212
+ let ran = 0;
213
+ let skipped = 0;
214
+ for (const scene of scenes) {
215
+ const key = relative(root, scene);
216
+ const rec = manifest.scenes[key] ?? {};
217
+ const videoPath = outputVideo(scene, cfg, root);
218
+ const plans = planScene(key, applicableSteps(scene), {
219
+ currentHash: (step) => hashInputs(stepInputs(scene, step), version),
220
+ recordedHash: (step) => rec[step],
221
+ outputExists: (step) => existsSync(stepOutput(scene, step, videoPath))
222
+ });
223
+ for (const plan of plans) {
224
+ log(`${key} ${plan.step}: ${plan.action} (${plan.reason})`);
225
+ if (plan.action === "skip") {
226
+ skipped++;
227
+ continue;
228
+ }
229
+ ran++;
230
+ if (!opts.explain) {
231
+ await deps.runStep(scene, plan.step, cfg, videoPath);
232
+ rec[plan.step] = hashInputs(stepInputs(scene, plan.step), version);
233
+ }
234
+ }
235
+ manifest.scenes[key] = rec;
236
+ allPlans.push(...plans);
237
+ }
238
+ if (!opts.explain) saveManifest(root, manifest);
239
+ return {
240
+ scenes: scenes.length,
241
+ ran,
242
+ skipped,
243
+ plans: allPlans
244
+ };
245
+ }
246
+ /** Default step executor — the shipped commands. */
247
+ async function defaultRunStep(scene, step, cfg, videoPath) {
248
+ switch (step) {
249
+ case "narrate": {
250
+ const { narrateCommand } = await import("./narrate.js");
251
+ await narrateCommand({ input: scene });
252
+ return;
253
+ }
254
+ case "sfx": {
255
+ const { prepareSfx } = await import("./sfx.js");
256
+ prepareSfx(sib(scene, ".sfx.json"));
257
+ return;
258
+ }
259
+ case "measure-loudness": {
260
+ const { measureLoudnessCommand } = await import("./loudness.js").then((n) => n.c);
261
+ await measureLoudnessCommand({ modulePath: scene });
262
+ return;
263
+ }
264
+ case "render": {
265
+ const { render } = await import("./render.js").then((n) => n.p);
266
+ await render({
267
+ modulePath: scene,
268
+ out: videoPath,
269
+ ...cfg.defaults?.fps !== void 0 ? { fps: cfg.defaults.fps } : {},
270
+ ...cfg.defaults?.cache ? { cache: {
271
+ dir: cfg.defaults.cache,
272
+ mode: "read-write"
273
+ } } : {}
274
+ });
275
+ return;
276
+ }
277
+ }
278
+ }
279
+ //#endregion
280
+ export { defineProject as n, build_exports as t };
@@ -1,7 +1,7 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime.js";
2
2
  import { o as loadSceneModule } from "./render.js";
3
- import { i as capsId, n as FrameCache, o as frameCacheKey } from "./frameCache.js";
4
3
  import { t as glissadeVersion } from "./version.js";
4
+ import { i as capsId, n as FrameCache, o as frameCacheKey } from "./frameCache.js";
5
5
  import { mkdtempSync, rmSync } from "node:fs";
6
6
  import { tmpdir } from "node:os";
7
7
  import { join } from "node:path";
package/dist/cli.js CHANGED
@@ -77,6 +77,7 @@ const USAGE = `usage:
77
77
  gs fonts audit <scene-module> list registered families, formats, and missing-glyph runs (§3.6)
78
78
  gs cache verify <scene-module> [--range a..b] [--sample <n>] assert cache hits == cold renders (§3.5)
79
79
  gs mcp <scene-module> start an MCP stdio server for this scene: describe / list_targets / apply_patch / undo / render_frame (the AI-native write layer)
80
+ gs build [filter...] [--config <glissade.config.ts>] [--explain] content-graph DAG runner: narrate→sfx→loudness→render per scene, runs ONLY the stale subtree
80
81
 
81
82
  render options:
82
83
  --out <path> output directory for a PNG sequence, or .mp4/.webm (needs ffmpeg). default: ./out
@@ -167,7 +168,7 @@ narration-lint options (lint the committed *.narration.timing.json + the real ca
167
168
  `;
168
169
  async function main() {
169
170
  const [command, ...rest] = process.argv.slice(2);
170
- if (command !== "render" && command !== "diff" && command !== "verify-determinism" && command !== "dev" && command !== "import" && command !== "narrate" && command !== "narration-lint" && command !== "sfx" && command !== "prepare" && command !== "measure-loudness" && command !== "fonts" && command !== "cache" && command !== "mcp") {
171
+ if (command !== "render" && command !== "diff" && command !== "verify-determinism" && command !== "dev" && command !== "import" && command !== "narrate" && command !== "narration-lint" && command !== "sfx" && command !== "prepare" && command !== "measure-loudness" && command !== "fonts" && command !== "cache" && command !== "mcp" && command !== "build") {
171
172
  console.error(USAGE);
172
173
  process.exit(command === void 0 || command === "help" || command === "--help" ? 0 : 1);
173
174
  }
@@ -190,6 +191,24 @@ async function main() {
190
191
  }
191
192
  return;
192
193
  }
194
+ if (command === "build") {
195
+ const { positional: bp, flags: bf } = parseArgs(rest);
196
+ const { buildCommand } = await import("./build.js").then((n) => n.t);
197
+ const config = bf.get("config") || "glissade.config.ts";
198
+ const explain = bf.has("explain");
199
+ try {
200
+ const r = await buildCommand({
201
+ config,
202
+ explain,
203
+ ...bp.length ? { only: bp } : {},
204
+ onLog: (line) => process.stderr.write(`${line}\n`)
205
+ });
206
+ process.stderr.write(`gs build: ${r.ran} step${r.ran === 1 ? "" : "s"} ${explain ? "WOULD run" : "ran"}, ${r.skipped} fresh, across ${r.scenes} scene${r.scenes === 1 ? "" : "s"}${explain ? " (--explain: nothing executed)" : ""}\n`);
207
+ } catch (err) {
208
+ fail(err instanceof Error ? err.message : String(err));
209
+ }
210
+ return;
211
+ }
193
212
  if (command === "cache") {
194
213
  const { positional: cp, flags: cf } = parseArgs(rest);
195
214
  const sub = cp[0];
@@ -0,0 +1,17 @@
1
+ //#region src/build.d.ts
2
+
3
+ interface ProjectConfig {
4
+ /** scene module paths or globs (`episodes/**\/*.ts`), relative to the config file. */
5
+ scenes: string[];
6
+ /** output dir for rendered videos (default: alongside each scene as `<base>.mp4`). */
7
+ out?: string;
8
+ /** per-scene render/cache defaults. */
9
+ defaults?: {
10
+ fps?: number;
11
+ cache?: string;
12
+ };
13
+ }
14
+ /** Identity helper for a typed `glissade.config.ts` default export. */
15
+ declare function defineProject(config: ProjectConfig): ProjectConfig;
16
+ //#endregion
17
+ export { type ProjectConfig, defineProject };
package/dist/config.js ADDED
@@ -0,0 +1,2 @@
1
+ import { n as defineProject } from "./build.js";
2
+ export { defineProject };
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { a as ffmpegAvailable, d as render, f as renderLocales, i as collectAudioClips, l as parseLocalesList, m as resolveLoudnessGainDb, n as SceneModuleError, o as loadSceneModule, r as buildMixWav, s as localeOutPath, t as LocaleArgsError, u as planFinalAudio } from "./render.js";
2
+ import { t as glissadeVersion } from "./version.js";
2
3
  import { a as computeGainDb, d as parseLoudnormJson, f as peakClampBinds, i as PUBLISH_PROFILES, l as measureFile, m as resolveProfile, n as LOUDNESS_SCHEMA_VERSION, o as computeMixHash, p as readLoudness, r as LoudnessError, s as loudnessPathFor, t as DEFAULT_PROFILE_ID, u as measureLoudnessCommand } from "./loudness.js";
3
4
  import { a as pickEncoder, i as parseEncoderList, n as availableEncoders, t as NoEncoderError } from "./encoders.js";
4
5
  import { a as splitFrameRange, n as renderSharded, r as sceneHasGpuNodes, t as ShardError } from "./shards.js";
@@ -10,6 +11,5 @@ import { t as importCommand } from "./import.js";
10
11
  import { i as snapshotAt, r as evaluateAt, t as diffCommand } from "./diff.js";
11
12
  import { a as fixDiff, c as lintNarration, n as lintTimingPathFor, o as formatTable, r as narrationLintCommand, s as hasErrors, t as buildCaptionProbe } from "./narrationLintCommand.js";
12
13
  import { a as clearFrameCache, c as parseCacheMaxSize, i as capsId, l as probeEntryHeader, n as FrameCache, o as frameCacheKey, r as FrameCacheError, t as DEFAULT_CACHE_MAX_SIZE } from "./frameCache.js";
13
- import { t as glissadeVersion } from "./version.js";
14
14
  import { t as cacheVerifyCommand } from "./cacheVerify.js";
15
15
  export { AudioMixError, DEFAULT_CACHE_MAX_SIZE, DEFAULT_PROFILE_ID, FfmpegVideoFrameSource, FrameCache, FrameCacheError, LOUDNESS_SCHEMA_VERSION, LocaleArgsError, LoudnessError, MachineExportError, NoEncoderError, PUBLISH_PROFILES, SceneModuleError, ShardError, VideoProbeError, applyMixGainDb, atempoChain, availableEncoders, buildCaptionProbe, buildMixWav, cacheVerifyCommand, capsId, clearFrameCache, collectAudioClips, computeGainDb, computeMixHash, dev, diffCommand, evaluateAt, ffmpegAvailable, fixDiff, formatTable, frameCacheKey, gainExpression, glissadeVersion, hasErrors, importCommand, lintNarration, lintTimingPathFor, loadSceneModule, localeOutPath, loudnessPathFor, measureFile, measureLoudnessCommand, narrationLintCommand, parseCacheMaxSize, parseEncoderList, parseLocalesList, parseLoudnormJson, peakClampBinds, pickEncoder, planAudioMix, planFinalAudio, probeEntryHeader, probeVideo, readLoudness, render, renderLocales, renderSharded, resolveAssetPath, resolveLoudnessGainDb, resolveProfile, resolveRenderDoc, sceneHasGpuNodes, snapshotAt, splitFrameRange };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glissade/cli",
3
- "version": "0.28.0-pre.1",
3
+ "version": "0.29.0-pre.0",
4
4
  "description": "glissade CLI: headless rendering via backend-skia (+ FFmpeg mux).",
5
5
  "license": "Apache-2.0",
6
6
  "engines": {
@@ -14,6 +14,10 @@
14
14
  ".": {
15
15
  "types": "./dist/index.d.ts",
16
16
  "default": "./dist/index.js"
17
+ },
18
+ "./config": {
19
+ "types": "./dist/config.d.ts",
20
+ "default": "./dist/config.js"
17
21
  }
18
22
  },
19
23
  "files": [
@@ -24,15 +28,15 @@
24
28
  "@napi-rs/canvas": "^0.1.65",
25
29
  "esbuild": "0.28.0",
26
30
  "jiti": "^2.4.2",
27
- "@glissade/backend-skia": "0.28.0-pre.1",
28
- "@glissade/core": "0.28.0-pre.1",
29
- "@glissade/interact": "0.28.0-pre.1",
30
- "@glissade/lottie": "0.28.0-pre.1",
31
- "@glissade/narrate": "0.28.0-pre.1",
32
- "@glissade/player": "0.28.0-pre.1",
33
- "@glissade/scene": "0.28.0-pre.1",
34
- "@glissade/sfx": "0.28.0-pre.1",
35
- "@glissade/svg": "0.28.0-pre.1"
31
+ "@glissade/backend-skia": "0.29.0-pre.0",
32
+ "@glissade/interact": "0.29.0-pre.0",
33
+ "@glissade/core": "0.29.0-pre.0",
34
+ "@glissade/lottie": "0.29.0-pre.0",
35
+ "@glissade/narrate": "0.29.0-pre.0",
36
+ "@glissade/player": "0.29.0-pre.0",
37
+ "@glissade/scene": "0.29.0-pre.0",
38
+ "@glissade/sfx": "0.29.0-pre.0",
39
+ "@glissade/svg": "0.29.0-pre.0"
36
40
  },
37
41
  "repository": {
38
42
  "type": "git",