@sage-rsc/talking-head-react 1.2.1 → 1.2.3

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.2.1",
3
+ "version": "1.2.3",
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",
@@ -27,7 +27,10 @@ import { getActiveTTSConfig, ELEVENLABS_CONFIG, DEEPGRAM_CONFIG } from '../confi
27
27
  * @param {Function} props.onSpeechEnd - Callback when speech ends
28
28
  * @param {string} props.className - Additional CSS classes
29
29
  * @param {Object} props.style - Additional inline styles
30
- * @param {Object} props.animations - Object mapping animation names to FBX file paths
30
+ * @param {Object} props.animations - Object mapping animation names to FBX file paths, or animation groups
31
+ * Can be: { "dance": "/animations/dance.fbx" } (single animation)
32
+ * Or: { "talking": ["/animations/talk1.fbx", "/animations/talk2.fbx"] } (group)
33
+ * @param {string} props.autoAnimationGroup - Animation group to automatically play when speaking (e.g., "talking")
31
34
  * @param {boolean} props.autoSpeak - Whether to automatically speak the text prop when ready
32
35
  * @param {Object} ref - Ref to access component methods
33
36
  */
@@ -51,6 +54,7 @@ const SimpleTalkingAvatar = forwardRef(({
51
54
  className = "",
52
55
  style = {},
53
56
  animations = {},
57
+ autoAnimationGroup = null, // e.g., "talking" - will randomly select from this group when speaking
54
58
  autoSpeak = false
55
59
  }, ref) => {
56
60
  const containerRef = useRef(null);
@@ -220,6 +224,44 @@ const SimpleTalkingAvatar = forwardRef(({
220
224
  }
221
225
  }, []);
222
226
 
227
+ // Helper function to get random animation from a group
228
+ const getRandomAnimation = useCallback((groupName) => {
229
+ if (!animations || !animations[groupName]) {
230
+ return null;
231
+ }
232
+
233
+ const group = animations[groupName];
234
+
235
+ // If it's an array, randomly select one
236
+ if (Array.isArray(group) && group.length > 0) {
237
+ const randomIndex = Math.floor(Math.random() * group.length);
238
+ return group[randomIndex];
239
+ }
240
+
241
+ // If it's a string, return it directly
242
+ if (typeof group === 'string') {
243
+ return group;
244
+ }
245
+
246
+ return null;
247
+ }, [animations]);
248
+
249
+ // Helper function to play random animation from a group
250
+ const playRandomAnimation = useCallback((groupName, disablePositionLock = false) => {
251
+ const animationPath = getRandomAnimation(groupName);
252
+ if (animationPath && talkingHeadRef.current) {
253
+ try {
254
+ talkingHeadRef.current.playAnimation(animationPath, null, 10, 0, 0.01, disablePositionLock);
255
+ console.log(`Playing random animation from "${groupName}" group:`, animationPath);
256
+ return animationPath;
257
+ } catch (error) {
258
+ console.warn(`Failed to play random animation from "${groupName}" group:`, error);
259
+ return null;
260
+ }
261
+ }
262
+ return null;
263
+ }, [getRandomAnimation]);
264
+
223
265
  // Speak text with proper callback handling
224
266
  const speakText = useCallback(async (textToSpeak, options = {}) => {
225
267
  if (!talkingHeadRef.current || !isReady) {
@@ -235,6 +277,13 @@ const SimpleTalkingAvatar = forwardRef(({
235
277
  // Always resume audio context first (required for user interaction)
236
278
  await resumeAudioContext();
237
279
 
280
+ // Play random animation from autoAnimationGroup if specified
281
+ // Check both autoAnimationGroup prop and options.animationGroup
282
+ const animationGroup = options.animationGroup || autoAnimationGroup;
283
+ if (animationGroup && !options.skipAnimation) {
284
+ playRandomAnimation(animationGroup);
285
+ }
286
+
238
287
  // Reset speech progress tracking
239
288
  speechProgressRef.current = { remainingText: null, originalText: null, options: null };
240
289
  originalSentencesRef.current = [];
@@ -279,7 +328,7 @@ const SimpleTalkingAvatar = forwardRef(({
279
328
  console.error('Error speaking text:', err);
280
329
  setError(err.message || 'Failed to speak text');
281
330
  }
282
- }, [isReady, onSpeechEnd, resumeAudioContext]);
331
+ }, [isReady, onSpeechEnd, resumeAudioContext, autoAnimationGroup, playRandomAnimation]);
283
332
 
284
333
  // Auto-speak text when ready and autoSpeak is true
285
334
  useEffect(() => {
@@ -394,6 +443,12 @@ const SimpleTalkingAvatar = forwardRef(({
394
443
  talkingHeadRef.current.playAnimation(animationName, null, 10, 0, 0.01, disablePositionLock);
395
444
  }
396
445
  },
446
+ playRandomAnimation: (groupName, disablePositionLock = false) => {
447
+ return playRandomAnimation(groupName, disablePositionLock);
448
+ },
449
+ getRandomAnimation: (groupName) => {
450
+ return getRandomAnimation(groupName);
451
+ },
397
452
  playReaction: (reactionType) => talkingHeadRef.current?.playReaction(reactionType),
398
453
  playCelebration: () => talkingHeadRef.current?.playCelebration(),
399
454
  setShowFullAvatar: (show) => {
@@ -5663,58 +5663,9 @@ class TalkingHead {
5663
5663
  const newTrack = track.clone();
5664
5664
  newTrack.name = newTrackName;
5665
5665
 
5666
- // Fix rotations for Ready Player Me arm bones (coordinate system correction)
5667
- // Ready Player Me uses a different coordinate system, so we need to adjust rotations
5668
- const isArmBone = mappedBoneName.includes('Arm') || mappedBoneName.includes('Hand') || mappedBoneName.includes('Shoulder');
5669
- const isLeftSide = mappedBoneName.includes('Left');
5670
- const isRightSide = mappedBoneName.includes('Right');
5671
- const isForearm = mappedBoneName.includes('ForeArm') || mappedBoneName.includes('Forearm');
5672
- const isHand = mappedBoneName.includes('Hand') && !mappedBoneName.includes('Index') &&
5673
- !mappedBoneName.includes('Thumb') && !mappedBoneName.includes('Middle') &&
5674
- !mappedBoneName.includes('Ring') && !mappedBoneName.includes('Pinky');
5675
-
5676
- if (isArmBone && (property === 'quaternion' || property === 'rotation')) {
5677
- if (property === 'quaternion' && newTrack.values && newTrack.values.length >= 4) {
5678
- // Adjust quaternion rotations for Ready Player Me coordinate system
5679
- const numKeyframes = newTrack.times.length;
5680
- for (let i = 0; i < numKeyframes; i++) {
5681
- const baseIdx = i * 4;
5682
- if (baseIdx + 3 < newTrack.values.length) {
5683
- let x = newTrack.values[baseIdx];
5684
- let y = newTrack.values[baseIdx + 1];
5685
- let z = newTrack.values[baseIdx + 2];
5686
- let w = newTrack.values[baseIdx + 3];
5687
-
5688
- let q = new THREE.Quaternion(x, y, z, w);
5689
-
5690
- // For Ready Player Me, left side bones may need coordinate system correction
5691
- // Try multiple approaches: invert quaternion or rotate around different axes
5692
- if (isLeftSide) {
5693
- if (isHand) {
5694
- // Left hand: Try inverting the quaternion (conjugate) to flip orientation
5695
- // This might fix the "folding behind" issue
5696
- q.conjugate();
5697
- // Then rotate 180 degrees around Y axis
5698
- const flipY = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI);
5699
- q = flipY.multiply(q);
5700
- } else if (isForearm) {
5701
- // Left forearm: Try rotating 180 degrees around Y axis first
5702
- const flipY = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI);
5703
- q = flipY.multiply(q);
5704
- // Then rotate 90 degrees around X axis
5705
- const adjustX = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), Math.PI / 2);
5706
- q = adjustX.multiply(q);
5707
- }
5708
- }
5709
-
5710
- newTrack.values[baseIdx] = q.x;
5711
- newTrack.values[baseIdx + 1] = q.y;
5712
- newTrack.values[baseIdx + 2] = q.z;
5713
- newTrack.values[baseIdx + 3] = q.w;
5714
- }
5715
- }
5716
- }
5717
- }
5666
+ // Note: Rotation corrections removed - they were causing issues with both arms
5667
+ // If left arm still has issues, it's likely a bone mapping problem, not rotation
5668
+ // Focus on getting bone names mapped correctly first
5718
5669
 
5719
5670
  mappedTracks.push(newTrack);
5720
5671