@glissade/narrate 0.63.2 → 0.64.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/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { AssetRef, AudioClip, Key, Track } from "@glissade/core";
2
2
  import { FilterSpec, Text } from "@glissade/scene";
3
+ import { SafeArea } from "@glissade/scene/diagnostics";
3
4
 
4
5
  //#region src/index.d.ts
5
6
 
@@ -254,6 +255,14 @@ interface CaptionCue {
254
255
  * drives the burned track AND the .srt/.vtt sidecars, so they match.
255
256
  */
256
257
  declare function splitCaption(segment: TimedSegment, maxChars?: number): CaptionCue[];
258
+ /**
259
+ * The canonical caption node id — the SINGLE source of truth `captionNode`,
260
+ * `captionTrack`, and `captionSafeArea` all pair on. Keeping it one string means
261
+ * `captionSafeArea(size).owner === captionNode(size).id` BY CONSTRUCTION, so a
262
+ * caption never SELF-COLLIDES with its own reserved band (the owner + its subtree
263
+ * are subtree-matched exempt in CAPTION_COLLISION).
264
+ */
265
+ declare const CAPTION_NODE_ID = "captions";
257
266
  declare function captionTrack(timing: NarrationTiming, opts?: CaptionTrackOptions): Track<string>;
258
267
  interface CaptionStyle {
259
268
  fontFamily?: string;
@@ -286,6 +295,40 @@ declare function captionNode(size: {
286
295
  w: number;
287
296
  h: number;
288
297
  }, style?: CaptionStyle): Text;
298
+ /**
299
+ * The TOP (device px) of the reserved caption band — the Y above which foreground
300
+ * art is safe and below which it collides with captions. Derived from `captionNode`'s
301
+ * OWN layout: the caption baseline sits at `h·(1−inset)` (inset 0.10 landscape /
302
+ * 0.18 portrait), and the band reserves a DEFAULT (2-line) caption block above that
303
+ * baseline down to the frame bottom, using captionNode's own font fraction /
304
+ * lineHeight / default maxLines. ONE `Math.round` → integer-stable + deterministic;
305
+ * landscape and portrait yield different tops (the platform-safe-area asymmetry).
306
+ */
307
+ declare function captionTop(size: {
308
+ w: number;
309
+ h: number;
310
+ }): number;
311
+ /**
312
+ * Build the reserved caption-band {@link SafeArea} for `critique`/`assess`'s
313
+ * `safeAreas` from narrate's OWN caption layout — the bottom band from
314
+ * {@link captionTop} to the frame bottom, OWNED by the caption node. Feed it to
315
+ * `assess(scene, tl, { safeAreas: [captionSafeArea(size)] })`: the caption + its
316
+ * children FILL the band (owner subtree-matched, no self-collision) while any other
317
+ * node intruding it raises CAPTION_COLLISION.
318
+ *
319
+ * The default `owner` is {@link CAPTION_NODE_ID} — the SAME id `captionNode(size)`
320
+ * pairs on — so the default is safe by construction. If you RENAME the caption node
321
+ * (`captionNode(size, …)` with a custom id via `new Text({ id })`), pass
322
+ * `captionSafeArea(size, { owner: '<your-id>' })` to keep the owner aligned (else the
323
+ * renamed caption becomes a non-owner intruding its own band — a false positive).
324
+ * Integer-pinned bounds (reuses {@link captionTop}; no new float math).
325
+ */
326
+ declare function captionSafeArea(size: {
327
+ w: number;
328
+ h: number;
329
+ }, opts?: {
330
+ owner?: string;
331
+ }): SafeArea;
289
332
  interface DuckOptions {
290
333
  /** gain while narration speaks; default 0.25 */
291
334
  duck?: number;
@@ -375,4 +418,4 @@ declare function music(timing: MusicTiming, at?: number): MusicAnchors;
375
418
  declare function toSrt(timing: NarrationTiming): string;
376
419
  declare function toVtt(timing: NarrationTiming): string;
377
420
  //#endregion
378
- export { BedMode, CaptionCue, CaptionStyle, CaptionTrackOptions, DuckOptions, MusicAnchors, MusicClipOptions, MusicTiming, NarrationAnchors, NarrationElement, NarrationError, NarrationPause, NarrationScript, NarrationSegment, NarrationTiming, TimedPause, TimedSegment, TimedWord, VoiceBlend, VoiceBlendEntry, VoiceSpec, captionNode, captionTrack, duckEnvelope, isPause, isVoiceBlend, music, narration, splitCaption, toSrt, toVtt, validateMusicTiming };
421
+ export { BedMode, CAPTION_NODE_ID, CaptionCue, CaptionStyle, CaptionTrackOptions, DuckOptions, MusicAnchors, MusicClipOptions, MusicTiming, NarrationAnchors, NarrationElement, NarrationError, NarrationPause, NarrationScript, NarrationSegment, NarrationTiming, TimedPause, TimedSegment, TimedWord, VoiceBlend, VoiceBlendEntry, VoiceSpec, captionNode, captionSafeArea, captionTop, captionTrack, duckEnvelope, isPause, isVoiceBlend, music, narration, splitCaption, toSrt, toVtt, validateMusicTiming };
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { key, track } from "@glissade/core";
2
2
  import { Text, breakLines, estimatingMeasurer, glow, quantize } from "@glissade/scene";
3
+ import "@glissade/scene/diagnostics";
3
4
  //#region src/index.ts
4
5
  /**
5
6
  * @glissade/narrate — narration + captions, the PURE side. TTS happens only
@@ -155,8 +156,16 @@ function splitCaption(segment, maxChars) {
155
156
  end: segment.start + (i + 1) * span
156
157
  }));
157
158
  }
159
+ /**
160
+ * The canonical caption node id — the SINGLE source of truth `captionNode`,
161
+ * `captionTrack`, and `captionSafeArea` all pair on. Keeping it one string means
162
+ * `captionSafeArea(size).owner === captionNode(size).id` BY CONSTRUCTION, so a
163
+ * caption never SELF-COLLIDES with its own reserved band (the owner + its subtree
164
+ * are subtree-matched exempt in CAPTION_COLLISION).
165
+ */
166
+ const CAPTION_NODE_ID = "captions";
158
167
  function captionTrack(timing, opts = {}) {
159
- const target = opts.target ?? "captions/text";
168
+ const target = opts.target ?? `captions/text`;
160
169
  const budget = timing.captionSplit?.maxChars;
161
170
  const keys = [key(0, "", { interp: "hold" })];
162
171
  let cursor = 0;
@@ -185,7 +194,7 @@ function captionNode(size, style = {}) {
185
194
  const lineHeight = style.lineHeight ?? 1.3;
186
195
  const bottomY = Math.round(size.h * (1 - inset));
187
196
  const node = new Text({
188
- id: "captions",
197
+ id: CAPTION_NODE_ID,
189
198
  text: "",
190
199
  align: "center",
191
200
  fontSize: baseFont,
@@ -226,6 +235,47 @@ function captionNode(size, style = {}) {
226
235
  return node;
227
236
  }
228
237
  /**
238
+ * The TOP (device px) of the reserved caption band — the Y above which foreground
239
+ * art is safe and below which it collides with captions. Derived from `captionNode`'s
240
+ * OWN layout: the caption baseline sits at `h·(1−inset)` (inset 0.10 landscape /
241
+ * 0.18 portrait), and the band reserves a DEFAULT (2-line) caption block above that
242
+ * baseline down to the frame bottom, using captionNode's own font fraction /
243
+ * lineHeight / default maxLines. ONE `Math.round` → integer-stable + deterministic;
244
+ * landscape and portrait yield different tops (the platform-safe-area asymmetry).
245
+ */
246
+ function captionTop(size) {
247
+ const portrait = size.h > size.w;
248
+ const inset = portrait ? .18 : .1;
249
+ const fontFrac = portrait ? .052 : .06;
250
+ return Math.round(size.h * (1 - inset) - Math.min(size.w, size.h) * fontFrac * 1.3 * 2);
251
+ }
252
+ /**
253
+ * Build the reserved caption-band {@link SafeArea} for `critique`/`assess`'s
254
+ * `safeAreas` from narrate's OWN caption layout — the bottom band from
255
+ * {@link captionTop} to the frame bottom, OWNED by the caption node. Feed it to
256
+ * `assess(scene, tl, { safeAreas: [captionSafeArea(size)] })`: the caption + its
257
+ * children FILL the band (owner subtree-matched, no self-collision) while any other
258
+ * node intruding it raises CAPTION_COLLISION.
259
+ *
260
+ * The default `owner` is {@link CAPTION_NODE_ID} — the SAME id `captionNode(size)`
261
+ * pairs on — so the default is safe by construction. If you RENAME the caption node
262
+ * (`captionNode(size, …)` with a custom id via `new Text({ id })`), pass
263
+ * `captionSafeArea(size, { owner: '<your-id>' })` to keep the owner aligned (else the
264
+ * renamed caption becomes a non-owner intruding its own band — a false positive).
265
+ * Integer-pinned bounds (reuses {@link captionTop}; no new float math).
266
+ */
267
+ function captionSafeArea(size, opts = {}) {
268
+ return {
269
+ bounds: {
270
+ minX: 0,
271
+ minY: captionTop(size),
272
+ maxX: size.w,
273
+ maxY: size.h
274
+ },
275
+ owner: opts.owner ?? "captions"
276
+ };
277
+ }
278
+ /**
229
279
  * The bed-ducking envelope every narrated video needs: duck windows are the
230
280
  * narration segments, with attack/release ramps and near-window merging. Pause
231
281
  * beats join in by their `bed` mode — `hold` (default) keeps the bed ducked
@@ -383,4 +433,4 @@ function toVtt(timing) {
383
433
  return "WEBVTT\n\n" + timing.segments.flatMap((s) => splitCaption(s, timing.captionSplit?.maxChars)).map((c) => `${srtTime(c.start, ".")} --> ${srtTime(c.end, ".")}\n${c.text}`).join("\n\n") + "\n";
384
434
  }
385
435
  //#endregion
386
- export { NarrationError, captionNode, captionTrack, duckEnvelope, isPause, isVoiceBlend, music, narration, splitCaption, toSrt, toVtt, validateMusicTiming };
436
+ export { CAPTION_NODE_ID, NarrationError, captionNode, captionSafeArea, captionTop, captionTrack, duckEnvelope, isPause, isVoiceBlend, music, narration, splitCaption, toSrt, toVtt, validateMusicTiming };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glissade/narrate",
3
- "version": "0.63.2",
3
+ "version": "0.64.0",
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
  "engines": {
@@ -22,8 +22,8 @@
22
22
  "dist"
23
23
  ],
24
24
  "dependencies": {
25
- "@glissade/core": "0.63.2",
26
- "@glissade/scene": "0.63.2"
25
+ "@glissade/core": "0.64.0",
26
+ "@glissade/scene": "0.64.0"
27
27
  },
28
28
  "peerDependencies": {
29
29
  "kokoro-js": "^1.2.0"