@glissade/narrate 0.4.2 → 0.4.3
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 +28 -2
- package/dist/index.js +40 -1
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AssetRef, AudioClip, Track } from "@glissade/core";
|
|
1
|
+
import { AssetRef, AudioClip, Key, Track } from "@glissade/core";
|
|
2
2
|
import { FilterSpec, Text } from "@glissade/scene";
|
|
3
3
|
|
|
4
4
|
//#region src/index.d.ts
|
|
@@ -92,7 +92,33 @@ declare function captionNode(size: {
|
|
|
92
92
|
w: number;
|
|
93
93
|
h: number;
|
|
94
94
|
}, style?: CaptionStyle): Text;
|
|
95
|
+
interface DuckOptions {
|
|
96
|
+
/** gain while narration speaks; default 0.25 */
|
|
97
|
+
duck?: number;
|
|
98
|
+
/** gain elsewhere; default 1 */
|
|
99
|
+
base?: number;
|
|
100
|
+
/** ramp-down seconds before a segment starts; default 0.15 */
|
|
101
|
+
attack?: number;
|
|
102
|
+
/** ramp-up seconds after a segment ends; default 0.4 */
|
|
103
|
+
release?: number;
|
|
104
|
+
/**
|
|
105
|
+
* Windows whose gap (after attack/release) is smaller than this stay
|
|
106
|
+
* ducked through — no pumping between close segments. Default 0.5.
|
|
107
|
+
*/
|
|
108
|
+
mergeGap?: number;
|
|
109
|
+
/** the music clip's `at` on the timeline; gain keys are CLIP-local. Default 0. */
|
|
110
|
+
clipAt?: number;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The bed-ducking envelope every narrated video needs: duck windows are the
|
|
114
|
+
* narration segments, with attack/release ramps and near-window merging.
|
|
115
|
+
* Pure function of the committed manifest — re-narrate and the ducking
|
|
116
|
+
* re-flows. Returns a keys-only gain envelope for AudioClip.gain.
|
|
117
|
+
*/
|
|
118
|
+
declare function duckEnvelope(timing: NarrationTiming, opts?: DuckOptions): {
|
|
119
|
+
keys: Key<number>[];
|
|
120
|
+
};
|
|
95
121
|
declare function toSrt(timing: NarrationTiming): string;
|
|
96
122
|
declare function toVtt(timing: NarrationTiming): string;
|
|
97
123
|
//#endregion
|
|
98
|
-
export { CaptionStyle, CaptionTrackOptions, NarrationAnchors, NarrationError, NarrationScript, NarrationSegment, NarrationTiming, TimedSegment, TimedWord, captionNode, captionTrack, narration, toSrt, toVtt };
|
|
124
|
+
export { CaptionStyle, CaptionTrackOptions, DuckOptions, NarrationAnchors, NarrationError, NarrationScript, NarrationSegment, NarrationTiming, TimedSegment, TimedWord, captionNode, captionTrack, duckEnvelope, narration, toSrt, toVtt };
|
package/dist/index.js
CHANGED
|
@@ -87,6 +87,45 @@ function captionNode(size, style = {}) {
|
|
|
87
87
|
filters: style.filters ?? glow("#000000cc", 3, 1)
|
|
88
88
|
});
|
|
89
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* The bed-ducking envelope every narrated video needs: duck windows are the
|
|
92
|
+
* narration segments, with attack/release ramps and near-window merging.
|
|
93
|
+
* Pure function of the committed manifest — re-narrate and the ducking
|
|
94
|
+
* re-flows. Returns a keys-only gain envelope for AudioClip.gain.
|
|
95
|
+
*/
|
|
96
|
+
function duckEnvelope(timing, opts = {}) {
|
|
97
|
+
const duck = opts.duck ?? .25;
|
|
98
|
+
const base = opts.base ?? 1;
|
|
99
|
+
const attack = opts.attack ?? .15;
|
|
100
|
+
const release = opts.release ?? .4;
|
|
101
|
+
const mergeGap = opts.mergeGap ?? .5;
|
|
102
|
+
const clipAt = opts.clipAt ?? 0;
|
|
103
|
+
const windows = [];
|
|
104
|
+
for (const s of [...timing.segments].sort((a, b) => a.start - b.start)) {
|
|
105
|
+
const last = windows[windows.length - 1];
|
|
106
|
+
if (last && s.start - last.end < attack + release + mergeGap) last.end = Math.max(last.end, s.start + s.duration);
|
|
107
|
+
else windows.push({
|
|
108
|
+
start: s.start,
|
|
109
|
+
end: s.start + s.duration
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
const keys = [];
|
|
113
|
+
for (const w of windows) {
|
|
114
|
+
const rampStart = w.start - attack - clipAt;
|
|
115
|
+
const down = w.start - clipAt;
|
|
116
|
+
const up = w.end - clipAt;
|
|
117
|
+
const rampEnd = w.end + release - clipAt;
|
|
118
|
+
if (rampEnd <= 0) continue;
|
|
119
|
+
if (rampStart > 0) keys.push(key(rampStart, base));
|
|
120
|
+
if (down > 0) keys.push(key(down, duck));
|
|
121
|
+
else if (keys.length === 0) keys.push(key(0, duck));
|
|
122
|
+
keys.push(key(Math.max(up, 1e-6), duck));
|
|
123
|
+
keys.push(key(rampEnd, base));
|
|
124
|
+
}
|
|
125
|
+
if (keys.length === 0) keys.push(key(0, base));
|
|
126
|
+
if (keys[0].t > 0) keys.unshift(key(0, base));
|
|
127
|
+
return { keys };
|
|
128
|
+
}
|
|
90
129
|
function srtTime(t, sep) {
|
|
91
130
|
const ms = Math.round(t * 1e3);
|
|
92
131
|
const h = Math.floor(ms / 36e5);
|
|
@@ -103,4 +142,4 @@ function toVtt(timing) {
|
|
|
103
142
|
return "WEBVTT\n\n" + timing.segments.map((s) => `${srtTime(s.start, ".")} --> ${srtTime(s.start + s.duration, ".")}\n${s.text}`).join("\n\n") + "\n";
|
|
104
143
|
}
|
|
105
144
|
//#endregion
|
|
106
|
-
export { NarrationError, captionNode, captionTrack, narration, toSrt, toVtt };
|
|
145
|
+
export { NarrationError, captionNode, captionTrack, duckEnvelope, narration, toSrt, toVtt };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glissade/narrate",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
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/
|
|
23
|
-
"@glissade/
|
|
22
|
+
"@glissade/scene": "0.4.3",
|
|
23
|
+
"@glissade/core": "0.4.3"
|
|
24
24
|
},
|
|
25
25
|
"repository": {
|
|
26
26
|
"type": "git",
|