@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 +3 -1
- package/dist/cues.js +62 -0
- package/dist/index.d.ts +2 -0
- package/dist/render.js +14 -2
- package/package.json +10 -10
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
|
|
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.
|
|
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.
|
|
24
|
-
"@glissade/core": "0.
|
|
25
|
-
"@glissade/interact": "0.
|
|
26
|
-
"@glissade/lottie": "0.
|
|
27
|
-
"@glissade/svg": "0.
|
|
28
|
-
"@glissade/narrate": "0.
|
|
29
|
-
"@glissade/player": "0.
|
|
30
|
-
"@glissade/scene": "0.
|
|
31
|
-
"@glissade/sfx": "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",
|