@glissade/cli 0.7.0-pre.0 → 0.8.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
@@ -51,7 +51,7 @@ 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
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)
54
+ --frame <n> render a single frame; --out foo.png writes that one file, --out <dir> writes a PNG into it
55
55
  --format png-seq force a PNG sequence even when --out looks like a video
56
56
  --trace <file> replay an InputTrace and bake it (machine scenes, §A.6)
57
57
  --state <name> render one machine state's timeline linearly
@@ -60,6 +60,7 @@ render options:
60
60
  --narration <m> auto (default): mix the voice from a sibling *.narration.timing.json | off
61
61
  --music <m> auto (default): mix a sibling *.music.timing.json bed, ducked under narration | off
62
62
  --sfx <m> auto (default): mix effect hits from a sibling *.sfx.timing.json | off
63
+ --chapters <m> vtt: also write WebVTT chapters from cue markers (cues.json is always written when cues exist)
63
64
 
64
65
  dev options:
65
66
  --record add a Record button; writes .trace.json sidecars next to the module
@@ -185,6 +186,7 @@ async function main() {
185
186
  ...frame !== void 0 ? { frame } : {},
186
187
  ...frameRange ? { frameRange } : {},
187
188
  ...formatFlag === "png-seq" ? { format: "png-seq" } : {},
189
+ ...flags.get("chapters") === "vtt" ? { chapters: "vtt" } : {},
188
190
  ...flags.has("trace") ? { trace: flags.get("trace") } : {},
189
191
  ...flags.has("state") ? { state: flags.get("state") } : {},
190
192
  ...flags.has("force") ? { force: true } : {},
package/dist/cues.js ADDED
@@ -0,0 +1,62 @@
1
+ import { writeFileSync } from "node:fs";
2
+ import { basename, dirname, join } from "node:path";
3
+ //#region src/cues.ts
4
+ /**
5
+ * Composer cue signaling (§ad-break): cue markers (those carrying a `data.kind`)
6
+ * are emitted at render as a deterministic `<stem>.cues.json` sidecar so a
7
+ * downstream NLE / ad-insertion pipeline has machine-readable break points, and
8
+ * optionally as WebVTT chapters. Mirrors the captions sidecar plumbing.
9
+ */
10
+ /** Cue markers (those carrying a string `data.kind`) → a sorted cue list. */
11
+ function collectCues(markers) {
12
+ const cues = [];
13
+ for (const m of markers) {
14
+ const data = m.data;
15
+ if (!data || typeof data !== "object" || typeof data.kind !== "string") continue;
16
+ cues.push({
17
+ t: m.t,
18
+ kind: data.kind,
19
+ name: m.name,
20
+ ...typeof data.duration === "number" ? { duration: data.duration } : {}
21
+ });
22
+ }
23
+ return cues.sort((a, b) => a.t - b.t);
24
+ }
25
+ function vttTime(seconds) {
26
+ const ms = Math.round(seconds * 1e3);
27
+ const p = (n, w = 2) => String(n).padStart(w, "0");
28
+ return `${p(Math.floor(ms / 36e5))}:${p(Math.floor(ms % 36e5 / 6e4))}:${p(Math.floor(ms % 6e4 / 1e3))}.${p(ms % 1e3, 3)}`;
29
+ }
30
+ /** WebVTT chapters from cues; each runs until its duration or the next cue. */
31
+ function cuesToVtt(cues, totalDuration) {
32
+ let out = "WEBVTT\n\n";
33
+ cues.forEach((c, i) => {
34
+ const end = c.duration !== void 0 ? c.t + c.duration : cues[i + 1]?.t ?? totalDuration;
35
+ out += `${c.name}\n${vttTime(c.t)} --> ${vttTime(end)}\n${c.kind}\n\n`;
36
+ });
37
+ return out;
38
+ }
39
+ /**
40
+ * Write `<stem>.cues.json` (always, when cues exist) and, when `chaptersVtt`,
41
+ * `<stem>.chapters.vtt`, next to the render output. Deterministic. Returns the
42
+ * paths written.
43
+ */
44
+ function writeCueSidecars(outPath, markers, totalDuration, chaptersVtt) {
45
+ const cues = collectCues(markers);
46
+ if (cues.length === 0) return [];
47
+ const isFile = /\.(mp4|webm)$/i.test(outPath);
48
+ const dir = isFile ? dirname(outPath) : outPath;
49
+ const prefix = isFile ? `${basename(outPath).replace(/\.(mp4|webm)$/i, "")}.` : "";
50
+ const written = [];
51
+ const jsonPath = join(dir, `${prefix}cues.json`);
52
+ writeFileSync(jsonPath, JSON.stringify({ cues }, null, 2) + "\n");
53
+ written.push(jsonPath);
54
+ if (chaptersVtt) {
55
+ const vttPath = join(dir, `${prefix}chapters.vtt`);
56
+ writeFileSync(vttPath, cuesToVtt(cues, totalDuration));
57
+ written.push(vttPath);
58
+ }
59
+ return written;
60
+ }
61
+ //#endregion
62
+ export { writeCueSidecars };
package/dist/index.d.ts CHANGED
@@ -30,6 +30,8 @@ interface RenderOptions {
30
30
  narration?: 'auto' | 'off';
31
31
  /** auto (default): mix effect hits from a sibling *.sfx.timing.json. */
32
32
  sfx?: 'auto' | 'off';
33
+ /** also write WebVTT chapters from cue markers ('vtt'); cues.json is always written when cues exist. */
34
+ chapters?: 'vtt' | 'off';
33
35
  onProgress?: (frame: number, total: number) => void;
34
36
  }
35
37
  declare class SceneModuleError extends Error {
package/dist/render.js CHANGED
@@ -52,6 +52,7 @@ async function render(opts) {
52
52
  const fps = opts.fps ?? doc.fps ?? 60;
53
53
  const captionsMode = opts.captions ?? "burn";
54
54
  const { hideCaptionsDoc, timingPathFor, writeCaptionSidecars } = await import("./captions.js").then((n) => n.t);
55
+ const { writeCueSidecars } = await import("./cues.js");
55
56
  if (captionsMode !== "burn" && scene.resolveTarget("captions/opacity") !== void 0) doc = hideCaptionsDoc(doc);
56
57
  const { compileTimeline } = await import("@glissade/core");
57
58
  const compiled = compileTimeline(doc);
@@ -69,8 +70,9 @@ async function render(opts) {
69
70
  }
70
71
  const total = lastFrame - firstFrame + 1;
71
72
  const isVideo = opts.format !== "png-seq" && /\.(mp4|webm)$/i.test(opts.out);
73
+ const singleFile = !isVideo && total === 1 && /\.png$/i.test(opts.out);
72
74
  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.`);
73
- const framesDir = isVideo ? mkdtempSync(join(tmpdir(), "glissade-frames-")) : resolve(opts.out);
75
+ const framesDir = isVideo ? mkdtempSync(join(tmpdir(), "glissade-frames-")) : singleFile ? dirname(resolve(opts.out)) : resolve(opts.out);
74
76
  mkdirSync(framesDir, { recursive: true });
75
77
  const backend = new SkiaBackend(scene.size.w, scene.size.h);
76
78
  scene.setTextMeasurer(backend);
@@ -98,7 +100,7 @@ async function render(opts) {
98
100
  }
99
101
  for (let f = firstFrame; f <= lastFrame; f++) {
100
102
  backend.render(withDeterminismGuards("throw", () => evaluate(scene, doc, f / fps)));
101
- writeFileSync(join(framesDir, `frame-${String(f).padStart(5, "0")}.png`), backend.encodePng());
103
+ writeFileSync(singleFile ? resolve(opts.out) : join(framesDir, `frame-${String(f).padStart(5, "0")}.png`), backend.encodePng());
102
104
  opts.onProgress?.(f - firstFrame + 1, total);
103
105
  }
104
106
  backend.dispose();
@@ -113,9 +115,18 @@ async function render(opts) {
113
115
  const { srt, vtt } = writeCaptionSidecars(timingPath, target);
114
116
  process.stderr.write(`captions: ${srt}, ${vtt}\n`);
115
117
  };
118
+ const emitCues = (target) => {
119
+ const written = writeCueSidecars(target, compiled.markers, duration, opts.chapters === "vtt");
120
+ if (written.length) process.stderr.write(`cues: ${written.join(", ")}\n`);
121
+ };
116
122
  if (!isVideo) {
123
+ if (singleFile) return {
124
+ frames: 1,
125
+ out: resolve(opts.out)
126
+ };
117
127
  if (compiled.audio.length > 0) process.stderr.write("note: PNG-sequence output ignores timeline audio; render to .mp4/.webm to mix it\n");
118
128
  emitSidecars(framesDir);
129
+ emitCues(framesDir);
119
130
  return {
120
131
  frames: total,
121
132
  out: framesDir
@@ -124,6 +135,7 @@ async function render(opts) {
124
135
  const outAbs = resolve(opts.out);
125
136
  mkdirSync(dirname(outAbs), { recursive: true });
126
137
  emitSidecars(outAbs);
138
+ emitCues(outAbs);
127
139
  const isWebm = /\.webm$/i.test(outAbs);
128
140
  const container = isWebm ? "webm" : "mp4";
129
141
  const { pickEncoder } = await import("./encoders.js").then((n) => n.r);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glissade/cli",
3
- "version": "0.7.0-pre.0",
3
+ "version": "0.8.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.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"
23
+ "@glissade/backend-skia": "0.8.0-pre.0",
24
+ "@glissade/core": "0.8.0-pre.0",
25
+ "@glissade/interact": "0.8.0-pre.0",
26
+ "@glissade/lottie": "0.8.0-pre.0",
27
+ "@glissade/svg": "0.8.0-pre.0",
28
+ "@glissade/narrate": "0.8.0-pre.0",
29
+ "@glissade/player": "0.8.0-pre.0",
30
+ "@glissade/scene": "0.8.0-pre.0",
31
+ "@glissade/sfx": "0.8.0-pre.0"
32
32
  },
33
33
  "repository": {
34
34
  "type": "git",