@sage-rsc/narrator-avatar 1.0.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/README.md +99 -0
- package/dist/narrator-avatar.js +530 -0
- package/dist/narrator-avatar.js.map +1 -0
- package/dist/narrator-avatar.umd.cjs +8 -0
- package/dist/narrator-avatar.umd.cjs.map +1 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# narrator-avatar
|
|
2
|
+
|
|
3
|
+
React component for 3D talking avatars with lip-sync, Deepgram or Google TTS, content-aware hand gestures, and pause/resume. Uses [@met4citizen/talkinghead](https://www.npmjs.com/package/@met4citizen/talkinghead) under the hood.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @sage-rsc/narrator-avatar
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
All required dependencies (including `@met4citizen/talkinghead` and React) are installed automatically.
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
Same API as in your app: render the component and control it via ref.
|
|
16
|
+
|
|
17
|
+
```jsx
|
|
18
|
+
import { useRef } from 'react';
|
|
19
|
+
import NarratorAvatar from '@sage-rsc/narrator-avatar';
|
|
20
|
+
|
|
21
|
+
function MyPage() {
|
|
22
|
+
const avatarRef = useRef(null);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div style={{ width: '400px', height: '500px' }}>
|
|
26
|
+
<NarratorAvatar
|
|
27
|
+
ref={avatarRef}
|
|
28
|
+
avatarUrl="/avatars/brunette.glb"
|
|
29
|
+
avatarBody="F"
|
|
30
|
+
ttsService="deepgram"
|
|
31
|
+
ttsVoice="aura-2-aurora-en"
|
|
32
|
+
ttsApiKey={import.meta.env.VITE_DEEPGRAM_API_KEY}
|
|
33
|
+
accurateLipSync={true}
|
|
34
|
+
speechRate={0.9}
|
|
35
|
+
onReady={() => console.log('Ready')}
|
|
36
|
+
onSpeechStart={(text) => console.log('Started:', text)}
|
|
37
|
+
onSpeechEnd={() => console.log('Ended')}
|
|
38
|
+
onSubtitle={(text) => console.log('Subtitle:', text)}
|
|
39
|
+
/>
|
|
40
|
+
<button onClick={() => avatarRef.current?.speakText('Hello! How are you?')}>
|
|
41
|
+
Speak
|
|
42
|
+
</button>
|
|
43
|
+
<button onClick={() => avatarRef.current?.pauseSpeaking()}>Pause</button>
|
|
44
|
+
<button onClick={() => avatarRef.current?.resumeSpeaking()}>Resume</button>
|
|
45
|
+
<button onClick={() => avatarRef.current?.stopSpeaking()}>Stop</button>
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Props
|
|
52
|
+
|
|
53
|
+
| Prop | Description |
|
|
54
|
+
|------|-------------|
|
|
55
|
+
| `avatarUrl` | URL to GLB model (e.g. `/avatars/brunette.glb`) |
|
|
56
|
+
| `avatarBody` | `'M'` or `'F'` for posture |
|
|
57
|
+
| `ttsService` | `'google'` or `'deepgram'` |
|
|
58
|
+
| `ttsVoice` | Deepgram: e.g. `aura-2-mars-en`, `aura-2-aurora-en`. Google: e.g. `en-GB-Standard-A` |
|
|
59
|
+
| `ttsApiKey` | API key for Deepgram (or set `VITE_DEEPGRAM_API_KEY` in env) |
|
|
60
|
+
| `accurateLipSync` | `true` = REST per phrase, best lip-sync + pause/resume |
|
|
61
|
+
| `speechRate` | e.g. `0.9` for 10% slower (pitch-preserving) |
|
|
62
|
+
| `speechGestures` | Content-aware hand gestures (default `true`) |
|
|
63
|
+
| `onReady`, `onError`, `onSpeechStart`, `onSpeechEnd`, `onSubtitle` | Callbacks |
|
|
64
|
+
|
|
65
|
+
## Ref API
|
|
66
|
+
|
|
67
|
+
- `speakText(text, options?)` – speak text (TTS)
|
|
68
|
+
- `pauseSpeaking()` – pause (phrase-level in accurateLipSync)
|
|
69
|
+
- `resumeSpeaking()` – continue from next phrase
|
|
70
|
+
- `stopSpeaking()` – stop and clear
|
|
71
|
+
- `isReady`, `isSpeaking` – state
|
|
72
|
+
|
|
73
|
+
## Using with Vite
|
|
74
|
+
|
|
75
|
+
If your app uses Vite, add this so the avatar’s dynamic lip-sync modules load correctly:
|
|
76
|
+
|
|
77
|
+
```js
|
|
78
|
+
// vite.config.js
|
|
79
|
+
export default defineConfig({
|
|
80
|
+
optimizeDeps: { exclude: ['@met4citizen/talkinghead'] },
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Build (for publishers)
|
|
85
|
+
|
|
86
|
+
To have the folder and filename match the package name (`narrator-avatar` / `NarratorAvatar`):
|
|
87
|
+
|
|
88
|
+
1. Rename the package folder to `narrator-avatar` (e.g. rename `react-talking-avatar` → `narrator-avatar`).
|
|
89
|
+
2. In that folder, rename `src/NarratorAvatar.jsx` → `src/NarratorAvatar.jsx`.
|
|
90
|
+
3. In `src/index.js`, change both imports from `'./NarratorAvatar.jsx'` to `'./NarratorAvatar.jsx'`.
|
|
91
|
+
|
|
92
|
+
Then:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
cd packages/narrator-avatar
|
|
96
|
+
npm install
|
|
97
|
+
npm run build
|
|
98
|
+
npm publish
|
|
99
|
+
```
|
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
import { jsxs as or, jsx as St } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef as ar, useRef as l, useState as It, useEffect as Pt, useCallback as H, useImperativeHandle as ur } from "react";
|
|
3
|
+
import { TalkingHead as cr } from "@met4citizen/talkinghead";
|
|
4
|
+
const B = {}, jt = "https://api.deepgram.com/v1/speak", ir = "wss://api.deepgram.com/v1/speak", qt = "encoding=linear16&container=wav&sample_rate=24000", Ht = 24e3, Mt = 44, Xt = 430;
|
|
5
|
+
function Zt(I) {
|
|
6
|
+
const n = I.trim();
|
|
7
|
+
return n ? n.split(new RegExp("(?<=[.!?])\\s+")).map((u) => u.trim()).filter(Boolean) : [];
|
|
8
|
+
}
|
|
9
|
+
function lr(I, n) {
|
|
10
|
+
if (n >= 1 || n <= 0) return I;
|
|
11
|
+
const o = new Int16Array(I), u = o.length;
|
|
12
|
+
if (u === 0) return I;
|
|
13
|
+
const T = Math.ceil(u / n), w = new Int16Array(T);
|
|
14
|
+
for (let A = 0; A < T; A++) {
|
|
15
|
+
const k = A * n, P = Math.floor(k), J = k - P, L = P >= u ? o[u - 1] : o[P], Y = P + 1 >= u ? o[u - 1] : o[P + 1], tt = (1 - J) * L + J * Y;
|
|
16
|
+
w[A] = Math.max(-32768, Math.min(32767, Math.round(tt)));
|
|
17
|
+
}
|
|
18
|
+
return w.buffer;
|
|
19
|
+
}
|
|
20
|
+
function vt(I, n) {
|
|
21
|
+
if (n.byteLength < Mt) return null;
|
|
22
|
+
const o = new DataView(n), u = o.getUint32(24, !0), T = Math.max(1, o.getUint16(22, !0)), w = n.byteLength - Mt, A = w / (2 * T), k = I.createBuffer(T, A, u), P = new Int16Array(n, Mt, w / 2 | 0);
|
|
23
|
+
for (let J = 0; J < T; J++) {
|
|
24
|
+
const L = k.getChannelData(J);
|
|
25
|
+
for (let Y = 0; Y < A; Y++)
|
|
26
|
+
L[Y] = P[Y * T + J] / 32768;
|
|
27
|
+
}
|
|
28
|
+
return k;
|
|
29
|
+
}
|
|
30
|
+
function dr(I) {
|
|
31
|
+
const n = I.trim().split(/\s+/).filter(Boolean);
|
|
32
|
+
if (n.length === 0)
|
|
33
|
+
return { words: [I || " "], wtimes: [0], wdurations: [Xt] };
|
|
34
|
+
const o = n.reduce((k, P) => k + P.length, 0) || 1, u = n.length * Xt, T = [], w = [];
|
|
35
|
+
let A = 0;
|
|
36
|
+
for (const k of n) {
|
|
37
|
+
T.push(A);
|
|
38
|
+
const P = k.length / o * u;
|
|
39
|
+
w.push(P), A += P;
|
|
40
|
+
}
|
|
41
|
+
return { words: n, wtimes: T, wdurations: w };
|
|
42
|
+
}
|
|
43
|
+
const Qt = ["handup", "index", "ok", "thumbup", "thumbdown", "side", "shrug", "namaste"];
|
|
44
|
+
function _t(I) {
|
|
45
|
+
const n = I.trim();
|
|
46
|
+
if (!n) return null;
|
|
47
|
+
const o = n.toLowerCase(), u = () => Math.random() > 0.5;
|
|
48
|
+
if (/\b(thank you|thanks|thank ya|welcome|bye|goodbye|see you|so long|namaste)\b/i.test(o))
|
|
49
|
+
return { name: "namaste", dur: 1.8, mirror: !1 };
|
|
50
|
+
if (/\?$/.test(n) || /\b(wait|hold on|one moment|hang on|let me ask|any questions?)\b/i.test(o) || /^(what|how|why|when|where|which|who|can you|could you|would you|do you|does|is it|are there)\b/i.test(n))
|
|
51
|
+
return { name: "handup", dur: 1.8, mirror: u() };
|
|
52
|
+
if (/\!$/.test(n) || /\b(great|awesome|excellent|love|perfect|yes|yeah|cool|amazing|wow|good job|well done|fantastic|brilliant|nice|wonderful|super|terrific|outstanding)\b/i.test(o))
|
|
53
|
+
return { name: "thumbup", dur: 1.8, mirror: u() };
|
|
54
|
+
if (/\b(wrong|bad idea|don't do that|never do|incorrect|nope|not right|that's wrong|avoid that)\b/i.test(o))
|
|
55
|
+
return { name: "thumbdown", dur: 1.6, mirror: u() };
|
|
56
|
+
if (/\b(no |not |don't|never |can't|won't|shouldn't|isn't|aren't|wasn't|weren't)\b/i.test(o) || /\b(no,|no\.|nah)\b/i.test(o))
|
|
57
|
+
return { name: "side", dur: 1.6, mirror: u() };
|
|
58
|
+
if (/\b(don't know|not sure|maybe|perhaps|might be|uncertain|i think|i guess|not certain|not really|depends|could be)\b/i.test(o))
|
|
59
|
+
return { name: "shrug", dur: 2, mirror: !1 };
|
|
60
|
+
if (/\b(first|second|third|one |two |three |number|remember|important|key|point|listen|look|note|step|next|then|finally|so,|so\.)\b/i.test(o) || /^(\d+[.)]\s)/.test(n))
|
|
61
|
+
return { name: "index", dur: 1.7, mirror: u() };
|
|
62
|
+
if (/\b(ok|okay|alright|sure|correct|right|exactly|got it|understood|done|ready|agreed|deal)\b/i.test(o))
|
|
63
|
+
return { name: "ok", dur: 1.6, mirror: u() };
|
|
64
|
+
const T = n.split("").reduce((A, k) => A * 31 + k.charCodeAt(0) | 0, 0), w = Qt[Math.abs(T) % Qt.length];
|
|
65
|
+
return { name: w, dur: 1.5, mirror: w === "shrug" || w === "namaste" ? !1 : u() };
|
|
66
|
+
}
|
|
67
|
+
const _ = "If the avatar still does not load, try opening this site in an Incognito/Private window or disabling browser extensions (e.g. MetaMask) for this origin.", xt = {
|
|
68
|
+
modelFPS: 60,
|
|
69
|
+
// Smoother animation (default 30)
|
|
70
|
+
modelPixelRatio: 2,
|
|
71
|
+
// Sharp on HiDPI (capped for perf)
|
|
72
|
+
modelMovementFactor: 0.85,
|
|
73
|
+
// Slightly subtler body movement
|
|
74
|
+
mixerGainSpeech: 2,
|
|
75
|
+
// Clearer speech volume
|
|
76
|
+
ttsTrimStart: 0,
|
|
77
|
+
// Viseme start trim (ms)
|
|
78
|
+
ttsTrimEnd: 300,
|
|
79
|
+
// Slightly less end trim for lip-sync (default 400)
|
|
80
|
+
avatarIdleEyeContact: 0.35,
|
|
81
|
+
// Natural idle eye contact [0,1]
|
|
82
|
+
avatarIdleHeadMove: 0.45,
|
|
83
|
+
// Natural idle head movement [0,1]
|
|
84
|
+
avatarSpeakingEyeContact: 0.6,
|
|
85
|
+
// Engagement while speaking [0,1]
|
|
86
|
+
avatarSpeakingHeadMove: 0.55,
|
|
87
|
+
// Head movement while speaking [0,1]
|
|
88
|
+
lightAmbientIntensity: 1.5,
|
|
89
|
+
// Slightly brighter ambient
|
|
90
|
+
lightDirectIntensity: 15,
|
|
91
|
+
// Clearer main light
|
|
92
|
+
cameraRotateEnable: !0,
|
|
93
|
+
// Allow user to rotate view
|
|
94
|
+
cameraZoomEnable: !0,
|
|
95
|
+
// Allow zoom for UX
|
|
96
|
+
cameraPanEnable: !1
|
|
97
|
+
}, fr = ar(({
|
|
98
|
+
avatarUrl: I = "/avatars/brunette.glb",
|
|
99
|
+
avatarBody: n = "F",
|
|
100
|
+
cameraView: o = "mid",
|
|
101
|
+
mood: u = "neutral",
|
|
102
|
+
ttsLang: T = "en-GB",
|
|
103
|
+
ttsVoice: w = "en-GB-Standard-A",
|
|
104
|
+
ttsService: A = "google",
|
|
105
|
+
// 'google' | 'deepgram'
|
|
106
|
+
ttsApiKey: k = null,
|
|
107
|
+
ttsEndpoint: P = "https://texttospeech.googleapis.com/v1beta1/text:synthesize",
|
|
108
|
+
lipsyncModules: J = ["en"],
|
|
109
|
+
lipsyncLang: L = "en",
|
|
110
|
+
// Smoothness overrides (merged with SMOOTH_DEFAULTS)
|
|
111
|
+
modelFPS: Y,
|
|
112
|
+
modelPixelRatio: tt,
|
|
113
|
+
modelMovementFactor: Dt,
|
|
114
|
+
mixerGainSpeech: Nt,
|
|
115
|
+
avatarIdleEyeContact: rt,
|
|
116
|
+
avatarIdleHeadMove: et,
|
|
117
|
+
avatarSpeakingEyeContact: nt,
|
|
118
|
+
avatarSpeakingHeadMove: st,
|
|
119
|
+
onReady: Ct = () => {
|
|
120
|
+
},
|
|
121
|
+
onError: $t = () => {
|
|
122
|
+
},
|
|
123
|
+
onSpeechStart: Ot = () => {
|
|
124
|
+
},
|
|
125
|
+
onSpeechEnd: Lt = () => {
|
|
126
|
+
},
|
|
127
|
+
onSubtitle: Rt = null,
|
|
128
|
+
/** Slower speech, same voice (e.g. 0.9 = 10% slower). Pitch-preserving time-stretch only in streaming. */
|
|
129
|
+
speechRate: ht = 1,
|
|
130
|
+
/** Use REST per phrase for exact word timings = perfect lip-sync (slightly slower start per phrase). */
|
|
131
|
+
accurateLipSync: pt = !1,
|
|
132
|
+
/** Enable content-aware hand gestures while speaking (default true). */
|
|
133
|
+
speechGestures: gt = !0,
|
|
134
|
+
/** Optional: (phrase) => { name, dur?, mirror? } | null to override or extend gesture choice. */
|
|
135
|
+
getGestureForPhrase: wt = null,
|
|
136
|
+
className: tr = "",
|
|
137
|
+
style: rr = {}
|
|
138
|
+
}, er) => {
|
|
139
|
+
const yt = l(null), m = l(null), [nr, ot] = It(!0), [Gt, at] = It(null), [Kt, sr] = It(!1), M = l(null), Bt = l(A), ut = l(k), ct = l(w), it = l(Ct), U = l($t), lt = l(Ot), c = l(Lt), $ = l(Rt), Wt = l(ht), bt = l(pt), At = l(gt), X = l(wt);
|
|
140
|
+
Pt(() => {
|
|
141
|
+
Wt.current = Math.max(0.6, Math.min(1.2, Number(ht) || 1)), bt.current = !!pt, At.current = !!gt, X.current = wt;
|
|
142
|
+
}, [ht, pt, gt, wt]), Pt(() => {
|
|
143
|
+
Bt.current = A, ut.current = k, ct.current = w, it.current = Ct, U.current = $t, lt.current = Ot, c.current = Lt, $.current = Rt;
|
|
144
|
+
});
|
|
145
|
+
const v = l(!1), dt = l(!1), Tt = l(null), x = l(!1), Q = l(!1), W = l(null);
|
|
146
|
+
Pt(() => {
|
|
147
|
+
const t = yt.current;
|
|
148
|
+
if (!t) return;
|
|
149
|
+
v.current = !1, dt.current = !1;
|
|
150
|
+
let s = !1, h = null, e = !1, f = null, p = null;
|
|
151
|
+
const D = () => {
|
|
152
|
+
var O;
|
|
153
|
+
if (s || e) return;
|
|
154
|
+
const g = yt.current;
|
|
155
|
+
if (!g || g.offsetWidth <= 0 || g.offsetHeight <= 0) return;
|
|
156
|
+
e = !0, p && p.disconnect();
|
|
157
|
+
const R = A === "deepgram", G = !R && (k || (B == null ? void 0 : B.VITE_GOOGLE_TTS_API_KEY) || ""), E = {
|
|
158
|
+
...xt,
|
|
159
|
+
cameraView: o,
|
|
160
|
+
lipsyncModules: J,
|
|
161
|
+
lipsyncLang: L,
|
|
162
|
+
ttsLang: T,
|
|
163
|
+
ttsVoice: w,
|
|
164
|
+
ttsTrimStart: xt.ttsTrimStart,
|
|
165
|
+
ttsTrimEnd: xt.ttsTrimEnd,
|
|
166
|
+
...Y != null && { modelFPS: Y },
|
|
167
|
+
...tt != null && { modelPixelRatio: tt },
|
|
168
|
+
...Dt != null && { modelMovementFactor: Dt },
|
|
169
|
+
...Nt != null && { mixerGainSpeech: Nt },
|
|
170
|
+
...rt != null && { avatarIdleEyeContact: rt },
|
|
171
|
+
...et != null && { avatarIdleHeadMove: et },
|
|
172
|
+
...nt != null && { avatarSpeakingEyeContact: nt },
|
|
173
|
+
...st != null && { avatarSpeakingHeadMove: st },
|
|
174
|
+
...R ? { ttsEndpoint: null, ttsApikey: null } : G ? { ttsEndpoint: P, ttsApikey: G } : { ttsEndpoint: null, ttsApikey: null }
|
|
175
|
+
};
|
|
176
|
+
try {
|
|
177
|
+
f = new cr(g, E);
|
|
178
|
+
} catch (r) {
|
|
179
|
+
if (s) return;
|
|
180
|
+
ot(!1);
|
|
181
|
+
const a = (r == null ? void 0 : r.message) ?? String(r);
|
|
182
|
+
at(a ? `${a}
|
|
183
|
+
|
|
184
|
+
${_}` : _), (O = U.current) == null || O.call(U, r);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
m.current = f;
|
|
188
|
+
const N = {
|
|
189
|
+
url: I,
|
|
190
|
+
body: n,
|
|
191
|
+
avatarMood: u,
|
|
192
|
+
ttsLang: T,
|
|
193
|
+
ttsVoice: w,
|
|
194
|
+
lipsyncLang: L,
|
|
195
|
+
...rt != null && { avatarIdleEyeContact: rt },
|
|
196
|
+
...et != null && { avatarIdleHeadMove: et },
|
|
197
|
+
...nt != null && { avatarSpeakingEyeContact: nt },
|
|
198
|
+
...st != null && { avatarSpeakingHeadMove: st }
|
|
199
|
+
};
|
|
200
|
+
h = setTimeout(() => {
|
|
201
|
+
v.current || dt.current || (ot(!1), at(`Avatar failed to load (timeout). Check that the model file exists (e.g. /avatars/brunette.glb).
|
|
202
|
+
|
|
203
|
+
` + _));
|
|
204
|
+
}, 15e3), f.showAvatar(N, (r) => {
|
|
205
|
+
r != null && r.lengthComputable && r.loaded != null && r.total != null && Math.min(100, Math.round(r.loaded / r.total * 100));
|
|
206
|
+
}).then(() => {
|
|
207
|
+
var r;
|
|
208
|
+
v.current || (dt.current = !0, h && clearTimeout(h), h = null, f.start(), ot(!1), sr(!0), at(null), (r = it.current) == null || r.call(it));
|
|
209
|
+
}).catch((r) => {
|
|
210
|
+
var i;
|
|
211
|
+
if (v.current) return;
|
|
212
|
+
dt.current = !0, h && clearTimeout(h), h = null, ot(!1);
|
|
213
|
+
const a = (r == null ? void 0 : r.message) || String(r);
|
|
214
|
+
at(a ? `${a}
|
|
215
|
+
|
|
216
|
+
${_}` : _), (i = U.current) == null || i.call(U, r);
|
|
217
|
+
});
|
|
218
|
+
};
|
|
219
|
+
return p = new ResizeObserver(() => D()), p.observe(t), requestAnimationFrame(() => D()), () => {
|
|
220
|
+
if (s = !0, v.current = !0, h && clearTimeout(h), p && p.disconnect(), M.current && clearInterval(M.current), m.current) {
|
|
221
|
+
try {
|
|
222
|
+
m.current.stop(), m.current.stopSpeaking();
|
|
223
|
+
} catch {
|
|
224
|
+
}
|
|
225
|
+
m.current = null;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
}, []);
|
|
229
|
+
const ft = H(() => {
|
|
230
|
+
M.current && clearInterval(M.current), M.current = setInterval(() => {
|
|
231
|
+
var t;
|
|
232
|
+
m.current && !m.current.isSpeaking && (M.current && clearInterval(M.current), M.current = null, (t = c.current) == null || t.call(c));
|
|
233
|
+
}, 200);
|
|
234
|
+
}, []), j = H(() => {
|
|
235
|
+
var s;
|
|
236
|
+
const t = (s = m.current) == null ? void 0 : s.audioCtx;
|
|
237
|
+
(t == null ? void 0 : t.state) === "suspended" && t.resume();
|
|
238
|
+
}, []), Ut = H(
|
|
239
|
+
async (t, s = {}, h) => {
|
|
240
|
+
const e = m.current;
|
|
241
|
+
if (!(e != null && e.audioCtx)) return;
|
|
242
|
+
j();
|
|
243
|
+
const f = ut.current || (B == null ? void 0 : B.VITE_DEEPGRAM_API_KEY) || "";
|
|
244
|
+
if (!f) {
|
|
245
|
+
console.warn("NarratorAvatar: Deepgram TTS requires ttsApiKey or VITE_DEEPGRAM_API_KEY");
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const p = s.ttsVoice || ct.current || "aura-2-thalia-en", D = new URLSearchParams({
|
|
249
|
+
encoding: "linear16",
|
|
250
|
+
sample_rate: String(Ht),
|
|
251
|
+
model: p
|
|
252
|
+
}), g = `${ir}?${D.toString()}`, R = Wt.current, G = Zt(t), E = G.length > 0 ? G : [t.trim() || " "], N = { current: 0 }, O = { current: null }, r = { current: !0 }, a = { current: E[0] };
|
|
253
|
+
let i = null, V = null;
|
|
254
|
+
const z = new Promise((d, C) => {
|
|
255
|
+
i = d, V = C;
|
|
256
|
+
}), mt = () => {
|
|
257
|
+
var C, K, q, y;
|
|
258
|
+
if (x.current) {
|
|
259
|
+
(C = O.current) == null || C.close(), W.current = null, i == null || i();
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (N.current += 1, N.current >= E.length) {
|
|
263
|
+
(K = c.current) == null || K.call(c), (q = O.current) == null || q.close(), W.current = null, i == null || i();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
r.current = !0, a.current = E[N.current], (y = $.current) == null || y.call($, a.current);
|
|
267
|
+
const d = O.current;
|
|
268
|
+
d && d.readyState === WebSocket.OPEN && (d.send(JSON.stringify({ type: "Speak", text: a.current })), d.send(JSON.stringify({ type: "Flush" })));
|
|
269
|
+
};
|
|
270
|
+
e.isStreaming || (Tt.current || (Tt.current = e.streamStart(
|
|
271
|
+
{
|
|
272
|
+
sampleRate: Ht,
|
|
273
|
+
waitForAudioChunks: !0,
|
|
274
|
+
lipsyncType: "words",
|
|
275
|
+
lipsyncLang: s.lipsyncLang || L
|
|
276
|
+
},
|
|
277
|
+
null,
|
|
278
|
+
mt,
|
|
279
|
+
null,
|
|
280
|
+
null
|
|
281
|
+
)), await Tt.current), x.current = !1;
|
|
282
|
+
const S = new WebSocket(g, ["token", f]);
|
|
283
|
+
return O.current = S, W.current = S, S.binaryType = "arraybuffer", S.onopen = () => {
|
|
284
|
+
var d;
|
|
285
|
+
r.current = !0, a.current = E[0], (d = $.current) == null || d.call($, E[0]), S.send(JSON.stringify({ type: "Speak", text: E[0] })), S.send(JSON.stringify({ type: "Flush" }));
|
|
286
|
+
}, S.onmessage = (d) => {
|
|
287
|
+
var q, y;
|
|
288
|
+
if (typeof d.data == "string") {
|
|
289
|
+
try {
|
|
290
|
+
const b = JSON.parse(d.data);
|
|
291
|
+
(b.type === "Flushed" || b.type === "Cleared") && e.streamNotifyEnd();
|
|
292
|
+
} catch {
|
|
293
|
+
}
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
let C = d.data instanceof ArrayBuffer ? d.data : (q = d.data) == null ? void 0 : q.buffer;
|
|
297
|
+
if (!C || C.byteLength === 0 || !e.isStreaming) return;
|
|
298
|
+
R < 1 && (C = lr(C, R));
|
|
299
|
+
const K = a.current;
|
|
300
|
+
if (r.current && K) {
|
|
301
|
+
if (r.current = !1, At.current && e.playGesture) {
|
|
302
|
+
const Z = (y = X.current) == null ? void 0 : y.call(X, K), F = Z ?? _t(K);
|
|
303
|
+
F != null && F.name && e.playGesture(F.name, F.dur ?? 1.8, F.mirror ?? !1, 800);
|
|
304
|
+
}
|
|
305
|
+
let { words: b, wtimes: kt, wdurations: Et } = dr(K);
|
|
306
|
+
if (R < 1) {
|
|
307
|
+
const Z = 1 / R;
|
|
308
|
+
kt = kt.map((F) => F * Z), Et = Et.map((F) => F * Z);
|
|
309
|
+
}
|
|
310
|
+
e.streamAudio({ audio: C, words: b, wtimes: kt, wdurations: Et });
|
|
311
|
+
} else
|
|
312
|
+
e.streamAudio({ audio: C });
|
|
313
|
+
}, S.onerror = () => {
|
|
314
|
+
V == null || V(new Error("Deepgram WebSocket error"));
|
|
315
|
+
}, S.onclose = () => {
|
|
316
|
+
var d;
|
|
317
|
+
O.current = null, W.current === S && (W.current = null), N.current < E.length && ((d = c.current) == null || d.call(c)), i == null || i();
|
|
318
|
+
}, z;
|
|
319
|
+
},
|
|
320
|
+
[L, j]
|
|
321
|
+
), Vt = H(
|
|
322
|
+
async (t, s = {}, h) => {
|
|
323
|
+
var G, E, N, O;
|
|
324
|
+
const e = m.current;
|
|
325
|
+
if (!(e != null && e.audioCtx)) return;
|
|
326
|
+
x.current = !1, Q.current = !1, j();
|
|
327
|
+
const f = ut.current || (B == null ? void 0 : B.VITE_DEEPGRAM_API_KEY) || "";
|
|
328
|
+
if (!f) {
|
|
329
|
+
console.warn("NarratorAvatar: Deepgram TTS requires ttsApiKey or VITE_DEEPGRAM_API_KEY");
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
e.isStreaming && e.streamStop();
|
|
333
|
+
const p = s.ttsVoice || ct.current || "aura-2-thalia-en", D = `${jt}?model=${encodeURIComponent(p)}&${qt}`, g = Zt(t), R = g.length > 0 ? g : [t.trim() || " "];
|
|
334
|
+
for (let r = 0; r < R.length && !x.current; r++) {
|
|
335
|
+
const a = R[r];
|
|
336
|
+
(G = $.current) == null || G.call($, a);
|
|
337
|
+
const i = await fetch(D, {
|
|
338
|
+
method: "POST",
|
|
339
|
+
headers: {
|
|
340
|
+
Authorization: `Token ${f}`,
|
|
341
|
+
"Content-Type": "application/json",
|
|
342
|
+
Accept: "audio/wav"
|
|
343
|
+
},
|
|
344
|
+
body: JSON.stringify({ text: a })
|
|
345
|
+
});
|
|
346
|
+
if (x.current) break;
|
|
347
|
+
if (!i.ok) throw new Error(`Deepgram TTS error: ${i.status} ${i.statusText}`);
|
|
348
|
+
const V = await i.arrayBuffer();
|
|
349
|
+
if (x.current) break;
|
|
350
|
+
const z = vt(e.audioCtx, V);
|
|
351
|
+
if (!z) throw new Error("Failed to prepare audio");
|
|
352
|
+
const mt = z.duration * 1e3, S = a.trim().split(/\s+/).filter(Boolean), d = S.reduce((y, b) => y + b.length, 0) || 1;
|
|
353
|
+
let C = 0;
|
|
354
|
+
const K = [], q = [];
|
|
355
|
+
for (const y of S) {
|
|
356
|
+
K.push(C);
|
|
357
|
+
const b = y.length / d * mt;
|
|
358
|
+
q.push(b), C += b;
|
|
359
|
+
}
|
|
360
|
+
if (K.length === 0 && (S.push(a), K.push(0), q.push(mt)), At.current && e.playGesture) {
|
|
361
|
+
const y = (E = X.current) == null ? void 0 : E.call(X, a), b = y ?? _t(a);
|
|
362
|
+
b != null && b.name && e.playGesture(b.name, b.dur ?? 1.8, b.mirror ?? !1, 800);
|
|
363
|
+
}
|
|
364
|
+
for (e.speakAudio(
|
|
365
|
+
{ audio: z, words: S, wtimes: K, wdurations: q },
|
|
366
|
+
{ lipsyncLang: s.lipsyncLang || L },
|
|
367
|
+
null
|
|
368
|
+
); (N = m.current) != null && N.isSpeaking && !x.current; )
|
|
369
|
+
await new Promise((y) => setTimeout(y, 100));
|
|
370
|
+
if (x.current) break;
|
|
371
|
+
for (; Q.current && !x.current; )
|
|
372
|
+
await new Promise((y) => setTimeout(y, 100));
|
|
373
|
+
if (x.current) break;
|
|
374
|
+
}
|
|
375
|
+
x.current || (O = c.current) == null || O.call(c);
|
|
376
|
+
},
|
|
377
|
+
[L, j]
|
|
378
|
+
);
|
|
379
|
+
H(
|
|
380
|
+
async (t, s = {}, h) => {
|
|
381
|
+
const e = m.current;
|
|
382
|
+
if (!(e != null && e.audioCtx)) return;
|
|
383
|
+
j();
|
|
384
|
+
const f = ut.current || (B == null ? void 0 : B.VITE_DEEPGRAM_API_KEY) || "";
|
|
385
|
+
if (!f) {
|
|
386
|
+
console.warn("NarratorAvatar: Deepgram TTS requires ttsApiKey or VITE_DEEPGRAM_API_KEY");
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
const p = s.ttsVoice || ct.current || "aura-2-thalia-en", D = `${jt}?model=${encodeURIComponent(p)}&${qt}`, g = await fetch(D, {
|
|
390
|
+
method: "POST",
|
|
391
|
+
headers: {
|
|
392
|
+
Authorization: `Token ${f}`,
|
|
393
|
+
"Content-Type": "application/json",
|
|
394
|
+
Accept: "audio/wav"
|
|
395
|
+
},
|
|
396
|
+
body: JSON.stringify({ text: t })
|
|
397
|
+
});
|
|
398
|
+
if (!g.ok)
|
|
399
|
+
throw new Error(`Deepgram TTS error: ${g.status} ${g.statusText}`);
|
|
400
|
+
const R = await g.arrayBuffer(), G = vt(e.audioCtx, R);
|
|
401
|
+
if (!G)
|
|
402
|
+
throw new Error("Failed to prepare audio for playback");
|
|
403
|
+
const E = G.duration * 1e3, N = t.trim().split(/\s+/).filter(Boolean), O = N.reduce((V, z) => V + z.length, 0) || 1;
|
|
404
|
+
let r = 0;
|
|
405
|
+
const a = [], i = [];
|
|
406
|
+
for (const V of N) {
|
|
407
|
+
a.push(r);
|
|
408
|
+
const z = V.length / O * E;
|
|
409
|
+
i.push(z), r += z;
|
|
410
|
+
}
|
|
411
|
+
a.length === 0 && (a.push(0), i.push(E), N.push(t)), j(), e.speakAudio(
|
|
412
|
+
{
|
|
413
|
+
audio: G,
|
|
414
|
+
words: N,
|
|
415
|
+
wtimes: a,
|
|
416
|
+
wdurations: i
|
|
417
|
+
},
|
|
418
|
+
{ lipsyncLang: s.lipsyncLang || L },
|
|
419
|
+
h
|
|
420
|
+
), ft();
|
|
421
|
+
},
|
|
422
|
+
[L, ft, j]
|
|
423
|
+
);
|
|
424
|
+
const zt = H((t, s = {}) => {
|
|
425
|
+
var e;
|
|
426
|
+
if (!m.current) return;
|
|
427
|
+
x.current = !1, Q.current = !1, j(), (e = lt.current) == null || e.call(lt, t);
|
|
428
|
+
const h = (f) => {
|
|
429
|
+
var D;
|
|
430
|
+
const p = Array.isArray(f) ? f.join(" ") : typeof f == "string" ? f : "";
|
|
431
|
+
p && ((D = $.current) == null || D.call($, p));
|
|
432
|
+
};
|
|
433
|
+
Bt.current === "deepgram" ? (bt.current ? Vt : Ut)(t, s, h).catch((p) => {
|
|
434
|
+
var D, g;
|
|
435
|
+
console.error("Deepgram TTS failed:", p), (D = c.current) == null || D.call(c), (g = U.current) == null || g.call(U, p);
|
|
436
|
+
}) : (m.current.speakText(t, s, h), ft());
|
|
437
|
+
}, [Ut, Vt, ft, j]), Ft = H(() => {
|
|
438
|
+
var s;
|
|
439
|
+
const t = m.current;
|
|
440
|
+
if (bt.current && t && !t.isStreaming) {
|
|
441
|
+
Q.current = !0, t.pauseSpeaking();
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
if (x.current = !0, W.current) {
|
|
445
|
+
try {
|
|
446
|
+
W.current.close();
|
|
447
|
+
} catch {
|
|
448
|
+
}
|
|
449
|
+
W.current = null;
|
|
450
|
+
}
|
|
451
|
+
t && (t.isStreaming ? t.streamInterrupt() : t.pauseSpeaking()), M.current && (clearInterval(M.current), M.current = null), (s = c.current) == null || s.call(c);
|
|
452
|
+
}, []), Jt = H(() => {
|
|
453
|
+
var s;
|
|
454
|
+
if (x.current = !0, W.current) {
|
|
455
|
+
try {
|
|
456
|
+
W.current.close();
|
|
457
|
+
} catch {
|
|
458
|
+
}
|
|
459
|
+
W.current = null;
|
|
460
|
+
}
|
|
461
|
+
M.current && (clearInterval(M.current), M.current = null);
|
|
462
|
+
const t = m.current;
|
|
463
|
+
t && (t.isStreaming ? t.streamInterrupt() : t.stopSpeaking()), (s = c.current) == null || s.call(c);
|
|
464
|
+
}, []), Yt = H(async () => {
|
|
465
|
+
Q.current = !1;
|
|
466
|
+
}, []);
|
|
467
|
+
return ur(er, () => ({
|
|
468
|
+
speakText: zt,
|
|
469
|
+
pauseSpeaking: Ft,
|
|
470
|
+
resumeSpeaking: Yt,
|
|
471
|
+
stopSpeaking: Jt,
|
|
472
|
+
isReady: Kt,
|
|
473
|
+
get isSpeaking() {
|
|
474
|
+
var t;
|
|
475
|
+
return !!((t = m.current) != null && t.isSpeaking);
|
|
476
|
+
},
|
|
477
|
+
talkingHead: m.current
|
|
478
|
+
}), [zt, Ft, Yt, Jt, Kt]), /* @__PURE__ */ or("div", { className: `narrator-avatar-container ${tr}`, style: { position: "relative", ...rr }, children: [
|
|
479
|
+
/* @__PURE__ */ St(
|
|
480
|
+
"div",
|
|
481
|
+
{
|
|
482
|
+
ref: yt,
|
|
483
|
+
className: "talking-head-viewer",
|
|
484
|
+
style: { width: "100%", height: "100%", minHeight: "400px" }
|
|
485
|
+
}
|
|
486
|
+
),
|
|
487
|
+
nr && /* @__PURE__ */ St(
|
|
488
|
+
"div",
|
|
489
|
+
{
|
|
490
|
+
className: "loading-overlay",
|
|
491
|
+
style: {
|
|
492
|
+
position: "absolute",
|
|
493
|
+
top: "50%",
|
|
494
|
+
left: "50%",
|
|
495
|
+
transform: "translate(-50%, -50%)",
|
|
496
|
+
color: "#333",
|
|
497
|
+
fontSize: "18px",
|
|
498
|
+
zIndex: 10
|
|
499
|
+
},
|
|
500
|
+
children: "Loading avatar..."
|
|
501
|
+
}
|
|
502
|
+
),
|
|
503
|
+
Gt && /* @__PURE__ */ St(
|
|
504
|
+
"div",
|
|
505
|
+
{
|
|
506
|
+
className: "error-overlay",
|
|
507
|
+
style: {
|
|
508
|
+
position: "absolute",
|
|
509
|
+
top: "50%",
|
|
510
|
+
left: "50%",
|
|
511
|
+
transform: "translate(-50%, -50%)",
|
|
512
|
+
color: "#c00",
|
|
513
|
+
fontSize: "14px",
|
|
514
|
+
textAlign: "center",
|
|
515
|
+
zIndex: 10,
|
|
516
|
+
padding: "20px",
|
|
517
|
+
maxWidth: "90%",
|
|
518
|
+
whiteSpace: "pre-line"
|
|
519
|
+
},
|
|
520
|
+
children: Gt
|
|
521
|
+
}
|
|
522
|
+
)
|
|
523
|
+
] });
|
|
524
|
+
});
|
|
525
|
+
fr.displayName = "NarratorAvatar";
|
|
526
|
+
export {
|
|
527
|
+
fr as NarratorAvatar,
|
|
528
|
+
fr as default
|
|
529
|
+
};
|
|
530
|
+
//# sourceMappingURL=narrator-avatar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"narrator-avatar.js","sources":["../src/NarratorAvatar.jsx"],"sourcesContent":["/**\n * React wrapper for @met4citizen/talkinghead (original TalkingHead).\n * Exposes: speakText, pauseSpeaking, stopSpeaking, resumeSpeaking (no-op).\n * TTS: ttsService 'google' | 'deepgram'; for Deepgram set ttsApiKey + ttsVoice (e.g. aura-2-mars-en).\n *\n * Smoothest output: uses SMOOTH_DEFAULTS (60 FPS, pixel ratio 2, movement factor 0.85,\n * mixer gain 2, tuned trim/lighting/eye contact). Override via props: modelFPS, modelPixelRatio,\n * modelMovementFactor, mixerGainSpeech, avatarIdleEyeContact, avatarIdleHeadMove,\n * avatarSpeakingEyeContact, avatarSpeakingHeadMove.\n */\n\nimport React, { useRef, useEffect, useCallback, useImperativeHandle, forwardRef, useState } from 'react';\nimport { TalkingHead } from '@met4citizen/talkinghead';\n\nconst DEEPGRAM_SPEAK_URL = 'https://api.deepgram.com/v1/speak';\nconst DEEPGRAM_SPEAK_WS_URL = 'wss://api.deepgram.com/v1/speak';\n/** Request linear16 WAV to avoid slow MP3 decode – faster time-to-first-audio. */\nconst DEEPGRAM_FAST_RESPONSE_PARAMS = 'encoding=linear16&container=wav&sample_rate=24000';\nconst STREAMING_SAMPLE_RATE = 24000;\nconst WAV_HEADER_SIZE = 44; // standard PCM WAV header length\n/** For streaming lip-sync when we don't have real word boundaries from the API. */\nconst ESTIMATED_MS_PER_WORD = 430;\n\n/** Split text into phrases (sentences) so we can stream and subtitle in natural breaks. */\nfunction splitIntoPhrases(text) {\n const t = text.trim();\n if (!t) return [];\n const parts = t.split(/(?<=[.!?])\\s+/);\n return parts.map((p) => p.trim()).filter(Boolean);\n}\n\n/** Time-stretch PCM (more samples = slower playback). Preserves pitch; linear interpolation to keep voice natural. */\nfunction stretchPCM(arrayBuffer, rate) {\n if (rate >= 1 || rate <= 0) return arrayBuffer;\n const int16 = new Int16Array(arrayBuffer);\n const n = int16.length;\n if (n === 0) return arrayBuffer;\n const outLen = Math.ceil(n / rate);\n const out = new Int16Array(outLen);\n for (let i = 0; i < outLen; i++) {\n const src = i * rate;\n const j = Math.floor(src);\n const f = src - j;\n const s0 = j >= n ? int16[n - 1] : int16[j];\n const s1 = j + 1 >= n ? int16[n - 1] : int16[j + 1];\n const v = (1 - f) * s0 + f * s1;\n out[i] = Math.max(-32768, Math.min(32767, Math.round(v)));\n }\n return out.buffer;\n}\n\n/**\n * Build an AudioBuffer from PCM WAV. Avoids decodeAudioData so the avatar can start\n * speaking as soon as the API responds – better for child-friendly responsiveness.\n */\nfunction wavToAudioBuffer(audioCtx, wavArrayBuffer) {\n if (wavArrayBuffer.byteLength < WAV_HEADER_SIZE) return null;\n const view = new DataView(wavArrayBuffer);\n const sampleRate = view.getUint32(24, true);\n const numChannels = Math.max(1, view.getUint16(22, true));\n const totalBytes = wavArrayBuffer.byteLength - WAV_HEADER_SIZE;\n const numSamplesPerChannel = totalBytes / (2 * numChannels); // 16-bit = 2 bytes per sample\n const audioBuffer = audioCtx.createBuffer(numChannels, numSamplesPerChannel, sampleRate);\n const int16 = new Int16Array(wavArrayBuffer, WAV_HEADER_SIZE, (totalBytes / 2) | 0);\n for (let ch = 0; ch < numChannels; ch++) {\n const channel = audioBuffer.getChannelData(ch);\n for (let i = 0; i < numSamplesPerChannel; i++) {\n channel[i] = int16[i * numChannels + ch] / 32768;\n }\n }\n return audioBuffer;\n}\n\nfunction estimatedWordTimings(text) {\n const words = text.trim().split(/\\s+/).filter(Boolean);\n if (words.length === 0) {\n return { words: [text || ' '], wtimes: [0], wdurations: [ESTIMATED_MS_PER_WORD] };\n }\n const totalLen = words.reduce((s, w) => s + w.length, 0) || 1;\n const totalMs = words.length * ESTIMATED_MS_PER_WORD;\n const wtimes = [];\n const wdurations = [];\n let t = 0;\n for (const w of words) {\n wtimes.push(t);\n const dur = (w.length / totalLen) * totalMs;\n wdurations.push(dur);\n t += dur;\n }\n return { words, wtimes, wdurations };\n}\n\n/** All hand gestures supported by talkinghead (wider vocabulary, less repetition). */\nconst GESTURE_NAMES = ['handup', 'index', 'ok', 'thumbup', 'thumbdown', 'side', 'shrug', 'namaste'];\n\n/**\n * Picks a gesture that matches the phrase content (generic, dynamic).\n * Returns { name, dur, mirror } or null to skip gesture.\n * Uses all 8 gestures and more trigger categories for variety.\n */\nfunction pickGestureForPhrase(phrase) {\n const t = phrase.trim();\n if (!t) return null;\n const lower = t.toLowerCase();\n const mir = () => Math.random() > 0.5;\n\n // Thanks / greeting / closing -> namaste\n if (/\\b(thank you|thanks|thank ya|welcome|bye|goodbye|see you|so long|namaste)\\b/i.test(lower)) {\n return { name: 'namaste', dur: 1.8, mirror: false };\n }\n // Question / asking / wait -> hand up\n if (/\\?$/.test(t) || /\\b(wait|hold on|one moment|hang on|let me ask|any questions?)\\b/i.test(lower) ||\n /^(what|how|why|when|where|which|who|can you|could you|would you|do you|does|is it|are there)\\b/i.test(t)) {\n return { name: 'handup', dur: 1.8, mirror: mir() };\n }\n // Enthusiasm / positive -> thumb up\n if (/\\!$/.test(t) || /\\b(great|awesome|excellent|love|perfect|yes|yeah|cool|amazing|wow|good job|well done|fantastic|brilliant|nice|wonderful|super|terrific|outstanding)\\b/i.test(lower)) {\n return { name: 'thumbup', dur: 1.8, mirror: mir() };\n }\n // Strong disapproval / wrong -> thumb down\n if (/\\b(wrong|bad idea|don't do that|never do|incorrect|nope|not right|that's wrong|avoid that)\\b/i.test(lower)) {\n return { name: 'thumbdown', dur: 1.6, mirror: mir() };\n }\n // Softer no / not / disagreement -> side (hand wave)\n if (/\\b(no |not |don't|never |can't|won't|shouldn't|isn't|aren't|wasn't|weren't)\\b/i.test(lower) || /\\b(no,|no\\.|nah)\\b/i.test(lower)) {\n return { name: 'side', dur: 1.6, mirror: mir() };\n }\n // Uncertainty -> shrug\n if (/\\b(don't know|not sure|maybe|perhaps|might be|uncertain|i think|i guess|not certain|not really|depends|could be)\\b/i.test(lower)) {\n return { name: 'shrug', dur: 2, mirror: false };\n }\n // Listing / emphasis / steps -> index\n if (/\\b(first|second|third|one |two |three |number|remember|important|key|point|listen|look|note|step|next|then|finally|so,|so\\.)\\b/i.test(lower) || /^(\\d+[.)]\\s)/.test(t)) {\n return { name: 'index', dur: 1.7, mirror: mir() };\n }\n // Approval / agreement / ready -> ok\n if (/\\b(ok|okay|alright|sure|correct|right|exactly|got it|understood|done|ready|agreed|deal)\\b/i.test(lower)) {\n return { name: 'ok', dur: 1.6, mirror: mir() };\n }\n // Neutral: spread across all 8 gestures by phrase hash (less repetition)\n const hash = t.split('').reduce((h, c) => (h * 31 + c.charCodeAt(0)) | 0, 0);\n const name = GESTURE_NAMES[Math.abs(hash) % GESTURE_NAMES.length];\n return { name, dur: 1.5, mirror: name === 'shrug' || name === 'namaste' ? false : mir() };\n}\n\n/** Shown when init/load fails; suggests extension or CSP as cause. */\nconst EXTENSION_FIX_MESSAGE =\n 'If the avatar still does not load, try opening this site in an Incognito/Private window or disabling browser extensions (e.g. MetaMask) for this origin.';\n\n/**\n * Default options tuned for smoothest output (animation, lip-sync, audio, lighting).\n * See @met4citizen/talkinghead README for all options.\n */\nconst SMOOTH_DEFAULTS = {\n modelFPS: 60, // Smoother animation (default 30)\n modelPixelRatio: 2, // Sharp on HiDPI (capped for perf)\n modelMovementFactor: 0.85, // Slightly subtler body movement\n mixerGainSpeech: 2, // Clearer speech volume\n ttsTrimStart: 0, // Viseme start trim (ms)\n ttsTrimEnd: 300, // Slightly less end trim for lip-sync (default 400)\n avatarIdleEyeContact: 0.35, // Natural idle eye contact [0,1]\n avatarIdleHeadMove: 0.45, // Natural idle head movement [0,1]\n avatarSpeakingEyeContact: 0.6, // Engagement while speaking [0,1]\n avatarSpeakingHeadMove: 0.55, // Head movement while speaking [0,1]\n lightAmbientIntensity: 1.5, // Slightly brighter ambient\n lightDirectIntensity: 15, // Clearer main light\n cameraRotateEnable: true, // Allow user to rotate view\n cameraZoomEnable: true, // Allow zoom for UX\n cameraPanEnable: false,\n};\n\nconst NarratorAvatar = forwardRef(({\n avatarUrl = '/avatars/brunette.glb',\n avatarBody = 'F',\n cameraView = 'mid',\n mood = 'neutral',\n ttsLang = 'en-GB',\n ttsVoice = 'en-GB-Standard-A',\n ttsService = 'google', // 'google' | 'deepgram'\n ttsApiKey = null,\n ttsEndpoint = 'https://texttospeech.googleapis.com/v1beta1/text:synthesize',\n lipsyncModules = ['en'],\n lipsyncLang = 'en',\n // Smoothness overrides (merged with SMOOTH_DEFAULTS)\n modelFPS,\n modelPixelRatio,\n modelMovementFactor,\n mixerGainSpeech,\n avatarIdleEyeContact,\n avatarIdleHeadMove,\n avatarSpeakingEyeContact,\n avatarSpeakingHeadMove,\n onReady = () => {},\n onError = () => {},\n onSpeechStart = () => {},\n onSpeechEnd = () => {},\n onSubtitle = null,\n /** Slower speech, same voice (e.g. 0.9 = 10% slower). Pitch-preserving time-stretch only in streaming. */\n speechRate = 1,\n /** Use REST per phrase for exact word timings = perfect lip-sync (slightly slower start per phrase). */\n accurateLipSync = false,\n /** Enable content-aware hand gestures while speaking (default true). */\n speechGestures = true,\n /** Optional: (phrase) => { name, dur?, mirror? } | null to override or extend gesture choice. */\n getGestureForPhrase = null,\n className = '',\n style = {},\n}, ref) => {\n const containerRef = useRef(null);\n const headRef = useRef(null);\n const [isLoading, setIsLoading] = useState(true);\n const [error, setError] = useState(null);\n const [isReady, setIsReady] = useState(false);\n const speechEndCheckRef = useRef(null);\n const ttsServiceRef = useRef(ttsService);\n const ttsApiKeyRef = useRef(ttsApiKey);\n const ttsVoiceRef = useRef(ttsVoice);\n const onReadyRef = useRef(onReady);\n const onErrorRef = useRef(onError);\n const onSpeechStartRef = useRef(onSpeechStart);\n const onSpeechEndRef = useRef(onSpeechEnd);\n const onSubtitleRef = useRef(onSubtitle);\n const speechRateRef = useRef(speechRate);\n const accurateLipSyncRef = useRef(accurateLipSync);\n const speechGesturesRef = useRef(speechGestures);\n const getGestureForPhraseRef = useRef(getGestureForPhrase);\n\n useEffect(() => {\n speechRateRef.current = Math.max(0.6, Math.min(1.2, Number(speechRate) || 1));\n accurateLipSyncRef.current = !!accurateLipSync;\n speechGesturesRef.current = !!speechGestures;\n getGestureForPhraseRef.current = getGestureForPhrase;\n }, [speechRate, accurateLipSync, speechGestures, getGestureForPhrase]);\n\n useEffect(() => {\n ttsServiceRef.current = ttsService;\n ttsApiKeyRef.current = ttsApiKey;\n ttsVoiceRef.current = ttsVoice;\n onReadyRef.current = onReady;\n onErrorRef.current = onError;\n onSpeechStartRef.current = onSpeechStart;\n onSpeechEndRef.current = onSpeechEnd;\n onSubtitleRef.current = onSubtitle;\n });\n\n const loadCancelledRef = useRef(false);\n const loadResolvedRef = useRef(false);\n const streamingStartPromiseRef = useRef(null);\n /** When true, phrase loop and streaming should stop (Stop clicked). */\n const speechAbortedRef = useRef(false);\n /** When true, phrase loop is paused (Pause clicked); resumeSpeaking() clears it so loop continues. */\n const speechPausedRef = useRef(false);\n /** Active stream WebSocket so we can close it on Stop/Pause. */\n const streamWsRef = useRef(null);\n\n useEffect(() => {\n const node = containerRef.current;\n if (!node) return;\n\n loadCancelledRef.current = false;\n loadResolvedRef.current = false;\n let cancelled = false;\n let timeoutId = null;\n let initDone = false;\n let head = null;\n let ro = null;\n\n const doInit = () => {\n if (cancelled || initDone) return;\n const el = containerRef.current;\n if (!el || el.offsetWidth <= 0 || el.offsetHeight <= 0) return;\n initDone = true;\n if (ro) ro.disconnect();\n const useDeepgram = ttsService === 'deepgram';\n const googleKey = !useDeepgram && (ttsApiKey || import.meta.env?.VITE_GOOGLE_TTS_API_KEY || '');\n const options = {\n ...SMOOTH_DEFAULTS,\n cameraView,\n lipsyncModules,\n lipsyncLang,\n ttsLang,\n ttsVoice,\n ttsTrimStart: SMOOTH_DEFAULTS.ttsTrimStart,\n ttsTrimEnd: SMOOTH_DEFAULTS.ttsTrimEnd,\n ...(modelFPS != null && { modelFPS }),\n ...(modelPixelRatio != null && { modelPixelRatio }),\n ...(modelMovementFactor != null && { modelMovementFactor }),\n ...(mixerGainSpeech != null && { mixerGainSpeech }),\n ...(avatarIdleEyeContact != null && { avatarIdleEyeContact }),\n ...(avatarIdleHeadMove != null && { avatarIdleHeadMove }),\n ...(avatarSpeakingEyeContact != null && { avatarSpeakingEyeContact }),\n ...(avatarSpeakingHeadMove != null && { avatarSpeakingHeadMove }),\n ...(useDeepgram\n ? { ttsEndpoint: null, ttsApikey: null }\n : googleKey\n ? { ttsEndpoint, ttsApikey: googleKey }\n : { ttsEndpoint: null, ttsApikey: null }),\n };\n try {\n head = new TalkingHead(el, options);\n } catch (initErr) {\n if (cancelled) return;\n setIsLoading(false);\n const msg = initErr?.message ?? String(initErr);\n setError(msg ? `${msg}\\n\\n${EXTENSION_FIX_MESSAGE}` : EXTENSION_FIX_MESSAGE);\n onErrorRef.current?.(initErr);\n return;\n }\n headRef.current = head;\n\n const avatarConfig = {\n url: avatarUrl,\n body: avatarBody,\n avatarMood: mood,\n ttsLang,\n ttsVoice,\n lipsyncLang,\n ...(avatarIdleEyeContact != null && { avatarIdleEyeContact }),\n ...(avatarIdleHeadMove != null && { avatarIdleHeadMove }),\n ...(avatarSpeakingEyeContact != null && { avatarSpeakingEyeContact }),\n ...(avatarSpeakingHeadMove != null && { avatarSpeakingHeadMove }),\n };\n\n timeoutId = setTimeout(() => {\n if (loadCancelledRef.current || loadResolvedRef.current) return;\n setIsLoading(false);\n setError('Avatar failed to load (timeout). Check that the model file exists (e.g. /avatars/brunette.glb).\\n\\n' + EXTENSION_FIX_MESSAGE);\n }, 15000);\n\n head\n .showAvatar(avatarConfig, (ev) => {\n if (ev?.lengthComputable && ev.loaded != null && ev.total != null) {\n const pct = Math.min(100, Math.round((ev.loaded / ev.total) * 100));\n }\n })\n .then(() => {\n if (loadCancelledRef.current) return;\n loadResolvedRef.current = true;\n if (timeoutId) clearTimeout(timeoutId);\n timeoutId = null;\n head.start();\n setIsLoading(false);\n setIsReady(true);\n setError(null);\n onReadyRef.current?.();\n })\n .catch((err) => {\n if (loadCancelledRef.current) return;\n loadResolvedRef.current = true;\n if (timeoutId) clearTimeout(timeoutId);\n timeoutId = null;\n setIsLoading(false);\n const msg = err?.message || String(err);\n setError(msg ? `${msg}\\n\\n${EXTENSION_FIX_MESSAGE}` : EXTENSION_FIX_MESSAGE);\n onErrorRef.current?.(err);\n });\n };\n\n ro = new ResizeObserver(() => doInit());\n ro.observe(node);\n requestAnimationFrame(() => doInit());\n\n return () => {\n cancelled = true;\n loadCancelledRef.current = true;\n if (timeoutId) clearTimeout(timeoutId);\n if (ro) ro.disconnect();\n if (speechEndCheckRef.current) clearInterval(speechEndCheckRef.current);\n if (headRef.current) {\n try {\n headRef.current.stop();\n headRef.current.stopSpeaking();\n } catch (e) {}\n headRef.current = null;\n }\n };\n }, []); // mount once; config changes would require remount to take effect\n\n const startSpeechEndPolling = useCallback(() => {\n if (speechEndCheckRef.current) clearInterval(speechEndCheckRef.current);\n speechEndCheckRef.current = setInterval(() => {\n if (headRef.current && !headRef.current.isSpeaking) {\n if (speechEndCheckRef.current) clearInterval(speechEndCheckRef.current);\n speechEndCheckRef.current = null;\n onSpeechEndRef.current?.();\n }\n }, 200);\n }, []);\n\n const resumeAudioContext = useCallback(() => {\n const ctx = headRef.current?.audioCtx;\n if (ctx?.state === 'suspended') ctx.resume();\n }, []);\n\n const speakWithDeepgramStreaming = useCallback(\n async (text, options = {}, onsubtitles) => {\n const head = headRef.current;\n if (!head?.audioCtx) return;\n resumeAudioContext();\n const apiKey = ttsApiKeyRef.current || import.meta.env?.VITE_DEEPGRAM_API_KEY || '';\n if (!apiKey) {\n console.warn('NarratorAvatar: Deepgram TTS requires ttsApiKey or VITE_DEEPGRAM_API_KEY');\n return;\n }\n const model = options.ttsVoice || ttsVoiceRef.current || 'aura-2-thalia-en';\n const params = new URLSearchParams({\n encoding: 'linear16',\n sample_rate: String(STREAMING_SAMPLE_RATE),\n model,\n });\n const wsUrl = `${DEEPGRAM_SPEAK_WS_URL}?${params.toString()}`;\n const rate = speechRateRef.current;\n const phrases = splitIntoPhrases(text);\n const phraseList = phrases.length > 0 ? phrases : [text.trim() || ' '];\n\n const phraseIndexRef = { current: 0 };\n const wsRef = { current: null };\n const firstChunkRef = { current: true };\n const currentPhraseRef = { current: phraseList[0] };\n let resolveAll = null;\n let rejectAll = null;\n const allDonePromise = new Promise((res, rej) => {\n resolveAll = res;\n rejectAll = rej;\n });\n\n const onAudioEnd = () => {\n if (speechAbortedRef.current) {\n wsRef.current?.close();\n streamWsRef.current = null;\n resolveAll?.();\n return;\n }\n phraseIndexRef.current += 1;\n if (phraseIndexRef.current >= phraseList.length) {\n onSpeechEndRef.current?.();\n wsRef.current?.close();\n streamWsRef.current = null;\n resolveAll?.();\n return;\n }\n firstChunkRef.current = true;\n currentPhraseRef.current = phraseList[phraseIndexRef.current];\n onSubtitleRef.current?.(currentPhraseRef.current);\n const ws = wsRef.current;\n if (ws && ws.readyState === WebSocket.OPEN) {\n ws.send(JSON.stringify({ type: 'Speak', text: currentPhraseRef.current }));\n ws.send(JSON.stringify({ type: 'Flush' }));\n }\n };\n\n if (!head.isStreaming) {\n if (!streamingStartPromiseRef.current) {\n streamingStartPromiseRef.current = head.streamStart(\n {\n sampleRate: STREAMING_SAMPLE_RATE,\n waitForAudioChunks: true,\n lipsyncType: 'words',\n lipsyncLang: options.lipsyncLang || lipsyncLang,\n },\n null,\n onAudioEnd,\n null,\n null\n );\n }\n await streamingStartPromiseRef.current;\n }\n\n speechAbortedRef.current = false;\n const ws = new WebSocket(wsUrl, ['token', apiKey]);\n wsRef.current = ws;\n streamWsRef.current = ws;\n ws.binaryType = 'arraybuffer';\n\n ws.onopen = () => {\n firstChunkRef.current = true;\n currentPhraseRef.current = phraseList[0];\n onSubtitleRef.current?.(phraseList[0]);\n ws.send(JSON.stringify({ type: 'Speak', text: phraseList[0] }));\n ws.send(JSON.stringify({ type: 'Flush' }));\n };\n\n ws.onmessage = (event) => {\n if (typeof event.data === 'string') {\n try {\n const msg = JSON.parse(event.data);\n if (msg.type === 'Flushed' || msg.type === 'Cleared') {\n head.streamNotifyEnd();\n }\n } catch (_) {}\n return;\n }\n let buf = event.data instanceof ArrayBuffer ? event.data : event.data?.buffer;\n if (!buf || buf.byteLength === 0 || !head.isStreaming) return;\n if (rate < 1) buf = stretchPCM(buf, rate);\n const phrase = currentPhraseRef.current;\n if (firstChunkRef.current && phrase) {\n firstChunkRef.current = false;\n if (speechGesturesRef.current && head.playGesture) {\n const custom = getGestureForPhraseRef.current?.(phrase);\n const g = custom !== undefined && custom !== null ? custom : pickGestureForPhrase(phrase);\n if (g?.name) head.playGesture(g.name, g.dur ?? 1.8, g.mirror ?? false, 800);\n }\n let { words, wtimes, wdurations } = estimatedWordTimings(phrase);\n if (rate < 1) {\n const scale = 1 / rate;\n wtimes = wtimes.map((t) => t * scale);\n wdurations = wdurations.map((d) => d * scale);\n }\n head.streamAudio({ audio: buf, words, wtimes, wdurations });\n } else {\n head.streamAudio({ audio: buf });\n }\n };\n\n ws.onerror = () => {\n rejectAll?.(new Error('Deepgram WebSocket error'));\n };\n ws.onclose = () => {\n wsRef.current = null;\n if (streamWsRef.current === ws) streamWsRef.current = null;\n if (phraseIndexRef.current < phraseList.length) {\n onSpeechEndRef.current?.();\n }\n resolveAll?.();\n };\n\n return allDonePromise;\n },\n [lipsyncLang, resumeAudioContext]\n );\n\n /** REST per phrase: exact duration → exact word timings → perfect lip-sync. Phrase subtitles. */\n const speakWithDeepgramAccurateLipSync = useCallback(\n async (text, options = {}, onsubtitles) => {\n const head = headRef.current;\n if (!head?.audioCtx) return;\n speechAbortedRef.current = false;\n speechPausedRef.current = false;\n resumeAudioContext();\n const apiKey = ttsApiKeyRef.current || import.meta.env?.VITE_DEEPGRAM_API_KEY || '';\n if (!apiKey) {\n console.warn('NarratorAvatar: Deepgram TTS requires ttsApiKey or VITE_DEEPGRAM_API_KEY');\n return;\n }\n if (head.isStreaming) head.streamStop();\n const model = options.ttsVoice || ttsVoiceRef.current || 'aura-2-thalia-en';\n const url = `${DEEPGRAM_SPEAK_URL}?model=${encodeURIComponent(model)}&${DEEPGRAM_FAST_RESPONSE_PARAMS}`;\n const phrases = splitIntoPhrases(text);\n const phraseList = phrases.length > 0 ? phrases : [text.trim() || ' '];\n\n for (let i = 0; i < phraseList.length; i++) {\n if (speechAbortedRef.current) break;\n const phrase = phraseList[i];\n onSubtitleRef.current?.(phrase);\n const res = await fetch(url, {\n method: 'POST',\n headers: {\n Authorization: `Token ${apiKey}`,\n 'Content-Type': 'application/json',\n Accept: 'audio/wav',\n },\n body: JSON.stringify({ text: phrase }),\n });\n if (speechAbortedRef.current) break;\n if (!res.ok) throw new Error(`Deepgram TTS error: ${res.status} ${res.statusText}`);\n const arrayBuffer = await res.arrayBuffer();\n if (speechAbortedRef.current) break;\n const audioBuffer = wavToAudioBuffer(head.audioCtx, arrayBuffer);\n if (!audioBuffer) throw new Error('Failed to prepare audio');\n const durationMs = audioBuffer.duration * 1000;\n const words = phrase.trim().split(/\\s+/).filter(Boolean);\n const totalLen = words.reduce((s, w) => s + w.length, 0) || 1;\n let t = 0;\n const wtimes = [];\n const wdurations = [];\n for (const w of words) {\n wtimes.push(t);\n const dur = (w.length / totalLen) * durationMs;\n wdurations.push(dur);\n t += dur;\n }\n if (wtimes.length === 0) {\n words.push(phrase);\n wtimes.push(0);\n wdurations.push(durationMs);\n }\n if (speechGesturesRef.current && head.playGesture) {\n const custom = getGestureForPhraseRef.current?.(phrase);\n const g = custom !== undefined && custom !== null ? custom : pickGestureForPhrase(phrase);\n if (g?.name) head.playGesture(g.name, g.dur ?? 1.8, g.mirror ?? false, 800);\n }\n head.speakAudio(\n { audio: audioBuffer, words, wtimes, wdurations },\n { lipsyncLang: options.lipsyncLang || lipsyncLang },\n null\n );\n while (headRef.current?.isSpeaking && !speechAbortedRef.current) {\n await new Promise((r) => setTimeout(r, 100));\n }\n if (speechAbortedRef.current) break;\n // Pause: wait until user clicks Resume or Stop\n while (speechPausedRef.current && !speechAbortedRef.current) {\n await new Promise((r) => setTimeout(r, 100));\n }\n if (speechAbortedRef.current) break;\n }\n if (!speechAbortedRef.current) onSpeechEndRef.current?.();\n },\n [lipsyncLang, resumeAudioContext]\n );\n\n const speakWithDeepgram = useCallback(\n async (text, options = {}, onsubtitles) => {\n const head = headRef.current;\n if (!head?.audioCtx) return;\n resumeAudioContext();\n const apiKey = ttsApiKeyRef.current || import.meta.env?.VITE_DEEPGRAM_API_KEY || '';\n if (!apiKey) {\n console.warn('NarratorAvatar: Deepgram TTS requires ttsApiKey or VITE_DEEPGRAM_API_KEY');\n return;\n }\n const model = options.ttsVoice || ttsVoiceRef.current || 'aura-2-thalia-en';\n const url = `${DEEPGRAM_SPEAK_URL}?model=${encodeURIComponent(model)}&${DEEPGRAM_FAST_RESPONSE_PARAMS}`;\n const res = await fetch(url, {\n method: 'POST',\n headers: {\n Authorization: `Token ${apiKey}`,\n 'Content-Type': 'application/json',\n Accept: 'audio/wav',\n },\n body: JSON.stringify({ text }),\n });\n if (!res.ok) {\n throw new Error(`Deepgram TTS error: ${res.status} ${res.statusText}`);\n }\n const arrayBuffer = await res.arrayBuffer();\n const audioBuffer = wavToAudioBuffer(head.audioCtx, arrayBuffer);\n if (!audioBuffer) {\n throw new Error('Failed to prepare audio for playback');\n }\n const durationMs = audioBuffer.duration * 1000;\n // Word-level timing for smoother lip-sync and word-level subtitles (proportional by length)\n const words = text.trim().split(/\\s+/).filter(Boolean);\n const totalLen = words.reduce((s, w) => s + w.length, 0) || 1;\n let t = 0;\n const wtimes = [];\n const wdurations = [];\n for (const w of words) {\n wtimes.push(t);\n const dur = (w.length / totalLen) * durationMs;\n wdurations.push(dur);\n t += dur;\n }\n if (wtimes.length === 0) {\n wtimes.push(0);\n wdurations.push(durationMs);\n words.push(text);\n }\n resumeAudioContext();\n head.speakAudio(\n {\n audio: audioBuffer,\n words,\n wtimes,\n wdurations,\n },\n { lipsyncLang: options.lipsyncLang || lipsyncLang },\n onsubtitles\n );\n startSpeechEndPolling();\n },\n [lipsyncLang, startSpeechEndPolling, resumeAudioContext]\n );\n\n const speakText = useCallback((text, options = {}) => {\n if (!headRef.current) return;\n speechAbortedRef.current = false;\n speechPausedRef.current = false;\n resumeAudioContext();\n onSpeechStartRef.current?.(text);\n const onsubtitles = (sub) => {\n const t = Array.isArray(sub) ? sub.join(' ') : typeof sub === 'string' ? sub : '';\n if (t) onSubtitleRef.current?.(t);\n };\n\n if (ttsServiceRef.current === 'deepgram') {\n const fn = accurateLipSyncRef.current ? speakWithDeepgramAccurateLipSync : speakWithDeepgramStreaming;\n fn(text, options, onsubtitles).catch((err) => {\n console.error('Deepgram TTS failed:', err);\n onSpeechEndRef.current?.();\n onErrorRef.current?.(err);\n });\n } else {\n headRef.current.speakText(text, options, onsubtitles);\n startSpeechEndPolling();\n }\n }, [speakWithDeepgramStreaming, speakWithDeepgramAccurateLipSync, startSpeechEndPolling, resumeAudioContext]);\n\n const pauseSpeaking = useCallback(() => {\n const head = headRef.current;\n if (accurateLipSyncRef.current && head && !head.isStreaming) {\n // True pause: stop current phrase, wait in loop until Resume (no abort)\n speechPausedRef.current = true;\n head.pauseSpeaking();\n return;\n }\n // Streaming or non–phrase mode: Pause = stop (cannot resume stream)\n speechAbortedRef.current = true;\n if (streamWsRef.current) {\n try { streamWsRef.current.close(); } catch (_) {}\n streamWsRef.current = null;\n }\n if (head) {\n if (head.isStreaming) head.streamInterrupt();\n else head.pauseSpeaking();\n }\n if (speechEndCheckRef.current) {\n clearInterval(speechEndCheckRef.current);\n speechEndCheckRef.current = null;\n }\n onSpeechEndRef.current?.();\n }, []);\n\n const stopSpeaking = useCallback(() => {\n speechAbortedRef.current = true;\n if (streamWsRef.current) {\n try { streamWsRef.current.close(); } catch (_) {}\n streamWsRef.current = null;\n }\n if (speechEndCheckRef.current) {\n clearInterval(speechEndCheckRef.current);\n speechEndCheckRef.current = null;\n }\n const head = headRef.current;\n if (head) {\n if (head.isStreaming) head.streamInterrupt();\n else head.stopSpeaking();\n }\n onSpeechEndRef.current?.();\n }, []);\n\n const resumeSpeaking = useCallback(async () => {\n speechPausedRef.current = false;\n }, []);\n\n useImperativeHandle(ref, () => ({\n speakText,\n pauseSpeaking,\n resumeSpeaking,\n stopSpeaking,\n isReady,\n get isSpeaking() {\n return !!headRef.current?.isSpeaking;\n },\n talkingHead: headRef.current,\n }), [speakText, pauseSpeaking, resumeSpeaking, stopSpeaking, isReady]);\n\n return (\n <div className={`narrator-avatar-container ${className}`} style={{ position: 'relative', ...style }}>\n <div\n ref={containerRef}\n className=\"talking-head-viewer\"\n style={{ width: '100%', height: '100%', minHeight: '400px' }}\n />\n {isLoading && (\n <div\n className=\"loading-overlay\"\n style={{\n position: 'absolute',\n top: '50%',\n left: '50%',\n transform: 'translate(-50%, -50%)',\n color: '#333',\n fontSize: '18px',\n zIndex: 10,\n }}\n >\n Loading avatar...\n </div>\n )}\n {error && (\n <div\n className=\"error-overlay\"\n style={{\n position: 'absolute',\n top: '50%',\n left: '50%',\n transform: 'translate(-50%, -50%)',\n color: '#c00',\n fontSize: '14px',\n textAlign: 'center',\n zIndex: 10,\n padding: '20px',\n maxWidth: '90%',\n whiteSpace: 'pre-line',\n }}\n >\n {error}\n </div>\n )}\n </div>\n );\n});\n\nNarratorAvatar.displayName = 'NarratorAvatar';\n\nexport default NarratorAvatar;\n"],"names":["DEEPGRAM_SPEAK_URL","DEEPGRAM_SPEAK_WS_URL","DEEPGRAM_FAST_RESPONSE_PARAMS","STREAMING_SAMPLE_RATE","WAV_HEADER_SIZE","ESTIMATED_MS_PER_WORD","splitIntoPhrases","text","t","p","stretchPCM","arrayBuffer","rate","int16","n","outLen","out","i","src","j","f","s0","s1","v","wavToAudioBuffer","audioCtx","wavArrayBuffer","view","sampleRate","numChannels","totalBytes","numSamplesPerChannel","audioBuffer","ch","channel","estimatedWordTimings","words","totalLen","s","w","totalMs","wtimes","wdurations","dur","GESTURE_NAMES","pickGestureForPhrase","phrase","lower","mir","hash","h","c","name","EXTENSION_FIX_MESSAGE","SMOOTH_DEFAULTS","NarratorAvatar","forwardRef","avatarUrl","avatarBody","cameraView","mood","ttsLang","ttsVoice","ttsService","ttsApiKey","ttsEndpoint","lipsyncModules","lipsyncLang","modelFPS","modelPixelRatio","modelMovementFactor","mixerGainSpeech","avatarIdleEyeContact","avatarIdleHeadMove","avatarSpeakingEyeContact","avatarSpeakingHeadMove","onReady","onError","onSpeechStart","onSpeechEnd","onSubtitle","speechRate","accurateLipSync","speechGestures","getGestureForPhrase","className","style","ref","containerRef","useRef","headRef","isLoading","setIsLoading","useState","error","setError","isReady","setIsReady","speechEndCheckRef","ttsServiceRef","ttsApiKeyRef","ttsVoiceRef","onReadyRef","onErrorRef","onSpeechStartRef","onSpeechEndRef","onSubtitleRef","speechRateRef","accurateLipSyncRef","speechGesturesRef","getGestureForPhraseRef","useEffect","loadCancelledRef","loadResolvedRef","streamingStartPromiseRef","speechAbortedRef","speechPausedRef","streamWsRef","node","cancelled","timeoutId","initDone","head","ro","doInit","el","useDeepgram","googleKey","__vite_import_meta_env__","options","TalkingHead","initErr","msg","_a","avatarConfig","ev","err","startSpeechEndPolling","useCallback","resumeAudioContext","ctx","speakWithDeepgramStreaming","onsubtitles","apiKey","model","params","wsUrl","phrases","phraseList","phraseIndexRef","wsRef","firstChunkRef","currentPhraseRef","resolveAll","rejectAll","allDonePromise","res","rej","onAudioEnd","_b","_c","_d","ws","event","buf","custom","g","scale","d","speakWithDeepgramAccurateLipSync","url","durationMs","r","speakText","sub","pauseSpeaking","stopSpeaking","resumeSpeaking","useImperativeHandle","jsxs","jsx"],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
(function(V,Y){typeof exports=="object"&&typeof module<"u"?Y(exports,require("react/jsx-runtime"),require("react"),require("@met4citizen/talkinghead")):typeof define=="function"&&define.amd?define(["exports","react/jsx-runtime","react","@met4citizen/talkinghead"],Y):(V=typeof globalThis<"u"?globalThis:V||self,Y(V.NarratorAvatar={},V.jsxRuntime,V.React,V.TalkingHead))})(this,(function(V,Y,e,vt){"use strict";const $={},Mt="https://api.deepgram.com/v1/speak",Zt="wss://api.deepgram.com/v1/speak",Pt="encoding=linear16&container=wav&sample_rate=24000";function Dt(I){const s=I.trim();return s?s.split(new RegExp("(?<=[.!?])\\s+")).map(i=>i.trim()).filter(Boolean):[]}function Xt(I,s){if(s>=1||s<=0)return I;const o=new Int16Array(I),i=o.length;if(i===0)return I;const E=Math.ceil(i/s),w=new Int16Array(E);for(let b=0;b<E;b++){const T=b*s,R=Math.floor(T),q=T-R,L=R>=i?o[i-1]:o[R],F=R+1>=i?o[i-1]:o[R+1],et=(1-q)*L+q*F;w[b]=Math.max(-32768,Math.min(32767,Math.round(et)))}return w.buffer}function xt(I,s){if(s.byteLength<44)return null;const o=new DataView(s),i=o.getUint32(24,!0),E=Math.max(1,o.getUint16(22,!0)),w=s.byteLength-44,b=w/(2*E),T=I.createBuffer(E,b,i),R=new Int16Array(s,44,w/2|0);for(let q=0;q<E;q++){const L=T.getChannelData(q);for(let F=0;F<b;F++)L[F]=R[F*E+q]/32768}return T}function Qt(I){const s=I.trim().split(/\s+/).filter(Boolean);if(s.length===0)return{words:[I||" "],wtimes:[0],wdurations:[430]};const o=s.reduce((T,R)=>T+R.length,0)||1,i=s.length*430,E=[],w=[];let b=0;for(const T of s){E.push(b);const R=T.length/o*i;w.push(R),b+=R}return{words:s,wtimes:E,wdurations:w}}const Ct=["handup","index","ok","thumbup","thumbdown","side","shrug","namaste"];function Nt(I){const s=I.trim();if(!s)return null;const o=s.toLowerCase(),i=()=>Math.random()>.5;if(/\b(thank you|thanks|thank ya|welcome|bye|goodbye|see you|so long|namaste)\b/i.test(o))return{name:"namaste",dur:1.8,mirror:!1};if(/\?$/.test(s)||/\b(wait|hold on|one moment|hang on|let me ask|any questions?)\b/i.test(o)||/^(what|how|why|when|where|which|who|can you|could you|would you|do you|does|is it|are there)\b/i.test(s))return{name:"handup",dur:1.8,mirror:i()};if(/\!$/.test(s)||/\b(great|awesome|excellent|love|perfect|yes|yeah|cool|amazing|wow|good job|well done|fantastic|brilliant|nice|wonderful|super|terrific|outstanding)\b/i.test(o))return{name:"thumbup",dur:1.8,mirror:i()};if(/\b(wrong|bad idea|don't do that|never do|incorrect|nope|not right|that's wrong|avoid that)\b/i.test(o))return{name:"thumbdown",dur:1.6,mirror:i()};if(/\b(no |not |don't|never |can't|won't|shouldn't|isn't|aren't|wasn't|weren't)\b/i.test(o)||/\b(no,|no\.|nah)\b/i.test(o))return{name:"side",dur:1.6,mirror:i()};if(/\b(don't know|not sure|maybe|perhaps|might be|uncertain|i think|i guess|not certain|not really|depends|could be)\b/i.test(o))return{name:"shrug",dur:2,mirror:!1};if(/\b(first|second|third|one |two |three |number|remember|important|key|point|listen|look|note|step|next|then|finally|so,|so\.)\b/i.test(o)||/^(\d+[.)]\s)/.test(s))return{name:"index",dur:1.7,mirror:i()};if(/\b(ok|okay|alright|sure|correct|right|exactly|got it|understood|done|ready|agreed|deal)\b/i.test(o))return{name:"ok",dur:1.6,mirror:i()};const E=s.split("").reduce((b,T)=>b*31+T.charCodeAt(0)|0,0),w=Ct[Math.abs(E)%Ct.length];return{name:w,dur:1.5,mirror:w==="shrug"||w==="namaste"?!1:i()}}const X="If the avatar still does not load, try opening this site in an Incognito/Private window or disabling browser extensions (e.g. MetaMask) for this origin.",pt={modelFPS:60,modelPixelRatio:2,modelMovementFactor:.85,mixerGainSpeech:2,ttsTrimStart:0,ttsTrimEnd:300,avatarIdleEyeContact:.35,avatarIdleHeadMove:.45,avatarSpeakingEyeContact:.6,avatarSpeakingHeadMove:.55,lightAmbientIntensity:1.5,lightDirectIntensity:15,cameraRotateEnable:!0,cameraZoomEnable:!0,cameraPanEnable:!1},gt=e.forwardRef(({avatarUrl:I="/avatars/brunette.glb",avatarBody:s="F",cameraView:o="mid",mood:i="neutral",ttsLang:E="en-GB",ttsVoice:w="en-GB-Standard-A",ttsService:b="google",ttsApiKey:T=null,ttsEndpoint:R="https://texttospeech.googleapis.com/v1beta1/text:synthesize",lipsyncModules:q=["en"],lipsyncLang:L="en",modelFPS:F,modelPixelRatio:et,modelMovementFactor:Ot,mixerGainSpeech:$t,avatarIdleEyeContact:rt,avatarIdleHeadMove:nt,avatarSpeakingEyeContact:st,avatarSpeakingHeadMove:ut,onReady:Lt=()=>{},onError:Gt=()=>{},onSpeechStart:Wt=()=>{},onSpeechEnd:Kt=()=>{},onSubtitle:_t=null,speechRate:wt=1,accurateLipSync:yt=!1,speechGestures:At=!0,getGestureForPhrase:bt=null,className:te="",style:ee={}},re)=>{const Et=e.useRef(null),m=e.useRef(null),[ne,ot]=e.useState(!0),[Vt,at]=e.useState(null),[Bt,se]=e.useState(!1),M=e.useRef(null),zt=e.useRef(b),it=e.useRef(T),ct=e.useRef(w),lt=e.useRef(Lt),B=e.useRef(Gt),ft=e.useRef(Wt),c=e.useRef(Kt),N=e.useRef(_t),jt=e.useRef(wt),Tt=e.useRef(yt),kt=e.useRef(At),v=e.useRef(bt);e.useEffect(()=>{jt.current=Math.max(.6,Math.min(1.2,Number(wt)||1)),Tt.current=!!yt,kt.current=!!At,v.current=bt},[wt,yt,At,bt]),e.useEffect(()=>{zt.current=b,it.current=T,ct.current=w,lt.current=Lt,B.current=Gt,ft.current=Wt,c.current=Kt,N.current=_t});const Q=e.useRef(!1),dt=e.useRef(!1),St=e.useRef(null),P=e.useRef(!1),tt=e.useRef(!1),_=e.useRef(null);e.useEffect(()=>{const t=Et.current;if(!t)return;Q.current=!1,dt.current=!1;let u=!1,h=null,n=!1,d=null,p=null;const D=()=>{var O;if(u||n)return;const g=Et.current;if(!g||g.offsetWidth<=0||g.offsetHeight<=0)return;n=!0,p&&p.disconnect();const G=b==="deepgram",W=!G&&(T||($==null?void 0:$.VITE_GOOGLE_TTS_API_KEY)||""),k={...pt,cameraView:o,lipsyncModules:q,lipsyncLang:L,ttsLang:E,ttsVoice:w,ttsTrimStart:pt.ttsTrimStart,ttsTrimEnd:pt.ttsTrimEnd,...F!=null&&{modelFPS:F},...et!=null&&{modelPixelRatio:et},...Ot!=null&&{modelMovementFactor:Ot},...$t!=null&&{mixerGainSpeech:$t},...rt!=null&&{avatarIdleEyeContact:rt},...nt!=null&&{avatarIdleHeadMove:nt},...st!=null&&{avatarSpeakingEyeContact:st},...ut!=null&&{avatarSpeakingHeadMove:ut},...G?{ttsEndpoint:null,ttsApikey:null}:W?{ttsEndpoint:R,ttsApikey:W}:{ttsEndpoint:null,ttsApikey:null}};try{d=new vt.TalkingHead(g,k)}catch(r){if(u)return;ot(!1);const a=(r==null?void 0:r.message)??String(r);at(a?`${a}
|
|
2
|
+
|
|
3
|
+
${X}`:X),(O=B.current)==null||O.call(B,r);return}m.current=d;const x={url:I,body:s,avatarMood:i,ttsLang:E,ttsVoice:w,lipsyncLang:L,...rt!=null&&{avatarIdleEyeContact:rt},...nt!=null&&{avatarIdleHeadMove:nt},...st!=null&&{avatarSpeakingEyeContact:st},...ut!=null&&{avatarSpeakingHeadMove:ut}};h=setTimeout(()=>{Q.current||dt.current||(ot(!1),at(`Avatar failed to load (timeout). Check that the model file exists (e.g. /avatars/brunette.glb).
|
|
4
|
+
|
|
5
|
+
`+X))},15e3),d.showAvatar(x,r=>{r!=null&&r.lengthComputable&&r.loaded!=null&&r.total!=null&&Math.min(100,Math.round(r.loaded/r.total*100))}).then(()=>{var r;Q.current||(dt.current=!0,h&&clearTimeout(h),h=null,d.start(),ot(!1),se(!0),at(null),(r=lt.current)==null||r.call(lt))}).catch(r=>{var l;if(Q.current)return;dt.current=!0,h&&clearTimeout(h),h=null,ot(!1);const a=(r==null?void 0:r.message)||String(r);at(a?`${a}
|
|
6
|
+
|
|
7
|
+
${X}`:X),(l=B.current)==null||l.call(B,r)})};return p=new ResizeObserver(()=>D()),p.observe(t),requestAnimationFrame(()=>D()),()=>{if(u=!0,Q.current=!0,h&&clearTimeout(h),p&&p.disconnect(),M.current&&clearInterval(M.current),m.current){try{m.current.stop(),m.current.stopSpeaking()}catch{}m.current=null}}},[]);const mt=e.useCallback(()=>{M.current&&clearInterval(M.current),M.current=setInterval(()=>{var t;m.current&&!m.current.isSpeaking&&(M.current&&clearInterval(M.current),M.current=null,(t=c.current)==null||t.call(c))},200)},[]),H=e.useCallback(()=>{var u;const t=(u=m.current)==null?void 0:u.audioCtx;(t==null?void 0:t.state)==="suspended"&&t.resume()},[]),Ut=e.useCallback(async(t,u={},h)=>{const n=m.current;if(!(n!=null&&n.audioCtx))return;H();const d=it.current||($==null?void 0:$.VITE_DEEPGRAM_API_KEY)||"";if(!d){console.warn("NarratorAvatar: Deepgram TTS requires ttsApiKey or VITE_DEEPGRAM_API_KEY");return}const p=u.ttsVoice||ct.current||"aura-2-thalia-en",D=new URLSearchParams({encoding:"linear16",sample_rate:String(24e3),model:p}),g=`${Zt}?${D.toString()}`,G=jt.current,W=Dt(t),k=W.length>0?W:[t.trim()||" "],x={current:0},O={current:null},r={current:!0},a={current:k[0]};let l=null,z=null;const j=new Promise((f,C)=>{l=f,z=C}),ht=()=>{var C,K,J,y;if(P.current){(C=O.current)==null||C.close(),_.current=null,l==null||l();return}if(x.current+=1,x.current>=k.length){(K=c.current)==null||K.call(c),(J=O.current)==null||J.close(),_.current=null,l==null||l();return}r.current=!0,a.current=k[x.current],(y=N.current)==null||y.call(N,a.current);const f=O.current;f&&f.readyState===WebSocket.OPEN&&(f.send(JSON.stringify({type:"Speak",text:a.current})),f.send(JSON.stringify({type:"Flush"})))};n.isStreaming||(St.current||(St.current=n.streamStart({sampleRate:24e3,waitForAudioChunks:!0,lipsyncType:"words",lipsyncLang:u.lipsyncLang||L},null,ht,null,null)),await St.current),P.current=!1;const S=new WebSocket(g,["token",d]);return O.current=S,_.current=S,S.binaryType="arraybuffer",S.onopen=()=>{var f;r.current=!0,a.current=k[0],(f=N.current)==null||f.call(N,k[0]),S.send(JSON.stringify({type:"Speak",text:k[0]})),S.send(JSON.stringify({type:"Flush"}))},S.onmessage=f=>{var J,y;if(typeof f.data=="string"){try{const A=JSON.parse(f.data);(A.type==="Flushed"||A.type==="Cleared")&&n.streamNotifyEnd()}catch{}return}let C=f.data instanceof ArrayBuffer?f.data:(J=f.data)==null?void 0:J.buffer;if(!C||C.byteLength===0||!n.isStreaming)return;G<1&&(C=Xt(C,G));const K=a.current;if(r.current&&K){if(r.current=!1,kt.current&&n.playGesture){const Z=(y=v.current)==null?void 0:y.call(v,K),U=Z??Nt(K);U!=null&&U.name&&n.playGesture(U.name,U.dur??1.8,U.mirror??!1,800)}let{words:A,wtimes:It,wdurations:Rt}=Qt(K);if(G<1){const Z=1/G;It=It.map(U=>U*Z),Rt=Rt.map(U=>U*Z)}n.streamAudio({audio:C,words:A,wtimes:It,wdurations:Rt})}else n.streamAudio({audio:C})},S.onerror=()=>{z==null||z(new Error("Deepgram WebSocket error"))},S.onclose=()=>{var f;O.current=null,_.current===S&&(_.current=null),x.current<k.length&&((f=c.current)==null||f.call(c)),l==null||l()},j},[L,H]),qt=e.useCallback(async(t,u={},h)=>{var W,k,x,O;const n=m.current;if(!(n!=null&&n.audioCtx))return;P.current=!1,tt.current=!1,H();const d=it.current||($==null?void 0:$.VITE_DEEPGRAM_API_KEY)||"";if(!d){console.warn("NarratorAvatar: Deepgram TTS requires ttsApiKey or VITE_DEEPGRAM_API_KEY");return}n.isStreaming&&n.streamStop();const p=u.ttsVoice||ct.current||"aura-2-thalia-en",D=`${Mt}?model=${encodeURIComponent(p)}&${Pt}`,g=Dt(t),G=g.length>0?g:[t.trim()||" "];for(let r=0;r<G.length&&!P.current;r++){const a=G[r];(W=N.current)==null||W.call(N,a);const l=await fetch(D,{method:"POST",headers:{Authorization:`Token ${d}`,"Content-Type":"application/json",Accept:"audio/wav"},body:JSON.stringify({text:a})});if(P.current)break;if(!l.ok)throw new Error(`Deepgram TTS error: ${l.status} ${l.statusText}`);const z=await l.arrayBuffer();if(P.current)break;const j=xt(n.audioCtx,z);if(!j)throw new Error("Failed to prepare audio");const ht=j.duration*1e3,S=a.trim().split(/\s+/).filter(Boolean),f=S.reduce((y,A)=>y+A.length,0)||1;let C=0;const K=[],J=[];for(const y of S){K.push(C);const A=y.length/f*ht;J.push(A),C+=A}if(K.length===0&&(S.push(a),K.push(0),J.push(ht)),kt.current&&n.playGesture){const y=(k=v.current)==null?void 0:k.call(v,a),A=y??Nt(a);A!=null&&A.name&&n.playGesture(A.name,A.dur??1.8,A.mirror??!1,800)}for(n.speakAudio({audio:j,words:S,wtimes:K,wdurations:J},{lipsyncLang:u.lipsyncLang||L},null);(x=m.current)!=null&&x.isSpeaking&&!P.current;)await new Promise(y=>setTimeout(y,100));if(P.current)break;for(;tt.current&&!P.current;)await new Promise(y=>setTimeout(y,100));if(P.current)break}P.current||(O=c.current)==null||O.call(c)},[L,H]);e.useCallback(async(t,u={},h)=>{const n=m.current;if(!(n!=null&&n.audioCtx))return;H();const d=it.current||($==null?void 0:$.VITE_DEEPGRAM_API_KEY)||"";if(!d){console.warn("NarratorAvatar: Deepgram TTS requires ttsApiKey or VITE_DEEPGRAM_API_KEY");return}const p=u.ttsVoice||ct.current||"aura-2-thalia-en",D=`${Mt}?model=${encodeURIComponent(p)}&${Pt}`,g=await fetch(D,{method:"POST",headers:{Authorization:`Token ${d}`,"Content-Type":"application/json",Accept:"audio/wav"},body:JSON.stringify({text:t})});if(!g.ok)throw new Error(`Deepgram TTS error: ${g.status} ${g.statusText}`);const G=await g.arrayBuffer(),W=xt(n.audioCtx,G);if(!W)throw new Error("Failed to prepare audio for playback");const k=W.duration*1e3,x=t.trim().split(/\s+/).filter(Boolean),O=x.reduce((z,j)=>z+j.length,0)||1;let r=0;const a=[],l=[];for(const z of x){a.push(r);const j=z.length/O*k;l.push(j),r+=j}a.length===0&&(a.push(0),l.push(k),x.push(t)),H(),n.speakAudio({audio:W,words:x,wtimes:a,wdurations:l},{lipsyncLang:u.lipsyncLang||L},h),mt()},[L,mt,H]);const Ft=e.useCallback((t,u={})=>{var n;if(!m.current)return;P.current=!1,tt.current=!1,H(),(n=ft.current)==null||n.call(ft,t);const h=d=>{var D;const p=Array.isArray(d)?d.join(" "):typeof d=="string"?d:"";p&&((D=N.current)==null||D.call(N,p))};zt.current==="deepgram"?(Tt.current?qt:Ut)(t,u,h).catch(p=>{var D,g;console.error("Deepgram TTS failed:",p),(D=c.current)==null||D.call(c),(g=B.current)==null||g.call(B,p)}):(m.current.speakText(t,u,h),mt())},[Ut,qt,mt,H]),Ht=e.useCallback(()=>{var u;const t=m.current;if(Tt.current&&t&&!t.isStreaming){tt.current=!0,t.pauseSpeaking();return}if(P.current=!0,_.current){try{_.current.close()}catch{}_.current=null}t&&(t.isStreaming?t.streamInterrupt():t.pauseSpeaking()),M.current&&(clearInterval(M.current),M.current=null),(u=c.current)==null||u.call(c)},[]),Jt=e.useCallback(()=>{var u;if(P.current=!0,_.current){try{_.current.close()}catch{}_.current=null}M.current&&(clearInterval(M.current),M.current=null);const t=m.current;t&&(t.isStreaming?t.streamInterrupt():t.stopSpeaking()),(u=c.current)==null||u.call(c)},[]),Yt=e.useCallback(async()=>{tt.current=!1},[]);return e.useImperativeHandle(re,()=>({speakText:Ft,pauseSpeaking:Ht,resumeSpeaking:Yt,stopSpeaking:Jt,isReady:Bt,get isSpeaking(){var t;return!!((t=m.current)!=null&&t.isSpeaking)},talkingHead:m.current}),[Ft,Ht,Yt,Jt,Bt]),Y.jsxs("div",{className:`narrator-avatar-container ${te}`,style:{position:"relative",...ee},children:[Y.jsx("div",{ref:Et,className:"talking-head-viewer",style:{width:"100%",height:"100%",minHeight:"400px"}}),ne&&Y.jsx("div",{className:"loading-overlay",style:{position:"absolute",top:"50%",left:"50%",transform:"translate(-50%, -50%)",color:"#333",fontSize:"18px",zIndex:10},children:"Loading avatar..."}),Vt&&Y.jsx("div",{className:"error-overlay",style:{position:"absolute",top:"50%",left:"50%",transform:"translate(-50%, -50%)",color:"#c00",fontSize:"14px",textAlign:"center",zIndex:10,padding:"20px",maxWidth:"90%",whiteSpace:"pre-line"},children:Vt})]})});gt.displayName="NarratorAvatar",V.NarratorAvatar=gt,V.default=gt,Object.defineProperties(V,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})}));
|
|
8
|
+
//# sourceMappingURL=narrator-avatar.umd.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"narrator-avatar.umd.cjs","sources":["../src/NarratorAvatar.jsx"],"sourcesContent":["/**\n * React wrapper for @met4citizen/talkinghead (original TalkingHead).\n * Exposes: speakText, pauseSpeaking, stopSpeaking, resumeSpeaking (no-op).\n * TTS: ttsService 'google' | 'deepgram'; for Deepgram set ttsApiKey + ttsVoice (e.g. aura-2-mars-en).\n *\n * Smoothest output: uses SMOOTH_DEFAULTS (60 FPS, pixel ratio 2, movement factor 0.85,\n * mixer gain 2, tuned trim/lighting/eye contact). Override via props: modelFPS, modelPixelRatio,\n * modelMovementFactor, mixerGainSpeech, avatarIdleEyeContact, avatarIdleHeadMove,\n * avatarSpeakingEyeContact, avatarSpeakingHeadMove.\n */\n\nimport React, { useRef, useEffect, useCallback, useImperativeHandle, forwardRef, useState } from 'react';\nimport { TalkingHead } from '@met4citizen/talkinghead';\n\nconst DEEPGRAM_SPEAK_URL = 'https://api.deepgram.com/v1/speak';\nconst DEEPGRAM_SPEAK_WS_URL = 'wss://api.deepgram.com/v1/speak';\n/** Request linear16 WAV to avoid slow MP3 decode – faster time-to-first-audio. */\nconst DEEPGRAM_FAST_RESPONSE_PARAMS = 'encoding=linear16&container=wav&sample_rate=24000';\nconst STREAMING_SAMPLE_RATE = 24000;\nconst WAV_HEADER_SIZE = 44; // standard PCM WAV header length\n/** For streaming lip-sync when we don't have real word boundaries from the API. */\nconst ESTIMATED_MS_PER_WORD = 430;\n\n/** Split text into phrases (sentences) so we can stream and subtitle in natural breaks. */\nfunction splitIntoPhrases(text) {\n const t = text.trim();\n if (!t) return [];\n const parts = t.split(/(?<=[.!?])\\s+/);\n return parts.map((p) => p.trim()).filter(Boolean);\n}\n\n/** Time-stretch PCM (more samples = slower playback). Preserves pitch; linear interpolation to keep voice natural. */\nfunction stretchPCM(arrayBuffer, rate) {\n if (rate >= 1 || rate <= 0) return arrayBuffer;\n const int16 = new Int16Array(arrayBuffer);\n const n = int16.length;\n if (n === 0) return arrayBuffer;\n const outLen = Math.ceil(n / rate);\n const out = new Int16Array(outLen);\n for (let i = 0; i < outLen; i++) {\n const src = i * rate;\n const j = Math.floor(src);\n const f = src - j;\n const s0 = j >= n ? int16[n - 1] : int16[j];\n const s1 = j + 1 >= n ? int16[n - 1] : int16[j + 1];\n const v = (1 - f) * s0 + f * s1;\n out[i] = Math.max(-32768, Math.min(32767, Math.round(v)));\n }\n return out.buffer;\n}\n\n/**\n * Build an AudioBuffer from PCM WAV. Avoids decodeAudioData so the avatar can start\n * speaking as soon as the API responds – better for child-friendly responsiveness.\n */\nfunction wavToAudioBuffer(audioCtx, wavArrayBuffer) {\n if (wavArrayBuffer.byteLength < WAV_HEADER_SIZE) return null;\n const view = new DataView(wavArrayBuffer);\n const sampleRate = view.getUint32(24, true);\n const numChannels = Math.max(1, view.getUint16(22, true));\n const totalBytes = wavArrayBuffer.byteLength - WAV_HEADER_SIZE;\n const numSamplesPerChannel = totalBytes / (2 * numChannels); // 16-bit = 2 bytes per sample\n const audioBuffer = audioCtx.createBuffer(numChannels, numSamplesPerChannel, sampleRate);\n const int16 = new Int16Array(wavArrayBuffer, WAV_HEADER_SIZE, (totalBytes / 2) | 0);\n for (let ch = 0; ch < numChannels; ch++) {\n const channel = audioBuffer.getChannelData(ch);\n for (let i = 0; i < numSamplesPerChannel; i++) {\n channel[i] = int16[i * numChannels + ch] / 32768;\n }\n }\n return audioBuffer;\n}\n\nfunction estimatedWordTimings(text) {\n const words = text.trim().split(/\\s+/).filter(Boolean);\n if (words.length === 0) {\n return { words: [text || ' '], wtimes: [0], wdurations: [ESTIMATED_MS_PER_WORD] };\n }\n const totalLen = words.reduce((s, w) => s + w.length, 0) || 1;\n const totalMs = words.length * ESTIMATED_MS_PER_WORD;\n const wtimes = [];\n const wdurations = [];\n let t = 0;\n for (const w of words) {\n wtimes.push(t);\n const dur = (w.length / totalLen) * totalMs;\n wdurations.push(dur);\n t += dur;\n }\n return { words, wtimes, wdurations };\n}\n\n/** All hand gestures supported by talkinghead (wider vocabulary, less repetition). */\nconst GESTURE_NAMES = ['handup', 'index', 'ok', 'thumbup', 'thumbdown', 'side', 'shrug', 'namaste'];\n\n/**\n * Picks a gesture that matches the phrase content (generic, dynamic).\n * Returns { name, dur, mirror } or null to skip gesture.\n * Uses all 8 gestures and more trigger categories for variety.\n */\nfunction pickGestureForPhrase(phrase) {\n const t = phrase.trim();\n if (!t) return null;\n const lower = t.toLowerCase();\n const mir = () => Math.random() > 0.5;\n\n // Thanks / greeting / closing -> namaste\n if (/\\b(thank you|thanks|thank ya|welcome|bye|goodbye|see you|so long|namaste)\\b/i.test(lower)) {\n return { name: 'namaste', dur: 1.8, mirror: false };\n }\n // Question / asking / wait -> hand up\n if (/\\?$/.test(t) || /\\b(wait|hold on|one moment|hang on|let me ask|any questions?)\\b/i.test(lower) ||\n /^(what|how|why|when|where|which|who|can you|could you|would you|do you|does|is it|are there)\\b/i.test(t)) {\n return { name: 'handup', dur: 1.8, mirror: mir() };\n }\n // Enthusiasm / positive -> thumb up\n if (/\\!$/.test(t) || /\\b(great|awesome|excellent|love|perfect|yes|yeah|cool|amazing|wow|good job|well done|fantastic|brilliant|nice|wonderful|super|terrific|outstanding)\\b/i.test(lower)) {\n return { name: 'thumbup', dur: 1.8, mirror: mir() };\n }\n // Strong disapproval / wrong -> thumb down\n if (/\\b(wrong|bad idea|don't do that|never do|incorrect|nope|not right|that's wrong|avoid that)\\b/i.test(lower)) {\n return { name: 'thumbdown', dur: 1.6, mirror: mir() };\n }\n // Softer no / not / disagreement -> side (hand wave)\n if (/\\b(no |not |don't|never |can't|won't|shouldn't|isn't|aren't|wasn't|weren't)\\b/i.test(lower) || /\\b(no,|no\\.|nah)\\b/i.test(lower)) {\n return { name: 'side', dur: 1.6, mirror: mir() };\n }\n // Uncertainty -> shrug\n if (/\\b(don't know|not sure|maybe|perhaps|might be|uncertain|i think|i guess|not certain|not really|depends|could be)\\b/i.test(lower)) {\n return { name: 'shrug', dur: 2, mirror: false };\n }\n // Listing / emphasis / steps -> index\n if (/\\b(first|second|third|one |two |three |number|remember|important|key|point|listen|look|note|step|next|then|finally|so,|so\\.)\\b/i.test(lower) || /^(\\d+[.)]\\s)/.test(t)) {\n return { name: 'index', dur: 1.7, mirror: mir() };\n }\n // Approval / agreement / ready -> ok\n if (/\\b(ok|okay|alright|sure|correct|right|exactly|got it|understood|done|ready|agreed|deal)\\b/i.test(lower)) {\n return { name: 'ok', dur: 1.6, mirror: mir() };\n }\n // Neutral: spread across all 8 gestures by phrase hash (less repetition)\n const hash = t.split('').reduce((h, c) => (h * 31 + c.charCodeAt(0)) | 0, 0);\n const name = GESTURE_NAMES[Math.abs(hash) % GESTURE_NAMES.length];\n return { name, dur: 1.5, mirror: name === 'shrug' || name === 'namaste' ? false : mir() };\n}\n\n/** Shown when init/load fails; suggests extension or CSP as cause. */\nconst EXTENSION_FIX_MESSAGE =\n 'If the avatar still does not load, try opening this site in an Incognito/Private window or disabling browser extensions (e.g. MetaMask) for this origin.';\n\n/**\n * Default options tuned for smoothest output (animation, lip-sync, audio, lighting).\n * See @met4citizen/talkinghead README for all options.\n */\nconst SMOOTH_DEFAULTS = {\n modelFPS: 60, // Smoother animation (default 30)\n modelPixelRatio: 2, // Sharp on HiDPI (capped for perf)\n modelMovementFactor: 0.85, // Slightly subtler body movement\n mixerGainSpeech: 2, // Clearer speech volume\n ttsTrimStart: 0, // Viseme start trim (ms)\n ttsTrimEnd: 300, // Slightly less end trim for lip-sync (default 400)\n avatarIdleEyeContact: 0.35, // Natural idle eye contact [0,1]\n avatarIdleHeadMove: 0.45, // Natural idle head movement [0,1]\n avatarSpeakingEyeContact: 0.6, // Engagement while speaking [0,1]\n avatarSpeakingHeadMove: 0.55, // Head movement while speaking [0,1]\n lightAmbientIntensity: 1.5, // Slightly brighter ambient\n lightDirectIntensity: 15, // Clearer main light\n cameraRotateEnable: true, // Allow user to rotate view\n cameraZoomEnable: true, // Allow zoom for UX\n cameraPanEnable: false,\n};\n\nconst NarratorAvatar = forwardRef(({\n avatarUrl = '/avatars/brunette.glb',\n avatarBody = 'F',\n cameraView = 'mid',\n mood = 'neutral',\n ttsLang = 'en-GB',\n ttsVoice = 'en-GB-Standard-A',\n ttsService = 'google', // 'google' | 'deepgram'\n ttsApiKey = null,\n ttsEndpoint = 'https://texttospeech.googleapis.com/v1beta1/text:synthesize',\n lipsyncModules = ['en'],\n lipsyncLang = 'en',\n // Smoothness overrides (merged with SMOOTH_DEFAULTS)\n modelFPS,\n modelPixelRatio,\n modelMovementFactor,\n mixerGainSpeech,\n avatarIdleEyeContact,\n avatarIdleHeadMove,\n avatarSpeakingEyeContact,\n avatarSpeakingHeadMove,\n onReady = () => {},\n onError = () => {},\n onSpeechStart = () => {},\n onSpeechEnd = () => {},\n onSubtitle = null,\n /** Slower speech, same voice (e.g. 0.9 = 10% slower). Pitch-preserving time-stretch only in streaming. */\n speechRate = 1,\n /** Use REST per phrase for exact word timings = perfect lip-sync (slightly slower start per phrase). */\n accurateLipSync = false,\n /** Enable content-aware hand gestures while speaking (default true). */\n speechGestures = true,\n /** Optional: (phrase) => { name, dur?, mirror? } | null to override or extend gesture choice. */\n getGestureForPhrase = null,\n className = '',\n style = {},\n}, ref) => {\n const containerRef = useRef(null);\n const headRef = useRef(null);\n const [isLoading, setIsLoading] = useState(true);\n const [error, setError] = useState(null);\n const [isReady, setIsReady] = useState(false);\n const speechEndCheckRef = useRef(null);\n const ttsServiceRef = useRef(ttsService);\n const ttsApiKeyRef = useRef(ttsApiKey);\n const ttsVoiceRef = useRef(ttsVoice);\n const onReadyRef = useRef(onReady);\n const onErrorRef = useRef(onError);\n const onSpeechStartRef = useRef(onSpeechStart);\n const onSpeechEndRef = useRef(onSpeechEnd);\n const onSubtitleRef = useRef(onSubtitle);\n const speechRateRef = useRef(speechRate);\n const accurateLipSyncRef = useRef(accurateLipSync);\n const speechGesturesRef = useRef(speechGestures);\n const getGestureForPhraseRef = useRef(getGestureForPhrase);\n\n useEffect(() => {\n speechRateRef.current = Math.max(0.6, Math.min(1.2, Number(speechRate) || 1));\n accurateLipSyncRef.current = !!accurateLipSync;\n speechGesturesRef.current = !!speechGestures;\n getGestureForPhraseRef.current = getGestureForPhrase;\n }, [speechRate, accurateLipSync, speechGestures, getGestureForPhrase]);\n\n useEffect(() => {\n ttsServiceRef.current = ttsService;\n ttsApiKeyRef.current = ttsApiKey;\n ttsVoiceRef.current = ttsVoice;\n onReadyRef.current = onReady;\n onErrorRef.current = onError;\n onSpeechStartRef.current = onSpeechStart;\n onSpeechEndRef.current = onSpeechEnd;\n onSubtitleRef.current = onSubtitle;\n });\n\n const loadCancelledRef = useRef(false);\n const loadResolvedRef = useRef(false);\n const streamingStartPromiseRef = useRef(null);\n /** When true, phrase loop and streaming should stop (Stop clicked). */\n const speechAbortedRef = useRef(false);\n /** When true, phrase loop is paused (Pause clicked); resumeSpeaking() clears it so loop continues. */\n const speechPausedRef = useRef(false);\n /** Active stream WebSocket so we can close it on Stop/Pause. */\n const streamWsRef = useRef(null);\n\n useEffect(() => {\n const node = containerRef.current;\n if (!node) return;\n\n loadCancelledRef.current = false;\n loadResolvedRef.current = false;\n let cancelled = false;\n let timeoutId = null;\n let initDone = false;\n let head = null;\n let ro = null;\n\n const doInit = () => {\n if (cancelled || initDone) return;\n const el = containerRef.current;\n if (!el || el.offsetWidth <= 0 || el.offsetHeight <= 0) return;\n initDone = true;\n if (ro) ro.disconnect();\n const useDeepgram = ttsService === 'deepgram';\n const googleKey = !useDeepgram && (ttsApiKey || import.meta.env?.VITE_GOOGLE_TTS_API_KEY || '');\n const options = {\n ...SMOOTH_DEFAULTS,\n cameraView,\n lipsyncModules,\n lipsyncLang,\n ttsLang,\n ttsVoice,\n ttsTrimStart: SMOOTH_DEFAULTS.ttsTrimStart,\n ttsTrimEnd: SMOOTH_DEFAULTS.ttsTrimEnd,\n ...(modelFPS != null && { modelFPS }),\n ...(modelPixelRatio != null && { modelPixelRatio }),\n ...(modelMovementFactor != null && { modelMovementFactor }),\n ...(mixerGainSpeech != null && { mixerGainSpeech }),\n ...(avatarIdleEyeContact != null && { avatarIdleEyeContact }),\n ...(avatarIdleHeadMove != null && { avatarIdleHeadMove }),\n ...(avatarSpeakingEyeContact != null && { avatarSpeakingEyeContact }),\n ...(avatarSpeakingHeadMove != null && { avatarSpeakingHeadMove }),\n ...(useDeepgram\n ? { ttsEndpoint: null, ttsApikey: null }\n : googleKey\n ? { ttsEndpoint, ttsApikey: googleKey }\n : { ttsEndpoint: null, ttsApikey: null }),\n };\n try {\n head = new TalkingHead(el, options);\n } catch (initErr) {\n if (cancelled) return;\n setIsLoading(false);\n const msg = initErr?.message ?? String(initErr);\n setError(msg ? `${msg}\\n\\n${EXTENSION_FIX_MESSAGE}` : EXTENSION_FIX_MESSAGE);\n onErrorRef.current?.(initErr);\n return;\n }\n headRef.current = head;\n\n const avatarConfig = {\n url: avatarUrl,\n body: avatarBody,\n avatarMood: mood,\n ttsLang,\n ttsVoice,\n lipsyncLang,\n ...(avatarIdleEyeContact != null && { avatarIdleEyeContact }),\n ...(avatarIdleHeadMove != null && { avatarIdleHeadMove }),\n ...(avatarSpeakingEyeContact != null && { avatarSpeakingEyeContact }),\n ...(avatarSpeakingHeadMove != null && { avatarSpeakingHeadMove }),\n };\n\n timeoutId = setTimeout(() => {\n if (loadCancelledRef.current || loadResolvedRef.current) return;\n setIsLoading(false);\n setError('Avatar failed to load (timeout). Check that the model file exists (e.g. /avatars/brunette.glb).\\n\\n' + EXTENSION_FIX_MESSAGE);\n }, 15000);\n\n head\n .showAvatar(avatarConfig, (ev) => {\n if (ev?.lengthComputable && ev.loaded != null && ev.total != null) {\n const pct = Math.min(100, Math.round((ev.loaded / ev.total) * 100));\n }\n })\n .then(() => {\n if (loadCancelledRef.current) return;\n loadResolvedRef.current = true;\n if (timeoutId) clearTimeout(timeoutId);\n timeoutId = null;\n head.start();\n setIsLoading(false);\n setIsReady(true);\n setError(null);\n onReadyRef.current?.();\n })\n .catch((err) => {\n if (loadCancelledRef.current) return;\n loadResolvedRef.current = true;\n if (timeoutId) clearTimeout(timeoutId);\n timeoutId = null;\n setIsLoading(false);\n const msg = err?.message || String(err);\n setError(msg ? `${msg}\\n\\n${EXTENSION_FIX_MESSAGE}` : EXTENSION_FIX_MESSAGE);\n onErrorRef.current?.(err);\n });\n };\n\n ro = new ResizeObserver(() => doInit());\n ro.observe(node);\n requestAnimationFrame(() => doInit());\n\n return () => {\n cancelled = true;\n loadCancelledRef.current = true;\n if (timeoutId) clearTimeout(timeoutId);\n if (ro) ro.disconnect();\n if (speechEndCheckRef.current) clearInterval(speechEndCheckRef.current);\n if (headRef.current) {\n try {\n headRef.current.stop();\n headRef.current.stopSpeaking();\n } catch (e) {}\n headRef.current = null;\n }\n };\n }, []); // mount once; config changes would require remount to take effect\n\n const startSpeechEndPolling = useCallback(() => {\n if (speechEndCheckRef.current) clearInterval(speechEndCheckRef.current);\n speechEndCheckRef.current = setInterval(() => {\n if (headRef.current && !headRef.current.isSpeaking) {\n if (speechEndCheckRef.current) clearInterval(speechEndCheckRef.current);\n speechEndCheckRef.current = null;\n onSpeechEndRef.current?.();\n }\n }, 200);\n }, []);\n\n const resumeAudioContext = useCallback(() => {\n const ctx = headRef.current?.audioCtx;\n if (ctx?.state === 'suspended') ctx.resume();\n }, []);\n\n const speakWithDeepgramStreaming = useCallback(\n async (text, options = {}, onsubtitles) => {\n const head = headRef.current;\n if (!head?.audioCtx) return;\n resumeAudioContext();\n const apiKey = ttsApiKeyRef.current || import.meta.env?.VITE_DEEPGRAM_API_KEY || '';\n if (!apiKey) {\n console.warn('NarratorAvatar: Deepgram TTS requires ttsApiKey or VITE_DEEPGRAM_API_KEY');\n return;\n }\n const model = options.ttsVoice || ttsVoiceRef.current || 'aura-2-thalia-en';\n const params = new URLSearchParams({\n encoding: 'linear16',\n sample_rate: String(STREAMING_SAMPLE_RATE),\n model,\n });\n const wsUrl = `${DEEPGRAM_SPEAK_WS_URL}?${params.toString()}`;\n const rate = speechRateRef.current;\n const phrases = splitIntoPhrases(text);\n const phraseList = phrases.length > 0 ? phrases : [text.trim() || ' '];\n\n const phraseIndexRef = { current: 0 };\n const wsRef = { current: null };\n const firstChunkRef = { current: true };\n const currentPhraseRef = { current: phraseList[0] };\n let resolveAll = null;\n let rejectAll = null;\n const allDonePromise = new Promise((res, rej) => {\n resolveAll = res;\n rejectAll = rej;\n });\n\n const onAudioEnd = () => {\n if (speechAbortedRef.current) {\n wsRef.current?.close();\n streamWsRef.current = null;\n resolveAll?.();\n return;\n }\n phraseIndexRef.current += 1;\n if (phraseIndexRef.current >= phraseList.length) {\n onSpeechEndRef.current?.();\n wsRef.current?.close();\n streamWsRef.current = null;\n resolveAll?.();\n return;\n }\n firstChunkRef.current = true;\n currentPhraseRef.current = phraseList[phraseIndexRef.current];\n onSubtitleRef.current?.(currentPhraseRef.current);\n const ws = wsRef.current;\n if (ws && ws.readyState === WebSocket.OPEN) {\n ws.send(JSON.stringify({ type: 'Speak', text: currentPhraseRef.current }));\n ws.send(JSON.stringify({ type: 'Flush' }));\n }\n };\n\n if (!head.isStreaming) {\n if (!streamingStartPromiseRef.current) {\n streamingStartPromiseRef.current = head.streamStart(\n {\n sampleRate: STREAMING_SAMPLE_RATE,\n waitForAudioChunks: true,\n lipsyncType: 'words',\n lipsyncLang: options.lipsyncLang || lipsyncLang,\n },\n null,\n onAudioEnd,\n null,\n null\n );\n }\n await streamingStartPromiseRef.current;\n }\n\n speechAbortedRef.current = false;\n const ws = new WebSocket(wsUrl, ['token', apiKey]);\n wsRef.current = ws;\n streamWsRef.current = ws;\n ws.binaryType = 'arraybuffer';\n\n ws.onopen = () => {\n firstChunkRef.current = true;\n currentPhraseRef.current = phraseList[0];\n onSubtitleRef.current?.(phraseList[0]);\n ws.send(JSON.stringify({ type: 'Speak', text: phraseList[0] }));\n ws.send(JSON.stringify({ type: 'Flush' }));\n };\n\n ws.onmessage = (event) => {\n if (typeof event.data === 'string') {\n try {\n const msg = JSON.parse(event.data);\n if (msg.type === 'Flushed' || msg.type === 'Cleared') {\n head.streamNotifyEnd();\n }\n } catch (_) {}\n return;\n }\n let buf = event.data instanceof ArrayBuffer ? event.data : event.data?.buffer;\n if (!buf || buf.byteLength === 0 || !head.isStreaming) return;\n if (rate < 1) buf = stretchPCM(buf, rate);\n const phrase = currentPhraseRef.current;\n if (firstChunkRef.current && phrase) {\n firstChunkRef.current = false;\n if (speechGesturesRef.current && head.playGesture) {\n const custom = getGestureForPhraseRef.current?.(phrase);\n const g = custom !== undefined && custom !== null ? custom : pickGestureForPhrase(phrase);\n if (g?.name) head.playGesture(g.name, g.dur ?? 1.8, g.mirror ?? false, 800);\n }\n let { words, wtimes, wdurations } = estimatedWordTimings(phrase);\n if (rate < 1) {\n const scale = 1 / rate;\n wtimes = wtimes.map((t) => t * scale);\n wdurations = wdurations.map((d) => d * scale);\n }\n head.streamAudio({ audio: buf, words, wtimes, wdurations });\n } else {\n head.streamAudio({ audio: buf });\n }\n };\n\n ws.onerror = () => {\n rejectAll?.(new Error('Deepgram WebSocket error'));\n };\n ws.onclose = () => {\n wsRef.current = null;\n if (streamWsRef.current === ws) streamWsRef.current = null;\n if (phraseIndexRef.current < phraseList.length) {\n onSpeechEndRef.current?.();\n }\n resolveAll?.();\n };\n\n return allDonePromise;\n },\n [lipsyncLang, resumeAudioContext]\n );\n\n /** REST per phrase: exact duration → exact word timings → perfect lip-sync. Phrase subtitles. */\n const speakWithDeepgramAccurateLipSync = useCallback(\n async (text, options = {}, onsubtitles) => {\n const head = headRef.current;\n if (!head?.audioCtx) return;\n speechAbortedRef.current = false;\n speechPausedRef.current = false;\n resumeAudioContext();\n const apiKey = ttsApiKeyRef.current || import.meta.env?.VITE_DEEPGRAM_API_KEY || '';\n if (!apiKey) {\n console.warn('NarratorAvatar: Deepgram TTS requires ttsApiKey or VITE_DEEPGRAM_API_KEY');\n return;\n }\n if (head.isStreaming) head.streamStop();\n const model = options.ttsVoice || ttsVoiceRef.current || 'aura-2-thalia-en';\n const url = `${DEEPGRAM_SPEAK_URL}?model=${encodeURIComponent(model)}&${DEEPGRAM_FAST_RESPONSE_PARAMS}`;\n const phrases = splitIntoPhrases(text);\n const phraseList = phrases.length > 0 ? phrases : [text.trim() || ' '];\n\n for (let i = 0; i < phraseList.length; i++) {\n if (speechAbortedRef.current) break;\n const phrase = phraseList[i];\n onSubtitleRef.current?.(phrase);\n const res = await fetch(url, {\n method: 'POST',\n headers: {\n Authorization: `Token ${apiKey}`,\n 'Content-Type': 'application/json',\n Accept: 'audio/wav',\n },\n body: JSON.stringify({ text: phrase }),\n });\n if (speechAbortedRef.current) break;\n if (!res.ok) throw new Error(`Deepgram TTS error: ${res.status} ${res.statusText}`);\n const arrayBuffer = await res.arrayBuffer();\n if (speechAbortedRef.current) break;\n const audioBuffer = wavToAudioBuffer(head.audioCtx, arrayBuffer);\n if (!audioBuffer) throw new Error('Failed to prepare audio');\n const durationMs = audioBuffer.duration * 1000;\n const words = phrase.trim().split(/\\s+/).filter(Boolean);\n const totalLen = words.reduce((s, w) => s + w.length, 0) || 1;\n let t = 0;\n const wtimes = [];\n const wdurations = [];\n for (const w of words) {\n wtimes.push(t);\n const dur = (w.length / totalLen) * durationMs;\n wdurations.push(dur);\n t += dur;\n }\n if (wtimes.length === 0) {\n words.push(phrase);\n wtimes.push(0);\n wdurations.push(durationMs);\n }\n if (speechGesturesRef.current && head.playGesture) {\n const custom = getGestureForPhraseRef.current?.(phrase);\n const g = custom !== undefined && custom !== null ? custom : pickGestureForPhrase(phrase);\n if (g?.name) head.playGesture(g.name, g.dur ?? 1.8, g.mirror ?? false, 800);\n }\n head.speakAudio(\n { audio: audioBuffer, words, wtimes, wdurations },\n { lipsyncLang: options.lipsyncLang || lipsyncLang },\n null\n );\n while (headRef.current?.isSpeaking && !speechAbortedRef.current) {\n await new Promise((r) => setTimeout(r, 100));\n }\n if (speechAbortedRef.current) break;\n // Pause: wait until user clicks Resume or Stop\n while (speechPausedRef.current && !speechAbortedRef.current) {\n await new Promise((r) => setTimeout(r, 100));\n }\n if (speechAbortedRef.current) break;\n }\n if (!speechAbortedRef.current) onSpeechEndRef.current?.();\n },\n [lipsyncLang, resumeAudioContext]\n );\n\n const speakWithDeepgram = useCallback(\n async (text, options = {}, onsubtitles) => {\n const head = headRef.current;\n if (!head?.audioCtx) return;\n resumeAudioContext();\n const apiKey = ttsApiKeyRef.current || import.meta.env?.VITE_DEEPGRAM_API_KEY || '';\n if (!apiKey) {\n console.warn('NarratorAvatar: Deepgram TTS requires ttsApiKey or VITE_DEEPGRAM_API_KEY');\n return;\n }\n const model = options.ttsVoice || ttsVoiceRef.current || 'aura-2-thalia-en';\n const url = `${DEEPGRAM_SPEAK_URL}?model=${encodeURIComponent(model)}&${DEEPGRAM_FAST_RESPONSE_PARAMS}`;\n const res = await fetch(url, {\n method: 'POST',\n headers: {\n Authorization: `Token ${apiKey}`,\n 'Content-Type': 'application/json',\n Accept: 'audio/wav',\n },\n body: JSON.stringify({ text }),\n });\n if (!res.ok) {\n throw new Error(`Deepgram TTS error: ${res.status} ${res.statusText}`);\n }\n const arrayBuffer = await res.arrayBuffer();\n const audioBuffer = wavToAudioBuffer(head.audioCtx, arrayBuffer);\n if (!audioBuffer) {\n throw new Error('Failed to prepare audio for playback');\n }\n const durationMs = audioBuffer.duration * 1000;\n // Word-level timing for smoother lip-sync and word-level subtitles (proportional by length)\n const words = text.trim().split(/\\s+/).filter(Boolean);\n const totalLen = words.reduce((s, w) => s + w.length, 0) || 1;\n let t = 0;\n const wtimes = [];\n const wdurations = [];\n for (const w of words) {\n wtimes.push(t);\n const dur = (w.length / totalLen) * durationMs;\n wdurations.push(dur);\n t += dur;\n }\n if (wtimes.length === 0) {\n wtimes.push(0);\n wdurations.push(durationMs);\n words.push(text);\n }\n resumeAudioContext();\n head.speakAudio(\n {\n audio: audioBuffer,\n words,\n wtimes,\n wdurations,\n },\n { lipsyncLang: options.lipsyncLang || lipsyncLang },\n onsubtitles\n );\n startSpeechEndPolling();\n },\n [lipsyncLang, startSpeechEndPolling, resumeAudioContext]\n );\n\n const speakText = useCallback((text, options = {}) => {\n if (!headRef.current) return;\n speechAbortedRef.current = false;\n speechPausedRef.current = false;\n resumeAudioContext();\n onSpeechStartRef.current?.(text);\n const onsubtitles = (sub) => {\n const t = Array.isArray(sub) ? sub.join(' ') : typeof sub === 'string' ? sub : '';\n if (t) onSubtitleRef.current?.(t);\n };\n\n if (ttsServiceRef.current === 'deepgram') {\n const fn = accurateLipSyncRef.current ? speakWithDeepgramAccurateLipSync : speakWithDeepgramStreaming;\n fn(text, options, onsubtitles).catch((err) => {\n console.error('Deepgram TTS failed:', err);\n onSpeechEndRef.current?.();\n onErrorRef.current?.(err);\n });\n } else {\n headRef.current.speakText(text, options, onsubtitles);\n startSpeechEndPolling();\n }\n }, [speakWithDeepgramStreaming, speakWithDeepgramAccurateLipSync, startSpeechEndPolling, resumeAudioContext]);\n\n const pauseSpeaking = useCallback(() => {\n const head = headRef.current;\n if (accurateLipSyncRef.current && head && !head.isStreaming) {\n // True pause: stop current phrase, wait in loop until Resume (no abort)\n speechPausedRef.current = true;\n head.pauseSpeaking();\n return;\n }\n // Streaming or non–phrase mode: Pause = stop (cannot resume stream)\n speechAbortedRef.current = true;\n if (streamWsRef.current) {\n try { streamWsRef.current.close(); } catch (_) {}\n streamWsRef.current = null;\n }\n if (head) {\n if (head.isStreaming) head.streamInterrupt();\n else head.pauseSpeaking();\n }\n if (speechEndCheckRef.current) {\n clearInterval(speechEndCheckRef.current);\n speechEndCheckRef.current = null;\n }\n onSpeechEndRef.current?.();\n }, []);\n\n const stopSpeaking = useCallback(() => {\n speechAbortedRef.current = true;\n if (streamWsRef.current) {\n try { streamWsRef.current.close(); } catch (_) {}\n streamWsRef.current = null;\n }\n if (speechEndCheckRef.current) {\n clearInterval(speechEndCheckRef.current);\n speechEndCheckRef.current = null;\n }\n const head = headRef.current;\n if (head) {\n if (head.isStreaming) head.streamInterrupt();\n else head.stopSpeaking();\n }\n onSpeechEndRef.current?.();\n }, []);\n\n const resumeSpeaking = useCallback(async () => {\n speechPausedRef.current = false;\n }, []);\n\n useImperativeHandle(ref, () => ({\n speakText,\n pauseSpeaking,\n resumeSpeaking,\n stopSpeaking,\n isReady,\n get isSpeaking() {\n return !!headRef.current?.isSpeaking;\n },\n talkingHead: headRef.current,\n }), [speakText, pauseSpeaking, resumeSpeaking, stopSpeaking, isReady]);\n\n return (\n <div className={`narrator-avatar-container ${className}`} style={{ position: 'relative', ...style }}>\n <div\n ref={containerRef}\n className=\"talking-head-viewer\"\n style={{ width: '100%', height: '100%', minHeight: '400px' }}\n />\n {isLoading && (\n <div\n className=\"loading-overlay\"\n style={{\n position: 'absolute',\n top: '50%',\n left: '50%',\n transform: 'translate(-50%, -50%)',\n color: '#333',\n fontSize: '18px',\n zIndex: 10,\n }}\n >\n Loading avatar...\n </div>\n )}\n {error && (\n <div\n className=\"error-overlay\"\n style={{\n position: 'absolute',\n top: '50%',\n left: '50%',\n transform: 'translate(-50%, -50%)',\n color: '#c00',\n fontSize: '14px',\n textAlign: 'center',\n zIndex: 10,\n padding: '20px',\n maxWidth: '90%',\n whiteSpace: 'pre-line',\n }}\n >\n {error}\n </div>\n )}\n </div>\n );\n});\n\nNarratorAvatar.displayName = 'NarratorAvatar';\n\nexport default NarratorAvatar;\n"],"names":["DEEPGRAM_SPEAK_URL","DEEPGRAM_SPEAK_WS_URL","DEEPGRAM_FAST_RESPONSE_PARAMS","splitIntoPhrases","text","t","p","stretchPCM","arrayBuffer","rate","int16","n","outLen","out","i","src","j","f","s0","s1","v","wavToAudioBuffer","audioCtx","wavArrayBuffer","view","sampleRate","numChannels","totalBytes","numSamplesPerChannel","audioBuffer","ch","channel","estimatedWordTimings","words","totalLen","s","w","totalMs","wtimes","wdurations","dur","GESTURE_NAMES","pickGestureForPhrase","phrase","lower","mir","hash","h","c","name","EXTENSION_FIX_MESSAGE","SMOOTH_DEFAULTS","NarratorAvatar","forwardRef","avatarUrl","avatarBody","cameraView","mood","ttsLang","ttsVoice","ttsService","ttsApiKey","ttsEndpoint","lipsyncModules","lipsyncLang","modelFPS","modelPixelRatio","modelMovementFactor","mixerGainSpeech","avatarIdleEyeContact","avatarIdleHeadMove","avatarSpeakingEyeContact","avatarSpeakingHeadMove","onReady","onError","onSpeechStart","onSpeechEnd","onSubtitle","speechRate","accurateLipSync","speechGestures","getGestureForPhrase","className","style","ref","containerRef","useRef","headRef","isLoading","setIsLoading","useState","error","setError","isReady","setIsReady","speechEndCheckRef","ttsServiceRef","ttsApiKeyRef","ttsVoiceRef","onReadyRef","onErrorRef","onSpeechStartRef","onSpeechEndRef","onSubtitleRef","speechRateRef","accurateLipSyncRef","speechGesturesRef","getGestureForPhraseRef","useEffect","loadCancelledRef","loadResolvedRef","streamingStartPromiseRef","speechAbortedRef","speechPausedRef","streamWsRef","node","cancelled","timeoutId","initDone","head","ro","doInit","el","useDeepgram","googleKey","__vite_import_meta_env__","options","TalkingHead","initErr","msg","_a","avatarConfig","ev","err","startSpeechEndPolling","useCallback","resumeAudioContext","ctx","speakWithDeepgramStreaming","onsubtitles","apiKey","model","params","wsUrl","phrases","phraseList","phraseIndexRef","wsRef","firstChunkRef","currentPhraseRef","resolveAll","rejectAll","allDonePromise","res","rej","onAudioEnd","_b","_c","_d","ws","event","buf","custom","g","scale","d","speakWithDeepgramAccurateLipSync","url","durationMs","r","speakText","sub","pauseSpeaking","stopSpeaking","resumeSpeaking","useImperativeHandle","jsxs","jsx"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sage-rsc/narrator-avatar",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "React component for 3D talking avatars with lip-sync, Deepgram/Google TTS, content-aware gestures, and pause/resume",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/narrator-avatar.umd.cjs",
|
|
7
|
+
"module": "./dist/narrator-avatar.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/narrator-avatar.js",
|
|
11
|
+
"require": "./dist/narrator-avatar.umd.cjs"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "vite build",
|
|
20
|
+
"prepublishOnly": "npm run build"
|
|
21
|
+
},
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"react": ">=18.0.0",
|
|
24
|
+
"react-dom": ">=18.0.0"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@met4citizen/talkinghead": "^1.7.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"react": "^18.3.1",
|
|
31
|
+
"react-dom": "^18.3.1",
|
|
32
|
+
"vite": "^6.0.0",
|
|
33
|
+
"@vitejs/plugin-react": "^4.3.0"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"react",
|
|
37
|
+
"avatar",
|
|
38
|
+
"talking",
|
|
39
|
+
"narrator-avatar",
|
|
40
|
+
"lip-sync",
|
|
41
|
+
"speech",
|
|
42
|
+
"tts",
|
|
43
|
+
"deepgram",
|
|
44
|
+
"voice",
|
|
45
|
+
"3d"
|
|
46
|
+
],
|
|
47
|
+
"author": "",
|
|
48
|
+
"license": "MIT"
|
|
49
|
+
}
|