talking-head-studio 0.4.11 → 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.
- package/README.md +279 -193
- package/dist/TalkingHead.d.ts +28 -3
- package/dist/TalkingHead.js +21 -2
- package/dist/TalkingHead.web.d.ts +31 -4
- package/dist/TalkingHead.web.js +11 -1
- package/dist/TalkingHeadVisualization.d.ts +22 -0
- package/dist/TalkingHeadVisualization.js +30 -10
- package/dist/api/studioApi.d.ts +12 -1
- package/dist/api/studioApi.js +16 -2
- package/dist/contract.d.ts +14 -0
- package/dist/contract.js +30 -0
- package/dist/core/avatar/avatarCapabilities.d.ts +60 -0
- package/dist/core/avatar/avatarCapabilities.js +100 -0
- package/dist/core/avatar/backends/gaussian.js +6 -4
- package/dist/core/avatar/motion.d.ts +1713 -0
- package/dist/core/avatar/motion.js +550 -0
- package/dist/core/avatar/motionRuntime.d.ts +46 -0
- package/dist/core/avatar/motionRuntime.js +84 -0
- package/dist/core/avatar/schema.d.ts +33 -5
- package/dist/core/avatar/visemes.d.ts +16 -1
- package/dist/core/avatar/visemes.js +48 -1
- package/dist/editor/AvatarCanvas.js +92 -1
- package/dist/editor/AvatarEditor.native.js +1 -0
- package/dist/editor/AvatarModel.js +1 -0
- package/dist/editor/FaceSqueezeEditor.d.ts +3 -1
- package/dist/editor/FaceSqueezeEditor.js +176 -112
- package/dist/editor/FaceSqueezeEditor.web.d.ts +3 -1
- package/dist/editor/FaceSqueezeEditor.web.js +30 -28
- package/dist/editor/RigidAccessory.js +17 -2
- package/dist/editor/SkinnedClothing.js +1 -0
- package/dist/editor/boneLockedDrag.d.ts +11 -0
- package/dist/editor/boneLockedDrag.js +68 -0
- package/dist/editor/boneSnap.web.d.ts +27 -0
- package/dist/editor/boneSnap.web.js +99 -0
- package/dist/editor/index.web.d.ts +10 -0
- package/dist/editor/index.web.js +26 -0
- package/dist/editor/sounds/haha.wav +0 -0
- package/dist/editor/sounds/owie.wav +0 -0
- package/dist/editor/sounds/stop.wav +0 -0
- package/dist/editor/studioTheme.d.ts +14 -14
- package/dist/editor/studioTheme.js +17 -14
- package/dist/editor/types.d.ts +1 -0
- package/dist/html/accessories.d.ts +7 -0
- package/dist/html/accessories.js +149 -0
- package/dist/html/motion.d.ts +1 -0
- package/dist/html/motion.js +189 -0
- package/dist/html/visemes.d.ts +7 -0
- package/dist/html/visemes.js +348 -0
- package/dist/html.d.ts +1 -1
- package/dist/html.js +55 -732
- package/dist/index.d.ts +7 -3
- package/dist/index.js +17 -1
- package/dist/index.web.d.ts +18 -1
- package/dist/index.web.js +36 -3
- package/dist/sketchfab/api.js +1 -0
- package/dist/sketchfab/glbInspect.d.ts +22 -0
- package/dist/sketchfab/glbInspect.js +58 -0
- package/dist/sketchfab/index.d.ts +3 -0
- package/dist/sketchfab/index.js +8 -1
- package/dist/sketchfab/inspectRemote.d.ts +13 -0
- package/dist/sketchfab/inspectRemote.js +77 -0
- package/dist/sketchfab/types.d.ts +10 -0
- package/dist/studio/AccessoryBrowserScreen.d.ts +6 -0
- package/dist/studio/AccessoryBrowserScreen.js +626 -0
- package/dist/studio/AccessoryPanel.d.ts +10 -0
- package/dist/studio/AccessoryPanel.js +396 -0
- package/dist/studio/AppearancePanel.d.ts +9 -0
- package/dist/studio/AppearancePanel.js +77 -0
- package/dist/studio/AvatarCreatorScreen.d.ts +5 -0
- package/dist/studio/AvatarCreatorScreen.js +806 -0
- package/dist/studio/AvatarEditorScreen.d.ts +14 -0
- package/dist/studio/AvatarEditorScreen.js +510 -0
- package/dist/studio/AvatarGrid.d.ts +23 -0
- package/dist/studio/AvatarGrid.js +257 -0
- package/dist/studio/ColorSwatch.d.ts +8 -0
- package/dist/studio/ColorSwatch.js +100 -0
- package/dist/studio/CreateVoiceProfileSheet.d.ts +8 -0
- package/dist/studio/CreateVoiceProfileSheet.js +242 -0
- package/dist/studio/DetailsPanel.d.ts +15 -0
- package/dist/studio/DetailsPanel.js +239 -0
- package/dist/studio/FilamentEditor.d.ts +2 -0
- package/dist/studio/FilamentEditor.js +6 -0
- package/dist/studio/PrecisionPanel.d.ts +2 -0
- package/dist/studio/PrecisionPanel.js +7 -0
- package/dist/studio/PublicGalleryScreen.d.ts +5 -0
- package/dist/studio/PublicGalleryScreen.js +358 -0
- package/dist/studio/SketchfabModelCard.d.ts +20 -0
- package/dist/studio/SketchfabModelCard.js +104 -0
- package/dist/studio/StudioBrowseHeader.d.ts +9 -0
- package/dist/studio/StudioBrowseHeader.js +28 -0
- package/dist/studio/StudioEmptyState.d.ts +8 -0
- package/dist/studio/StudioEmptyState.js +29 -0
- package/dist/studio/StudioFloatingAction.d.ts +13 -0
- package/dist/studio/StudioFloatingAction.js +42 -0
- package/dist/studio/StudioSectionHeader.d.ts +7 -0
- package/dist/studio/StudioSectionHeader.js +27 -0
- package/dist/studio/StudioSurfaceCard.d.ts +8 -0
- package/dist/studio/StudioSurfaceCard.js +20 -0
- package/dist/studio/VoicePanel.d.ts +15 -0
- package/dist/studio/VoicePanel.js +305 -0
- package/dist/studio/constants.d.ts +3 -0
- package/dist/studio/constants.js +6 -0
- package/dist/studio/index.d.ts +29 -0
- package/dist/studio/index.js +54 -0
- package/dist/studio/useSketchfabCapabilities.d.ts +31 -0
- package/dist/studio/useSketchfabCapabilities.js +82 -0
- package/dist/tts/useDirectVisemeStream.js +15 -10
- package/dist/utils/avatarUtils.js +92 -5
- package/dist/utils/faceLandmarkerToShapeWeights.js +2 -4
- package/dist/voice/useAudioPlayer.js +17 -4
- package/dist/voice/useVoicePreview.js +4 -2
- package/dist/wardrobe/index.d.ts +1 -0
- package/dist/wardrobe/index.js +6 -1
- package/dist/wardrobe/useAccessoryGestures.d.ts +20 -0
- package/dist/wardrobe/useAccessoryGestures.js +94 -0
- package/dist/wardrobe/useAvatarWardrobeHydration.js +8 -2
- package/dist/wardrobe/useStudioAvatar.js +11 -2
- package/dist/wardrobe/wardrobeStore.d.ts +2 -0
- package/dist/wardrobe/wardrobeStore.js +12 -2
- package/dist/wgpu/R3FWebGpuCanvas.d.ts +15 -0
- package/dist/wgpu/R3FWebGpuCanvas.js +176 -0
- package/dist/wgpu/WgpuAvatar.d.ts +26 -2
- package/dist/wgpu/WgpuAvatar.js +296 -39
- package/dist/wgpu/accessoryDefaults.d.ts +12 -0
- package/dist/wgpu/accessoryDefaults.js +19 -0
- package/dist/wgpu/blobShim.d.ts +2 -0
- package/dist/wgpu/blobShim.js +191 -0
- package/dist/wgpu/index.d.ts +1 -0
- package/dist/wgpu/index.js +4 -1
- package/dist/wgpu/loadGLTFFromUri.d.ts +2 -0
- package/dist/wgpu/loadGLTFFromUri.js +75 -0
- package/dist/wgpu/morphTables.js +21 -10
- package/dist/wgpu/motionState.d.ts +20 -0
- package/dist/wgpu/motionState.js +31 -0
- package/dist/wgpu/patchThreeForRN.d.ts +28 -0
- package/dist/wgpu/patchThreeForRN.js +292 -0
- package/dist/wgpu/scenePlacement.d.ts +5 -0
- package/dist/wgpu/scenePlacement.js +50 -0
- package/dist/wgpu/useAuthedModelUri.js +4 -2
- package/dist/wgpu/useNativeGLTF.d.ts +7 -0
- package/dist/wgpu/useNativeGLTF.js +36 -0
- package/package.json +97 -31
package/dist/TalkingHead.js
CHANGED
|
@@ -6,6 +6,7 @@ const react_1 = require("react");
|
|
|
6
6
|
const react_native_1 = require("react-native");
|
|
7
7
|
const react_native_webview_1 = require("react-native-webview");
|
|
8
8
|
const html_1 = require("./html");
|
|
9
|
+
const shouldLogDebugMessages = __DEV__;
|
|
9
10
|
exports.TalkingHead = (0, react_1.forwardRef)(({ avatarUrl, authToken, mood = 'neutral', cameraView = 'upper', cameraDistance = -0.5, hairColor, skinColor, eyeColor, accessories, onLoadingChange, onReady, onError, onAvatarState, onVoiceMood, style, vendorBaseUrl, }, ref) => {
|
|
10
11
|
const webViewRef = (0, react_1.useRef)(null);
|
|
11
12
|
const readyRef = (0, react_1.useRef)(false);
|
|
@@ -129,6 +130,14 @@ exports.TalkingHead = (0, react_1.forwardRef)(({ avatarUrl, authToken, mood = 'n
|
|
|
129
130
|
}
|
|
130
131
|
},
|
|
131
132
|
dispatchMotion: (name) => post({ type: 'motion', name }),
|
|
133
|
+
stopMotion: () => post({ type: 'stop_motion' }),
|
|
134
|
+
playGesture: (name, opts) => post({ type: 'gesture', name, dur: opts?.dur, mirror: opts?.mirror, ms: opts?.ms }),
|
|
135
|
+
stopGesture: (ms) => post({ type: 'stop_gesture', ms }),
|
|
136
|
+
playPose: (url, dur) => post({ type: 'pose', url, dur }),
|
|
137
|
+
stopPose: () => post({ type: 'stop_pose' }),
|
|
138
|
+
playAnimation: (url, opts) => post({ type: 'animation', url, dur: opts?.dur, index: opts?.index }),
|
|
139
|
+
stopAnimation: () => post({ type: 'stop_animation' }),
|
|
140
|
+
lookAt: (x, y, ms) => post({ type: 'look_at', x, y, ms }),
|
|
132
141
|
}), [post]);
|
|
133
142
|
// Sync mood via postMessage only — never causes a WebView reload
|
|
134
143
|
(0, react_1.useEffect)(() => {
|
|
@@ -231,7 +240,7 @@ exports.TalkingHead = (0, react_1.forwardRef)(({ avatarUrl, authToken, mood = 'n
|
|
|
231
240
|
else if (msg.type === 'voiceMood') {
|
|
232
241
|
onVoiceMoodRef.current?.(msg.mood);
|
|
233
242
|
}
|
|
234
|
-
else if (msg.type === 'log') {
|
|
243
|
+
else if (msg.type === 'log' && shouldLogDebugMessages) {
|
|
235
244
|
console.log('[TalkingHead]', msg.message);
|
|
236
245
|
}
|
|
237
246
|
}
|
|
@@ -249,7 +258,17 @@ exports.TalkingHead = (0, react_1.forwardRef)(({ avatarUrl, authToken, mood = 'n
|
|
|
249
258
|
const { statusCode, description, url } = event.nativeEvent;
|
|
250
259
|
onErrorRef.current?.(`[http ${statusCode}] ${description || url || 'Avatar request failed'}`);
|
|
251
260
|
}, []);
|
|
252
|
-
return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: [styles.container, style], children: (0, jsx_runtime_1.jsx)(react_native_webview_1.WebView, { ref: webViewRef, source: { html, baseUrl: webViewBaseUrl }, style: styles.webview, javaScriptEnabled: true, domStorageEnabled: true, allowsInlineMediaPlayback: true, mediaPlaybackRequiresUserAction: false, onMessage: onMessage, onError: handleWebViewError, onHttpError: handleWebViewHttpError, onLoadStart: () =>
|
|
261
|
+
return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: [styles.container, style], children: (0, jsx_runtime_1.jsx)(react_native_webview_1.WebView, { ref: webViewRef, source: { html, baseUrl: webViewBaseUrl }, style: styles.webview, javaScriptEnabled: true, domStorageEnabled: true, allowsInlineMediaPlayback: true, mediaPlaybackRequiresUserAction: false, onMessage: onMessage, onError: handleWebViewError, onHttpError: handleWebViewHttpError, onLoadStart: () => {
|
|
262
|
+
if (shouldLogDebugMessages)
|
|
263
|
+
console.log('[TalkingHead] WebView load start');
|
|
264
|
+
}, onLoadEnd: () => {
|
|
265
|
+
if (shouldLogDebugMessages)
|
|
266
|
+
console.log('[TalkingHead] WebView load end');
|
|
267
|
+
}, onLoadProgress: (event) => {
|
|
268
|
+
if (shouldLogDebugMessages) {
|
|
269
|
+
console.log('[TalkingHead] WebView progress', event.nativeEvent.progress);
|
|
270
|
+
}
|
|
271
|
+
}, originWhitelist: ['*'], allowFileAccess: true, allowFileAccessFromFileURLs: true, allowUniversalAccessFromFileURLs: true, mixedContentMode: "always" }, webViewKey) }));
|
|
253
272
|
});
|
|
254
273
|
exports.TalkingHead.displayName = 'TalkingHead';
|
|
255
274
|
const styles = react_native_1.StyleSheet.create({
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import type { TalkingHeadLoadingState, TalkingHeadVisemeCue, TalkingHeadVisemeSchedule } from './TalkingHead';
|
|
3
|
-
|
|
4
|
-
export type
|
|
2
|
+
import type { TalkingHeadLoadingState, TalkingHeadLoadingStage, TalkingHeadViseme, TalkingHeadVisemeCue, TalkingHeadVisemeSchedule } from './TalkingHead';
|
|
3
|
+
import type { MotionKey, TalkingHeadGesture, TalkingHeadPose } from './core/avatar/motion';
|
|
4
|
+
export type { TalkingHeadLoadingStage, TalkingHeadLoadingState, TalkingHeadViseme, TalkingHeadVisemeCue, TalkingHeadVisemeSchedule, };
|
|
5
|
+
export type TalkingHeadMood = 'neutral' | 'happy' | 'sad' | 'angry' | 'fear' | 'disgust' | 'love' | 'sleep' | 'excited' | 'thinking' | 'concerned' | 'surprised';
|
|
5
6
|
export interface TalkingHeadAccessory {
|
|
6
7
|
id: string;
|
|
7
8
|
url: string;
|
|
@@ -31,6 +32,7 @@ export type TalkingHeadPropsAlias = TalkingHeadProps;
|
|
|
31
32
|
export type AvatarPlayerProps = TalkingHeadProps;
|
|
32
33
|
export interface TalkingHeadRef {
|
|
33
34
|
sendAmplitude: (amplitude: number) => void;
|
|
35
|
+
sendViseme: (viseme: TalkingHeadViseme, weight?: number) => void;
|
|
34
36
|
scheduleVisemes: (schedule: TalkingHeadVisemeSchedule) => void;
|
|
35
37
|
clearVisemes: () => void;
|
|
36
38
|
setMood: (mood: TalkingHeadMood) => void;
|
|
@@ -38,7 +40,32 @@ export interface TalkingHeadRef {
|
|
|
38
40
|
setSkinColor: (color: string) => void;
|
|
39
41
|
setEyeColor: (color: string) => void;
|
|
40
42
|
setAccessories: (accessories: TalkingHeadAccessory[]) => void;
|
|
41
|
-
|
|
43
|
+
/** Play a procedural motion (e.g. 'attack', 'defend', 'groove'). */
|
|
44
|
+
dispatchMotion(name: MotionKey): void;
|
|
45
|
+
dispatchMotion(name: string): void;
|
|
46
|
+
/** Stop the current procedural motion and return to rest. */
|
|
47
|
+
stopMotion: () => void;
|
|
48
|
+
/** Play an upstream TalkingHead hand gesture (e.g. 'thumbup'). */
|
|
49
|
+
playGesture: (name: TalkingHeadGesture | string, opts?: {
|
|
50
|
+
dur?: number;
|
|
51
|
+
mirror?: boolean;
|
|
52
|
+
ms?: number;
|
|
53
|
+
}) => void;
|
|
54
|
+
/** Stop the current gesture, easing out over `ms`. */
|
|
55
|
+
stopGesture: (ms?: number) => void;
|
|
56
|
+
/** Strike a pose — a built-in template name (e.g. 'oneknee') or a pose-file URL. */
|
|
57
|
+
playPose: (urlOrTemplate: TalkingHeadPose | string, dur?: number) => void;
|
|
58
|
+
/** Release the current pose and return to the default stance. */
|
|
59
|
+
stopPose: () => void;
|
|
60
|
+
/** Play a full body animation from a GLB/FBX URL (e.g. a combat move). */
|
|
61
|
+
playAnimation: (url: string, opts?: {
|
|
62
|
+
dur?: number;
|
|
63
|
+
index?: number;
|
|
64
|
+
}) => void;
|
|
65
|
+
/** Stop the current body animation. */
|
|
66
|
+
stopAnimation: () => void;
|
|
67
|
+
/** Turn head/eyes toward viewport coordinates (px), easing over `ms`. */
|
|
68
|
+
lookAt: (x: number, y: number, ms?: number) => void;
|
|
42
69
|
}
|
|
43
70
|
/** @deprecated Use AvatarPlayerRef */
|
|
44
71
|
export type TalkingHeadRefAlias = TalkingHeadRef;
|
package/dist/TalkingHead.web.js
CHANGED
|
@@ -37,6 +37,7 @@ exports.TalkingHead = void 0;
|
|
|
37
37
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
38
38
|
const react_1 = __importStar(require("react"));
|
|
39
39
|
const html_1 = require("./html");
|
|
40
|
+
const shouldLogDebugMessages = typeof process !== 'undefined' && process.env.NODE_ENV !== 'production';
|
|
40
41
|
const containerStyle = {
|
|
41
42
|
overflow: 'hidden',
|
|
42
43
|
borderRadius: 12,
|
|
@@ -79,6 +80,7 @@ exports.TalkingHead = (0, react_1.forwardRef)(({ avatarUrl, authToken, mood = 'n
|
|
|
79
80
|
}, []);
|
|
80
81
|
(0, react_1.useImperativeHandle)(ref, () => ({
|
|
81
82
|
sendAmplitude: (amplitude) => post({ type: 'amplitude', value: amplitude }),
|
|
83
|
+
sendViseme: (viseme, weight = 1.0) => post({ type: 'viseme', viseme, weight }),
|
|
82
84
|
scheduleVisemes: (schedule) => post({ type: 'schedule_visemes', schedule }),
|
|
83
85
|
clearVisemes: () => post({ type: 'clear_visemes' }),
|
|
84
86
|
setMood: (nextMood) => {
|
|
@@ -108,6 +110,14 @@ exports.TalkingHead = (0, react_1.forwardRef)(({ avatarUrl, authToken, mood = 'n
|
|
|
108
110
|
}
|
|
109
111
|
},
|
|
110
112
|
dispatchMotion: (name) => post({ type: 'motion', name }),
|
|
113
|
+
stopMotion: () => post({ type: 'stop_motion' }),
|
|
114
|
+
playGesture: (name, opts) => post({ type: 'gesture', name, dur: opts?.dur, mirror: opts?.mirror, ms: opts?.ms }),
|
|
115
|
+
stopGesture: (ms) => post({ type: 'stop_gesture', ms }),
|
|
116
|
+
playPose: (url, dur) => post({ type: 'pose', url, dur }),
|
|
117
|
+
stopPose: () => post({ type: 'stop_pose' }),
|
|
118
|
+
playAnimation: (url, opts) => post({ type: 'animation', url, dur: opts?.dur, index: opts?.index }),
|
|
119
|
+
stopAnimation: () => post({ type: 'stop_animation' }),
|
|
120
|
+
lookAt: (x, y, ms) => post({ type: 'look_at', x, y, ms }),
|
|
111
121
|
}), [post]);
|
|
112
122
|
(0, react_1.useEffect)(() => {
|
|
113
123
|
pendingMoodRef.current = mood;
|
|
@@ -202,7 +212,7 @@ exports.TalkingHead = (0, react_1.forwardRef)(({ avatarUrl, authToken, mood = 'n
|
|
|
202
212
|
else if (msg.type === 'avatarState') {
|
|
203
213
|
onAvatarStateRef.current?.(msg.state);
|
|
204
214
|
}
|
|
205
|
-
else if (msg.type === 'log') {
|
|
215
|
+
else if (msg.type === 'log' && shouldLogDebugMessages) {
|
|
206
216
|
console.log('[TalkingHead]', msg.message);
|
|
207
217
|
}
|
|
208
218
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import './wgpu/blobShim';
|
|
1
2
|
import React from 'react';
|
|
2
3
|
import { ViewStyle, StyleProp } from 'react-native';
|
|
3
4
|
import { type TalkingHeadMood, type TalkingHeadAccessory, type TalkingHeadViseme, type TalkingHeadVisemeSchedule } from './TalkingHead';
|
|
@@ -27,6 +28,27 @@ export interface TalkingHeadVisualizationRef {
|
|
|
27
28
|
clearVisemes: () => void;
|
|
28
29
|
/** Trigger a named motion on the avatar (e.g. 'celebrate', 'groove', 'wave') */
|
|
29
30
|
playMotion: (name: string) => void;
|
|
31
|
+
/** Stop the current procedural motion and return to rest. */
|
|
32
|
+
stopMotion: () => void;
|
|
33
|
+
/** Play an upstream TalkingHead gesture or native procedural fallback. */
|
|
34
|
+
playGesture: (name: string, opts?: {
|
|
35
|
+
dur?: number;
|
|
36
|
+
mirror?: boolean;
|
|
37
|
+
ms?: number;
|
|
38
|
+
}) => void;
|
|
39
|
+
/** Stop the current gesture. */
|
|
40
|
+
stopGesture: (ms?: number) => void;
|
|
41
|
+
/** Strike a pose by built-in name or pose URL. */
|
|
42
|
+
playPose: (name: string, dur?: number) => void;
|
|
43
|
+
/** Release the current pose and return to rest. */
|
|
44
|
+
stopPose: () => void;
|
|
45
|
+
/** Play a hosted animation clip on renderers that support it. */
|
|
46
|
+
playAnimation: (url: string, opts?: {
|
|
47
|
+
dur?: number;
|
|
48
|
+
index?: number;
|
|
49
|
+
}) => void;
|
|
50
|
+
/** Stop the current hosted animation clip. */
|
|
51
|
+
stopAnimation: () => void;
|
|
30
52
|
}
|
|
31
53
|
/**
|
|
32
54
|
* TalkingHeadVisualization — optimized component for rendering the 3D avatar.
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.TalkingHeadVisualization = void 0;
|
|
4
4
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
// IMPORTANT: blobShim must be first — installs Blob+createObjectURL patch
|
|
6
|
+
// before @react-three/fiber's polyfills() runs.
|
|
7
|
+
require("./wgpu/blobShim");
|
|
5
8
|
/* eslint-disable @typescript-eslint/no-unused-expressions */
|
|
6
9
|
const react_1 = require("react");
|
|
7
10
|
const react_native_1 = require("react-native");
|
|
@@ -60,7 +63,7 @@ exports.TalkingHeadVisualization = (0, react_1.forwardRef)(({ style, avatarUrl,
|
|
|
60
63
|
setFallbackUrl(_fallbackDataUri);
|
|
61
64
|
return;
|
|
62
65
|
}
|
|
63
|
-
getFallbackAvatarUrl().then((u) => u && setFallbackUrl(u));
|
|
66
|
+
void getFallbackAvatarUrl().then((u) => u && setFallbackUrl(u));
|
|
64
67
|
}, []);
|
|
65
68
|
const [mood, setMood] = (0, react_1.useState)(initialMood);
|
|
66
69
|
const [isAvatarReady, setIsAvatarReady] = (0, react_1.useState)(false);
|
|
@@ -98,7 +101,7 @@ exports.TalkingHeadVisualization = (0, react_1.forwardRef)(({ style, avatarUrl,
|
|
|
98
101
|
cues: pendingSchedule.cues.length,
|
|
99
102
|
ageMs,
|
|
100
103
|
});
|
|
101
|
-
activeAvatar()?.scheduleVisemes(pendingSchedule);
|
|
104
|
+
activeAvatar()?.scheduleVisemes?.(pendingSchedule);
|
|
102
105
|
onVisemeScheduleApplied?.({
|
|
103
106
|
requestId: pendingSchedule.requestId ?? null,
|
|
104
107
|
appliedAtMs: Date.now(),
|
|
@@ -108,10 +111,24 @@ exports.TalkingHeadVisualization = (0, react_1.forwardRef)(({ style, avatarUrl,
|
|
|
108
111
|
}, [onVisemeScheduleApplied, activeAvatar]);
|
|
109
112
|
(0, react_1.useImperativeHandle)(ref, () => ({
|
|
110
113
|
setMood: (m) => setMood(m),
|
|
111
|
-
sendAmplitude: (a) => activeAvatar()?.sendAmplitude(a),
|
|
112
|
-
sendViseme: (viseme, weight) => activeAvatar()?.sendViseme(viseme, weight),
|
|
113
|
-
clearVisemes: () => activeAvatar()?.clearVisemes(),
|
|
114
|
-
playMotion: (name) =>
|
|
114
|
+
sendAmplitude: (a) => activeAvatar()?.sendAmplitude?.(a),
|
|
115
|
+
sendViseme: (viseme, weight) => activeAvatar()?.sendViseme?.(viseme, weight),
|
|
116
|
+
clearVisemes: () => activeAvatar()?.clearVisemes?.(),
|
|
117
|
+
playMotion: (name) => {
|
|
118
|
+
const avatar = activeAvatar();
|
|
119
|
+
if (avatar?.playMotion) {
|
|
120
|
+
avatar.playMotion(name);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
avatar?.dispatchMotion?.(name);
|
|
124
|
+
},
|
|
125
|
+
stopMotion: () => activeAvatar()?.stopMotion?.(),
|
|
126
|
+
playGesture: (name, opts) => activeAvatar()?.playGesture?.(name, opts),
|
|
127
|
+
stopGesture: (ms) => activeAvatar()?.stopGesture?.(ms),
|
|
128
|
+
playPose: (name, dur) => activeAvatar()?.playPose?.(name, dur),
|
|
129
|
+
stopPose: () => activeAvatar()?.stopPose?.(),
|
|
130
|
+
playAnimation: (url, opts) => activeAvatar()?.playAnimation?.(url, opts),
|
|
131
|
+
stopAnimation: () => activeAvatar()?.stopAnimation?.(),
|
|
115
132
|
scheduleVisemes: (schedule) => {
|
|
116
133
|
const scheduleKey = `${schedule.requestId ?? 'anonymous'}:${schedule.startedAtMs ?? 0}`;
|
|
117
134
|
if (lastScheduledVisemeKeyRef.current === scheduleKey)
|
|
@@ -119,7 +136,7 @@ exports.TalkingHeadVisualization = (0, react_1.forwardRef)(({ style, avatarUrl,
|
|
|
119
136
|
const av = activeAvatar();
|
|
120
137
|
// WgpuAvatar buffers pending schedules internally — no ready gate needed.
|
|
121
138
|
// WebView (avatarRef) still needs the ready gate.
|
|
122
|
-
if (!av || (!wgpuRef.current && !isAvatarReady)) {
|
|
139
|
+
if (!av?.scheduleVisemes || (!wgpuRef.current && !isAvatarReady)) {
|
|
123
140
|
pendingVisemeScheduleRef.current = schedule;
|
|
124
141
|
return;
|
|
125
142
|
}
|
|
@@ -140,8 +157,11 @@ exports.TalkingHeadVisualization = (0, react_1.forwardRef)(({ style, avatarUrl,
|
|
|
140
157
|
lastScheduledVisemeKeyRef.current = null;
|
|
141
158
|
}, [avatarUrl]);
|
|
142
159
|
const handleError = (0, react_1.useCallback)((message) => {
|
|
143
|
-
console.warn('[TalkingHeadVisualization] Avatar
|
|
160
|
+
console.warn('[TalkingHeadVisualization] Avatar error:', message);
|
|
144
161
|
setAvatarError(message);
|
|
162
|
+
// Only fall back to local GLB for hard load failures, not morph-target warnings
|
|
163
|
+
if (message.includes('morph') || message.includes('No mesh'))
|
|
164
|
+
return;
|
|
145
165
|
setUseFallback(true);
|
|
146
166
|
}, []);
|
|
147
167
|
// Effective avatar URL: use remote if available, fallback GLB if error or missing
|
|
@@ -169,7 +189,7 @@ exports.TalkingHeadVisualization = (0, react_1.forwardRef)(({ style, avatarUrl,
|
|
|
169
189
|
ready: isAvatarReady,
|
|
170
190
|
});
|
|
171
191
|
const av = activeAvatar();
|
|
172
|
-
if (!av || (!wgpuRef.current && !isAvatarReady)) {
|
|
192
|
+
if (!av?.scheduleVisemes || (!wgpuRef.current && !isAvatarReady)) {
|
|
173
193
|
pendingVisemeScheduleRef.current = visemeSchedule;
|
|
174
194
|
return;
|
|
175
195
|
}
|
|
@@ -193,7 +213,7 @@ exports.TalkingHeadVisualization = (0, react_1.forwardRef)(({ style, avatarUrl,
|
|
|
193
213
|
if (!effectiveAvatarUrl) {
|
|
194
214
|
return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: [style, styles.placeholder] }));
|
|
195
215
|
}
|
|
196
|
-
return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [style, styles.container], pointerEvents: "box-none", children: [(0, jsx_runtime_1.jsx)(TalkingHead_1.TalkingHead, { ref: avatarRef, avatarUrl: effectiveAvatarUrl, authToken: authToken, cameraView: cameraView, cameraDistance: cameraDistance, accessories: accessories, mood: mood, onLoadingChange: handleLoadingChange, onReady: handleReady, onError: handleError, onVoiceMood: onVoiceMood, style: react_native_1.StyleSheet.
|
|
216
|
+
return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [style, styles.container], pointerEvents: "box-none", children: [(0, jsx_runtime_1.jsx)(TalkingHead_1.TalkingHead, { ref: avatarRef, avatarUrl: effectiveAvatarUrl, authToken: authToken, cameraView: cameraView, cameraDistance: cameraDistance, accessories: accessories, mood: mood, onLoadingChange: handleLoadingChange, onReady: handleReady, onError: handleError, onVoiceMood: onVoiceMood, style: react_native_1.StyleSheet.absoluteFillObject, vendorBaseUrl: vendorBaseUrl }), !isAvatarReady && ((0, jsx_runtime_1.jsx)(react_native_1.View, { testID: "talking-head-loading", style: styles.loadingOverlay, pointerEvents: "none", children: (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.loadingCard, children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.loadingBadge, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.loadingBadgeText, children: "AVATAR" }) }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.loadingTitle, children: avatarError ? 'Avatar failed to load' : getLoadingLabel(loadingState.stage) }), avatarError ? ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.loadingHint, children: avatarError })) : typeof loadingState.progress === 'number' ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)(react_native_1.Text, { style: styles.loadingPercent, children: [loadingState.progress, "%"] }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.progressTrack, children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: { flex: Math.max(6, loadingState.progress ?? 0), height: '100%', borderRadius: 999, backgroundColor: '#5eead4' } }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: { flex: 100 - Math.max(6, loadingState.progress ?? 0) } })] })] })) : ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.loadingHint, children: "Preparing the avatar scene\u2026" }))] }) }))] }));
|
|
197
217
|
});
|
|
198
218
|
exports.TalkingHeadVisualization.displayName = 'TalkingHeadVisualization';
|
|
199
219
|
const styles = react_native_1.StyleSheet.create({
|
package/dist/api/studioApi.d.ts
CHANGED
|
@@ -9,7 +9,18 @@ export declare function getMyAvatars(): Promise<Avatar[]>;
|
|
|
9
9
|
export declare function getAvatar(id: string): Promise<Avatar>;
|
|
10
10
|
export declare function updateAvatar(id: string, data: AvatarUpdate): Promise<Avatar>;
|
|
11
11
|
export declare function deleteAvatar(id: string): Promise<void>;
|
|
12
|
-
export
|
|
12
|
+
export interface CreateAvatarOptions {
|
|
13
|
+
/** Sketchfab model uid this avatar was imported from. */
|
|
14
|
+
sketchfabUid?: string;
|
|
15
|
+
/** Whether the GLB has morph targets (enables real lipsync). */
|
|
16
|
+
sketchfabHasMorphs?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export declare function createAvatar(fileUri: string, name: string, description?: string, options?: CreateAvatarOptions): Promise<Avatar>;
|
|
19
|
+
/**
|
|
20
|
+
* Sketchfab uids that previously imported with working morph targets.
|
|
21
|
+
* Used to pin "verified" models to the top of search results.
|
|
22
|
+
*/
|
|
23
|
+
export declare function getVerifiedSketchfabUids(): Promise<string[]>;
|
|
13
24
|
export declare function getPublicAvatars(): Promise<PublicAvatar[]>;
|
|
14
25
|
export declare function getVoiceProfileSamples(profileId: string): Promise<ProfileSample[]>;
|
|
15
26
|
export declare function getVoiceProfiles(): Promise<VoiceProfile[]>;
|
package/dist/api/studioApi.js
CHANGED
|
@@ -8,6 +8,7 @@ exports.getAvatar = getAvatar;
|
|
|
8
8
|
exports.updateAvatar = updateAvatar;
|
|
9
9
|
exports.deleteAvatar = deleteAvatar;
|
|
10
10
|
exports.createAvatar = createAvatar;
|
|
11
|
+
exports.getVerifiedSketchfabUids = getVerifiedSketchfabUids;
|
|
11
12
|
exports.getPublicAvatars = getPublicAvatars;
|
|
12
13
|
exports.getVoiceProfileSamples = getVoiceProfileSamples;
|
|
13
14
|
exports.getVoiceProfiles = getVoiceProfiles;
|
|
@@ -28,7 +29,9 @@ exports.uploadVoiceSample = uploadVoiceSample;
|
|
|
28
29
|
// ---------------------------------------------------------------------------
|
|
29
30
|
// Injectable configuration
|
|
30
31
|
// ---------------------------------------------------------------------------
|
|
31
|
-
let _baseUrl = process
|
|
32
|
+
let _baseUrl = typeof process !== 'undefined'
|
|
33
|
+
? process.env.EXPO_PUBLIC_BACKEND_URL ?? ''
|
|
34
|
+
: '';
|
|
32
35
|
let _getToken = null;
|
|
33
36
|
function configureAvatarApi(opts) {
|
|
34
37
|
if (opts.baseUrl !== undefined)
|
|
@@ -110,7 +113,7 @@ function updateAvatar(id, data) {
|
|
|
110
113
|
function deleteAvatar(id) {
|
|
111
114
|
return studioFetch(`/v1/avatars/${id}`, { method: 'DELETE' });
|
|
112
115
|
}
|
|
113
|
-
async function createAvatar(fileUri, name, description) {
|
|
116
|
+
async function createAvatar(fileUri, name, description, options) {
|
|
114
117
|
const formData = new FormData();
|
|
115
118
|
formData.append('file', {
|
|
116
119
|
uri: fileUri,
|
|
@@ -121,8 +124,19 @@ async function createAvatar(fileUri, name, description) {
|
|
|
121
124
|
if (description) {
|
|
122
125
|
formData.append('description', description);
|
|
123
126
|
}
|
|
127
|
+
if (options?.sketchfabUid) {
|
|
128
|
+
formData.append('sketchfab_uid', options.sketchfabUid);
|
|
129
|
+
formData.append('sketchfab_has_morphs', options.sketchfabHasMorphs ? 'true' : 'false');
|
|
130
|
+
}
|
|
124
131
|
return studioFetchForm('/v1/avatars', 'POST', formData);
|
|
125
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Sketchfab uids that previously imported with working morph targets.
|
|
135
|
+
* Used to pin "verified" models to the top of search results.
|
|
136
|
+
*/
|
|
137
|
+
function getVerifiedSketchfabUids() {
|
|
138
|
+
return studioFetch('/v1/sketchfab/verified').then((r) => r.uids);
|
|
139
|
+
}
|
|
126
140
|
function getPublicAvatars() {
|
|
127
141
|
return studioFetch('/v1/avatars/public');
|
|
128
142
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable avatar contract entrypoint for SiteBay clients.
|
|
3
|
+
*
|
|
4
|
+
* Runtime packages should import avatar control, viseme, and backend surface
|
|
5
|
+
* types from `talking-head-studio/contract` instead of reaching into internal
|
|
6
|
+
* `src/core/avatar/*` paths.
|
|
7
|
+
*/
|
|
8
|
+
export type { AgentVisemePayload, ArkitBlendShape, EngineVisemeMappingArtifact, OculusViseme, OculusVisemeWeights, RhubarbViseme, VisemeCue, } from './core/avatar/visemes';
|
|
9
|
+
export { ARKIT_TO_OCULUS, ENGINE_VISEME_MAPPING, OCULUS_MORPH_TARGET_NAMES, OCULUS_VISEMES, RHUBARB_TO_OCULUS, RHUBARB_VISEMES, getArkitWeightsForViseme, oculusWeightsForRhubarbCue, remapArkitToOculus, } from './core/avatar/visemes';
|
|
10
|
+
export type { EyeGaze, ExpressionState, FaceControl, HeadPose, Viseme, } from './core/avatar/faceControls';
|
|
11
|
+
export { applyVisemeToExpression, createNeutralExpression, visemeToExpression, } from './core/avatar/faceControls';
|
|
12
|
+
export type { AvatarBackend, AvatarRenderTarget, CalibrationProfile, } from './core/avatar/backend';
|
|
13
|
+
export { MOTION_DEFS, MOTION_KEYS, isMotionKey, TALKINGHEAD_GESTURES, TALKINGHEAD_POSES, } from './core/avatar/motion';
|
|
14
|
+
export type { MotionKey, MotionDef, MotionDefWithBones, MotionBoneKey, MotionOscillator, TalkingHeadGesture, TalkingHeadPose, } from './core/avatar/motion';
|
package/dist/contract.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Stable avatar contract entrypoint for SiteBay clients.
|
|
4
|
+
*
|
|
5
|
+
* Runtime packages should import avatar control, viseme, and backend surface
|
|
6
|
+
* types from `talking-head-studio/contract` instead of reaching into internal
|
|
7
|
+
* `src/core/avatar/*` paths.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.TALKINGHEAD_POSES = exports.TALKINGHEAD_GESTURES = exports.isMotionKey = exports.MOTION_KEYS = exports.MOTION_DEFS = exports.visemeToExpression = exports.createNeutralExpression = exports.applyVisemeToExpression = exports.remapArkitToOculus = exports.oculusWeightsForRhubarbCue = exports.getArkitWeightsForViseme = exports.RHUBARB_VISEMES = exports.RHUBARB_TO_OCULUS = exports.OCULUS_VISEMES = exports.OCULUS_MORPH_TARGET_NAMES = exports.ENGINE_VISEME_MAPPING = exports.ARKIT_TO_OCULUS = void 0;
|
|
11
|
+
var visemes_1 = require("./core/avatar/visemes");
|
|
12
|
+
Object.defineProperty(exports, "ARKIT_TO_OCULUS", { enumerable: true, get: function () { return visemes_1.ARKIT_TO_OCULUS; } });
|
|
13
|
+
Object.defineProperty(exports, "ENGINE_VISEME_MAPPING", { enumerable: true, get: function () { return visemes_1.ENGINE_VISEME_MAPPING; } });
|
|
14
|
+
Object.defineProperty(exports, "OCULUS_MORPH_TARGET_NAMES", { enumerable: true, get: function () { return visemes_1.OCULUS_MORPH_TARGET_NAMES; } });
|
|
15
|
+
Object.defineProperty(exports, "OCULUS_VISEMES", { enumerable: true, get: function () { return visemes_1.OCULUS_VISEMES; } });
|
|
16
|
+
Object.defineProperty(exports, "RHUBARB_TO_OCULUS", { enumerable: true, get: function () { return visemes_1.RHUBARB_TO_OCULUS; } });
|
|
17
|
+
Object.defineProperty(exports, "RHUBARB_VISEMES", { enumerable: true, get: function () { return visemes_1.RHUBARB_VISEMES; } });
|
|
18
|
+
Object.defineProperty(exports, "getArkitWeightsForViseme", { enumerable: true, get: function () { return visemes_1.getArkitWeightsForViseme; } });
|
|
19
|
+
Object.defineProperty(exports, "oculusWeightsForRhubarbCue", { enumerable: true, get: function () { return visemes_1.oculusWeightsForRhubarbCue; } });
|
|
20
|
+
Object.defineProperty(exports, "remapArkitToOculus", { enumerable: true, get: function () { return visemes_1.remapArkitToOculus; } });
|
|
21
|
+
var faceControls_1 = require("./core/avatar/faceControls");
|
|
22
|
+
Object.defineProperty(exports, "applyVisemeToExpression", { enumerable: true, get: function () { return faceControls_1.applyVisemeToExpression; } });
|
|
23
|
+
Object.defineProperty(exports, "createNeutralExpression", { enumerable: true, get: function () { return faceControls_1.createNeutralExpression; } });
|
|
24
|
+
Object.defineProperty(exports, "visemeToExpression", { enumerable: true, get: function () { return faceControls_1.visemeToExpression; } });
|
|
25
|
+
var motion_1 = require("./core/avatar/motion");
|
|
26
|
+
Object.defineProperty(exports, "MOTION_DEFS", { enumerable: true, get: function () { return motion_1.MOTION_DEFS; } });
|
|
27
|
+
Object.defineProperty(exports, "MOTION_KEYS", { enumerable: true, get: function () { return motion_1.MOTION_KEYS; } });
|
|
28
|
+
Object.defineProperty(exports, "isMotionKey", { enumerable: true, get: function () { return motion_1.isMotionKey; } });
|
|
29
|
+
Object.defineProperty(exports, "TALKINGHEAD_GESTURES", { enumerable: true, get: function () { return motion_1.TALKINGHEAD_GESTURES; } });
|
|
30
|
+
Object.defineProperty(exports, "TALKINGHEAD_POSES", { enumerable: true, get: function () { return motion_1.TALKINGHEAD_POSES; } });
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type AvatarSchemaReport, type VisemeTier } from './schema';
|
|
2
|
+
import { type MotionBoneKey } from './motion';
|
|
3
|
+
export interface MotionCoverage {
|
|
4
|
+
/** Motion-engine bone keys this rig can drive (head, spine, arms, …). */
|
|
5
|
+
matched: MotionBoneKey[];
|
|
6
|
+
/** Motion-engine bone keys with no matching bone. */
|
|
7
|
+
missing: MotionBoneKey[];
|
|
8
|
+
/** Enough of the body is rigged for the procedural motion engine to read as
|
|
9
|
+
* "moving" (spine + head + at least one arm). */
|
|
10
|
+
ready: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface AvatarCapabilities {
|
|
13
|
+
/** Full schema report (visemes, skeleton, mesh) from walkAvatarSchema. */
|
|
14
|
+
schema: AvatarSchemaReport;
|
|
15
|
+
/** Best lip-sync strategy: oculus / arkit / minimal / none. */
|
|
16
|
+
lipSyncTier: VisemeTier;
|
|
17
|
+
/** Can this avatar lip-sync from a viseme schedule (morph-driven)? */
|
|
18
|
+
canLipSync: boolean;
|
|
19
|
+
/** Can the talking-head motion engine give this avatar body motion/gestures? */
|
|
20
|
+
canMove: boolean;
|
|
21
|
+
/** Procedural-motion bone coverage detail. */
|
|
22
|
+
motion: MotionCoverage;
|
|
23
|
+
/** Baked animation clip count (informational; the motion engine is procedural). */
|
|
24
|
+
animations: number;
|
|
25
|
+
}
|
|
26
|
+
type GltfJson = {
|
|
27
|
+
nodes?: {
|
|
28
|
+
name?: string;
|
|
29
|
+
}[];
|
|
30
|
+
meshes?: {
|
|
31
|
+
primitives?: {
|
|
32
|
+
indices?: number;
|
|
33
|
+
attributes?: Record<string, number>;
|
|
34
|
+
targets?: unknown[];
|
|
35
|
+
}[];
|
|
36
|
+
extras?: {
|
|
37
|
+
targetNames?: string[];
|
|
38
|
+
};
|
|
39
|
+
}[];
|
|
40
|
+
skins?: {
|
|
41
|
+
joints?: number[];
|
|
42
|
+
}[];
|
|
43
|
+
accessors?: {
|
|
44
|
+
count?: number;
|
|
45
|
+
}[];
|
|
46
|
+
animations?: unknown[];
|
|
47
|
+
};
|
|
48
|
+
/** Decode a .glb (binary) or .gltf (JSON) ArrayBuffer/Uint8Array into its glTF JSON. */
|
|
49
|
+
export declare function parseGlbJson(data: ArrayBuffer | Uint8Array): GltfJson;
|
|
50
|
+
/** Capability report from glTF JSON (the unit-testable core). */
|
|
51
|
+
export declare function inspectAvatarGltf(json: GltfJson): AvatarCapabilities;
|
|
52
|
+
/** Capability report from raw GLB/glTF bytes (browser: fetch(url).arrayBuffer()). */
|
|
53
|
+
export declare function inspectAvatarCapabilities(data: ArrayBuffer | Uint8Array): AvatarCapabilities;
|
|
54
|
+
/**
|
|
55
|
+
* Capability report from a GLB's raw JSON-chunk bytes — for the streaming
|
|
56
|
+
* import path that reads only the header + JSON chunk off disk (mirrors
|
|
57
|
+
* sketchfab's inspectGlbJsonChunk, but for full avatar capabilities).
|
|
58
|
+
*/
|
|
59
|
+
export declare function inspectAvatarJsonChunk(jsonBytes: Uint8Array): AvatarCapabilities;
|
|
60
|
+
export {};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/core/avatar/glbInspect.ts
|
|
3
|
+
// Pre-flight avatar capability check from raw GLB/glTF bytes — no Three.js, no
|
|
4
|
+
// WebGL load required. Reuses walkAvatarSchema() (the single source of truth for
|
|
5
|
+
// viseme tier + humanoid rig) by adapting the glTF JSON into the minimal scene
|
|
6
|
+
// that walker expects, then layers on motion-engine bone coverage.
|
|
7
|
+
//
|
|
8
|
+
// The avatar creator runs this when a user picks/uploads/URLs an avatar, so it
|
|
9
|
+
// can soft-warn ("this model can't lip-sync / won't gesture") before committing.
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.parseGlbJson = parseGlbJson;
|
|
12
|
+
exports.inspectAvatarGltf = inspectAvatarGltf;
|
|
13
|
+
exports.inspectAvatarCapabilities = inspectAvatarCapabilities;
|
|
14
|
+
exports.inspectAvatarJsonChunk = inspectAvatarJsonChunk;
|
|
15
|
+
const schema_1 = require("./schema");
|
|
16
|
+
const motion_1 = require("./motion");
|
|
17
|
+
/** Decode a .glb (binary) or .gltf (JSON) ArrayBuffer/Uint8Array into its glTF JSON. */
|
|
18
|
+
function parseGlbJson(data) {
|
|
19
|
+
const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
20
|
+
// GLB: 'glTF' magic (0x46546C67), then 12-byte header + JSON chunk.
|
|
21
|
+
if (bytes.length >= 12 && bytes[0] === 0x67 && bytes[1] === 0x6c && bytes[2] === 0x54 && bytes[3] === 0x46) {
|
|
22
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
23
|
+
const jsonLen = view.getUint32(12, true); // first chunk length (JSON)
|
|
24
|
+
const jsonBytes = bytes.subarray(20, 20 + jsonLen);
|
|
25
|
+
return JSON.parse(new TextDecoder().decode(jsonBytes));
|
|
26
|
+
}
|
|
27
|
+
// Otherwise assume .gltf JSON text.
|
|
28
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
29
|
+
}
|
|
30
|
+
const norm = (s) => s.toLowerCase().replace(/[_\s\-.]/g, '');
|
|
31
|
+
/** Capability report from glTF JSON (the unit-testable core). */
|
|
32
|
+
function inspectAvatarGltf(json) {
|
|
33
|
+
const nodes = json.nodes ?? [];
|
|
34
|
+
const meshes = json.meshes ?? [];
|
|
35
|
+
const skins = json.skins ?? [];
|
|
36
|
+
const accessors = json.accessors ?? [];
|
|
37
|
+
// Adapt the glTF JSON into the minimal { traverse } scene walkAvatarSchema wants.
|
|
38
|
+
const objects = [];
|
|
39
|
+
for (const skin of skins) {
|
|
40
|
+
const bones = (skin.joints ?? []).map((j) => ({ name: nodes[j]?.name }));
|
|
41
|
+
objects.push({ isSkinnedMesh: true, skeleton: { bones } });
|
|
42
|
+
for (const j of skin.joints ?? []) {
|
|
43
|
+
const nm = nodes[j]?.name;
|
|
44
|
+
if (nm)
|
|
45
|
+
objects.push({ isBone: true, name: nm });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
for (const mesh of meshes) {
|
|
49
|
+
const targetNames = mesh.extras?.targetNames ?? [];
|
|
50
|
+
const dict = targetNames.length ? Object.fromEntries(targetNames.map((n, i) => [String(n), i])) : undefined;
|
|
51
|
+
let idxCount = 0;
|
|
52
|
+
let posCount = 0;
|
|
53
|
+
let hasIdx = false;
|
|
54
|
+
for (const prim of mesh.primitives ?? []) {
|
|
55
|
+
if (prim.indices != null && accessors[prim.indices]) {
|
|
56
|
+
idxCount += accessors[prim.indices].count ?? 0;
|
|
57
|
+
hasIdx = true;
|
|
58
|
+
}
|
|
59
|
+
const posIdx = prim.attributes?.POSITION;
|
|
60
|
+
if (posIdx != null && accessors[posIdx])
|
|
61
|
+
posCount += accessors[posIdx].count ?? 0;
|
|
62
|
+
}
|
|
63
|
+
objects.push({
|
|
64
|
+
isMesh: true,
|
|
65
|
+
...(dict ? { morphTargetDictionary: dict } : {}),
|
|
66
|
+
geometry: hasIdx ? { index: { count: idxCount } } : { attributes: { position: { count: posCount } } },
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
const schema = (0, schema_1.walkAvatarSchema)({ traverse: (cb) => objects.forEach((o) => cb(o)) });
|
|
70
|
+
// Motion-engine coverage: which logical bones the procedural engine can drive.
|
|
71
|
+
const bonesNorm = schema.skeleton.bones.map(norm);
|
|
72
|
+
const matched = [];
|
|
73
|
+
const missing = [];
|
|
74
|
+
for (const [key, kws] of Object.entries(motion_1.MOTION_BONE_SEARCH)) {
|
|
75
|
+
const hit = kws.some((kw) => bonesNorm.some((b) => b.includes(norm(kw))));
|
|
76
|
+
(hit ? matched : missing).push(key);
|
|
77
|
+
}
|
|
78
|
+
const has = (k) => matched.includes(k);
|
|
79
|
+
const ready = schema.skeleton.hasRig && has('spine') && has('head') && (has('leftArm') || has('rightArm'));
|
|
80
|
+
return {
|
|
81
|
+
schema,
|
|
82
|
+
lipSyncTier: schema.morphs.visemeTier,
|
|
83
|
+
canLipSync: schema.morphs.visemeTier !== 'none',
|
|
84
|
+
canMove: ready,
|
|
85
|
+
motion: { matched, missing, ready },
|
|
86
|
+
animations: (json.animations ?? []).length,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/** Capability report from raw GLB/glTF bytes (browser: fetch(url).arrayBuffer()). */
|
|
90
|
+
function inspectAvatarCapabilities(data) {
|
|
91
|
+
return inspectAvatarGltf(parseGlbJson(data));
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Capability report from a GLB's raw JSON-chunk bytes — for the streaming
|
|
95
|
+
* import path that reads only the header + JSON chunk off disk (mirrors
|
|
96
|
+
* sketchfab's inspectGlbJsonChunk, but for full avatar capabilities).
|
|
97
|
+
*/
|
|
98
|
+
function inspectAvatarJsonChunk(jsonBytes) {
|
|
99
|
+
return inspectAvatarGltf(JSON.parse(new TextDecoder().decode(jsonBytes)));
|
|
100
|
+
}
|
|
@@ -142,10 +142,12 @@ class GaussianBackend {
|
|
|
142
142
|
this.boneMap.clear();
|
|
143
143
|
const allBones = new Map();
|
|
144
144
|
this.scene.traverse((obj) => {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
145
|
+
const bone = obj;
|
|
146
|
+
if (bone.isBone)
|
|
147
|
+
allBones.set(bone.name, bone);
|
|
148
|
+
const skinned = obj;
|
|
149
|
+
if (skinned.isSkinnedMesh && skinned.skeleton?.bones) {
|
|
150
|
+
for (const b of skinned.skeleton.bones) {
|
|
149
151
|
allBones.set(b.name, b);
|
|
150
152
|
}
|
|
151
153
|
}
|