@glissade/cli 0.9.0 → 0.9.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 CHANGED
@@ -61,6 +61,8 @@ render options:
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
63
  --chapters <m> vtt: also write WebVTT chapters from cue markers (cues.json is always written when cues exist)
64
+ (YouTube needs the 1st chapter at 0:00 — auto-anchored — and each chapter >= 10s; author cue ts accordingly)
65
+ --chapters-kind <k[,k]> cue kinds that become VTT chapters (default: chapter); cues.json keeps all kinds
64
66
  --strict fail on an unregistered font family or an uncovered glyph (§3.6; default: warn)
65
67
 
66
68
  dev options:
@@ -189,6 +191,7 @@ async function main() {
189
191
  ...frameRange ? { frameRange } : {},
190
192
  ...formatFlag === "png-seq" ? { format: "png-seq" } : {},
191
193
  ...flags.get("chapters") === "vtt" ? { chapters: "vtt" } : {},
194
+ ...flags.has("chapters-kind") ? { chapterKinds: new Set(flags.get("chapters-kind").split(",").map((s) => s.trim()).filter(Boolean)) } : {},
192
195
  ...flags.has("trace") ? { trace: flags.get("trace") } : {},
193
196
  ...flags.has("state") ? { state: flags.get("state") } : {},
194
197
  ...flags.has("force") ? { force: true } : {},
package/dist/cues.js CHANGED
@@ -28,20 +28,31 @@ function vttTime(seconds) {
28
28
  const p = (n, w = 2) => String(n).padStart(w, "0");
29
29
  return `${p(Math.floor(ms / 36e5))}:${p(Math.floor(ms % 36e5 / 6e4))}:${p(Math.floor(ms % 6e4 / 1e3))}.${p(ms % 1e3, 3)}`;
30
30
  }
31
+ /** The cue kinds that become WebVTT chapters by default (the YouTube use case). */
32
+ const DEFAULT_CHAPTER_KINDS = new Set(["chapter"]);
31
33
  /**
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).
34
+ * WebVTT chapters from cues. Only cues whose `kind` is in `kinds` (default just
35
+ * `'chapter'`) become chapters ad-break / plain `cue` markers stay out of the
36
+ * chapter list (they remain in `cues.json` for machines), so the VTT pastes
37
+ * straight into a YouTube description. Each chapter runs until its duration or
38
+ * the next chapter; the cue text is the human `title` (falling back to `name`),
39
+ * never the machine `kind`. When the earliest chapter starts after 0, a `00:00`
40
+ * "Intro" chapter is auto-anchored (valid WebVTT, and YouTube needs a 0:00 start).
41
+ *
42
+ * Author note — YouTube's chapter rules are author-side, not enforced here:
43
+ * it needs the FIRST chapter at exactly 0:00 (the auto-anchor handles this, but
44
+ * a tiny first chapter from a lead-in will read oddly — pin chapter 1 to t=0
45
+ * instead) and EVERY chapter to be >= 10s (fold a too-short beat into its
46
+ * neighbour). cues.json is unaffected; only the human-pasted VTT cares.
37
47
  */
38
- function cuesToVtt(cues, totalDuration) {
39
- const anchored = cues.length > 0 && cues[0].t > 0 ? [{
48
+ function cuesToVtt(cues, totalDuration, kinds = DEFAULT_CHAPTER_KINDS) {
49
+ const chapters = cues.filter((c) => kinds.has(c.kind));
50
+ const anchored = chapters.length > 0 && chapters[0].t > 0 ? [{
40
51
  t: 0,
41
52
  kind: "chapter",
42
53
  name: "Intro",
43
54
  title: "Intro"
44
- }, ...cues] : cues;
55
+ }, ...chapters] : chapters;
45
56
  let out = "WEBVTT\n\n";
46
57
  anchored.forEach((c, i) => {
47
58
  const end = c.duration !== void 0 ? c.t + c.duration : anchored[i + 1]?.t ?? totalDuration;
@@ -54,7 +65,7 @@ function cuesToVtt(cues, totalDuration) {
54
65
  * `<stem>.chapters.vtt`, next to the render output. Deterministic. Returns the
55
66
  * paths written.
56
67
  */
57
- function writeCueSidecars(outPath, markers, totalDuration, chaptersVtt) {
68
+ function writeCueSidecars(outPath, markers, totalDuration, chaptersVtt, chapterKinds = DEFAULT_CHAPTER_KINDS) {
58
69
  const cues = collectCues(markers);
59
70
  if (cues.length === 0) return [];
60
71
  const isFile = /\.(mp4|webm)$/i.test(outPath);
@@ -66,7 +77,7 @@ function writeCueSidecars(outPath, markers, totalDuration, chaptersVtt) {
66
77
  written.push(jsonPath);
67
78
  if (chaptersVtt) {
68
79
  const vttPath = join(dir, `${prefix}chapters.vtt`);
69
- writeFileSync(vttPath, cuesToVtt(cues, totalDuration));
80
+ writeFileSync(vttPath, cuesToVtt(cues, totalDuration, chapterKinds));
70
81
  written.push(vttPath);
71
82
  }
72
83
  return written;
package/dist/index.d.ts CHANGED
@@ -32,6 +32,8 @@ interface RenderOptions {
32
32
  sfx?: 'auto' | 'off';
33
33
  /** also write WebVTT chapters from cue markers ('vtt'); cues.json is always written when cues exist. */
34
34
  chapters?: 'vtt' | 'off';
35
+ /** cue kinds that become VTT chapters (default just 'chapter'); cues.json keeps all kinds. */
36
+ chapterKinds?: ReadonlySet<string>;
35
37
  /** --strict: font validation throws on an unregistered family / missing glyph (§3.6). Default dev-warn. */
36
38
  strictFonts?: boolean;
37
39
  onProgress?: (frame: number, total: number) => void;
package/dist/render.js CHANGED
@@ -126,7 +126,7 @@ async function render(opts) {
126
126
  process.stderr.write(`captions: ${srt}, ${vtt}\n`);
127
127
  };
128
128
  const emitCues = (target) => {
129
- const written = writeCueSidecars(target, compiled.markers, duration, opts.chapters === "vtt");
129
+ const written = writeCueSidecars(target, compiled.markers, duration, opts.chapters === "vtt", opts.chapterKinds);
130
130
  if (written.length) process.stderr.write(`cues: ${written.join(", ")}\n`);
131
131
  };
132
132
  if (!isVideo) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glissade/cli",
3
- "version": "0.9.0",
3
+ "version": "0.9.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.9.0",
24
- "@glissade/core": "0.9.0",
25
- "@glissade/interact": "0.9.0",
26
- "@glissade/lottie": "0.9.0",
27
- "@glissade/svg": "0.9.0",
28
- "@glissade/narrate": "0.9.0",
29
- "@glissade/player": "0.9.0",
30
- "@glissade/scene": "0.9.0",
31
- "@glissade/sfx": "0.9.0"
23
+ "@glissade/backend-skia": "0.9.1",
24
+ "@glissade/core": "0.9.1",
25
+ "@glissade/interact": "0.9.1",
26
+ "@glissade/lottie": "0.9.1",
27
+ "@glissade/svg": "0.9.1",
28
+ "@glissade/narrate": "0.9.1",
29
+ "@glissade/player": "0.9.1",
30
+ "@glissade/scene": "0.9.1",
31
+ "@glissade/sfx": "0.9.1"
32
32
  },
33
33
  "repository": {
34
34
  "type": "git",