@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sage-rsc/talking-head-react",
3
- "version": "1.7.5",
3
+ "version": "1.7.7",
4
4
  "description": "A reusable React component for 3D talking avatars with lip-sync and text-to-speech",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",
@@ -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.onSpeechEnd - Callback when speech ends
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
- try {
120
- const manifestAnimations = await loadAnimationsFromManifest(animations.manifest);
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
- } catch (error) {
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
- console.log(`📁 Loading animations from folders with genderKey="${genderKey}" for avatarBody="${avatarBody}"`);
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
- console.log(`❌ No animations found for "${groupName}"`);
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: remainingText || null,
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
- const manifest = await response.json();
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
- console.error('Failed to load animation manifest:', error);
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
  }