talking-head-studio 0.4.0 → 0.4.2
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 +13 -13
- package/dist/editor/AvatarCanvas.d.ts +3 -14
- package/dist/editor/AvatarCanvas.js +5 -4
- package/dist/editor/AvatarEditor.d.ts +1 -0
- package/dist/editor/AvatarEditor.js +6 -0
- package/dist/editor/AvatarEditor.native.d.ts +4 -0
- package/dist/editor/AvatarEditor.native.js +93 -0
- package/dist/editor/boneSnap.d.ts +25 -0
- package/dist/editor/boneSnap.js +93 -0
- package/dist/editor/index.d.ts +4 -0
- package/dist/editor/index.js +13 -1
- package/dist/editor/types.d.ts +18 -4
- package/dist/utils/avatarUtils.d.ts +2 -3
- package/dist/utils/avatarUtils.js +5 -6
- package/dist/wardrobe/wardrobeStore.d.ts +1 -1
- package/dist/wgpu/WgpuAvatar.d.ts +3 -0
- package/dist/wgpu/WgpuAvatar.js +55 -87
- package/dist/{filament → wgpu}/morphTables.js +1 -1
- package/dist/wgpu/useAuthedModelUri.d.ts +11 -0
- package/dist/{filament/useAuthedFilamentUri.js → wgpu/useAuthedModelUri.js} +9 -9
- package/package.json +2 -15
- package/dist/filament/FilamentAvatar.d.ts +0 -41
- package/dist/filament/FilamentAvatar.js +0 -755
- 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/index.d.ts +0 -6
- package/dist/filament/index.js +0 -24
- package/dist/filament/useAuthedFilamentUri.d.ts +0 -11
- /package/dist/{filament/editor → editor}/studioTheme.d.ts +0 -0
- /package/dist/{filament/editor → editor}/studioTheme.js +0 -0
- /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/wgpu/WgpuAvatar.js
CHANGED
|
@@ -43,14 +43,26 @@ const react_native_1 = require("react-native");
|
|
|
43
43
|
const THREE = __importStar(require("three"));
|
|
44
44
|
const native_1 = require("@react-three/fiber/native");
|
|
45
45
|
const native_2 = require("@react-three/drei/native");
|
|
46
|
-
const morphTables_1 = require("
|
|
47
|
-
const
|
|
46
|
+
const morphTables_1 = require("./morphTables");
|
|
47
|
+
const useAuthedModelUri_1 = require("./useAuthedModelUri");
|
|
48
48
|
// ---------------------------------------------------------------------------
|
|
49
49
|
// Camera defaults — match FilamentAvatar constants
|
|
50
50
|
// ---------------------------------------------------------------------------
|
|
51
51
|
const CAMERA_POSITION = [0, 1.52, 0.85];
|
|
52
52
|
const CAMERA_TARGET = new THREE.Vector3(0, 1.52, 0);
|
|
53
|
-
const CAMERA_FOV_FULL = 38;
|
|
53
|
+
const CAMERA_FOV_FULL = 38;
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Scene/camera ref capture — runs inside Canvas so it can access R3F context
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
function SceneCapture({ onSceneReady }) {
|
|
58
|
+
const { scene, camera } = (0, native_1.useThree)();
|
|
59
|
+
(0, react_1.useEffect)(() => {
|
|
60
|
+
onSceneReady(scene, camera);
|
|
61
|
+
// Only fire once on mount — scene/camera identity is stable
|
|
62
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
63
|
+
}, []);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
54
66
|
function buildMorphIndex(mesh) {
|
|
55
67
|
const map = new Map();
|
|
56
68
|
const dict = mesh.morphTargetDictionary;
|
|
@@ -58,7 +70,7 @@ function buildMorphIndex(mesh) {
|
|
|
58
70
|
return map;
|
|
59
71
|
for (const [name, idx] of Object.entries(dict)) {
|
|
60
72
|
map.set(name.toLowerCase(), idx);
|
|
61
|
-
map.set(name, idx);
|
|
73
|
+
map.set(name, idx);
|
|
62
74
|
}
|
|
63
75
|
return map;
|
|
64
76
|
}
|
|
@@ -70,13 +82,12 @@ function resolveIndex(morphIndex, aliases) {
|
|
|
70
82
|
}
|
|
71
83
|
return undefined;
|
|
72
84
|
}
|
|
73
|
-
function AvatarScene({ uri, morphStateRef, fov, onReady, onError }) {
|
|
85
|
+
function AvatarScene({ uri, morphStateRef, fov, onReady, onError, onSceneReady }) {
|
|
74
86
|
const { camera } = (0, native_1.useThree)();
|
|
75
87
|
const gltf = (0, native_2.useGLTF)(uri);
|
|
76
88
|
const headMeshRef = (0, react_1.useRef)(null);
|
|
77
89
|
const morphIndexRef = (0, react_1.useRef)(new Map());
|
|
78
90
|
const readyFiredRef = (0, react_1.useRef)(false);
|
|
79
|
-
// Set up camera on mount
|
|
80
91
|
(0, react_1.useEffect)(() => {
|
|
81
92
|
if (!(camera instanceof THREE.PerspectiveCamera))
|
|
82
93
|
return;
|
|
@@ -85,7 +96,6 @@ function AvatarScene({ uri, morphStateRef, fov, onReady, onError }) {
|
|
|
85
96
|
camera.lookAt(CAMERA_TARGET);
|
|
86
97
|
camera.updateProjectionMatrix();
|
|
87
98
|
}, [camera, fov]);
|
|
88
|
-
// Find the head/face mesh with morph targets after GLTF loads
|
|
89
99
|
(0, react_1.useEffect)(() => {
|
|
90
100
|
if (!gltf?.scene)
|
|
91
101
|
return;
|
|
@@ -111,58 +121,43 @@ function AvatarScene({ uri, morphStateRef, fov, onReady, onError }) {
|
|
|
111
121
|
onReady();
|
|
112
122
|
}
|
|
113
123
|
}, [gltf, onReady, onError]);
|
|
114
|
-
// Per-frame morph weight application
|
|
115
124
|
(0, native_1.useFrame)((_, delta) => {
|
|
116
125
|
const mesh = headMeshRef.current;
|
|
117
126
|
if (!mesh?.morphTargetInfluences)
|
|
118
127
|
return;
|
|
119
128
|
const state = morphStateRef.current;
|
|
120
|
-
const alpha = Math.min(1, state.alpha * delta * 60);
|
|
121
|
-
// Merge mood base + viseme target
|
|
129
|
+
const alpha = Math.min(1, state.alpha * delta * 60);
|
|
122
130
|
const combined = { ...state.moodBase };
|
|
123
131
|
for (const [name, w] of Object.entries(state.visemeTarget)) {
|
|
124
132
|
combined[name] = Math.max(combined[name] ?? 0, w);
|
|
125
133
|
}
|
|
126
|
-
|
|
127
|
-
const allNames = new Set([
|
|
128
|
-
...Object.keys(state.current),
|
|
129
|
-
...Object.keys(combined),
|
|
130
|
-
]);
|
|
134
|
+
const allNames = new Set([...Object.keys(state.current), ...Object.keys(combined)]);
|
|
131
135
|
for (const name of allNames) {
|
|
132
136
|
const target = combined[name] ?? 0;
|
|
133
137
|
const cur = state.current[name] ?? 0;
|
|
134
138
|
const next = cur + (target - cur) * alpha;
|
|
135
139
|
state.current[name] = next;
|
|
136
140
|
const idx = resolveIndex(morphIndexRef.current, [name]);
|
|
137
|
-
if (idx !== undefined)
|
|
141
|
+
if (idx !== undefined)
|
|
138
142
|
mesh.morphTargetInfluences[idx] = next;
|
|
139
|
-
}
|
|
140
143
|
}
|
|
141
|
-
// Decay viseme layer
|
|
142
144
|
for (const name of Object.keys(state.visemeTarget)) {
|
|
143
|
-
state.visemeTarget[name] *= Math.pow(0.1, delta);
|
|
144
|
-
if (state.visemeTarget[name] < 0.001)
|
|
145
|
+
state.visemeTarget[name] *= Math.pow(0.1, delta);
|
|
146
|
+
if (state.visemeTarget[name] < 0.001)
|
|
145
147
|
delete state.visemeTarget[name];
|
|
146
|
-
}
|
|
147
148
|
}
|
|
148
149
|
});
|
|
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
|
+
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [onSceneReady && (0, jsx_runtime_1.jsx)(SceneCapture, { onSceneReady: onSceneReady }), (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
|
}
|
|
151
152
|
// ---------------------------------------------------------------------------
|
|
152
|
-
// Error boundary
|
|
153
|
+
// Error boundary
|
|
153
154
|
// ---------------------------------------------------------------------------
|
|
154
155
|
class GltfErrorBoundary extends react_1.default.Component {
|
|
155
|
-
constructor(props) {
|
|
156
|
-
super(props);
|
|
157
|
-
this.state = { hasError: false };
|
|
158
|
-
}
|
|
156
|
+
constructor(props) { super(props); this.state = { hasError: false }; }
|
|
159
157
|
static getDerivedStateFromError() { return { hasError: true }; }
|
|
160
158
|
componentDidCatch(err) { this.props.onError(err.message); }
|
|
161
|
-
render() {
|
|
162
|
-
|
|
163
|
-
return null;
|
|
164
|
-
return this.props.children;
|
|
165
|
-
}
|
|
159
|
+
render() { if (this.state.hasError)
|
|
160
|
+
return null; return this.props.children; }
|
|
166
161
|
}
|
|
167
162
|
// ---------------------------------------------------------------------------
|
|
168
163
|
// Viseme schedule player
|
|
@@ -172,41 +167,30 @@ function applyVisemeCue(morphState, visemeKey) {
|
|
|
172
167
|
if (!aliases)
|
|
173
168
|
return;
|
|
174
169
|
const w = morphTables_1.VISEME_WEIGHTS[visemeKey] ?? morphTables_1.DEFAULT_VISEME_WEIGHT;
|
|
175
|
-
for (const alias of aliases)
|
|
170
|
+
for (const alias of aliases)
|
|
176
171
|
morphState.visemeTarget[alias] = w;
|
|
177
|
-
}
|
|
178
172
|
}
|
|
179
173
|
// ---------------------------------------------------------------------------
|
|
180
174
|
// Main exported component
|
|
181
175
|
// ---------------------------------------------------------------------------
|
|
182
|
-
exports.WgpuAvatar = (0, react_1.forwardRef)(({ style, avatarUrl, focalLength, mood = 'neutral', onReady, onError, }, ref) => {
|
|
183
|
-
// Resolve authenticated file URI (same logic as FilamentAvatar)
|
|
176
|
+
exports.WgpuAvatar = (0, react_1.forwardRef)(({ style, avatarUrl, focalLength, mood = 'neutral', onReady, onError, onSceneReady }, ref) => {
|
|
184
177
|
const remoteUrl = avatarUrl && (avatarUrl.startsWith('http://') || avatarUrl.startsWith('https://'))
|
|
185
|
-
? avatarUrl
|
|
186
|
-
|
|
187
|
-
const fileResult = (0, useAuthedFilamentUri_1.useAuthedFilamentUri)(remoteUrl);
|
|
188
|
-
// Lock-in URI on first resolve — never change mid-session
|
|
178
|
+
? avatarUrl : null;
|
|
179
|
+
const fileResult = (0, useAuthedModelUri_1.useAuthedModelUri)(remoteUrl);
|
|
189
180
|
const lockedUriRef = (0, react_1.useRef)(null);
|
|
190
181
|
const currentUri = fileResult?.uri ?? null;
|
|
191
|
-
if (lockedUriRef.current === null && currentUri)
|
|
182
|
+
if (lockedUriRef.current === null && currentUri)
|
|
192
183
|
lockedUriRef.current = currentUri;
|
|
193
|
-
}
|
|
194
184
|
const localUri = lockedUriRef.current;
|
|
195
185
|
const fov = focalLength
|
|
196
186
|
? Math.round(2 * Math.atan(21.634 / focalLength) * (180 / Math.PI))
|
|
197
187
|
: CAMERA_FOV_FULL;
|
|
198
|
-
// Shared morph state — mutated directly in useFrame, never causes re-renders
|
|
199
188
|
const morphStateRef = (0, react_1.useRef)({
|
|
200
|
-
current: {},
|
|
201
|
-
visemeTarget: {},
|
|
202
|
-
moodBase: {},
|
|
203
|
-
alpha: 0.18,
|
|
189
|
+
current: {}, visemeTarget: {}, moodBase: {}, alpha: 0.18,
|
|
204
190
|
});
|
|
205
|
-
// Update mood baseline when mood prop changes
|
|
206
191
|
(0, react_1.useEffect)(() => {
|
|
207
192
|
morphStateRef.current.moodBase = { ...(morphTables_1.MOOD_MORPHS[mood] ?? {}) };
|
|
208
193
|
}, [mood]);
|
|
209
|
-
// Pending viseme schedule
|
|
210
194
|
const scheduleRef = (0, react_1.useRef)(null);
|
|
211
195
|
const scheduleTimersRef = (0, react_1.useRef)([]);
|
|
212
196
|
const [isReady, setIsReady] = (0, react_1.useState)(false);
|
|
@@ -215,21 +199,6 @@ exports.WgpuAvatar = (0, react_1.forwardRef)(({ style, avatarUrl, focalLength, m
|
|
|
215
199
|
clearTimeout(t);
|
|
216
200
|
scheduleTimersRef.current = [];
|
|
217
201
|
}, []);
|
|
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
202
|
const applySchedule = (0, react_1.useCallback)((schedule) => {
|
|
234
203
|
clearScheduleTimers();
|
|
235
204
|
const now = Date.now();
|
|
@@ -238,39 +207,41 @@ exports.WgpuAvatar = (0, react_1.forwardRef)(({ style, avatarUrl, focalLength, m
|
|
|
238
207
|
for (const cue of schedule.cues) {
|
|
239
208
|
const delay = cue.startMs - offset;
|
|
240
209
|
if (delay < -200)
|
|
241
|
-
continue;
|
|
242
|
-
const
|
|
243
|
-
const visemeKey = morphTables_1.RHUBARB_TO_VISEME[rhubarbKey] ?? rhubarbKey;
|
|
210
|
+
continue;
|
|
211
|
+
const visemeKey = morphTables_1.RHUBARB_TO_VISEME[cue.viseme] ?? cue.viseme;
|
|
244
212
|
const t = setTimeout(() => {
|
|
245
213
|
applyVisemeCue(morphStateRef.current, visemeKey);
|
|
246
214
|
}, Math.max(0, delay));
|
|
247
215
|
scheduleTimersRef.current.push(t);
|
|
248
216
|
}
|
|
249
217
|
}, [clearScheduleTimers]);
|
|
250
|
-
|
|
218
|
+
const handleReady = (0, react_1.useCallback)(() => {
|
|
219
|
+
setIsReady(true);
|
|
220
|
+
onReady?.();
|
|
221
|
+
if (scheduleRef.current) {
|
|
222
|
+
const s = scheduleRef.current;
|
|
223
|
+
scheduleRef.current = null;
|
|
224
|
+
applySchedule(s);
|
|
225
|
+
}
|
|
226
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
227
|
+
}, [onReady]);
|
|
228
|
+
const handleError = (0, react_1.useCallback)((msg) => {
|
|
229
|
+
console.warn('[WgpuAvatar] GLTF error:', msg);
|
|
230
|
+
onError?.(msg);
|
|
231
|
+
}, [onError]);
|
|
251
232
|
(0, react_1.useEffect)(() => () => clearScheduleTimers(), [clearScheduleTimers]);
|
|
252
233
|
(0, react_1.useImperativeHandle)(ref, () => ({
|
|
253
|
-
setMood: (m) => {
|
|
254
|
-
|
|
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
|
-
},
|
|
234
|
+
setMood: (m) => { morphStateRef.current.moodBase = { ...(morphTables_1.MOOD_MORPHS[m] ?? {}) }; },
|
|
235
|
+
sendAmplitude: (_amplitude) => { },
|
|
261
236
|
sendViseme: (viseme, weight) => {
|
|
262
237
|
const aliases = morphTables_1.VISEME_MORPH_ALIASES[viseme];
|
|
263
238
|
if (!aliases)
|
|
264
239
|
return;
|
|
265
240
|
const w = weight ?? morphTables_1.VISEME_WEIGHTS[viseme] ?? morphTables_1.DEFAULT_VISEME_WEIGHT;
|
|
266
|
-
for (const alias of aliases)
|
|
241
|
+
for (const alias of aliases)
|
|
267
242
|
morphStateRef.current.visemeTarget[alias] = w;
|
|
268
|
-
}
|
|
269
|
-
},
|
|
270
|
-
clearVisemes: () => {
|
|
271
|
-
clearScheduleTimers();
|
|
272
|
-
morphStateRef.current.visemeTarget = {};
|
|
273
243
|
},
|
|
244
|
+
clearVisemes: () => { clearScheduleTimers(); morphStateRef.current.visemeTarget = {}; },
|
|
274
245
|
scheduleVisemes: (schedule) => {
|
|
275
246
|
if (!isReady) {
|
|
276
247
|
scheduleRef.current = schedule;
|
|
@@ -279,12 +250,9 @@ exports.WgpuAvatar = (0, react_1.forwardRef)(({ style, avatarUrl, focalLength, m
|
|
|
279
250
|
applySchedule(schedule);
|
|
280
251
|
},
|
|
281
252
|
}), [isReady, applySchedule, clearScheduleTimers]);
|
|
282
|
-
if (!localUri)
|
|
253
|
+
if (!localUri)
|
|
283
254
|
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 }) }) }) }));
|
|
255
|
+
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 }) => { gl.setClearColor(new THREE.Color('#1a1a2e')); }, 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, onSceneReady: onSceneReady }) }) }) }));
|
|
288
256
|
});
|
|
289
257
|
exports.WgpuAvatar.displayName = 'WgpuAvatar';
|
|
290
258
|
const styles = react_native_1.StyleSheet.create({
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// ---------------------------------------------------------------------------
|
|
3
|
-
// Morph target tables for
|
|
3
|
+
// Morph target tables for avatar viseme/mood rendering
|
|
4
4
|
// ---------------------------------------------------------------------------
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.MOOD_MORPHS = exports.DEFAULT_VISEME_WEIGHT = exports.VISEME_WEIGHTS = exports.VISEME_MORPH_ALIASES = exports.RHUBARB_TO_VISEME = void 0;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type AuthedFileResult = {
|
|
2
|
+
uri: string;
|
|
3
|
+
size: number;
|
|
4
|
+
} | null;
|
|
5
|
+
/**
|
|
6
|
+
* Downloads a remote URL (with Bearer auth) to the local cache and returns a
|
|
7
|
+
* `file://` URI suitable for WgpuAvatar model source.
|
|
8
|
+
* The native model fetcher doesn't send auth headers, so we pre-fetch here.
|
|
9
|
+
* Also returns the file size in bytes for GPU memory budgeting.
|
|
10
|
+
*/
|
|
11
|
+
export declare function useAuthedModelUri(remoteUrl: string | null): AuthedFileResult;
|
|
@@ -23,7 +23,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
|
|
23
23
|
return result;
|
|
24
24
|
};
|
|
25
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
-
exports.
|
|
26
|
+
exports.useAuthedModelUri = void 0;
|
|
27
27
|
const react_1 = require("react");
|
|
28
28
|
const FileSystem = __importStar(require("expo-file-system/legacy"));
|
|
29
29
|
const studioApi_1 = require("../api/studioApi");
|
|
@@ -31,11 +31,11 @@ const studioApi_1 = require("../api/studioApi");
|
|
|
31
31
|
const failedUrls = new Set();
|
|
32
32
|
/**
|
|
33
33
|
* Downloads a remote URL (with Bearer auth) to the local cache and returns a
|
|
34
|
-
* `file://` URI suitable for
|
|
35
|
-
* The native
|
|
34
|
+
* `file://` URI suitable for WgpuAvatar model source.
|
|
35
|
+
* The native model fetcher doesn't send auth headers, so we pre-fetch here.
|
|
36
36
|
* Also returns the file size in bytes for GPU memory budgeting.
|
|
37
37
|
*/
|
|
38
|
-
function
|
|
38
|
+
function useAuthedModelUri(remoteUrl) {
|
|
39
39
|
const [result, setResult] = (0, react_1.useState)(null);
|
|
40
40
|
(0, react_1.useEffect)(() => {
|
|
41
41
|
if (!remoteUrl) {
|
|
@@ -57,7 +57,7 @@ function useAuthedFilamentUri(remoteUrl) {
|
|
|
57
57
|
// Strip query params (access_token changes per session) so the same GLB is cached across sessions
|
|
58
58
|
const urlWithoutQuery = remoteUrl.split('?')[0];
|
|
59
59
|
const key = urlWithoutQuery.replace(/[^a-zA-Z0-9]/g, '_').slice(-100);
|
|
60
|
-
const dir = `${FileSystem.cacheDirectory}
|
|
60
|
+
const dir = `${FileSystem.cacheDirectory}model/`;
|
|
61
61
|
// Append .glb so native loaders can identify the format
|
|
62
62
|
const localPath = `${dir}${key}.glb`;
|
|
63
63
|
await FileSystem.makeDirectoryAsync(dir, { intermediates: true });
|
|
@@ -85,7 +85,7 @@ function useAuthedFilamentUri(remoteUrl) {
|
|
|
85
85
|
const downloaded = await FileSystem.getInfoAsync(dlResult.uri);
|
|
86
86
|
const downloadedSize = downloaded.size;
|
|
87
87
|
if (!downloaded.exists || !downloadedSize || downloadedSize < 1000) {
|
|
88
|
-
console.error('[
|
|
88
|
+
console.error('[useAuthedModelUri] Download too small (' + (downloadedSize ?? 0) + 'B), likely an error response');
|
|
89
89
|
failedUrls.add(remoteUrl);
|
|
90
90
|
await FileSystem.deleteAsync(dlResult.uri);
|
|
91
91
|
return;
|
|
@@ -96,7 +96,7 @@ function useAuthedFilamentUri(remoteUrl) {
|
|
|
96
96
|
position: 0,
|
|
97
97
|
});
|
|
98
98
|
if (dlHeader !== 'Z2xURg==') {
|
|
99
|
-
console.error('[
|
|
99
|
+
console.error('[useAuthedModelUri] Downloaded file is not a valid GLB (bad magic bytes)');
|
|
100
100
|
failedUrls.add(remoteUrl);
|
|
101
101
|
await FileSystem.deleteAsync(dlResult.uri);
|
|
102
102
|
return;
|
|
@@ -105,7 +105,7 @@ function useAuthedFilamentUri(remoteUrl) {
|
|
|
105
105
|
setResult({ uri: dlResult.uri, size: downloadedSize });
|
|
106
106
|
}
|
|
107
107
|
catch (e) {
|
|
108
|
-
console.error('[
|
|
108
|
+
console.error('[useAuthedModelUri] Failed to download:', remoteUrl.slice(-60), e);
|
|
109
109
|
}
|
|
110
110
|
})();
|
|
111
111
|
return () => {
|
|
@@ -114,4 +114,4 @@ function useAuthedFilamentUri(remoteUrl) {
|
|
|
114
114
|
}, [remoteUrl]);
|
|
115
115
|
return result;
|
|
116
116
|
}
|
|
117
|
-
exports.
|
|
117
|
+
exports.useAuthedModelUri = useAuthedModelUri;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "talking-head-studio",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
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,16 +37,6 @@
|
|
|
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"
|
|
49
|
-
},
|
|
50
40
|
"./wgpu": {
|
|
51
41
|
"react-native": "./dist/wgpu/index.js",
|
|
52
42
|
"types": "./dist/wgpu/index.d.ts",
|
|
@@ -104,6 +94,7 @@
|
|
|
104
94
|
"sideEffects": false,
|
|
105
95
|
"dependencies": {
|
|
106
96
|
"@mediapipe/tasks-vision": "^0.10.34",
|
|
97
|
+
"expo-gl": "^55.0.10",
|
|
107
98
|
"zustand": "^5.0.12"
|
|
108
99
|
},
|
|
109
100
|
"peerDependencies": {
|
|
@@ -114,7 +105,6 @@
|
|
|
114
105
|
"expo-file-system": ">=17",
|
|
115
106
|
"react": ">=18",
|
|
116
107
|
"react-native": ">=0.73",
|
|
117
|
-
"react-native-filament": ">=1",
|
|
118
108
|
"react-native-webview": ">=13",
|
|
119
109
|
"react-native-wgpu": ">=0.1",
|
|
120
110
|
"three": ">=0.170"
|
|
@@ -126,9 +116,6 @@
|
|
|
126
116
|
"react-native-webview": {
|
|
127
117
|
"optional": true
|
|
128
118
|
},
|
|
129
|
-
"react-native-filament": {
|
|
130
|
-
"optional": true
|
|
131
|
-
},
|
|
132
119
|
"react-native-wgpu": {
|
|
133
120
|
"optional": true
|
|
134
121
|
},
|
|
@@ -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 = 50;
|
|
15
|
-
export declare const CAMERA_FOCAL_PIP = 85;
|
|
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 {};
|