@glissade/cli 0.7.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
@@ -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);
@@ -114,6 +115,10 @@ async function render(opts) {
114
115
  const { srt, vtt } = writeCaptionSidecars(timingPath, target);
115
116
  process.stderr.write(`captions: ${srt}, ${vtt}\n`);
116
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
+ };
117
122
  if (!isVideo) {
118
123
  if (singleFile) return {
119
124
  frames: 1,
@@ -121,6 +126,7 @@ async function render(opts) {
121
126
  };
122
127
  if (compiled.audio.length > 0) process.stderr.write("note: PNG-sequence output ignores timeline audio; render to .mp4/.webm to mix it\n");
123
128
  emitSidecars(framesDir);
129
+ emitCues(framesDir);
124
130
  return {
125
131
  frames: total,
126
132
  out: framesDir
@@ -129,6 +135,7 @@ async function render(opts) {
129
135
  const outAbs = resolve(opts.out);
130
136
  mkdirSync(dirname(outAbs), { recursive: true });
131
137
  emitSidecars(outAbs);
138
+ emitCues(outAbs);
132
139
  const isWebm = /\.webm$/i.test(outAbs);
133
140
  const container = isWebm ? "webm" : "mp4";
134
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",
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",
24
- "@glissade/core": "0.7.0",
25
- "@glissade/interact": "0.7.0",
26
- "@glissade/lottie": "0.7.0",
27
- "@glissade/svg": "0.7.0",
28
- "@glissade/narrate": "0.7.0",
29
- "@glissade/player": "0.7.0",
30
- "@glissade/scene": "0.7.0",
31
- "@glissade/sfx": "0.7.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",