talking-head-studio 0.4.10 → 0.4.12

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.
Files changed (178) hide show
  1. package/README.md +299 -337
  2. package/dist/TalkingHead.d.ts +44 -28
  3. package/dist/TalkingHead.js +21 -2
  4. package/dist/TalkingHead.web.d.ts +37 -4
  5. package/dist/TalkingHead.web.js +28 -8
  6. package/dist/TalkingHeadVisualization.d.ts +22 -0
  7. package/dist/TalkingHeadVisualization.js +30 -10
  8. package/dist/api/studioApi.d.ts +12 -1
  9. package/dist/api/studioApi.js +41 -28
  10. package/dist/appearance/apply.js +2 -3
  11. package/dist/appearance/matchers.js +1 -2
  12. package/dist/appearance/schema.js +1 -2
  13. package/dist/contract.d.ts +14 -0
  14. package/dist/contract.js +30 -0
  15. package/dist/core/avatar/avatarCapabilities.d.ts +60 -0
  16. package/dist/core/avatar/avatarCapabilities.js +100 -0
  17. package/dist/core/avatar/backend.d.ts +130 -0
  18. package/dist/core/avatar/backend.js +4 -0
  19. package/dist/core/avatar/backends/gaussian.d.ts +49 -0
  20. package/dist/core/avatar/backends/gaussian.js +293 -0
  21. package/dist/core/avatar/backends/index.d.ts +3 -0
  22. package/dist/core/avatar/backends/index.js +7 -0
  23. package/dist/core/avatar/backends/morphTarget.d.ts +39 -0
  24. package/dist/core/avatar/backends/morphTarget.js +179 -0
  25. package/dist/core/avatar/faceControls.d.ts +40 -0
  26. package/dist/core/avatar/faceControls.js +138 -0
  27. package/dist/core/avatar/motion.d.ts +1713 -0
  28. package/dist/core/avatar/motion.js +550 -0
  29. package/dist/core/avatar/motionRuntime.d.ts +46 -0
  30. package/dist/core/avatar/motionRuntime.js +84 -0
  31. package/dist/core/avatar/schema.d.ts +78 -0
  32. package/dist/core/avatar/schema.js +134 -0
  33. package/dist/core/avatar/visemes.d.ts +47 -1
  34. package/dist/core/avatar/visemes.js +114 -1
  35. package/dist/editor/AvatarCanvas.js +93 -3
  36. package/dist/editor/AvatarEditor.native.js +19 -9
  37. package/dist/editor/AvatarModel.js +2 -2
  38. package/dist/editor/FaceSqueezeEditor.d.ts +3 -1
  39. package/dist/editor/FaceSqueezeEditor.js +195 -121
  40. package/dist/editor/FaceSqueezeEditor.web.d.ts +3 -1
  41. package/dist/editor/FaceSqueezeEditor.web.js +32 -30
  42. package/dist/editor/RigidAccessory.js +18 -4
  43. package/dist/editor/SkinnedClothing.js +19 -9
  44. package/dist/editor/boneLockedDrag.d.ts +11 -0
  45. package/dist/editor/boneLockedDrag.js +68 -0
  46. package/dist/editor/boneSnap.js +22 -12
  47. package/dist/editor/boneSnap.web.d.ts +27 -0
  48. package/dist/editor/boneSnap.web.js +99 -0
  49. package/dist/editor/index.web.d.ts +10 -0
  50. package/dist/editor/index.web.js +26 -0
  51. package/dist/editor/sounds/haha.wav +0 -0
  52. package/dist/editor/sounds/owie.wav +0 -0
  53. package/dist/editor/sounds/stop.wav +0 -0
  54. package/dist/editor/studioTheme.d.ts +14 -14
  55. package/dist/editor/studioTheme.js +19 -16
  56. package/dist/editor/types.d.ts +1 -0
  57. package/dist/html/accessories.d.ts +7 -0
  58. package/dist/html/accessories.js +149 -0
  59. package/dist/html/motion.d.ts +1 -0
  60. package/dist/html/motion.js +189 -0
  61. package/dist/html/visemes.d.ts +7 -0
  62. package/dist/html/visemes.js +348 -0
  63. package/dist/html.d.ts +1 -1
  64. package/dist/html.js +56 -734
  65. package/dist/index.d.ts +19 -1
  66. package/dist/index.js +44 -5
  67. package/dist/index.web.d.ts +18 -1
  68. package/dist/index.web.js +36 -3
  69. package/dist/platform/api/types.d.ts +10 -0
  70. package/dist/platform/api/types.js +2 -0
  71. package/dist/platform/marketplace/types.d.ts +32 -0
  72. package/dist/platform/marketplace/types.js +2 -0
  73. package/dist/platform/sdk/unity.d.ts +27 -0
  74. package/dist/platform/sdk/unity.js +2 -0
  75. package/dist/platform/sdk/unreal.d.ts +23 -0
  76. package/dist/platform/sdk/unreal.js +2 -0
  77. package/dist/platform/sdk/web.d.ts +16 -0
  78. package/dist/platform/sdk/web.js +2 -0
  79. package/dist/sketchfab/api.js +5 -5
  80. package/dist/sketchfab/glbInspect.d.ts +22 -0
  81. package/dist/sketchfab/glbInspect.js +58 -0
  82. package/dist/sketchfab/index.d.ts +3 -0
  83. package/dist/sketchfab/index.js +8 -1
  84. package/dist/sketchfab/inspectRemote.d.ts +13 -0
  85. package/dist/sketchfab/inspectRemote.js +77 -0
  86. package/dist/sketchfab/types.d.ts +10 -0
  87. package/dist/sketchfab/useSketchfabSearch.js +1 -2
  88. package/dist/studio/AccessoryBrowserScreen.d.ts +6 -0
  89. package/dist/studio/AccessoryBrowserScreen.js +626 -0
  90. package/dist/studio/AccessoryPanel.d.ts +10 -0
  91. package/dist/studio/AccessoryPanel.js +396 -0
  92. package/dist/studio/AppearancePanel.d.ts +9 -0
  93. package/dist/studio/AppearancePanel.js +77 -0
  94. package/dist/studio/AvatarCreatorScreen.d.ts +5 -0
  95. package/dist/studio/AvatarCreatorScreen.js +806 -0
  96. package/dist/studio/AvatarEditorScreen.d.ts +14 -0
  97. package/dist/studio/AvatarEditorScreen.js +510 -0
  98. package/dist/studio/AvatarGrid.d.ts +23 -0
  99. package/dist/studio/AvatarGrid.js +257 -0
  100. package/dist/studio/ColorSwatch.d.ts +8 -0
  101. package/dist/studio/ColorSwatch.js +100 -0
  102. package/dist/studio/CreateVoiceProfileSheet.d.ts +8 -0
  103. package/dist/studio/CreateVoiceProfileSheet.js +242 -0
  104. package/dist/studio/DetailsPanel.d.ts +15 -0
  105. package/dist/studio/DetailsPanel.js +239 -0
  106. package/dist/studio/FilamentEditor.d.ts +2 -0
  107. package/dist/studio/FilamentEditor.js +6 -0
  108. package/dist/studio/PrecisionPanel.d.ts +2 -0
  109. package/dist/studio/PrecisionPanel.js +7 -0
  110. package/dist/studio/PublicGalleryScreen.d.ts +5 -0
  111. package/dist/studio/PublicGalleryScreen.js +358 -0
  112. package/dist/studio/SketchfabModelCard.d.ts +20 -0
  113. package/dist/studio/SketchfabModelCard.js +104 -0
  114. package/dist/studio/StudioBrowseHeader.d.ts +9 -0
  115. package/dist/studio/StudioBrowseHeader.js +28 -0
  116. package/dist/studio/StudioEmptyState.d.ts +8 -0
  117. package/dist/studio/StudioEmptyState.js +29 -0
  118. package/dist/studio/StudioFloatingAction.d.ts +13 -0
  119. package/dist/studio/StudioFloatingAction.js +42 -0
  120. package/dist/studio/StudioSectionHeader.d.ts +7 -0
  121. package/dist/studio/StudioSectionHeader.js +27 -0
  122. package/dist/studio/StudioSurfaceCard.d.ts +8 -0
  123. package/dist/studio/StudioSurfaceCard.js +20 -0
  124. package/dist/studio/VoicePanel.d.ts +15 -0
  125. package/dist/studio/VoicePanel.js +305 -0
  126. package/dist/studio/constants.d.ts +3 -0
  127. package/dist/studio/constants.js +6 -0
  128. package/dist/studio/index.d.ts +29 -0
  129. package/dist/studio/index.js +54 -0
  130. package/dist/studio/useSketchfabCapabilities.d.ts +31 -0
  131. package/dist/studio/useSketchfabCapabilities.js +82 -0
  132. package/dist/tts/useDirectVisemeStream.d.ts +2 -6
  133. package/dist/tts/useDirectVisemeStream.js +16 -12
  134. package/dist/tts/useMotionMarkers.d.ts +0 -1
  135. package/dist/tts/useMotionMarkers.js +1 -2
  136. package/dist/utils/avatarUtils.js +94 -8
  137. package/dist/utils/faceLandmarkerToShapeWeights.js +21 -14
  138. package/dist/voice/convertToWav.js +1 -2
  139. package/dist/voice/index.d.ts +3 -0
  140. package/dist/voice/index.js +6 -1
  141. package/dist/voice/useAudioPlayer.js +18 -6
  142. package/dist/voice/useAudioRecording.js +1 -2
  143. package/dist/voice/useFaceControls.d.ts +14 -0
  144. package/dist/voice/useFaceControls.js +81 -0
  145. package/dist/voice/useVoicePreview.d.ts +7 -0
  146. package/dist/voice/useVoicePreview.js +83 -0
  147. package/dist/wardrobe/index.d.ts +3 -0
  148. package/dist/wardrobe/index.js +8 -1
  149. package/dist/wardrobe/useAccessoryGestures.d.ts +20 -0
  150. package/dist/wardrobe/useAccessoryGestures.js +94 -0
  151. package/dist/wardrobe/useAvatarWardrobeHydration.js +9 -4
  152. package/dist/wardrobe/useStudioAvatar.d.ts +29 -0
  153. package/dist/wardrobe/useStudioAvatar.js +186 -0
  154. package/dist/wardrobe/wardrobeStore.d.ts +2 -0
  155. package/dist/wardrobe/wardrobeStore.js +12 -2
  156. package/dist/wgpu/R3FWebGpuCanvas.d.ts +15 -0
  157. package/dist/wgpu/R3FWebGpuCanvas.js +176 -0
  158. package/dist/wgpu/WgpuAvatar.d.ts +26 -2
  159. package/dist/wgpu/WgpuAvatar.js +313 -46
  160. package/dist/wgpu/accessoryDefaults.d.ts +12 -0
  161. package/dist/wgpu/accessoryDefaults.js +19 -0
  162. package/dist/wgpu/blobShim.d.ts +2 -0
  163. package/dist/wgpu/blobShim.js +191 -0
  164. package/dist/wgpu/index.d.ts +1 -0
  165. package/dist/wgpu/index.js +4 -1
  166. package/dist/wgpu/loadGLTFFromUri.d.ts +2 -0
  167. package/dist/wgpu/loadGLTFFromUri.js +75 -0
  168. package/dist/wgpu/morphTables.js +21 -10
  169. package/dist/wgpu/motionState.d.ts +20 -0
  170. package/dist/wgpu/motionState.js +31 -0
  171. package/dist/wgpu/patchThreeForRN.d.ts +28 -0
  172. package/dist/wgpu/patchThreeForRN.js +292 -0
  173. package/dist/wgpu/scenePlacement.d.ts +5 -0
  174. package/dist/wgpu/scenePlacement.js +50 -0
  175. package/dist/wgpu/useAuthedModelUri.js +22 -11
  176. package/dist/wgpu/useNativeGLTF.d.ts +7 -0
  177. package/dist/wgpu/useNativeGLTF.js +36 -0
  178. package/package.json +102 -32
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # talking-head-studio
2
2
 
