@glissade/narrate 0.63.2-pre.1 → 0.64.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/index.d.ts +44 -1
- package/dist/index.js +53 -3
- package/package.json +3 -3
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 ??
|
|
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:
|
|
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.
|
|
3
|
+
"version": "0.64.0-pre.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.
|
|
26
|
-
"@glissade/scene": "0.
|
|
25
|
+
"@glissade/core": "0.64.0-pre.0",
|
|
26
|
+
"@glissade/scene": "0.64.0-pre.0"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
29
29
|
"kokoro-js": "^1.2.0"
|