@glissade/narrate 0.5.0-pre.3 → 0.5.0-pre.5

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/index.d.ts CHANGED
@@ -97,6 +97,12 @@ interface NarrationAnchors {
97
97
  duration(id: string): number;
98
98
  /** start + offset — a sub-beat inside a segment or pause window */
99
99
  at(id: string, offset?: number): number;
100
+ /**
101
+ * Assert every id exists in the manifest — a build-time fast-fail that lists
102
+ * ALL unknown ids at once (vs. discovering stale refs one render at a time
103
+ * after rewiring). Returns the anchors, so chain it: `narration(t).require([...])`.
104
+ */
105
+ require(ids: readonly string[]): NarrationAnchors;
100
106
  readonly totalDuration: number;
101
107
  /** '<id>.start' / '<id>.end' labels (segments + pauses) — merge into the timeline for studio visibility */
102
108
  labels(): Record<string, number>;
@@ -124,6 +130,10 @@ interface CaptionStyle {
124
130
  /** bottom inset as a fraction of scene height; defaults 0.10 (landscape) / 0.18 (portrait) */
125
131
  bottomInsetFrac?: number;
126
132
  lineHeight?: number;
133
+ /** lines a caption may use before it auto-shrinks to fit; default 2 */
134
+ maxLines?: number;
135
+ /** floor for auto-shrink, as a fraction of the base font size; default 0.7 */
136
+ minScale?: number;
127
137
  }
128
138
  /**
129
139
  * Bottom-centered captions inside the platform-safe area: portrait scenes
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { key, track } from "@glissade/core";
2
- import { Text, glow } from "@glissade/scene";
2
+ import { Text, breakLines, estimatingMeasurer, glow, quantize } from "@glissade/scene";
3
3
  //#region src/index.ts
4
4
  /**
5
5
  * @glissade/narrate — narration + captions, the PURE side. TTS happens only
@@ -39,11 +39,16 @@ function narration(timing) {
39
39
  if (!b) throw new NarrationError(`no narration beat '${id}' (have: ${[...byId.keys()].join(", ")})`);
40
40
  return b;
41
41
  };
42
- return {
42
+ const anchors = {
43
43
  start: (id) => beat(id).start,
44
44
  end: (id) => beat(id).start + beat(id).duration,
45
45
  duration: (id) => beat(id).duration,
46
46
  at: (id, offset = 0) => beat(id).start + offset,
47
+ require: (ids) => {
48
+ const missing = ids.filter((id) => !byId.has(id));
49
+ if (missing.length > 0) throw new NarrationError(`narration references unknown id${missing.length > 1 ? "s" : ""} ${missing.map((m) => `'${m}'`).join(", ")} — have: ${[...byId.keys()].join(", ")}`);
50
+ return anchors;
51
+ },
47
52
  totalDuration: timing.totalDuration,
48
53
  labels: () => {
49
54
  const out = {};
@@ -69,6 +74,7 @@ function narration(timing) {
69
74
  return out;
70
75
  }
71
76
  };
77
+ return anchors;
72
78
  }
73
79
  function captionTrack(timing, opts = {}) {
74
80
  const target = opts.target ?? "captions/text";
@@ -93,18 +99,47 @@ function captionTrack(timing, opts = {}) {
93
99
  function captionNode(size, style = {}) {
94
100
  const portrait = size.h > size.w;
95
101
  const inset = style.bottomInsetFrac ?? (portrait ? .18 : .1);
96
- return new Text({
102
+ const baseFont = style.fontSize ?? Math.round(Math.min(size.w, size.h) * (portrait ? .052 : .06));
103
+ const fontFamily = style.fontFamily ?? "sans-serif";
104
+ const width = Math.round(size.w * (style.widthFrac ?? .82));
105
+ const lineHeight = style.lineHeight ?? 1.3;
106
+ const maxLines = Math.max(1, style.maxLines ?? 2);
107
+ const minFont = Math.max(1, Math.round(baseFont * (style.minScale ?? .7)));
108
+ const bottomY = Math.round(size.h * (1 - inset));
109
+ const node = new Text({
97
110
  id: "captions",
98
111
  text: "",
99
112
  align: "center",
100
- fontSize: style.fontSize ?? Math.round(Math.min(size.w, size.h) * (portrait ? .052 : .06)),
113
+ fontSize: baseFont,
101
114
  ...style.fontFamily !== void 0 ? { fontFamily: style.fontFamily } : {},
102
115
  fill: style.fill ?? "#ffffff",
103
- width: Math.round(size.w * (style.widthFrac ?? .82)),
104
- lineHeight: style.lineHeight ?? 1.3,
105
- position: [size.w / 2, Math.round(size.h * (1 - inset))],
116
+ width,
117
+ lineHeight,
118
+ position: [size.w / 2, bottomY],
106
119
  filters: style.filters ?? glow("#000000cc", 3, 1)
107
120
  });
121
+ const lineCountAt = (font, m) => {
122
+ const t = node.text();
123
+ if (!t) return 0;
124
+ return breakLines(t, {
125
+ family: fontFamily,
126
+ size: font,
127
+ weight: node.fontWeight
128
+ }, width > 0 ? width : void 0, m).length;
129
+ };
130
+ node.fontSize.bindSource(() => {
131
+ const m = node.measurerSource?.() ?? estimatingMeasurer;
132
+ let font = baseFont;
133
+ while (font > minFont && lineCountAt(font, m) > maxLines) font -= 1;
134
+ return font;
135
+ });
136
+ node.position.bindSource(() => {
137
+ const m = node.measurerSource?.() ?? estimatingMeasurer;
138
+ const lines = Math.max(1, lineCountAt(node.fontSize(), m));
139
+ const step = quantize(node.fontSize() * lineHeight);
140
+ return [size.w / 2, bottomY - (lines - 1) * step];
141
+ });
142
+ return node;
108
143
  }
109
144
  /**
110
145
  * The bed-ducking envelope every narrated video needs: duck windows are the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glissade/narrate",
3
- "version": "0.5.0-pre.3",
3
+ "version": "0.5.0-pre.5",
4
4
  "description": "glissade narration + captions: TTS at prepare time (gs narrate), deterministic caching, narration-anchored timeline beats, and captions as plain tracks. Render stays offline.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -19,8 +19,8 @@
19
19
  "dist"
20
20
  ],
21
21
  "dependencies": {
22
- "@glissade/core": "0.5.0-pre.3",
23
- "@glissade/scene": "0.5.0-pre.3"
22
+ "@glissade/core": "0.5.0-pre.5",
23
+ "@glissade/scene": "0.5.0-pre.5"
24
24
  },
25
25
  "repository": {
26
26
  "type": "git",