@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 +10 -0
- package/dist/index.js +42 -7
- package/package.json +3 -3
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
|
-
|
|
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
|
-
|
|
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:
|
|
113
|
+
fontSize: baseFont,
|
|
101
114
|
...style.fontFamily !== void 0 ? { fontFamily: style.fontFamily } : {},
|
|
102
115
|
fill: style.fill ?? "#ffffff",
|
|
103
|
-
width
|
|
104
|
-
lineHeight
|
|
105
|
-
position: [size.w / 2,
|
|
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
|
+
"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.
|
|
23
|
-
"@glissade/scene": "0.5.0-pre.
|
|
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",
|