3
- **The missing UI layer for AI Agents. Drop-in, lip-syncing 3D avatars for Web, React, and React Native.**
3
+ **Make any GLB model talk on the web and on React Native — with phoneme-accurate, audio-aligned lip-sync. With or without blend shapes.**
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/talking-head-studio.svg)](https://www.npmjs.com/package/talking-head-studio)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
@@ -8,56 +8,122 @@
8
8
 
9
9
  ---
10
10
 
11
- ## Why this?
11
+ ## The point: lip-sync that's driven by the audio, not guessed
12
12
 
13
- - **Zero-Jank React Native & Web:** True cross-platform rendering. React Native gets a blazing fast wgpu-accelerated native render loop, skipping WebView bridge latency entirely. React on web gets a robust `react-three-fiber` setup. Same API, same props.
14
- - **Universal GLB Compatibility:** Bring any GLB. Out-of-the-box support for standard ARKit blendshapes. Rigged models get full phoneme-based lip-sync. Non-rigged models get an amplitude-driven jaw animation fallback.
15
- - **Built for AI & Voice Pipelines:** Wire `sendAmplitude` or visemes directly to LiveKit, Web Audio, ElevenLabs, OpenAI Realtime, or any audio source.
16
- - **Always Alive:** Procedural idle animations (breathing, nodding, swaying) keep your avatar from feeling like a static doll.
17
- - **Dynamic Wardrobe & Accessories:** Swap hair, skin, and eye colors on the fly. Attach hats, glasses, or backpacks to any bone at runtime.
13
+ Most avatar libraries flap a jaw open in proportion to audio loudness. That reads as
14
+ "mouth moving," not "speaking." talking-head-studio is built around a different model: a
15
+ **viseme schedule** a timed list of mouth shapes derived from the actual synthesized
16
+ speech drives morph targets on the model.
18
17
 
19
- ---
18
+ ```
19
+ TTS server ──▶ AgentVisemePayload ──▶ scheduleVisemes() ──▶ morph drive
20
+ (word-aligned { cues: [{ viseme, startMs, (this library, (Three.js
21
+ phonemes) endMs }], durationMs } web + native) morph targets)
22
+ ```
23
+
24
+ The wire format is `AgentVisemePayload`: per-phoneme cues using the 9-shape Rhubarb
25
+ vocabulary (`A`–`H`, `X`), each with a start/end time in milliseconds. The library maps
26
+ those onto Oculus viseme morphs and schedules them against the audio clock, so the mouth
27
+ hits each shape *when that sound is actually heard*.
28
+
29
+ This pairs directly with a TTS server that emits viseme timings from real word alignment
30
+ (we built [Qwen3-TTS](https://github.com/sitebay/Qwen3-TTS) for exactly this — it serves
31
+ `AgentVisemePayload` over an SSE endpoint). But the format is open: emit cues from any
32
+ source and the renderer consumes them identically.
33
+
34
+ ### Four lip-sync tiers — every model works
35
+
36
+ The model decides the fidelity; you don't have to pre-process anything.
37
+
38
+ | Your model has… | Method | Quality |
39
+ |---|---|---|
40
+ | Oculus viseme morphs | Direct morph drive (`MorphTargetBackend`) | Excellent |
41
+ | ARKit blend shapes (52 AUs) | `remapArkitToOculus()` → morph drive | Good |
42
+ | Only `jawOpen` / `mouthOpen` | Amplitude fallback | Acceptable |
43
+ | No face rig at all | Gaussian splat backend *(roadmap — not yet built)* | Excellent |
20
44
 
21
- ## Table of Contents
22
-
23
- - [Installation](#installation)
24
- - [Quick Start](#quick-start)
25
- - [Subpath Exports](#subpath-exports)
26
- - [Props](#props)
27
- - [Ref API](#ref-api)
28
- - [Accessories](#accessories)
29
- - [Color Customization](#color-customization)
30
- - [Voice Pipeline Integration](#voice-pipeline-integration)
31
- - [GLB Compatibility](#glb-compatibility)
32
- - [Plain React / Next.js](#plain-react--nextjs)
33
- - [MotionEngine (Upcoming)](#motionengine-upcoming)
34
- - [Contributing](#contributing)
35
- - [Credits](#credits)
36
- - [License](#license)
45
+ If a model has no viseme morphs, scheduled cues still fall back to the jaw/amplitude path
46
+ automatically — you never get a frozen face.
37
47
 
38
48
  ---
39
49
 
40
- ## Installation
50
+ ## Two renderers, one contract
41
51
 
42
- ### React Native / Expo
52
+ The same `AgentVisemePayload` / `FaceControl` contract drives both render paths, so you
53
+ write your voice pipeline once:
54
+
55
+ - **Web** — an isolated `<iframe>` running [met4citizen TalkingHead](https://github.com/met4citizen/TalkingHead)
56
+ as the rig (`TalkingHead.web.tsx`). Drop it into any React / Next / Vite app.
57
+ - **React Native** — a native WebGPU renderer (`WgpuAvatar`, via `react-native-wgpu` +
58
+ react-three-fiber). No WebView, no postMessage latency, morphs driven on the GPU.
59
+
60
+ Capabilities differ slightly between the two — see the [capability matrix](#runtime-capability-matrix).
61
+
62
+ ---
63
+
64
+ ## Install
43
65
 
44
66
  ```bash
67
+ # React Native / Expo WebView path
45
68
  npm install talking-head-studio react-native-webview
69
+
70
+ # React Native / Expo native WebGPU path
71
+ npx expo install react-native-wgpu @react-three/fiber three three-stdlib expo-asset
72
+
73
+ # Web (React, Next.js, Vite)
74
+ npm install talking-head-studio
46
75
  ```
47
76
 
48
- `react-native-webview` is a peer dependency. If you are using Expo, it is available as a built-in package.
77
+ `three`, `@react-three/fiber`, and the platform packages are peer dependencies bring your
78
+ own versions. `react-native-webview` is only required for the WebView renderer. Native
79
+ WebGPU uses `react-native-wgpu` and must run in a native build, not Expo Go.
49
80
 
50
- ### Web only (React, Next.js, Vite)
81
+ ### React Native / Expo WebGPU setup
82
+
83
+ Native WebGPU needs the React Native new architecture and the WebGPU build of Three.js.
84
+ The example app in `example/` has the full working config; these are the important parts:
85
+
86
+ ```jsonc
87
+ // app.json
88
+ {
89
+ "expo": {
90
+ "newArchEnabled": true,
91
+ "plugins": ["expo-asset"]
92
+ }
93
+ }
94
+ ```
95
+
96
+ ```js
97
+ // metro.config.js
98
+ const path = require('path');
99
+ const { getDefaultConfig } = require('expo/metro-config');
100
+
101
+ const config = getDefaultConfig(__dirname);
102
+ const nodeModules = path.resolve(__dirname, 'node_modules');
103
+ const threeWebgpu = path.resolve(nodeModules, 'three/build/three.webgpu.js');
104
+
105
+ config.resolver.assetExts.push('glb');
106
+ config.resolver.extraNodeModules = {
107
+ three: threeWebgpu,
108
+ };
109
+
110
+ module.exports = config;
111
+ ```
112
+
113
+ Build and launch a native app so `WebGPUModule` is linked:
51
114
 
52
115
  ```bash
53
- npm install talking-head-studio
116
+ npx expo prebuild --platform android --no-install
117
+ npx expo run:android
54
118
  ```
55
119
 
56
- No `react-native` or WebView runtime dependency needed. The package ships a web entry point that renders via `<iframe srcdoc>` automatically when bundled for browser targets.
120
+ Expo Go cannot load the native WebGPU module.
57
121
 
58
122
  ---
59
123
 
60
- ## Quick Start
124
+ ## Quick start
125
+
126
+ ### Web / React Native component
61
127
 
62
128
  ```tsx
63
129
  import { useRef } from 'react';
@@ -72,397 +138,293 @@ export default function Avatar() {
72
138
  avatarUrl="https://example.com/your-model.glb"
73
139
  mood="happy"
74
140
  cameraView="upper"
75
- hairColor="#1a1a2e"
76
- skinColor="#e0a370"
77
- accessories={[
78
- {
79
- id: 'sunglasses',
80
- url: 'https://example.com/sunglasses.glb',
81
- bone: 'Head',
82
- position: [0, 0.08, 0.12],
83
- rotation: [0, 0, 0],
84
- scale: 1.0,
85
- },
86
- ]}
87
141
  style={{ width: 400, height: 600 }}
88
- onReady={() => console.log('Avatar loaded')}
89
- onError={(msg) => console.error('Load failed:', msg)}
142
+ onReady={() => {
143
+ // Drive the mouth from a viseme schedule (e.g. from your TTS server)
144
+ ref.current?.scheduleVisemes({
145
+ cues: [
146
+ { viseme: 'A', startMs: 0, endMs: 90 },
147
+ { viseme: 'E', startMs: 90, endMs: 170 },
148
+ { viseme: 'X', startMs: 170, endMs: 220 },
149
+ ],
150
+ durationMs: 220,
151
+ audioStartedAtMs: Date.now(),
152
+ });
153
+ }}
90
154
  />
91
155
  );
92
156
  }
93
157
  ```
94
158
 
95
- ---
96
-
97
- ## Subpath Exports
98
-
99
- The package ships five independent entry points. Import only what you need — each subpath has its own optional peer dependencies.
100
-
101
- ### `talking-head-studio` — Live talking avatar
102
- ```tsx
103
- import { TalkingHead } from 'talking-head-studio';
104
- // Peer deps: react
105
- // Native-only peers: react-native (optional), react-native-webview (optional)
106
- ```
107
-
108
- ### `talking-head-studio/editor` — 3D editor with gizmo (web)
109
- R3F-based canvas with PivotControls gizmo for placing accessories on an avatar. Web only.
110
- ```tsx
111
- import { AvatarCanvas } from 'talking-head-studio/editor';
112
- // Peer deps: @react-three/fiber, @react-three/drei, three
113
- ```
159
+ ### Native WebGPU (React Native, no WebView)
114
160
 
115
- ### `talking-head-studio/appearance` — Material color system
116
- Apply skin/hair/eye colors to any GLB avatar. Works in both the live view and the 3D editor.
117
161
  ```tsx
118
- import { applyAppearanceToObject3D, type AvatarAppearance } from 'talking-head-studio/appearance';
119
- // No extra peer deps
120
- ```
162
+ import { WgpuAvatar, type WgpuAvatarRef } from 'talking-head-studio/wgpu';
121
163
 
122
- ### `talking-head-studio/voice` Audio recording hooks
123
- Headless hooks for recording voice samples (WebM→WAV conversion included). Backend-agnostic — send audio wherever you want (Qwen3-TTS, ElevenLabs, Groq, etc).
124
- ```tsx
125
- import { useAudioRecording, useAudioPlayer } from 'talking-head-studio/voice';
126
- // No extra peer deps (browser APIs only)
127
- ```
164
+ const ref = useRef<WgpuAvatarRef>(null);
128
165
 
129
- ### `talking-head-studio/sketchfab` — Sketchfab search & download
130
- Headless hooks and utilities for searching and downloading GLB models from Sketchfab. Bring your own UI and API key.
131
- ```tsx
132
- import { useSketchfabSearch, ACCESSORY_CATEGORIES, downloadModel } from 'talking-head-studio/sketchfab';
133
- // No extra peer deps
166
+ <WgpuAvatar
167
+ ref={ref}
168
+ avatarUrl="https://example.com/your-model.glb"
169
+ mood="neutral"
170
+ style={{ flex: 1 }}
171
+ />;
172
+ // ref.current?.scheduleVisemes(payload) — same contract as the web component
134
173
  ```
135
174
 
136
175
  ---
137
176
 
138
- ## Props
177
+ ## TalkingHead component — props & ref
178
+
179
+ ### Props
139
180
 
140
181
  | Prop | Type | Default | Description |
141
182
  |------|------|---------|-------------|
142
- | `avatarUrl` | `string` | **required** | URL to any `.glb` model. Rigged or non-rigged. |
143
- | `authToken` | `string \| null` | `null` | Bearer token sent when fetching the model URL. CDN URLs are excluded automatically. |
144
- | `mood` | `TalkingHeadMood` | `'neutral'` | Avatar expression. See [Moods](#moods) below. |
145
- | `cameraView` | `'head' \| 'upper' \| 'full'` | `'upper'` | Camera framing preset. |
146
- | `cameraDistance` | `number` | `-0.5` | Camera zoom offset. Negative values zoom in. |
147
- | `hairColor` | `string` | -- | CSS color applied to materials whose name contains `hair` or `fur`. |
148
- | `skinColor` | `string` | -- | CSS color applied to materials whose name contains `skin`, `body`, or `face`. |
149
- | `eyeColor` | `string` | -- | CSS color applied to materials whose name contains `eye` or `iris`. |
150
- | `accessories` | `TalkingHeadAccessory[]` | `[]` | Array of GLB items to attach to bones. See [Accessories](#accessories). |
151
- | `onReady` | `() => void` | -- | Fires once the avatar and scene are fully loaded. |
152
- | `onError` | `(message: string) => void` | -- | Fires on load failure. |
153
- | `style` | `ViewStyle` | -- | Container style (works on both native and web). |
154
-
155
- ### Moods
156
-
157
- The `mood` prop accepts one of:
158
-
159
- ```
160
- neutral | happy | sad | angry | excited | thinking | concerned | surprised
161
- ```
162
-
163
- 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.
164
-
165
- ---
183
+ | `avatarUrl` | `string` | required | Any `.glb`. Rigged or not. |
184
+ | `authToken` | `string \| null` | `null` | Bearer token for authenticated GLB URLs. |
185
+ | `mood` | `TalkingHeadMood` | `'neutral'` | `neutral \| happy \| sad \| angry \| fear \| disgust \| love \| sleep \| excited \| thinking \| concerned \| surprised` |
186
+ | `cameraView` | `'head' \| 'upper' \| 'full'` | `'upper'` | Framing preset. |
187
+ | `cameraDistance` | `number` | `-0.5` | Zoom offset. Negative = closer. |
188
+ | `hairColor` | `string` | | Hex color. Applied to materials named `hair`, `fur`. |
189
+ | `skinColor` | `string` | | Applied to `skin`, `body`, `face`. |
190
+ | `eyeColor` | `string` | | Applied to `eye`, `iris`. |
191
+ | `accessories` | `TalkingHeadAccessory[]` | `[]` | Bone-attached GLB items. |
192
+ | `onReady` | `() => void` | | Fired when fully loaded. |
193
+ | `onError` | `(msg: string) => void` | | Fired on load failure. |
194
+ | `style` | `ViewStyle / CSSProperties` | | Container style. |
195
+
196
+ ### Ref methods
166
197
 
167
- ## Ref API
168
-
169
- 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.
170
-
171
- ```tsx
172
- const ref = useRef<TalkingHeadRef>(null);
173
-
174
- // Drive lip-sync from an audio amplitude value (0..1)
175
- ref.current?.sendAmplitude(0.7);
198
+ ```ts
199
+ // Lip-sync
200
+ ref.current?.scheduleVisemes(payload); // AgentVisemePayload full timed lip-sync schedule
201
+ ref.current?.clearVisemes();
202
+ ref.current?.sendAmplitude(0.7); // amplitude 0..1 → jaw (fallback / no schedule)
176
203
 
177
- // Change expression
204
+ // Expression & appearance
178
205
  ref.current?.setMood('excited');
179
-
180
- // Change colors at runtime
181
206
  ref.current?.setHairColor('#ff0000');
182
207
  ref.current?.setSkinColor('#8d5524');
183
208
  ref.current?.setEyeColor('#2e86de');
184
-
185
- // Swap accessories without re-mounting the component
186
- ref.current?.setAccessories([
187
- {
188
- id: 'crown',
189
- url: 'https://example.com/crown.glb',
190
- bone: 'Head',
191
- position: [0, 0.22, 0],
192
- rotation: [0, 0, 0],
193
- scale: 0.8,
194
- },
195
- ]);
209
+ ref.current?.setAccessories([...]);
210
+
211
+ // Body — procedural motions, gestures, poses, animation clips
212
+ ref.current?.dispatchMotion('groove'); // looping procedural motion
213
+ ref.current?.stopMotion();
214
+ ref.current?.playGesture('thumbup'); // upstream hand gesture
215
+ ref.current?.playPose('oneknee'); // upstream pose template
216
+ ref.current?.playAnimation('/animations/wave.glb', { dur: 2 });
217
+ ref.current?.lookAt(120, 80, 500); // turn toward viewport coords
196
218
  ```
197
219
 
198
- ### Ref Methods
199
-
200
- | Method | Signature | Description |
201
- |--------|-----------|-------------|
202
- | `sendAmplitude` | `(amplitude: number) => void` | Feed audio amplitude (0 to 1) for jaw animation. |
203
- | `setMood` | `(mood: TalkingHeadMood) => void` | Change avatar expression at runtime. |
204
- | `setHairColor` | `(color: string) => void` | Update hair material color. |
205
- | `setSkinColor` | `(color: string) => void` | Update skin material color. |
206
- | `setEyeColor` | `(color: string) => void` | Update eye/iris material color. |
207
- | `setAccessories` | `(accessories: TalkingHeadAccessory[]) => void` | Replace the entire accessory set. Handles loading, diffing, and cleanup automatically. |
220
+ The motion vocabulary (`groove`, `wave`, `nod`, `idle`, `attack`, `defend`, `celebrate`,
221
+ plus every upstream gesture/pose name) is exported as typed constants —
222
+ `MOTION_KEYS`, `TALKINGHEAD_GESTURES`, `TALKINGHEAD_POSES`, and the `isMotionKey()` guard —
223
+ from both the package root and `talking-head-studio/contract`.
224
+
225
+ ### Runtime capability matrix
226
+
227
+ Both renderers share one API; where native can't match the WebView's upstream rig, it
228
+ falls back to a procedural approximation rather than failing. This table is the honest gap
229
+ list.
230
+
231
+ | Feature | Web (iframe) | Native (WGPU) | Notes |
232
+ |---|:---:|:---:|---|
233
+ | Viseme schedules (`scheduleVisemes`) | ✅ | ✅ | Both consume `AgentVisemePayload`. |
234
+ | Amplitude jaw fallback (`sendAmplitude`) | ✅ | ⚠️ | Web drives jaw from amplitude; native exposes the method for API parity. |
235
+ | Core procedural motions (`groove`, `attack`, `defend`) | ✅ | ✅ | Shared `MOTION_DEFS` source of truth. |
236
+ | Gesture names (`thumbup`, `shrug`, …) | ✅ | ⚠️ | Web delegates to TalkingHead; native uses procedural approximations. |
237
+ | Pose names (`oneknee`, `kneel`, `sitting`, …) | ✅ | ⚠️ | Web delegates to TalkingHead; native uses static procedural poses. |
238
+ | Full mood vocabulary | ✅ | ✅ | All 8 upstream moods + friendly aliases. |
239
+ | External animation clips (`playAnimation`) | ✅ | ⚠️ | Web delegates to TalkingHead; native plays GLB clips via `AnimationMixer`. |
240
+ | Gaze (`lookAt`) | ✅ | ❌ | Native eye/head-gaze bridge is future work. |
241
+ | Listening / mic-reactive mouth | ⚠️ | ❌ | Web can route host-provided audio; native bridge not implemented. |
208
242
 
209
243
  ---
210
244
 
211
- ## Accessories
212
-
213
- Attach any GLB model to any bone on the avatar skeleton. The system handles loading, disposal, and transform updates.
245
+ ## Self-hosting the runtime assets
214
246
 
215
- ### Accessory shape
247
+ By default the web iframe pulls the TalkingHead rig, three.js, and the HeadAudio model
248
+ from public CDNs (jsDelivr, gstatic). To run fully self-hosted — no external CDN — vendor
249
+ those files and point the renderer at your own origin:
216
250
 
217
251
  ```ts
218
- interface TalkingHeadAccessory {
219
- id: string; // Unique identifier for diffing
220
- url: string; // URL to a .glb file
221
- bone: string; // Target bone name (e.g. "Head", "RightHand", "Spine")
222
- position: [number, number, number]; // Offset from the bone origin
223
- rotation: [number, number, number]; // Euler rotation in radians
224
- scale: number; // Uniform scale factor
225
- }
226
- ```
252
+ import { buildAvatarHtml } from 'talking-head-studio/html';
227
253
 
228
- ### Example: hat + glasses + backpack
229
-
230
- ```tsx
231
- <TalkingHead
232
- avatarUrl="https://example.com/avatar.glb"
233
- accessories={[
234
- {
235
- id: 'cowboy-hat',
236
- url: '/models/cowboy-hat.glb',
237
- bone: 'Head',
238
- position: [0, 0.18, 0],
239
- rotation: [0, 0, 0],
240
- scale: 1.2,
241
- },
242
- {
243
- id: 'aviators',
244
- url: '/models/aviator-glasses.glb',
245
- bone: 'Head',
246
- position: [0, 0.06, 0.11],
247
- rotation: [0, 0, 0],
248
- scale: 1.0,
249
- },
250
- {
251
- id: 'backpack',
252
- url: '/models/backpack.glb',
253
- bone: 'Spine1',
254
- position: [0, 0, -0.15],
255
- rotation: [0, Math.PI, 0],
256
- scale: 0.9,
257
- },
258
- ]}
259
- />
254
+ const html = buildAvatarHtml({
255
+ avatarUrl: 'https://your-cdn/model.glb',
256
+ vendorBaseUrl: 'https://your-cdn/vendor', // serves three.module.js, talkinghead.mjs, etc.
257
+ // ...
258
+ });
260
259
  ```
261
260
 
262
- ### Common bone names
261
+ `vendorBaseUrl` replaces every CDN reference; `dracoDecoderUrl` overrides the DRACO decoder
262
+ location independently.
263
263
 
264
- Mixamo-rigged models typically expose these bones:
265
-
266
- ```
267
- Head, Neck, Spine, Spine1, Spine2,
268
- LeftShoulder, LeftArm, LeftForeArm, LeftHand,
269
- RightShoulder, RightArm, RightForeArm, RightHand,
270
- LeftUpLeg, LeftLeg, LeftFoot,
271
- RightUpLeg, RightLeg, RightFoot
272
- ```
273
-
274
- 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.
264
+ ---
275
265
 
276
- ### Runtime accessory swaps
266
+ ## FaceControl the lower-level contract
277
267
 
278
- ```tsx
279
- // Remove all accessories
280
- ref.current?.setAccessories([]);
268
+ If you're writing a custom backend or a game-engine integration, `FaceControl` is the
269
+ single value that flows between a voice pipeline and any avatar backend.
281
270
 
282
- // Swap glasses for a monocle
283
- ref.current?.setAccessories([
284
- { id: 'monocle', url: '/models/monocle.glb', bone: 'Head', position: [0.03, 0.07, 0.11], rotation: [0, 0, 0], scale: 0.6 },
285
- ]);
271
+ ```ts
272
+ import type { FaceControl, ExpressionState, HeadPose, EyeGaze } from 'talking-head-studio';
273
+
274
+ type HeadPose = { yaw: number; pitch: number; roll: number }; // each -1..1
275
+ type EyeGaze = { x: number; y: number }; // each -1..1
276
+
277
+ type ExpressionState = {
278
+ jawOpen: number; mouthSmile: number; mouthFunnel: number; mouthPucker: number;
279
+ mouthWide: number; upperLipRaise: number; lowerLipDepress: number; cheekRaise: number;
280
+ blinkLeft: number; blinkRight: number; browInnerUp: number;
281
+ browDownLeft: number; browDownRight: number;
282
+ eyeGazeLeft: EyeGaze; eyeGazeRight: EyeGaze;
283
+ }; // all weights 0..1 unless noted
286
284
  ```
287
285
 
288
- Accessories that were previously loaded but are absent from the new array are automatically disposed (geometry, materials, textures).
289
-
290
- ---
291
-
292
- ## Color Customization
286
+ Drive it from a viseme schedule:
293
287
 
294
- Colors can be set via props (applied on initial load) or via the ref API (applied at runtime without reloading the model).
295
-
296
- The system matches material names against known keywords:
288
+ ```ts
289
+ import { useFaceControlsFromVisemes } from 'talking-head-studio';
297
290
 
298
- | Target | Material name keywords |
299
- |--------|----------------------|
300
- | Hair | `hair`, `fur` |
301
- | Skin | `skin`, `body`, `face` |
302
- | Eyes | `eye`, `iris` |
291
+ const faceControl = useFaceControlsFromVisemes(schedule); // rAF-sampled FaceControl
292
+ ```
303
293
 
304
- ```tsx
305
- // Via props
306
- <TalkingHead hairColor="#2d1b00" skinColor="#f0c8a0" eyeColor="#3d6b4f" />
294
+ Or implement a backend against it:
307
295
 
308
- // Via ref (runtime)
309
- ref.current?.setHairColor('#ff4500');
310
- ref.current?.setSkinColor('#c68642');
311
- ref.current?.setEyeColor('#1abc9c');
296
+ ```ts
297
+ import type { AvatarBackend, AvatarRenderTarget, FaceControl } from 'talking-head-studio';
298
+
299
+ class MyBackend implements AvatarBackend {
300
+ initialize() {}
301
+ attach(target: AvatarRenderTarget) {}
302
+ setControl(control: FaceControl) {}
303
+ renderFrame() {}
304
+ dispose() {}
305
+ }
312
306
  ```
313
307
 
314
- This works on both rigged and non-rigged models -- any GLB with appropriately named materials will respond to color changes.
308
+ ### MorphTargetBackend the built-in Three.js adapter
315
309
 
316
- ---
317
-
318
- ## Voice Pipeline Integration
310
+ The concrete `AvatarBackend` for GLB-with-morphs. Hand it a loaded scene; it discovers
311
+ morph targets, builds a lookup cache, and drives them from `FaceControl`.
319
312
 
320
- The component is designed to sit at the end of a voice pipeline. Feed it audio amplitude and it handles the rest.
313
+ ```ts
314
+ import { MorphTargetBackend, createNeutralExpression } from 'talking-head-studio';
315
+
316
+ const backend = new MorphTargetBackend(gltf.scene, {
317
+ mood: 'neutral',
318
+ expressionScale: 1.0,
319
+ calibration: {
320
+ neutral: { pose: { yaw: 0, pitch: 0, roll: 0 }, expr: createNeutralExpression() },
321
+ ranges: { jawOpen: { min: 0, max: 0.85 } }, // clamp jaw for this model
322
+ gazeLimits: { x: { min: -0.6, max: 0.6 } },
323
+ },
324
+ });
321
325
 
322
- ### Primary: HeadAudio phoneme lip-sync
326
+ backend.setControl(faceControl);
327
+ backend.renderFrame();
328
+ console.log(backend.availableChannels); // what this model actually supports
329
+ ```
323
330
 
324
- 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.
331
+ ### ARKit Oculus remap (no ML, no artist work)
325
332
 
326
- ### Fallback: amplitude-driven jaw
333
+ ```ts
334
+ import { remapArkitToOculus, getArkitWeightsForViseme } from 'talking-head-studio';
327
335
 
328
- 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.
336
+ remapArkitToOculus({ jawOpen: 0.7, mouthLowerDownLeft: 0.4 }); // { aa: 0.68, oh: 0.12, ... }
337
+ getArkitWeightsForViseme('ou'); // → { mouthPucker: 0.9, ... }
338
+ ```
329
339
 
330
- ### LiveKit integration
340
+ The full `ARKIT_TO_OCULUS` coefficient table is exported for building your own bake pipeline.
331
341
 
332
- ```tsx
333
- import { useDataChannel } from '@livekit/components-react';
342
+ ---
334
343
 
335
- function AvatarWithLiveKit() {
336
- const ref = useRef<TalkingHeadRef>(null);
344
+ ## Accessories
337
345
 
338
- useDataChannel('agent_speaking', (data) => {
339
- if (data.amplitude !== undefined) {
340
- ref.current?.sendAmplitude(data.amplitude);
341
- }
342
- });
346
+ Any GLB attached to any skeleton bone, placeable at runtime.
343
347
 
344
- return <TalkingHead ref={ref} avatarUrl="..." />;
348
+ ```ts
349
+ interface TalkingHeadAccessory {
350
+ id: string;
351
+ url: string;
352
+ bone: string; // 'Head' | 'Spine' | 'RightHand' | ...
353
+ position: [number, number, number];
354
+ rotation: [number, number, number]; // Euler, radians
355
+ scale: number;
345
356
  }
346
357
  ```
347
358
 
348
- ### Web Audio analyser
349
-
350
- ```tsx
351
- const audioCtx = new AudioContext();
352
- const analyser = audioCtx.createAnalyser();
353
- const buf = new Uint8Array(analyser.frequencyBinCount);
354
-
355
- // Connect your audio source to the analyser
356
- source.connect(analyser);
357
-
358
- // Poll amplitude and feed the avatar
359
- const interval = setInterval(() => {
360
- analyser.getByteFrequencyData(buf);
361
- const amplitude = buf.reduce((a, b) => a + b, 0) / buf.length / 255;
362
- ref.current?.sendAmplitude(amplitude);
363
- }, 50);
364
- ```
365
-
366
- ### Any audio source
367
-
368
- 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.
359
+ Common Mixamo bones: `Head, Neck, Spine, Spine1, Spine2, LeftHand, RightHand, LeftFoot, RightFoot, Hips`.
360
+ The 3D editor (`talking-head-studio/editor`, web only) provides a gizmo for live placement.
369
361
 
370
362
  ---
371
363
 
372
- ## GLB Compatibility
373
-
374
- ### Rigged models (full feature set)
375
-
376
- For the complete experience -- phoneme lip-sync, expressions, moods, gestures -- your GLB should have:
377
-
378
- - A **Mixamo-compatible armature** (the component expects standard bone names)
379
- - **ARKit blend shapes** and/or **Oculus viseme blend shapes** for lip-sync
380
- - Standard Three.js-compatible GLB format
381
-
382
- Models from [Avaturn](https://avaturn.me/) or any Mixamo-rigged source work out of the box.
383
-
384
- ### Non-rigged models (static fallback)
385
-
386
- Any valid GLB loads successfully. Non-rigged models get:
364
+ ## Subpath exports
387
365
 
388
- - Auto-framing and centering in the viewport
389
- - Orbit controls for rotation
390
- - Embedded animation playback (walk cycles, idle loops, etc.)
391
- - Amplitude-driven jaw via morph targets (if the model has `jawOpen`, `mouthOpen`, or `viseme_aa` blend shapes)
392
- - Color customization (if materials are named appropriately)
393
- - Accessory attachment (falls back to scene root if no bones exist)
366
+ | Import | Description |
367
+ |------|-------------|
368
+ | `talking-head-studio` | Avatar component + `FaceControl` contracts + motion constants |
369
+ | `talking-head-studio/contract` | Stable type-only entrypoint visemes, FaceControl, backends, motion |
370
+ | `talking-head-studio/html` | `buildAvatarHtml()` for self-hosted / custom iframe embedding |
371
+ | `talking-head-studio/wgpu` | React Native WebGPU renderer (`WgpuAvatar`) |
372
+ | `talking-head-studio/editor` | R3F 3D editor with placement gizmo (web only) |
373
+ | `talking-head-studio/appearance` | Material color system for any GLB |
374
+ | `talking-head-studio/voice` | Audio recording + WAV conversion hooks |
375
+ | `talking-head-studio/sketchfab` | Sketchfab search + download hooks |
376
+ | `talking-head-studio/api` | Studio API client (avatar CRUD, voice profiles) |
377
+ | `talking-head-studio/wardrobe` | Accessory + outfit state management |
394
378
 
395
- ### Upstream documentation
396
-
397
- For detailed model authoring guidance, see the [TalkingHead documentation](https://github.com/met4citizen/TalkingHead).
379
+ Workspace packages (`packages/avatar-creator`, `packages/agent-avatar`) ship an embeddable
380
+ creator widget and a LiveKit + MCP agent integration.
398
381
 
399
382
  ---
400
383
 
401
- ## Plain React / Next.js
402
-
403
- This works on the web without `react-native` or `react-native-webview` installed at runtime.
404
-
405
- On web, the component renders an `<iframe>` with `srcdoc` containing the full Three.js scene. No WebView, no native modules, no build plugins.
406
-
407
- ```tsx
408
- // Works in any React 18+ web app
409
- import { TalkingHead } from 'talking-head-studio';
410
-
411
- export default function Page() {
412
- return (
413
- <TalkingHead
414
- avatarUrl="/models/avatar.glb"
415
- mood="happy"
416
- style={{ width: 600, height: 800 }}
417
- />
418
- );
419
- }
420
- ```
421
-
422
- Metro and Expo use the native entry backed by `react-native-webview`. Standard web bundlers use the browser entry backed by a plain `<iframe>`. The API is identical.
384
+ ## Roadmap
423
385
 
424
- ---
386
+ > **Status legend:** ✅ shipped · 🔜 in progress · 🧪 designed, not yet built
425
387
 
426
- ## MotionEngine (Upcoming)
388
+ **Shipped today**
389
+ - ✅ `FaceControl` face-control space (pose + expression + gaze) and `AvatarBackend` interface
390
+ - ✅ `MorphTargetBackend` — GLB morph discovery + mood layering
391
+ - ✅ ARKit → Oculus analytical remap with full coefficient table
392
+ - ✅ `AgentVisemePayload` viseme schedule format + `scheduleVisemes` on both renderers
393
+ - ✅ Shared procedural motion engine (web + native WGPU), gestures, poses, animation clips
394
+ - ✅ Self-hosting via `buildAvatarHtml({ vendorBaseUrl })`
395
+ - ✅ `packages/avatar-creator`, `packages/agent-avatar`
427
396
 
428
- [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.
397
+ **In progress**
398
+ - 🔜 Native (WGPU) gaze bridge (`lookAt`) and mic-reactive listening
399
+ - 🔜 GLB schema walker — report morph coverage, bones, LODs, viseme tier for any model
429
400
 
430
- Stay tuned.
401
+ **Designed, not yet built**
402
+ - 🧪 `GaussianBackend` — Gaussian-splat renderer + FLAME per-viseme delta transfer, so a
403
+ model with *no* face rig still gets excellent lip-sync. This is the zero-prerequisite path.
404
+ - 🧪 FLAME viseme transfer pipeline (companion backend) — bake Oculus visemes into a GLB
405
+ that lacks them
406
+ - 🧪 Unity / Unreal SDKs implementing the same `AvatarBackend` contract
407
+ - 🧪 Avatar marketplace + RPM import tooling (`CatalogItem` / `AvatarAsset` types exist;
408
+ backend and store do not)
431
409
 
432
410
  ---
433
411
 
434
412
  ## Contributing
435
413
 
436
- Contributions are welcome. Please open an issue to discuss your idea before submitting a pull request.
437
-
438
414
  ```bash
439
415
  git clone https://github.com/sitebay/talking-head-studio.git
440
416
  cd talking-head-studio
441
417
  npm install
442
- npm run typecheck
418
+ npm run typecheck # must be clean
443
419
  npm test
444
420
  ```
445
421
 
446
- ---
447
-
448
- ## Credits
449
-
450
- This project builds on excellent open-source work:
451
-
452
- - [met4citizen/TalkingHead](https://github.com/met4citizen/TalkingHead) -- The 3D avatar engine powering model loading, rigging, and expression systems.
453
- - [met4citizen/HeadAudio](https://github.com/met4citizen/HeadAudio) -- Phoneme-based lip-sync from audio streams using AudioWorklet.
454
- - [lhupyn/motion-engine](https://github.com/lhupyn/motion-engine) -- Real-time body motion tracking (upcoming integration).
455
- - [Three.js](https://threejs.org/) -- 3D rendering, loaded via CDN at runtime.
456
-
457
- ---
458
-
459
- ## License
460
-
461
- MIT
462
- at runtime.
422
+ Monorepo with `packages/*` as npm workspaces; the main library is the root package. The
423
+ publish gate (`prepublishOnly`) runs lint, typecheck, tests, and metadata checks.
463
424
 
464
425
  ---
465
426
 
466
- ## License
427
+ ## Credits & license
467
428
 
468
- MIT
429
+ Built on [met4citizen/TalkingHead](https://github.com/met4citizen/TalkingHead) (rig +
430
+ gestures/poses on the web path) and [Three.js](https://threejs.org). MIT licensed.