@glissade/narrate 0.4.1 → 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 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.1",
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/core": "0.4.1",
23
- "@glissade/scene": "0.4.1"
22
+ "@glissade/scene": "0.4.3",
23
+ "@glissade/core": "0.4.3"
24
24
  },
25
25
  "repository": {
26
26
  "type": "git",