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.
- package/dist/TalkingHeadVisualization.d.ts +1 -1
- package/dist/TalkingHeadVisualization.js +9 -9
- package/dist/wgpu/WgpuAvatar.d.ts +36 -0
- package/dist/wgpu/WgpuAvatar.js +293 -0
- package/dist/wgpu/index.d.ts +2 -0
- package/dist/wgpu/index.js +5 -0
- package/package.json +8 -13
- package/dist/filament/FilamentAvatar.d.ts +0 -41
- package/dist/filament/FilamentAvatar.js +0 -715
- package/dist/filament/editor/FilamentEditor.d.ts +0 -16
- package/dist/filament/editor/FilamentEditor.js +0 -880
- package/dist/filament/editor/FilamentEditor.web.d.ts +0 -19
- package/dist/filament/editor/FilamentEditor.web.js +0 -58
- package/dist/filament/editor/PrecisionPanel.d.ts +0 -1
- package/dist/filament/editor/PrecisionPanel.js +0 -252
- package/dist/filament/editor/boneSnap.d.ts +0 -10
- package/dist/filament/editor/boneSnap.js +0 -97
- package/dist/filament/editor/index.d.ts +0 -5
- package/dist/filament/editor/index.js +0 -19
- package/dist/filament/editor/studioTheme.d.ts +0 -86
- package/dist/filament/editor/studioTheme.js +0 -89
- package/dist/filament/index.d.ts +0 -6
- package/dist/filament/index.js +0 -24
- /package/dist/{filament → wgpu}/faceSqueezeAssets.d.ts +0 -0
- /package/dist/{filament → wgpu}/faceSqueezeAssets.js +0 -0
- /package/dist/{filament → wgpu}/morphTables.d.ts +0 -0
- /package/dist/{filament → wgpu}/morphTables.js +0 -0
- /package/dist/{filament → wgpu}/useAuthedFilamentUri.d.ts +0 -0
- /package/dist/{filament → wgpu}/useAuthedFilamentUri.js +0 -0
|
@@ -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
|
|
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
|
|
9
|
+
const WgpuAvatar_1 = require("./wgpu/WgpuAvatar");
|
|
10
10
|
const avatarUtils_1 = require("./utils/avatarUtils");
|
|
11
|
-
const faceSqueezeAssets_1 = require("./
|
|
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
|
|
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
|
|
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)(() => (
|
|
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 || (!
|
|
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 || (!
|
|
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)(
|
|
187
|
-
|
|
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,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
|
+
"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
|
-
"./
|
|
41
|
-
"react-native": "./dist/
|
|
42
|
-
"types": "./dist/
|
|
43
|
-
"default": "./dist/
|
|
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-
|
|
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": "^
|
|
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 {};
|