talking-head-studio 0.4.6 → 0.4.8
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.js +9 -22
- package/dist/html.js +26 -25
- package/dist/wgpu/WgpuAvatar.js +1 -1
- package/package.json +1 -1
|
@@ -46,8 +46,10 @@ function getLoadingLabel(stage) {
|
|
|
46
46
|
*/
|
|
47
47
|
exports.TalkingHeadVisualization = (0, react_1.forwardRef)(({ style, avatarUrl, authToken, cameraView = 'head', cameraDistance = 0.2, accessories, mood: initialMood = 'neutral', aspect, focalLength, visemeSchedule, onVisemeScheduleApplied, onVoiceMood, vendorBaseUrl }, ref) => {
|
|
48
48
|
const avatarRef = (0, react_1.useRef)(null);
|
|
49
|
-
// On native, WgpuAvatar ref is
|
|
50
|
-
//
|
|
49
|
+
// On native, WgpuAvatar ref is stored here so scheduleVisemes / sendAmplitude can route to it.
|
|
50
|
+
// useImperativeHandle is the sole owner of the forwarded ref on all platforms —
|
|
51
|
+
// do NOT manually forward ref inside the WgpuAvatar callback ref, as that bypasses
|
|
52
|
+
// the dedup key check in scheduleVisemes and causes double-fire.
|
|
51
53
|
const wgpuRef = (0, react_1.useRef)(null);
|
|
52
54
|
// Unified accessor — WgpuAvatar on native, WebView on web
|
|
53
55
|
const activeAvatar = (0, react_1.useCallback)(() => (wgpuRef.current ?? avatarRef.current), []);
|
|
@@ -183,26 +185,15 @@ exports.TalkingHeadVisualization = (0, react_1.forwardRef)(({ style, avatarUrl,
|
|
|
183
185
|
lastScheduledVisemeKeyRef.current = scheduleKey;
|
|
184
186
|
}, [isAvatarReady, onVisemeScheduleApplied, visemeSchedule, activeAvatar]);
|
|
185
187
|
// On native use WgpuAvatar — direct morph writes, no WebView bridge.
|
|
188
|
+
// NOTE: Only store wgpuRef here — do NOT forward `ref` manually. useImperativeHandle
|
|
189
|
+
// above is the sole owner of the forwarded ref and handles the dedup key check.
|
|
186
190
|
if (react_native_1.Platform.OS !== 'web') {
|
|
187
|
-
return ((0, jsx_runtime_1.jsx)(WgpuAvatar_1.WgpuAvatar, { focalLength: focalLength, ref: (fr) => {
|
|
188
|
-
wgpuRef.current = fr;
|
|
189
|
-
if (typeof ref === 'function')
|
|
190
|
-
ref(fr);
|
|
191
|
-
else if (ref)
|
|
192
|
-
ref.current = fr;
|
|
193
|
-
if (fr && pendingVisemeScheduleRef.current) {
|
|
194
|
-
fr.scheduleVisemes(pendingVisemeScheduleRef.current);
|
|
195
|
-
pendingVisemeScheduleRef.current = null;
|
|
196
|
-
}
|
|
197
|
-
}, style: style, avatarUrl: avatarUrl ?? null, aspect: aspect, mood: mood, accessories: accessories, onReady: handleReady, onError: handleError }));
|
|
191
|
+
return ((0, jsx_runtime_1.jsx)(WgpuAvatar_1.WgpuAvatar, { focalLength: focalLength, ref: (fr) => { wgpuRef.current = fr; }, style: style, avatarUrl: avatarUrl ?? null, aspect: aspect, mood: mood, accessories: accessories, onReady: handleReady, onError: handleError }));
|
|
198
192
|
}
|
|
199
193
|
if (!effectiveAvatarUrl) {
|
|
200
194
|
return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: [style, styles.placeholder] }));
|
|
201
195
|
}
|
|
202
|
-
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.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.
|
|
203
|
-
styles.progressFill,
|
|
204
|
-
{ width: `${Math.max(6, loadingState.progress)}%` },
|
|
205
|
-
] }) })] })) : ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.loadingHint, children: "Preparing the avatar scene\u2026" }))] }) }))] }));
|
|
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.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.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" }))] }) }))] }));
|
|
206
197
|
});
|
|
207
198
|
exports.TalkingHeadVisualization.displayName = 'TalkingHeadVisualization';
|
|
208
199
|
const styles = react_native_1.StyleSheet.create({
|
|
@@ -264,11 +255,7 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
264
255
|
backgroundColor: 'rgba(148, 163, 184, 0.18)',
|
|
265
256
|
marginTop: 12,
|
|
266
257
|
overflow: 'hidden',
|
|
267
|
-
|
|
268
|
-
progressFill: {
|
|
269
|
-
height: '100%',
|
|
270
|
-
borderRadius: 999,
|
|
271
|
-
backgroundColor: '#5eead4',
|
|
258
|
+
flexDirection: 'row',
|
|
272
259
|
},
|
|
273
260
|
placeholder: {
|
|
274
261
|
backgroundColor: 'rgba(30,34,53,0.5)',
|
package/dist/html.js
CHANGED
|
@@ -40,25 +40,26 @@ function buildAvatarHtml(config) {
|
|
|
40
40
|
}
|
|
41
41
|
</script>
|
|
42
42
|
<script>
|
|
43
|
-
window.ReactNativeWebView
|
|
43
|
+
function rnPost(d) { window.ReactNativeWebView && window.ReactNativeWebView.postMessage(d); }
|
|
44
|
+
rnPost(
|
|
44
45
|
JSON.stringify({ type: 'log', message: '[bootstrap] inline script start' })
|
|
45
46
|
);
|
|
46
47
|
function postBootstrapError(kind, message) {
|
|
47
|
-
|
|
48
|
+
rnPost(
|
|
48
49
|
JSON.stringify({ type: 'error', message: '[' + kind + '] ' + String(message || 'Unknown error') })
|
|
49
50
|
);
|
|
50
51
|
}
|
|
51
52
|
document.addEventListener('DOMContentLoaded', function() {
|
|
52
|
-
|
|
53
|
+
rnPost(
|
|
53
54
|
JSON.stringify({ type: 'log', message: '[bootstrap] DOMContentLoaded' })
|
|
54
55
|
);
|
|
55
56
|
});
|
|
56
57
|
window.addEventListener('error', function(event) {
|
|
57
|
-
postBootstrapError('window.error', event
|
|
58
|
+
postBootstrapError('window.error', (event && event.message) || (event && event.error && event.error.message) || 'Script error');
|
|
58
59
|
});
|
|
59
60
|
window.addEventListener('unhandledrejection', function(event) {
|
|
60
|
-
const reason = event
|
|
61
|
-
postBootstrapError('unhandledrejection', reason
|
|
61
|
+
const reason = event && event.reason;
|
|
62
|
+
postBootstrapError('unhandledrejection', (reason && reason.message) || reason || 'Unhandled promise rejection');
|
|
62
63
|
});
|
|
63
64
|
</script>
|
|
64
65
|
</head>
|
|
@@ -66,7 +67,7 @@ window.addEventListener('unhandledrejection', function(event) {
|
|
|
66
67
|
<div id="avatar"></div>
|
|
67
68
|
<script type="module">
|
|
68
69
|
(async function() {
|
|
69
|
-
|
|
70
|
+
rnPost(
|
|
70
71
|
JSON.stringify({ type: 'log', message: '[module] script start' })
|
|
71
72
|
);
|
|
72
73
|
const AUTH_TOKEN = ${JSON.stringify(config.authToken ?? null)};
|
|
@@ -94,11 +95,11 @@ let staticModel = null;
|
|
|
94
95
|
let initStarted = false;
|
|
95
96
|
|
|
96
97
|
function log(msg) {
|
|
97
|
-
|
|
98
|
+
rnPost(JSON.stringify({ type: 'log', message: msg }));
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
function emitLoading(stage, progress = null) {
|
|
101
|
-
|
|
102
|
+
rnPost(
|
|
102
103
|
JSON.stringify({ type: 'loading', stage, progress }),
|
|
103
104
|
);
|
|
104
105
|
}
|
|
@@ -125,13 +126,13 @@ function applyColorOverrides() {
|
|
|
125
126
|
mats.forEach((mat) => {
|
|
126
127
|
const name = (mat.name || '').toLowerCase();
|
|
127
128
|
if (HAIR_COLOR && (name.includes('hair') || name.includes('fur'))) {
|
|
128
|
-
mat.color
|
|
129
|
+
mat.color && mat.color.set(HAIR_COLOR);
|
|
129
130
|
}
|
|
130
131
|
if (SKIN_COLOR && (name.includes('skin') || name.includes('body') || name.includes('face'))) {
|
|
131
|
-
mat.color
|
|
132
|
+
mat.color && mat.color.set(SKIN_COLOR);
|
|
132
133
|
}
|
|
133
134
|
if (EYE_COLOR && (name.includes('eye') || name.includes('iris'))) {
|
|
134
|
-
mat.color
|
|
135
|
+
mat.color && mat.color.set(EYE_COLOR);
|
|
135
136
|
}
|
|
136
137
|
});
|
|
137
138
|
});
|
|
@@ -204,7 +205,7 @@ async function loadStaticFallback(loadedAvatarUrl) {
|
|
|
204
205
|
applyAccessories(pendingAccessoriesList);
|
|
205
206
|
motionInitFromRoot(staticModel);
|
|
206
207
|
emitLoading('ready', 100);
|
|
207
|
-
|
|
208
|
+
rnPost(JSON.stringify({ type: 'ready' }));
|
|
208
209
|
|
|
209
210
|
window.addEventListener('resize', () => {
|
|
210
211
|
camera.aspect = window.innerWidth / window.innerHeight;
|
|
@@ -228,11 +229,11 @@ async function loadStaticFallback(loadedAvatarUrl) {
|
|
|
228
229
|
}
|
|
229
230
|
}, (err) => {
|
|
230
231
|
log('Fallback Error: ' + err.message);
|
|
231
|
-
|
|
232
|
+
rnPost(JSON.stringify({ type: 'error', message: err.message }));
|
|
232
233
|
});
|
|
233
234
|
} catch (err) {
|
|
234
235
|
log('Fallback setup error: ' + err.message);
|
|
235
|
-
|
|
236
|
+
rnPost(JSON.stringify({ type: 'error', message: err.message }));
|
|
236
237
|
}
|
|
237
238
|
}
|
|
238
239
|
|
|
@@ -299,7 +300,7 @@ async function init() {
|
|
|
299
300
|
jawMorphCache = null;
|
|
300
301
|
visemeMorphCache = null;
|
|
301
302
|
rhubarbMorphCache = null;
|
|
302
|
-
head.armature
|
|
303
|
+
head.armature && head.armature.traverse(function(child) {
|
|
303
304
|
if (child.isMesh && child.morphTargetDictionary && child.morphTargetInfluences) {
|
|
304
305
|
mouthMeshes.push(child);
|
|
305
306
|
}
|
|
@@ -338,17 +339,17 @@ async function init() {
|
|
|
338
339
|
applyAccessories(pendingAccessoriesList);
|
|
339
340
|
if (head && head.armature) motionInitFromRoot(head.armature);
|
|
340
341
|
emitLoading('ready', 100);
|
|
341
|
-
|
|
342
|
+
rnPost(JSON.stringify({ type: 'ready' }));
|
|
342
343
|
}
|
|
343
344
|
} catch (err) {
|
|
344
345
|
initStarted = false; // allow retry on error
|
|
345
346
|
log('Error: ' + err.message);
|
|
346
|
-
|
|
347
|
+
rnPost(JSON.stringify({ type: 'error', message: err.message }));
|
|
347
348
|
}
|
|
348
349
|
}
|
|
349
350
|
|
|
350
351
|
function startAudioInterception() {
|
|
351
|
-
if (!head
|
|
352
|
+
if (!head || !head.audioCtx || !head.audioSpeechGainNode) return;
|
|
352
353
|
const audioCtx = head.audioCtx;
|
|
353
354
|
const gainNode = head.audioSpeechGainNode;
|
|
354
355
|
const connected = new WeakSet();
|
|
@@ -400,14 +401,14 @@ function updateVoiceMood(rawAmplitude) {
|
|
|
400
401
|
voiceMoodFramesBelow = 0;
|
|
401
402
|
if (voiceMoodCurrent !== 'excited' && voiceMoodFramesAbove >= VOICE_MOOD_FRAMES_REQUIRED) {
|
|
402
403
|
voiceMoodCurrent = 'excited';
|
|
403
|
-
|
|
404
|
+
rnPost(JSON.stringify({ type: 'voiceMood', mood: 'excited' }));
|
|
404
405
|
}
|
|
405
406
|
} else if (voiceEnergySmoothed < VOICE_ENERGY_NEUTRAL_THRESH) {
|
|
406
407
|
voiceMoodFramesBelow++;
|
|
407
408
|
voiceMoodFramesAbove = 0;
|
|
408
409
|
if (voiceMoodCurrent !== 'neutral' && voiceMoodFramesBelow >= VOICE_MOOD_FRAMES_REQUIRED * 3) {
|
|
409
410
|
voiceMoodCurrent = 'neutral';
|
|
410
|
-
|
|
411
|
+
rnPost(JSON.stringify({ type: 'voiceMood', mood: 'neutral' }));
|
|
411
412
|
}
|
|
412
413
|
}
|
|
413
414
|
}
|
|
@@ -578,7 +579,7 @@ function clearScheduledVisemes() {
|
|
|
578
579
|
for (const key of Object.keys(visemeState)) visemeState[key] = 0;
|
|
579
580
|
}
|
|
580
581
|
|
|
581
|
-
function tickVisemeDecay(deltaSeconds
|
|
582
|
+
function tickVisemeDecay(deltaSeconds) {
|
|
582
583
|
if (!visemeMorphCache) return;
|
|
583
584
|
|
|
584
585
|
const isScheduled = Date.now() < visemeModeUntil;
|
|
@@ -614,7 +615,7 @@ function tickVisemeDecay(deltaSeconds?: number) {
|
|
|
614
615
|
// Use realtime (not newvalue) — newvalue is consumed and cleared after
|
|
615
616
|
// a single frame, so scheduled visemes would vanish immediately.
|
|
616
617
|
// realtime persists until explicitly set to null.
|
|
617
|
-
if (head
|
|
618
|
+
if (head && head.mtAvatar && e.morphName && head.mtAvatar[e.morphName]) {
|
|
618
619
|
const mt = head.mtAvatar[e.morphName];
|
|
619
620
|
mt.realtime = targetWeight > 0 ? targetWeight : null;
|
|
620
621
|
mt.needsUpdate = true;
|
|
@@ -798,7 +799,7 @@ async function applyAccessories(accessoriesList) {
|
|
|
798
799
|
if (loadedUrl.startsWith('blob:')) URL.revokeObjectURL(loadedUrl);
|
|
799
800
|
log('[ACC] GLB loaded OK for ' + accData.id);
|
|
800
801
|
const model = gltf.scene;
|
|
801
|
-
const latestData = currentAccessories[accData.id]
|
|
802
|
+
const latestData = (currentAccessories[accData.id] && currentAccessories[accData.id].latestData) || accData;
|
|
802
803
|
let targetBone = null;
|
|
803
804
|
let prefixCandidate = null;
|
|
804
805
|
root.traverse((child) => {
|
|
@@ -1158,7 +1159,7 @@ function onIncomingMessage(event) {
|
|
|
1158
1159
|
if (typeof window.playMotion === 'function') {
|
|
1159
1160
|
window.playMotion(msg.name);
|
|
1160
1161
|
}
|
|
1161
|
-
|
|
1162
|
+
rnPost(JSON.stringify({ type: 'avatarState', state: 'motion:' + msg.name }));
|
|
1162
1163
|
log('motion dispatched: ' + msg.name);
|
|
1163
1164
|
}
|
|
1164
1165
|
} catch (err) {
|
package/dist/wgpu/WgpuAvatar.js
CHANGED
|
@@ -232,7 +232,7 @@ exports.WgpuAvatar = (0, react_1.forwardRef)(({ style, avatarUrl, focalLength, m
|
|
|
232
232
|
(0, react_1.useEffect)(() => () => clearScheduleTimers(), [clearScheduleTimers]);
|
|
233
233
|
(0, react_1.useImperativeHandle)(ref, () => ({
|
|
234
234
|
setMood: (m) => { morphStateRef.current.moodBase = { ...(morphTables_1.MOOD_MORPHS[m] ?? {}) }; },
|
|
235
|
-
sendAmplitude: (
|
|
235
|
+
sendAmplitude: () => { },
|
|
236
236
|
sendViseme: (viseme, weight) => {
|
|
237
237
|
const aliases = morphTables_1.VISEME_MORPH_ALIASES[viseme];
|
|
238
238
|
if (!aliases)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "talking-head-studio",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.8",
|
|
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",
|