narrator-avatar 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +137 -0
- package/dist/narrator-avatar.js +539 -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,137 @@
|
|
|
1
|
+
# narrator-avatar
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/narrator-avatar)
|
|
4
|
+
[](https://www.npmjs.com/package/narrator-avatar)
|
|
5
|
+
|
|
6
|
+
React component for 3D talking avatars with lip-sync, Deepgram or Google TTS, content-aware hand gestures, and pause/resume. Built on [@met4citizen/talkinghead](https://www.npmjs.com/package/@met4citizen/talkinghead).
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- **3D avatars** – Ready Player Me–compatible GLB models (full-body)
|
|
11
|
+
- **Lip-sync** – Word-level sync with Deepgram or Google TTS
|
|
12
|
+
- **TTS** – Deepgram (streaming) or Google Cloud Text-to-Speech
|
|
13
|
+
- **Gestures** – Content-aware hand gestures using all 8 built-in: handup, index, ok, thumbup, thumbdown, side, shrug, namaste
|
|
14
|
+
- **Playback** – Speak, Pause, Resume, Stop (phrase-level in accurate mode)
|
|
15
|
+
- **Accessibility** – Subtitle callback for closed captions
|
|
16
|
+
|
|
17
|
+
## Table of contents
|
|
18
|
+
|
|
19
|
+
- [Install](#install)
|
|
20
|
+
- [Usage](#usage)
|
|
21
|
+
- [Using with Vite](#using-with-vite)
|
|
22
|
+
- [Props](#props)
|
|
23
|
+
- [Ref API](#ref-api)
|
|
24
|
+
- [Environment variables](#environment-variables)
|
|
25
|
+
- [TypeScript](#typescript)
|
|
26
|
+
- [License](#license)
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install narrator-avatar
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Peer dependencies (React 18+) and `@met4citizen/talkinghead` are installed automatically.
|
|
35
|
+
|
|
36
|
+
**Using Vite?** Add one line to your Vite config or the avatar will fail to load. See [Using with Vite](#using-with-vite).
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
```jsx
|
|
41
|
+
import { useRef } from 'react';
|
|
42
|
+
import NarratorAvatar from 'narrator-avatar';
|
|
43
|
+
|
|
44
|
+
function MyPage() {
|
|
45
|
+
const avatarRef = useRef(null);
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div style={{ width: '400px', height: '500px' }}>
|
|
49
|
+
<NarratorAvatar
|
|
50
|
+
ref={avatarRef}
|
|
51
|
+
avatarUrl="/avatars/brunette.glb"
|
|
52
|
+
avatarBody="F"
|
|
53
|
+
ttsService="deepgram"
|
|
54
|
+
ttsVoice="aura-2-aurora-en"
|
|
55
|
+
ttsApiKey={import.meta.env.VITE_DEEPGRAM_API_KEY}
|
|
56
|
+
accurateLipSync={true}
|
|
57
|
+
speechRate={0.9}
|
|
58
|
+
onReady={() => console.log('Ready')}
|
|
59
|
+
onSpeechStart={(text) => console.log('Started:', text)}
|
|
60
|
+
onSpeechEnd={() => console.log('Ended')}
|
|
61
|
+
onSubtitle={(text) => console.log('Subtitle:', text)}
|
|
62
|
+
/>
|
|
63
|
+
<button onClick={() => avatarRef.current?.speakText('Hello! How are you?')}>
|
|
64
|
+
Speak
|
|
65
|
+
</button>
|
|
66
|
+
<button onClick={() => avatarRef.current?.pauseSpeaking()}>Pause</button>
|
|
67
|
+
<button onClick={() => avatarRef.current?.resumeSpeaking()}>Resume</button>
|
|
68
|
+
<button onClick={() => avatarRef.current?.stopSpeaking()}>Stop</button>
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Using with Vite
|
|
75
|
+
|
|
76
|
+
**Required for Vite apps.** The underlying `@met4citizen/talkinghead` package loads lip-sync language modules via dynamic `import()`. If Vite pre-bundles it, those imports break and you get a 404 on `lipsync-en.mjs` and `Cannot read properties of undefined (reading 'preProcessText')`.
|
|
77
|
+
|
|
78
|
+
Exclude it from optimization:
|
|
79
|
+
|
|
80
|
+
```js
|
|
81
|
+
// vite.config.js or vite.config.ts
|
|
82
|
+
import { defineConfig } from 'vite';
|
|
83
|
+
import react from '@vitejs/plugin-react';
|
|
84
|
+
|
|
85
|
+
export default defineConfig({
|
|
86
|
+
plugins: [react()],
|
|
87
|
+
optimizeDeps: { exclude: ['@met4citizen/talkinghead'] },
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Restart the dev server after changing the config.
|
|
92
|
+
|
|
93
|
+
## Props
|
|
94
|
+
|
|
95
|
+
| Prop | Description |
|
|
96
|
+
|------|-------------|
|
|
97
|
+
| `avatarUrl` | URL to GLB model (e.g. `/avatars/brunette.glb`) |
|
|
98
|
+
| `avatarBody` | `'M'` or `'F'` for posture |
|
|
99
|
+
| `cameraView` | Camera framing (default `'mid'`) |
|
|
100
|
+
| `cameraRotateEnable` | Allow mouse drag to rotate view (default `false`). Set `true` to enable. |
|
|
101
|
+
| `cameraZoomEnable` | Allow mouse wheel to zoom (default `false`). Set `true` to enable. |
|
|
102
|
+
| `cameraPanEnable` | Allow mouse to pan (default `false`) |
|
|
103
|
+
| `ttsService` | `'google'` or `'deepgram'` |
|
|
104
|
+
| `ttsVoice` | Deepgram: e.g. `aura-2-mars-en`, `aura-2-aurora-en`. Google: e.g. `en-GB-Standard-A` |
|
|
105
|
+
| `ttsApiKey` | API key (or set `VITE_DEEPGRAM_API_KEY` / `VITE_GOOGLE_TTS_API_KEY`) |
|
|
106
|
+
| `lipsyncModules` | Array of language codes (default `['en']`) |
|
|
107
|
+
| `lipsyncLang` | Lip-sync language (default `'en'`) |
|
|
108
|
+
| `accurateLipSync` | `true` = REST per phrase, best lip-sync + pause/resume |
|
|
109
|
+
| `speechRate` | e.g. `0.9` for 10% slower (pitch-preserving) |
|
|
110
|
+
| `speechGestures` | Content-aware hand gestures (default `true`) |
|
|
111
|
+
| `onReady`, `onError`, `onSpeechStart`, `onSpeechEnd`, `onSubtitle` | Callbacks |
|
|
112
|
+
|
|
113
|
+
## Ref API
|
|
114
|
+
|
|
115
|
+
| Method / property | Description |
|
|
116
|
+
|-------------------|-------------|
|
|
117
|
+
| `speakText(text, options?)` | Speak text via TTS |
|
|
118
|
+
| `pauseSpeaking()` | Pause (phrase-level when `accurateLipSync` is true) |
|
|
119
|
+
| `resumeSpeaking()` | Resume from next phrase |
|
|
120
|
+
| `stopSpeaking()` | Stop and clear |
|
|
121
|
+
| `isReady` | Whether the avatar has finished loading |
|
|
122
|
+
| `isSpeaking` | Whether the avatar is currently speaking |
|
|
123
|
+
|
|
124
|
+
## Environment variables
|
|
125
|
+
|
|
126
|
+
| Variable | Use |
|
|
127
|
+
|----------|-----|
|
|
128
|
+
| `VITE_DEEPGRAM_API_KEY` | Deepgram TTS (or pass `ttsApiKey` prop) |
|
|
129
|
+
| `VITE_GOOGLE_TTS_API_KEY` | Google TTS when `ttsService="google"` (or pass `ttsApiKey`) |
|
|
130
|
+
|
|
131
|
+
## TypeScript
|
|
132
|
+
|
|
133
|
+
The package is JavaScript. For TypeScript, add a declaration file (e.g. `src/narrator-avatar.d.ts`) that declares the component props and ref type, or use the component with `// @ts-expect-error` if you prefer.
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
MIT
|
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
import { jsxs as ir, jsx as St } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef as cr, useRef as l, useState as It, useEffect as Pt, useCallback as q, useImperativeHandle as lr } from "react";
|
|
3
|
+
import { TalkingHead as dr } from "@met4citizen/talkinghead";
|
|
4
|
+
const B = {}, Yt = "https://api.deepgram.com/v1/speak", fr = "wss://api.deepgram.com/v1/speak", jt = "encoding=linear16&container=wav&sample_rate=24000", qt = 24e3, Mt = 44, Ht = 430;
|
|
5
|
+
function Xt(I) {
|
|
6
|
+
const n = I.trim();
|
|
7
|
+
return n ? n.split(new RegExp("(?<=[.!?])\\s+")).map((u) => u.trim()).filter(Boolean) : [];
|
|
8
|
+
}
|
|
9
|
+
function mr(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 k = Math.ceil(u / n), w = new Int16Array(k);
|
|
14
|
+
for (let A = 0; A < k; A++) {
|
|
15
|
+
const T = A * n, P = Math.floor(T), F = T - P, L = P >= u ? o[u - 1] : o[P], J = P + 1 >= u ? o[u - 1] : o[P + 1], tt = (1 - F) * L + F * J;
|
|
16
|
+
w[A] = Math.max(-32768, Math.min(32767, Math.round(tt)));
|
|
17
|
+
}
|
|
18
|
+
return w.buffer;
|
|
19
|
+
}
|
|
20
|
+
function Zt(I, n) {
|
|
21
|
+
if (n.byteLength < Mt) return null;
|
|
22
|
+
const o = new DataView(n), u = o.getUint32(24, !0), k = Math.max(1, o.getUint16(22, !0)), w = n.byteLength - Mt, A = w / (2 * k), T = I.createBuffer(k, A, u), P = new Int16Array(n, Mt, w / 2 | 0);
|
|
23
|
+
for (let F = 0; F < k; F++) {
|
|
24
|
+
const L = T.getChannelData(F);
|
|
25
|
+
for (let J = 0; J < A; J++)
|
|
26
|
+
L[J] = P[J * k + F] / 32768;
|
|
27
|
+
}
|
|
28
|
+
return T;
|
|
29
|
+
}
|
|
30
|
+
function hr(I) {
|
|
31
|
+
const n = I.trim().split(/\s+/).filter(Boolean);
|
|
32
|
+
if (n.length === 0)
|
|
33
|
+
return { words: [I || " "], wtimes: [0], wdurations: [Ht] };
|
|
34
|
+
const o = n.reduce((T, P) => T + P.length, 0) || 1, u = n.length * Ht, k = [], w = [];
|
|
35
|
+
let A = 0;
|
|
36
|
+
for (const T of n) {
|
|
37
|
+
k.push(A);
|
|
38
|
+
const P = T.length / o * u;
|
|
39
|
+
w.push(P), A += P;
|
|
40
|
+
}
|
|
41
|
+
return { words: n, wtimes: k, 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|hello|hi there|good morning|good afternoon|good evening)\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?|raise your hand|tell me|question)\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|bravo|good|nice work|love it|correct)\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|avoid|bad)\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|wave|hello,|hi,)\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|sometimes|hard to say)\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\.|watch|see here|over here|this way)\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|yep|yes,|absolutely)\b/i.test(o))
|
|
63
|
+
return { name: "ok", dur: 1.6, mirror: u() };
|
|
64
|
+
const k = n.split("").reduce((A, T) => A * 31 + T.charCodeAt(0) | 0, 0), w = Qt[Math.abs(k) % 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: !1,
|
|
93
|
+
// No mouse rotate by default (avoids accidental drag)
|
|
94
|
+
cameraZoomEnable: !1,
|
|
95
|
+
// No mouse zoom by default
|
|
96
|
+
cameraPanEnable: !1
|
|
97
|
+
}, pr = cr(({
|
|
98
|
+
avatarUrl: I = "/avatars/brunette.glb",
|
|
99
|
+
avatarBody: n = "F",
|
|
100
|
+
cameraView: o = "mid",
|
|
101
|
+
mood: u = "neutral",
|
|
102
|
+
ttsLang: k = "en-GB",
|
|
103
|
+
ttsVoice: w = "en-GB-Standard-A",
|
|
104
|
+
ttsService: A = "google",
|
|
105
|
+
// 'google' | 'deepgram'
|
|
106
|
+
ttsApiKey: T = null,
|
|
107
|
+
ttsEndpoint: P = "https://texttospeech.googleapis.com/v1beta1/text:synthesize",
|
|
108
|
+
lipsyncModules: F = ["en"],
|
|
109
|
+
lipsyncLang: L = "en",
|
|
110
|
+
// Smoothness overrides (merged with SMOOTH_DEFAULTS)
|
|
111
|
+
modelFPS: J,
|
|
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
|
+
/** Allow user to rotate camera with mouse (default false). Set true to allow drag-to-rotate. */
|
|
137
|
+
cameraRotateEnable: tr = !1,
|
|
138
|
+
/** Allow user to zoom with mouse wheel (default false). Set true to allow zoom. */
|
|
139
|
+
cameraZoomEnable: rr = !1,
|
|
140
|
+
/** Allow user to pan camera (default false). */
|
|
141
|
+
cameraPanEnable: er = !1,
|
|
142
|
+
className: nr = "",
|
|
143
|
+
style: sr = {}
|
|
144
|
+
}, or) => {
|
|
145
|
+
const yt = l(null), m = l(null), [ar, ot] = It(!0), [Gt, at] = It(null), [Kt, ur] = It(!1), M = l(null), Bt = l(A), ut = l(T), it = l(w), ct = l(Ct), U = l($t), lt = l(Ot), i = l(Lt), $ = l(Rt), Wt = l(ht), bt = l(pt), At = l(gt), H = l(wt);
|
|
146
|
+
Pt(() => {
|
|
147
|
+
Wt.current = Math.max(0.6, Math.min(1.2, Number(ht) || 1)), bt.current = !!pt, At.current = !!gt, H.current = wt;
|
|
148
|
+
}, [ht, pt, gt, wt]), Pt(() => {
|
|
149
|
+
Bt.current = A, ut.current = T, it.current = w, ct.current = Ct, U.current = $t, lt.current = Ot, i.current = Lt, $.current = Rt;
|
|
150
|
+
});
|
|
151
|
+
const Z = l(!1), dt = l(!1), kt = l(null), x = l(!1), Q = l(!1), W = l(null);
|
|
152
|
+
Pt(() => {
|
|
153
|
+
const t = yt.current;
|
|
154
|
+
if (!t) return;
|
|
155
|
+
Z.current = !1, dt.current = !1;
|
|
156
|
+
let s = !1, h = null, e = !1, f = null, p = null;
|
|
157
|
+
const D = () => {
|
|
158
|
+
var O;
|
|
159
|
+
if (s || e) return;
|
|
160
|
+
const g = yt.current;
|
|
161
|
+
if (!g || g.offsetWidth <= 0 || g.offsetHeight <= 0) return;
|
|
162
|
+
e = !0, p && p.disconnect();
|
|
163
|
+
const R = A === "deepgram", G = !R && (T || (B == null ? void 0 : B.VITE_GOOGLE_TTS_API_KEY) || ""), E = {
|
|
164
|
+
...xt,
|
|
165
|
+
cameraView: o,
|
|
166
|
+
lipsyncModules: F,
|
|
167
|
+
lipsyncLang: L,
|
|
168
|
+
ttsLang: k,
|
|
169
|
+
ttsVoice: w,
|
|
170
|
+
ttsTrimStart: xt.ttsTrimStart,
|
|
171
|
+
ttsTrimEnd: xt.ttsTrimEnd,
|
|
172
|
+
...J != null && { modelFPS: J },
|
|
173
|
+
...tt != null && { modelPixelRatio: tt },
|
|
174
|
+
...Dt != null && { modelMovementFactor: Dt },
|
|
175
|
+
...Nt != null && { mixerGainSpeech: Nt },
|
|
176
|
+
...rt != null && { avatarIdleEyeContact: rt },
|
|
177
|
+
...et != null && { avatarIdleHeadMove: et },
|
|
178
|
+
...nt != null && { avatarSpeakingEyeContact: nt },
|
|
179
|
+
...st != null && { avatarSpeakingHeadMove: st },
|
|
180
|
+
cameraRotateEnable: tr,
|
|
181
|
+
cameraZoomEnable: rr,
|
|
182
|
+
cameraPanEnable: er,
|
|
183
|
+
...R ? { ttsEndpoint: null, ttsApikey: null } : G ? { ttsEndpoint: P, ttsApikey: G } : { ttsEndpoint: null, ttsApikey: null }
|
|
184
|
+
};
|
|
185
|
+
try {
|
|
186
|
+
f = new dr(g, E);
|
|
187
|
+
} catch (r) {
|
|
188
|
+
if (s) return;
|
|
189
|
+
ot(!1);
|
|
190
|
+
const a = (r == null ? void 0 : r.message) ?? String(r);
|
|
191
|
+
at(a ? `${a}
|
|
192
|
+
|
|
193
|
+
${_}` : _), (O = U.current) == null || O.call(U, r);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
m.current = f;
|
|
197
|
+
const N = {
|
|
198
|
+
url: I,
|
|
199
|
+
body: n,
|
|
200
|
+
avatarMood: u,
|
|
201
|
+
ttsLang: k,
|
|
202
|
+
ttsVoice: w,
|
|
203
|
+
lipsyncLang: L,
|
|
204
|
+
...rt != null && { avatarIdleEyeContact: rt },
|
|
205
|
+
...et != null && { avatarIdleHeadMove: et },
|
|
206
|
+
...nt != null && { avatarSpeakingEyeContact: nt },
|
|
207
|
+
...st != null && { avatarSpeakingHeadMove: st }
|
|
208
|
+
};
|
|
209
|
+
h = setTimeout(() => {
|
|
210
|
+
Z.current || dt.current || (ot(!1), at(`Avatar failed to load (timeout). Check that the model file exists (e.g. /avatars/brunette.glb).
|
|
211
|
+
|
|
212
|
+
` + _));
|
|
213
|
+
}, 15e3), f.showAvatar(N, (r) => {
|
|
214
|
+
r != null && r.lengthComputable && r.loaded != null && r.total != null && Math.min(100, Math.round(r.loaded / r.total * 100));
|
|
215
|
+
}).then(() => {
|
|
216
|
+
var r;
|
|
217
|
+
Z.current || (dt.current = !0, h && clearTimeout(h), h = null, f.start(), ot(!1), ur(!0), at(null), (r = ct.current) == null || r.call(ct));
|
|
218
|
+
}).catch((r) => {
|
|
219
|
+
var c;
|
|
220
|
+
if (Z.current) return;
|
|
221
|
+
dt.current = !0, h && clearTimeout(h), h = null, ot(!1);
|
|
222
|
+
const a = (r == null ? void 0 : r.message) || String(r);
|
|
223
|
+
at(a ? `${a}
|
|
224
|
+
|
|
225
|
+
${_}` : _), (c = U.current) == null || c.call(U, r);
|
|
226
|
+
});
|
|
227
|
+
};
|
|
228
|
+
return p = new ResizeObserver(() => D()), p.observe(t), requestAnimationFrame(() => D()), () => {
|
|
229
|
+
if (s = !0, Z.current = !0, h && clearTimeout(h), p && p.disconnect(), M.current && clearInterval(M.current), m.current) {
|
|
230
|
+
try {
|
|
231
|
+
m.current.stop(), m.current.stopSpeaking();
|
|
232
|
+
} catch {
|
|
233
|
+
}
|
|
234
|
+
m.current = null;
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
}, []);
|
|
238
|
+
const ft = q(() => {
|
|
239
|
+
M.current && clearInterval(M.current), M.current = setInterval(() => {
|
|
240
|
+
var t;
|
|
241
|
+
m.current && !m.current.isSpeaking && (M.current && clearInterval(M.current), M.current = null, (t = i.current) == null || t.call(i));
|
|
242
|
+
}, 200);
|
|
243
|
+
}, []), Y = q(() => {
|
|
244
|
+
var s;
|
|
245
|
+
const t = (s = m.current) == null ? void 0 : s.audioCtx;
|
|
246
|
+
(t == null ? void 0 : t.state) === "suspended" && t.resume();
|
|
247
|
+
}, []), Ut = q(
|
|
248
|
+
async (t, s = {}, h) => {
|
|
249
|
+
const e = m.current;
|
|
250
|
+
if (!(e != null && e.audioCtx)) return;
|
|
251
|
+
Y();
|
|
252
|
+
const f = ut.current || (B == null ? void 0 : B.VITE_DEEPGRAM_API_KEY) || "";
|
|
253
|
+
if (!f) {
|
|
254
|
+
console.warn("NarratorAvatar: Deepgram TTS requires ttsApiKey or VITE_DEEPGRAM_API_KEY");
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const p = s.ttsVoice || it.current || "aura-2-thalia-en", D = new URLSearchParams({
|
|
258
|
+
encoding: "linear16",
|
|
259
|
+
sample_rate: String(qt),
|
|
260
|
+
model: p
|
|
261
|
+
}), g = `${fr}?${D.toString()}`, R = Wt.current, G = Xt(t), E = G.length > 0 ? G : [t.trim() || " "], N = { current: 0 }, O = { current: null }, r = { current: !0 }, a = { current: E[0] };
|
|
262
|
+
let c = null, V = null;
|
|
263
|
+
const z = new Promise((d, C) => {
|
|
264
|
+
c = d, V = C;
|
|
265
|
+
}), mt = () => {
|
|
266
|
+
var C, K, j, y;
|
|
267
|
+
if (x.current) {
|
|
268
|
+
(C = O.current) == null || C.close(), W.current = null, c == null || c();
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (N.current += 1, N.current >= E.length) {
|
|
272
|
+
(K = i.current) == null || K.call(i), (j = O.current) == null || j.close(), W.current = null, c == null || c();
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
r.current = !0, a.current = E[N.current], (y = $.current) == null || y.call($, a.current);
|
|
276
|
+
const d = O.current;
|
|
277
|
+
d && d.readyState === WebSocket.OPEN && (d.send(JSON.stringify({ type: "Speak", text: a.current })), d.send(JSON.stringify({ type: "Flush" })));
|
|
278
|
+
};
|
|
279
|
+
e.isStreaming || (kt.current || (kt.current = e.streamStart(
|
|
280
|
+
{
|
|
281
|
+
sampleRate: qt,
|
|
282
|
+
waitForAudioChunks: !0,
|
|
283
|
+
lipsyncType: "words",
|
|
284
|
+
lipsyncLang: s.lipsyncLang || L
|
|
285
|
+
},
|
|
286
|
+
null,
|
|
287
|
+
mt,
|
|
288
|
+
null,
|
|
289
|
+
null
|
|
290
|
+
)), await kt.current), x.current = !1;
|
|
291
|
+
const S = new WebSocket(g, ["token", f]);
|
|
292
|
+
return O.current = S, W.current = S, S.binaryType = "arraybuffer", S.onopen = () => {
|
|
293
|
+
var d;
|
|
294
|
+
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" }));
|
|
295
|
+
}, S.onmessage = (d) => {
|
|
296
|
+
var j, y;
|
|
297
|
+
if (typeof d.data == "string") {
|
|
298
|
+
try {
|
|
299
|
+
const b = JSON.parse(d.data);
|
|
300
|
+
(b.type === "Flushed" || b.type === "Cleared") && e.streamNotifyEnd();
|
|
301
|
+
} catch {
|
|
302
|
+
}
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
let C = d.data instanceof ArrayBuffer ? d.data : (j = d.data) == null ? void 0 : j.buffer;
|
|
306
|
+
if (!C || C.byteLength === 0 || !e.isStreaming) return;
|
|
307
|
+
R < 1 && (C = mr(C, R));
|
|
308
|
+
const K = a.current;
|
|
309
|
+
if (r.current && K) {
|
|
310
|
+
if (r.current = !1, At.current && e.playGesture) {
|
|
311
|
+
const X = (y = H.current) == null ? void 0 : y.call(H, K), v = X ?? _t(K);
|
|
312
|
+
v != null && v.name && e.playGesture(v.name, v.dur ?? 1.8, v.mirror ?? !1, 800);
|
|
313
|
+
}
|
|
314
|
+
let { words: b, wtimes: Tt, wdurations: Et } = hr(K);
|
|
315
|
+
if (R < 1) {
|
|
316
|
+
const X = 1 / R;
|
|
317
|
+
Tt = Tt.map((v) => v * X), Et = Et.map((v) => v * X);
|
|
318
|
+
}
|
|
319
|
+
e.streamAudio({ audio: C, words: b, wtimes: Tt, wdurations: Et });
|
|
320
|
+
} else
|
|
321
|
+
e.streamAudio({ audio: C });
|
|
322
|
+
}, S.onerror = () => {
|
|
323
|
+
V == null || V(new Error("Deepgram WebSocket error"));
|
|
324
|
+
}, S.onclose = () => {
|
|
325
|
+
var d;
|
|
326
|
+
O.current = null, W.current === S && (W.current = null), N.current < E.length && ((d = i.current) == null || d.call(i)), c == null || c();
|
|
327
|
+
}, z;
|
|
328
|
+
},
|
|
329
|
+
[L, Y]
|
|
330
|
+
), Vt = q(
|
|
331
|
+
async (t, s = {}, h) => {
|
|
332
|
+
var G, E, N, O;
|
|
333
|
+
const e = m.current;
|
|
334
|
+
if (!(e != null && e.audioCtx)) return;
|
|
335
|
+
x.current = !1, Q.current = !1, Y();
|
|
336
|
+
const f = ut.current || (B == null ? void 0 : B.VITE_DEEPGRAM_API_KEY) || "";
|
|
337
|
+
if (!f) {
|
|
338
|
+
console.warn("NarratorAvatar: Deepgram TTS requires ttsApiKey or VITE_DEEPGRAM_API_KEY");
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
e.isStreaming && e.streamStop();
|
|
342
|
+
const p = s.ttsVoice || it.current || "aura-2-thalia-en", D = `${Yt}?model=${encodeURIComponent(p)}&${jt}`, g = Xt(t), R = g.length > 0 ? g : [t.trim() || " "];
|
|
343
|
+
for (let r = 0; r < R.length && !x.current; r++) {
|
|
344
|
+
const a = R[r];
|
|
345
|
+
(G = $.current) == null || G.call($, a);
|
|
346
|
+
const c = await fetch(D, {
|
|
347
|
+
method: "POST",
|
|
348
|
+
headers: {
|
|
349
|
+
Authorization: `Token ${f}`,
|
|
350
|
+
"Content-Type": "application/json",
|
|
351
|
+
Accept: "audio/wav"
|
|
352
|
+
},
|
|
353
|
+
body: JSON.stringify({ text: a })
|
|
354
|
+
});
|
|
355
|
+
if (x.current) break;
|
|
356
|
+
if (!c.ok) throw new Error(`Deepgram TTS error: ${c.status} ${c.statusText}`);
|
|
357
|
+
const V = await c.arrayBuffer();
|
|
358
|
+
if (x.current) break;
|
|
359
|
+
const z = Zt(e.audioCtx, V);
|
|
360
|
+
if (!z) throw new Error("Failed to prepare audio");
|
|
361
|
+
const mt = z.duration * 1e3, S = a.trim().split(/\s+/).filter(Boolean), d = S.reduce((y, b) => y + b.length, 0) || 1;
|
|
362
|
+
let C = 0;
|
|
363
|
+
const K = [], j = [];
|
|
364
|
+
for (const y of S) {
|
|
365
|
+
K.push(C);
|
|
366
|
+
const b = y.length / d * mt;
|
|
367
|
+
j.push(b), C += b;
|
|
368
|
+
}
|
|
369
|
+
if (K.length === 0 && (S.push(a), K.push(0), j.push(mt)), At.current && e.playGesture) {
|
|
370
|
+
const y = (E = H.current) == null ? void 0 : E.call(H, a), b = y ?? _t(a);
|
|
371
|
+
b != null && b.name && e.playGesture(b.name, b.dur ?? 1.8, b.mirror ?? !1, 800);
|
|
372
|
+
}
|
|
373
|
+
for (e.speakAudio(
|
|
374
|
+
{ audio: z, words: S, wtimes: K, wdurations: j },
|
|
375
|
+
{ lipsyncLang: s.lipsyncLang || L },
|
|
376
|
+
null
|
|
377
|
+
); (N = m.current) != null && N.isSpeaking && !x.current; )
|
|
378
|
+
await new Promise((y) => setTimeout(y, 100));
|
|
379
|
+
if (x.current) break;
|
|
380
|
+
for (; Q.current && !x.current; )
|
|
381
|
+
await new Promise((y) => setTimeout(y, 100));
|
|
382
|
+
if (x.current) break;
|
|
383
|
+
}
|
|
384
|
+
x.current || (O = i.current) == null || O.call(i);
|
|
385
|
+
},
|
|
386
|
+
[L, Y]
|
|
387
|
+
);
|
|
388
|
+
q(
|
|
389
|
+
async (t, s = {}, h) => {
|
|
390
|
+
const e = m.current;
|
|
391
|
+
if (!(e != null && e.audioCtx)) return;
|
|
392
|
+
Y();
|
|
393
|
+
const f = ut.current || (B == null ? void 0 : B.VITE_DEEPGRAM_API_KEY) || "";
|
|
394
|
+
if (!f) {
|
|
395
|
+
console.warn("NarratorAvatar: Deepgram TTS requires ttsApiKey or VITE_DEEPGRAM_API_KEY");
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
const p = s.ttsVoice || it.current || "aura-2-thalia-en", D = `${Yt}?model=${encodeURIComponent(p)}&${jt}`, g = await fetch(D, {
|
|
399
|
+
method: "POST",
|
|
400
|
+
headers: {
|
|
401
|
+
Authorization: `Token ${f}`,
|
|
402
|
+
"Content-Type": "application/json",
|
|
403
|
+
Accept: "audio/wav"
|
|
404
|
+
},
|
|
405
|
+
body: JSON.stringify({ text: t })
|
|
406
|
+
});
|
|
407
|
+
if (!g.ok)
|
|
408
|
+
throw new Error(`Deepgram TTS error: ${g.status} ${g.statusText}`);
|
|
409
|
+
const R = await g.arrayBuffer(), G = Zt(e.audioCtx, R);
|
|
410
|
+
if (!G)
|
|
411
|
+
throw new Error("Failed to prepare audio for playback");
|
|
412
|
+
const E = G.duration * 1e3, N = t.trim().split(/\s+/).filter(Boolean), O = N.reduce((V, z) => V + z.length, 0) || 1;
|
|
413
|
+
let r = 0;
|
|
414
|
+
const a = [], c = [];
|
|
415
|
+
for (const V of N) {
|
|
416
|
+
a.push(r);
|
|
417
|
+
const z = V.length / O * E;
|
|
418
|
+
c.push(z), r += z;
|
|
419
|
+
}
|
|
420
|
+
a.length === 0 && (a.push(0), c.push(E), N.push(t)), Y(), e.speakAudio(
|
|
421
|
+
{
|
|
422
|
+
audio: G,
|
|
423
|
+
words: N,
|
|
424
|
+
wtimes: a,
|
|
425
|
+
wdurations: c
|
|
426
|
+
},
|
|
427
|
+
{ lipsyncLang: s.lipsyncLang || L },
|
|
428
|
+
h
|
|
429
|
+
), ft();
|
|
430
|
+
},
|
|
431
|
+
[L, ft, Y]
|
|
432
|
+
);
|
|
433
|
+
const zt = q((t, s = {}) => {
|
|
434
|
+
var e;
|
|
435
|
+
if (!m.current) return;
|
|
436
|
+
x.current = !1, Q.current = !1, Y(), (e = lt.current) == null || e.call(lt, t);
|
|
437
|
+
const h = (f) => {
|
|
438
|
+
var D;
|
|
439
|
+
const p = Array.isArray(f) ? f.join(" ") : typeof f == "string" ? f : "";
|
|
440
|
+
p && ((D = $.current) == null || D.call($, p));
|
|
441
|
+
};
|
|
442
|
+
Bt.current === "deepgram" ? (bt.current ? Vt : Ut)(t, s, h).catch((p) => {
|
|
443
|
+
var D, g;
|
|
444
|
+
console.error("Deepgram TTS failed:", p), (D = i.current) == null || D.call(i), (g = U.current) == null || g.call(U, p);
|
|
445
|
+
}) : (m.current.speakText(t, s, h), ft());
|
|
446
|
+
}, [Ut, Vt, ft, Y]), vt = q(() => {
|
|
447
|
+
var s;
|
|
448
|
+
const t = m.current;
|
|
449
|
+
if (bt.current && t && !t.isStreaming) {
|
|
450
|
+
Q.current = !0, t.pauseSpeaking();
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
if (x.current = !0, W.current) {
|
|
454
|
+
try {
|
|
455
|
+
W.current.close();
|
|
456
|
+
} catch {
|
|
457
|
+
}
|
|
458
|
+
W.current = null;
|
|
459
|
+
}
|
|
460
|
+
t && (t.isStreaming ? t.streamInterrupt() : t.pauseSpeaking()), M.current && (clearInterval(M.current), M.current = null), (s = i.current) == null || s.call(i);
|
|
461
|
+
}, []), Ft = q(() => {
|
|
462
|
+
var s;
|
|
463
|
+
if (x.current = !0, W.current) {
|
|
464
|
+
try {
|
|
465
|
+
W.current.close();
|
|
466
|
+
} catch {
|
|
467
|
+
}
|
|
468
|
+
W.current = null;
|
|
469
|
+
}
|
|
470
|
+
M.current && (clearInterval(M.current), M.current = null);
|
|
471
|
+
const t = m.current;
|
|
472
|
+
t && (t.isStreaming ? t.streamInterrupt() : t.stopSpeaking()), (s = i.current) == null || s.call(i);
|
|
473
|
+
}, []), Jt = q(async () => {
|
|
474
|
+
Q.current = !1;
|
|
475
|
+
}, []);
|
|
476
|
+
return lr(or, () => ({
|
|
477
|
+
speakText: zt,
|
|
478
|
+
pauseSpeaking: vt,
|
|
479
|
+
resumeSpeaking: Jt,
|
|
480
|
+
stopSpeaking: Ft,
|
|
481
|
+
isReady: Kt,
|
|
482
|
+
get isSpeaking() {
|
|
483
|
+
var t;
|
|
484
|
+
return !!((t = m.current) != null && t.isSpeaking);
|
|
485
|
+
},
|
|
486
|
+
talkingHead: m.current
|
|
487
|
+
}), [zt, vt, Jt, Ft, Kt]), /* @__PURE__ */ ir("div", { className: `narrator-avatar-container ${nr}`, style: { position: "relative", ...sr }, children: [
|
|
488
|
+
/* @__PURE__ */ St(
|
|
489
|
+
"div",
|
|
490
|
+
{
|
|
491
|
+
ref: yt,
|
|
492
|
+
className: "talking-head-viewer",
|
|
493
|
+
style: { width: "100%", height: "100%", minHeight: "400px" }
|
|
494
|
+
}
|
|
495
|
+
),
|
|
496
|
+
ar && /* @__PURE__ */ St(
|
|
497
|
+
"div",
|
|
498
|
+
{
|
|
499
|
+
className: "loading-overlay",
|
|
500
|
+
style: {
|
|
501
|
+
position: "absolute",
|
|
502
|
+
top: "50%",
|
|
503
|
+
left: "50%",
|
|
504
|
+
transform: "translate(-50%, -50%)",
|
|
505
|
+
color: "#333",
|
|
506
|
+
fontSize: "18px",
|
|
507
|
+
zIndex: 10
|
|
508
|
+
},
|
|
509
|
+
children: "Loading avatar..."
|
|
510
|
+
}
|
|
511
|
+
),
|
|
512
|
+
Gt && /* @__PURE__ */ St(
|
|
513
|
+
"div",
|
|
514
|
+
{
|
|
515
|
+
className: "error-overlay",
|
|
516
|
+
style: {
|
|
517
|
+
position: "absolute",
|
|
518
|
+
top: "50%",
|
|
519
|
+
left: "50%",
|
|
520
|
+
transform: "translate(-50%, -50%)",
|
|
521
|
+
color: "#c00",
|
|
522
|
+
fontSize: "14px",
|
|
523
|
+
textAlign: "center",
|
|
524
|
+
zIndex: 10,
|
|
525
|
+
padding: "20px",
|
|
526
|
+
maxWidth: "90%",
|
|
527
|
+
whiteSpace: "pre-line"
|
|
528
|
+
},
|
|
529
|
+
children: Gt
|
|
530
|
+
}
|
|
531
|
+
)
|
|
532
|
+
] });
|
|
533
|
+
});
|
|
534
|
+
pr.displayName = "NarratorAvatar";
|
|
535
|
+
export {
|
|
536
|
+
pr as NarratorAvatar,
|
|
537
|
+
pr as default
|
|
538
|
+
};
|
|
539
|
+
//# 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/**\n * All hand gestures supported by @met4citizen/talkinghead (complete built-in set).\n * No other gesture names are available in the library; custom gestures require gestureTemplates.\n */\nconst GESTURE_NAMES = ['handup', 'index', 'ok', 'thumbup', 'thumbdown', 'side', 'shrug', 'namaste'];\n\n/**\n * Picks a gesture that matches the phrase content. Uses all 8 built-in gestures.\n * Returns { name, dur, mirror } or null to skip gesture.\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 / please -> namaste\n if (/\\b(thank you|thanks|thank ya|welcome|bye|goodbye|see you|so long|namaste|hello|hi there|good morning|good afternoon|good evening)\\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?|raise your hand|tell me|question)\\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|bravo|good|nice work|love it|correct)\\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|avoid|bad)\\b/i.test(lower)) {\n return { name: 'thumbdown', dur: 1.6, mirror: mir() };\n }\n // Softer no / not / disagreement / wave -> 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|wave|hello,|hi,)\\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|sometimes|hard to say)\\b/i.test(lower)) {\n return { name: 'shrug', dur: 2, mirror: false };\n }\n // Listing / emphasis / steps / directing attention -> index\n if (/\\b(first|second|third|one |two |three |number|remember|important|key|point|listen|look|note|step|next|then|finally|so,|so\\.|watch|see here|over here|this way)\\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|yep|yes,|absolutely)\\b/i.test(lower)) {\n return { name: 'ok', dur: 1.6, mirror: mir() };\n }\n // Neutral: spread across all 8 gestures by phrase hash\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: false, // No mouse rotate by default (avoids accidental drag)\n cameraZoomEnable: false, // No mouse zoom by default\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 /** Allow user to rotate camera with mouse (default false). Set true to allow drag-to-rotate. */\n cameraRotateEnable = false,\n /** Allow user to zoom with mouse wheel (default false). Set true to allow zoom. */\n cameraZoomEnable = false,\n /** Allow user to pan camera (default false). */\n cameraPanEnable = false,\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 cameraRotateEnable,\n cameraZoomEnable,\n cameraPanEnable,\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","cameraRotateEnable","cameraZoomEnable","cameraPanEnable","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,J){typeof exports=="object"&&typeof module<"u"?J(exports,require("react/jsx-runtime"),require("react"),require("@met4citizen/talkinghead")):typeof define=="function"&&define.amd?define(["exports","react/jsx-runtime","react","@met4citizen/talkinghead"],J):(V=typeof globalThis<"u"?globalThis:V||self,J(V.NarratorAvatar={},V.jsxRuntime,V.React,V.TalkingHead))})(this,(function(V,J,e,Yt){"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 u=new Int16Array(I),i=u.length;if(i===0)return I;const E=Math.ceil(i/s),w=new Int16Array(E);for(let A=0;A<E;A++){const T=A*s,R=Math.floor(T),U=T-R,L=R>=i?u[i-1]:u[R],q=R+1>=i?u[i-1]:u[R+1],et=(1-U)*L+U*q;w[A]=Math.max(-32768,Math.min(32767,Math.round(et)))}return w.buffer}function xt(I,s){if(s.byteLength<44)return null;const u=new DataView(s),i=u.getUint32(24,!0),E=Math.max(1,u.getUint16(22,!0)),w=s.byteLength-44,A=w/(2*E),T=I.createBuffer(E,A,i),R=new Int16Array(s,44,w/2|0);for(let U=0;U<E;U++){const L=T.getChannelData(U);for(let q=0;q<A;q++)L[q]=R[q*E+U]/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 u=s.reduce((T,R)=>T+R.length,0)||1,i=s.length*430,E=[],w=[];let A=0;for(const T of s){E.push(A);const R=T.length/u*i;w.push(R),A+=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 u=s.toLowerCase(),i=()=>Math.random()>.5;if(/\b(thank you|thanks|thank ya|welcome|bye|goodbye|see you|so long|namaste|hello|hi there|good morning|good afternoon|good evening)\b/i.test(u))return{name:"namaste",dur:1.8,mirror:!1};if(/\?$/.test(s)||/\b(wait|hold on|one moment|hang on|let me ask|any questions?|raise your hand|tell me|question)\b/i.test(u)||/^(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|bravo|good|nice work|love it|correct)\b/i.test(u))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|avoid|bad)\b/i.test(u))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|wave|hello,|hi,)\b/i.test(u)||/\b(no,|no\.|nah)\b/i.test(u))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|sometimes|hard to say)\b/i.test(u))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\.|watch|see here|over here|this way)\b/i.test(u)||/^(\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|yep|yes,|absolutely)\b/i.test(u))return{name:"ok",dur:1.6,mirror:i()};const E=s.split("").reduce((A,T)=>A*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:!1,cameraZoomEnable:!1,cameraPanEnable:!1},gt=e.forwardRef(({avatarUrl:I="/avatars/brunette.glb",avatarBody:s="F",cameraView:u="mid",mood:i="neutral",ttsLang:E="en-GB",ttsVoice:w="en-GB-Standard-A",ttsService:A="google",ttsApiKey:T=null,ttsEndpoint:R="https://texttospeech.googleapis.com/v1beta1/text:synthesize",lipsyncModules:U=["en"],lipsyncLang:L="en",modelFPS:q,modelPixelRatio:et,modelMovementFactor:Ot,mixerGainSpeech:$t,avatarIdleEyeContact:rt,avatarIdleHeadMove:nt,avatarSpeakingEyeContact:st,avatarSpeakingHeadMove:ot,onReady:Lt=()=>{},onError:Gt=()=>{},onSpeechStart:Wt=()=>{},onSpeechEnd:Kt=()=>{},onSubtitle:_t=null,speechRate:wt=1,accurateLipSync:yt=!1,speechGestures:bt=!0,getGestureForPhrase:At=null,cameraRotateEnable:te=!1,cameraZoomEnable:ee=!1,cameraPanEnable:re=!1,className:ne="",style:se={}},oe)=>{const Et=e.useRef(null),m=e.useRef(null),[ue,ut]=e.useState(!0),[Vt,at]=e.useState(null),[Bt,ae]=e.useState(!1),M=e.useRef(null),vt=e.useRef(A),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),zt=e.useRef(wt),Tt=e.useRef(yt),kt=e.useRef(bt),Y=e.useRef(At);e.useEffect(()=>{zt.current=Math.max(.6,Math.min(1.2,Number(wt)||1)),Tt.current=!!yt,kt.current=!!bt,Y.current=At},[wt,yt,bt,At]),e.useEffect(()=>{vt.current=A,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 o=!1,h=null,n=!1,d=null,p=null;const D=()=>{var O;if(o||n)return;const g=Et.current;if(!g||g.offsetWidth<=0||g.offsetHeight<=0)return;n=!0,p&&p.disconnect();const G=A==="deepgram",W=!G&&(T||($==null?void 0:$.VITE_GOOGLE_TTS_API_KEY)||""),k={...pt,cameraView:u,lipsyncModules:U,lipsyncLang:L,ttsLang:E,ttsVoice:w,ttsTrimStart:pt.ttsTrimStart,ttsTrimEnd:pt.ttsTrimEnd,...q!=null&&{modelFPS:q},...et!=null&&{modelPixelRatio:et},...Ot!=null&&{modelMovementFactor:Ot},...$t!=null&&{mixerGainSpeech:$t},...rt!=null&&{avatarIdleEyeContact:rt},...nt!=null&&{avatarIdleHeadMove:nt},...st!=null&&{avatarSpeakingEyeContact:st},...ot!=null&&{avatarSpeakingHeadMove:ot},cameraRotateEnable:te,cameraZoomEnable:ee,cameraPanEnable:re,...G?{ttsEndpoint:null,ttsApikey:null}:W?{ttsEndpoint:R,ttsApikey:W}:{ttsEndpoint:null,ttsApikey:null}};try{d=new Yt.TalkingHead(g,k)}catch(r){if(o)return;ut(!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},...ot!=null&&{avatarSpeakingHeadMove:ot}};h=setTimeout(()=>{Q.current||dt.current||(ut(!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(),ut(!1),ae(!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,ut(!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(o=!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)},[]),F=e.useCallback(()=>{var o;const t=(o=m.current)==null?void 0:o.audioCtx;(t==null?void 0:t.state)==="suspended"&&t.resume()},[]),jt=e.useCallback(async(t,o={},h)=>{const n=m.current;if(!(n!=null&&n.audioCtx))return;F();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=o.ttsVoice||ct.current||"aura-2-thalia-en",D=new URLSearchParams({encoding:"linear16",sample_rate:String(24e3),model:p}),g=`${Zt}?${D.toString()}`,G=zt.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,v=null;const z=new Promise((f,C)=>{l=f,v=C}),ht=()=>{var C,K,H,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),(H=O.current)==null||H.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:o.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 H,y;if(typeof f.data=="string"){try{const b=JSON.parse(f.data);(b.type==="Flushed"||b.type==="Cleared")&&n.streamNotifyEnd()}catch{}return}let C=f.data instanceof ArrayBuffer?f.data:(H=f.data)==null?void 0:H.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=Y.current)==null?void 0:y.call(Y,K),j=Z??Nt(K);j!=null&&j.name&&n.playGesture(j.name,j.dur??1.8,j.mirror??!1,800)}let{words:b,wtimes:It,wdurations:Rt}=Qt(K);if(G<1){const Z=1/G;It=It.map(j=>j*Z),Rt=Rt.map(j=>j*Z)}n.streamAudio({audio:C,words:b,wtimes:It,wdurations:Rt})}else n.streamAudio({audio:C})},S.onerror=()=>{v==null||v(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()},z},[L,F]),Ut=e.useCallback(async(t,o={},h)=>{var W,k,x,O;const n=m.current;if(!(n!=null&&n.audioCtx))return;P.current=!1,tt.current=!1,F();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=o.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 v=await l.arrayBuffer();if(P.current)break;const z=xt(n.audioCtx,v);if(!z)throw new Error("Failed to prepare audio");const ht=z.duration*1e3,S=a.trim().split(/\s+/).filter(Boolean),f=S.reduce((y,b)=>y+b.length,0)||1;let C=0;const K=[],H=[];for(const y of S){K.push(C);const b=y.length/f*ht;H.push(b),C+=b}if(K.length===0&&(S.push(a),K.push(0),H.push(ht)),kt.current&&n.playGesture){const y=(k=Y.current)==null?void 0:k.call(Y,a),b=y??Nt(a);b!=null&&b.name&&n.playGesture(b.name,b.dur??1.8,b.mirror??!1,800)}for(n.speakAudio({audio:z,words:S,wtimes:K,wdurations:H},{lipsyncLang:o.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,F]);e.useCallback(async(t,o={},h)=>{const n=m.current;if(!(n!=null&&n.audioCtx))return;F();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=o.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((v,z)=>v+z.length,0)||1;let r=0;const a=[],l=[];for(const v of x){a.push(r);const z=v.length/O*k;l.push(z),r+=z}a.length===0&&(a.push(0),l.push(k),x.push(t)),F(),n.speakAudio({audio:W,words:x,wtimes:a,wdurations:l},{lipsyncLang:o.lipsyncLang||L},h),mt()},[L,mt,F]);const qt=e.useCallback((t,o={})=>{var n;if(!m.current)return;P.current=!1,tt.current=!1,F(),(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))};vt.current==="deepgram"?(Tt.current?Ut:jt)(t,o,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,o,h),mt())},[jt,Ut,mt,F]),Ft=e.useCallback(()=>{var o;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),(o=c.current)==null||o.call(c)},[]),Ht=e.useCallback(()=>{var o;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()),(o=c.current)==null||o.call(c)},[]),Jt=e.useCallback(async()=>{tt.current=!1},[]);return e.useImperativeHandle(oe,()=>({speakText:qt,pauseSpeaking:Ft,resumeSpeaking:Jt,stopSpeaking:Ht,isReady:Bt,get isSpeaking(){var t;return!!((t=m.current)!=null&&t.isSpeaking)},talkingHead:m.current}),[qt,Ft,Jt,Ht,Bt]),J.jsxs("div",{className:`narrator-avatar-container ${ne}`,style:{position:"relative",...se},children:[J.jsx("div",{ref:Et,className:"talking-head-viewer",style:{width:"100%",height:"100%",minHeight:"400px"}}),ue&&J.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&&J.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/**\n * All hand gestures supported by @met4citizen/talkinghead (complete built-in set).\n * No other gesture names are available in the library; custom gestures require gestureTemplates.\n */\nconst GESTURE_NAMES = ['handup', 'index', 'ok', 'thumbup', 'thumbdown', 'side', 'shrug', 'namaste'];\n\n/**\n * Picks a gesture that matches the phrase content. Uses all 8 built-in gestures.\n * Returns { name, dur, mirror } or null to skip gesture.\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 / please -> namaste\n if (/\\b(thank you|thanks|thank ya|welcome|bye|goodbye|see you|so long|namaste|hello|hi there|good morning|good afternoon|good evening)\\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?|raise your hand|tell me|question)\\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|bravo|good|nice work|love it|correct)\\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|avoid|bad)\\b/i.test(lower)) {\n return { name: 'thumbdown', dur: 1.6, mirror: mir() };\n }\n // Softer no / not / disagreement / wave -> 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|wave|hello,|hi,)\\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|sometimes|hard to say)\\b/i.test(lower)) {\n return { name: 'shrug', dur: 2, mirror: false };\n }\n // Listing / emphasis / steps / directing attention -> index\n if (/\\b(first|second|third|one |two |three |number|remember|important|key|point|listen|look|note|step|next|then|finally|so,|so\\.|watch|see here|over here|this way)\\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|yep|yes,|absolutely)\\b/i.test(lower)) {\n return { name: 'ok', dur: 1.6, mirror: mir() };\n }\n // Neutral: spread across all 8 gestures by phrase hash\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: false, // No mouse rotate by default (avoids accidental drag)\n cameraZoomEnable: false, // No mouse zoom by default\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 /** Allow user to rotate camera with mouse (default false). Set true to allow drag-to-rotate. */\n cameraRotateEnable = false,\n /** Allow user to zoom with mouse wheel (default false). Set true to allow zoom. */\n cameraZoomEnable = false,\n /** Allow user to pan camera (default false). */\n cameraPanEnable = false,\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 cameraRotateEnable,\n cameraZoomEnable,\n cameraPanEnable,\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","cameraRotateEnable","cameraZoomEnable","cameraPanEnable","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": "narrator-avatar",
|
|
3
|
+
"version": "1.0.3",
|
|
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
|
+
}
|