@sage-rsc/talking-head-react 1.7.5 → 1.7.7
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/index.cjs +5 -5
- package/dist/index.js +1051 -1031
- package/package.json +1 -1
- package/src/components/SimpleTalkingAvatar.jsx +59 -18
- package/src/utils/animationLoader.js +24 -2
package/package.json
CHANGED
|
@@ -25,7 +25,8 @@ import { loadAnimationsFromManifest, autoLoadAnimationsFromFolders } from '../ut
|
|
|
25
25
|
* @param {Function} props.onReady - Callback when avatar is ready
|
|
26
26
|
* @param {Function} props.onLoading - Callback for loading progress
|
|
27
27
|
* @param {Function} props.onError - Callback for errors
|
|
28
|
-
* @param {Function} props.
|
|
28
|
+
* @param {Function} props.onSpeechStart - Callback when avatar starts speaking
|
|
29
|
+
* @param {Function} props.onSpeechEnd - Callback when avatar finishes speaking
|
|
29
30
|
* @param {string} props.className - Additional CSS classes
|
|
30
31
|
* @param {Object} props.style - Additional inline styles
|
|
31
32
|
* @param {Object} props.animations - Object mapping animation names to FBX file paths, or animation groups
|
|
@@ -53,6 +54,7 @@ const SimpleTalkingAvatar = forwardRef(({
|
|
|
53
54
|
onReady = () => {},
|
|
54
55
|
onLoading = () => {},
|
|
55
56
|
onError = () => {},
|
|
57
|
+
onSpeechStart = () => {},
|
|
56
58
|
onSpeechEnd = () => {},
|
|
57
59
|
className = "",
|
|
58
60
|
style = {},
|
|
@@ -116,11 +118,12 @@ const SimpleTalkingAvatar = forwardRef(({
|
|
|
116
118
|
|
|
117
119
|
// Option 1: Load from manifest file only
|
|
118
120
|
if (animations.manifest && !animations.auto) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
+
const manifestAnimations = await loadAnimationsFromManifest(animations.manifest);
|
|
122
|
+
// loadAnimationsFromManifest returns {} on error, so check if we got anything
|
|
123
|
+
const hasAnimations = Object.keys(manifestAnimations).length > 0;
|
|
124
|
+
if (hasAnimations) {
|
|
121
125
|
setLoadedAnimations(manifestAnimations);
|
|
122
|
-
}
|
|
123
|
-
console.error('Failed to load animation manifest:', error);
|
|
126
|
+
} else {
|
|
124
127
|
setLoadedAnimations(animations);
|
|
125
128
|
}
|
|
126
129
|
}
|
|
@@ -153,7 +156,7 @@ const SimpleTalkingAvatar = forwardRef(({
|
|
|
153
156
|
folderPaths[`shared_talking`] = `${basePath}/shared/talking`;
|
|
154
157
|
folderPaths[`shared_idle`] = `${basePath}/shared/idle`;
|
|
155
158
|
|
|
156
|
-
|
|
159
|
+
// Loading animations from folders
|
|
157
160
|
const discoveredAnimations = await autoLoadAnimationsFromFolders(folderPaths, avatarBody);
|
|
158
161
|
|
|
159
162
|
// Check if we found any animations
|
|
@@ -404,33 +407,30 @@ const SimpleTalkingAvatar = forwardRef(({
|
|
|
404
407
|
if (loadedAnimations._genderSpecific) {
|
|
405
408
|
const genderKey = getGenderKey(avatarBody);
|
|
406
409
|
|
|
407
|
-
// Debug: Log gender detection
|
|
408
|
-
console.log(`🔍 Gender detection: avatarBody="${avatarBody}" -> genderKey="${genderKey}"`);
|
|
409
|
-
|
|
410
410
|
const genderGroups = loadedAnimations._genderSpecific[genderKey];
|
|
411
411
|
|
|
412
412
|
// Try gender-specific first
|
|
413
413
|
if (genderGroups && genderGroups[groupName]) {
|
|
414
414
|
group = genderGroups[groupName];
|
|
415
|
-
console.log(`✅ Found ${genderKey} animations for "${groupName}": ${Array.isArray(group) ? group.length : 1} animation(s)`);
|
|
416
415
|
}
|
|
417
416
|
// Fallback to shared gender-specific animations
|
|
418
417
|
else if (loadedAnimations._genderSpecific.shared && loadedAnimations._genderSpecific.shared[groupName]) {
|
|
419
418
|
group = loadedAnimations._genderSpecific.shared[groupName];
|
|
420
|
-
console.log(`✅ Found shared animations for "${groupName}": ${Array.isArray(group) ? group.length : 1} animation(s)`);
|
|
421
|
-
} else {
|
|
422
|
-
console.log(`⚠️ No ${genderKey} or shared animations found for "${groupName}"`);
|
|
423
419
|
}
|
|
424
420
|
}
|
|
425
421
|
|
|
426
422
|
// Fallback to root-level animations if gender-specific not found
|
|
427
423
|
if (!group && loadedAnimations[groupName]) {
|
|
428
424
|
group = loadedAnimations[groupName];
|
|
429
|
-
console.log(`✅ Found root-level animations for "${groupName}": ${Array.isArray(group) ? group.length : 1} animation(s)`);
|
|
430
425
|
}
|
|
431
426
|
|
|
432
427
|
if (!group) {
|
|
433
|
-
|
|
428
|
+
// Only log warning if animations were actually configured (not just empty object)
|
|
429
|
+
const hasAnyAnimations = Object.keys(loadedAnimations).length > 0 ||
|
|
430
|
+
(loadedAnimations._genderSpecific && Object.keys(loadedAnimations._genderSpecific).length > 0);
|
|
431
|
+
if (hasAnyAnimations) {
|
|
432
|
+
console.warn(`⚠️ No animations found for group "${groupName}". Make sure animations are configured correctly.`);
|
|
433
|
+
}
|
|
434
434
|
return [];
|
|
435
435
|
}
|
|
436
436
|
|
|
@@ -582,6 +582,16 @@ const SimpleTalkingAvatar = forwardRef(({
|
|
|
582
582
|
playRandomAnimation(animationGroup);
|
|
583
583
|
}
|
|
584
584
|
|
|
585
|
+
// Call onSpeechStart callback
|
|
586
|
+
try {
|
|
587
|
+
onSpeechStart(textToSpeak);
|
|
588
|
+
if (options.onSpeechStart) {
|
|
589
|
+
options.onSpeechStart(textToSpeak);
|
|
590
|
+
}
|
|
591
|
+
} catch (err) {
|
|
592
|
+
console.warn('Error in onSpeechStart callback:', err);
|
|
593
|
+
}
|
|
594
|
+
|
|
585
595
|
// Reset speech progress tracking
|
|
586
596
|
speechProgressRef.current = { remainingText: null, originalText: null, options: null };
|
|
587
597
|
originalSentencesRef.current = [];
|
|
@@ -691,7 +701,7 @@ const SimpleTalkingAvatar = forwardRef(({
|
|
|
691
701
|
speechEndIntervalRef.current = null;
|
|
692
702
|
}
|
|
693
703
|
|
|
694
|
-
// Extract remaining text from speech queue
|
|
704
|
+
// Extract remaining text from speech queue (not yet sent to TTS)
|
|
695
705
|
let remainingText = '';
|
|
696
706
|
if (speechQueue.length > 0) {
|
|
697
707
|
remainingText = speechQueue.map(item => {
|
|
@@ -702,9 +712,33 @@ const SimpleTalkingAvatar = forwardRef(({
|
|
|
702
712
|
}).join(' ');
|
|
703
713
|
}
|
|
704
714
|
|
|
715
|
+
// Extract text from audio playlist (currently playing or queued audio)
|
|
716
|
+
// This includes the currently playing sentence if it was interrupted
|
|
717
|
+
let audioPlaylistText = '';
|
|
718
|
+
if (audioPlaylist.length > 0) {
|
|
719
|
+
audioPlaylistText = audioPlaylist
|
|
720
|
+
.map(item => {
|
|
721
|
+
// Try to get text from the audio item
|
|
722
|
+
if (item.text) {
|
|
723
|
+
if (Array.isArray(item.text)) {
|
|
724
|
+
return item.text.map(wordObj => wordObj.word).join(' ');
|
|
725
|
+
}
|
|
726
|
+
return item.text;
|
|
727
|
+
}
|
|
728
|
+
return '';
|
|
729
|
+
})
|
|
730
|
+
.filter(text => text.trim().length > 0)
|
|
731
|
+
.join(' ');
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Combine: if audio is playing, include that text first, then remaining queue text
|
|
735
|
+
const combinedRemainingText = audioPlaylistText
|
|
736
|
+
? (audioPlaylistText + (remainingText ? ' ' + remainingText : ''))
|
|
737
|
+
: remainingText;
|
|
738
|
+
|
|
705
739
|
// Store progress for resume
|
|
706
740
|
speechProgressRef.current = {
|
|
707
|
-
remainingText:
|
|
741
|
+
remainingText: combinedRemainingText || null,
|
|
708
742
|
originalText: pausedSpeechRef.current?.text || null,
|
|
709
743
|
options: pausedSpeechRef.current?.options || null
|
|
710
744
|
};
|
|
@@ -765,8 +799,15 @@ const SimpleTalkingAvatar = forwardRef(({
|
|
|
765
799
|
|
|
766
800
|
setIsPaused(false);
|
|
767
801
|
isPausedRef.current = false;
|
|
802
|
+
|
|
803
|
+
// Call onSpeechEnd callback when manually stopped
|
|
804
|
+
try {
|
|
805
|
+
onSpeechEnd();
|
|
806
|
+
} catch (err) {
|
|
807
|
+
console.warn('Error in onSpeechEnd callback (stopSpeaking):', err);
|
|
808
|
+
}
|
|
768
809
|
}
|
|
769
|
-
}, []);
|
|
810
|
+
}, [onSpeechEnd]);
|
|
770
811
|
|
|
771
812
|
// Expose methods via ref
|
|
772
813
|
useImperativeHandle(ref, () => ({
|
|
@@ -13,13 +13,35 @@ export async function loadAnimationsFromManifest(manifestPath) {
|
|
|
13
13
|
try {
|
|
14
14
|
const response = await fetch(manifestPath);
|
|
15
15
|
if (!response.ok) {
|
|
16
|
+
// Don't log error for 404 - manifest is optional
|
|
17
|
+
if (response.status === 404) {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
16
20
|
throw new Error(`Failed to fetch manifest: ${response.status} ${response.statusText}`);
|
|
17
21
|
}
|
|
18
|
-
|
|
22
|
+
|
|
23
|
+
// Check if response is actually JSON (not HTML error page)
|
|
24
|
+
const contentType = response.headers.get('content-type');
|
|
25
|
+
if (contentType && !contentType.includes('application/json')) {
|
|
26
|
+
// Response is not JSON (probably HTML error page)
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const text = await response.text();
|
|
31
|
+
// Try to parse JSON, but check if it looks like HTML first
|
|
32
|
+
if (text.trim().startsWith('<!')) {
|
|
33
|
+
// This is HTML, not JSON (likely a 404 page)
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const manifest = JSON.parse(text);
|
|
19
38
|
const animations = manifest.animations || {};
|
|
20
39
|
return animations;
|
|
21
40
|
} catch (error) {
|
|
22
|
-
|
|
41
|
+
// Only log if it's not a JSON parse error (which we handle above)
|
|
42
|
+
if (!(error instanceof SyntaxError)) {
|
|
43
|
+
console.warn('⚠️ Could not load animation manifest (this is optional):', manifestPath);
|
|
44
|
+
}
|
|
23
45
|
return {};
|
|
24
46
|
}
|
|
25
47
|
}
|