@glissade/cli 0.7.0 → 0.8.0-pre.1
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 +2 -0
- package/dist/cues.js +75 -0
- package/dist/index.d.ts +2 -0
- package/dist/render.js +7 -0
- package/package.json +10 -10
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,75 @@
|
|
|
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.title === "string" ? { title: data.title } : {},
|
|
21
|
+
...typeof data.duration === "number" ? { duration: data.duration } : {}
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return cues.sort((a, b) => a.t - b.t);
|
|
25
|
+
}
|
|
26
|
+
function vttTime(seconds) {
|
|
27
|
+
const ms = Math.round(seconds * 1e3);
|
|
28
|
+
const p = (n, w = 2) => String(n).padStart(w, "0");
|
|
29
|
+
return `${p(Math.floor(ms / 36e5))}:${p(Math.floor(ms % 36e5 / 6e4))}:${p(Math.floor(ms % 6e4 / 1e3))}.${p(ms % 1e3, 3)}`;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* WebVTT chapters from cues; each runs until its duration or the next cue. The
|
|
33
|
+
* cue text is the human `title` (falling back to `name`), not the machine
|
|
34
|
+
* `kind`. When the earliest cue starts after 0, a `00:00` "Intro" chapter is
|
|
35
|
+
* auto-anchored — both valid WebVTT and required for YouTube description
|
|
36
|
+
* chapters (which read the cue text as the title and need a 0:00 start).
|
|
37
|
+
*/
|
|
38
|
+
function cuesToVtt(cues, totalDuration) {
|
|
39
|
+
const anchored = cues.length > 0 && cues[0].t > 0 ? [{
|
|
40
|
+
t: 0,
|
|
41
|
+
kind: "chapter",
|
|
42
|
+
name: "Intro",
|
|
43
|
+
title: "Intro"
|
|
44
|
+
}, ...cues] : cues;
|
|
45
|
+
let out = "WEBVTT\n\n";
|
|
46
|
+
anchored.forEach((c, i) => {
|
|
47
|
+
const end = c.duration !== void 0 ? c.t + c.duration : anchored[i + 1]?.t ?? totalDuration;
|
|
48
|
+
out += `${c.name}\n${vttTime(c.t)} --> ${vttTime(end)}\n${c.title ?? c.name}\n\n`;
|
|
49
|
+
});
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Write `<stem>.cues.json` (always, when cues exist) and, when `chaptersVtt`,
|
|
54
|
+
* `<stem>.chapters.vtt`, next to the render output. Deterministic. Returns the
|
|
55
|
+
* paths written.
|
|
56
|
+
*/
|
|
57
|
+
function writeCueSidecars(outPath, markers, totalDuration, chaptersVtt) {
|
|
58
|
+
const cues = collectCues(markers);
|
|
59
|
+
if (cues.length === 0) return [];
|
|
60
|
+
const isFile = /\.(mp4|webm)$/i.test(outPath);
|
|
61
|
+
const dir = isFile ? dirname(outPath) : outPath;
|
|
62
|
+
const prefix = isFile ? `${basename(outPath).replace(/\.(mp4|webm)$/i, "")}.` : "";
|
|
63
|
+
const written = [];
|
|
64
|
+
const jsonPath = join(dir, `${prefix}cues.json`);
|
|
65
|
+
writeFileSync(jsonPath, JSON.stringify({ cues }, null, 2) + "\n");
|
|
66
|
+
written.push(jsonPath);
|
|
67
|
+
if (chaptersVtt) {
|
|
68
|
+
const vttPath = join(dir, `${prefix}chapters.vtt`);
|
|
69
|
+
writeFileSync(vttPath, cuesToVtt(cues, totalDuration));
|
|
70
|
+
written.push(vttPath);
|
|
71
|
+
}
|
|
72
|
+
return written;
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
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.
|
|
3
|
+
"version": "0.8.0-pre.1",
|
|
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.1",
|
|
24
|
+
"@glissade/core": "0.8.0-pre.1",
|
|
25
|
+
"@glissade/interact": "0.8.0-pre.1",
|
|
26
|
+
"@glissade/lottie": "0.8.0-pre.1",
|
|
27
|
+
"@glissade/svg": "0.8.0-pre.1",
|
|
28
|
+
"@glissade/narrate": "0.8.0-pre.1",
|
|
29
|
+
"@glissade/player": "0.8.0-pre.1",
|
|
30
|
+
"@glissade/scene": "0.8.0-pre.1",
|
|
31
|
+
"@glissade/sfx": "0.8.0-pre.1"
|
|
32
32
|
},
|
|
33
33
|
"repository": {
|
|
34
34
|
"type": "git",
|