talking-head-studio 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +459 -0
- package/dist/TalkingHead.d.ts +35 -0
- package/dist/TalkingHead.d.ts.map +1 -0
- package/dist/TalkingHead.js +107 -0
- package/dist/TalkingHead.web.d.ts +35 -0
- package/dist/TalkingHead.web.d.ts.map +1 -0
- package/dist/TalkingHead.web.js +117 -0
- package/dist/__tests__/TalkingHead.test.d.ts +2 -0
- package/dist/__tests__/TalkingHead.test.d.ts.map +1 -0
- package/dist/__tests__/TalkingHead.test.js +23 -0
- package/dist/__tests__/sketchfab.test.d.ts +2 -0
- package/dist/__tests__/sketchfab.test.d.ts.map +1 -0
- package/dist/__tests__/sketchfab.test.js +21 -0
- package/dist/appearance/apply.d.ts +7 -0
- package/dist/appearance/apply.d.ts.map +1 -0
- package/dist/appearance/apply.js +56 -0
- package/dist/appearance/index.d.ts +5 -0
- package/dist/appearance/index.d.ts.map +1 -0
- package/dist/appearance/index.js +3 -0
- package/dist/appearance/matchers.d.ts +3 -0
- package/dist/appearance/matchers.d.ts.map +1 -0
- package/dist/appearance/matchers.js +32 -0
- package/dist/appearance/schema.d.ts +9 -0
- package/dist/appearance/schema.d.ts.map +1 -0
- package/dist/appearance/schema.js +20 -0
- package/dist/editor/AvatarCanvas.d.ts +16 -0
- package/dist/editor/AvatarCanvas.d.ts.map +1 -0
- package/dist/editor/AvatarCanvas.js +85 -0
- package/dist/editor/AvatarCanvasErrorBoundary.d.ts +17 -0
- package/dist/editor/AvatarCanvasErrorBoundary.d.ts.map +1 -0
- package/dist/editor/AvatarCanvasErrorBoundary.js +41 -0
- package/dist/editor/AvatarModel.d.ts +12 -0
- package/dist/editor/AvatarModel.d.ts.map +1 -0
- package/dist/editor/AvatarModel.js +31 -0
- package/dist/editor/RigidAccessory.d.ts +15 -0
- package/dist/editor/RigidAccessory.d.ts.map +1 -0
- package/dist/editor/RigidAccessory.js +76 -0
- package/dist/editor/SkinnedClothing.d.ts +7 -0
- package/dist/editor/SkinnedClothing.d.ts.map +1 -0
- package/dist/editor/SkinnedClothing.js +88 -0
- package/dist/editor/index.d.ts +6 -0
- package/dist/editor/index.d.ts.map +1 -0
- package/dist/editor/index.js +4 -0
- package/dist/editor/types.d.ts +28 -0
- package/dist/editor/types.d.ts.map +1 -0
- package/dist/editor/types.js +1 -0
- package/dist/html.d.ts +13 -0
- package/dist/html.d.ts.map +1 -0
- package/dist/html.js +560 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.web.d.ts +4 -0
- package/dist/index.web.d.ts.map +1 -0
- package/dist/index.web.js +2 -0
- package/dist/sketchfab/api.d.ts +12 -0
- package/dist/sketchfab/api.d.ts.map +1 -0
- package/dist/sketchfab/api.js +52 -0
- package/dist/sketchfab/categories.d.ts +5 -0
- package/dist/sketchfab/categories.d.ts.map +1 -0
- package/dist/sketchfab/categories.js +124 -0
- package/dist/sketchfab/index.d.ts +7 -0
- package/dist/sketchfab/index.d.ts.map +1 -0
- package/dist/sketchfab/index.js +3 -0
- package/dist/sketchfab/types.d.ts +51 -0
- package/dist/sketchfab/types.d.ts.map +1 -0
- package/dist/sketchfab/types.js +1 -0
- package/dist/sketchfab/useSketchfabSearch.d.ts +19 -0
- package/dist/sketchfab/useSketchfabSearch.d.ts.map +1 -0
- package/dist/sketchfab/useSketchfabSearch.js +78 -0
- package/dist/voice/convertToWav.d.ts +6 -0
- package/dist/voice/convertToWav.d.ts.map +1 -0
- package/dist/voice/convertToWav.js +74 -0
- package/dist/voice/index.d.ts +6 -0
- package/dist/voice/index.d.ts.map +1 -0
- package/dist/voice/index.js +3 -0
- package/dist/voice/useAudioPlayer.d.ts +11 -0
- package/dist/voice/useAudioPlayer.d.ts.map +1 -0
- package/dist/voice/useAudioPlayer.js +61 -0
- package/dist/voice/useAudioRecording.d.ts +14 -0
- package/dist/voice/useAudioRecording.d.ts.map +1 -0
- package/dist/voice/useAudioRecording.js +162 -0
- package/package.json +120 -0
- package/src/TalkingHead.tsx +207 -0
- package/src/TalkingHead.web.tsx +210 -0
- package/src/__tests__/TalkingHead.test.tsx +32 -0
- package/src/__tests__/sketchfab.test.ts +24 -0
- package/src/appearance/apply.ts +94 -0
- package/src/appearance/index.ts +4 -0
- package/src/appearance/matchers.ts +43 -0
- package/src/appearance/schema.ts +35 -0
- package/src/editor/AvatarCanvas.tsx +167 -0
- package/src/editor/AvatarCanvasErrorBoundary.tsx +64 -0
- package/src/editor/AvatarModel.tsx +49 -0
- package/src/editor/RigidAccessory.tsx +130 -0
- package/src/editor/SkinnedClothing.tsx +114 -0
- package/src/editor/index.ts +5 -0
- package/src/editor/r3f-shim.d.ts +34 -0
- package/src/editor/types.ts +30 -0
- package/src/html.ts +572 -0
- package/src/index.ts +8 -0
- package/src/index.web.ts +8 -0
- package/src/sketchfab/api.ts +82 -0
- package/src/sketchfab/categories.ts +127 -0
- package/src/sketchfab/index.ts +6 -0
- package/src/sketchfab/types.ts +40 -0
- package/src/sketchfab/useSketchfabSearch.ts +110 -0
- package/src/voice/convertToWav.ts +87 -0
- package/src/voice/index.ts +7 -0
- package/src/voice/useAudioPlayer.ts +78 -0
- package/src/voice/useAudioRecording.ts +207 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SiteBay
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
# talking-head-studio
|
|
2
|
+
|
|
3
|
+
**Drop a talking, lip-syncing 3D avatar into any React app -- native or web -- in under five minutes.**
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/talking-head-studio)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](https://github.com/sitebay/react-native-talking-head/actions)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Why this?
|
|
12
|
+
|
|
13
|
+
- **Truly cross-platform.** One component, two renderers. React Native gets a WebView; React on web gets an iframe with `srcdoc`. Same API, same props, same ref.
|
|
14
|
+
- **Bring any GLB.** Rigged models with ARKit/Oculus blend shapes get full phoneme-based lip-sync via HeadAudio. Non-rigged models still work -- they get a static viewer with amplitude-driven jaw animation as a fallback. No vendor lock-in to a single avatar format.
|
|
15
|
+
- **Built for LLM voice pipelines.** Wire `sendAmplitude` to LiveKit, Web Audio, ElevenLabs, or any audio source. The avatar speaks when your AI speaks.
|
|
16
|
+
- **Accessory system.** Attach hats, glasses, backpacks, or any GLB to any bone at runtime. Position, rotate, and scale each piece independently.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Table of Contents
|
|
21
|
+
|
|
22
|
+
- [Installation](#installation)
|
|
23
|
+
- [Quick Start](#quick-start)
|
|
24
|
+
- [Subpath Exports](#subpath-exports)
|
|
25
|
+
- [Props](#props)
|
|
26
|
+
- [Ref API](#ref-api)
|
|
27
|
+
- [Accessories](#accessories)
|
|
28
|
+
- [Color Customization](#color-customization)
|
|
29
|
+
- [Voice Pipeline Integration](#voice-pipeline-integration)
|
|
30
|
+
- [GLB Compatibility](#glb-compatibility)
|
|
31
|
+
- [Plain React / Next.js](#plain-react--nextjs)
|
|
32
|
+
- [MotionEngine (Upcoming)](#motionengine-upcoming)
|
|
33
|
+
- [Contributing](#contributing)
|
|
34
|
+
- [Credits](#credits)
|
|
35
|
+
- [License](#license)
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
### React Native / Expo
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm install talking-head-studio react-native-webview
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`react-native-webview` is a peer dependency. If you are using Expo, it is available as a built-in package.
|
|
48
|
+
|
|
49
|
+
### Web only (React, Next.js, Vite)
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npm install talking-head-studio
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
No WebView dependency needed. The package ships a `.web.tsx` entry point that renders via `<iframe srcdoc>` automatically when bundled for web targets.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Quick Start
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
import { useRef } from 'react';
|
|
63
|
+
import { TalkingHead, type TalkingHeadRef } from 'talking-head-studio';
|
|
64
|
+
|
|
65
|
+
export default function Avatar() {
|
|
66
|
+
const ref = useRef<TalkingHeadRef>(null);
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<TalkingHead
|
|
70
|
+
ref={ref}
|
|
71
|
+
avatarUrl="https://models.readyplayer.me/your-model.glb"
|
|
72
|
+
mood="happy"
|
|
73
|
+
cameraView="upper"
|
|
74
|
+
hairColor="#1a1a2e"
|
|
75
|
+
skinColor="#e0a370"
|
|
76
|
+
accessories={[
|
|
77
|
+
{
|
|
78
|
+
id: 'sunglasses',
|
|
79
|
+
url: 'https://example.com/sunglasses.glb',
|
|
80
|
+
bone: 'Head',
|
|
81
|
+
position: [0, 0.08, 0.12],
|
|
82
|
+
rotation: [0, 0, 0],
|
|
83
|
+
scale: 1.0,
|
|
84
|
+
},
|
|
85
|
+
]}
|
|
86
|
+
style={{ width: 400, height: 600 }}
|
|
87
|
+
onReady={() => console.log('Avatar loaded')}
|
|
88
|
+
onError={(msg) => console.error('Load failed:', msg)}
|
|
89
|
+
/>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Subpath Exports
|
|
97
|
+
|
|
98
|
+
The package ships five independent entry points. Import only what you need — each subpath has its own optional peer dependencies.
|
|
99
|
+
|
|
100
|
+
### `talking-head-studio` — Live talking avatar
|
|
101
|
+
```tsx
|
|
102
|
+
import { TalkingHead } from 'talking-head-studio';
|
|
103
|
+
// Peer deps: react, react-native (optional), react-native-webview (optional)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### `talking-head-studio/editor` — 3D editor with gizmo (web)
|
|
107
|
+
R3F-based canvas with PivotControls gizmo for placing accessories on an avatar. Web only.
|
|
108
|
+
```tsx
|
|
109
|
+
import { AvatarCanvas } from 'talking-head-studio/editor';
|
|
110
|
+
// Peer deps: @react-three/fiber, @react-three/drei, three
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### `talking-head-studio/appearance` — Material color system
|
|
114
|
+
Apply skin/hair/eye colors to any GLB avatar. Works in both the live view and the 3D editor.
|
|
115
|
+
```tsx
|
|
116
|
+
import { applyAppearanceToObject3D, type AvatarAppearance } from 'talking-head-studio/appearance';
|
|
117
|
+
// No extra peer deps
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### `talking-head-studio/voice` — Audio recording hooks
|
|
121
|
+
Headless hooks for recording voice samples (WebM→WAV conversion included). Backend-agnostic — send audio wherever you want (Qwen3-TTS, ElevenLabs, Groq, etc).
|
|
122
|
+
```tsx
|
|
123
|
+
import { useAudioRecording, useAudioPlayer } from 'talking-head-studio/voice';
|
|
124
|
+
// No extra peer deps (browser APIs only)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### `talking-head-studio/sketchfab` — Sketchfab search & download
|
|
128
|
+
Headless hooks and utilities for searching and downloading GLB models from Sketchfab. Bring your own UI and API key.
|
|
129
|
+
```tsx
|
|
130
|
+
import { useSketchfabSearch, ACCESSORY_CATEGORIES, downloadModel } from 'talking-head-studio/sketchfab';
|
|
131
|
+
// No extra peer deps
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Props
|
|
137
|
+
|
|
138
|
+
| Prop | Type | Default | Description |
|
|
139
|
+
|------|------|---------|-------------|
|
|
140
|
+
| `avatarUrl` | `string` | **required** | URL to any `.glb` model. Rigged or non-rigged. |
|
|
141
|
+
| `authToken` | `string \| null` | `null` | Bearer token sent when fetching the model URL. CDN URLs are excluded automatically. |
|
|
142
|
+
| `mood` | `TalkingHeadMood` | `'neutral'` | Avatar expression. See [Moods](#moods) below. |
|
|
143
|
+
| `cameraView` | `'head' \| 'upper' \| 'full'` | `'upper'` | Camera framing preset. |
|
|
144
|
+
| `cameraDistance` | `number` | `-0.5` | Camera zoom offset. Negative values zoom in. |
|
|
145
|
+
| `hairColor` | `string` | -- | CSS color applied to materials whose name contains `hair` or `fur`. |
|
|
146
|
+
| `skinColor` | `string` | -- | CSS color applied to materials whose name contains `skin`, `body`, or `face`. |
|
|
147
|
+
| `eyeColor` | `string` | -- | CSS color applied to materials whose name contains `eye` or `iris`. |
|
|
148
|
+
| `accessories` | `TalkingHeadAccessory[]` | `[]` | Array of GLB items to attach to bones. See [Accessories](#accessories). |
|
|
149
|
+
| `onReady` | `() => void` | -- | Fires once the avatar and scene are fully loaded. |
|
|
150
|
+
| `onError` | `(message: string) => void` | -- | Fires on load failure. |
|
|
151
|
+
| `style` | `ViewStyle` | -- | Container style (works on both native and web). |
|
|
152
|
+
|
|
153
|
+
### Moods
|
|
154
|
+
|
|
155
|
+
The `mood` prop accepts one of:
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
neutral | happy | sad | angry | excited | thinking | concerned | surprised
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Mood can be changed at any time via props or the ref API. On rigged models, mood maps to blend shape expressions. On non-rigged models, mood is a no-op.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Ref API
|
|
166
|
+
|
|
167
|
+
Access runtime controls through a React ref. Every method is safe to call at any time -- calls made before the avatar is ready are silently dropped.
|
|
168
|
+
|
|
169
|
+
```tsx
|
|
170
|
+
const ref = useRef<TalkingHeadRef>(null);
|
|
171
|
+
|
|
172
|
+
// Drive lip-sync from an audio amplitude value (0..1)
|
|
173
|
+
ref.current?.sendAmplitude(0.7);
|
|
174
|
+
|
|
175
|
+
// Change expression
|
|
176
|
+
ref.current?.setMood('excited');
|
|
177
|
+
|
|
178
|
+
// Change colors at runtime
|
|
179
|
+
ref.current?.setHairColor('#ff0000');
|
|
180
|
+
ref.current?.setSkinColor('#8d5524');
|
|
181
|
+
ref.current?.setEyeColor('#2e86de');
|
|
182
|
+
|
|
183
|
+
// Swap accessories without re-mounting the component
|
|
184
|
+
ref.current?.setAccessories([
|
|
185
|
+
{
|
|
186
|
+
id: 'crown',
|
|
187
|
+
url: 'https://example.com/crown.glb',
|
|
188
|
+
bone: 'Head',
|
|
189
|
+
position: [0, 0.22, 0],
|
|
190
|
+
rotation: [0, 0, 0],
|
|
191
|
+
scale: 0.8,
|
|
192
|
+
},
|
|
193
|
+
]);
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Ref Methods
|
|
197
|
+
|
|
198
|
+
| Method | Signature | Description |
|
|
199
|
+
|--------|-----------|-------------|
|
|
200
|
+
| `sendAmplitude` | `(amplitude: number) => void` | Feed audio amplitude (0 to 1) for jaw animation. |
|
|
201
|
+
| `setMood` | `(mood: TalkingHeadMood) => void` | Change avatar expression at runtime. |
|
|
202
|
+
| `setHairColor` | `(color: string) => void` | Update hair material color. |
|
|
203
|
+
| `setSkinColor` | `(color: string) => void` | Update skin material color. |
|
|
204
|
+
| `setEyeColor` | `(color: string) => void` | Update eye/iris material color. |
|
|
205
|
+
| `setAccessories` | `(accessories: TalkingHeadAccessory[]) => void` | Replace the entire accessory set. Handles loading, diffing, and cleanup automatically. |
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Accessories
|
|
210
|
+
|
|
211
|
+
Attach any GLB model to any bone on the avatar skeleton. The system handles loading, disposal, and transform updates.
|
|
212
|
+
|
|
213
|
+
### Accessory shape
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
interface TalkingHeadAccessory {
|
|
217
|
+
id: string; // Unique identifier for diffing
|
|
218
|
+
url: string; // URL to a .glb file
|
|
219
|
+
bone: string; // Target bone name (e.g. "Head", "RightHand", "Spine")
|
|
220
|
+
position: [number, number, number]; // Offset from the bone origin
|
|
221
|
+
rotation: [number, number, number]; // Euler rotation in radians
|
|
222
|
+
scale: number; // Uniform scale factor
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Example: hat + glasses + backpack
|
|
227
|
+
|
|
228
|
+
```tsx
|
|
229
|
+
<TalkingHead
|
|
230
|
+
avatarUrl="https://example.com/avatar.glb"
|
|
231
|
+
accessories={[
|
|
232
|
+
{
|
|
233
|
+
id: 'cowboy-hat',
|
|
234
|
+
url: '/models/cowboy-hat.glb',
|
|
235
|
+
bone: 'Head',
|
|
236
|
+
position: [0, 0.18, 0],
|
|
237
|
+
rotation: [0, 0, 0],
|
|
238
|
+
scale: 1.2,
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
id: 'aviators',
|
|
242
|
+
url: '/models/aviator-glasses.glb',
|
|
243
|
+
bone: 'Head',
|
|
244
|
+
position: [0, 0.06, 0.11],
|
|
245
|
+
rotation: [0, 0, 0],
|
|
246
|
+
scale: 1.0,
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
id: 'backpack',
|
|
250
|
+
url: '/models/backpack.glb',
|
|
251
|
+
bone: 'Spine1',
|
|
252
|
+
position: [0, 0, -0.15],
|
|
253
|
+
rotation: [0, Math.PI, 0],
|
|
254
|
+
scale: 0.9,
|
|
255
|
+
},
|
|
256
|
+
]}
|
|
257
|
+
/>
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Common bone names
|
|
261
|
+
|
|
262
|
+
Mixamo-rigged models typically expose these bones:
|
|
263
|
+
|
|
264
|
+
```
|
|
265
|
+
Head, Neck, Spine, Spine1, Spine2,
|
|
266
|
+
LeftShoulder, LeftArm, LeftForeArm, LeftHand,
|
|
267
|
+
RightShoulder, RightArm, RightForeArm, RightHand,
|
|
268
|
+
LeftUpLeg, LeftLeg, LeftFoot,
|
|
269
|
+
RightUpLeg, RightLeg, RightFoot
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Bone matching is flexible -- if an exact match is not found, the component tries a prefix match (useful for Sketchfab exports like `Head_5`). If no bone matches, the accessory falls back to the scene root.
|
|
273
|
+
|
|
274
|
+
### Runtime accessory swaps
|
|
275
|
+
|
|
276
|
+
```tsx
|
|
277
|
+
// Remove all accessories
|
|
278
|
+
ref.current?.setAccessories([]);
|
|
279
|
+
|
|
280
|
+
// Swap glasses for a monocle
|
|
281
|
+
ref.current?.setAccessories([
|
|
282
|
+
{ id: 'monocle', url: '/models/monocle.glb', bone: 'Head', position: [0.03, 0.07, 0.11], rotation: [0, 0, 0], scale: 0.6 },
|
|
283
|
+
]);
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Accessories that were previously loaded but are absent from the new array are automatically disposed (geometry, materials, textures).
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## Color Customization
|
|
291
|
+
|
|
292
|
+
Colors can be set via props (applied on initial load) or via the ref API (applied at runtime without reloading the model).
|
|
293
|
+
|
|
294
|
+
The system matches material names against known keywords:
|
|
295
|
+
|
|
296
|
+
| Target | Material name keywords |
|
|
297
|
+
|--------|----------------------|
|
|
298
|
+
| Hair | `hair`, `fur` |
|
|
299
|
+
| Skin | `skin`, `body`, `face` |
|
|
300
|
+
| Eyes | `eye`, `iris` |
|
|
301
|
+
|
|
302
|
+
```tsx
|
|
303
|
+
// Via props
|
|
304
|
+
<TalkingHead hairColor="#2d1b00" skinColor="#f0c8a0" eyeColor="#3d6b4f" />
|
|
305
|
+
|
|
306
|
+
// Via ref (runtime)
|
|
307
|
+
ref.current?.setHairColor('#ff4500');
|
|
308
|
+
ref.current?.setSkinColor('#c68642');
|
|
309
|
+
ref.current?.setEyeColor('#1abc9c');
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
This works on both rigged and non-rigged models -- any GLB with appropriately named materials will respond to color changes.
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## Voice Pipeline Integration
|
|
317
|
+
|
|
318
|
+
The component is designed to sit at the end of a voice pipeline. Feed it audio amplitude and it handles the rest.
|
|
319
|
+
|
|
320
|
+
### Primary: HeadAudio phoneme lip-sync
|
|
321
|
+
|
|
322
|
+
On rigged models in browser contexts with Web Audio available, [HeadAudio](https://github.com/met4citizen/HeadAudio) provides phoneme-level lip-sync automatically. Audio elements in the page are intercepted and routed through the lip-sync engine -- no wiring required on your end.
|
|
323
|
+
|
|
324
|
+
### Fallback: amplitude-driven jaw
|
|
325
|
+
|
|
326
|
+
When phoneme-level lip-sync is unavailable (React Native WebView, non-rigged models, or missing blend shapes), `sendAmplitude` drives jaw movement directly via morph targets.
|
|
327
|
+
|
|
328
|
+
### LiveKit integration
|
|
329
|
+
|
|
330
|
+
```tsx
|
|
331
|
+
import { useDataChannel } from '@livekit/components-react';
|
|
332
|
+
|
|
333
|
+
function AvatarWithLiveKit() {
|
|
334
|
+
const ref = useRef<TalkingHeadRef>(null);
|
|
335
|
+
|
|
336
|
+
useDataChannel('agent_speaking', (data) => {
|
|
337
|
+
if (data.amplitude !== undefined) {
|
|
338
|
+
ref.current?.sendAmplitude(data.amplitude);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
return <TalkingHead ref={ref} avatarUrl="..." />;
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### Web Audio analyser
|
|
347
|
+
|
|
348
|
+
```tsx
|
|
349
|
+
const audioCtx = new AudioContext();
|
|
350
|
+
const analyser = audioCtx.createAnalyser();
|
|
351
|
+
const buf = new Uint8Array(analyser.frequencyBinCount);
|
|
352
|
+
|
|
353
|
+
// Connect your audio source to the analyser
|
|
354
|
+
source.connect(analyser);
|
|
355
|
+
|
|
356
|
+
// Poll amplitude and feed the avatar
|
|
357
|
+
const interval = setInterval(() => {
|
|
358
|
+
analyser.getByteFrequencyData(buf);
|
|
359
|
+
const amplitude = buf.reduce((a, b) => a + b, 0) / buf.length / 255;
|
|
360
|
+
ref.current?.sendAmplitude(amplitude);
|
|
361
|
+
}, 50);
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### Any audio source
|
|
365
|
+
|
|
366
|
+
The only contract is a number between 0 and 1, called at roughly 20 Hz. This works with ElevenLabs, OpenAI Realtime, Deepgram, Whisper, or any other TTS/STT pipeline.
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
## GLB Compatibility
|
|
371
|
+
|
|
372
|
+
### Rigged models (full feature set)
|
|
373
|
+
|
|
374
|
+
For the complete experience -- phoneme lip-sync, expressions, moods, gestures -- your GLB should have:
|
|
375
|
+
|
|
376
|
+
- A **Mixamo-compatible armature** (the component expects standard bone names)
|
|
377
|
+
- **ARKit blend shapes** and/or **Oculus viseme blend shapes** for lip-sync
|
|
378
|
+
- Standard Three.js-compatible GLB format
|
|
379
|
+
|
|
380
|
+
Models from [Ready Player Me](https://readyplayer.me/), [Avaturn](https://avaturn.me/), or any Mixamo-rigged source work out of the box.
|
|
381
|
+
|
|
382
|
+
### Non-rigged models (static fallback)
|
|
383
|
+
|
|
384
|
+
Any valid GLB loads successfully. Non-rigged models get:
|
|
385
|
+
|
|
386
|
+
- Auto-framing and centering in the viewport
|
|
387
|
+
- Orbit controls for rotation
|
|
388
|
+
- Embedded animation playback (walk cycles, idle loops, etc.)
|
|
389
|
+
- Amplitude-driven jaw via morph targets (if the model has `jawOpen`, `mouthOpen`, or `viseme_aa` blend shapes)
|
|
390
|
+
- Color customization (if materials are named appropriately)
|
|
391
|
+
- Accessory attachment (falls back to scene root if no bones exist)
|
|
392
|
+
|
|
393
|
+
### Upstream documentation
|
|
394
|
+
|
|
395
|
+
For detailed model authoring guidance, see the [TalkingHead documentation](https://github.com/met4citizen/TalkingHead).
|
|
396
|
+
|
|
397
|
+
---
|
|
398
|
+
|
|
399
|
+
## Plain React / Next.js
|
|
400
|
+
|
|
401
|
+
Despite the package name, this works on the web without React Native installed. The `react-native` and `react-native-webview` peer dependencies are both marked optional.
|
|
402
|
+
|
|
403
|
+
On web, the component renders an `<iframe>` with `srcdoc` containing the full Three.js scene. No WebView, no native modules, no build plugins.
|
|
404
|
+
|
|
405
|
+
```tsx
|
|
406
|
+
// Works in any React 18+ web app
|
|
407
|
+
import { TalkingHead } from 'talking-head-studio';
|
|
408
|
+
|
|
409
|
+
export default function Page() {
|
|
410
|
+
return (
|
|
411
|
+
<TalkingHead
|
|
412
|
+
avatarUrl="/models/avatar.glb"
|
|
413
|
+
mood="happy"
|
|
414
|
+
style={{ width: 600, height: 800 }}
|
|
415
|
+
/>
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
Bundlers that support the `react-native` platform field (Metro, Expo) resolve `TalkingHead.tsx` (WebView). Standard web bundlers (webpack, Vite, esbuild) resolve `TalkingHead.web.tsx` (iframe). The API is identical.
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
## MotionEngine (Upcoming)
|
|
425
|
+
|
|
426
|
+
[MotionEngine](https://github.com/lhupyn/motion-engine) integration is in development. This will add real-time body tracking and gesture replay to the avatar, driven by webcam or motion capture data.
|
|
427
|
+
|
|
428
|
+
Stay tuned.
|
|
429
|
+
|
|
430
|
+
---
|
|
431
|
+
|
|
432
|
+
## Contributing
|
|
433
|
+
|
|
434
|
+
Contributions are welcome. Please open an issue to discuss your idea before submitting a pull request.
|
|
435
|
+
|
|
436
|
+
```bash
|
|
437
|
+
git clone https://github.com/sitebay/react-native-talking-head.git
|
|
438
|
+
cd react-native-talking-head
|
|
439
|
+
npm install
|
|
440
|
+
npm run typecheck
|
|
441
|
+
npm test
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
---
|
|
445
|
+
|
|
446
|
+
## Credits
|
|
447
|
+
|
|
448
|
+
This project builds on excellent open-source work:
|
|
449
|
+
|
|
450
|
+
- [met4citizen/TalkingHead](https://github.com/met4citizen/TalkingHead) -- The 3D avatar engine powering model loading, rigging, and expression systems.
|
|
451
|
+
- [met4citizen/HeadAudio](https://github.com/met4citizen/HeadAudio) -- Phoneme-based lip-sync from audio streams using AudioWorklet.
|
|
452
|
+
- [lhupyn/motion-engine](https://github.com/lhupyn/motion-engine) -- Real-time body motion tracking (upcoming integration).
|
|
453
|
+
- [Three.js](https://threejs.org/) -- 3D rendering, loaded via CDN at runtime.
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
## License
|
|
458
|
+
|
|
459
|
+
MIT
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
export type TalkingHeadMood = 'neutral' | 'happy' | 'sad' | 'angry' | 'excited' | 'thinking' | 'concerned' | 'surprised';
|
|
4
|
+
export interface TalkingHeadAccessory {
|
|
5
|
+
id: string;
|
|
6
|
+
url: string;
|
|
7
|
+
bone: string;
|
|
8
|
+
position: [number, number, number];
|
|
9
|
+
rotation: [number, number, number];
|
|
10
|
+
scale: number;
|
|
11
|
+
}
|
|
12
|
+
export interface TalkingHeadProps {
|
|
13
|
+
avatarUrl: string;
|
|
14
|
+
authToken?: string | null;
|
|
15
|
+
mood?: TalkingHeadMood;
|
|
16
|
+
cameraView?: 'head' | 'upper' | 'full';
|
|
17
|
+
cameraDistance?: number;
|
|
18
|
+
hairColor?: string;
|
|
19
|
+
skinColor?: string;
|
|
20
|
+
eyeColor?: string;
|
|
21
|
+
accessories?: TalkingHeadAccessory[];
|
|
22
|
+
onReady?: () => void;
|
|
23
|
+
onError?: (message: string) => void;
|
|
24
|
+
style?: StyleProp<ViewStyle>;
|
|
25
|
+
}
|
|
26
|
+
export interface TalkingHeadRef {
|
|
27
|
+
sendAmplitude: (amplitude: number) => void;
|
|
28
|
+
setMood: (mood: TalkingHeadMood) => void;
|
|
29
|
+
setHairColor: (color: string) => void;
|
|
30
|
+
setSkinColor: (color: string) => void;
|
|
31
|
+
setEyeColor: (color: string) => void;
|
|
32
|
+
setAccessories: (accessories: TalkingHeadAccessory[]) => void;
|
|
33
|
+
}
|
|
34
|
+
export declare const TalkingHead: React.ForwardRefExoticComponent<TalkingHeadProps & React.RefAttributes<TalkingHeadRef>>;
|
|
35
|
+
//# sourceMappingURL=TalkingHead.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TalkingHead.d.ts","sourceRoot":"","sources":["../src/TalkingHead.tsx"],"names":[],"mappings":"AAAA,OAAO,KAON,MAAM,OAAO,CAAC;AACf,OAAO,EAAE,KAAK,SAAS,EAAoB,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAIhF,MAAM,MAAM,eAAe,GACvB,SAAS,GACT,OAAO,GACP,KAAK,GACL,OAAO,GACP,SAAS,GACT,UAAU,GACV,WAAW,GACX,WAAW,CAAC;AAEhB,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,IAAI,CAAC,EAAE,eAAe,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;IACvC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,oBAAoB,EAAE,CAAC;IACrC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B;AAED,MAAM,WAAW,cAAc;IAC7B,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,OAAO,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,IAAI,CAAC;IACzC,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,cAAc,EAAE,CAAC,WAAW,EAAE,oBAAoB,EAAE,KAAK,IAAI,CAAC;CAC/D;AAED,eAAO,MAAM,WAAW,yFAyIvB,CAAC"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, } from 'react';
|
|
2
|
+
import { StyleSheet, View } from 'react-native';
|
|
3
|
+
import { WebView } from 'react-native-webview';
|
|
4
|
+
import { buildAvatarHtml } from './html';
|
|
5
|
+
export const TalkingHead = forwardRef(({ avatarUrl, authToken, mood = 'neutral', cameraView = 'upper', cameraDistance = -0.5, hairColor, skinColor, eyeColor, accessories, onReady, onError, style, }, ref) => {
|
|
6
|
+
const webViewRef = useRef(null);
|
|
7
|
+
const post = useCallback((msg) => {
|
|
8
|
+
webViewRef.current?.postMessage(JSON.stringify(msg));
|
|
9
|
+
}, []);
|
|
10
|
+
useImperativeHandle(ref, () => ({
|
|
11
|
+
sendAmplitude: (amplitude) => post({ type: 'amplitude', value: amplitude }),
|
|
12
|
+
setMood: (nextMood) => post({ type: 'mood', value: nextMood }),
|
|
13
|
+
setHairColor: (color) => post({ type: 'hair_color', value: color }),
|
|
14
|
+
setSkinColor: (color) => post({ type: 'skin_color', value: color }),
|
|
15
|
+
setEyeColor: (color) => post({ type: 'eye_color', value: color }),
|
|
16
|
+
setAccessories: (newAccessories) => post({ type: 'set_accessories', accessories: newAccessories }),
|
|
17
|
+
}), [post]);
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (readyRef.current)
|
|
20
|
+
post({ type: 'mood', value: mood });
|
|
21
|
+
}, [mood, post]);
|
|
22
|
+
// Track whether the WebView JS is ready to receive messages
|
|
23
|
+
const readyRef = useRef(false);
|
|
24
|
+
// Always hold the latest accessories so the ready handler can send them
|
|
25
|
+
const accessoriesRef = useRef(accessories);
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
accessoriesRef.current = accessories;
|
|
28
|
+
}, [accessories]);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
// Only post if the WebView is already ready; otherwise the ready handler sends them
|
|
31
|
+
if (accessories && readyRef.current) {
|
|
32
|
+
post({ type: 'set_accessories', accessories });
|
|
33
|
+
}
|
|
34
|
+
}, [accessories, post]);
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (hairColor && readyRef.current)
|
|
37
|
+
post({ type: 'hair_color', value: hairColor });
|
|
38
|
+
}, [hairColor, post]);
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (skinColor && readyRef.current)
|
|
41
|
+
post({ type: 'skin_color', value: skinColor });
|
|
42
|
+
}, [skinColor, post]);
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (eyeColor && readyRef.current)
|
|
45
|
+
post({ type: 'eye_color', value: eyeColor });
|
|
46
|
+
}, [eyeColor, post]);
|
|
47
|
+
// Color props are intentionally excluded from deps — live updates go via postMessage.
|
|
48
|
+
// Only avatarUrl, authToken, cameraView, cameraDistance cause a full WebView reload.
|
|
49
|
+
const [initialMood] = React.useState(mood);
|
|
50
|
+
const [initialHairColor] = React.useState(hairColor);
|
|
51
|
+
const [initialSkinColor] = React.useState(skinColor);
|
|
52
|
+
const [initialEyeColor] = React.useState(eyeColor);
|
|
53
|
+
const html = useMemo(() => buildAvatarHtml({
|
|
54
|
+
avatarUrl,
|
|
55
|
+
authToken,
|
|
56
|
+
mood: initialMood,
|
|
57
|
+
cameraView,
|
|
58
|
+
cameraDistance,
|
|
59
|
+
initialHairColor: initialHairColor,
|
|
60
|
+
initialSkinColor: initialSkinColor,
|
|
61
|
+
initialEyeColor: initialEyeColor,
|
|
62
|
+
}), [
|
|
63
|
+
avatarUrl,
|
|
64
|
+
authToken,
|
|
65
|
+
cameraView,
|
|
66
|
+
cameraDistance,
|
|
67
|
+
initialMood,
|
|
68
|
+
initialHairColor,
|
|
69
|
+
initialSkinColor,
|
|
70
|
+
initialEyeColor,
|
|
71
|
+
]);
|
|
72
|
+
const onMessage = useCallback((event) => {
|
|
73
|
+
try {
|
|
74
|
+
const msg = JSON.parse(event.nativeEvent.data);
|
|
75
|
+
if (msg.type === 'ready') {
|
|
76
|
+
readyRef.current = true;
|
|
77
|
+
// Flush any accessories that arrived before the WebView was ready
|
|
78
|
+
if (accessoriesRef.current?.length) {
|
|
79
|
+
post({ type: 'set_accessories', accessories: accessoriesRef.current });
|
|
80
|
+
}
|
|
81
|
+
onReady?.();
|
|
82
|
+
}
|
|
83
|
+
else if (msg.type === 'error')
|
|
84
|
+
onError?.(msg.message);
|
|
85
|
+
else if (msg.type === 'log')
|
|
86
|
+
console.log('[TalkingHead]', msg.message);
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
console.warn('[TalkingHead] Invalid message received from WebView:', err);
|
|
90
|
+
}
|
|
91
|
+
}, [onReady, onError, post]);
|
|
92
|
+
return (<View style={[styles.container, style]}>
|
|
93
|
+
<WebView ref={webViewRef} source={{ html }} style={styles.webview} javaScriptEnabled domStorageEnabled allowsInlineMediaPlayback mediaPlaybackRequiresUserAction={false} onMessage={onMessage} originWhitelist={['*']} mixedContentMode="always"/>
|
|
94
|
+
</View>);
|
|
95
|
+
});
|
|
96
|
+
TalkingHead.displayName = 'TalkingHead';
|
|
97
|
+
const styles = StyleSheet.create({
|
|
98
|
+
container: {
|
|
99
|
+
overflow: 'hidden',
|
|
100
|
+
borderRadius: 12,
|
|
101
|
+
backgroundColor: 'transparent',
|
|
102
|
+
},
|
|
103
|
+
webview: {
|
|
104
|
+
flex: 1,
|
|
105
|
+
backgroundColor: 'transparent',
|
|
106
|
+
},
|
|
107
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
export type TalkingHeadMood = 'neutral' | 'happy' | 'sad' | 'angry' | 'excited' | 'thinking' | 'concerned' | 'surprised';
|
|
4
|
+
export interface TalkingHeadAccessory {
|
|
5
|
+
id: string;
|
|
6
|
+
url: string;
|
|
7
|
+
bone: string;
|
|
8
|
+
position: [number, number, number];
|
|
9
|
+
rotation: [number, number, number];
|
|
10
|
+
scale: number;
|
|
11
|
+
}
|
|
12
|
+
export interface TalkingHeadProps {
|
|
13
|
+
avatarUrl: string;
|
|
14
|
+
authToken?: string | null;
|
|
15
|
+
mood?: TalkingHeadMood;
|
|
16
|
+
cameraView?: 'head' | 'upper' | 'full';
|
|
17
|
+
cameraDistance?: number;
|
|
18
|
+
hairColor?: string;
|
|
19
|
+
skinColor?: string;
|
|
20
|
+
eyeColor?: string;
|
|
21
|
+
accessories?: TalkingHeadAccessory[];
|
|
22
|
+
onReady?: () => void;
|
|
23
|
+
onError?: (message: string) => void;
|
|
24
|
+
style?: StyleProp<ViewStyle>;
|
|
25
|
+
}
|
|
26
|
+
export interface TalkingHeadRef {
|
|
27
|
+
sendAmplitude: (amplitude: number) => void;
|
|
28
|
+
setMood: (mood: TalkingHeadMood) => void;
|
|
29
|
+
setHairColor: (color: string) => void;
|
|
30
|
+
setSkinColor: (color: string) => void;
|
|
31
|
+
setEyeColor: (color: string) => void;
|
|
32
|
+
setAccessories: (accessories: TalkingHeadAccessory[]) => void;
|
|
33
|
+
}
|
|
34
|
+
export declare const TalkingHead: React.ForwardRefExoticComponent<TalkingHeadProps & React.RefAttributes<TalkingHeadRef>>;
|
|
35
|
+
//# sourceMappingURL=TalkingHead.web.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TalkingHead.web.d.ts","sourceRoot":"","sources":["../src/TalkingHead.web.tsx"],"names":[],"mappings":"AAAA,OAAO,KAON,MAAM,OAAO,CAAC;AACf,OAAO,EAAE,KAAK,SAAS,EAAoB,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAGhF,MAAM,MAAM,eAAe,GACvB,SAAS,GACT,OAAO,GACP,KAAK,GACL,OAAO,GACP,SAAS,GACT,UAAU,GACV,WAAW,GACX,WAAW,CAAC;AAEhB,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,IAAI,CAAC,EAAE,eAAe,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;IACvC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,oBAAoB,EAAE,CAAC;IACrC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B;AAED,MAAM,WAAW,cAAc;IAC7B,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,OAAO,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,IAAI,CAAC;IACzC,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,cAAc,EAAE,CAAC,WAAW,EAAE,oBAAoB,EAAE,KAAK,IAAI,CAAC;CAC/D;AAED,eAAO,MAAM,WAAW,yFAiJvB,CAAC"}
|