talking-head-studio 0.2.8 → 0.3.0
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/TalkingHead.d.ts +8 -0
- package/dist/TalkingHead.js +104 -7
- package/dist/TalkingHead.web.d.ts +3 -2
- package/dist/TalkingHead.web.js +17 -2
- package/dist/TalkingHeadVisualization.d.ts +35 -0
- package/dist/TalkingHeadVisualization.js +277 -0
- package/dist/api/index.d.ts +2 -0
- package/dist/api/index.js +18 -0
- package/dist/api/studioApi.d.ts +38 -0
- package/dist/api/studioApi.js +235 -0
- package/dist/api/types.d.ts +87 -0
- package/dist/api/types.js +5 -0
- package/dist/assets/face-squeeze-local.glb +0 -0
- package/dist/filament/FilamentAvatar.d.ts +41 -0
- package/dist/filament/FilamentAvatar.js +737 -0
- package/dist/filament/faceSqueezeAssets.d.ts +1 -0
- package/dist/filament/faceSqueezeAssets.js +5 -0
- package/dist/filament/index.d.ts +5 -0
- package/dist/filament/index.js +22 -0
- package/dist/filament/morphTables.d.ts +5 -0
- package/dist/filament/morphTables.js +93 -0
- package/dist/filament/useAuthedFilamentUri.d.ts +11 -0
- package/dist/filament/useAuthedFilamentUri.js +126 -0
- package/dist/html.d.ts +7 -0
- package/dist/html.js +255 -56
- package/dist/index.d.ts +9 -2
- package/dist/index.js +13 -2
- package/dist/index.web.d.ts +6 -2
- package/dist/index.web.js +10 -2
- package/dist/utils/avatarUtils.d.ts +13 -0
- package/dist/utils/avatarUtils.js +56 -0
- package/dist/wardrobe/index.d.ts +2 -0
- package/dist/wardrobe/index.js +20 -0
- package/dist/wardrobe/useAvatarWardrobeHydration.d.ts +7 -0
- package/dist/wardrobe/useAvatarWardrobeHydration.js +34 -0
- package/dist/wardrobe/wardrobeStore.d.ts +30 -0
- package/dist/wardrobe/wardrobeStore.js +106 -0
- package/package.json +33 -4
package/dist/TalkingHead.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
3
3
|
export type TalkingHeadMood = 'neutral' | 'happy' | 'sad' | 'angry' | 'excited' | 'thinking' | 'concerned' | 'surprised';
|
|
4
|
+
export type TalkingHeadLoadingStage = 'booting' | 'fetching_model' | 'loading_avatar' | 'loading_fallback' | 'ready';
|
|
5
|
+
export interface TalkingHeadLoadingState {
|
|
6
|
+
stage: TalkingHeadLoadingStage;
|
|
7
|
+
progress?: number | null;
|
|
8
|
+
}
|
|
4
9
|
/**
|
|
5
10
|
* Standard viseme keys supported by the avatar.
|
|
6
11
|
* Use with sendViseme() from your TTS viseme callbacks.
|
|
@@ -46,9 +51,12 @@ export interface TalkingHeadProps {
|
|
|
46
51
|
skinColor?: string;
|
|
47
52
|
eyeColor?: string;
|
|
48
53
|
accessories?: TalkingHeadAccessory[];
|
|
54
|
+
onLoadingChange?: (state: TalkingHeadLoadingState) => void;
|
|
49
55
|
onReady?: () => void;
|
|
50
56
|
onError?: (message: string) => void;
|
|
51
57
|
style?: StyleProp<ViewStyle>;
|
|
58
|
+
/** Base URL for vendored assets. When set, replaces all cdn.jsdelivr.net references. */
|
|
59
|
+
vendorBaseUrl?: string | null;
|
|
52
60
|
}
|
|
53
61
|
export interface TalkingHeadRef {
|
|
54
62
|
sendAmplitude: (amplitude: number) => void;
|
package/dist/TalkingHead.js
CHANGED
|
@@ -6,14 +6,23 @@ 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
|
-
exports.TalkingHead = (0, react_1.forwardRef)(({ avatarUrl, authToken, mood = 'neutral', cameraView = 'upper', cameraDistance = -0.5, hairColor, skinColor, eyeColor, accessories, onReady, onError, style, }, ref) => {
|
|
9
|
+
exports.TalkingHead = (0, react_1.forwardRef)(({ avatarUrl, authToken, mood = 'neutral', cameraView = 'upper', cameraDistance = -0.5, hairColor, skinColor, eyeColor, accessories, onLoadingChange, onReady, onError, style, vendorBaseUrl, }, ref) => {
|
|
10
10
|
const webViewRef = (0, react_1.useRef)(null);
|
|
11
11
|
const readyRef = (0, react_1.useRef)(false);
|
|
12
|
+
const firstMessageSeenRef = (0, react_1.useRef)(false);
|
|
13
|
+
const bootTimerRef = (0, react_1.useRef)(null);
|
|
12
14
|
const pendingMoodRef = (0, react_1.useRef)(mood);
|
|
13
15
|
const pendingHairColorRef = (0, react_1.useRef)(hairColor);
|
|
14
16
|
const pendingSkinColorRef = (0, react_1.useRef)(skinColor);
|
|
15
17
|
const pendingEyeColorRef = (0, react_1.useRef)(eyeColor);
|
|
16
18
|
const accessoriesRef = (0, react_1.useRef)(accessories);
|
|
19
|
+
// Stable refs for callbacks so they never retrigger the boot effect.
|
|
20
|
+
const onLoadingChangeRef = (0, react_1.useRef)(onLoadingChange);
|
|
21
|
+
onLoadingChangeRef.current = onLoadingChange;
|
|
22
|
+
const onReadyRef = (0, react_1.useRef)(onReady);
|
|
23
|
+
onReadyRef.current = onReady;
|
|
24
|
+
const onErrorRef = (0, react_1.useRef)(onError);
|
|
25
|
+
onErrorRef.current = onError;
|
|
17
26
|
// The WebView HTML is built once from stable initial values.
|
|
18
27
|
// avatarUrl + authToken changing causes a controlled key-based remount.
|
|
19
28
|
// All other prop changes (mood, colors, accessories) go via postMessage.
|
|
@@ -27,9 +36,63 @@ exports.TalkingHead = (0, react_1.forwardRef)(({ avatarUrl, authToken, mood = 'n
|
|
|
27
36
|
setWebViewKey(`${avatarUrl}__${authToken ?? ''}`);
|
|
28
37
|
}
|
|
29
38
|
}, [avatarUrl, authToken]);
|
|
30
|
-
|
|
31
|
-
|
|
39
|
+
(0, react_1.useEffect)(() => {
|
|
40
|
+
if (!avatarUrl)
|
|
41
|
+
return;
|
|
42
|
+
firstMessageSeenRef.current = false;
|
|
43
|
+
onLoadingChangeRef.current?.({ stage: 'booting', progress: null });
|
|
44
|
+
if (bootTimerRef.current)
|
|
45
|
+
clearTimeout(bootTimerRef.current);
|
|
46
|
+
bootTimerRef.current = setTimeout(() => {
|
|
47
|
+
if (readyRef.current || firstMessageSeenRef.current)
|
|
48
|
+
return;
|
|
49
|
+
const vendorLabel = vendorBaseUrl ? ` vendor=${vendorBaseUrl}` : ' vendor=<cdn>';
|
|
50
|
+
const avatarLabel = avatarUrl && avatarUrl.length > 80 ? avatarUrl.slice(0, 80) + '…' : avatarUrl;
|
|
51
|
+
onErrorRef.current?.(`[boot timeout] No WebView messages received after 8s.${vendorLabel} avatar=${avatarLabel}`);
|
|
52
|
+
}, 8000);
|
|
53
|
+
return () => {
|
|
54
|
+
if (bootTimerRef.current) {
|
|
55
|
+
clearTimeout(bootTimerRef.current);
|
|
56
|
+
bootTimerRef.current = null;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}, [avatarUrl, authToken, vendorBaseUrl]);
|
|
60
|
+
const postQueue = (0, react_1.useRef)([]);
|
|
61
|
+
const flushTimer = (0, react_1.useRef)(null);
|
|
62
|
+
(0, react_1.useEffect)(() => () => {
|
|
63
|
+
if (flushTimer.current)
|
|
64
|
+
clearTimeout(flushTimer.current);
|
|
65
|
+
}, []);
|
|
66
|
+
const flushQueue = (0, react_1.useCallback)(() => {
|
|
67
|
+
flushTimer.current = null;
|
|
68
|
+
try {
|
|
69
|
+
const wv = webViewRef.current;
|
|
70
|
+
if (!wv)
|
|
71
|
+
return;
|
|
72
|
+
const msgs = postQueue.current.splice(0);
|
|
73
|
+
if (msgs.length === 0)
|
|
74
|
+
return;
|
|
75
|
+
// Batch all pending messages into a single injectJavaScript call
|
|
76
|
+
const js = msgs
|
|
77
|
+
.map((m) => {
|
|
78
|
+
const json = JSON.stringify(JSON.stringify(m));
|
|
79
|
+
return `window.dispatchEvent(new MessageEvent('message',{data:${json}}))`;
|
|
80
|
+
})
|
|
81
|
+
.join(';');
|
|
82
|
+
wv.injectJavaScript(js + ';true;');
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// WebView ref frozen/invalidated during unmount — ignore
|
|
86
|
+
postQueue.current = [];
|
|
87
|
+
}
|
|
32
88
|
}, []);
|
|
89
|
+
const post = (0, react_1.useCallback)((msg) => {
|
|
90
|
+
postQueue.current.push(msg);
|
|
91
|
+
// Coalesce into a single microtask to avoid flooding injectJavaScript
|
|
92
|
+
if (!flushTimer.current) {
|
|
93
|
+
flushTimer.current = setTimeout(flushQueue, 0);
|
|
94
|
+
}
|
|
95
|
+
}, [flushQueue]);
|
|
33
96
|
(0, react_1.useImperativeHandle)(ref, () => ({
|
|
34
97
|
sendAmplitude: (amplitude) => post({ type: 'amplitude', value: amplitude }),
|
|
35
98
|
sendViseme: (viseme, weight = 1.0) => post({ type: 'viseme', viseme, weight }),
|
|
@@ -94,6 +157,18 @@ exports.TalkingHead = (0, react_1.forwardRef)(({ avatarUrl, authToken, mood = 'n
|
|
|
94
157
|
const [initialHairColor] = (0, react_1.useState)(hairColor);
|
|
95
158
|
const [initialSkinColor] = (0, react_1.useState)(skinColor);
|
|
96
159
|
const [initialEyeColor] = (0, react_1.useState)(eyeColor);
|
|
160
|
+
// Use the vendor origin as baseUrl so dynamic module imports are same-origin
|
|
161
|
+
// and don't get blocked by the WebView's security model.
|
|
162
|
+
const webViewBaseUrl = (0, react_1.useMemo)(() => {
|
|
163
|
+
if (!vendorBaseUrl)
|
|
164
|
+
return 'https://localhost/';
|
|
165
|
+
try {
|
|
166
|
+
return new URL(vendorBaseUrl).origin + '/';
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return 'https://localhost/';
|
|
170
|
+
}
|
|
171
|
+
}, [vendorBaseUrl]);
|
|
97
172
|
// html is stable — only rebuilds when webViewKey changes (avatarUrl/authToken)
|
|
98
173
|
const html = (0, react_1.useMemo)(() => (0, html_1.buildAvatarHtml)({
|
|
99
174
|
avatarUrl,
|
|
@@ -104,14 +179,26 @@ exports.TalkingHead = (0, react_1.forwardRef)(({ avatarUrl, authToken, mood = 'n
|
|
|
104
179
|
initialHairColor,
|
|
105
180
|
initialSkinColor,
|
|
106
181
|
initialEyeColor,
|
|
182
|
+
vendorBaseUrl,
|
|
107
183
|
}),
|
|
108
184
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
109
185
|
[webViewKey]);
|
|
110
186
|
const onMessage = (0, react_1.useCallback)((event) => {
|
|
111
187
|
try {
|
|
188
|
+
firstMessageSeenRef.current = true;
|
|
112
189
|
const msg = JSON.parse(event.nativeEvent.data);
|
|
190
|
+
if (msg.type === 'loading' && typeof msg.stage === 'string') {
|
|
191
|
+
onLoadingChangeRef.current?.({
|
|
192
|
+
stage: msg.stage,
|
|
193
|
+
progress: typeof msg.progress === 'number' && Number.isFinite(msg.progress)
|
|
194
|
+
? msg.progress
|
|
195
|
+
: null,
|
|
196
|
+
});
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
113
199
|
if (msg.type === 'ready') {
|
|
114
200
|
readyRef.current = true;
|
|
201
|
+
onLoadingChangeRef.current?.({ stage: 'ready', progress: 100 });
|
|
115
202
|
// Flush pending appearance updates that arrived before the WebView was ready.
|
|
116
203
|
if (pendingMoodRef.current) {
|
|
117
204
|
post({ type: 'mood', value: pendingMoodRef.current });
|
|
@@ -128,10 +215,10 @@ exports.TalkingHead = (0, react_1.forwardRef)(({ avatarUrl, authToken, mood = 'n
|
|
|
128
215
|
if (accessoriesRef.current?.length) {
|
|
129
216
|
post({ type: 'set_accessories', accessories: accessoriesRef.current });
|
|
130
217
|
}
|
|
131
|
-
|
|
218
|
+
onReadyRef.current?.();
|
|
132
219
|
}
|
|
133
220
|
else if (msg.type === 'error') {
|
|
134
|
-
|
|
221
|
+
onErrorRef.current?.(msg.message);
|
|
135
222
|
}
|
|
136
223
|
else if (msg.type === 'log') {
|
|
137
224
|
console.log('[TalkingHead]', msg.message);
|
|
@@ -140,8 +227,18 @@ exports.TalkingHead = (0, react_1.forwardRef)(({ avatarUrl, authToken, mood = 'n
|
|
|
140
227
|
catch (err) {
|
|
141
228
|
console.warn('[TalkingHead] Invalid message from WebView:', err);
|
|
142
229
|
}
|
|
143
|
-
}, [
|
|
144
|
-
|
|
230
|
+
}, [post]);
|
|
231
|
+
const handleWebViewError = (0, react_1.useCallback)((event) => {
|
|
232
|
+
firstMessageSeenRef.current = true;
|
|
233
|
+
const description = event.nativeEvent.description || 'WebView failed to load avatar';
|
|
234
|
+
onErrorRef.current?.(`[webview] ${description}`);
|
|
235
|
+
}, []);
|
|
236
|
+
const handleWebViewHttpError = (0, react_1.useCallback)((event) => {
|
|
237
|
+
firstMessageSeenRef.current = true;
|
|
238
|
+
const { statusCode, description, url } = event.nativeEvent;
|
|
239
|
+
onErrorRef.current?.(`[http ${statusCode}] ${description || url || 'Avatar request failed'}`);
|
|
240
|
+
}, []);
|
|
241
|
+
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: () => console.log('[TalkingHead] WebView load start'), onLoadEnd: () => console.log('[TalkingHead] WebView load end'), onLoadProgress: (event) => console.log('[TalkingHead] WebView progress', event.nativeEvent.progress), originWhitelist: ['*'], allowFileAccess: true, allowFileAccessFromFileURLs: true, allowUniversalAccessFromFileURLs: true, mixedContentMode: "always" }, webViewKey) }));
|
|
145
242
|
});
|
|
146
243
|
exports.TalkingHead.displayName = 'TalkingHead';
|
|
147
244
|
const styles = react_native_1.StyleSheet.create({
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import type { TalkingHeadVisemeCue, TalkingHeadVisemeSchedule } from './TalkingHead';
|
|
3
|
-
export type { TalkingHeadVisemeCue, TalkingHeadVisemeSchedule };
|
|
2
|
+
import type { TalkingHeadLoadingState, TalkingHeadVisemeCue, TalkingHeadVisemeSchedule } from './TalkingHead';
|
|
3
|
+
export type { TalkingHeadLoadingState, TalkingHeadVisemeCue, TalkingHeadVisemeSchedule };
|
|
4
4
|
export type TalkingHeadMood = 'neutral' | 'happy' | 'sad' | 'angry' | 'excited' | 'thinking' | 'concerned' | 'surprised';
|
|
5
5
|
export interface TalkingHeadAccessory {
|
|
6
6
|
id: string;
|
|
@@ -20,6 +20,7 @@ export interface TalkingHeadProps {
|
|
|
20
20
|
skinColor?: string;
|
|
21
21
|
eyeColor?: string;
|
|
22
22
|
accessories?: TalkingHeadAccessory[];
|
|
23
|
+
onLoadingChange?: (state: TalkingHeadLoadingState) => void;
|
|
23
24
|
onReady?: () => void;
|
|
24
25
|
onError?: (message: string) => void;
|
|
25
26
|
style?: React.CSSProperties;
|
package/dist/TalkingHead.web.js
CHANGED
|
@@ -48,7 +48,7 @@ const iframeStyle = {
|
|
|
48
48
|
border: 'none',
|
|
49
49
|
backgroundColor: 'transparent',
|
|
50
50
|
};
|
|
51
|
-
exports.TalkingHead = (0, react_1.forwardRef)(({ avatarUrl, authToken, mood = 'neutral', cameraView = 'upper', cameraDistance = -0.5, hairColor, skinColor, eyeColor, accessories, onReady, onError, style, }, ref) => {
|
|
51
|
+
exports.TalkingHead = (0, react_1.forwardRef)(({ avatarUrl, authToken, mood = 'neutral', cameraView = 'upper', cameraDistance = -0.5, hairColor, skinColor, eyeColor, accessories, onLoadingChange, onReady, onError, style, }, ref) => {
|
|
52
52
|
const iframeRef = (0, react_1.useRef)(null);
|
|
53
53
|
const readyRef = (0, react_1.useRef)(false);
|
|
54
54
|
const pendingMoodRef = (0, react_1.useRef)(mood);
|
|
@@ -59,6 +59,11 @@ exports.TalkingHead = (0, react_1.forwardRef)(({ avatarUrl, authToken, mood = 'n
|
|
|
59
59
|
(0, react_1.useEffect)(() => {
|
|
60
60
|
accessoriesRef.current = accessories;
|
|
61
61
|
}, [accessories]);
|
|
62
|
+
(0, react_1.useEffect)(() => {
|
|
63
|
+
if (!avatarUrl)
|
|
64
|
+
return;
|
|
65
|
+
onLoadingChange?.({ stage: 'booting', progress: null });
|
|
66
|
+
}, [avatarUrl, authToken, onLoadingChange]);
|
|
62
67
|
const post = (0, react_1.useCallback)((msg) => {
|
|
63
68
|
iframeRef.current?.contentWindow?.postMessage(JSON.stringify(msg), '*');
|
|
64
69
|
}, []);
|
|
@@ -151,8 +156,18 @@ exports.TalkingHead = (0, react_1.forwardRef)(({ avatarUrl, authToken, mood = 'n
|
|
|
151
156
|
return;
|
|
152
157
|
try {
|
|
153
158
|
const msg = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
|
|
159
|
+
if (msg.type === 'loading' && typeof msg.stage === 'string') {
|
|
160
|
+
onLoadingChange?.({
|
|
161
|
+
stage: msg.stage,
|
|
162
|
+
progress: typeof msg.progress === 'number' && Number.isFinite(msg.progress)
|
|
163
|
+
? msg.progress
|
|
164
|
+
: null,
|
|
165
|
+
});
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
154
168
|
if (msg.type === 'ready') {
|
|
155
169
|
readyRef.current = true;
|
|
170
|
+
onLoadingChange?.({ stage: 'ready', progress: 100 });
|
|
156
171
|
if (pendingMoodRef.current) {
|
|
157
172
|
post({ type: 'mood', value: pendingMoodRef.current });
|
|
158
173
|
}
|
|
@@ -183,7 +198,7 @@ exports.TalkingHead = (0, react_1.forwardRef)(({ avatarUrl, authToken, mood = 'n
|
|
|
183
198
|
};
|
|
184
199
|
window.addEventListener('message', onMessage);
|
|
185
200
|
return () => window.removeEventListener('message', onMessage);
|
|
186
|
-
}, [onReady, onError, post]);
|
|
201
|
+
}, [onLoadingChange, onReady, onError, post]);
|
|
187
202
|
return ((0, jsx_runtime_1.jsx)("div", { style: { ...containerStyle, ...style }, children: (0, jsx_runtime_1.jsx)("iframe", { ref: iframeRef, srcDoc: srcdoc, style: iframeStyle, sandbox: "allow-scripts allow-same-origin", title: "TalkingHead Avatar" }) }));
|
|
188
203
|
});
|
|
189
204
|
exports.TalkingHead.displayName = 'TalkingHead';
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ViewStyle, StyleProp } from 'react-native';
|
|
3
|
+
import { type TalkingHeadMood, type TalkingHeadAccessory, type TalkingHeadViseme, type TalkingHeadVisemeSchedule } from './TalkingHead';
|
|
4
|
+
interface TalkingHeadVisualizationProps {
|
|
5
|
+
style?: StyleProp<ViewStyle>;
|
|
6
|
+
avatarUrl: string | null;
|
|
7
|
+
authToken?: string;
|
|
8
|
+
cameraView?: 'head' | 'upper' | 'full';
|
|
9
|
+
cameraDistance?: number;
|
|
10
|
+
accessories?: TalkingHeadAccessory[];
|
|
11
|
+
mood?: TalkingHeadMood;
|
|
12
|
+
aspect?: number;
|
|
13
|
+
focalLength?: number;
|
|
14
|
+
visemeSchedule?: TalkingHeadVisemeSchedule | null;
|
|
15
|
+
onVisemeScheduleApplied?: (info: {
|
|
16
|
+
requestId: string | null;
|
|
17
|
+
appliedAtMs: number;
|
|
18
|
+
}) => void;
|
|
19
|
+
vendorBaseUrl?: string | null;
|
|
20
|
+
}
|
|
21
|
+
export interface TalkingHeadVisualizationRef {
|
|
22
|
+
setMood: (mood: TalkingHeadMood) => void;
|
|
23
|
+
sendAmplitude: (amplitude: number) => void;
|
|
24
|
+
sendViseme: (viseme: TalkingHeadViseme, weight?: number) => void;
|
|
25
|
+
scheduleVisemes: (schedule: TalkingHeadVisemeSchedule) => void;
|
|
26
|
+
clearVisemes: () => void;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* TalkingHeadVisualization — optimized component for rendering the 3D avatar.
|
|
30
|
+
*
|
|
31
|
+
* On native: uses FilamentAvatar (direct morph writes, no WebView bridge).
|
|
32
|
+
* On web: uses TalkingHead WebView renderer.
|
|
33
|
+
*/
|
|
34
|
+
export declare const TalkingHeadVisualization: React.ForwardRefExoticComponent<TalkingHeadVisualizationProps & React.RefAttributes<TalkingHeadVisualizationRef>>;
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TalkingHeadVisualization = void 0;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
/* eslint-disable @typescript-eslint/no-unused-expressions */
|
|
6
|
+
const react_1 = require("react");
|
|
7
|
+
const react_native_1 = require("react-native");
|
|
8
|
+
const TalkingHead_1 = require("./TalkingHead");
|
|
9
|
+
const FilamentAvatar_1 = require("./filament/FilamentAvatar");
|
|
10
|
+
const avatarUtils_1 = require("./utils/avatarUtils");
|
|
11
|
+
const faceSqueezeAssets_1 = require("./filament/faceSqueezeAssets");
|
|
12
|
+
// Cached fallback data URI — resolved once, reused across all instances
|
|
13
|
+
let _fallbackDataUri = null;
|
|
14
|
+
let _fallbackPromise = null;
|
|
15
|
+
function getFallbackAvatarUrl() {
|
|
16
|
+
if (_fallbackDataUri)
|
|
17
|
+
return Promise.resolve(_fallbackDataUri);
|
|
18
|
+
if (!_fallbackPromise) {
|
|
19
|
+
_fallbackPromise = (0, avatarUtils_1.resolveLocalAssetUrl)(faceSqueezeAssets_1.FACE_SQUEEZE_LOCAL_MODULE).then((uri) => {
|
|
20
|
+
_fallbackDataUri = uri;
|
|
21
|
+
return uri;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return _fallbackPromise;
|
|
25
|
+
}
|
|
26
|
+
function getLoadingLabel(stage) {
|
|
27
|
+
switch (stage) {
|
|
28
|
+
case 'fetching_model':
|
|
29
|
+
return 'Fetching avatar';
|
|
30
|
+
case 'loading_avatar':
|
|
31
|
+
return 'Loading avatar';
|
|
32
|
+
case 'loading_fallback':
|
|
33
|
+
return 'Loading viewer';
|
|
34
|
+
case 'ready':
|
|
35
|
+
return 'Ready';
|
|
36
|
+
case 'booting':
|
|
37
|
+
default:
|
|
38
|
+
return 'Starting avatar';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* TalkingHeadVisualization — optimized component for rendering the 3D avatar.
|
|
43
|
+
*
|
|
44
|
+
* On native: uses FilamentAvatar (direct morph writes, no WebView bridge).
|
|
45
|
+
* On web: uses TalkingHead WebView renderer.
|
|
46
|
+
*/
|
|
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
|
+
const avatarRef = (0, react_1.useRef)(null);
|
|
49
|
+
// On native, Filament ref is wired via callback ref — store it here so
|
|
50
|
+
// scheduleVisemes / sendAmplitude can route to it.
|
|
51
|
+
const filamentRef = (0, react_1.useRef)(null);
|
|
52
|
+
// Unified accessor — Filament on native, WebView on web
|
|
53
|
+
const activeAvatar = (0, react_1.useCallback)(() => (filamentRef.current ?? avatarRef.current), []);
|
|
54
|
+
// Fallback local GLB data URI — resolved once on mount
|
|
55
|
+
const [fallbackUrl, setFallbackUrl] = (0, react_1.useState)(_fallbackDataUri);
|
|
56
|
+
(0, react_1.useEffect)(() => {
|
|
57
|
+
if (_fallbackDataUri) {
|
|
58
|
+
setFallbackUrl(_fallbackDataUri);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
getFallbackAvatarUrl().then((u) => u && setFallbackUrl(u));
|
|
62
|
+
}, []);
|
|
63
|
+
const [mood, setMood] = (0, react_1.useState)(initialMood);
|
|
64
|
+
const [isAvatarReady, setIsAvatarReady] = (0, react_1.useState)(false);
|
|
65
|
+
const [avatarError, setAvatarError] = (0, react_1.useState)(null);
|
|
66
|
+
const [useFallback, setUseFallback] = (0, react_1.useState)(false);
|
|
67
|
+
const [loadingState, setLoadingState] = (0, react_1.useState)({
|
|
68
|
+
stage: 'booting',
|
|
69
|
+
progress: null,
|
|
70
|
+
});
|
|
71
|
+
const pendingVisemeScheduleRef = (0, react_1.useRef)(null);
|
|
72
|
+
const lastScheduledVisemeKeyRef = (0, react_1.useRef)(null);
|
|
73
|
+
const handleReady = (0, react_1.useCallback)(() => {
|
|
74
|
+
setAvatarError(null);
|
|
75
|
+
setIsAvatarReady(true);
|
|
76
|
+
setLoadingState({ stage: 'ready', progress: 100 });
|
|
77
|
+
const pendingSchedule = pendingVisemeScheduleRef.current;
|
|
78
|
+
if (!pendingSchedule)
|
|
79
|
+
return;
|
|
80
|
+
const ageMs = typeof pendingSchedule.startedAtMs === 'number'
|
|
81
|
+
? Math.max(0, Date.now() - pendingSchedule.startedAtMs)
|
|
82
|
+
: null;
|
|
83
|
+
const durationMs = pendingSchedule.durationMs ?? 0;
|
|
84
|
+
// Drop if audio already finished playing
|
|
85
|
+
if (ageMs != null && durationMs > 0 && ageMs > durationMs + 500) {
|
|
86
|
+
__DEV__ && console.log('[TalkingHead] Dropping expired pending viseme schedule', {
|
|
87
|
+
requestId: pendingSchedule.requestId ?? null,
|
|
88
|
+
ageMs,
|
|
89
|
+
durationMs,
|
|
90
|
+
});
|
|
91
|
+
pendingVisemeScheduleRef.current = null;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
__DEV__ && console.log('[TalkingHead] Applying pending viseme schedule', {
|
|
95
|
+
requestId: pendingSchedule.requestId ?? null,
|
|
96
|
+
cues: pendingSchedule.cues.length,
|
|
97
|
+
ageMs,
|
|
98
|
+
});
|
|
99
|
+
activeAvatar()?.scheduleVisemes(pendingSchedule);
|
|
100
|
+
onVisemeScheduleApplied?.({
|
|
101
|
+
requestId: pendingSchedule.requestId ?? null,
|
|
102
|
+
appliedAtMs: Date.now(),
|
|
103
|
+
});
|
|
104
|
+
lastScheduledVisemeKeyRef.current = `${pendingSchedule.requestId ?? 'anonymous'}:${pendingSchedule.startedAtMs ?? 0}`;
|
|
105
|
+
pendingVisemeScheduleRef.current = null;
|
|
106
|
+
}, [onVisemeScheduleApplied, activeAvatar]);
|
|
107
|
+
(0, react_1.useImperativeHandle)(ref, () => ({
|
|
108
|
+
setMood: (m) => setMood(m),
|
|
109
|
+
sendAmplitude: (a) => activeAvatar()?.sendAmplitude(a),
|
|
110
|
+
sendViseme: (viseme, weight) => activeAvatar()?.sendViseme(viseme, weight),
|
|
111
|
+
clearVisemes: () => activeAvatar()?.clearVisemes(),
|
|
112
|
+
scheduleVisemes: (schedule) => {
|
|
113
|
+
const scheduleKey = `${schedule.requestId ?? 'anonymous'}:${schedule.startedAtMs ?? 0}`;
|
|
114
|
+
if (lastScheduledVisemeKeyRef.current === scheduleKey)
|
|
115
|
+
return;
|
|
116
|
+
const av = activeAvatar();
|
|
117
|
+
// Filament buffers pending schedules internally — no ready gate needed.
|
|
118
|
+
// WebView (avatarRef) still needs the ready gate.
|
|
119
|
+
if (!av || (!filamentRef.current && !isAvatarReady)) {
|
|
120
|
+
pendingVisemeScheduleRef.current = schedule;
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
av.scheduleVisemes(schedule);
|
|
124
|
+
onVisemeScheduleApplied?.({ requestId: schedule.requestId ?? null, appliedAtMs: Date.now() });
|
|
125
|
+
lastScheduledVisemeKeyRef.current = scheduleKey;
|
|
126
|
+
},
|
|
127
|
+
}), [isAvatarReady, onVisemeScheduleApplied, activeAvatar]);
|
|
128
|
+
(0, react_1.useEffect)(() => {
|
|
129
|
+
setMood(initialMood);
|
|
130
|
+
}, [initialMood]);
|
|
131
|
+
(0, react_1.useEffect)(() => {
|
|
132
|
+
setIsAvatarReady(false);
|
|
133
|
+
setAvatarError(null);
|
|
134
|
+
setUseFallback(false);
|
|
135
|
+
setLoadingState({ stage: 'booting', progress: null });
|
|
136
|
+
pendingVisemeScheduleRef.current = null;
|
|
137
|
+
lastScheduledVisemeKeyRef.current = null;
|
|
138
|
+
}, [avatarUrl]);
|
|
139
|
+
const handleError = (0, react_1.useCallback)((message) => {
|
|
140
|
+
console.warn('[TalkingHeadVisualization] Avatar load failed, switching to fallback:', message);
|
|
141
|
+
setAvatarError(message);
|
|
142
|
+
setUseFallback(true);
|
|
143
|
+
}, []);
|
|
144
|
+
// Effective avatar URL: use remote if available, fallback GLB if error or missing
|
|
145
|
+
const effectiveAvatarUrl = useFallback || !avatarUrl ? fallbackUrl : avatarUrl;
|
|
146
|
+
const handleLoadingChange = (0, react_1.useCallback)((nextState) => {
|
|
147
|
+
setLoadingState({
|
|
148
|
+
stage: nextState.stage,
|
|
149
|
+
progress: typeof nextState.progress === 'number' && Number.isFinite(nextState.progress)
|
|
150
|
+
? Math.max(0, Math.min(100, Math.round(nextState.progress)))
|
|
151
|
+
: null,
|
|
152
|
+
});
|
|
153
|
+
}, []);
|
|
154
|
+
(0, react_1.useEffect)(() => {
|
|
155
|
+
if (!visemeSchedule || visemeSchedule.cues.length === 0)
|
|
156
|
+
return;
|
|
157
|
+
const scheduleKey = `${visemeSchedule.requestId ?? 'anonymous'}:${visemeSchedule.startedAtMs ?? 0}`;
|
|
158
|
+
if (lastScheduledVisemeKeyRef.current === scheduleKey)
|
|
159
|
+
return;
|
|
160
|
+
__DEV__ && console.log('[TalkingHead] Received viseme schedule', {
|
|
161
|
+
requestId: visemeSchedule.requestId ?? null,
|
|
162
|
+
cues: visemeSchedule.cues.length,
|
|
163
|
+
ageMs: typeof visemeSchedule.startedAtMs === 'number'
|
|
164
|
+
? Math.max(0, Date.now() - visemeSchedule.startedAtMs)
|
|
165
|
+
: null,
|
|
166
|
+
ready: isAvatarReady,
|
|
167
|
+
});
|
|
168
|
+
const av = activeAvatar();
|
|
169
|
+
if (!av || (!filamentRef.current && !isAvatarReady)) {
|
|
170
|
+
pendingVisemeScheduleRef.current = visemeSchedule;
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
__DEV__ && console.log('[TalkingHead] Scheduling visemes immediately', {
|
|
174
|
+
requestId: visemeSchedule.requestId ?? null,
|
|
175
|
+
cues: visemeSchedule.cues.length,
|
|
176
|
+
});
|
|
177
|
+
av.scheduleVisemes(visemeSchedule);
|
|
178
|
+
onVisemeScheduleApplied?.({
|
|
179
|
+
requestId: visemeSchedule.requestId ?? null,
|
|
180
|
+
appliedAtMs: Date.now(),
|
|
181
|
+
});
|
|
182
|
+
lastScheduledVisemeKeyRef.current = scheduleKey;
|
|
183
|
+
}, [isAvatarReady, onVisemeScheduleApplied, visemeSchedule, activeAvatar]);
|
|
184
|
+
// On native use Filament — direct morph writes, no WebView bridge.
|
|
185
|
+
if (react_native_1.Platform.OS !== 'web') {
|
|
186
|
+
return ((0, jsx_runtime_1.jsx)(FilamentAvatar_1.FilamentAvatar, { focalLength: focalLength, ref: (fr) => {
|
|
187
|
+
filamentRef.current = fr;
|
|
188
|
+
if (typeof ref === 'function')
|
|
189
|
+
ref(fr);
|
|
190
|
+
else if (ref)
|
|
191
|
+
ref.current = fr;
|
|
192
|
+
if (fr && pendingVisemeScheduleRef.current) {
|
|
193
|
+
fr.scheduleVisemes(pendingVisemeScheduleRef.current);
|
|
194
|
+
pendingVisemeScheduleRef.current = null;
|
|
195
|
+
}
|
|
196
|
+
}, style: style, avatarUrl: avatarUrl ?? null, aspect: aspect, mood: mood, accessories: accessories, onReady: handleReady, onError: handleError }));
|
|
197
|
+
}
|
|
198
|
+
if (!effectiveAvatarUrl) {
|
|
199
|
+
return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: [style, styles.placeholder] }));
|
|
200
|
+
}
|
|
201
|
+
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, style: react_native_1.StyleSheet.absoluteFill, 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.jsx)(react_native_1.View, { style: styles.progressTrack, children: (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
|
|
202
|
+
styles.progressFill,
|
|
203
|
+
{ width: `${Math.max(6, loadingState.progress)}%` },
|
|
204
|
+
] }) })] })) : ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.loadingHint, children: "Preparing the avatar scene\u2026" }))] }) }))] }));
|
|
205
|
+
});
|
|
206
|
+
exports.TalkingHeadVisualization.displayName = 'TalkingHeadVisualization';
|
|
207
|
+
const styles = react_native_1.StyleSheet.create({
|
|
208
|
+
container: {
|
|
209
|
+
overflow: 'hidden',
|
|
210
|
+
},
|
|
211
|
+
loadingOverlay: {
|
|
212
|
+
...react_native_1.StyleSheet.absoluteFillObject,
|
|
213
|
+
alignItems: 'center',
|
|
214
|
+
justifyContent: 'center',
|
|
215
|
+
backgroundColor: 'rgba(5, 10, 18, 0.42)',
|
|
216
|
+
padding: 20,
|
|
217
|
+
},
|
|
218
|
+
loadingCard: {
|
|
219
|
+
minWidth: 190,
|
|
220
|
+
maxWidth: 240,
|
|
221
|
+
borderRadius: 20,
|
|
222
|
+
paddingHorizontal: 18,
|
|
223
|
+
paddingVertical: 16,
|
|
224
|
+
backgroundColor: 'rgba(13, 20, 31, 0.88)',
|
|
225
|
+
borderWidth: 1,
|
|
226
|
+
borderColor: 'rgba(151, 163, 184, 0.18)',
|
|
227
|
+
alignItems: 'center',
|
|
228
|
+
},
|
|
229
|
+
loadingBadge: {
|
|
230
|
+
paddingHorizontal: 8,
|
|
231
|
+
paddingVertical: 4,
|
|
232
|
+
borderRadius: 999,
|
|
233
|
+
backgroundColor: 'rgba(83, 156, 255, 0.16)',
|
|
234
|
+
marginBottom: 10,
|
|
235
|
+
},
|
|
236
|
+
loadingBadgeText: {
|
|
237
|
+
color: '#c7dbff',
|
|
238
|
+
fontSize: 10,
|
|
239
|
+
fontWeight: '700',
|
|
240
|
+
letterSpacing: 1.1,
|
|
241
|
+
},
|
|
242
|
+
loadingTitle: {
|
|
243
|
+
color: '#f8fbff',
|
|
244
|
+
fontSize: 18,
|
|
245
|
+
fontWeight: '700',
|
|
246
|
+
},
|
|
247
|
+
loadingPercent: {
|
|
248
|
+
marginTop: 8,
|
|
249
|
+
color: '#7dd3fc',
|
|
250
|
+
fontSize: 28,
|
|
251
|
+
fontWeight: '800',
|
|
252
|
+
},
|
|
253
|
+
loadingHint: {
|
|
254
|
+
marginTop: 10,
|
|
255
|
+
color: 'rgba(226, 232, 240, 0.82)',
|
|
256
|
+
fontSize: 13,
|
|
257
|
+
textAlign: 'center',
|
|
258
|
+
},
|
|
259
|
+
progressTrack: {
|
|
260
|
+
width: '100%',
|
|
261
|
+
height: 8,
|
|
262
|
+
borderRadius: 999,
|
|
263
|
+
backgroundColor: 'rgba(148, 163, 184, 0.18)',
|
|
264
|
+
marginTop: 12,
|
|
265
|
+
overflow: 'hidden',
|
|
266
|
+
},
|
|
267
|
+
progressFill: {
|
|
268
|
+
height: '100%',
|
|
269
|
+
borderRadius: 999,
|
|
270
|
+
backgroundColor: '#5eead4',
|
|
271
|
+
},
|
|
272
|
+
placeholder: {
|
|
273
|
+
backgroundColor: 'rgba(30,34,53,0.5)',
|
|
274
|
+
alignItems: 'center',
|
|
275
|
+
justifyContent: 'center',
|
|
276
|
+
},
|
|
277
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
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 __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./types"), exports);
|
|
18
|
+
__exportStar(require("./studioApi"), exports);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Avatar, AvatarUpdate, PublicAvatar, VoiceProfile, ProfileSample, WearableAsset, PlacementSuggestion } from './types';
|
|
2
|
+
export declare function configureAvatarApi(opts: {
|
|
3
|
+
baseUrl?: string;
|
|
4
|
+
getToken: () => Promise<string | null>;
|
|
5
|
+
}): void;
|
|
6
|
+
export declare function getToken(): Promise<string | null>;
|
|
7
|
+
export declare function studioFetch<T>(path: string, options?: RequestInit): Promise<T>;
|
|
8
|
+
export declare function getMyAvatars(): Promise<Avatar[]>;
|
|
9
|
+
export declare function getAvatar(id: string): Promise<Avatar>;
|
|
10
|
+
export declare function updateAvatar(id: string, data: AvatarUpdate): Promise<Avatar>;
|
|
11
|
+
export declare function deleteAvatar(id: string): Promise<void>;
|
|
12
|
+
export declare function createAvatar(fileUri: string, name: string, description?: string): Promise<Avatar>;
|
|
13
|
+
export declare function getPublicAvatars(): Promise<PublicAvatar[]>;
|
|
14
|
+
export declare function getVoiceProfileSamples(profileId: string): Promise<ProfileSample[]>;
|
|
15
|
+
export declare function getVoiceProfiles(): Promise<VoiceProfile[]>;
|
|
16
|
+
export declare function setDefaultVoice(avatarId: string, profileId: string): Promise<Avatar>;
|
|
17
|
+
export declare function removeDefaultVoice(avatarId: string): Promise<Avatar>;
|
|
18
|
+
export declare function avatarFileUrl(avatar: Avatar): string;
|
|
19
|
+
export declare function avatarThumbnailUrl(avatar: Avatar | PublicAvatar): string | null;
|
|
20
|
+
export declare function avatarAnimatedThumbnailUrl(avatar: Avatar | PublicAvatar): string | null;
|
|
21
|
+
export declare function thumbnailHeaders(): Promise<Record<string, string>>;
|
|
22
|
+
export declare function listAssets(category?: string): Promise<WearableAsset[]>;
|
|
23
|
+
export declare function getAsset(id: string): Promise<WearableAsset>;
|
|
24
|
+
export declare function uploadAsset(fileUri: string, meta: {
|
|
25
|
+
name: string;
|
|
26
|
+
category: string;
|
|
27
|
+
type: 'skinned' | 'rigid';
|
|
28
|
+
slot?: string;
|
|
29
|
+
attach_bone?: string;
|
|
30
|
+
offset_position?: number[];
|
|
31
|
+
offset_rotation?: number[];
|
|
32
|
+
hides_body_parts?: string[];
|
|
33
|
+
}): Promise<WearableAsset>;
|
|
34
|
+
export declare function deleteAsset(id: string): Promise<void>;
|
|
35
|
+
export declare function suggestPlacement(assetId: string, avatarId: string): Promise<PlacementSuggestion>;
|
|
36
|
+
export declare function assetFileUrl(asset: WearableAsset): string;
|
|
37
|
+
export declare function createVoiceProfile(name: string, language?: string): Promise<VoiceProfile>;
|
|
38
|
+
export declare function uploadVoiceSample(profileId: string, fileUri: string, fileName: string, referenceText: string): Promise<VoiceProfile>;
|