talking-head-studio 0.3.9 → 0.4.1

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.
@@ -28,7 +28,7 @@ export interface TalkingHeadVisualizationRef {
28
28
  /**
29
29
  * TalkingHeadVisualization — optimized component for rendering the 3D avatar.
30
30
  *
31
- * On native: uses FilamentAvatar (direct morph writes, no WebView bridge).
31
+ * On native: uses WgpuAvatar (direct morph writes, no WebView bridge).
32
32
  * On web: uses TalkingHead WebView renderer.
33
33
  */
34
34
  export declare const TalkingHeadVisualization: React.ForwardRefExoticComponent<TalkingHeadVisualizationProps & React.RefAttributes<TalkingHeadVisualizationRef>>;
@@ -6,9 +6,9 @@ const jsx_runtime_1 = require("react/jsx-runtime");
6
6
  const react_1 = require("react");
7
7
  const react_native_1 = require("react-native");
8
8
  const TalkingHead_1 = require("./TalkingHead");
9
- const FilamentAvatar_1 = require("./filament/FilamentAvatar");
9
+ const WgpuAvatar_1 = require("./wgpu/WgpuAvatar");
10
10
  const avatarUtils_1 = require("./utils/avatarUtils");
11
- const faceSqueezeAssets_1 = require("./filament/faceSqueezeAssets");
11
+ const faceSqueezeAssets_1 = require("./wgpu/faceSqueezeAssets");
12
12
  // Cached fallback data URI — resolved once, reused across all instances
13
13
  let _fallbackDataUri = null;
14
14
  let _fallbackPromise = null;
@@ -41,16 +41,16 @@ function getLoadingLabel(stage) {
41
41
  /**
42
42
  * TalkingHeadVisualization — optimized component for rendering the 3D avatar.
43
43
  *
44
- * On native: uses FilamentAvatar (direct morph writes, no WebView bridge).
44
+ * On native: uses WgpuAvatar (direct morph writes, no WebView bridge).
45
45
  * On web: uses TalkingHead WebView renderer.
46
46
  */
47
47
  exports.TalkingHeadVisualization = (0, react_1.forwardRef)(({ style, avatarUrl, authToken, cameraView = 'head', cameraDistance = 0.2, accessories, mood: initialMood = 'neutral', aspect, focalLength, visemeSchedule, onVisemeScheduleApplied, vendorBaseUrl }, ref) => {
48
48
  const avatarRef = (0, react_1.useRef)(null);
49
49
  // On native, Filament ref is wired via callback ref — store it here so
50
50
  // scheduleVisemes / sendAmplitude can route to it.
51
- const filamentRef = (0, react_1.useRef)(null);
51
+ const wgpuRef = (0, react_1.useRef)(null);
52
52
  // Unified accessor — Filament on native, WebView on web
53
- const activeAvatar = (0, react_1.useCallback)(() => (filamentRef.current ?? avatarRef.current), []);
53
+ const activeAvatar = (0, react_1.useCallback)(() => (wgpuRef.current ?? avatarRef.current), []);
54
54
  // Fallback local GLB data URI — resolved once on mount
55
55
  const [fallbackUrl, setFallbackUrl] = (0, react_1.useState)(_fallbackDataUri);
56
56
  (0, react_1.useEffect)(() => {
@@ -116,7 +116,7 @@ exports.TalkingHeadVisualization = (0, react_1.forwardRef)(({ style, avatarUrl,
116
116
  const av = activeAvatar();
117
117
  // Filament buffers pending schedules internally — no ready gate needed.
118
118
  // WebView (avatarRef) still needs the ready gate.
119
- if (!av || (!filamentRef.current && !isAvatarReady)) {
119
+ if (!av || (!wgpuRef.current && !isAvatarReady)) {
120
120
  pendingVisemeScheduleRef.current = schedule;
121
121
  return;
122
122
  }
@@ -166,7 +166,7 @@ exports.TalkingHeadVisualization = (0, react_1.forwardRef)(({ style, avatarUrl,
166
166
  ready: isAvatarReady,
167
167
  });
168
168
  const av = activeAvatar();
169
- if (!av || (!filamentRef.current && !isAvatarReady)) {
169
+ if (!av || (!wgpuRef.current && !isAvatarReady)) {
170
170
  pendingVisemeScheduleRef.current = visemeSchedule;
171
171
  return;
172
172
  }
@@ -183,8 +183,8 @@ exports.TalkingHeadVisualization = (0, react_1.forwardRef)(({ style, avatarUrl,
183
183
  }, [isAvatarReady, onVisemeScheduleApplied, visemeSchedule, activeAvatar]);
184
184
  // On native use Filament — direct morph writes, no WebView bridge.
185
185
  if (react_native_1.Platform.OS !== 'web') {
186
- return ((0, jsx_runtime_1.jsx)(FilamentAvatar_1.FilamentAvatar, { focalLength: focalLength, ref: (fr) => {
187
- filamentRef.current = fr;
186
+ return ((0, jsx_runtime_1.jsx)(WgpuAvatar_1.WgpuAvatar, { focalLength: focalLength, ref: (fr) => {
187
+ wgpuRef.current = fr;
188
188
  if (typeof ref === 'function')
189
189
  ref(fr);
190
190
  else if (ref)
@@ -0,0 +1,36 @@
1
+ /**
2
+ * WgpuAvatar — drop-in replacement for FilamentAvatar using react-three-fiber
3
+ * + react-native-wgpu (or expo-gl fallback). Exposes the same ref interface as
4
+ * FilamentAvatarRef so siteclaw can swap renderers without changing call sites.
5
+ *
6
+ * No SurfaceTexture, no choreographer, no JNI surface lifecycle bugs.
7
+ *
8
+ * Peer deps required by the host app:
9
+ * react-native-wgpu (or expo-gl for the expo-gl fallback canvas)
10
+ * @react-three/fiber >= 8
11
+ * @react-three/drei >= 9
12
+ * three >= 0.170
13
+ */
14
+ import React from 'react';
15
+ import { type StyleProp, type ViewStyle } from 'react-native';
16
+ import type { TalkingHeadMood, TalkingHeadAccessory, TalkingHeadVisemeSchedule } from '../index';
17
+ export interface WgpuAvatarRef {
18
+ setMood: (mood: TalkingHeadMood) => void;
19
+ sendAmplitude: (amplitude: number) => void;
20
+ sendViseme: (viseme: string, weight?: number) => void;
21
+ scheduleVisemes: (schedule: TalkingHeadVisemeSchedule) => void;
22
+ clearVisemes: () => void;
23
+ }
24
+ interface WgpuAvatarProps {
25
+ style?: StyleProp<ViewStyle>;
26
+ avatarUrl: string | null;
27
+ authToken?: string;
28
+ aspect?: number;
29
+ focalLength?: number;
30
+ mood?: TalkingHeadMood;
31
+ accessories?: TalkingHeadAccessory[];
32
+ onReady?: () => void;
33
+ onError?: (message: string) => void;
34
+ }
35
+ export declare const WgpuAvatar: React.ForwardRefExoticComponent<WgpuAvatarProps & React.RefAttributes<WgpuAvatarRef>>;
36
+ export {};
@@ -0,0 +1,293 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.WgpuAvatar = void 0;
27
+ const jsx_runtime_1 = require("react/jsx-runtime");
28
+ /**
29
+ * WgpuAvatar — drop-in replacement for FilamentAvatar using react-three-fiber
30
+ * + react-native-wgpu (or expo-gl fallback). Exposes the same ref interface as
31
+ * FilamentAvatarRef so siteclaw can swap renderers without changing call sites.
32
+ *
33
+ * No SurfaceTexture, no choreographer, no JNI surface lifecycle bugs.
34
+ *
35
+ * Peer deps required by the host app:
36
+ * react-native-wgpu (or expo-gl for the expo-gl fallback canvas)
37
+ * @react-three/fiber >= 8
38
+ * @react-three/drei >= 9
39
+ * three >= 0.170
40
+ */
41
+ const react_1 = __importStar(require("react"));
42
+ const react_native_1 = require("react-native");
43
+ const THREE = __importStar(require("three"));
44
+ const native_1 = require("@react-three/fiber/native");
45
+ const native_2 = require("@react-three/drei/native");
46
+ const morphTables_1 = require("./morphTables");
47
+ const useAuthedFilamentUri_1 = require("./useAuthedFilamentUri");
48
+ // ---------------------------------------------------------------------------
49
+ // Camera defaults — match FilamentAvatar constants
50
+ // ---------------------------------------------------------------------------
51
+ const CAMERA_POSITION = [0, 1.52, 0.85];
52
+ const CAMERA_TARGET = new THREE.Vector3(0, 1.52, 0);
53
+ const CAMERA_FOV_FULL = 38; // rough Three.js FOV equiv to 50mm focal length
54
+ function buildMorphIndex(mesh) {
55
+ const map = new Map();
56
+ const dict = mesh.morphTargetDictionary;
57
+ if (!dict)
58
+ return map;
59
+ for (const [name, idx] of Object.entries(dict)) {
60
+ map.set(name.toLowerCase(), idx);
61
+ map.set(name, idx); // keep original casing too
62
+ }
63
+ return map;
64
+ }
65
+ function resolveIndex(morphIndex, aliases) {
66
+ for (const alias of aliases) {
67
+ const idx = morphIndex.get(alias) ?? morphIndex.get(alias.toLowerCase());
68
+ if (idx !== undefined)
69
+ return idx;
70
+ }
71
+ return undefined;
72
+ }
73
+ function AvatarScene({ uri, morphStateRef, fov, onReady, onError }) {
74
+ const { camera } = (0, native_1.useThree)();
75
+ const gltf = (0, native_2.useGLTF)(uri);
76
+ const headMeshRef = (0, react_1.useRef)(null);
77
+ const morphIndexRef = (0, react_1.useRef)(new Map());
78
+ const readyFiredRef = (0, react_1.useRef)(false);
79
+ // Set up camera on mount
80
+ (0, react_1.useEffect)(() => {
81
+ if (!(camera instanceof THREE.PerspectiveCamera))
82
+ return;
83
+ camera.fov = fov;
84
+ camera.position.set(...CAMERA_POSITION);
85
+ camera.lookAt(CAMERA_TARGET);
86
+ camera.updateProjectionMatrix();
87
+ }, [camera, fov]);
88
+ // Find the head/face mesh with morph targets after GLTF loads
89
+ (0, react_1.useEffect)(() => {
90
+ if (!gltf?.scene)
91
+ return;
92
+ let bestMesh = null;
93
+ let bestCount = 0;
94
+ gltf.scene.traverse((node) => {
95
+ if (!(node instanceof THREE.Mesh))
96
+ return;
97
+ const count = Object.keys(node.morphTargetDictionary ?? {}).length;
98
+ if (count > bestCount) {
99
+ bestCount = count;
100
+ bestMesh = node;
101
+ }
102
+ });
103
+ if (!bestMesh) {
104
+ onError('No mesh with morph targets found in GLB');
105
+ return;
106
+ }
107
+ headMeshRef.current = bestMesh;
108
+ morphIndexRef.current = buildMorphIndex(bestMesh);
109
+ if (!readyFiredRef.current) {
110
+ readyFiredRef.current = true;
111
+ onReady();
112
+ }
113
+ }, [gltf, onReady, onError]);
114
+ // Per-frame morph weight application
115
+ (0, native_1.useFrame)((_, delta) => {
116
+ const mesh = headMeshRef.current;
117
+ if (!mesh?.morphTargetInfluences)
118
+ return;
119
+ const state = morphStateRef.current;
120
+ const alpha = Math.min(1, state.alpha * delta * 60); // normalize to 60fps
121
+ // Merge mood base + viseme target
122
+ const combined = { ...state.moodBase };
123
+ for (const [name, w] of Object.entries(state.visemeTarget)) {
124
+ combined[name] = Math.max(combined[name] ?? 0, w);
125
+ }
126
+ // Smooth current toward combined
127
+ const allNames = new Set([
128
+ ...Object.keys(state.current),
129
+ ...Object.keys(combined),
130
+ ]);
131
+ for (const name of allNames) {
132
+ const target = combined[name] ?? 0;
133
+ const cur = state.current[name] ?? 0;
134
+ const next = cur + (target - cur) * alpha;
135
+ state.current[name] = next;
136
+ const idx = resolveIndex(morphIndexRef.current, [name]);
137
+ if (idx !== undefined) {
138
+ mesh.morphTargetInfluences[idx] = next;
139
+ }
140
+ }
141
+ // Decay viseme layer
142
+ for (const name of Object.keys(state.visemeTarget)) {
143
+ state.visemeTarget[name] *= Math.pow(0.1, delta); // ~10x decay/s
144
+ if (state.visemeTarget[name] < 0.001) {
145
+ delete state.visemeTarget[name];
146
+ }
147
+ }
148
+ });
149
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("ambientLight", { intensity: 0.6 }), (0, jsx_runtime_1.jsx)("directionalLight", { position: [1, 3, 2], intensity: 1.2, castShadow: false }), (0, jsx_runtime_1.jsx)("primitive", { object: gltf.scene })] }));
150
+ }
151
+ // ---------------------------------------------------------------------------
152
+ // Error boundary so GLTF load failures surface to onError
153
+ // ---------------------------------------------------------------------------
154
+ class GltfErrorBoundary extends react_1.default.Component {
155
+ constructor(props) {
156
+ super(props);
157
+ this.state = { hasError: false };
158
+ }
159
+ static getDerivedStateFromError() { return { hasError: true }; }
160
+ componentDidCatch(err) { this.props.onError(err.message); }
161
+ render() {
162
+ if (this.state.hasError)
163
+ return null;
164
+ return this.props.children;
165
+ }
166
+ }
167
+ // ---------------------------------------------------------------------------
168
+ // Viseme schedule player
169
+ // ---------------------------------------------------------------------------
170
+ function applyVisemeCue(morphState, visemeKey) {
171
+ const aliases = morphTables_1.VISEME_MORPH_ALIASES[visemeKey];
172
+ if (!aliases)
173
+ return;
174
+ const w = morphTables_1.VISEME_WEIGHTS[visemeKey] ?? morphTables_1.DEFAULT_VISEME_WEIGHT;
175
+ for (const alias of aliases) {
176
+ morphState.visemeTarget[alias] = w;
177
+ }
178
+ }
179
+ // ---------------------------------------------------------------------------
180
+ // Main exported component
181
+ // ---------------------------------------------------------------------------
182
+ exports.WgpuAvatar = (0, react_1.forwardRef)(({ style, avatarUrl, focalLength, mood = 'neutral', onReady, onError, }, ref) => {
183
+ // Resolve authenticated file URI (same logic as FilamentAvatar)
184
+ const remoteUrl = avatarUrl && (avatarUrl.startsWith('http://') || avatarUrl.startsWith('https://'))
185
+ ? avatarUrl
186
+ : null;
187
+ const fileResult = (0, useAuthedFilamentUri_1.useAuthedFilamentUri)(remoteUrl);
188
+ // Lock-in URI on first resolve — never change mid-session
189
+ const lockedUriRef = (0, react_1.useRef)(null);
190
+ const currentUri = fileResult?.uri ?? null;
191
+ if (lockedUriRef.current === null && currentUri) {
192
+ lockedUriRef.current = currentUri;
193
+ }
194
+ const localUri = lockedUriRef.current;
195
+ const fov = focalLength
196
+ ? Math.round(2 * Math.atan(21.634 / focalLength) * (180 / Math.PI))
197
+ : CAMERA_FOV_FULL;
198
+ // Shared morph state — mutated directly in useFrame, never causes re-renders
199
+ const morphStateRef = (0, react_1.useRef)({
200
+ current: {},
201
+ visemeTarget: {},
202
+ moodBase: {},
203
+ alpha: 0.18,
204
+ });
205
+ // Update mood baseline when mood prop changes
206
+ (0, react_1.useEffect)(() => {
207
+ morphStateRef.current.moodBase = { ...(morphTables_1.MOOD_MORPHS[mood] ?? {}) };
208
+ }, [mood]);
209
+ // Pending viseme schedule
210
+ const scheduleRef = (0, react_1.useRef)(null);
211
+ const scheduleTimersRef = (0, react_1.useRef)([]);
212
+ const [isReady, setIsReady] = (0, react_1.useState)(false);
213
+ const clearScheduleTimers = (0, react_1.useCallback)(() => {
214
+ for (const t of scheduleTimersRef.current)
215
+ clearTimeout(t);
216
+ scheduleTimersRef.current = [];
217
+ }, []);
218
+ const handleReady = (0, react_1.useCallback)(() => {
219
+ setIsReady(true);
220
+ onReady?.();
221
+ // Flush any pending schedule
222
+ if (scheduleRef.current) {
223
+ const s = scheduleRef.current;
224
+ scheduleRef.current = null;
225
+ applySchedule(s);
226
+ }
227
+ // eslint-disable-next-line react-hooks/exhaustive-deps
228
+ }, [onReady]);
229
+ const handleError = (0, react_1.useCallback)((msg) => {
230
+ console.warn('[WgpuAvatar] GLTF error:', msg);
231
+ onError?.(msg);
232
+ }, [onError]);
233
+ const applySchedule = (0, react_1.useCallback)((schedule) => {
234
+ clearScheduleTimers();
235
+ const now = Date.now();
236
+ const startedAt = schedule.startedAtMs ?? now;
237
+ const offset = now - startedAt;
238
+ for (const cue of schedule.cues) {
239
+ const delay = cue.startMs - offset;
240
+ if (delay < -200)
241
+ continue; // already expired
242
+ const rhubarbKey = cue.viseme;
243
+ const visemeKey = morphTables_1.RHUBARB_TO_VISEME[rhubarbKey] ?? rhubarbKey;
244
+ const t = setTimeout(() => {
245
+ applyVisemeCue(morphStateRef.current, visemeKey);
246
+ }, Math.max(0, delay));
247
+ scheduleTimersRef.current.push(t);
248
+ }
249
+ }, [clearScheduleTimers]);
250
+ // Cleanup on unmount
251
+ (0, react_1.useEffect)(() => () => clearScheduleTimers(), [clearScheduleTimers]);
252
+ (0, react_1.useImperativeHandle)(ref, () => ({
253
+ setMood: (m) => {
254
+ morphStateRef.current.moodBase = { ...(morphTables_1.MOOD_MORPHS[m] ?? {}) };
255
+ },
256
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
257
+ sendAmplitude: (_amplitude) => {
258
+ // Optional: could drive jaw open with amplitude
259
+ // morphStateRef.current.visemeTarget['jawOpen'] = _amplitude * 0.3;
260
+ },
261
+ sendViseme: (viseme, weight) => {
262
+ const aliases = morphTables_1.VISEME_MORPH_ALIASES[viseme];
263
+ if (!aliases)
264
+ return;
265
+ const w = weight ?? morphTables_1.VISEME_WEIGHTS[viseme] ?? morphTables_1.DEFAULT_VISEME_WEIGHT;
266
+ for (const alias of aliases) {
267
+ morphStateRef.current.visemeTarget[alias] = w;
268
+ }
269
+ },
270
+ clearVisemes: () => {
271
+ clearScheduleTimers();
272
+ morphStateRef.current.visemeTarget = {};
273
+ },
274
+ scheduleVisemes: (schedule) => {
275
+ if (!isReady) {
276
+ scheduleRef.current = schedule;
277
+ return;
278
+ }
279
+ applySchedule(schedule);
280
+ },
281
+ }), [isReady, applySchedule, clearScheduleTimers]);
282
+ if (!localUri) {
283
+ return (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [styles.placeholder, style] });
284
+ }
285
+ return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: [styles.container, style], children: (0, jsx_runtime_1.jsx)(native_1.Canvas, { style: react_native_1.StyleSheet.absoluteFill, camera: { fov, position: CAMERA_POSITION, near: 0.01, far: 100 }, gl: { antialias: true, alpha: false }, onCreated: ({ gl }) => {
286
+ gl.setClearColor(new THREE.Color('#1a1a2e'));
287
+ }, children: (0, jsx_runtime_1.jsx)(GltfErrorBoundary, { onError: handleError, children: (0, jsx_runtime_1.jsx)(AvatarScene, { uri: localUri, morphStateRef: morphStateRef, fov: fov, onReady: handleReady, onError: handleError }) }) }) }));
288
+ });
289
+ exports.WgpuAvatar.displayName = 'WgpuAvatar';
290
+ const styles = react_native_1.StyleSheet.create({
291
+ container: { overflow: 'hidden', backgroundColor: '#1a1a2e' },
292
+ placeholder: { backgroundColor: '#1a1a2e' },
293
+ });
@@ -0,0 +1,2 @@
1
+ export { WgpuAvatar } from './WgpuAvatar';
2
+ export type { WgpuAvatarRef } from './WgpuAvatar';
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WgpuAvatar = void 0;
4
+ var WgpuAvatar_1 = require("./WgpuAvatar");
5
+ Object.defineProperty(exports, "WgpuAvatar", { enumerable: true, get: function () { return WgpuAvatar_1.WgpuAvatar; } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "talking-head-studio",
3
- "version": "0.3.9",
3
+ "version": "0.4.1",
4
4
  "description": "Cross-platform 3D avatar component for React Native & web — lip-sync, gestures, accessories, and LLM integration. Powered by TalkingHead + Three.js.",
5
5
  "main": "dist/index.web.js",
6
6
  "browser": "dist/index.web.js",
@@ -37,15 +37,10 @@
37
37
  "types": "./dist/wardrobe/index.d.ts",
38
38
  "default": "./dist/wardrobe/index.js"
39
39
  },
40
- "./filament": {
41
- "react-native": "./dist/filament/index.js",
42
- "types": "./dist/filament/index.d.ts",
43
- "default": "./dist/filament/index.js"
44
- },
45
- "./filament/editor": {
46
- "react-native": "./dist/filament/editor/index.js",
47
- "types": "./dist/filament/editor/index.d.ts",
48
- "default": "./dist/filament/editor/index.js"
40
+ "./wgpu": {
41
+ "react-native": "./dist/wgpu/index.js",
42
+ "types": "./dist/wgpu/index.d.ts",
43
+ "default": "./dist/wgpu/index.js"
49
44
  }
50
45
  },
51
46
  "files": [
@@ -109,8 +104,8 @@
109
104
  "expo-file-system": ">=17",
110
105
  "react": ">=18",
111
106
  "react-native": ">=0.73",
112
- "react-native-filament": ">=1",
113
107
  "react-native-webview": ">=13",
108
+ "react-native-wgpu": ">=0.1",
114
109
  "three": ">=0.170"
115
110
  },
116
111
  "peerDependenciesMeta": {
@@ -120,7 +115,7 @@
120
115
  "react-native-webview": {
121
116
  "optional": true
122
117
  },
123
- "react-native-filament": {
118
+ "react-native-wgpu": {
124
119
  "optional": true
125
120
  },
126
121
  "expo": {
@@ -178,7 +173,7 @@
178
173
  "react-native-gesture-handler": "^2.30.0",
179
174
  "react-native-reanimated": "^4.2.3",
180
175
  "react-native-webview": "^13.16.0",
181
- "react-test-renderer": "^19.2.4",
176
+ "react-test-renderer": "^18.3.1",
182
177
  "three": "^0.180.0",
183
178
  "ts-jest": "^29.4.6",
184
179
  "typescript": "^5.3.3",
@@ -1,41 +0,0 @@
1
- /**
2
- * FilamentAvatar — native Filament-based avatar renderer.
3
- *
4
- * Replaces the TalkingHead WebView on iOS/Android with direct Filament
5
- * morph target writes — no WebView bridge, no JS→native→JS roundtrip,
6
- * visemes applied at 60fps in React Native JS.
7
- *
8
- * Implements the full TalkingHeadRef interface so it can be swapped in
9
- * transparently for the WebView renderer.
10
- */
11
- import React from 'react';
12
- import { ViewStyle, StyleProp } from 'react-native';
13
- import type { TalkingHeadViseme, TalkingHeadVisemeSchedule, TalkingHeadAccessory, TalkingHeadMood } from '../TalkingHead';
14
- export declare const CAMERA_FOCAL_FULL = 28;
15
- export declare const CAMERA_FOCAL_PIP = 50;
16
- export interface FilamentAvatarRef {
17
- sendViseme: (viseme: TalkingHeadViseme, weight?: number) => void;
18
- scheduleVisemes: (schedule: TalkingHeadVisemeSchedule) => void;
19
- clearVisemes: () => void;
20
- sendAmplitude: (amplitude: number) => void;
21
- setMood: (mood: TalkingHeadMood) => void;
22
- setHairColor: (color: string) => void;
23
- setSkinColor: (color: string) => void;
24
- setEyeColor: (color: string) => void;
25
- setAccessories: (accessories: TalkingHeadAccessory[]) => void;
26
- }
27
- interface FilamentAvatarProps {
28
- style?: StyleProp<ViewStyle>;
29
- avatarUrl: string | null;
30
- aspect?: number;
31
- focalLength?: number;
32
- mood?: TalkingHeadMood;
33
- hairColor?: string;
34
- skinColor?: string;
35
- eyeColor?: string;
36
- accessories?: TalkingHeadAccessory[];
37
- onReady?: () => void;
38
- onError?: (message: string) => void;
39
- }
40
- export declare const FilamentAvatar: React.ForwardRefExoticComponent<FilamentAvatarProps & React.RefAttributes<FilamentAvatarRef>>;
41
- export {};