@sage-rsc/talking-head-react 1.6.6 → 1.6.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 CHANGED
@@ -4,7 +4,7 @@
4
4
  ${e}
5
5
  </voice>
6
6
  </speak>
7
- `,s=await fetch(this.opt.ttsEndpoint,{method:"POST",headers:{"Ocp-Apim-Subscription-Key":this.opt.ttsApikey,"Content-Type":"application/ssml+xml","X-Microsoft-OutputFormat":"audio-16khz-128kbitrate-mono-mp3"},body:o});if(!s.ok)throw new Error(`Azure TTS error: ${s.status} ${s.statusText}`);const i=await s.arrayBuffer(),a=await this.audioCtx.decodeAudioData(i),c=await this.audioAnalyzer.analyzeAudio(a,e),l=[];for(let r=0;r<c.visemes.length;r++){const d=c.visemes[r],h=d.startTime*1e3,g=d.duration*1e3,v=d.intensity;l.push({template:{name:"viseme"},ts:[h-Math.min(60,2*g/3),h+Math.min(25,g/2),h+g+Math.min(60,g/2)],vs:{["viseme_"+d.viseme]:[null,v,0]}})}const u=[...t.anim,...l];this.audioPlaylist.push({anim:u,audio:a}),this.onSubtitles=t.onSubtitles||null,this.resetLips(),t.mood&&this.setMood(t.mood),this.playAudio()}async synthesizeWithExternalTTS(t){let e="<speak>";t.text.forEach((i,a)=>{a>0&&(e+=" <mark name='"+i.mark+"'/>"),e+=i.word.replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;").replaceAll("'","&apos;").replace(new RegExp("^\\p{Dash_Punctuation}$","ug"),'<break time="750ms"/>')}),e+="</speak>";const n={method:"POST",headers:{"Content-Type":"application/json; charset=utf-8"},body:JSON.stringify({input:{ssml:e},voice:{languageCode:t.lang||this.avatar.ttsLang||this.opt.ttsLang,name:t.voice||this.avatar.ttsVoice||this.opt.ttsVoice},audioConfig:{audioEncoding:this.ttsAudioEncoding,speakingRate:(t.rate||this.avatar.ttsRate||this.opt.ttsRate)+this.mood.speech.deltaRate,pitch:(t.pitch||this.avatar.ttsPitch||this.opt.ttsPitch)+this.mood.speech.deltaPitch,volumeGainDb:(t.volume||this.avatar.ttsVolume||this.opt.ttsVolume)+this.mood.speech.deltaVolume},enableTimePointing:[1]})};this.opt.jwtGet&&typeof this.opt.jwtGet=="function"&&(n.headers.Authorization="Bearer "+await this.opt.jwtGet());const o=await fetch(this.opt.ttsEndpoint+(this.opt.ttsApikey?"?key="+this.opt.ttsApikey:""),n),s=await o.json();if(o.status===200&&s&&s.audioContent){const i=this.b64ToArrayBuffer(s.audioContent),a=await this.audioCtx.decodeAudioData(i);this.speakWithHands();const c=[0];let l=0;t.text.forEach((d,h)=>{if(h>0){let g=c[c.length-1];s.timepoints[l]&&(g=s.timepoints[l].timeSeconds*1e3,s.timepoints[l].markName===""+d.mark&&l++),c.push(g)}});const u=[{mark:0,time:0}];c.forEach((d,h)=>{if(h>0){let g=d-c[h-1];u[h-1].duration=g,u.push({mark:h,time:d})}});let r=1e3*a.duration;r>this.opt.ttsTrimEnd&&(r=r-this.opt.ttsTrimEnd),u[u.length-1].duration=r-u[u.length-1].time,t.anim.forEach(d=>{const h=u[d.mark];if(h)for(let g=0;g<d.ts.length;g++)d.ts[g]=h.time+d.ts[g]*h.duration+this.opt.ttsTrimStart}),this.audioPlaylist.push({anim:t.anim,audio:a}),this.onSubtitles=t.onSubtitles||null,this.resetLips(),t.mood&&this.setMood(t.mood),this.playAudio()}else this.startSpeaking(!0)}async startSpeaking(t=!1){if(!(!this.armature||this.isSpeaking&&!t))if(this.stateName="speaking",this.isSpeaking=!0,this.speechQueue.length){let e=this.speechQueue.shift();if(e.emoji){this.lookAtCamera(500);let n=e.emoji.dt.reduce((o,s)=>o+s,0);this.animQueue.push(this.animFactory(e.emoji)),setTimeout(this.startSpeaking.bind(this),n,!0)}else if(e.break)setTimeout(this.startSpeaking.bind(this),e.break,!0);else if(e.audio)e.isRaw||(this.lookAtCamera(500),this.speakWithHands(),this.resetLips()),this.audioPlaylist.push({anim:e.anim,audio:e.audio,isRaw:e.isRaw}),this.onSubtitles=e.onSubtitles||null,e.mood&&this.setMood(e.mood),this.playAudio();else if(e.text){this.lookAtCamera(500);try{!this.opt.ttsEndpoint||this.opt.ttsEndpoint===""?await this.synthesizeWithBrowserTTS(e):this.opt.ttsService==="elevenlabs"?await this.synthesizeWithElevenLabsTTS(e):this.opt.ttsService==="deepgram"?await this.synthesizeWithDeepgramTTS(e):this.opt.ttsService==="azure"?await this.synthesizeWithAzureTTS(e):await this.synthesizeWithExternalTTS(e)}catch(n){console.error("Error:",n),this.startSpeaking(!0)}}else e.anim?(this.onSubtitles=e.onSubtitles||null,this.resetLips(),e.mood&&this.setMood(e.mood),e.anim.forEach((n,o)=>{for(let s=0;s<n.ts.length;s++)n.ts[s]=this.animClock+10*o;this.animQueue.push(n)}),setTimeout(this.startSpeaking.bind(this),10*e.anim.length,!0)):e.marker?(typeof e.marker=="function"&&e.marker(),this.startSpeaking(!0)):this.startSpeaking(!0)}else this.stateName="idle",this.isSpeaking=!1}pauseSpeaking(){try{this.audioSpeechSource.stop()}catch{}this.audioPlaylist.length=0,this.stateName="idle",this.isSpeaking=!1,this.isAudioPlaying=!1,this.animQueue=this.animQueue.filter(t=>t.template.name!=="viseme"&&t.template.name!=="subtitles"&&t.template.name!=="blendshapes"),this.armature&&(this.resetLips(),this.render())}stopSpeaking(){try{this.audioSpeechSource.stop()}catch{}this.audioPlaylist.length=0,this.speechQueue.length=0,this.animQueue=this.animQueue.filter(t=>t.template.name!=="viseme"&&t.template.name!=="subtitles"&&t.template.name!=="blendshapes"),this.stateName="idle",this.isSpeaking=!1,this.isAudioPlaying=!1,this.armature&&(this.resetLips(),this.render())}async streamStart(t={},e=null,n=null,o=null,s=null){if(this.stopSpeaking(),this.isStreaming=!0,t.waitForAudioChunks!==void 0&&(this.streamWaitForAudioChunks=t.waitForAudioChunks),this.streamWaitForAudioChunks||(this.streamAudioStartTime=this.animClock),this.streamLipsyncQueue=[],this.streamLipsyncType=t.lipsyncType||this.streamLipsyncType||"visemes",this.streamLipsyncLang=t.lipsyncLang||this.streamLipsyncLang||this.avatar.lipsyncLang||this.opt.lipsyncLang,this.onAudioStart=e,this.onAudioEnd=n,this.onMetrics=s,t.sampleRate!==void 0){const a=t.sampleRate;typeof a=="number"&&a>=8e3&&a<=96e3?a!==this.audioCtx.sampleRate&&this.initAudioGraph(a):console.warn("Invalid sampleRate provided. It must be a number between 8000 and 96000 Hz.")}if(t.gain!==void 0&&(this.audioStreamGainNode.gain.value=t.gain),!this.streamWorkletNode||!this.streamWorkletNode.port||this.streamWorkletNode.numberOfOutputs===0||this.streamWorkletNode.context!==this.audioCtx){if(this.streamWorkletNode)try{this.streamWorkletNode.disconnect(),this.streamWorkletNode=null}catch{}if(!this.workletLoaded)try{const a=this.audioCtx.audioWorklet.addModule(At.href),c=new Promise((l,u)=>setTimeout(()=>u(new Error("Worklet loading timed out")),5e3));await Promise.race([a,c]),this.workletLoaded=!0}catch(a){throw console.error("Failed to load audio worklet:",a),new Error("Failed to initialize streaming speech")}this.streamWorkletNode=new AudioWorkletNode(this.audioCtx,"playback-worklet",{processorOptions:{sampleRate:this.audioCtx.sampleRate,metrics:t.metrics||{enabled:!1}}}),this.streamWorkletNode.connect(this.audioStreamGainNode),this.streamWorkletNode.connect(this.audioAnalyzerNode),this.streamWorkletNode.port.onmessage=a=>{if(a.data.type==="playback-started"&&(this.isSpeaking=!0,this.stateName="speaking",this.streamWaitForAudioChunks&&(this.streamAudioStartTime=this.animClock),this._processStreamLipsyncQueue(),this.speakWithHands(),this.onAudioStart))try{this.onAudioStart?.()}catch(c){console.error(c)}if(a.data.type==="playback-ended"&&(this._streamPause(),this.onAudioEnd))try{this.onAudioEnd()}catch{}if(this.onMetrics&&a.data.type==="metrics")try{this.onMetrics(a.data)}catch{}}}if(t.metrics)try{this.streamWorkletNode.port.postMessage({type:"config-metrics",data:t.metrics})}catch{}if(this.resetLips(),this.lookAtCamera(500),t.mood&&this.setMood(t.mood),this.onSubtitles=o||null,this.audioCtx.state==="suspended"||this.audioCtx.state==="interrupted"){const a=this.audioCtx.resume(),c=new Promise((l,u)=>setTimeout(()=>u("p2"),1e3));try{await Promise.race([a,c])}catch{console.warn("Can't play audio. Web Audio API suspended. This is often due to calling some speak method before the first user action, which is typically prevented by the browser.");return}}}streamNotifyEnd(){!this.isStreaming||!this.streamWorkletNode||this.streamWorkletNode.port.postMessage({type:"no-more-data"})}streamInterrupt(){if(!this.isStreaming)return;const t=this.isSpeaking;if(this.streamWorkletNode)try{this.streamWorkletNode.port.postMessage({type:"stop"})}catch{}if(this._streamPause(!0),t&&this.onAudioEnd)try{this.onAudioEnd()}catch{}}streamStop(){if(this.isStreaming){if(this.streamInterrupt(),this.streamWorkletNode){try{this.streamWorkletNode.disconnect()}catch{}this.streamWorkletNode=null}this.isStreaming=!1}}_streamPause(t=!1){this.isSpeaking=!1,this.stateName="idle",t&&(this.streamWaitForAudioChunks&&(this.streamAudioStartTime=null),this.streamLipsyncQueue=[],this.animQueue=this.animQueue.filter(e=>e.template.name!=="viseme"&&e.template.name!=="subtitles"&&e.template.name!=="blendshapes"),this.armature&&(this.resetLips(),this.render()))}_processStreamLipsyncQueue(){if(this.isStreaming)for(;this.streamLipsyncQueue.length>0;){const t=this.streamLipsyncQueue.shift();this._processLipsyncData(t,this.streamAudioStartTime)}}_processLipsyncData(t,e){if(this.isStreaming){if(t.visemes&&this.streamLipsyncType=="visemes")for(let n=0;n<t.visemes.length;n++){const o=t.visemes[n],s=e+t.vtimes[n],i=t.vdurations[n],a={template:{name:"viseme"},ts:[s-2*i/3,s+i/2,s+i+i/2],vs:{["viseme_"+o]:[null,o==="PP"||o==="FF"?.9:.6,0]}};this.animQueue.push(a)}if(t.words&&(this.onSubtitles||this.streamLipsyncType=="words"))for(let n=0;n<t.words.length;n++){const o=t.words[n],s=t.wtimes[n];let i=t.wdurations[n];if(o.length&&(this.onSubtitles&&this.animQueue.push({template:{name:"subtitles"},ts:[e+s],vs:{subtitles:[" "+o]}}),this.streamLipsyncType=="words")){const a=this.streamLipsyncLang||this.avatar.lipsyncLang||this.opt.lipsyncLang,c=this.lipsyncPreProcessText(o,a),l=this.lipsyncWordsToVisemes(c,a);if(l&&l.visemes&&l.visemes.length){const u=l.times[l.visemes.length-1]+l.durations[l.visemes.length-1],r=Math.min(i,Math.max(0,i-l.visemes.length*150));let d=.6+this.convertRange(r,[0,i],[0,.4]);if(i=Math.min(i,l.visemes.length*200),u>0)for(let h=0;h<l.visemes.length;h++){const g=e+s+l.times[h]/u*i,v=l.durations[h]/u*i;this.animQueue.push({template:{name:"viseme"},ts:[g-Math.min(60,2*v/3),g+Math.min(25,v/2),g+v+Math.min(60,v/2)],vs:{["viseme_"+l.visemes[h]]:[null,l.visemes[h]==="PP"||l.visemes[h]==="FF"?.9:d,0]}})}}}}if(t.anims&&this.streamLipsyncType=="blendshapes")for(let n=0;n<t.anims.length;n++){let o=t.anims[n];o.delay+=e;let s=this.animFactory(o,!1,1,1,!0);this.animQueue.push(s)}}}streamAudio(t){if(!(!this.isStreaming||!this.streamWorkletNode)){if(this.isSpeaking||(this.streamLipsyncQueue=[],this.streamAudioStartTime=null),this.isSpeaking=!0,this.stateName="speaking",t.audio!==void 0){const e={type:"audioData",data:null};if(t.audio instanceof ArrayBuffer)e.data=t.audio,this.streamWorkletNode.port.postMessage(e,[e.data]);else if(t.audio instanceof Int16Array||t.audio instanceof Uint8Array){const n=t.audio.buffer.slice(t.audio.byteOffset,t.audio.byteOffset+t.audio.byteLength);e.data=n,this.streamWorkletNode.port.postMessage(e,[e.data])}else if(t.audio instanceof Float32Array){const n=new Int16Array(t.audio.length);for(let o=0;o<t.audio.length;o++){let s=Math.max(-1,Math.min(1,t.audio[o]));n[o]=s<0?s*32768:s*32767}e.data=n.buffer,this.streamWorkletNode.port.postMessage(e,[e.data])}else console.error("r.audio is not a supported type. Must be ArrayBuffer, Int16Array, Uint8Array, or Float32Array:",t.audio)}if(t.visemes||t.anims||t.words){if(this.streamWaitForAudioChunks&&!this.streamAudioStartTime){this.streamLipsyncQueue.length>=200&&this.streamLipsyncQueue.shift(),this.streamLipsyncQueue.push(t);return}else!this.streamWaitForAudioChunks&&!this.streamAudioStartTime&&(this.streamAudioStartTime=this.animClock);this._processLipsyncData(t,this.streamAudioStartTime)}}}makeEyeContact(t){this.animQueue.push(this.animFactory({name:"eyecontact",dt:[0,t],vs:{eyeContact:[1]}}))}lookAhead(t){if(t){let e=(Math.random()-.5)/4,n=(Math.random()-.5)/4,o=this.animQueue.findIndex(i=>i.template.name==="lookat");o!==-1&&this.animQueue.splice(o,1);const s={name:"lookat",dt:[750,t],vs:{bodyRotateX:[e],bodyRotateY:[n],eyesRotateX:[-3*e+.1],eyesRotateY:[-5*n],browInnerUp:[[0,.7]],mouthLeft:[[0,.7]],mouthRight:[[0,.7]],eyeContact:[0],headMove:[0]}};this.animQueue.push(this.animFactory(s))}}lookAtCamera(t){let e;if(this.speakTo&&(e=new f.Vector3,this.speakTo.objectLeftEye?.isObject3D?(this.speakTo.armature.objectHead,this.speakTo.objectLeftEye.updateMatrixWorld(!0),this.speakTo.objectRightEye.updateMatrixWorld(!0),we.setFromMatrixPosition(this.speakTo.objectLeftEye.matrixWorld),He.setFromMatrixPosition(this.speakTo.objectRightEye.matrixWorld),e.addVectors(we,He).divideScalar(2)):this.speakTo.isObject3D?this.speakTo.getWorldPosition(e):this.speakTo.isVector3?e.set(this.speakTo):this.speakTo.x&&this.speakTo.y&&this.speakTo.z&&e.set(this.speakTo.x,this.speakTo.y,this.speakTo.z)),!e){if(this.avatar.hasOwnProperty("avatarIgnoreCamera")){if(this.avatar.avatarIgnoreCamera){this.lookAhead(t);return}}else if(this.opt.avatarIgnoreCamera){this.lookAhead(t);return}this.lookAt(null,null,t);return}this.objectLeftEye.updateMatrixWorld(!0),this.objectRightEye.updateMatrixWorld(!0),we.setFromMatrixPosition(this.objectLeftEye.matrixWorld),He.setFromMatrixPosition(this.objectRightEye.matrixWorld),we.add(He).divideScalar(2),ne.copy(this.armature.quaternion),ne.multiply(this.poseTarget.props["Hips.quaternion"]),ne.multiply(this.poseTarget.props["Spine.quaternion"]),ne.multiply(this.poseTarget.props["Spine1.quaternion"]),ne.multiply(this.poseTarget.props["Spine2.quaternion"]),ne.multiply(this.poseTarget.props["Neck.quaternion"]),ne.multiply(this.poseTarget.props["Head.quaternion"]);const n=new f.Vector3().subVectors(e,we).normalize(),o=Math.atan2(n.x,n.z),s=Math.asin(-n.y);q.set(s,o,0,"YXZ");const a=new f.Quaternion().setFromEuler(q),c=new f.Quaternion().copy(a).multiply(ne.clone().invert());q.setFromQuaternion(c,"YXZ");let l=q.x/(40/24)+.2,u=q.y/(9/4),r=Math.min(.6,Math.max(-.3,l)),d=Math.min(.8,Math.max(-.8,u)),h=(Math.random()-.5)/4,g=(Math.random()-.5)/4;if(t){let v=this.animQueue.findIndex(S=>S.template.name==="lookat");v!==-1&&this.animQueue.splice(v,1);const R={name:"lookat",dt:[750,t],vs:{bodyRotateX:[r+h],bodyRotateY:[d+g],eyesRotateX:[-3*h+.1],eyesRotateY:[-5*g],browInnerUp:[[0,.7]],mouthLeft:[[0,.7]],mouthRight:[[0,.7]],eyeContact:[0],headMove:[0]}};this.animQueue.push(this.animFactory(R))}}lookAt(t,e,n){if(!this.camera)return;const o=this.nodeAvatar.getBoundingClientRect();this.objectLeftEye.updateMatrixWorld(!0),this.objectRightEye.updateMatrixWorld(!0);const s=new f.Vector3().setFromMatrixPosition(this.objectLeftEye.matrixWorld),i=new f.Vector3().setFromMatrixPosition(this.objectRightEye.matrixWorld),a=new f.Vector3().addVectors(s,i).divideScalar(2);a.project(this.camera);let c=(a.x+1)/2*o.width+o.left,l=-(a.y-1)/2*o.height+o.top;t===null&&(t=c),e===null&&(e=l),ne.copy(this.armature.quaternion),ne.multiply(this.poseTarget.props["Hips.quaternion"]),ne.multiply(this.poseTarget.props["Spine.quaternion"]),ne.multiply(this.poseTarget.props["Spine1.quaternion"]),ne.multiply(this.poseTarget.props["Spine2.quaternion"]),ne.multiply(this.poseTarget.props["Neck.quaternion"]),ne.multiply(this.poseTarget.props["Head.quaternion"]),q.setFromQuaternion(ne);let u=q.x/(40/24),r=q.y/(9/4),d=Math.min(.4,Math.max(-.4,this.camera.rotation.x)),h=Math.min(.4,Math.max(-.4,this.camera.rotation.y)),g=Math.max(window.innerWidth-c,c),v=Math.max(window.innerHeight-l,l),R=this.convertRange(e,[l-v,l+v],[-.3,.6])-u+d,S=this.convertRange(t,[c-g,c+g],[-.8,.8])-r+h;R=Math.min(.6,Math.max(-.3,R)),S=Math.min(.8,Math.max(-.8,S));let M=(Math.random()-.5)/4,p=(Math.random()-.5)/4;if(n){let O=this.animQueue.findIndex(y=>y.template.name==="lookat");O!==-1&&this.animQueue.splice(O,1);const P={name:"lookat",dt:[750,n],vs:{bodyRotateX:[R+M],bodyRotateY:[S+p],eyesRotateX:[-3*M+.1],eyesRotateY:[-5*p],browInnerUp:[[0,.7]],mouthLeft:[[0,.7]],mouthRight:[[0,.7]],eyeContact:[0],headMove:[0]}};this.animQueue.push(this.animFactory(P))}}touchAt(t,e){if(!this.camera)return;const n=this.nodeAvatar.getBoundingClientRect(),o=new f.Vector2((t-n.left)/n.width*2-1,-((e-n.top)/n.height)*2+1),s=new f.Raycaster;s.setFromCamera(o,this.camera);const i=s.intersectObject(this.armature);if(i.length>0){const a=i[0].point,c=new f.Vector3,l=new f.Vector3;this.objectLeftArm.getWorldPosition(c),this.objectRightArm.getWorldPosition(l);const u=c.distanceToSquared(a),r=l.distanceToSquared(a);u<r?(this.ikSolve({iterations:20,root:"LeftShoulder",effector:"LeftHandMiddle1",links:[{link:"LeftHand",minx:-.5,maxx:.5,miny:-1,maxy:1,minz:-.5,maxz:.5,maxAngle:.1},{link:"LeftForeArm",minx:-.5,maxx:1.5,miny:-1.5,maxy:1.5,minz:-.5,maxz:3,maxAngle:.2},{link:"LeftArm",minx:-1.5,maxx:1.5,miny:0,maxy:0,minz:-1,maxz:3}]},a,!1,1e3),this.setValue("handFistLeft",0)):(this.ikSolve({iterations:20,root:"RightShoulder",effector:"RightHandMiddle1",links:[{link:"RightHand",minx:-.5,maxx:.5,miny:-1,maxy:1,minz:-.5,maxz:.5,maxAngle:.1},{link:"RightForeArm",minx:-.5,maxx:1.5,miny:-1.5,maxy:1.5,minz:-3,maxz:.5,maxAngle:.2},{link:"RightArm",minx:-1.5,maxx:1.5,miny:0,maxy:0,minz:-1,maxz:3}]},a,!1,1e3),this.setValue("handFistRight",0))}else["LeftArm","LeftForeArm","LeftHand","RightArm","RightForeArm","RightHand"].forEach(a=>{let c=a+".quaternion";this.poseTarget.props[c].copy(this.getPoseTemplateProp(c)),this.poseTarget.props[c].t=this.animClock,this.poseTarget.props[c].d=1e3});return i.length>0}speakWithHands(t=0,e=.5){if(this.mixer||this.gesture||!this.poseTarget.template.standing||this.poseTarget.template.bend||Math.random()>e)return;this.ikSolve({root:"LeftShoulder",effector:"LeftHandMiddle1",links:[{link:"LeftHand",minx:-.5,maxx:.5,miny:-1,maxy:1,minz:-.5,maxz:.5},{link:"LeftForeArm",minx:-.5,maxx:1.5,miny:-1.5,maxy:1.5,minz:-.5,maxz:3},{link:"LeftArm",minx:-1.5,maxx:1.5,miny:-1.5,maxy:1.5,minz:-1,maxz:3}]},new f.Vector3(this.gaussianRandom(0,.5),this.gaussianRandom(-.8,-.2),this.gaussianRandom(0,.5)),!0),this.ikSolve({root:"RightShoulder",effector:"RightHandMiddle1",links:[{link:"RightHand",minx:-.5,maxx:.5,miny:-1,maxy:1,minz:-.5,maxz:.5},{link:"RightForeArm",minx:-.5,maxx:1.5,miny:-1.5,maxy:1.5,minz:-3,maxz:.5},{link:"RightArm"}]},new f.Vector3(this.gaussianRandom(-.5,0),this.gaussianRandom(-.8,-.2),this.gaussianRandom(0,.5)),!0);const n=[],o=[];n.push(100+Math.round(Math.random()*500)),o.push({duration:1e3,props:{"LeftHand.quaternion":new f.Quaternion().setFromEuler(new f.Euler(0,-1-Math.random(),0)),"RightHand.quaternion":new f.Quaternion().setFromEuler(new f.Euler(0,1+Math.random(),0))}}),["LeftArm","LeftForeArm","RightArm","RightForeArm"].forEach(i=>{o[0].props[i+".quaternion"]=this.ikMesh.getObjectByName(i).quaternion.clone()}),n.push(1e3+Math.round(Math.random()*500)),o.push({duration:2e3,props:{}}),["LeftArm","LeftForeArm","RightArm","RightForeArm","LeftHand","RightHand"].forEach(i=>{o[1].props[i+".quaternion"]=null});const s=this.animFactory({name:"talkinghands",delay:t,dt:n,vs:{moveto:o}});this.animQueue.push(s)}getSlowdownRate(t){return this.animSlowdownRate}setSlowdownRate(t){this.animSlowdownRate=t,this.audioSpeechSource.playbackRate.value=1/this.animSlowdownRate,this.audioBackgroundSource.playbackRate.value=1/this.animSlowdownRate}getAutoRotateSpeed(t){return this.controls.autoRotateSpeed}setAutoRotateSpeed(t){this.controls.autoRotateSpeed=t,this.controls.autoRotate=t>0}start(){this.armature&&this.isRunning===!1&&(this.audioCtx.resume(),this.animTimeLast=performance.now(),this.isRunning=!0,this.isAvatarOnly||requestAnimationFrame(this.animate.bind(this)))}stop(){this.isRunning=!1,this.audioCtx.suspend()}startListening(t,e={},n=null){this.listeningAnalyzer=t,this.listeningAnalyzer.fftSize=256,this.listeningAnalyzer.smoothingTimeConstant=.1,this.listeningAnalyzer.minDecibels=-70,this.listeningAnalyzer.maxDecibels=-10,this.listeningOnchange=n&&typeof n=="function"?n:null,this.listeningSilenceThresholdLevel=e?.hasOwnProperty("listeningSilenceThresholdLevel")?e.listeningSilenceThresholdLevel:this.opt.listeningSilenceThresholdLevel,this.listeningSilenceThresholdMs=e?.hasOwnProperty("listeningSilenceThresholdMs")?e.listeningSilenceThresholdMs:this.opt.listeningSilenceThresholdMs,this.listeningSilenceDurationMax=e?.hasOwnProperty("listeningSilenceDurationMax")?e.listeningSilenceDurationMax:this.opt.listeningSilenceDurationMax,this.listeningActiveThresholdLevel=e?.hasOwnProperty("listeningActiveThresholdLevel")?e.listeningActiveThresholdLevel:this.opt.listeningActiveThresholdLevel,this.listeningActiveThresholdMs=e?.hasOwnProperty("listeningActiveThresholdMs")?e.listeningActiveThresholdMs:this.opt.listeningActiveThresholdMs,this.listeningActiveDurationMax=e?.hasOwnProperty("listeningActiveDurationMax")?e.listeningActiveDurationMax:this.opt.listeningActiveDurationMax,this.listeningActive=!1,this.listeningVolume=0,this.listeningTimer=0,this.listeningTimerTotal=0,this.isListening=!0}stopListening(){this.isListening=!1}async playAnimation(t,e=null,n=10,o=0,s=.01,i=!1,a=null){if(!this.armature)return;this.positionWasLocked=!i,i||this.lockAvatarPosition();let c=this.animClips.find(l=>l.url===t+"-"+o);if(c){let l=this.animQueue.find(h=>h.template.name==="pose");l&&(l.ts[0]=1/0),Object.entries(c.pose.props).forEach(h=>{this.poseBase.props[h[0]]=h[1].clone(),this.poseTarget.props[h[0]]=h[1].clone(),this.poseTarget.props[h[0]].t=0,this.poseTarget.props[h[0]].d=1e3}),this.mixer||(this.mixer=new f.AnimationMixer(this.armature)),this.animationFinishedCallback=a;const u=()=>{this.animationFinishedCallback&&(this.animationFinishedCallback(),this.animationFinishedCallback=null),this.stopAnimation()};this.mixer.addEventListener("finished",u,{once:!0});const r=Math.ceil(n/c.clip.duration),d=this.mixer.clipAction(c.clip);if(d.setLoop(f.LoopRepeat,r),d.clampWhenFinished=!0,this.currentFBXAction&&this.currentFBXAction.isRunning()){this.currentFBXAction.fadeOut(.3),setTimeout(()=>{this.currentFBXAction=d;try{d.fadeIn(.5).play(),console.log("FBX animation started successfully (with fade transition):",t)}catch(h){console.warn("FBX animation failed to start:",h),this.stopAnimation()}},300);return}this.currentFBXAction=d;try{d.fadeIn(.5).play(),console.log("FBX animation started successfully:",t)}catch(h){console.warn("FBX animation failed to start:",h),this.stopAnimation();return}if(d.getClip().tracks.length===0){console.warn("FBX animation has no valid tracks, stopping"),this.stopAnimation();return}}else{if(t.split(".").pop().toLowerCase()!=="fbx"){console.error(`Invalid file type for FBX animation: ${t}. Expected .fbx file.`);return}let u=!1;try{const h=await fetch(t,{method:"HEAD"});if(u=h.ok,!u){console.error(`FBX file not found at ${t}. Status: ${h.status}`),console.error("Please check:"),console.error("1. File path is correct (note: path is case-sensitive)"),console.error("2. File exists in your public folder"),console.error("3. File is accessible (not blocked by server)");return}}catch(h){console.warn(`Could not verify file existence for ${t}, attempting to load anyway:`,h)}const r=new je.FBXLoader;let d;try{d=await r.loadAsync(t,e)}catch(h){console.error(`Failed to load FBX animation from ${t}:`,h),console.error("Error details:",{message:h.message,url:t,suggestion:"Make sure the file is a valid FBX file and the path is correct"}),h.message&&h.message.includes("version number")&&(console.error("FBX Loader Error: Cannot find version number"),console.error("This error usually means:"),console.error("1. The file is not a valid FBX file (might be GLB, corrupted, or wrong format)"),console.error("2. The file might be corrupted"),console.error("3. The file path might be incorrect"),console.error("4. The server returned an HTML error page instead of the FBX file"),console.error("5. The file might not exist at that path"),console.error(""),console.error("Solution: Please verify:"),console.error(` - File exists at: ${t}`),console.error(" - File is a valid FBX binary file"),console.error(" - File path matches your public folder structure"),console.error(" - File is not corrupted"));try{const g=await fetch(t),v=g.headers.get("content-type"),R=await g.text();console.error("Response details:",{status:g.status,contentType:v,firstBytes:R.substring(0,100),isHTML:R.trim().startsWith("<!DOCTYPE")||R.trim().startsWith("<html")}),(R.trim().startsWith("<!DOCTYPE")||R.trim().startsWith("<html"))&&console.error("The server returned an HTML page instead of an FBX file. The file path is likely incorrect.")}catch(g){console.error("Could not fetch file for debugging:",g)}return}if(d&&d.animations&&d.animations[o]){let h=d.animations[o];const g=new Set;this.armature&&this.armature.traverse(y=>{(y.isBone||y.type==="Bone")&&g.add(y.name)});const v=new Map,R=y=>{if(g.has(y))return y;let E=y.replace(/^mixamorig/i,"").replace(/^CC_Base_/i,"").replace(/^RPM_/i,"");if(g.has(E))return E;const b=E.toLowerCase();if(b.includes("left")&&b.includes("arm")){if(b.includes("fore")||b.includes("lower")){if(g.has("LeftForeArm"))return"LeftForeArm";if(g.has("LeftForearm"))return"LeftForearm"}else if(!b.includes("fore")&&!b.includes("hand")&&g.has("LeftArm"))return"LeftArm"}if(b.includes("right")&&b.includes("arm")){if(b.includes("fore")||b.includes("lower")){if(g.has("RightForeArm"))return"RightForeArm";if(g.has("RightForearm"))return"RightForearm"}else if(!b.includes("fore")&&!b.includes("hand")&&g.has("RightArm"))return"RightArm"}if(b.includes("left")&&b.includes("hand")&&!b.includes("index")&&!b.includes("thumb")&&!b.includes("middle")&&!b.includes("ring")&&!b.includes("pinky")&&g.has("LeftHand"))return"LeftHand";if(b.includes("right")&&b.includes("hand")&&!b.includes("index")&&!b.includes("thumb")&&!b.includes("middle")&&!b.includes("ring")&&!b.includes("pinky")&&g.has("RightHand"))return"RightHand";if(b.includes("left")&&(b.includes("shoulder")||b.includes("clavicle"))&&g.has("LeftShoulder"))return"LeftShoulder";if(b.includes("right")&&(b.includes("shoulder")||b.includes("clavicle"))&&g.has("RightShoulder"))return"RightShoulder";const k={LeftArm:"LeftArm",leftArm:"LeftArm",LEFTARM:"LeftArm",RightArm:"RightArm",rightArm:"RightArm",RIGHTARM:"RightArm",LeftForeArm:"LeftForeArm",leftForeArm:"LeftForeArm",leftForearm:"LeftForeArm",LeftForearm:"LeftForeArm",RightForeArm:"RightForeArm",rightForeArm:"RightForeArm",rightForearm:"RightForeArm",RightForearm:"RightForeArm",LeftHand:"LeftHand",leftHand:"LeftHand",RightHand:"RightHand",rightHand:"RightHand",LeftShoulder:"LeftShoulder",leftShoulder:"LeftShoulder",RightShoulder:"RightShoulder",rightShoulder:"RightShoulder",Spine:"Spine1",spine:"Spine1",Spine1:"Spine1",Spine2:"Spine2",Head:"Head",head:"Head",Neck:"Neck",neck:"Neck",Hips:"Hips",hips:"Hips",Root:"Hips",root:"Hips"};if(k[E]){const T=k[E];if(g.has(T))return T}for(const T of g)if(T.toLowerCase()===b)return T;for(const T of g){const A=T.toLowerCase();if((b.includes("left")&&A.includes("left")||b.includes("right")&&A.includes("right"))&&(b.includes("arm")&&A.includes("arm")&&!A.includes("fore")||b.includes("forearm")&&A.includes("forearm")||b.includes("hand")&&A.includes("hand")&&!A.includes("index")&&!A.includes("thumb")||b.includes("shoulder")&&A.includes("shoulder")))return T}return null},S=new Set;h.tracks.forEach(y=>{const E=y.name.split(".");S.add(E[0])}),Array.from(S).filter(y=>y.toLowerCase().includes("arm")||y.toLowerCase().includes("hand")||y.toLowerCase().includes("shoulder")),Array.from(g).filter(y=>y.includes("Arm")||y.includes("Hand")||y.includes("Shoulder"));const M=[],p=new Set;h.tracks.forEach(y=>{const b=y.name.replaceAll("mixamorig","").split("."),k=b[0],T=b[1],A=R(k);if(!(A&&(A==="LeftShoulder"||A==="RightShoulder")&&(T==="quaternion"||T==="rotation")))if(A&&T){const W=`${A}.${T}`,G=y.clone();G.name=W,M.push(G),k!==A&&v.set(k,A)}else p.add(k),(k.toLowerCase().includes("arm")||k.toLowerCase().includes("hand")||k.toLowerCase().includes("shoulder"))&&console.warn(`⚠️ Arm bone "${k}" could not be mapped to avatar skeleton`)}),M.length>0?h=new f.AnimationClip(h.name,h.duration,M):console.error("No tracks could be mapped! Animation may not work correctly.");const O={};h.tracks.forEach(y=>{y.name=y.name.replaceAll("mixamorig","");const E=y.name.split(".");if(E[1]==="position"){for(let b=0;b<y.values.length;b++)y.values[b]=y.values[b]*s;O[y.name]=new f.Vector3(y.values[0],y.values[1],y.values[2])}else E[1]==="quaternion"?O[y.name]=new f.Quaternion(y.values[0],y.values[1],y.values[2],y.values[3]):E[1]==="rotation"&&(O[E[0]+".quaternion"]=new f.Quaternion().setFromEuler(new f.Euler(y.values[0],y.values[1],y.values[2],"XYZ")).normalize())});const P={props:O};O["Hips.position"]&&(O["Hips.position"].y<.5?P.lying=!0:P.standing=!0),this.animClips.push({url:t+"-"+o,clip:h,pose:P}),this.playAnimation(t,e,n,o,s)}else{const h="Animation "+t+" (ndx="+o+") not found";console.error(h),d&&d.animations?console.error(`FBX file loaded but has ${d.animations.length} animation(s), requested index ${o}`):console.error(d?"FBX file loaded but contains no animations":"FBX file failed to load or is invalid")}}}stopAnimation(){if(this.currentFBXAction&&(this.currentFBXAction.stop(),this.currentFBXAction=null),this.mixer&&this.mixer._actions.length===0&&(this.mixer=null),this.positionWasLocked&&this.unlockAvatarPosition(),this.gesture)for(let[e,n]of Object.entries(this.gesture))n.t=this.animClock,n.d=1e3,this.poseTarget.props.hasOwnProperty(e)&&(this.poseTarget.props[e].copy(n),this.poseTarget.props[e].t=this.animClock,this.poseTarget.props[e].d=1e3);let t=this.animQueue.find(e=>e.template.name==="pose");t&&(t.ts[0]=this.animClock),this.setPoseFromTemplate(null)}async playPose(t,e=null,n=5,o=0,s=.01){if(!this.armature)return;let i=this.poseTemplates[t];if(!i){const a=this.animPoses.find(c=>c.url===t+"-"+o);a&&(i=a.pose)}if(i){this.poseName=t,this.mixer=null;let a=this.animQueue.find(c=>c.template.name==="pose");a&&(a.ts[0]=this.animClock+n*1e3+2e3),this.setPoseFromTemplate(i)}else{let c=await new je.FBXLoader().loadAsync(t,e);if(c&&c.animations&&c.animations[o]){let l=c.animations[o];const u={};l.tracks.forEach(d=>{d.name=d.name.replaceAll("mixamorig","");const h=d.name.split(".");h[1]==="position"?u[d.name]=new f.Vector3(d.values[0]*s,d.values[1]*s,d.values[2]*s):h[1]==="quaternion"?u[d.name]=new f.Quaternion(d.values[0],d.values[1],d.values[2],d.values[3]):h[1]==="rotation"&&(u[h[0]+".quaternion"]=new f.Quaternion().setFromEuler(new f.Euler(d.values[0],d.values[1],d.values[2],"XYZ")).normalize())});const r={props:u};u["Hips.position"]&&(u["Hips.position"].y<.5?r.lying=!0:r.standing=!0),this.animPoses.push({url:t+"-"+o,pose:r}),this.playPose(t,e,n,o,s)}else{const l="Pose "+t+" (ndx="+o+") not found";console.error(l)}}}stopPose(){this.stopAnimation()}playGesture(t,e=3,n=!1,o=1e3){if(!this.armature)return;let s=this.gestureTemplates[t];if(s){this.gestureTimeout&&(clearTimeout(this.gestureTimeout),this.gestureTimeout=null);let a=this.animQueue.findIndex(c=>c.template.name==="talkinghands");a!==-1&&(this.animQueue[a].ts=this.animQueue[a].ts.map(c=>0)),this.gesture=this.propsToThreeObjects(s),n&&(this.gesture=this.mirrorPose(this.gesture)),t==="namaste"&&this.avatar.body==="M"&&(this.gesture["RightArm.quaternion"].rotateTowards(new f.Quaternion(0,1,0,0),-.25),this.gesture["LeftArm.quaternion"].rotateTowards(new f.Quaternion(0,1,0,0),-.25));for(let[c,l]of Object.entries(this.gesture))l.t=this.animClock,l.d=o,this.poseTarget.props.hasOwnProperty(c)&&(this.poseTarget.props[c].copy(l),this.poseTarget.props[c].t=this.animClock,this.poseTarget.props[c].d=o);e&&Number.isFinite(e)&&(this.gestureTimeout=setTimeout(this.stopGesture.bind(this,o),1e3*e))}let i=this.animEmojis[t];if(i&&(i&&i.link&&(i=this.animEmojis[i.link]),i)){this.lookAtCamera(500);const a=this.animFactory(i);if(a.gesture=!0,e&&Number.isFinite(e)){const c=a.ts[0],u=a.ts[a.ts.length-1]-c;if(e*1e3-u>0){const d=[];for(let v=1;v<a.ts.length;v++)d.push(a.ts[v]-a.ts[v-1]);const h=i.template?.rescale||d.map(v=>v/u),g=e*1e3-u;a.ts=a.ts.map((v,R,S)=>R===0?c:S[R-1]+d[R-1]+h[R-1]*g)}else{const d=e*1e3/u;a.ts=a.ts.map(h=>c+d*(h-c))}}this.animQueue.push(a)}}stopGesture(t=1e3){if(this.gestureTimeout&&(clearTimeout(this.gestureTimeout),this.gestureTimeout=null),this.gesture){const n=Object.entries(this.gesture);this.gesture=null;for(const[o,s]of n)this.poseTarget.props.hasOwnProperty(o)&&(this.poseTarget.props[o].copy(this.getPoseTemplateProp(o)),this.poseTarget.props[o].t=this.animClock,this.poseTarget.props[o].d=t)}let e=this.animQueue.findIndex(n=>n.gesture);e!==-1&&this.animQueue.splice(e,1)}ikSolve(t,e=null,n=!1,o=null){const s=new f.Vector3,i=new f.Vector3,a=new f.Vector3,c=new f.Vector3,l=new f.Quaternion,u=new f.Vector3,r=new f.Vector3,d=new f.Vector3,h=this.ikMesh.getObjectByName(t.root);h.position.setFromMatrixPosition(this.armature.getObjectByName(t.root).matrixWorld),h.quaternion.setFromRotationMatrix(this.armature.getObjectByName(t.root).matrixWorld),e&&n&&e.applyQuaternion(this.armature.quaternion).add(h.position);const g=this.ikMesh.getObjectByName(t.effector),v=t.links;v.forEach(S=>{S.bone=this.ikMesh.getObjectByName(S.link),S.bone.quaternion.copy(this.getPoseTemplateProp(S.link+".quaternion"))}),h.updateMatrixWorld(!0);const R=t.iterations||10;if(e)for(let S=0;S<R;S++){let M=!1;for(let p=0,O=v.length;p<O;p++){const P=v[p].bone;P.matrixWorld.decompose(c,l,u),l.invert(),i.setFromMatrixPosition(g.matrixWorld),a.subVectors(i,c),a.applyQuaternion(l),a.normalize(),s.subVectors(e,c),s.applyQuaternion(l),s.normalize();let y=s.dot(a);y>1?y=1:y<-1&&(y=-1),y=Math.acos(y),!(y<1e-5)&&(v[p].minAngle!==void 0&&y<v[p].minAngle&&(y=v[p].minAngle),v[p].maxAngle!==void 0&&y>v[p].maxAngle&&(y=v[p].maxAngle),r.crossVectors(a,s),r.normalize(),ne.setFromAxisAngle(r,y),P.quaternion.multiply(ne),P.rotation.setFromVector3(d.setFromEuler(P.rotation).clamp(new f.Vector3(v[p].minx!==void 0?v[p].minx:-1/0,v[p].miny!==void 0?v[p].miny:-1/0,v[p].minz!==void 0?v[p].minz:-1/0),new f.Vector3(v[p].maxx!==void 0?v[p].maxx:1/0,v[p].maxy!==void 0?v[p].maxy:1/0,v[p].maxz!==void 0?v[p].maxz:1/0))),P.updateMatrixWorld(!0),M=!0)}if(!M)break}o&&v.forEach(S=>{this.poseTarget.props[S.link+".quaternion"].copy(S.bone.quaternion),this.poseTarget.props[S.link+".quaternion"].t=this.animClock,this.poseTarget.props[S.link+".quaternion"].d=o})}dispose(){this.isRunning=!1,this.stop(),this.stopSpeaking(),this.streamStop(),this.isAvatarOnly?this.armature&&(this.armature.parent&&this.armature.parent.remove(this.armature),this.clearThree(this.armature)):(this.clearThree(this.scene),this.resizeobserver.disconnect(),this.renderer&&(this.renderer.dispose(),this.renderer.domElement&&this.renderer.domElement.parentNode&&this.renderer.domElement.parentNode.removeChild(this.renderer.domElement),this.renderer=null)),this.clearThree(this.ikMesh),this.dynamicbones.dispose()}}const Te={apiKey:"sk_ace57ef3ef65a92b9d3bee2a00183b78ca790bc3e10964f2",endpoint:"https://api.elevenlabs.io/v1/text-to-speech",defaultVoice:"21m00Tcm4TlvDq8ikWAM",voices:{rachel:"21m00Tcm4TlvDq8ikWAM",drew:"29vD33N1CtxCmqQRPOHJ",bella:"EXAVITQu4vr4xnSDxMaL",antoni:"ErXwobaYiN019PkySvjV",elli:"MF3mGyEYCl7XYWbV9V6O",josh:"VR6AewLTigWG4xSOukaG"}},Ue={defaultVoice:"aura-2-thalia-en",voices:{thalia:"aura-2-thalia-en",asteria:"aura-2-asteria-en",orion:"aura-2-orion-en",stella:"aura-2-stella-en",athena:"aura-2-athena-en",hera:"aura-2-hera-en",zeus:"aura-2-zeus-en"}};function Pe(){return{service:"elevenlabs",endpoint:Te.endpoint,apiKey:Te.apiKey,defaultVoice:Te.defaultVoice,voices:Te.voices}}function St(){const N=Pe(),t=[];return Object.entries(N.voices).forEach(([e,n])=>{t.push({value:n,label:`${e.charAt(0).toUpperCase()+e.slice(1)} (${N.service})`})}),t}const Ze=x.forwardRef(({avatarUrl:N="/avatars/brunette.glb",avatarBody:t="F",mood:e="neutral",ttsLang:n="en",ttsService:o=null,ttsVoice:s=null,ttsApiKey:i=null,bodyMovement:a="idle",movementIntensity:c=.5,showFullAvatar:l=!0,cameraView:u="upper",onReady:r=()=>{},onLoading:d=()=>{},onError:h=()=>{},className:g="",style:v={},animations:R={}},S)=>{const M=x.useRef(null),p=x.useRef(null),O=x.useRef(l),P=x.useRef(null),y=x.useRef(null),E=x.useRef(!1),b=x.useRef({remainingText:null,originalText:null,options:null}),k=x.useRef([]),T=x.useRef(0),[A,W]=x.useState(!0),[G,j]=x.useState(null),[Y,ae]=x.useState(!1),[re,xe]=x.useState(!1);x.useEffect(()=>{E.current=re},[re]),x.useEffect(()=>{O.current=l},[l]);const oe=Pe(),Se=o||oe.service;let $;Se==="browser"?$={service:"browser",endpoint:"",apiKey:null,defaultVoice:"Google US English"}:Se==="elevenlabs"?$={service:"elevenlabs",endpoint:"https://api.elevenlabs.io/v1/text-to-speech",apiKey:i||oe.apiKey,defaultVoice:s||oe.defaultVoice||Te.defaultVoice,voices:oe.voices||Te.voices}:Se==="deepgram"?$={service:"deepgram",endpoint:"https://api.deepgram.com/v1/speak",apiKey:i||oe.apiKey,defaultVoice:s||oe.defaultVoice||Ue.defaultVoice,voices:oe.voices||Ue.voices}:$={...oe,apiKey:i!==null?i:oe.apiKey};const I={url:N,body:t,avatarMood:e,ttsLang:Se==="browser"?"en-US":n,ttsVoice:s||$.defaultVoice,lipsyncLang:"en",showFullAvatar:l,bodyMovement:a,movementIntensity:c},L={ttsEndpoint:$.endpoint,ttsApikey:$.apiKey,ttsService:Se,lipsyncModules:["en"],cameraView:u},F=x.useCallback(async()=>{if(!(!M.current||p.current))try{if(W(!0),j(null),p.current=new Ge(M.current,L),p.current.controls&&(p.current.controls.enableRotate=!1,p.current.controls.enableZoom=!1,p.current.controls.enablePan=!1,p.current.controls.enableDamping=!1),R&&Object.keys(R).length>0&&(p.current.customAnimations=R),await p.current.showAvatar(I,Q=>{if(Q.lengthComputable){const ie=Math.min(100,Math.round(Q.loaded/Q.total*100));d(ie)}}),await new Promise(Q=>{const ie=()=>{p.current.lipsync&&Object.keys(p.current.lipsync).length>0?Q():setTimeout(ie,100)};ie()}),p.current&&p.current.setShowFullAvatar)try{p.current.setShowFullAvatar(l)}catch(Q){console.warn("Error setting full body mode on initialization:",Q)}p.current&&p.current.controls&&(p.current.controls.enableRotate=!1,p.current.controls.enableZoom=!1,p.current.controls.enablePan=!1,p.current.controls.enableDamping=!1,p.current.controls.update()),W(!1),ae(!0),r(p.current);const X=()=>{document.visibilityState==="visible"?p.current?.start():p.current?.stop()};return document.addEventListener("visibilitychange",X),()=>{document.removeEventListener("visibilitychange",X)}}catch(C){console.error("Error initializing TalkingHead:",C),j(C.message||"Failed to initialize avatar"),W(!1),h(C)}},[N,t,e,n,o,s,i,l,a,c,u]);x.useEffect(()=>(F(),()=>{p.current&&(p.current.stop(),p.current.dispose(),p.current=null)}),[F]),x.useEffect(()=>{if(!M.current||!p.current)return;const C=new ResizeObserver(Q=>{for(const ie of Q)p.current&&p.current.onResize&&p.current.onResize()});C.observe(M.current);const X=()=>{p.current&&p.current.onResize&&p.current.onResize()};return window.addEventListener("resize",X),()=>{C.disconnect(),window.removeEventListener("resize",X)}},[Y]);const D=x.useCallback(async()=>{if(p.current&&p.current.audioCtx)try{(p.current.audioCtx.state==="suspended"||p.current.audioCtx.state==="interrupted")&&(await p.current.audioCtx.resume(),console.log("Audio context resumed"))}catch(C){console.warn("Failed to resume audio context:",C)}},[]),Z=x.useCallback(async(C,X={})=>{if(p.current&&Y)try{y.current&&(clearInterval(y.current),y.current=null),P.current={text:C,options:X},b.current={remainingText:null,originalText:null,options:null};const Q=/[!\.\?\n\p{Extended_Pictographic}]/ug,ie=C.split(Q).map(_=>_.trim()).filter(_=>_.length>0);k.current=ie,T.current=0,xe(!1),E.current=!1,await D();const ge={...X,lipsyncLang:X.lipsyncLang||I.lipsyncLang||"en"};if(X.onSpeechEnd&&p.current){const _=p.current;let de=null,be=0;const ye=1200;let ve=!1;de=setInterval(()=>{if(be++,E.current)return;if(be>ye){if(de&&(clearInterval(de),de=null,y.current=null),!ve&&!E.current){ve=!0;try{X.onSpeechEnd()}catch(z){console.error("Error in onSpeechEnd callback (timeout):",z)}}return}const Re=!_.speechQueue||_.speechQueue.length===0,Me=!_.audioPlaylist||_.audioPlaylist.length===0;_&&_.isSpeaking===!1&&Re&&Me&&_.isAudioPlaying===!1&&!ve&&!E.current&&setTimeout(()=>{if(_&&!E.current&&_.isSpeaking===!1&&(!_.speechQueue||_.speechQueue.length===0)&&(!_.audioPlaylist||_.audioPlaylist.length===0)&&_.isAudioPlaying===!1&&!ve&&!E.current){ve=!0,de&&(clearInterval(de),de=null,y.current=null);try{X.onSpeechEnd()}catch(U){console.error("Error in onSpeechEnd callback:",U)}}},100)},100),y.current=de}p.current.lipsync&&Object.keys(p.current.lipsync).length>0?(p.current.setSlowdownRate&&p.current.setSlowdownRate(1.05),p.current.speakText(C,ge)):setTimeout(async()=>{await D(),p.current&&p.current.lipsync&&(p.current.setSlowdownRate&&p.current.setSlowdownRate(1.05),p.current.speakText(C,ge))},100)}catch(Q){console.error("Error speaking text:",Q),j(Q.message||"Failed to speak text")}},[Y,D,I.lipsyncLang]),se=x.useCallback(()=>{p.current&&(p.current.stopSpeaking(),p.current.setSlowdownRate&&p.current.setSlowdownRate(1),P.current=null,xe(!1))},[]),te=x.useCallback(()=>{if(p.current&&p.current.pauseSpeaking){const C=p.current;if(C.isSpeaking||C.audioPlaylist&&C.audioPlaylist.length>0||C.speechQueue&&C.speechQueue.length>0){y.current&&(clearInterval(y.current),y.current=null);let Q="";if(P.current&&k.current.length>0){const ie=k.current.length,ge=C.speechQueue?C.speechQueue.filter(ye=>ye&&ye.text&&Array.isArray(ye.text)&&ye.text.length>0).length:0,_=C.audioPlaylist&&C.audioPlaylist.length>0,de=ge+(_?1:0),be=ie-de;if(de>0&&be<ie&&(Q=k.current.slice(be).join(". ").trim(),!Q&&ge>0&&C.speechQueue)){const ve=C.speechQueue.filter(Re=>Re&&Re.text&&Array.isArray(Re.text)&&Re.text.length>0).map(Re=>Re.text.map(Me=>Me.word||"").filter(Me=>Me.length>0).join(" ")).filter(Re=>Re.length>0).join(" ");ve&&ve.trim()&&(Q=ve.trim())}}P.current&&(b.current={remainingText:Q||null,originalText:P.current.text,options:P.current.options}),C.speechQueue&&(C.speechQueue.length=0),p.current.pauseSpeaking(),E.current=!0,xe(!0)}}},[]),J=x.useCallback(async()=>{if(!p.current||!re)return;let C="",X={};if(b.current&&b.current.remainingText)C=b.current.remainingText,X=b.current.options||{},b.current={remainingText:null,originalText:null,options:null};else if(P.current&&P.current.text)C=P.current.text,X=P.current.options||{};else{console.warn("Resume called but no paused speech found"),xe(!1),E.current=!1;return}xe(!1),E.current=!1,await D();const Q={...X,lipsyncLang:X.lipsyncLang||I.lipsyncLang||"en"};try{await Z(C,Q)}catch(ie){console.error("Error resuming speech:",ie),xe(!1),E.current=!1}},[D,re,Z,I]),he=x.useCallback(C=>{p.current&&p.current.setMood(C)},[]),Ce=x.useCallback(C=>{p.current&&p.current.setSlowdownRate&&p.current.setSlowdownRate(C)},[]),ke=x.useCallback((C,X=!1)=>{if(p.current&&p.current.playAnimation){if(R&&R[C]&&(C=R[C]),p.current.setShowFullAvatar)try{p.current.setShowFullAvatar(O.current)}catch(ie){console.warn("Error setting full body mode:",ie)}if(C.includes("."))try{p.current.playAnimation(C,null,10,0,.01,X)}catch(ie){console.warn(`Failed to play ${C}:`,ie);try{p.current.setBodyMovement("idle")}catch(ge){console.warn("Fallback animation also failed:",ge)}}else{const ie=[".fbx",".glb",".gltf"];let ge=!1;for(const _ of ie)try{p.current.playAnimation(C+_,null,10,0,.01,X),ge=!0;break}catch{}if(!ge){console.warn("Animation not found:",C);try{p.current.setBodyMovement("idle")}catch(_){console.warn("Fallback animation also failed:",_)}}}}},[R]),We=x.useCallback(()=>{p.current&&p.current.onResize&&p.current.onResize()},[]);return x.useImperativeHandle(S,()=>({speakText:Z,stopSpeaking:se,pauseSpeaking:te,resumeSpeaking:J,resumeAudioContext:D,setMood:he,setTimingAdjustment:Ce,playAnimation:ke,isReady:Y,isPaused:re,talkingHead:p.current,handleResize:We,setBodyMovement:C=>{if(p.current&&p.current.setShowFullAvatar&&p.current.setBodyMovement)try{p.current.setShowFullAvatar(O.current),p.current.setBodyMovement(C)}catch(X){console.warn("Error setting body movement:",X)}},setMovementIntensity:C=>p.current?.setMovementIntensity(C),playRandomDance:()=>{if(p.current&&p.current.setShowFullAvatar&&p.current.playRandomDance)try{p.current.setShowFullAvatar(O.current),p.current.playRandomDance()}catch(C){console.warn("Error playing random dance:",C)}},playReaction:C=>{if(p.current&&p.current.setShowFullAvatar&&p.current.playReaction)try{p.current.setShowFullAvatar(O.current),p.current.playReaction(C)}catch(X){console.warn("Error playing reaction:",X)}},playCelebration:()=>{if(p.current&&p.current.setShowFullAvatar&&p.current.playCelebration)try{p.current.setShowFullAvatar(O.current),p.current.playCelebration()}catch(C){console.warn("Error playing celebration:",C)}},setShowFullAvatar:C=>{if(p.current&&p.current.setShowFullAvatar)try{O.current=C,p.current.setShowFullAvatar(C)}catch(X){console.warn("Error setting showFullAvatar:",X)}},lockAvatarPosition:()=>{if(p.current&&p.current.lockAvatarPosition)try{p.current.lockAvatarPosition()}catch(C){console.warn("Error locking avatar position:",C)}},unlockAvatarPosition:()=>{if(p.current&&p.current.unlockAvatarPosition)try{p.current.unlockAvatarPosition()}catch(C){console.warn("Error unlocking avatar position:",C)}}})),V.jsxs("div",{className:`talking-head-avatar ${g}`,style:{width:"100%",height:"100%",position:"relative",...v},children:[V.jsx("div",{ref:M,className:"talking-head-viewer",style:{width:"100%",height:"100%",minHeight:"400px"}}),A&&V.jsx("div",{className:"loading-overlay",style:{position:"absolute",top:"50%",left:"50%",transform:"translate(-50%, -50%)",color:"white",fontSize:"18px",zIndex:10},children:"Loading avatar..."}),G&&V.jsx("div",{className:"error-overlay",style:{position:"absolute",top:"50%",left:"50%",transform:"translate(-50%, -50%)",color:"#ff6b6b",fontSize:"16px",textAlign:"center",zIndex:10,padding:"20px",borderRadius:"8px"},children:G})]})});Ze.displayName="TalkingHeadAvatar";const Ke=x.forwardRef(({text:N="Hello! I'm a talking avatar. How are you today?",onLoading:t=()=>{},onError:e=()=>{},onReady:n=()=>{},className:o="",style:s={},avatarConfig:i={}},a)=>{const c=x.useRef(null),l=x.useRef(null),[u,r]=x.useState(!0),[d,h]=x.useState(null),[g,v]=x.useState(!1),R=Pe(),S=i.ttsService||R.service,M=S==="browser"?{endpoint:"",apiKey:null,defaultVoice:"Google US English"}:{...R,apiKey:i.ttsApiKey!==void 0&&i.ttsApiKey!==null?i.ttsApiKey:R.apiKey,endpoint:S==="elevenlabs"&&i.ttsApiKey?"https://api.elevenlabs.io/v1/text-to-speech":R.endpoint},p={url:"/avatars/brunette.glb",body:"F",avatarMood:"neutral",ttsLang:S==="browser"?"en-US":"en",ttsVoice:i.ttsVoice||M.defaultVoice,lipsyncLang:"en",showFullAvatar:!0,bodyMovement:"idle",movementIntensity:.5,...i},O={ttsEndpoint:M.endpoint,ttsApikey:M.apiKey,ttsService:S,lipsyncModules:["en"],cameraView:"upper"},P=x.useCallback(async()=>{if(!(!c.current||l.current))try{if(r(!0),h(null),l.current=new Ge(c.current,O),await l.current.showAvatar(p,G=>{if(G.lengthComputable){const j=Math.min(100,Math.round(G.loaded/G.total*100));t(j)}}),l.current.morphs&&l.current.morphs.length>0){const G=l.current.morphs[0].morphTargetDictionary;console.log("Available morph targets:",Object.keys(G));const j=Object.keys(G).filter(Y=>Y.startsWith("viseme_"));console.log("Viseme morph targets found:",j),j.length===0&&(console.warn("No viseme morph targets found! Lip-sync will not work properly."),console.log("Expected viseme targets: viseme_aa, viseme_E, viseme_I, viseme_O, viseme_U, viseme_PP, viseme_SS, viseme_TH, viseme_DD, viseme_FF, viseme_kk, viseme_nn, viseme_RR, viseme_CH, viseme_sil"))}if(await new Promise(G=>{const j=()=>{l.current.lipsync&&Object.keys(l.current.lipsync).length>0?(console.log("Lip-sync modules loaded:",Object.keys(l.current.lipsync)),G()):(console.log("Waiting for lip-sync modules to load..."),setTimeout(j,100))};j()}),l.current&&l.current.setShowFullAvatar)try{l.current.setShowFullAvatar(!0),console.log("Avatar initialized in full body mode")}catch(G){console.warn("Error setting full body mode on initialization:",G)}r(!1),v(!0),n(l.current);const W=()=>{document.visibilityState==="visible"?l.current?.start():l.current?.stop()};return document.addEventListener("visibilitychange",W),()=>{document.removeEventListener("visibilitychange",W)}}catch(A){console.error("Error initializing TalkingHead:",A),h(A.message||"Failed to initialize avatar"),r(!1),e(A)}},[]);x.useEffect(()=>(P(),()=>{l.current&&(l.current.stop(),l.current.dispose(),l.current=null)}),[P]);const y=x.useCallback(A=>{if(l.current&&g)try{console.log("Speaking text:",A),console.log("Avatar config:",p),console.log("TalkingHead instance:",l.current),l.current.lipsync&&Object.keys(l.current.lipsync).length>0?(console.log("Lip-sync modules loaded:",Object.keys(l.current.lipsync)),l.current.setSlowdownRate&&(l.current.setSlowdownRate(1.05),console.log("Applied timing adjustment for better lip-sync")),l.current.speakText(A)):(console.warn("Lip-sync modules not ready, waiting..."),setTimeout(()=>{l.current&&l.current.lipsync?(console.log("Lip-sync now ready, speaking..."),l.current.setSlowdownRate&&(l.current.setSlowdownRate(1.05),console.log("Applied timing adjustment for better lip-sync")),l.current.speakText(A)):console.error("Lip-sync still not ready after waiting")},500))}catch(W){console.error("Error speaking text:",W),h(W.message||"Failed to speak text")}else console.warn("Avatar not ready for speaking. isReady:",g,"talkingHeadRef:",!!l.current)},[g,p]),E=x.useCallback(()=>{l.current&&(l.current.stopSpeaking(),l.current.setSlowdownRate&&(l.current.setSlowdownRate(1),console.log("Reset timing to normal")))},[]),b=x.useCallback(A=>{l.current&&l.current.setMood(A)},[]),k=x.useCallback(A=>{l.current&&l.current.setSlowdownRate&&(l.current.setSlowdownRate(A),console.log("Timing adjustment set to:",A))},[]),T=x.useCallback((A,W=!1)=>{if(l.current&&l.current.playAnimation){if(l.current.setShowFullAvatar)try{l.current.setShowFullAvatar(!0)}catch(j){console.warn("Error setting full body mode:",j)}if(A.includes("."))try{l.current.playAnimation(A,null,10,0,.01,W),console.log("Playing animation:",A)}catch(j){console.log(`Failed to play ${A}:`,j);try{l.current.setBodyMovement("idle"),console.log("Fallback to idle animation")}catch(Y){console.warn("Fallback animation also failed:",Y)}}else{const j=[".fbx",".glb",".gltf"];let Y=!1;for(const ae of j)try{l.current.playAnimation(A+ae,null,10,0,.01,W),console.log("Playing animation:",A+ae),Y=!0;break}catch{console.log(`Failed to play ${A}${ae}, trying next format...`)}if(!Y){console.warn("Animation system not available or animation not found:",A);try{l.current.setBodyMovement("idle"),console.log("Fallback to idle animation")}catch(ae){console.warn("Fallback animation also failed:",ae)}}}}else console.warn("Animation system not available or animation not found:",A)},[]);return x.useImperativeHandle(a,()=>({speakText:y,stopSpeaking:E,setMood:b,setTimingAdjustment:k,playAnimation:T,isReady:g,talkingHead:l.current,setBodyMovement:A=>{if(l.current&&l.current.setShowFullAvatar&&l.current.setBodyMovement)try{l.current.setShowFullAvatar(!0),l.current.setBodyMovement(A),console.log("Body movement set with full body mode:",A)}catch(W){console.warn("Error setting body movement:",W)}},setMovementIntensity:A=>l.current?.setMovementIntensity(A),playRandomDance:()=>{if(l.current&&l.current.setShowFullAvatar&&l.current.playRandomDance)try{l.current.setShowFullAvatar(!0),l.current.playRandomDance(),console.log("Random dance played with full body mode")}catch(A){console.warn("Error playing random dance:",A)}},playReaction:A=>{if(l.current&&l.current.setShowFullAvatar&&l.current.playReaction)try{l.current.setShowFullAvatar(!0),l.current.playReaction(A),console.log("Reaction played with full body mode:",A)}catch(W){console.warn("Error playing reaction:",W)}},playCelebration:()=>{if(l.current&&l.current.setShowFullAvatar&&l.current.playCelebration)try{l.current.setShowFullAvatar(!0),l.current.playCelebration(),console.log("Celebration played with full body mode")}catch(A){console.warn("Error playing celebration:",A)}},setShowFullAvatar:A=>{if(l.current&&l.current.setShowFullAvatar)try{l.current.setShowFullAvatar(A),console.log("Show full avatar set to:",A)}catch(W){console.warn("Error setting showFullAvatar:",W)}},lockAvatarPosition:()=>{if(l.current&&l.current.lockAvatarPosition)try{l.current.lockAvatarPosition()}catch(A){console.warn("Error locking avatar position:",A)}},unlockAvatarPosition:()=>{if(l.current&&l.current.unlockAvatarPosition)try{l.current.unlockAvatarPosition()}catch(A){console.warn("Error unlocking avatar position:",A)}}})),V.jsxs("div",{className:`talking-head-container ${o}`,style:s,children:[V.jsx("div",{ref:c,className:"talking-head-viewer",style:{width:"100%",height:"100%",minHeight:"400px"}}),u&&V.jsx("div",{className:"loading-overlay",style:{position:"absolute",top:"50%",left:"50%",transform:"translate(-50%, -50%)",color:"white",fontSize:"18px",zIndex:10},children:"Loading avatar..."}),d&&V.jsx("div",{className:"error-overlay",style:{position:"absolute",top:"50%",left:"50%",transform:"translate(-50%, -50%)",color:"#ff6b6b",fontSize:"16px",textAlign:"center",zIndex:10,padding:"20px",borderRadius:"8px"},children:d})]})});Ke.displayName="TalkingHeadComponent";async function Ee(N){try{const t=await fetch(N);if(!t.ok)throw new Error(`Failed to fetch manifest: ${t.status} ${t.statusText}`);return(await t.json()).animations||{}}catch(t){return console.error("Failed to load animation manifest:",t),{}}}async function kt(N,t="F"){const e=[],n=N.replace(/\/$/,"");try{const s=[`${n}/.list.json`,`/api/directory?path=${encodeURIComponent(n)}`,`${n}/index.json`];for(const i of s)try{const a=await fetch(i);if(a.ok){const c=await a.json(),u=(Array.isArray(c)?c:c.files||[]).filter(r=>typeof r=="string"&&r.toLowerCase().endsWith(".fbx")).map(r=>r.startsWith("/")?r:`${n}/${r}`);if(u.length>0)return u}}catch{continue}}catch(s){console.warn(`⚠️ Could not use directory listing API for ${n}:`,s)}const o=[];t==="M"||t==="m"?o.push(`${n}/male`,`${n}/m`):o.push(`${n}/female`,`${n}/f`),o.push(`${n}/shared`);for(const s of o)try{const i=`${s}/.list.json`,a=await fetch(i);if(a.ok){const c=await a.json(),l=(Array.isArray(c)?c:c.files||[]).filter(u=>typeof u=="string"&&u.toLowerCase().endsWith(".fbx")).map(u=>u.startsWith("/")?u:`${s}/${u}`);l.length>0&&e.push(...l)}}catch{continue}return e.length>0,e}async function _e(N,t="F"){const e={};for(const[n,o]of Object.entries(N))try{const s=await kt(o,t);s.length>0&&(e[n]=s)}catch(s){console.error(`❌ Failed to auto-load animations from ${o}:`,s)}return e}const Je=x.forwardRef(({text:N=null,avatarUrl:t="/avatars/brunette.glb",avatarBody:e="F",mood:n="neutral",ttsLang:o="en",ttsService:s=null,ttsVoice:i=null,ttsApiKey:a=null,bodyMovement:c="idle",movementIntensity:l=.5,showFullAvatar:u=!1,cameraView:r="upper",onReady:d=()=>{},onLoading:h=()=>{},onError:g=()=>{},onSpeechEnd:v=()=>{},className:R="",style:S={},animations:M={},autoAnimationGroup:p=null,autoIdleGroup:O=null,autoSpeak:P=!1},y)=>{const E=x.useRef(null),b=x.useRef(null),k=x.useRef(u),T=x.useRef(null),A=x.useRef(null),W=x.useRef(!1),G=x.useRef({remainingText:null,originalText:null,options:null}),j=x.useRef([]),[Y,ae]=x.useState(!0),[re,xe]=x.useState(null),[oe,Se]=x.useState(!1),[$,I]=x.useState(!1),[L,F]=x.useState(M),D=x.useRef(null),Z=x.useRef(!1),se=x.useRef(null),te=x.useRef([]),J=x.useRef([]);x.useEffect(()=>{W.current=$},[$]),x.useEffect(()=>{(async()=>{if(M.manifest&&M.auto)try{const z=await Ee(M.manifest);F(z);return}catch{}if(M.manifest&&!M.auto)try{const z=await Ee(M.manifest);F(z)}catch(z){console.error("Failed to load animation manifest:",z),F(M)}else if(M.auto)try{let z=null;if(M.manifest)try{z=await Ee(M.manifest)}catch{}if(typeof M.auto=="string"){const U=M.auto,ee={talking:`${U}/talking`,idle:`${U}/idle`},K=e==="M"?"male":"female";ee[`${K}_talking`]=`${U}/${K}/talking`,ee[`${K}_idle`]=`${U}/${K}/idle`,ee.shared_talking=`${U}/shared/talking`,ee.shared_idle=`${U}/shared/idle`;const Ie=await _e(ee,e);if(!Object.values(Ie).some(le=>Array.isArray(le)&&le.length>0)&&z){F(z);return}const me={_genderSpecific:{[K]:{},shared:{}}};Object.entries(Ie).forEach(([le,ze])=>{if(le.includes("_")){const[Oe,...et]=le.split("_"),Fe=et.join("_");Oe==="shared"?(me._genderSpecific.shared[Fe]||(me._genderSpecific.shared[Fe]=[]),me._genderSpecific.shared[Fe].push(...ze)):Oe===K&&(me._genderSpecific[K][Fe]||(me._genderSpecific[K][Fe]=[]),me._genderSpecific[K][Fe].push(...ze))}else me[le]=ze}),z&&(z._genderSpecific&&Object.keys(z._genderSpecific).forEach(le=>{me._genderSpecific[le]||(me._genderSpecific[le]={}),Object.entries(z._genderSpecific[le]).forEach(([ze,Oe])=>{me._genderSpecific[le][ze]||(me._genderSpecific[le][ze]=Oe)})}),Object.entries(z).forEach(([le,ze])=>{le!=="_genderSpecific"&&!me[le]&&(me[le]=ze)})),F(me)}else if(typeof M.auto=="object"){const U=await _e(M.auto,e),ee=Object.values(U).some(K=>Array.isArray(K)&&K.length>0);F(!ee&&z?z:U)}}catch(z){if(console.error("Failed to auto-discover animations:",z),M.manifest)try{const U=await Ee(M.manifest);F(U)}catch{F(M)}else F(M)}else F(M)})()},[M,e]),x.useEffect(()=>{k.current=u},[u]);const he=Pe(),Ce=s||he.service;let ke;Ce==="browser"?ke={service:"browser",endpoint:"",apiKey:null,defaultVoice:"Google US English"}:Ce==="elevenlabs"?ke={service:"elevenlabs",endpoint:"https://api.elevenlabs.io/v1/text-to-speech",apiKey:a||he.apiKey,defaultVoice:i||he.defaultVoice||Te.defaultVoice,voices:he.voices||Te.voices}:Ce==="deepgram"?ke={service:"deepgram",endpoint:"https://api.deepgram.com/v1/speak",apiKey:a||he.apiKey,defaultVoice:i||he.defaultVoice||Ue.defaultVoice,voices:he.voices||Ue.voices}:ke={...he,apiKey:a!==null?a:he.apiKey};const We={url:t,body:e,avatarMood:n,ttsLang:Ce==="browser"?"en-US":o,ttsVoice:i||ke.defaultVoice,lipsyncLang:"en",showFullAvatar:u,bodyMovement:c,movementIntensity:l},C={ttsEndpoint:ke.endpoint,ttsApikey:ke.apiKey,ttsService:Ce,lipsyncModules:["en"],cameraView:r},X=x.useCallback(async()=>{if(!(!E.current||b.current))try{ae(!0),xe(null),b.current=new Ge(E.current,C),await b.current.showAvatar(We,z=>{if(z.lengthComputable){const U=Math.min(100,Math.round(z.loaded/z.total*100));h(U)}}),ae(!1),Se(!0),d(b.current);const w=()=>{document.visibilityState==="visible"?b.current?.start():b.current?.stop()};return document.addEventListener("visibilitychange",w),()=>{document.removeEventListener("visibilitychange",w)}}catch(w){console.error("Error initializing TalkingHead:",w),xe(w.message||"Failed to initialize avatar"),ae(!1),g(w)}},[]);x.useEffect(()=>(X(),()=>{b.current&&(b.current.stop(),b.current.dispose(),b.current=null)}),[X]);const Q=x.useCallback(async()=>{if(b.current)try{const w=b.current.audioCtx||b.current.audioContext;w&&(w.state==="suspended"||w.state==="interrupted")&&await w.resume()}catch(w){console.warn("Failed to resume audio context:",w)}},[]),ie=x.useCallback(w=>{if(!L)return[];let z=null;if(L._genderSpecific){const ee=(e?.toUpperCase()||"F")==="M"?"male":"female",K=L._genderSpecific[ee];K&&K[w]?z=K[w]:L._genderSpecific.shared&&L._genderSpecific.shared[w]&&(z=L._genderSpecific.shared[w])}return!z&&L[w]&&(z=L[w]),z?Array.isArray(z)?[...z]:typeof z=="string"?[z]:[]:[]},[L,e]),ge=x.useCallback(w=>{const z=[...w];for(let U=z.length-1;U>0;U--){const ee=Math.floor(Math.random()*(U+1));[z[U],z[ee]]=[z[ee],z[U]]}return z},[]),_=x.useCallback(w=>{if(J.current.length===0){const U=ie(w);if(U.length===0)return null;J.current=ge(U),te.current=[]}const z=J.current.shift();return te.current.push(z),z},[ie,ge]),de=x.useCallback(w=>w?w.split("/").pop().replace(".fbx","").replace(/[-_]/g," "):"Unknown",[]),be=x.useCallback((w,z=!1,U=null)=>{if(!b.current)return null;const ee=_(w);if(ee)try{const K=de(ee);console.log(`🎬 Playing animation: "${K}"`);const Ie=()=>{Z.current&&se.current===w?setTimeout(()=>{be(w,z,U)},100):U&&U()};return b.current.playAnimation(ee,null,10,0,.01,z,Ie),ee}catch(K){return console.error("Failed to play animation:",K),null}return null},[_,de]),ye=x.useCallback(async(w,z={})=>{if(!b.current||!oe||!w||w.trim()==="")return;await Q();const U=z.animationGroup||p;U&&!z.skipAnimation&&(Z.current=!0,se.current=U,J.current=[],te.current=[],be(U)),G.current={remainingText:null,originalText:null,options:null},j.current=[],T.current={text:w,options:z},A.current&&(clearInterval(A.current),A.current=null),I(!1),W.current=!1;const ee=w.split(/[.!?]+/).filter(Ie=>Ie.trim().length>0);j.current=ee;const K={lipsyncLang:z.lipsyncLang||"en",onSpeechEnd:()=>{A.current&&(clearInterval(A.current),A.current=null),Z.current=!1,se.current=null,J.current=[],te.current=[],z.onSpeechEnd&&z.onSpeechEnd(),v()}};try{b.current.speakText(w,K)}catch(Ie){console.error("Error speaking text:",Ie),xe(Ie.message||"Failed to speak text")}},[oe,v,Q,p,be]);x.useEffect(()=>{if(!oe||!O||!b.current)return;D.current&&clearInterval(D.current);const w=()=>{b.current&&!W.current&&be(O)};return w(),D.current=setInterval(()=>{w()},12e3+Math.random()*3e3),()=>{D.current&&(clearInterval(D.current),D.current=null)}},[oe,O,be]),x.useEffect(()=>{oe&&N&&P&&b.current&&ye(N)},[oe,N,P,ye]);const ve=x.useCallback(()=>{if(b.current)try{const w=b.current.isSpeaking||!1,z=b.current.audioPlaylist||[],U=b.current.speechQueue||[];if(w||z.length>0||U.length>0){A.current&&(clearInterval(A.current),A.current=null);let ee="";U.length>0&&(ee=U.map(K=>K.text&&Array.isArray(K.text)?K.text.map(Ie=>Ie.word).join(" "):K.text||"").join(" ")),G.current={remainingText:ee||null,originalText:T.current?.text||null,options:T.current?.options||null},b.current.speechQueue.length=0,b.current.pauseSpeaking(),I(!0),W.current=!0}}catch(w){console.warn("Error pausing speech:",w)}},[]),Re=x.useCallback(async()=>{if(!(!b.current||!$))try{await Q(),I(!1),W.current=!1;const w=G.current?.remainingText,z=G.current?.originalText||T.current?.text,U=G.current?.options||T.current?.options||{},ee=w||z;ee&&ye(ee,U)}catch(w){console.warn("Error resuming speech:",w),I(!1),W.current=!1}},[$,ye,Q]),Me=x.useCallback(()=>{b.current&&(b.current.stopSpeaking(),A.current&&(clearInterval(A.current),A.current=null),Z.current=!1,se.current=null,J.current=[],te.current=[],I(!1),W.current=!1)},[]);return x.useImperativeHandle(y,()=>({speakText:ye,pauseSpeaking:ve,resumeSpeaking:Re,stopSpeaking:Me,resumeAudioContext:Q,isPaused:()=>$,setMood:w=>b.current?.setMood(w),setBodyMovement:w=>{b.current&&b.current.setBodyMovement(w)},playAnimation:(w,z=!1)=>{b.current&&b.current.playAnimation&&b.current.playAnimation(w,null,10,0,.01,z)},playRandomAnimation:(w,z=!1)=>be(w,z),getRandomAnimation:w=>getRandomAnimation(w),playReaction:w=>b.current?.playReaction(w),playCelebration:()=>b.current?.playCelebration(),setShowFullAvatar:w=>{b.current&&(k.current=w,b.current.setShowFullAvatar(w))},isReady:oe,talkingHead:b.current})),V.jsxs("div",{className:`simple-talking-avatar-container ${R}`,style:S,children:[V.jsx("div",{ref:E,className:"talking-head-viewer",style:{width:"100%",height:"100%",minHeight:"400px"}}),Y&&V.jsx("div",{className:"loading-overlay",style:{position:"absolute",top:"50%",left:"50%",transform:"translate(-50%, -50%)",color:"white",fontSize:"18px",zIndex:10},children:"Loading avatar..."}),re&&V.jsx("div",{className:"error-overlay",style:{position:"absolute",top:"50%",left:"50%",transform:"translate(-50%, -50%)",color:"#ff6b6b",fontSize:"16px",textAlign:"center",zIndex:10,padding:"20px",borderRadius:"8px"},children:re})]})});Je.displayName="SimpleTalkingAvatar";const $e=x.forwardRef(({curriculumData:N=null,avatarConfig:t={},animations:e={},onLessonStart:n=()=>{},onLessonComplete:o=()=>{},onQuestionAnswer:s=()=>{},onCurriculumComplete:i=()=>{},onCustomAction:a=()=>{},autoStart:c=!1},l)=>{const u=x.useRef(null),r=x.useRef({currentModuleIndex:0,currentLessonIndex:0,currentQuestionIndex:0,isTeaching:!1,isQuestionMode:!1,lessonCompleted:!1,curriculumCompleted:!1,score:0,totalQuestions:0}),d=x.useRef({onLessonStart:n,onLessonComplete:o,onQuestionAnswer:s,onCurriculumComplete:i,onCustomAction:a}),h=x.useRef(null),g=x.useRef(null),v=x.useRef(null),R=x.useRef(null),S=x.useRef(null),M=x.useRef(null),p=x.useRef(null),O=x.useRef(N?.curriculum||{title:"Default Curriculum",description:"No curriculum data provided",language:"en",modules:[]}),P=x.useRef({avatarUrl:t.avatarUrl||"/avatars/brunette.glb",avatarBody:t.avatarBody||"F",mood:t.mood||"happy",ttsLang:t.ttsLang||"en",ttsService:t.ttsService||null,ttsVoice:t.ttsVoice||null,ttsApiKey:t.ttsApiKey||null,bodyMovement:t.bodyMovement||"gesturing",movementIntensity:t.movementIntensity||.7,showFullAvatar:t.showFullAvatar!==void 0?t.showFullAvatar:!1,animations:e,lipsyncLang:"en"});x.useEffect(()=>{d.current={onLessonStart:n,onLessonComplete:o,onQuestionAnswer:s,onCurriculumComplete:i,onCustomAction:a}},[n,o,s,i,a]),x.useEffect(()=>{O.current=N?.curriculum||{title:"Default Curriculum",description:"No curriculum data provided",language:"en",modules:[]},P.current={avatarUrl:t.avatarUrl||"/avatars/brunette.glb",avatarBody:t.avatarBody||"F",mood:t.mood||"happy",ttsLang:t.ttsLang||"en",ttsService:t.ttsService||null,ttsVoice:t.ttsVoice||null,ttsApiKey:t.ttsApiKey||null,bodyMovement:t.bodyMovement||"gesturing",movementIntensity:t.movementIntensity||.7,showFullAvatar:t.showFullAvatar!==void 0?t.showFullAvatar:!1,animations:e,lipsyncLang:"en"}},[N,t,e]);const y=x.useCallback(()=>(O.current||{modules:[]}).modules[r.current.currentModuleIndex]?.lessons[r.current.currentLessonIndex],[]),E=x.useCallback(()=>y()?.questions[r.current.currentQuestionIndex],[y]),b=x.useCallback((I,L)=>L.type==="multiple_choice"||L.type==="true_false"?I===L.answer:L.type==="code_test"&&typeof I=="object"&&I!==null?I.passed===!0:!1,[]),k=x.useCallback(()=>{r.current.lessonCompleted=!0,r.current.isQuestionMode=!1;const I=r.current.totalQuestions>0?Math.round(r.current.score/r.current.totalQuestions*100):100;let L="Congratulations! You've completed this lesson";if(r.current.totalQuestions>0?L+=` You got ${r.current.score} correct out of ${r.current.totalQuestions} question${r.current.totalQuestions===1?"":"s"}, achieving a score of ${I} percent. `:L+="! ",I>=80?L+="Excellent work! You have a great understanding of this topic.":I>=60?L+="Good job! You understand most of the concepts.":L+="Keep practicing! You're making progress.",d.current.onLessonComplete({moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex,score:r.current.score,totalQuestions:r.current.totalQuestions,percentage:I}),d.current.onCustomAction({type:"lessonComplete",moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex,score:r.current.score,totalQuestions:r.current.totalQuestions,percentage:I}),u.current){if(u.current.setMood("happy"),e.lessonComplete)try{u.current.playAnimation(e.lessonComplete,!0)}catch{u.current.playCelebration()}const F=O.current||{modules:[]},D=F.modules[r.current.currentModuleIndex],Z=r.current.currentLessonIndex<(D?.lessons?.length||0)-1,se=r.current.currentModuleIndex<(F.modules?.length||0)-1,te=Z||se,J=P.current||{lipsyncLang:"en"};u.current.speakText(L,{lipsyncLang:J.lipsyncLang,onSpeechEnd:()=>{d.current.onCustomAction({type:"lessonCompleteFeedbackDone",moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex,score:r.current.score,totalQuestions:r.current.totalQuestions,percentage:I,hasNextLesson:te})}})}},[e.lessonComplete]),T=x.useCallback(()=>{r.current.curriculumCompleted=!0;const I=O.current||{modules:[]};if(d.current.onCurriculumComplete({modules:I.modules.length,totalLessons:I.modules.reduce((L,F)=>L+F.lessons.length,0)}),u.current){if(u.current.setMood("celebrating"),e.curriculumComplete)try{u.current.playAnimation(e.curriculumComplete,!0)}catch{u.current.playCelebration()}const L=P.current||{lipsyncLang:"en"};u.current.speakText("Amazing! You've completed the entire curriculum! You're now ready to move on to more advanced topics. Well done!",{lipsyncLang:L.lipsyncLang})}},[e.curriculumComplete]),A=x.useCallback(()=>{const I=y();r.current.isQuestionMode=!0,r.current.currentQuestionIndex=0,r.current.totalQuestions=I?.questions?.length||0,r.current.score=0;const L=E();L&&d.current.onCustomAction({type:"questionStart",moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex,questionIndex:r.current.currentQuestionIndex,totalQuestions:r.current.totalQuestions,question:L,score:r.current.score});const F=()=>{if(!u.current||!L)return;if(u.current.setMood("happy"),e.questionStart)try{u.current.playAnimation(e.questionStart,!0)}catch(Z){console.warn("Failed to play questionStart animation:",Z)}const D=P.current||{lipsyncLang:"en"};L.type==="code_test"?u.current.speakText(`Let's test your coding skills! Here's your first challenge: ${L.question}`,{lipsyncLang:D.lipsyncLang}):L.type==="multiple_choice"?u.current.speakText(`Now let me ask you some questions. Here's the first one: ${L.question}`,{lipsyncLang:D.lipsyncLang}):L.type==="true_false"?u.current.speakText(`Let's start with some true or false questions. First question: ${L.question}`,{lipsyncLang:D.lipsyncLang}):u.current.speakText(`Now let me ask you some questions. Here's the first one: ${L.question}`,{lipsyncLang:D.lipsyncLang})};if(u.current&&u.current.isReady&&L)F();else if(u.current&&u.current.isReady){const D=P.current||{lipsyncLang:"en"};u.current.speakText("Now let me ask you some questions to test your understanding.",{lipsyncLang:D.lipsyncLang})}else{const D=setInterval(()=>{u.current&&u.current.isReady&&(clearInterval(D),L&&F())},100);setTimeout(()=>{clearInterval(D)},5e3)}},[e.questionStart,y,E]),W=x.useCallback(()=>{const I=y();if(r.current.currentQuestionIndex<(I?.questions?.length||0)-1){u.current&&u.current.stopSpeaking&&u.current.stopSpeaking(),r.current.currentQuestionIndex+=1;const L=E();L&&d.current.onCustomAction({type:"nextQuestion",moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex,questionIndex:r.current.currentQuestionIndex,totalQuestions:r.current.totalQuestions,question:L,score:r.current.score});const F=()=>{if(!u.current||!L)return;if(u.current.setMood("happy"),u.current.setBodyMovement("idle"),e.nextQuestion)try{u.current.playAnimation(e.nextQuestion,!0)}catch(J){console.warn("Failed to play nextQuestion animation:",J)}const D=P.current||{lipsyncLang:"en"},se=y()?.questions?.length||0,te=r.current.currentQuestionIndex>=se-1;if(L.type==="code_test"){const J=te?`Great! Here's your final coding challenge: ${L.question}`:`Great! Now let's move on to your next coding challenge: ${L.question}`;u.current.speakText(J,{lipsyncLang:D.lipsyncLang})}else if(L.type==="multiple_choice"){const J=te?`Alright! Here's your final question: ${L.question}`:`Alright! Here's your next question: ${L.question}`;u.current.speakText(J,{lipsyncLang:D.lipsyncLang})}else if(L.type==="true_false"){const J=te?`Now let's try this final one: ${L.question}`:`Now let's try this one: ${L.question}`;u.current.speakText(J,{lipsyncLang:D.lipsyncLang})}else{const J=te?`Here's your final question: ${L.question}`:`Here's the next question: ${L.question}`;u.current.speakText(J,{lipsyncLang:D.lipsyncLang})}};if(u.current&&u.current.isReady&&L)F();else if(L){const D=setInterval(()=>{u.current&&u.current.isReady&&(clearInterval(D),F())},100);setTimeout(()=>{clearInterval(D)},5e3)}}else d.current.onCustomAction({type:"allQuestionsComplete",moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex,totalQuestions:r.current.totalQuestions,score:r.current.score})},[e.nextQuestion,y,E]),G=x.useCallback(()=>{const I=O.current||{modules:[]},L=I.modules[r.current.currentModuleIndex];if(r.current.currentLessonIndex<(L?.lessons?.length||0)-1){r.current.currentLessonIndex+=1,r.current.currentQuestionIndex=0,r.current.lessonCompleted=!1,r.current.isQuestionMode=!1,r.current.isTeaching=!1,r.current.score=0,r.current.totalQuestions=0;const D=I.modules[r.current.currentModuleIndex],Z=r.current.currentLessonIndex<(D?.lessons?.length||0)-1,se=r.current.currentModuleIndex<(I.modules?.length||0)-1,te=Z||se;d.current.onCustomAction({type:"lessonStart",moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex,hasNextLesson:te}),d.current.onLessonStart({moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex,lesson:y()}),u.current&&(u.current.setMood("happy"),u.current.setBodyMovement("idle"))}else if(r.current.currentModuleIndex<(I.modules?.length||0)-1){r.current.currentModuleIndex+=1,r.current.currentLessonIndex=0,r.current.currentQuestionIndex=0,r.current.lessonCompleted=!1,r.current.isQuestionMode=!1,r.current.isTeaching=!1,r.current.score=0,r.current.totalQuestions=0;const Z=I.modules[r.current.currentModuleIndex],se=r.current.currentLessonIndex<(Z?.lessons?.length||0)-1,te=r.current.currentModuleIndex<(I.modules?.length||0)-1,J=se||te;d.current.onCustomAction({type:"lessonStart",moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex,hasNextLesson:J}),d.current.onLessonStart({moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex,lesson:y()}),u.current&&(u.current.setMood("happy"),u.current.setBodyMovement("idle"))}else S.current&&S.current()},[]),j=x.useCallback(()=>{const I=y();let L=null;if(I?.avatar_script&&I?.body){const F=I.avatar_script.trim(),D=I.body.trim(),Z=F.match(/[.!?]$/)?" ":". ";L=`${F}${Z}${D}`}else L=I?.avatar_script||I?.body||null;if(u.current&&u.current.isReady&&L){r.current.isTeaching=!0,r.current.isQuestionMode=!1,r.current.score=0,r.current.totalQuestions=0,u.current.setMood("happy");let F=!1;if(e.teaching)try{u.current.playAnimation(e.teaching,!0),F=!0}catch(Z){console.warn("Failed to play teaching animation:",Z)}F||u.current.setBodyMovement("gesturing");const D=P.current||{lipsyncLang:"en"};d.current.onLessonStart({moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex,lesson:I}),d.current.onCustomAction({type:"teachingStart",moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex,lesson:I}),u.current.speakText(L,{lipsyncLang:D.lipsyncLang,onSpeechEnd:()=>{r.current.isTeaching=!1,d.current.onCustomAction({type:"teachingComplete",moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex,lesson:I,hasQuestions:I.questions&&I.questions.length>0}),I?.code_example&&d.current.onCustomAction({type:"codeExampleReady",moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex,lesson:I,codeExample:I.code_example})}})}},[e.teaching,y]),Y=x.useCallback(I=>{const L=E(),F=b(I,L);if(F&&(r.current.score+=1),d.current.onQuestionAnswer({moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex,questionIndex:r.current.currentQuestionIndex,answer:I,isCorrect:F,question:L}),u.current)if(F){if(u.current.setMood("happy"),e.correct)try{u.current.playReaction("happy")}catch{u.current.setBodyMovement("happy")}u.current.setBodyMovement("gesturing");const Z=y()?.questions?.length||0;r.current.currentQuestionIndex>=Z-1;const se=r.current.currentQuestionIndex<Z-1;console.log("[CurriculumLearning] Answer feedback - questionIndex:",r.current.currentQuestionIndex,"totalQuestions:",Z,"hasNextQuestion:",se);const te=L.type==="code_test"?`Great job! Your code passed all the tests! ${L.explanation||""}`:`Excellent! That's correct! ${L.explanation||""}`,J=P.current||{lipsyncLang:"en"};u.current.speakText(te,{lipsyncLang:J.lipsyncLang,onSpeechEnd:()=>{d.current.onCustomAction({type:"answerFeedbackComplete",moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex,questionIndex:r.current.currentQuestionIndex,isCorrect:!0,hasNextQuestion:se,score:r.current.score,totalQuestions:r.current.totalQuestions})}})}else{if(u.current.setMood("sad"),e.incorrect)try{u.current.playAnimation(e.incorrect,!0)}catch{u.current.setBodyMovement("idle")}u.current.setBodyMovement("gesturing");const Z=y()?.questions?.length||0,se=r.current.currentQuestionIndex>=Z-1,te=r.current.currentQuestionIndex<Z-1;console.log("[CurriculumLearning] Answer feedback (incorrect) - questionIndex:",r.current.currentQuestionIndex,"totalQuestions:",Z,"hasNextQuestion:",te);const J=L.type==="code_test"?`Your code didn't pass all the tests. ${L.explanation||"Try again!"}`:`Not quite right, but don't worry! ${L.explanation||""}${se?"":" Let's move on to the next question."}`,he=P.current||{lipsyncLang:"en"};u.current.speakText(J,{lipsyncLang:he.lipsyncLang,onSpeechEnd:()=>{d.current.onCustomAction({type:"answerFeedbackComplete",moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex,questionIndex:r.current.currentQuestionIndex,isCorrect:!1,hasNextQuestion:te,score:r.current.score,totalQuestions:r.current.totalQuestions})}})}else{const Z=y()?.questions?.length||0;d.current.onCustomAction({type:"answerFeedbackComplete",moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex,questionIndex:r.current.currentQuestionIndex,isCorrect:F,hasNextQuestion:r.current.currentQuestionIndex<Z-1,score:r.current.score,totalQuestions:r.current.totalQuestions,avatarNotReady:!0})}},[e.correct,e.incorrect,E,y,b]),ae=x.useCallback(I=>{const L=E();if(!I||typeof I!="object"){console.error("Invalid code test result format. Expected object with {passed: boolean, ...}");return}if(L?.type!=="code_test"){console.warn("Current question is not a code test. Use handleAnswerSelect for other question types.");return}const F={passed:I.passed===!0,results:I.results||[],output:I.output||"",error:I.error||null,executionTime:I.executionTime||null,testCount:I.testCount||0,passedCount:I.passedCount||0,failedCount:I.failedCount||0};d.current.onCustomAction({type:"codeTestSubmitted",moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex,questionIndex:r.current.currentQuestionIndex,testResult:F,question:L}),p.current&&p.current(F)},[E,b]),re=x.useCallback(()=>{if(r.current.currentQuestionIndex>0){r.current.currentQuestionIndex-=1;const I=E();I&&d.current.onCustomAction({type:"questionStart",moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex,questionIndex:r.current.currentQuestionIndex,totalQuestions:r.current.totalQuestions,question:I,score:r.current.score});const L=()=>{if(!u.current||!I)return;u.current.setMood("happy"),u.current.setBodyMovement("idle");const F=P.current||{lipsyncLang:"en"};I.type==="code_test"?u.current.speakText(`Let's go back to this coding challenge: ${I.question}`,{lipsyncLang:F.lipsyncLang}):u.current.speakText(`Going back to: ${I.question}`,{lipsyncLang:F.lipsyncLang})};if(u.current&&u.current.isReady&&I)L();else if(I){const F=setInterval(()=>{u.current&&u.current.isReady&&(clearInterval(F),L())},100);setTimeout(()=>{clearInterval(F)},5e3)}}},[E]),xe=x.useCallback(()=>{const I=O.current||{modules:[]};if(I.modules[r.current.currentModuleIndex],r.current.currentLessonIndex>0)r.current.currentLessonIndex-=1,r.current.currentQuestionIndex=0,r.current.lessonCompleted=!1,r.current.isQuestionMode=!1,r.current.isTeaching=!1,r.current.score=0,r.current.totalQuestions=0,d.current.onCustomAction({type:"lessonStart",moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex}),d.current.onLessonStart({moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex,lesson:y()}),u.current&&(u.current.setMood("happy"),u.current.setBodyMovement("idle"));else if(r.current.currentModuleIndex>0){const D=I.modules[r.current.currentModuleIndex-1];r.current.currentModuleIndex-=1,r.current.currentLessonIndex=(D?.lessons?.length||1)-1,r.current.currentQuestionIndex=0,r.current.lessonCompleted=!1,r.current.isQuestionMode=!1,r.current.isTeaching=!1,r.current.score=0,r.current.totalQuestions=0,d.current.onCustomAction({type:"lessonStart",moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex}),d.current.onLessonStart({moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex,lesson:y()}),u.current&&(u.current.setMood("happy"),u.current.setBodyMovement("idle"))}},[y]),oe=x.useCallback(()=>{r.current.currentModuleIndex=0,r.current.currentLessonIndex=0,r.current.currentQuestionIndex=0,r.current.isTeaching=!1,r.current.isQuestionMode=!1,r.current.lessonCompleted=!1,r.current.curriculumCompleted=!1,r.current.score=0,r.current.totalQuestions=0},[]),Se=x.useCallback(I=>{console.log("Avatar is ready!",I);const L=y(),F=L?.avatar_script||L?.body;c&&F&&setTimeout(()=>{h.current&&h.current()},10)},[c,y]);x.useLayoutEffect(()=>{h.current=j,g.current=G,v.current=k,R.current=W,S.current=T,M.current=A,p.current=Y}),x.useImperativeHandle(l,()=>({startTeaching:j,startQuestions:A,handleAnswerSelect:Y,handleCodeTestResult:ae,nextQuestion:W,previousQuestion:re,nextLesson:G,previousLesson:xe,completeLesson:k,completeCurriculum:T,resetCurriculum:oe,getState:()=>({...r.current}),getCurrentQuestion:()=>E(),getCurrentLesson:()=>y(),getAvatarRef:()=>u.current,speakText:async(I,L={})=>{await u.current?.resumeAudioContext?.();const F=P.current||{lipsyncLang:"en"};u.current?.speakText(I,{...L,lipsyncLang:L.lipsyncLang||F.lipsyncLang})},resumeAudioContext:async()=>{if(u.current?.resumeAudioContext)return await u.current.resumeAudioContext();const I=u.current?.talkingHead;if(I?.audioCtx){const L=I.audioCtx;if(L.state==="suspended"||L.state==="interrupted")try{await L.resume(),console.log("Audio context resumed via talkingHead")}catch(F){console.warn("Failed to resume audio context:",F)}}else console.warn("Audio context not available yet")},stopSpeaking:()=>u.current?.stopSpeaking(),pauseSpeaking:()=>u.current?.pauseSpeaking(),resumeSpeaking:async()=>await u.current?.resumeSpeaking(),isPaused:()=>u.current&&typeof u.current.isPaused<"u"?u.current.isPaused:!1,setMood:I=>u.current?.setMood(I),playAnimation:(I,L)=>u.current?.playAnimation(I,L),setBodyMovement:I=>u.current?.setBodyMovement(I),setMovementIntensity:I=>u.current?.setMovementIntensity(I),playRandomDance:()=>u.current?.playRandomDance(),playReaction:I=>u.current?.playReaction(I),playCelebration:()=>u.current?.playCelebration(),setShowFullAvatar:I=>u.current?.setShowFullAvatar(I),setTimingAdjustment:I=>u.current?.setTimingAdjustment(I),lockAvatarPosition:()=>u.current?.lockAvatarPosition(),unlockAvatarPosition:()=>u.current?.unlockAvatarPosition(),triggerCustomAction:(I,L)=>{d.current.onCustomAction({type:I,...L,state:{...r.current}})},handleResize:()=>u.current?.handleResize(),isAvatarReady:()=>u.current?.isReady||!1}),[j,A,Y,ae,W,G,k,T,oe,E,y]);const $=P.current||{avatarUrl:"/avatars/brunette.glb",avatarBody:"F",mood:"happy",ttsLang:"en",ttsService:null,ttsVoice:null,ttsApiKey:null,bodyMovement:"gesturing",movementIntensity:.7,showFullAvatar:!1,animations:e};return V.jsx("div",{style:{width:"100%",height:"100%"},children:V.jsx(Ze,{ref:u,avatarUrl:$.avatarUrl,avatarBody:$.avatarBody,mood:$.mood,ttsLang:$.ttsLang,ttsService:$.ttsService,ttsVoice:$.ttsVoice,ttsApiKey:$.ttsApiKey,bodyMovement:$.bodyMovement,movementIntensity:$.movementIntensity,showFullAvatar:$.showFullAvatar,cameraView:"upper",animations:$.animations,onReady:Se,onLoading:()=>{},onError:I=>{console.error("Avatar error:",I)}})})});$e.displayName="CurriculumLearning";function wt({manifestPath:N="/animations/manifest.json",avatarBody:t="F",onAnimationPlay:e=null,onAnimationsSelected:n=null,onDeleteAnimations:o=null,style:s={}}){const[i,a]=x.useState([]),[c,l]=x.useState(new Set),[u,r]=x.useState(!0),[d,h]=x.useState(null),[g,v]=x.useState("all"),[R,S]=x.useState("");x.useEffect(()=>{(async()=>{r(!0),h(null);try{const T=await Ee(N),A=[];if(T._genderSpecific){const G=(t?.toUpperCase()||"F")==="M"?"male":"female";T._genderSpecific[G]&&Object.entries(T._genderSpecific[G]).forEach(([j,Y])=>{(Array.isArray(Y)?Y:[Y]).forEach(re=>{A.push({path:re,group:j,gender:G,name:re.split("/").pop().replace(".fbx","")})})}),T._genderSpecific.shared&&Object.entries(T._genderSpecific.shared).forEach(([j,Y])=>{(Array.isArray(Y)?Y:[Y]).forEach(re=>{A.push({path:re,group:j,gender:"shared",name:re.split("/").pop().replace(".fbx","")})})})}Object.entries(T).forEach(([W,G])=>{W!=="_genderSpecific"&&(Array.isArray(G)?G:[G]).forEach(Y=>{typeof Y=="string"&&A.push({path:Y,group:W,gender:"root",name:Y.split("/").pop().replace(".fbx","")})})}),a(A),r(!1)}catch(T){console.error("Failed to load animations:",T),h(T.message),r(!1)}})()},[N,t]);const M=["all",...new Set(i.map(k=>k.group))],p=i.filter(k=>{const T=g==="all"||k.group===g,A=R===""||k.name.toLowerCase().includes(R.toLowerCase())||k.path.toLowerCase().includes(R.toLowerCase());return T&&A}),O=k=>{const T=new Set(c);T.has(k)?T.delete(k):T.add(k),l(T),n&&n(Array.from(T))},P=()=>{const k=new Set(c);p.forEach(T=>{k.add(T.path)}),l(k),n&&n(Array.from(k))},y=()=>{const k=new Set(c);p.forEach(T=>{k.delete(T.path)}),l(k),n&&n(Array.from(k))},E=()=>{const T=i.filter(A=>!c.has(A.path)).map(A=>A.path);if(T.length===0){alert("No animations to delete. Select animations to keep, then delete will remove the unselected ones.");return}window.confirm(`Are you sure you want to delete ${T.length} animation(s)?
7
+ `,s=await fetch(this.opt.ttsEndpoint,{method:"POST",headers:{"Ocp-Apim-Subscription-Key":this.opt.ttsApikey,"Content-Type":"application/ssml+xml","X-Microsoft-OutputFormat":"audio-16khz-128kbitrate-mono-mp3"},body:o});if(!s.ok)throw new Error(`Azure TTS error: ${s.status} ${s.statusText}`);const i=await s.arrayBuffer(),a=await this.audioCtx.decodeAudioData(i),c=await this.audioAnalyzer.analyzeAudio(a,e),l=[];for(let r=0;r<c.visemes.length;r++){const d=c.visemes[r],h=d.startTime*1e3,g=d.duration*1e3,v=d.intensity;l.push({template:{name:"viseme"},ts:[h-Math.min(60,2*g/3),h+Math.min(25,g/2),h+g+Math.min(60,g/2)],vs:{["viseme_"+d.viseme]:[null,v,0]}})}const u=[...t.anim,...l];this.audioPlaylist.push({anim:u,audio:a}),this.onSubtitles=t.onSubtitles||null,this.resetLips(),t.mood&&this.setMood(t.mood),this.playAudio()}async synthesizeWithExternalTTS(t){let e="<speak>";t.text.forEach((i,a)=>{a>0&&(e+=" <mark name='"+i.mark+"'/>"),e+=i.word.replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;").replaceAll("'","&apos;").replace(new RegExp("^\\p{Dash_Punctuation}$","ug"),'<break time="750ms"/>')}),e+="</speak>";const n={method:"POST",headers:{"Content-Type":"application/json; charset=utf-8"},body:JSON.stringify({input:{ssml:e},voice:{languageCode:t.lang||this.avatar.ttsLang||this.opt.ttsLang,name:t.voice||this.avatar.ttsVoice||this.opt.ttsVoice},audioConfig:{audioEncoding:this.ttsAudioEncoding,speakingRate:(t.rate||this.avatar.ttsRate||this.opt.ttsRate)+this.mood.speech.deltaRate,pitch:(t.pitch||this.avatar.ttsPitch||this.opt.ttsPitch)+this.mood.speech.deltaPitch,volumeGainDb:(t.volume||this.avatar.ttsVolume||this.opt.ttsVolume)+this.mood.speech.deltaVolume},enableTimePointing:[1]})};this.opt.jwtGet&&typeof this.opt.jwtGet=="function"&&(n.headers.Authorization="Bearer "+await this.opt.jwtGet());const o=await fetch(this.opt.ttsEndpoint+(this.opt.ttsApikey?"?key="+this.opt.ttsApikey:""),n),s=await o.json();if(o.status===200&&s&&s.audioContent){const i=this.b64ToArrayBuffer(s.audioContent),a=await this.audioCtx.decodeAudioData(i);this.speakWithHands();const c=[0];let l=0;t.text.forEach((d,h)=>{if(h>0){let g=c[c.length-1];s.timepoints[l]&&(g=s.timepoints[l].timeSeconds*1e3,s.timepoints[l].markName===""+d.mark&&l++),c.push(g)}});const u=[{mark:0,time:0}];c.forEach((d,h)=>{if(h>0){let g=d-c[h-1];u[h-1].duration=g,u.push({mark:h,time:d})}});let r=1e3*a.duration;r>this.opt.ttsTrimEnd&&(r=r-this.opt.ttsTrimEnd),u[u.length-1].duration=r-u[u.length-1].time,t.anim.forEach(d=>{const h=u[d.mark];if(h)for(let g=0;g<d.ts.length;g++)d.ts[g]=h.time+d.ts[g]*h.duration+this.opt.ttsTrimStart}),this.audioPlaylist.push({anim:t.anim,audio:a}),this.onSubtitles=t.onSubtitles||null,this.resetLips(),t.mood&&this.setMood(t.mood),this.playAudio()}else this.startSpeaking(!0)}async startSpeaking(t=!1){if(!(!this.armature||this.isSpeaking&&!t))if(this.stateName="speaking",this.isSpeaking=!0,this.speechQueue.length){let e=this.speechQueue.shift();if(e.emoji){this.lookAtCamera(500);let n=e.emoji.dt.reduce((o,s)=>o+s,0);this.animQueue.push(this.animFactory(e.emoji)),setTimeout(this.startSpeaking.bind(this),n,!0)}else if(e.break)setTimeout(this.startSpeaking.bind(this),e.break,!0);else if(e.audio)e.isRaw||(this.lookAtCamera(500),this.speakWithHands(),this.resetLips()),this.audioPlaylist.push({anim:e.anim,audio:e.audio,isRaw:e.isRaw}),this.onSubtitles=e.onSubtitles||null,e.mood&&this.setMood(e.mood),this.playAudio();else if(e.text){this.lookAtCamera(500);try{!this.opt.ttsEndpoint||this.opt.ttsEndpoint===""?await this.synthesizeWithBrowserTTS(e):this.opt.ttsService==="elevenlabs"?await this.synthesizeWithElevenLabsTTS(e):this.opt.ttsService==="deepgram"?await this.synthesizeWithDeepgramTTS(e):this.opt.ttsService==="azure"?await this.synthesizeWithAzureTTS(e):await this.synthesizeWithExternalTTS(e)}catch(n){console.error("Error:",n),this.startSpeaking(!0)}}else e.anim?(this.onSubtitles=e.onSubtitles||null,this.resetLips(),e.mood&&this.setMood(e.mood),e.anim.forEach((n,o)=>{for(let s=0;s<n.ts.length;s++)n.ts[s]=this.animClock+10*o;this.animQueue.push(n)}),setTimeout(this.startSpeaking.bind(this),10*e.anim.length,!0)):e.marker?(typeof e.marker=="function"&&e.marker(),this.startSpeaking(!0)):this.startSpeaking(!0)}else this.stateName="idle",this.isSpeaking=!1}pauseSpeaking(){try{this.audioSpeechSource.stop()}catch{}this.audioPlaylist.length=0,this.stateName="idle",this.isSpeaking=!1,this.isAudioPlaying=!1,this.animQueue=this.animQueue.filter(t=>t.template.name!=="viseme"&&t.template.name!=="subtitles"&&t.template.name!=="blendshapes"),this.armature&&(this.resetLips(),this.render())}stopSpeaking(){try{this.audioSpeechSource.stop()}catch{}this.audioPlaylist.length=0,this.speechQueue.length=0,this.animQueue=this.animQueue.filter(t=>t.template.name!=="viseme"&&t.template.name!=="subtitles"&&t.template.name!=="blendshapes"),this.stateName="idle",this.isSpeaking=!1,this.isAudioPlaying=!1,this.armature&&(this.resetLips(),this.render())}async streamStart(t={},e=null,n=null,o=null,s=null){if(this.stopSpeaking(),this.isStreaming=!0,t.waitForAudioChunks!==void 0&&(this.streamWaitForAudioChunks=t.waitForAudioChunks),this.streamWaitForAudioChunks||(this.streamAudioStartTime=this.animClock),this.streamLipsyncQueue=[],this.streamLipsyncType=t.lipsyncType||this.streamLipsyncType||"visemes",this.streamLipsyncLang=t.lipsyncLang||this.streamLipsyncLang||this.avatar.lipsyncLang||this.opt.lipsyncLang,this.onAudioStart=e,this.onAudioEnd=n,this.onMetrics=s,t.sampleRate!==void 0){const a=t.sampleRate;typeof a=="number"&&a>=8e3&&a<=96e3?a!==this.audioCtx.sampleRate&&this.initAudioGraph(a):console.warn("Invalid sampleRate provided. It must be a number between 8000 and 96000 Hz.")}if(t.gain!==void 0&&(this.audioStreamGainNode.gain.value=t.gain),!this.streamWorkletNode||!this.streamWorkletNode.port||this.streamWorkletNode.numberOfOutputs===0||this.streamWorkletNode.context!==this.audioCtx){if(this.streamWorkletNode)try{this.streamWorkletNode.disconnect(),this.streamWorkletNode=null}catch{}if(!this.workletLoaded)try{const a=this.audioCtx.audioWorklet.addModule(At.href),c=new Promise((l,u)=>setTimeout(()=>u(new Error("Worklet loading timed out")),5e3));await Promise.race([a,c]),this.workletLoaded=!0}catch(a){throw console.error("Failed to load audio worklet:",a),new Error("Failed to initialize streaming speech")}this.streamWorkletNode=new AudioWorkletNode(this.audioCtx,"playback-worklet",{processorOptions:{sampleRate:this.audioCtx.sampleRate,metrics:t.metrics||{enabled:!1}}}),this.streamWorkletNode.connect(this.audioStreamGainNode),this.streamWorkletNode.connect(this.audioAnalyzerNode),this.streamWorkletNode.port.onmessage=a=>{if(a.data.type==="playback-started"&&(this.isSpeaking=!0,this.stateName="speaking",this.streamWaitForAudioChunks&&(this.streamAudioStartTime=this.animClock),this._processStreamLipsyncQueue(),this.speakWithHands(),this.onAudioStart))try{this.onAudioStart?.()}catch(c){console.error(c)}if(a.data.type==="playback-ended"&&(this._streamPause(),this.onAudioEnd))try{this.onAudioEnd()}catch{}if(this.onMetrics&&a.data.type==="metrics")try{this.onMetrics(a.data)}catch{}}}if(t.metrics)try{this.streamWorkletNode.port.postMessage({type:"config-metrics",data:t.metrics})}catch{}if(this.resetLips(),this.lookAtCamera(500),t.mood&&this.setMood(t.mood),this.onSubtitles=o||null,this.audioCtx.state==="suspended"||this.audioCtx.state==="interrupted"){const a=this.audioCtx.resume(),c=new Promise((l,u)=>setTimeout(()=>u("p2"),1e3));try{await Promise.race([a,c])}catch{console.warn("Can't play audio. Web Audio API suspended. This is often due to calling some speak method before the first user action, which is typically prevented by the browser.");return}}}streamNotifyEnd(){!this.isStreaming||!this.streamWorkletNode||this.streamWorkletNode.port.postMessage({type:"no-more-data"})}streamInterrupt(){if(!this.isStreaming)return;const t=this.isSpeaking;if(this.streamWorkletNode)try{this.streamWorkletNode.port.postMessage({type:"stop"})}catch{}if(this._streamPause(!0),t&&this.onAudioEnd)try{this.onAudioEnd()}catch{}}streamStop(){if(this.isStreaming){if(this.streamInterrupt(),this.streamWorkletNode){try{this.streamWorkletNode.disconnect()}catch{}this.streamWorkletNode=null}this.isStreaming=!1}}_streamPause(t=!1){this.isSpeaking=!1,this.stateName="idle",t&&(this.streamWaitForAudioChunks&&(this.streamAudioStartTime=null),this.streamLipsyncQueue=[],this.animQueue=this.animQueue.filter(e=>e.template.name!=="viseme"&&e.template.name!=="subtitles"&&e.template.name!=="blendshapes"),this.armature&&(this.resetLips(),this.render()))}_processStreamLipsyncQueue(){if(this.isStreaming)for(;this.streamLipsyncQueue.length>0;){const t=this.streamLipsyncQueue.shift();this._processLipsyncData(t,this.streamAudioStartTime)}}_processLipsyncData(t,e){if(this.isStreaming){if(t.visemes&&this.streamLipsyncType=="visemes")for(let n=0;n<t.visemes.length;n++){const o=t.visemes[n],s=e+t.vtimes[n],i=t.vdurations[n],a={template:{name:"viseme"},ts:[s-2*i/3,s+i/2,s+i+i/2],vs:{["viseme_"+o]:[null,o==="PP"||o==="FF"?.9:.6,0]}};this.animQueue.push(a)}if(t.words&&(this.onSubtitles||this.streamLipsyncType=="words"))for(let n=0;n<t.words.length;n++){const o=t.words[n],s=t.wtimes[n];let i=t.wdurations[n];if(o.length&&(this.onSubtitles&&this.animQueue.push({template:{name:"subtitles"},ts:[e+s],vs:{subtitles:[" "+o]}}),this.streamLipsyncType=="words")){const a=this.streamLipsyncLang||this.avatar.lipsyncLang||this.opt.lipsyncLang,c=this.lipsyncPreProcessText(o,a),l=this.lipsyncWordsToVisemes(c,a);if(l&&l.visemes&&l.visemes.length){const u=l.times[l.visemes.length-1]+l.durations[l.visemes.length-1],r=Math.min(i,Math.max(0,i-l.visemes.length*150));let d=.6+this.convertRange(r,[0,i],[0,.4]);if(i=Math.min(i,l.visemes.length*200),u>0)for(let h=0;h<l.visemes.length;h++){const g=e+s+l.times[h]/u*i,v=l.durations[h]/u*i;this.animQueue.push({template:{name:"viseme"},ts:[g-Math.min(60,2*v/3),g+Math.min(25,v/2),g+v+Math.min(60,v/2)],vs:{["viseme_"+l.visemes[h]]:[null,l.visemes[h]==="PP"||l.visemes[h]==="FF"?.9:d,0]}})}}}}if(t.anims&&this.streamLipsyncType=="blendshapes")for(let n=0;n<t.anims.length;n++){let o=t.anims[n];o.delay+=e;let s=this.animFactory(o,!1,1,1,!0);this.animQueue.push(s)}}}streamAudio(t){if(!(!this.isStreaming||!this.streamWorkletNode)){if(this.isSpeaking||(this.streamLipsyncQueue=[],this.streamAudioStartTime=null),this.isSpeaking=!0,this.stateName="speaking",t.audio!==void 0){const e={type:"audioData",data:null};if(t.audio instanceof ArrayBuffer)e.data=t.audio,this.streamWorkletNode.port.postMessage(e,[e.data]);else if(t.audio instanceof Int16Array||t.audio instanceof Uint8Array){const n=t.audio.buffer.slice(t.audio.byteOffset,t.audio.byteOffset+t.audio.byteLength);e.data=n,this.streamWorkletNode.port.postMessage(e,[e.data])}else if(t.audio instanceof Float32Array){const n=new Int16Array(t.audio.length);for(let o=0;o<t.audio.length;o++){let s=Math.max(-1,Math.min(1,t.audio[o]));n[o]=s<0?s*32768:s*32767}e.data=n.buffer,this.streamWorkletNode.port.postMessage(e,[e.data])}else console.error("r.audio is not a supported type. Must be ArrayBuffer, Int16Array, Uint8Array, or Float32Array:",t.audio)}if(t.visemes||t.anims||t.words){if(this.streamWaitForAudioChunks&&!this.streamAudioStartTime){this.streamLipsyncQueue.length>=200&&this.streamLipsyncQueue.shift(),this.streamLipsyncQueue.push(t);return}else!this.streamWaitForAudioChunks&&!this.streamAudioStartTime&&(this.streamAudioStartTime=this.animClock);this._processLipsyncData(t,this.streamAudioStartTime)}}}makeEyeContact(t){this.animQueue.push(this.animFactory({name:"eyecontact",dt:[0,t],vs:{eyeContact:[1]}}))}lookAhead(t){if(t){let e=(Math.random()-.5)/4,n=(Math.random()-.5)/4,o=this.animQueue.findIndex(i=>i.template.name==="lookat");o!==-1&&this.animQueue.splice(o,1);const s={name:"lookat",dt:[750,t],vs:{bodyRotateX:[e],bodyRotateY:[n],eyesRotateX:[-3*e+.1],eyesRotateY:[-5*n],browInnerUp:[[0,.7]],mouthLeft:[[0,.7]],mouthRight:[[0,.7]],eyeContact:[0],headMove:[0]}};this.animQueue.push(this.animFactory(s))}}lookAtCamera(t){let e;if(this.speakTo&&(e=new f.Vector3,this.speakTo.objectLeftEye?.isObject3D?(this.speakTo.armature.objectHead,this.speakTo.objectLeftEye.updateMatrixWorld(!0),this.speakTo.objectRightEye.updateMatrixWorld(!0),we.setFromMatrixPosition(this.speakTo.objectLeftEye.matrixWorld),He.setFromMatrixPosition(this.speakTo.objectRightEye.matrixWorld),e.addVectors(we,He).divideScalar(2)):this.speakTo.isObject3D?this.speakTo.getWorldPosition(e):this.speakTo.isVector3?e.set(this.speakTo):this.speakTo.x&&this.speakTo.y&&this.speakTo.z&&e.set(this.speakTo.x,this.speakTo.y,this.speakTo.z)),!e){if(this.avatar.hasOwnProperty("avatarIgnoreCamera")){if(this.avatar.avatarIgnoreCamera){this.lookAhead(t);return}}else if(this.opt.avatarIgnoreCamera){this.lookAhead(t);return}this.lookAt(null,null,t);return}this.objectLeftEye.updateMatrixWorld(!0),this.objectRightEye.updateMatrixWorld(!0),we.setFromMatrixPosition(this.objectLeftEye.matrixWorld),He.setFromMatrixPosition(this.objectRightEye.matrixWorld),we.add(He).divideScalar(2),ne.copy(this.armature.quaternion),ne.multiply(this.poseTarget.props["Hips.quaternion"]),ne.multiply(this.poseTarget.props["Spine.quaternion"]),ne.multiply(this.poseTarget.props["Spine1.quaternion"]),ne.multiply(this.poseTarget.props["Spine2.quaternion"]),ne.multiply(this.poseTarget.props["Neck.quaternion"]),ne.multiply(this.poseTarget.props["Head.quaternion"]);const n=new f.Vector3().subVectors(e,we).normalize(),o=Math.atan2(n.x,n.z),s=Math.asin(-n.y);q.set(s,o,0,"YXZ");const a=new f.Quaternion().setFromEuler(q),c=new f.Quaternion().copy(a).multiply(ne.clone().invert());q.setFromQuaternion(c,"YXZ");let l=q.x/(40/24)+.2,u=q.y/(9/4),r=Math.min(.6,Math.max(-.3,l)),d=Math.min(.8,Math.max(-.8,u)),h=(Math.random()-.5)/4,g=(Math.random()-.5)/4;if(t){let v=this.animQueue.findIndex(S=>S.template.name==="lookat");v!==-1&&this.animQueue.splice(v,1);const R={name:"lookat",dt:[750,t],vs:{bodyRotateX:[r+h],bodyRotateY:[d+g],eyesRotateX:[-3*h+.1],eyesRotateY:[-5*g],browInnerUp:[[0,.7]],mouthLeft:[[0,.7]],mouthRight:[[0,.7]],eyeContact:[0],headMove:[0]}};this.animQueue.push(this.animFactory(R))}}lookAt(t,e,n){if(!this.camera)return;const o=this.nodeAvatar.getBoundingClientRect();this.objectLeftEye.updateMatrixWorld(!0),this.objectRightEye.updateMatrixWorld(!0);const s=new f.Vector3().setFromMatrixPosition(this.objectLeftEye.matrixWorld),i=new f.Vector3().setFromMatrixPosition(this.objectRightEye.matrixWorld),a=new f.Vector3().addVectors(s,i).divideScalar(2);a.project(this.camera);let c=(a.x+1)/2*o.width+o.left,l=-(a.y-1)/2*o.height+o.top;t===null&&(t=c),e===null&&(e=l),ne.copy(this.armature.quaternion),ne.multiply(this.poseTarget.props["Hips.quaternion"]),ne.multiply(this.poseTarget.props["Spine.quaternion"]),ne.multiply(this.poseTarget.props["Spine1.quaternion"]),ne.multiply(this.poseTarget.props["Spine2.quaternion"]),ne.multiply(this.poseTarget.props["Neck.quaternion"]),ne.multiply(this.poseTarget.props["Head.quaternion"]),q.setFromQuaternion(ne);let u=q.x/(40/24),r=q.y/(9/4),d=Math.min(.4,Math.max(-.4,this.camera.rotation.x)),h=Math.min(.4,Math.max(-.4,this.camera.rotation.y)),g=Math.max(window.innerWidth-c,c),v=Math.max(window.innerHeight-l,l),R=this.convertRange(e,[l-v,l+v],[-.3,.6])-u+d,S=this.convertRange(t,[c-g,c+g],[-.8,.8])-r+h;R=Math.min(.6,Math.max(-.3,R)),S=Math.min(.8,Math.max(-.8,S));let M=(Math.random()-.5)/4,p=(Math.random()-.5)/4;if(n){let O=this.animQueue.findIndex(y=>y.template.name==="lookat");O!==-1&&this.animQueue.splice(O,1);const P={name:"lookat",dt:[750,n],vs:{bodyRotateX:[R+M],bodyRotateY:[S+p],eyesRotateX:[-3*M+.1],eyesRotateY:[-5*p],browInnerUp:[[0,.7]],mouthLeft:[[0,.7]],mouthRight:[[0,.7]],eyeContact:[0],headMove:[0]}};this.animQueue.push(this.animFactory(P))}}touchAt(t,e){if(!this.camera)return;const n=this.nodeAvatar.getBoundingClientRect(),o=new f.Vector2((t-n.left)/n.width*2-1,-((e-n.top)/n.height)*2+1),s=new f.Raycaster;s.setFromCamera(o,this.camera);const i=s.intersectObject(this.armature);if(i.length>0){const a=i[0].point,c=new f.Vector3,l=new f.Vector3;this.objectLeftArm.getWorldPosition(c),this.objectRightArm.getWorldPosition(l);const u=c.distanceToSquared(a),r=l.distanceToSquared(a);u<r?(this.ikSolve({iterations:20,root:"LeftShoulder",effector:"LeftHandMiddle1",links:[{link:"LeftHand",minx:-.5,maxx:.5,miny:-1,maxy:1,minz:-.5,maxz:.5,maxAngle:.1},{link:"LeftForeArm",minx:-.5,maxx:1.5,miny:-1.5,maxy:1.5,minz:-.5,maxz:3,maxAngle:.2},{link:"LeftArm",minx:-1.5,maxx:1.5,miny:0,maxy:0,minz:-1,maxz:3}]},a,!1,1e3),this.setValue("handFistLeft",0)):(this.ikSolve({iterations:20,root:"RightShoulder",effector:"RightHandMiddle1",links:[{link:"RightHand",minx:-.5,maxx:.5,miny:-1,maxy:1,minz:-.5,maxz:.5,maxAngle:.1},{link:"RightForeArm",minx:-.5,maxx:1.5,miny:-1.5,maxy:1.5,minz:-3,maxz:.5,maxAngle:.2},{link:"RightArm",minx:-1.5,maxx:1.5,miny:0,maxy:0,minz:-1,maxz:3}]},a,!1,1e3),this.setValue("handFistRight",0))}else["LeftArm","LeftForeArm","LeftHand","RightArm","RightForeArm","RightHand"].forEach(a=>{let c=a+".quaternion";this.poseTarget.props[c].copy(this.getPoseTemplateProp(c)),this.poseTarget.props[c].t=this.animClock,this.poseTarget.props[c].d=1e3});return i.length>0}speakWithHands(t=0,e=.5){if(this.mixer||this.gesture||!this.poseTarget.template.standing||this.poseTarget.template.bend||Math.random()>e)return;this.ikSolve({root:"LeftShoulder",effector:"LeftHandMiddle1",links:[{link:"LeftHand",minx:-.5,maxx:.5,miny:-1,maxy:1,minz:-.5,maxz:.5},{link:"LeftForeArm",minx:-.5,maxx:1.5,miny:-1.5,maxy:1.5,minz:-.5,maxz:3},{link:"LeftArm",minx:-1.5,maxx:1.5,miny:-1.5,maxy:1.5,minz:-1,maxz:3}]},new f.Vector3(this.gaussianRandom(0,.5),this.gaussianRandom(-.8,-.2),this.gaussianRandom(0,.5)),!0),this.ikSolve({root:"RightShoulder",effector:"RightHandMiddle1",links:[{link:"RightHand",minx:-.5,maxx:.5,miny:-1,maxy:1,minz:-.5,maxz:.5},{link:"RightForeArm",minx:-.5,maxx:1.5,miny:-1.5,maxy:1.5,minz:-3,maxz:.5},{link:"RightArm"}]},new f.Vector3(this.gaussianRandom(-.5,0),this.gaussianRandom(-.8,-.2),this.gaussianRandom(0,.5)),!0);const n=[],o=[];n.push(100+Math.round(Math.random()*500)),o.push({duration:1e3,props:{"LeftHand.quaternion":new f.Quaternion().setFromEuler(new f.Euler(0,-1-Math.random(),0)),"RightHand.quaternion":new f.Quaternion().setFromEuler(new f.Euler(0,1+Math.random(),0))}}),["LeftArm","LeftForeArm","RightArm","RightForeArm"].forEach(i=>{o[0].props[i+".quaternion"]=this.ikMesh.getObjectByName(i).quaternion.clone()}),n.push(1e3+Math.round(Math.random()*500)),o.push({duration:2e3,props:{}}),["LeftArm","LeftForeArm","RightArm","RightForeArm","LeftHand","RightHand"].forEach(i=>{o[1].props[i+".quaternion"]=null});const s=this.animFactory({name:"talkinghands",delay:t,dt:n,vs:{moveto:o}});this.animQueue.push(s)}getSlowdownRate(t){return this.animSlowdownRate}setSlowdownRate(t){this.animSlowdownRate=t,this.audioSpeechSource.playbackRate.value=1/this.animSlowdownRate,this.audioBackgroundSource.playbackRate.value=1/this.animSlowdownRate}getAutoRotateSpeed(t){return this.controls.autoRotateSpeed}setAutoRotateSpeed(t){this.controls.autoRotateSpeed=t,this.controls.autoRotate=t>0}start(){this.armature&&this.isRunning===!1&&(this.audioCtx.resume(),this.animTimeLast=performance.now(),this.isRunning=!0,this.isAvatarOnly||requestAnimationFrame(this.animate.bind(this)))}stop(){this.isRunning=!1,this.audioCtx.suspend()}startListening(t,e={},n=null){this.listeningAnalyzer=t,this.listeningAnalyzer.fftSize=256,this.listeningAnalyzer.smoothingTimeConstant=.1,this.listeningAnalyzer.minDecibels=-70,this.listeningAnalyzer.maxDecibels=-10,this.listeningOnchange=n&&typeof n=="function"?n:null,this.listeningSilenceThresholdLevel=e?.hasOwnProperty("listeningSilenceThresholdLevel")?e.listeningSilenceThresholdLevel:this.opt.listeningSilenceThresholdLevel,this.listeningSilenceThresholdMs=e?.hasOwnProperty("listeningSilenceThresholdMs")?e.listeningSilenceThresholdMs:this.opt.listeningSilenceThresholdMs,this.listeningSilenceDurationMax=e?.hasOwnProperty("listeningSilenceDurationMax")?e.listeningSilenceDurationMax:this.opt.listeningSilenceDurationMax,this.listeningActiveThresholdLevel=e?.hasOwnProperty("listeningActiveThresholdLevel")?e.listeningActiveThresholdLevel:this.opt.listeningActiveThresholdLevel,this.listeningActiveThresholdMs=e?.hasOwnProperty("listeningActiveThresholdMs")?e.listeningActiveThresholdMs:this.opt.listeningActiveThresholdMs,this.listeningActiveDurationMax=e?.hasOwnProperty("listeningActiveDurationMax")?e.listeningActiveDurationMax:this.opt.listeningActiveDurationMax,this.listeningActive=!1,this.listeningVolume=0,this.listeningTimer=0,this.listeningTimerTotal=0,this.isListening=!0}stopListening(){this.isListening=!1}async playAnimation(t,e=null,n=10,o=0,s=.01,i=!1,a=null){if(!this.armature)return;this.positionWasLocked=!i,i||this.lockAvatarPosition();let c=this.animClips.find(l=>l.url===t+"-"+o);if(c){let l=this.animQueue.find(h=>h.template.name==="pose");l&&(l.ts[0]=1/0),Object.entries(c.pose.props).forEach(h=>{this.poseBase.props[h[0]]=h[1].clone(),this.poseTarget.props[h[0]]=h[1].clone(),this.poseTarget.props[h[0]].t=0,this.poseTarget.props[h[0]].d=1e3}),this.mixer||(this.mixer=new f.AnimationMixer(this.armature)),this.animationFinishedCallback=a;const u=()=>{this.animationFinishedCallback&&(this.animationFinishedCallback(),this.animationFinishedCallback=null),this.stopAnimation()};this.mixer.addEventListener("finished",u,{once:!0});let r=1;n>0&&(r=Math.ceil(n/c.clip.duration));const d=this.mixer.clipAction(c.clip);if(d.setLoop(f.LoopRepeat,r),d.clampWhenFinished=!0,this.currentFBXAction&&this.currentFBXAction.isRunning()){this.currentFBXAction.fadeOut(.3),setTimeout(()=>{this.currentFBXAction=d;try{d.fadeIn(.5).play(),console.log("FBX animation started successfully (with fade transition):",t)}catch(h){console.warn("FBX animation failed to start:",h),this.stopAnimation()}},300);return}this.currentFBXAction=d;try{d.fadeIn(.5).play(),console.log("FBX animation started successfully:",t)}catch(h){console.warn("FBX animation failed to start:",h),this.stopAnimation();return}if(d.getClip().tracks.length===0){console.warn("FBX animation has no valid tracks, stopping"),this.stopAnimation();return}}else{if(t.split(".").pop().toLowerCase()!=="fbx"){console.error(`Invalid file type for FBX animation: ${t}. Expected .fbx file.`);return}let u=!1;try{const h=await fetch(t,{method:"HEAD"});if(u=h.ok,!u){console.error(`FBX file not found at ${t}. Status: ${h.status}`),console.error("Please check:"),console.error("1. File path is correct (note: path is case-sensitive)"),console.error("2. File exists in your public folder"),console.error("3. File is accessible (not blocked by server)");return}}catch(h){console.warn(`Could not verify file existence for ${t}, attempting to load anyway:`,h)}const r=new je.FBXLoader;let d;try{d=await r.loadAsync(t,e)}catch(h){console.error(`Failed to load FBX animation from ${t}:`,h),console.error("Error details:",{message:h.message,url:t,suggestion:"Make sure the file is a valid FBX file and the path is correct"}),h.message&&h.message.includes("version number")&&(console.error("FBX Loader Error: Cannot find version number"),console.error("This error usually means:"),console.error("1. The file is not a valid FBX file (might be GLB, corrupted, or wrong format)"),console.error("2. The file might be corrupted"),console.error("3. The file path might be incorrect"),console.error("4. The server returned an HTML error page instead of the FBX file"),console.error("5. The file might not exist at that path"),console.error(""),console.error("Solution: Please verify:"),console.error(` - File exists at: ${t}`),console.error(" - File is a valid FBX binary file"),console.error(" - File path matches your public folder structure"),console.error(" - File is not corrupted"));try{const g=await fetch(t),v=g.headers.get("content-type"),R=await g.text();console.error("Response details:",{status:g.status,contentType:v,firstBytes:R.substring(0,100),isHTML:R.trim().startsWith("<!DOCTYPE")||R.trim().startsWith("<html")}),(R.trim().startsWith("<!DOCTYPE")||R.trim().startsWith("<html"))&&console.error("The server returned an HTML page instead of an FBX file. The file path is likely incorrect.")}catch(g){console.error("Could not fetch file for debugging:",g)}return}if(d&&d.animations&&d.animations[o]){let h=d.animations[o];const g=new Set;this.armature&&this.armature.traverse(y=>{(y.isBone||y.type==="Bone")&&g.add(y.name)});const v=new Map,R=y=>{if(g.has(y))return y;let E=y.replace(/^mixamorig/i,"").replace(/^CC_Base_/i,"").replace(/^RPM_/i,"");if(g.has(E))return E;const b=E.toLowerCase();if(b.includes("left")&&b.includes("arm")){if(b.includes("fore")||b.includes("lower")){if(g.has("LeftForeArm"))return"LeftForeArm";if(g.has("LeftForearm"))return"LeftForearm"}else if(!b.includes("fore")&&!b.includes("hand")&&g.has("LeftArm"))return"LeftArm"}if(b.includes("right")&&b.includes("arm")){if(b.includes("fore")||b.includes("lower")){if(g.has("RightForeArm"))return"RightForeArm";if(g.has("RightForearm"))return"RightForearm"}else if(!b.includes("fore")&&!b.includes("hand")&&g.has("RightArm"))return"RightArm"}if(b.includes("left")&&b.includes("hand")&&!b.includes("index")&&!b.includes("thumb")&&!b.includes("middle")&&!b.includes("ring")&&!b.includes("pinky")&&g.has("LeftHand"))return"LeftHand";if(b.includes("right")&&b.includes("hand")&&!b.includes("index")&&!b.includes("thumb")&&!b.includes("middle")&&!b.includes("ring")&&!b.includes("pinky")&&g.has("RightHand"))return"RightHand";if(b.includes("left")&&(b.includes("shoulder")||b.includes("clavicle"))&&g.has("LeftShoulder"))return"LeftShoulder";if(b.includes("right")&&(b.includes("shoulder")||b.includes("clavicle"))&&g.has("RightShoulder"))return"RightShoulder";const k={LeftArm:"LeftArm",leftArm:"LeftArm",LEFTARM:"LeftArm",RightArm:"RightArm",rightArm:"RightArm",RIGHTARM:"RightArm",LeftForeArm:"LeftForeArm",leftForeArm:"LeftForeArm",leftForearm:"LeftForeArm",LeftForearm:"LeftForeArm",RightForeArm:"RightForeArm",rightForeArm:"RightForeArm",rightForearm:"RightForeArm",RightForearm:"RightForeArm",LeftHand:"LeftHand",leftHand:"LeftHand",RightHand:"RightHand",rightHand:"RightHand",LeftShoulder:"LeftShoulder",leftShoulder:"LeftShoulder",RightShoulder:"RightShoulder",rightShoulder:"RightShoulder",Spine:"Spine1",spine:"Spine1",Spine1:"Spine1",Spine2:"Spine2",Head:"Head",head:"Head",Neck:"Neck",neck:"Neck",Hips:"Hips",hips:"Hips",Root:"Hips",root:"Hips"};if(k[E]){const T=k[E];if(g.has(T))return T}for(const T of g)if(T.toLowerCase()===b)return T;for(const T of g){const A=T.toLowerCase();if((b.includes("left")&&A.includes("left")||b.includes("right")&&A.includes("right"))&&(b.includes("arm")&&A.includes("arm")&&!A.includes("fore")||b.includes("forearm")&&A.includes("forearm")||b.includes("hand")&&A.includes("hand")&&!A.includes("index")&&!A.includes("thumb")||b.includes("shoulder")&&A.includes("shoulder")))return T}return null},S=new Set;h.tracks.forEach(y=>{const E=y.name.split(".");S.add(E[0])}),Array.from(S).filter(y=>y.toLowerCase().includes("arm")||y.toLowerCase().includes("hand")||y.toLowerCase().includes("shoulder")),Array.from(g).filter(y=>y.includes("Arm")||y.includes("Hand")||y.includes("Shoulder"));const M=[],p=new Set;h.tracks.forEach(y=>{const b=y.name.replaceAll("mixamorig","").split("."),k=b[0],T=b[1],A=R(k);if(!(A&&(A==="LeftShoulder"||A==="RightShoulder")&&(T==="quaternion"||T==="rotation")))if(A&&T){const W=`${A}.${T}`,G=y.clone();G.name=W,M.push(G),k!==A&&v.set(k,A)}else p.add(k),(k.toLowerCase().includes("arm")||k.toLowerCase().includes("hand")||k.toLowerCase().includes("shoulder"))&&console.warn(`⚠️ Arm bone "${k}" could not be mapped to avatar skeleton`)}),M.length>0?h=new f.AnimationClip(h.name,h.duration,M):console.error("No tracks could be mapped! Animation may not work correctly.");const O={};h.tracks.forEach(y=>{y.name=y.name.replaceAll("mixamorig","");const E=y.name.split(".");if(E[1]==="position"){for(let b=0;b<y.values.length;b++)y.values[b]=y.values[b]*s;O[y.name]=new f.Vector3(y.values[0],y.values[1],y.values[2])}else E[1]==="quaternion"?O[y.name]=new f.Quaternion(y.values[0],y.values[1],y.values[2],y.values[3]):E[1]==="rotation"&&(O[E[0]+".quaternion"]=new f.Quaternion().setFromEuler(new f.Euler(y.values[0],y.values[1],y.values[2],"XYZ")).normalize())});const P={props:O};O["Hips.position"]&&(O["Hips.position"].y<.5?P.lying=!0:P.standing=!0),this.animClips.push({url:t+"-"+o,clip:h,pose:P}),this.playAnimation(t,e,n,o,s)}else{const h="Animation "+t+" (ndx="+o+") not found";console.error(h),d&&d.animations?console.error(`FBX file loaded but has ${d.animations.length} animation(s), requested index ${o}`):console.error(d?"FBX file loaded but contains no animations":"FBX file failed to load or is invalid")}}}stopAnimation(){if(this.currentFBXAction&&(this.currentFBXAction.stop(),this.currentFBXAction=null),this.mixer&&this.mixer._actions.length===0&&(this.mixer=null),this.positionWasLocked&&this.unlockAvatarPosition(),this.gesture)for(let[e,n]of Object.entries(this.gesture))n.t=this.animClock,n.d=1e3,this.poseTarget.props.hasOwnProperty(e)&&(this.poseTarget.props[e].copy(n),this.poseTarget.props[e].t=this.animClock,this.poseTarget.props[e].d=1e3);let t=this.animQueue.find(e=>e.template.name==="pose");t&&(t.ts[0]=this.animClock),this.setPoseFromTemplate(null)}async playPose(t,e=null,n=5,o=0,s=.01){if(!this.armature)return;let i=this.poseTemplates[t];if(!i){const a=this.animPoses.find(c=>c.url===t+"-"+o);a&&(i=a.pose)}if(i){this.poseName=t,this.mixer=null;let a=this.animQueue.find(c=>c.template.name==="pose");a&&(a.ts[0]=this.animClock+n*1e3+2e3),this.setPoseFromTemplate(i)}else{let c=await new je.FBXLoader().loadAsync(t,e);if(c&&c.animations&&c.animations[o]){let l=c.animations[o];const u={};l.tracks.forEach(d=>{d.name=d.name.replaceAll("mixamorig","");const h=d.name.split(".");h[1]==="position"?u[d.name]=new f.Vector3(d.values[0]*s,d.values[1]*s,d.values[2]*s):h[1]==="quaternion"?u[d.name]=new f.Quaternion(d.values[0],d.values[1],d.values[2],d.values[3]):h[1]==="rotation"&&(u[h[0]+".quaternion"]=new f.Quaternion().setFromEuler(new f.Euler(d.values[0],d.values[1],d.values[2],"XYZ")).normalize())});const r={props:u};u["Hips.position"]&&(u["Hips.position"].y<.5?r.lying=!0:r.standing=!0),this.animPoses.push({url:t+"-"+o,pose:r}),this.playPose(t,e,n,o,s)}else{const l="Pose "+t+" (ndx="+o+") not found";console.error(l)}}}stopPose(){this.stopAnimation()}playGesture(t,e=3,n=!1,o=1e3){if(!this.armature)return;let s=this.gestureTemplates[t];if(s){this.gestureTimeout&&(clearTimeout(this.gestureTimeout),this.gestureTimeout=null);let a=this.animQueue.findIndex(c=>c.template.name==="talkinghands");a!==-1&&(this.animQueue[a].ts=this.animQueue[a].ts.map(c=>0)),this.gesture=this.propsToThreeObjects(s),n&&(this.gesture=this.mirrorPose(this.gesture)),t==="namaste"&&this.avatar.body==="M"&&(this.gesture["RightArm.quaternion"].rotateTowards(new f.Quaternion(0,1,0,0),-.25),this.gesture["LeftArm.quaternion"].rotateTowards(new f.Quaternion(0,1,0,0),-.25));for(let[c,l]of Object.entries(this.gesture))l.t=this.animClock,l.d=o,this.poseTarget.props.hasOwnProperty(c)&&(this.poseTarget.props[c].copy(l),this.poseTarget.props[c].t=this.animClock,this.poseTarget.props[c].d=o);e&&Number.isFinite(e)&&(this.gestureTimeout=setTimeout(this.stopGesture.bind(this,o),1e3*e))}let i=this.animEmojis[t];if(i&&(i&&i.link&&(i=this.animEmojis[i.link]),i)){this.lookAtCamera(500);const a=this.animFactory(i);if(a.gesture=!0,e&&Number.isFinite(e)){const c=a.ts[0],u=a.ts[a.ts.length-1]-c;if(e*1e3-u>0){const d=[];for(let v=1;v<a.ts.length;v++)d.push(a.ts[v]-a.ts[v-1]);const h=i.template?.rescale||d.map(v=>v/u),g=e*1e3-u;a.ts=a.ts.map((v,R,S)=>R===0?c:S[R-1]+d[R-1]+h[R-1]*g)}else{const d=e*1e3/u;a.ts=a.ts.map(h=>c+d*(h-c))}}this.animQueue.push(a)}}stopGesture(t=1e3){if(this.gestureTimeout&&(clearTimeout(this.gestureTimeout),this.gestureTimeout=null),this.gesture){const n=Object.entries(this.gesture);this.gesture=null;for(const[o,s]of n)this.poseTarget.props.hasOwnProperty(o)&&(this.poseTarget.props[o].copy(this.getPoseTemplateProp(o)),this.poseTarget.props[o].t=this.animClock,this.poseTarget.props[o].d=t)}let e=this.animQueue.findIndex(n=>n.gesture);e!==-1&&this.animQueue.splice(e,1)}ikSolve(t,e=null,n=!1,o=null){const s=new f.Vector3,i=new f.Vector3,a=new f.Vector3,c=new f.Vector3,l=new f.Quaternion,u=new f.Vector3,r=new f.Vector3,d=new f.Vector3,h=this.ikMesh.getObjectByName(t.root);h.position.setFromMatrixPosition(this.armature.getObjectByName(t.root).matrixWorld),h.quaternion.setFromRotationMatrix(this.armature.getObjectByName(t.root).matrixWorld),e&&n&&e.applyQuaternion(this.armature.quaternion).add(h.position);const g=this.ikMesh.getObjectByName(t.effector),v=t.links;v.forEach(S=>{S.bone=this.ikMesh.getObjectByName(S.link),S.bone.quaternion.copy(this.getPoseTemplateProp(S.link+".quaternion"))}),h.updateMatrixWorld(!0);const R=t.iterations||10;if(e)for(let S=0;S<R;S++){let M=!1;for(let p=0,O=v.length;p<O;p++){const P=v[p].bone;P.matrixWorld.decompose(c,l,u),l.invert(),i.setFromMatrixPosition(g.matrixWorld),a.subVectors(i,c),a.applyQuaternion(l),a.normalize(),s.subVectors(e,c),s.applyQuaternion(l),s.normalize();let y=s.dot(a);y>1?y=1:y<-1&&(y=-1),y=Math.acos(y),!(y<1e-5)&&(v[p].minAngle!==void 0&&y<v[p].minAngle&&(y=v[p].minAngle),v[p].maxAngle!==void 0&&y>v[p].maxAngle&&(y=v[p].maxAngle),r.crossVectors(a,s),r.normalize(),ne.setFromAxisAngle(r,y),P.quaternion.multiply(ne),P.rotation.setFromVector3(d.setFromEuler(P.rotation).clamp(new f.Vector3(v[p].minx!==void 0?v[p].minx:-1/0,v[p].miny!==void 0?v[p].miny:-1/0,v[p].minz!==void 0?v[p].minz:-1/0),new f.Vector3(v[p].maxx!==void 0?v[p].maxx:1/0,v[p].maxy!==void 0?v[p].maxy:1/0,v[p].maxz!==void 0?v[p].maxz:1/0))),P.updateMatrixWorld(!0),M=!0)}if(!M)break}o&&v.forEach(S=>{this.poseTarget.props[S.link+".quaternion"].copy(S.bone.quaternion),this.poseTarget.props[S.link+".quaternion"].t=this.animClock,this.poseTarget.props[S.link+".quaternion"].d=o})}dispose(){this.isRunning=!1,this.stop(),this.stopSpeaking(),this.streamStop(),this.isAvatarOnly?this.armature&&(this.armature.parent&&this.armature.parent.remove(this.armature),this.clearThree(this.armature)):(this.clearThree(this.scene),this.resizeobserver.disconnect(),this.renderer&&(this.renderer.dispose(),this.renderer.domElement&&this.renderer.domElement.parentNode&&this.renderer.domElement.parentNode.removeChild(this.renderer.domElement),this.renderer=null)),this.clearThree(this.ikMesh),this.dynamicbones.dispose()}}const Te={apiKey:"sk_ace57ef3ef65a92b9d3bee2a00183b78ca790bc3e10964f2",endpoint:"https://api.elevenlabs.io/v1/text-to-speech",defaultVoice:"21m00Tcm4TlvDq8ikWAM",voices:{rachel:"21m00Tcm4TlvDq8ikWAM",drew:"29vD33N1CtxCmqQRPOHJ",bella:"EXAVITQu4vr4xnSDxMaL",antoni:"ErXwobaYiN019PkySvjV",elli:"MF3mGyEYCl7XYWbV9V6O",josh:"VR6AewLTigWG4xSOukaG"}},Ue={defaultVoice:"aura-2-thalia-en",voices:{thalia:"aura-2-thalia-en",asteria:"aura-2-asteria-en",orion:"aura-2-orion-en",stella:"aura-2-stella-en",athena:"aura-2-athena-en",hera:"aura-2-hera-en",zeus:"aura-2-zeus-en"}};function Pe(){return{service:"elevenlabs",endpoint:Te.endpoint,apiKey:Te.apiKey,defaultVoice:Te.defaultVoice,voices:Te.voices}}function St(){const N=Pe(),t=[];return Object.entries(N.voices).forEach(([e,n])=>{t.push({value:n,label:`${e.charAt(0).toUpperCase()+e.slice(1)} (${N.service})`})}),t}const Ze=x.forwardRef(({avatarUrl:N="/avatars/brunette.glb",avatarBody:t="F",mood:e="neutral",ttsLang:n="en",ttsService:o=null,ttsVoice:s=null,ttsApiKey:i=null,bodyMovement:a="idle",movementIntensity:c=.5,showFullAvatar:l=!0,cameraView:u="upper",onReady:r=()=>{},onLoading:d=()=>{},onError:h=()=>{},className:g="",style:v={},animations:R={}},S)=>{const M=x.useRef(null),p=x.useRef(null),O=x.useRef(l),P=x.useRef(null),y=x.useRef(null),E=x.useRef(!1),b=x.useRef({remainingText:null,originalText:null,options:null}),k=x.useRef([]),T=x.useRef(0),[A,W]=x.useState(!0),[G,j]=x.useState(null),[Y,ae]=x.useState(!1),[re,xe]=x.useState(!1);x.useEffect(()=>{E.current=re},[re]),x.useEffect(()=>{O.current=l},[l]);const oe=Pe(),Se=o||oe.service;let $;Se==="browser"?$={service:"browser",endpoint:"",apiKey:null,defaultVoice:"Google US English"}:Se==="elevenlabs"?$={service:"elevenlabs",endpoint:"https://api.elevenlabs.io/v1/text-to-speech",apiKey:i||oe.apiKey,defaultVoice:s||oe.defaultVoice||Te.defaultVoice,voices:oe.voices||Te.voices}:Se==="deepgram"?$={service:"deepgram",endpoint:"https://api.deepgram.com/v1/speak",apiKey:i||oe.apiKey,defaultVoice:s||oe.defaultVoice||Ue.defaultVoice,voices:oe.voices||Ue.voices}:$={...oe,apiKey:i!==null?i:oe.apiKey};const I={url:N,body:t,avatarMood:e,ttsLang:Se==="browser"?"en-US":n,ttsVoice:s||$.defaultVoice,lipsyncLang:"en",showFullAvatar:l,bodyMovement:a,movementIntensity:c},L={ttsEndpoint:$.endpoint,ttsApikey:$.apiKey,ttsService:Se,lipsyncModules:["en"],cameraView:u},F=x.useCallback(async()=>{if(!(!M.current||p.current))try{if(W(!0),j(null),p.current=new Ge(M.current,L),p.current.controls&&(p.current.controls.enableRotate=!1,p.current.controls.enableZoom=!1,p.current.controls.enablePan=!1,p.current.controls.enableDamping=!1),R&&Object.keys(R).length>0&&(p.current.customAnimations=R),await p.current.showAvatar(I,Q=>{if(Q.lengthComputable){const ie=Math.min(100,Math.round(Q.loaded/Q.total*100));d(ie)}}),await new Promise(Q=>{const ie=()=>{p.current.lipsync&&Object.keys(p.current.lipsync).length>0?Q():setTimeout(ie,100)};ie()}),p.current&&p.current.setShowFullAvatar)try{p.current.setShowFullAvatar(l)}catch(Q){console.warn("Error setting full body mode on initialization:",Q)}p.current&&p.current.controls&&(p.current.controls.enableRotate=!1,p.current.controls.enableZoom=!1,p.current.controls.enablePan=!1,p.current.controls.enableDamping=!1,p.current.controls.update()),W(!1),ae(!0),r(p.current);const X=()=>{document.visibilityState==="visible"?p.current?.start():p.current?.stop()};return document.addEventListener("visibilitychange",X),()=>{document.removeEventListener("visibilitychange",X)}}catch(C){console.error("Error initializing TalkingHead:",C),j(C.message||"Failed to initialize avatar"),W(!1),h(C)}},[N,t,e,n,o,s,i,l,a,c,u]);x.useEffect(()=>(F(),()=>{p.current&&(p.current.stop(),p.current.dispose(),p.current=null)}),[F]),x.useEffect(()=>{if(!M.current||!p.current)return;const C=new ResizeObserver(Q=>{for(const ie of Q)p.current&&p.current.onResize&&p.current.onResize()});C.observe(M.current);const X=()=>{p.current&&p.current.onResize&&p.current.onResize()};return window.addEventListener("resize",X),()=>{C.disconnect(),window.removeEventListener("resize",X)}},[Y]);const D=x.useCallback(async()=>{if(p.current&&p.current.audioCtx)try{(p.current.audioCtx.state==="suspended"||p.current.audioCtx.state==="interrupted")&&(await p.current.audioCtx.resume(),console.log("Audio context resumed"))}catch(C){console.warn("Failed to resume audio context:",C)}},[]),Z=x.useCallback(async(C,X={})=>{if(p.current&&Y)try{y.current&&(clearInterval(y.current),y.current=null),P.current={text:C,options:X},b.current={remainingText:null,originalText:null,options:null};const Q=/[!\.\?\n\p{Extended_Pictographic}]/ug,ie=C.split(Q).map(_=>_.trim()).filter(_=>_.length>0);k.current=ie,T.current=0,xe(!1),E.current=!1,await D();const ge={...X,lipsyncLang:X.lipsyncLang||I.lipsyncLang||"en"};if(X.onSpeechEnd&&p.current){const _=p.current;let de=null,be=0;const ye=1200;let ve=!1;de=setInterval(()=>{if(be++,E.current)return;if(be>ye){if(de&&(clearInterval(de),de=null,y.current=null),!ve&&!E.current){ve=!0;try{X.onSpeechEnd()}catch(z){console.error("Error in onSpeechEnd callback (timeout):",z)}}return}const Re=!_.speechQueue||_.speechQueue.length===0,Me=!_.audioPlaylist||_.audioPlaylist.length===0;_&&_.isSpeaking===!1&&Re&&Me&&_.isAudioPlaying===!1&&!ve&&!E.current&&setTimeout(()=>{if(_&&!E.current&&_.isSpeaking===!1&&(!_.speechQueue||_.speechQueue.length===0)&&(!_.audioPlaylist||_.audioPlaylist.length===0)&&_.isAudioPlaying===!1&&!ve&&!E.current){ve=!0,de&&(clearInterval(de),de=null,y.current=null);try{X.onSpeechEnd()}catch(U){console.error("Error in onSpeechEnd callback:",U)}}},100)},100),y.current=de}p.current.lipsync&&Object.keys(p.current.lipsync).length>0?(p.current.setSlowdownRate&&p.current.setSlowdownRate(1.05),p.current.speakText(C,ge)):setTimeout(async()=>{await D(),p.current&&p.current.lipsync&&(p.current.setSlowdownRate&&p.current.setSlowdownRate(1.05),p.current.speakText(C,ge))},100)}catch(Q){console.error("Error speaking text:",Q),j(Q.message||"Failed to speak text")}},[Y,D,I.lipsyncLang]),se=x.useCallback(()=>{p.current&&(p.current.stopSpeaking(),p.current.setSlowdownRate&&p.current.setSlowdownRate(1),P.current=null,xe(!1))},[]),te=x.useCallback(()=>{if(p.current&&p.current.pauseSpeaking){const C=p.current;if(C.isSpeaking||C.audioPlaylist&&C.audioPlaylist.length>0||C.speechQueue&&C.speechQueue.length>0){y.current&&(clearInterval(y.current),y.current=null);let Q="";if(P.current&&k.current.length>0){const ie=k.current.length,ge=C.speechQueue?C.speechQueue.filter(ye=>ye&&ye.text&&Array.isArray(ye.text)&&ye.text.length>0).length:0,_=C.audioPlaylist&&C.audioPlaylist.length>0,de=ge+(_?1:0),be=ie-de;if(de>0&&be<ie&&(Q=k.current.slice(be).join(". ").trim(),!Q&&ge>0&&C.speechQueue)){const ve=C.speechQueue.filter(Re=>Re&&Re.text&&Array.isArray(Re.text)&&Re.text.length>0).map(Re=>Re.text.map(Me=>Me.word||"").filter(Me=>Me.length>0).join(" ")).filter(Re=>Re.length>0).join(" ");ve&&ve.trim()&&(Q=ve.trim())}}P.current&&(b.current={remainingText:Q||null,originalText:P.current.text,options:P.current.options}),C.speechQueue&&(C.speechQueue.length=0),p.current.pauseSpeaking(),E.current=!0,xe(!0)}}},[]),J=x.useCallback(async()=>{if(!p.current||!re)return;let C="",X={};if(b.current&&b.current.remainingText)C=b.current.remainingText,X=b.current.options||{},b.current={remainingText:null,originalText:null,options:null};else if(P.current&&P.current.text)C=P.current.text,X=P.current.options||{};else{console.warn("Resume called but no paused speech found"),xe(!1),E.current=!1;return}xe(!1),E.current=!1,await D();const Q={...X,lipsyncLang:X.lipsyncLang||I.lipsyncLang||"en"};try{await Z(C,Q)}catch(ie){console.error("Error resuming speech:",ie),xe(!1),E.current=!1}},[D,re,Z,I]),he=x.useCallback(C=>{p.current&&p.current.setMood(C)},[]),Ce=x.useCallback(C=>{p.current&&p.current.setSlowdownRate&&p.current.setSlowdownRate(C)},[]),ke=x.useCallback((C,X=!1)=>{if(p.current&&p.current.playAnimation){if(R&&R[C]&&(C=R[C]),p.current.setShowFullAvatar)try{p.current.setShowFullAvatar(O.current)}catch(ie){console.warn("Error setting full body mode:",ie)}if(C.includes("."))try{p.current.playAnimation(C,null,10,0,.01,X)}catch(ie){console.warn(`Failed to play ${C}:`,ie);try{p.current.setBodyMovement("idle")}catch(ge){console.warn("Fallback animation also failed:",ge)}}else{const ie=[".fbx",".glb",".gltf"];let ge=!1;for(const _ of ie)try{p.current.playAnimation(C+_,null,10,0,.01,X),ge=!0;break}catch{}if(!ge){console.warn("Animation not found:",C);try{p.current.setBodyMovement("idle")}catch(_){console.warn("Fallback animation also failed:",_)}}}}},[R]),We=x.useCallback(()=>{p.current&&p.current.onResize&&p.current.onResize()},[]);return x.useImperativeHandle(S,()=>({speakText:Z,stopSpeaking:se,pauseSpeaking:te,resumeSpeaking:J,resumeAudioContext:D,setMood:he,setTimingAdjustment:Ce,playAnimation:ke,isReady:Y,isPaused:re,talkingHead:p.current,handleResize:We,setBodyMovement:C=>{if(p.current&&p.current.setShowFullAvatar&&p.current.setBodyMovement)try{p.current.setShowFullAvatar(O.current),p.current.setBodyMovement(C)}catch(X){console.warn("Error setting body movement:",X)}},setMovementIntensity:C=>p.current?.setMovementIntensity(C),playRandomDance:()=>{if(p.current&&p.current.setShowFullAvatar&&p.current.playRandomDance)try{p.current.setShowFullAvatar(O.current),p.current.playRandomDance()}catch(C){console.warn("Error playing random dance:",C)}},playReaction:C=>{if(p.current&&p.current.setShowFullAvatar&&p.current.playReaction)try{p.current.setShowFullAvatar(O.current),p.current.playReaction(C)}catch(X){console.warn("Error playing reaction:",X)}},playCelebration:()=>{if(p.current&&p.current.setShowFullAvatar&&p.current.playCelebration)try{p.current.setShowFullAvatar(O.current),p.current.playCelebration()}catch(C){console.warn("Error playing celebration:",C)}},setShowFullAvatar:C=>{if(p.current&&p.current.setShowFullAvatar)try{O.current=C,p.current.setShowFullAvatar(C)}catch(X){console.warn("Error setting showFullAvatar:",X)}},lockAvatarPosition:()=>{if(p.current&&p.current.lockAvatarPosition)try{p.current.lockAvatarPosition()}catch(C){console.warn("Error locking avatar position:",C)}},unlockAvatarPosition:()=>{if(p.current&&p.current.unlockAvatarPosition)try{p.current.unlockAvatarPosition()}catch(C){console.warn("Error unlocking avatar position:",C)}}})),V.jsxs("div",{className:`talking-head-avatar ${g}`,style:{width:"100%",height:"100%",position:"relative",...v},children:[V.jsx("div",{ref:M,className:"talking-head-viewer",style:{width:"100%",height:"100%",minHeight:"400px"}}),A&&V.jsx("div",{className:"loading-overlay",style:{position:"absolute",top:"50%",left:"50%",transform:"translate(-50%, -50%)",color:"white",fontSize:"18px",zIndex:10},children:"Loading avatar..."}),G&&V.jsx("div",{className:"error-overlay",style:{position:"absolute",top:"50%",left:"50%",transform:"translate(-50%, -50%)",color:"#ff6b6b",fontSize:"16px",textAlign:"center",zIndex:10,padding:"20px",borderRadius:"8px"},children:G})]})});Ze.displayName="TalkingHeadAvatar";const Ke=x.forwardRef(({text:N="Hello! I'm a talking avatar. How are you today?",onLoading:t=()=>{},onError:e=()=>{},onReady:n=()=>{},className:o="",style:s={},avatarConfig:i={}},a)=>{const c=x.useRef(null),l=x.useRef(null),[u,r]=x.useState(!0),[d,h]=x.useState(null),[g,v]=x.useState(!1),R=Pe(),S=i.ttsService||R.service,M=S==="browser"?{endpoint:"",apiKey:null,defaultVoice:"Google US English"}:{...R,apiKey:i.ttsApiKey!==void 0&&i.ttsApiKey!==null?i.ttsApiKey:R.apiKey,endpoint:S==="elevenlabs"&&i.ttsApiKey?"https://api.elevenlabs.io/v1/text-to-speech":R.endpoint},p={url:"/avatars/brunette.glb",body:"F",avatarMood:"neutral",ttsLang:S==="browser"?"en-US":"en",ttsVoice:i.ttsVoice||M.defaultVoice,lipsyncLang:"en",showFullAvatar:!0,bodyMovement:"idle",movementIntensity:.5,...i},O={ttsEndpoint:M.endpoint,ttsApikey:M.apiKey,ttsService:S,lipsyncModules:["en"],cameraView:"upper"},P=x.useCallback(async()=>{if(!(!c.current||l.current))try{if(r(!0),h(null),l.current=new Ge(c.current,O),await l.current.showAvatar(p,G=>{if(G.lengthComputable){const j=Math.min(100,Math.round(G.loaded/G.total*100));t(j)}}),l.current.morphs&&l.current.morphs.length>0){const G=l.current.morphs[0].morphTargetDictionary;console.log("Available morph targets:",Object.keys(G));const j=Object.keys(G).filter(Y=>Y.startsWith("viseme_"));console.log("Viseme morph targets found:",j),j.length===0&&(console.warn("No viseme morph targets found! Lip-sync will not work properly."),console.log("Expected viseme targets: viseme_aa, viseme_E, viseme_I, viseme_O, viseme_U, viseme_PP, viseme_SS, viseme_TH, viseme_DD, viseme_FF, viseme_kk, viseme_nn, viseme_RR, viseme_CH, viseme_sil"))}if(await new Promise(G=>{const j=()=>{l.current.lipsync&&Object.keys(l.current.lipsync).length>0?(console.log("Lip-sync modules loaded:",Object.keys(l.current.lipsync)),G()):(console.log("Waiting for lip-sync modules to load..."),setTimeout(j,100))};j()}),l.current&&l.current.setShowFullAvatar)try{l.current.setShowFullAvatar(!0),console.log("Avatar initialized in full body mode")}catch(G){console.warn("Error setting full body mode on initialization:",G)}r(!1),v(!0),n(l.current);const W=()=>{document.visibilityState==="visible"?l.current?.start():l.current?.stop()};return document.addEventListener("visibilitychange",W),()=>{document.removeEventListener("visibilitychange",W)}}catch(A){console.error("Error initializing TalkingHead:",A),h(A.message||"Failed to initialize avatar"),r(!1),e(A)}},[]);x.useEffect(()=>(P(),()=>{l.current&&(l.current.stop(),l.current.dispose(),l.current=null)}),[P]);const y=x.useCallback(A=>{if(l.current&&g)try{console.log("Speaking text:",A),console.log("Avatar config:",p),console.log("TalkingHead instance:",l.current),l.current.lipsync&&Object.keys(l.current.lipsync).length>0?(console.log("Lip-sync modules loaded:",Object.keys(l.current.lipsync)),l.current.setSlowdownRate&&(l.current.setSlowdownRate(1.05),console.log("Applied timing adjustment for better lip-sync")),l.current.speakText(A)):(console.warn("Lip-sync modules not ready, waiting..."),setTimeout(()=>{l.current&&l.current.lipsync?(console.log("Lip-sync now ready, speaking..."),l.current.setSlowdownRate&&(l.current.setSlowdownRate(1.05),console.log("Applied timing adjustment for better lip-sync")),l.current.speakText(A)):console.error("Lip-sync still not ready after waiting")},500))}catch(W){console.error("Error speaking text:",W),h(W.message||"Failed to speak text")}else console.warn("Avatar not ready for speaking. isReady:",g,"talkingHeadRef:",!!l.current)},[g,p]),E=x.useCallback(()=>{l.current&&(l.current.stopSpeaking(),l.current.setSlowdownRate&&(l.current.setSlowdownRate(1),console.log("Reset timing to normal")))},[]),b=x.useCallback(A=>{l.current&&l.current.setMood(A)},[]),k=x.useCallback(A=>{l.current&&l.current.setSlowdownRate&&(l.current.setSlowdownRate(A),console.log("Timing adjustment set to:",A))},[]),T=x.useCallback((A,W=!1)=>{if(l.current&&l.current.playAnimation){if(l.current.setShowFullAvatar)try{l.current.setShowFullAvatar(!0)}catch(j){console.warn("Error setting full body mode:",j)}if(A.includes("."))try{l.current.playAnimation(A,null,10,0,.01,W),console.log("Playing animation:",A)}catch(j){console.log(`Failed to play ${A}:`,j);try{l.current.setBodyMovement("idle"),console.log("Fallback to idle animation")}catch(Y){console.warn("Fallback animation also failed:",Y)}}else{const j=[".fbx",".glb",".gltf"];let Y=!1;for(const ae of j)try{l.current.playAnimation(A+ae,null,10,0,.01,W),console.log("Playing animation:",A+ae),Y=!0;break}catch{console.log(`Failed to play ${A}${ae}, trying next format...`)}if(!Y){console.warn("Animation system not available or animation not found:",A);try{l.current.setBodyMovement("idle"),console.log("Fallback to idle animation")}catch(ae){console.warn("Fallback animation also failed:",ae)}}}}else console.warn("Animation system not available or animation not found:",A)},[]);return x.useImperativeHandle(a,()=>({speakText:y,stopSpeaking:E,setMood:b,setTimingAdjustment:k,playAnimation:T,isReady:g,talkingHead:l.current,setBodyMovement:A=>{if(l.current&&l.current.setShowFullAvatar&&l.current.setBodyMovement)try{l.current.setShowFullAvatar(!0),l.current.setBodyMovement(A),console.log("Body movement set with full body mode:",A)}catch(W){console.warn("Error setting body movement:",W)}},setMovementIntensity:A=>l.current?.setMovementIntensity(A),playRandomDance:()=>{if(l.current&&l.current.setShowFullAvatar&&l.current.playRandomDance)try{l.current.setShowFullAvatar(!0),l.current.playRandomDance(),console.log("Random dance played with full body mode")}catch(A){console.warn("Error playing random dance:",A)}},playReaction:A=>{if(l.current&&l.current.setShowFullAvatar&&l.current.playReaction)try{l.current.setShowFullAvatar(!0),l.current.playReaction(A),console.log("Reaction played with full body mode:",A)}catch(W){console.warn("Error playing reaction:",W)}},playCelebration:()=>{if(l.current&&l.current.setShowFullAvatar&&l.current.playCelebration)try{l.current.setShowFullAvatar(!0),l.current.playCelebration(),console.log("Celebration played with full body mode")}catch(A){console.warn("Error playing celebration:",A)}},setShowFullAvatar:A=>{if(l.current&&l.current.setShowFullAvatar)try{l.current.setShowFullAvatar(A),console.log("Show full avatar set to:",A)}catch(W){console.warn("Error setting showFullAvatar:",W)}},lockAvatarPosition:()=>{if(l.current&&l.current.lockAvatarPosition)try{l.current.lockAvatarPosition()}catch(A){console.warn("Error locking avatar position:",A)}},unlockAvatarPosition:()=>{if(l.current&&l.current.unlockAvatarPosition)try{l.current.unlockAvatarPosition()}catch(A){console.warn("Error unlocking avatar position:",A)}}})),V.jsxs("div",{className:`talking-head-container ${o}`,style:s,children:[V.jsx("div",{ref:c,className:"talking-head-viewer",style:{width:"100%",height:"100%",minHeight:"400px"}}),u&&V.jsx("div",{className:"loading-overlay",style:{position:"absolute",top:"50%",left:"50%",transform:"translate(-50%, -50%)",color:"white",fontSize:"18px",zIndex:10},children:"Loading avatar..."}),d&&V.jsx("div",{className:"error-overlay",style:{position:"absolute",top:"50%",left:"50%",transform:"translate(-50%, -50%)",color:"#ff6b6b",fontSize:"16px",textAlign:"center",zIndex:10,padding:"20px",borderRadius:"8px"},children:d})]})});Ke.displayName="TalkingHeadComponent";async function Ee(N){try{const t=await fetch(N);if(!t.ok)throw new Error(`Failed to fetch manifest: ${t.status} ${t.statusText}`);return(await t.json()).animations||{}}catch(t){return console.error("Failed to load animation manifest:",t),{}}}async function kt(N,t="F"){const e=[],n=N.replace(/\/$/,"");try{const s=[`${n}/.list.json`,`/api/directory?path=${encodeURIComponent(n)}`,`${n}/index.json`];for(const i of s)try{const a=await fetch(i);if(a.ok){const c=await a.json(),u=(Array.isArray(c)?c:c.files||[]).filter(r=>typeof r=="string"&&r.toLowerCase().endsWith(".fbx")).map(r=>r.startsWith("/")?r:`${n}/${r}`);if(u.length>0)return u}}catch{continue}}catch(s){console.warn(`⚠️ Could not use directory listing API for ${n}:`,s)}const o=[];t==="M"||t==="m"?o.push(`${n}/male`,`${n}/m`):o.push(`${n}/female`,`${n}/f`),o.push(`${n}/shared`);for(const s of o)try{const i=`${s}/.list.json`,a=await fetch(i);if(a.ok){const c=await a.json(),l=(Array.isArray(c)?c:c.files||[]).filter(u=>typeof u=="string"&&u.toLowerCase().endsWith(".fbx")).map(u=>u.startsWith("/")?u:`${s}/${u}`);l.length>0&&e.push(...l)}}catch{continue}return e.length>0,e}async function _e(N,t="F"){const e={};for(const[n,o]of Object.entries(N))try{const s=await kt(o,t);s.length>0&&(e[n]=s)}catch(s){console.error(`❌ Failed to auto-load animations from ${o}:`,s)}return e}const Je=x.forwardRef(({text:N=null,avatarUrl:t="/avatars/brunette.glb",avatarBody:e="F",mood:n="neutral",ttsLang:o="en",ttsService:s=null,ttsVoice:i=null,ttsApiKey:a=null,bodyMovement:c="idle",movementIntensity:l=.5,showFullAvatar:u=!1,cameraView:r="upper",onReady:d=()=>{},onLoading:h=()=>{},onError:g=()=>{},onSpeechEnd:v=()=>{},className:R="",style:S={},animations:M={},autoAnimationGroup:p=null,autoIdleGroup:O=null,autoSpeak:P=!1},y)=>{const E=x.useRef(null),b=x.useRef(null),k=x.useRef(u),T=x.useRef(null),A=x.useRef(null),W=x.useRef(!1),G=x.useRef({remainingText:null,originalText:null,options:null}),j=x.useRef([]),[Y,ae]=x.useState(!0),[re,xe]=x.useState(null),[oe,Se]=x.useState(!1),[$,I]=x.useState(!1),[L,F]=x.useState(M),D=x.useRef(null),Z=x.useRef(!1),se=x.useRef(null),te=x.useRef([]),J=x.useRef([]);x.useEffect(()=>{W.current=$},[$]),x.useEffect(()=>{(async()=>{if(M.manifest&&M.auto)try{const z=await Ee(M.manifest);F(z);return}catch{}if(M.manifest&&!M.auto)try{const z=await Ee(M.manifest);F(z)}catch(z){console.error("Failed to load animation manifest:",z),F(M)}else if(M.auto)try{let z=null;if(M.manifest)try{z=await Ee(M.manifest)}catch{}if(typeof M.auto=="string"){const U=M.auto,ee={talking:`${U}/talking`,idle:`${U}/idle`},K=e==="M"?"male":"female";ee[`${K}_talking`]=`${U}/${K}/talking`,ee[`${K}_idle`]=`${U}/${K}/idle`,ee.shared_talking=`${U}/shared/talking`,ee.shared_idle=`${U}/shared/idle`;const Ie=await _e(ee,e);if(!Object.values(Ie).some(le=>Array.isArray(le)&&le.length>0)&&z){F(z);return}const me={_genderSpecific:{[K]:{},shared:{}}};Object.entries(Ie).forEach(([le,ze])=>{if(le.includes("_")){const[Oe,...et]=le.split("_"),Fe=et.join("_");Oe==="shared"?(me._genderSpecific.shared[Fe]||(me._genderSpecific.shared[Fe]=[]),me._genderSpecific.shared[Fe].push(...ze)):Oe===K&&(me._genderSpecific[K][Fe]||(me._genderSpecific[K][Fe]=[]),me._genderSpecific[K][Fe].push(...ze))}else me[le]=ze}),z&&(z._genderSpecific&&Object.keys(z._genderSpecific).forEach(le=>{me._genderSpecific[le]||(me._genderSpecific[le]={}),Object.entries(z._genderSpecific[le]).forEach(([ze,Oe])=>{me._genderSpecific[le][ze]||(me._genderSpecific[le][ze]=Oe)})}),Object.entries(z).forEach(([le,ze])=>{le!=="_genderSpecific"&&!me[le]&&(me[le]=ze)})),F(me)}else if(typeof M.auto=="object"){const U=await _e(M.auto,e),ee=Object.values(U).some(K=>Array.isArray(K)&&K.length>0);F(!ee&&z?z:U)}}catch(z){if(console.error("Failed to auto-discover animations:",z),M.manifest)try{const U=await Ee(M.manifest);F(U)}catch{F(M)}else F(M)}else F(M)})()},[M,e]),x.useEffect(()=>{k.current=u},[u]);const he=Pe(),Ce=s||he.service;let ke;Ce==="browser"?ke={service:"browser",endpoint:"",apiKey:null,defaultVoice:"Google US English"}:Ce==="elevenlabs"?ke={service:"elevenlabs",endpoint:"https://api.elevenlabs.io/v1/text-to-speech",apiKey:a||he.apiKey,defaultVoice:i||he.defaultVoice||Te.defaultVoice,voices:he.voices||Te.voices}:Ce==="deepgram"?ke={service:"deepgram",endpoint:"https://api.deepgram.com/v1/speak",apiKey:a||he.apiKey,defaultVoice:i||he.defaultVoice||Ue.defaultVoice,voices:he.voices||Ue.voices}:ke={...he,apiKey:a!==null?a:he.apiKey};const We={url:t,body:e,avatarMood:n,ttsLang:Ce==="browser"?"en-US":o,ttsVoice:i||ke.defaultVoice,lipsyncLang:"en",showFullAvatar:u,bodyMovement:c,movementIntensity:l},C={ttsEndpoint:ke.endpoint,ttsApikey:ke.apiKey,ttsService:Ce,lipsyncModules:["en"],cameraView:r},X=x.useCallback(async()=>{if(!(!E.current||b.current))try{ae(!0),xe(null),b.current=new Ge(E.current,C),await b.current.showAvatar(We,z=>{if(z.lengthComputable){const U=Math.min(100,Math.round(z.loaded/z.total*100));h(U)}}),ae(!1),Se(!0),d(b.current);const w=()=>{document.visibilityState==="visible"?b.current?.start():b.current?.stop()};return document.addEventListener("visibilitychange",w),()=>{document.removeEventListener("visibilitychange",w)}}catch(w){console.error("Error initializing TalkingHead:",w),xe(w.message||"Failed to initialize avatar"),ae(!1),g(w)}},[]);x.useEffect(()=>(X(),()=>{b.current&&(b.current.stop(),b.current.dispose(),b.current=null)}),[X]);const Q=x.useCallback(async()=>{if(b.current)try{const w=b.current.audioCtx||b.current.audioContext;w&&(w.state==="suspended"||w.state==="interrupted")&&await w.resume()}catch(w){console.warn("Failed to resume audio context:",w)}},[]),ie=x.useCallback(w=>{if(!L)return[];let z=null;if(L._genderSpecific){const ee=(e?.toUpperCase()||"F")==="M"?"male":"female",K=L._genderSpecific[ee];K&&K[w]?z=K[w]:L._genderSpecific.shared&&L._genderSpecific.shared[w]&&(z=L._genderSpecific.shared[w])}return!z&&L[w]&&(z=L[w]),z?Array.isArray(z)?[...z]:typeof z=="string"?[z]:[]:[]},[L,e]),ge=x.useCallback(w=>{const z=[...w];for(let U=z.length-1;U>0;U--){const ee=Math.floor(Math.random()*(U+1));[z[U],z[ee]]=[z[ee],z[U]]}return z},[]),_=x.useCallback(w=>{if(J.current.length===0){const U=ie(w);if(U.length===0)return null;J.current=ge(U),te.current=[]}const z=J.current.shift();return z&&te.current.push(z),z},[ie,ge]),de=x.useCallback(w=>w?w.split("/").pop().replace(".fbx","").replace(/[-_]/g," "):"Unknown",[]),be=x.useCallback((w,z=!1,U=null)=>{if(!b.current)return null;const ee=_(w);if(ee)try{const K=de(ee);console.log(`🎬 Playing animation: "${K}"`);const Ie=()=>{Z.current&&se.current===w?setTimeout(()=>{be(w,z,U)},100):U&&U()};return b.current.playAnimation(ee,null,0,0,.01,z,Ie),ee}catch(K){return console.error("Failed to play animation:",K),null}return null},[_,de]),ye=x.useCallback(async(w,z={})=>{if(!b.current||!oe||!w||w.trim()==="")return;await Q();const U=z.animationGroup||p;U&&!z.skipAnimation&&(Z.current=!0,se.current=U,J.current=[],te.current=[],be(U)),G.current={remainingText:null,originalText:null,options:null},j.current=[],T.current={text:w,options:z},A.current&&(clearInterval(A.current),A.current=null),I(!1),W.current=!1;const ee=w.split(/[.!?]+/).filter(Ie=>Ie.trim().length>0);j.current=ee;const K={lipsyncLang:z.lipsyncLang||"en",onSpeechEnd:()=>{A.current&&(clearInterval(A.current),A.current=null),Z.current=!1,se.current=null,J.current=[],te.current=[],z.onSpeechEnd&&z.onSpeechEnd(),v()}};try{b.current.speakText(w,K)}catch(Ie){console.error("Error speaking text:",Ie),xe(Ie.message||"Failed to speak text")}},[oe,v,Q,p,be]);x.useEffect(()=>{if(!oe||!O||!b.current)return;D.current&&clearInterval(D.current);const w=()=>{b.current&&!W.current&&be(O)};return w(),D.current=setInterval(()=>{w()},12e3+Math.random()*3e3),()=>{D.current&&(clearInterval(D.current),D.current=null)}},[oe,O,be]),x.useEffect(()=>{oe&&N&&P&&b.current&&ye(N)},[oe,N,P,ye]);const ve=x.useCallback(()=>{if(b.current)try{const w=b.current.isSpeaking||!1,z=b.current.audioPlaylist||[],U=b.current.speechQueue||[];if(w||z.length>0||U.length>0){A.current&&(clearInterval(A.current),A.current=null);let ee="";U.length>0&&(ee=U.map(K=>K.text&&Array.isArray(K.text)?K.text.map(Ie=>Ie.word).join(" "):K.text||"").join(" ")),G.current={remainingText:ee||null,originalText:T.current?.text||null,options:T.current?.options||null},b.current.speechQueue.length=0,b.current.pauseSpeaking(),I(!0),W.current=!0}}catch(w){console.warn("Error pausing speech:",w)}},[]),Re=x.useCallback(async()=>{if(!(!b.current||!$))try{await Q(),I(!1),W.current=!1;const w=G.current?.remainingText,z=G.current?.originalText||T.current?.text,U=G.current?.options||T.current?.options||{},ee=w||z;ee&&ye(ee,U)}catch(w){console.warn("Error resuming speech:",w),I(!1),W.current=!1}},[$,ye,Q]),Me=x.useCallback(()=>{b.current&&(b.current.stopSpeaking(),A.current&&(clearInterval(A.current),A.current=null),Z.current=!1,se.current=null,J.current=[],te.current=[],I(!1),W.current=!1)},[]);return x.useImperativeHandle(y,()=>({speakText:ye,pauseSpeaking:ve,resumeSpeaking:Re,stopSpeaking:Me,resumeAudioContext:Q,isPaused:()=>$,setMood:w=>b.current?.setMood(w),setBodyMovement:w=>{b.current&&b.current.setBodyMovement(w)},playAnimation:(w,z=!1)=>{b.current&&b.current.playAnimation&&b.current.playAnimation(w,null,10,0,.01,z)},playRandomAnimation:(w,z=!1)=>be(w,z),getRandomAnimation:w=>getRandomAnimation(w),playReaction:w=>b.current?.playReaction(w),playCelebration:()=>b.current?.playCelebration(),setShowFullAvatar:w=>{b.current&&(k.current=w,b.current.setShowFullAvatar(w))},isReady:oe,talkingHead:b.current})),V.jsxs("div",{className:`simple-talking-avatar-container ${R}`,style:S,children:[V.jsx("div",{ref:E,className:"talking-head-viewer",style:{width:"100%",height:"100%",minHeight:"400px"}}),Y&&V.jsx("div",{className:"loading-overlay",style:{position:"absolute",top:"50%",left:"50%",transform:"translate(-50%, -50%)",color:"white",fontSize:"18px",zIndex:10},children:"Loading avatar..."}),re&&V.jsx("div",{className:"error-overlay",style:{position:"absolute",top:"50%",left:"50%",transform:"translate(-50%, -50%)",color:"#ff6b6b",fontSize:"16px",textAlign:"center",zIndex:10,padding:"20px",borderRadius:"8px"},children:re})]})});Je.displayName="SimpleTalkingAvatar";const $e=x.forwardRef(({curriculumData:N=null,avatarConfig:t={},animations:e={},onLessonStart:n=()=>{},onLessonComplete:o=()=>{},onQuestionAnswer:s=()=>{},onCurriculumComplete:i=()=>{},onCustomAction:a=()=>{},autoStart:c=!1},l)=>{const u=x.useRef(null),r=x.useRef({currentModuleIndex:0,currentLessonIndex:0,currentQuestionIndex:0,isTeaching:!1,isQuestionMode:!1,lessonCompleted:!1,curriculumCompleted:!1,score:0,totalQuestions:0}),d=x.useRef({onLessonStart:n,onLessonComplete:o,onQuestionAnswer:s,onCurriculumComplete:i,onCustomAction:a}),h=x.useRef(null),g=x.useRef(null),v=x.useRef(null),R=x.useRef(null),S=x.useRef(null),M=x.useRef(null),p=x.useRef(null),O=x.useRef(N?.curriculum||{title:"Default Curriculum",description:"No curriculum data provided",language:"en",modules:[]}),P=x.useRef({avatarUrl:t.avatarUrl||"/avatars/brunette.glb",avatarBody:t.avatarBody||"F",mood:t.mood||"happy",ttsLang:t.ttsLang||"en",ttsService:t.ttsService||null,ttsVoice:t.ttsVoice||null,ttsApiKey:t.ttsApiKey||null,bodyMovement:t.bodyMovement||"gesturing",movementIntensity:t.movementIntensity||.7,showFullAvatar:t.showFullAvatar!==void 0?t.showFullAvatar:!1,animations:e,lipsyncLang:"en"});x.useEffect(()=>{d.current={onLessonStart:n,onLessonComplete:o,onQuestionAnswer:s,onCurriculumComplete:i,onCustomAction:a}},[n,o,s,i,a]),x.useEffect(()=>{O.current=N?.curriculum||{title:"Default Curriculum",description:"No curriculum data provided",language:"en",modules:[]},P.current={avatarUrl:t.avatarUrl||"/avatars/brunette.glb",avatarBody:t.avatarBody||"F",mood:t.mood||"happy",ttsLang:t.ttsLang||"en",ttsService:t.ttsService||null,ttsVoice:t.ttsVoice||null,ttsApiKey:t.ttsApiKey||null,bodyMovement:t.bodyMovement||"gesturing",movementIntensity:t.movementIntensity||.7,showFullAvatar:t.showFullAvatar!==void 0?t.showFullAvatar:!1,animations:e,lipsyncLang:"en"}},[N,t,e]);const y=x.useCallback(()=>(O.current||{modules:[]}).modules[r.current.currentModuleIndex]?.lessons[r.current.currentLessonIndex],[]),E=x.useCallback(()=>y()?.questions[r.current.currentQuestionIndex],[y]),b=x.useCallback((I,L)=>L.type==="multiple_choice"||L.type==="true_false"?I===L.answer:L.type==="code_test"&&typeof I=="object"&&I!==null?I.passed===!0:!1,[]),k=x.useCallback(()=>{r.current.lessonCompleted=!0,r.current.isQuestionMode=!1;const I=r.current.totalQuestions>0?Math.round(r.current.score/r.current.totalQuestions*100):100;let L="Congratulations! You've completed this lesson";if(r.current.totalQuestions>0?L+=` You got ${r.current.score} correct out of ${r.current.totalQuestions} question${r.current.totalQuestions===1?"":"s"}, achieving a score of ${I} percent. `:L+="! ",I>=80?L+="Excellent work! You have a great understanding of this topic.":I>=60?L+="Good job! You understand most of the concepts.":L+="Keep practicing! You're making progress.",d.current.onLessonComplete({moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex,score:r.current.score,totalQuestions:r.current.totalQuestions,percentage:I}),d.current.onCustomAction({type:"lessonComplete",moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex,score:r.current.score,totalQuestions:r.current.totalQuestions,percentage:I}),u.current){if(u.current.setMood("happy"),e.lessonComplete)try{u.current.playAnimation(e.lessonComplete,!0)}catch{u.current.playCelebration()}const F=O.current||{modules:[]},D=F.modules[r.current.currentModuleIndex],Z=r.current.currentLessonIndex<(D?.lessons?.length||0)-1,se=r.current.currentModuleIndex<(F.modules?.length||0)-1,te=Z||se,J=P.current||{lipsyncLang:"en"};u.current.speakText(L,{lipsyncLang:J.lipsyncLang,onSpeechEnd:()=>{d.current.onCustomAction({type:"lessonCompleteFeedbackDone",moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex,score:r.current.score,totalQuestions:r.current.totalQuestions,percentage:I,hasNextLesson:te})}})}},[e.lessonComplete]),T=x.useCallback(()=>{r.current.curriculumCompleted=!0;const I=O.current||{modules:[]};if(d.current.onCurriculumComplete({modules:I.modules.length,totalLessons:I.modules.reduce((L,F)=>L+F.lessons.length,0)}),u.current){if(u.current.setMood("celebrating"),e.curriculumComplete)try{u.current.playAnimation(e.curriculumComplete,!0)}catch{u.current.playCelebration()}const L=P.current||{lipsyncLang:"en"};u.current.speakText("Amazing! You've completed the entire curriculum! You're now ready to move on to more advanced topics. Well done!",{lipsyncLang:L.lipsyncLang})}},[e.curriculumComplete]),A=x.useCallback(()=>{const I=y();r.current.isQuestionMode=!0,r.current.currentQuestionIndex=0,r.current.totalQuestions=I?.questions?.length||0,r.current.score=0;const L=E();L&&d.current.onCustomAction({type:"questionStart",moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex,questionIndex:r.current.currentQuestionIndex,totalQuestions:r.current.totalQuestions,question:L,score:r.current.score});const F=()=>{if(!u.current||!L)return;if(u.current.setMood("happy"),e.questionStart)try{u.current.playAnimation(e.questionStart,!0)}catch(Z){console.warn("Failed to play questionStart animation:",Z)}const D=P.current||{lipsyncLang:"en"};L.type==="code_test"?u.current.speakText(`Let's test your coding skills! Here's your first challenge: ${L.question}`,{lipsyncLang:D.lipsyncLang}):L.type==="multiple_choice"?u.current.speakText(`Now let me ask you some questions. Here's the first one: ${L.question}`,{lipsyncLang:D.lipsyncLang}):L.type==="true_false"?u.current.speakText(`Let's start with some true or false questions. First question: ${L.question}`,{lipsyncLang:D.lipsyncLang}):u.current.speakText(`Now let me ask you some questions. Here's the first one: ${L.question}`,{lipsyncLang:D.lipsyncLang})};if(u.current&&u.current.isReady&&L)F();else if(u.current&&u.current.isReady){const D=P.current||{lipsyncLang:"en"};u.current.speakText("Now let me ask you some questions to test your understanding.",{lipsyncLang:D.lipsyncLang})}else{const D=setInterval(()=>{u.current&&u.current.isReady&&(clearInterval(D),L&&F())},100);setTimeout(()=>{clearInterval(D)},5e3)}},[e.questionStart,y,E]),W=x.useCallback(()=>{const I=y();if(r.current.currentQuestionIndex<(I?.questions?.length||0)-1){u.current&&u.current.stopSpeaking&&u.current.stopSpeaking(),r.current.currentQuestionIndex+=1;const L=E();L&&d.current.onCustomAction({type:"nextQuestion",moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex,questionIndex:r.current.currentQuestionIndex,totalQuestions:r.current.totalQuestions,question:L,score:r.current.score});const F=()=>{if(!u.current||!L)return;if(u.current.setMood("happy"),u.current.setBodyMovement("idle"),e.nextQuestion)try{u.current.playAnimation(e.nextQuestion,!0)}catch(J){console.warn("Failed to play nextQuestion animation:",J)}const D=P.current||{lipsyncLang:"en"},se=y()?.questions?.length||0,te=r.current.currentQuestionIndex>=se-1;if(L.type==="code_test"){const J=te?`Great! Here's your final coding challenge: ${L.question}`:`Great! Now let's move on to your next coding challenge: ${L.question}`;u.current.speakText(J,{lipsyncLang:D.lipsyncLang})}else if(L.type==="multiple_choice"){const J=te?`Alright! Here's your final question: ${L.question}`:`Alright! Here's your next question: ${L.question}`;u.current.speakText(J,{lipsyncLang:D.lipsyncLang})}else if(L.type==="true_false"){const J=te?`Now let's try this final one: ${L.question}`:`Now let's try this one: ${L.question}`;u.current.speakText(J,{lipsyncLang:D.lipsyncLang})}else{const J=te?`Here's your final question: ${L.question}`:`Here's the next question: ${L.question}`;u.current.speakText(J,{lipsyncLang:D.lipsyncLang})}};if(u.current&&u.current.isReady&&L)F();else if(L){const D=setInterval(()=>{u.current&&u.current.isReady&&(clearInterval(D),F())},100);setTimeout(()=>{clearInterval(D)},5e3)}}else d.current.onCustomAction({type:"allQuestionsComplete",moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex,totalQuestions:r.current.totalQuestions,score:r.current.score})},[e.nextQuestion,y,E]),G=x.useCallback(()=>{const I=O.current||{modules:[]},L=I.modules[r.current.currentModuleIndex];if(r.current.currentLessonIndex<(L?.lessons?.length||0)-1){r.current.currentLessonIndex+=1,r.current.currentQuestionIndex=0,r.current.lessonCompleted=!1,r.current.isQuestionMode=!1,r.current.isTeaching=!1,r.current.score=0,r.current.totalQuestions=0;const D=I.modules[r.current.currentModuleIndex],Z=r.current.currentLessonIndex<(D?.lessons?.length||0)-1,se=r.current.currentModuleIndex<(I.modules?.length||0)-1,te=Z||se;d.current.onCustomAction({type:"lessonStart",moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex,hasNextLesson:te}),d.current.onLessonStart({moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex,lesson:y()}),u.current&&(u.current.setMood("happy"),u.current.setBodyMovement("idle"))}else if(r.current.currentModuleIndex<(I.modules?.length||0)-1){r.current.currentModuleIndex+=1,r.current.currentLessonIndex=0,r.current.currentQuestionIndex=0,r.current.lessonCompleted=!1,r.current.isQuestionMode=!1,r.current.isTeaching=!1,r.current.score=0,r.current.totalQuestions=0;const Z=I.modules[r.current.currentModuleIndex],se=r.current.currentLessonIndex<(Z?.lessons?.length||0)-1,te=r.current.currentModuleIndex<(I.modules?.length||0)-1,J=se||te;d.current.onCustomAction({type:"lessonStart",moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex,hasNextLesson:J}),d.current.onLessonStart({moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex,lesson:y()}),u.current&&(u.current.setMood("happy"),u.current.setBodyMovement("idle"))}else S.current&&S.current()},[]),j=x.useCallback(()=>{const I=y();let L=null;if(I?.avatar_script&&I?.body){const F=I.avatar_script.trim(),D=I.body.trim(),Z=F.match(/[.!?]$/)?" ":". ";L=`${F}${Z}${D}`}else L=I?.avatar_script||I?.body||null;if(u.current&&u.current.isReady&&L){r.current.isTeaching=!0,r.current.isQuestionMode=!1,r.current.score=0,r.current.totalQuestions=0,u.current.setMood("happy");let F=!1;if(e.teaching)try{u.current.playAnimation(e.teaching,!0),F=!0}catch(Z){console.warn("Failed to play teaching animation:",Z)}F||u.current.setBodyMovement("gesturing");const D=P.current||{lipsyncLang:"en"};d.current.onLessonStart({moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex,lesson:I}),d.current.onCustomAction({type:"teachingStart",moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex,lesson:I}),u.current.speakText(L,{lipsyncLang:D.lipsyncLang,onSpeechEnd:()=>{r.current.isTeaching=!1,d.current.onCustomAction({type:"teachingComplete",moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex,lesson:I,hasQuestions:I.questions&&I.questions.length>0}),I?.code_example&&d.current.onCustomAction({type:"codeExampleReady",moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex,lesson:I,codeExample:I.code_example})}})}},[e.teaching,y]),Y=x.useCallback(I=>{const L=E(),F=b(I,L);if(F&&(r.current.score+=1),d.current.onQuestionAnswer({moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex,questionIndex:r.current.currentQuestionIndex,answer:I,isCorrect:F,question:L}),u.current)if(F){if(u.current.setMood("happy"),e.correct)try{u.current.playReaction("happy")}catch{u.current.setBodyMovement("happy")}u.current.setBodyMovement("gesturing");const Z=y()?.questions?.length||0;r.current.currentQuestionIndex>=Z-1;const se=r.current.currentQuestionIndex<Z-1;console.log("[CurriculumLearning] Answer feedback - questionIndex:",r.current.currentQuestionIndex,"totalQuestions:",Z,"hasNextQuestion:",se);const te=L.type==="code_test"?`Great job! Your code passed all the tests! ${L.explanation||""}`:`Excellent! That's correct! ${L.explanation||""}`,J=P.current||{lipsyncLang:"en"};u.current.speakText(te,{lipsyncLang:J.lipsyncLang,onSpeechEnd:()=>{d.current.onCustomAction({type:"answerFeedbackComplete",moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex,questionIndex:r.current.currentQuestionIndex,isCorrect:!0,hasNextQuestion:se,score:r.current.score,totalQuestions:r.current.totalQuestions})}})}else{if(u.current.setMood("sad"),e.incorrect)try{u.current.playAnimation(e.incorrect,!0)}catch{u.current.setBodyMovement("idle")}u.current.setBodyMovement("gesturing");const Z=y()?.questions?.length||0,se=r.current.currentQuestionIndex>=Z-1,te=r.current.currentQuestionIndex<Z-1;console.log("[CurriculumLearning] Answer feedback (incorrect) - questionIndex:",r.current.currentQuestionIndex,"totalQuestions:",Z,"hasNextQuestion:",te);const J=L.type==="code_test"?`Your code didn't pass all the tests. ${L.explanation||"Try again!"}`:`Not quite right, but don't worry! ${L.explanation||""}${se?"":" Let's move on to the next question."}`,he=P.current||{lipsyncLang:"en"};u.current.speakText(J,{lipsyncLang:he.lipsyncLang,onSpeechEnd:()=>{d.current.onCustomAction({type:"answerFeedbackComplete",moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex,questionIndex:r.current.currentQuestionIndex,isCorrect:!1,hasNextQuestion:te,score:r.current.score,totalQuestions:r.current.totalQuestions})}})}else{const Z=y()?.questions?.length||0;d.current.onCustomAction({type:"answerFeedbackComplete",moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex,questionIndex:r.current.currentQuestionIndex,isCorrect:F,hasNextQuestion:r.current.currentQuestionIndex<Z-1,score:r.current.score,totalQuestions:r.current.totalQuestions,avatarNotReady:!0})}},[e.correct,e.incorrect,E,y,b]),ae=x.useCallback(I=>{const L=E();if(!I||typeof I!="object"){console.error("Invalid code test result format. Expected object with {passed: boolean, ...}");return}if(L?.type!=="code_test"){console.warn("Current question is not a code test. Use handleAnswerSelect for other question types.");return}const F={passed:I.passed===!0,results:I.results||[],output:I.output||"",error:I.error||null,executionTime:I.executionTime||null,testCount:I.testCount||0,passedCount:I.passedCount||0,failedCount:I.failedCount||0};d.current.onCustomAction({type:"codeTestSubmitted",moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex,questionIndex:r.current.currentQuestionIndex,testResult:F,question:L}),p.current&&p.current(F)},[E,b]),re=x.useCallback(()=>{if(r.current.currentQuestionIndex>0){r.current.currentQuestionIndex-=1;const I=E();I&&d.current.onCustomAction({type:"questionStart",moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex,questionIndex:r.current.currentQuestionIndex,totalQuestions:r.current.totalQuestions,question:I,score:r.current.score});const L=()=>{if(!u.current||!I)return;u.current.setMood("happy"),u.current.setBodyMovement("idle");const F=P.current||{lipsyncLang:"en"};I.type==="code_test"?u.current.speakText(`Let's go back to this coding challenge: ${I.question}`,{lipsyncLang:F.lipsyncLang}):u.current.speakText(`Going back to: ${I.question}`,{lipsyncLang:F.lipsyncLang})};if(u.current&&u.current.isReady&&I)L();else if(I){const F=setInterval(()=>{u.current&&u.current.isReady&&(clearInterval(F),L())},100);setTimeout(()=>{clearInterval(F)},5e3)}}},[E]),xe=x.useCallback(()=>{const I=O.current||{modules:[]};if(I.modules[r.current.currentModuleIndex],r.current.currentLessonIndex>0)r.current.currentLessonIndex-=1,r.current.currentQuestionIndex=0,r.current.lessonCompleted=!1,r.current.isQuestionMode=!1,r.current.isTeaching=!1,r.current.score=0,r.current.totalQuestions=0,d.current.onCustomAction({type:"lessonStart",moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex}),d.current.onLessonStart({moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex,lesson:y()}),u.current&&(u.current.setMood("happy"),u.current.setBodyMovement("idle"));else if(r.current.currentModuleIndex>0){const D=I.modules[r.current.currentModuleIndex-1];r.current.currentModuleIndex-=1,r.current.currentLessonIndex=(D?.lessons?.length||1)-1,r.current.currentQuestionIndex=0,r.current.lessonCompleted=!1,r.current.isQuestionMode=!1,r.current.isTeaching=!1,r.current.score=0,r.current.totalQuestions=0,d.current.onCustomAction({type:"lessonStart",moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex}),d.current.onLessonStart({moduleIndex:r.current.currentModuleIndex,lessonIndex:r.current.currentLessonIndex,lesson:y()}),u.current&&(u.current.setMood("happy"),u.current.setBodyMovement("idle"))}},[y]),oe=x.useCallback(()=>{r.current.currentModuleIndex=0,r.current.currentLessonIndex=0,r.current.currentQuestionIndex=0,r.current.isTeaching=!1,r.current.isQuestionMode=!1,r.current.lessonCompleted=!1,r.current.curriculumCompleted=!1,r.current.score=0,r.current.totalQuestions=0},[]),Se=x.useCallback(I=>{console.log("Avatar is ready!",I);const L=y(),F=L?.avatar_script||L?.body;c&&F&&setTimeout(()=>{h.current&&h.current()},10)},[c,y]);x.useLayoutEffect(()=>{h.current=j,g.current=G,v.current=k,R.current=W,S.current=T,M.current=A,p.current=Y}),x.useImperativeHandle(l,()=>({startTeaching:j,startQuestions:A,handleAnswerSelect:Y,handleCodeTestResult:ae,nextQuestion:W,previousQuestion:re,nextLesson:G,previousLesson:xe,completeLesson:k,completeCurriculum:T,resetCurriculum:oe,getState:()=>({...r.current}),getCurrentQuestion:()=>E(),getCurrentLesson:()=>y(),getAvatarRef:()=>u.current,speakText:async(I,L={})=>{await u.current?.resumeAudioContext?.();const F=P.current||{lipsyncLang:"en"};u.current?.speakText(I,{...L,lipsyncLang:L.lipsyncLang||F.lipsyncLang})},resumeAudioContext:async()=>{if(u.current?.resumeAudioContext)return await u.current.resumeAudioContext();const I=u.current?.talkingHead;if(I?.audioCtx){const L=I.audioCtx;if(L.state==="suspended"||L.state==="interrupted")try{await L.resume(),console.log("Audio context resumed via talkingHead")}catch(F){console.warn("Failed to resume audio context:",F)}}else console.warn("Audio context not available yet")},stopSpeaking:()=>u.current?.stopSpeaking(),pauseSpeaking:()=>u.current?.pauseSpeaking(),resumeSpeaking:async()=>await u.current?.resumeSpeaking(),isPaused:()=>u.current&&typeof u.current.isPaused<"u"?u.current.isPaused:!1,setMood:I=>u.current?.setMood(I),playAnimation:(I,L)=>u.current?.playAnimation(I,L),setBodyMovement:I=>u.current?.setBodyMovement(I),setMovementIntensity:I=>u.current?.setMovementIntensity(I),playRandomDance:()=>u.current?.playRandomDance(),playReaction:I=>u.current?.playReaction(I),playCelebration:()=>u.current?.playCelebration(),setShowFullAvatar:I=>u.current?.setShowFullAvatar(I),setTimingAdjustment:I=>u.current?.setTimingAdjustment(I),lockAvatarPosition:()=>u.current?.lockAvatarPosition(),unlockAvatarPosition:()=>u.current?.unlockAvatarPosition(),triggerCustomAction:(I,L)=>{d.current.onCustomAction({type:I,...L,state:{...r.current}})},handleResize:()=>u.current?.handleResize(),isAvatarReady:()=>u.current?.isReady||!1}),[j,A,Y,ae,W,G,k,T,oe,E,y]);const $=P.current||{avatarUrl:"/avatars/brunette.glb",avatarBody:"F",mood:"happy",ttsLang:"en",ttsService:null,ttsVoice:null,ttsApiKey:null,bodyMovement:"gesturing",movementIntensity:.7,showFullAvatar:!1,animations:e};return V.jsx("div",{style:{width:"100%",height:"100%"},children:V.jsx(Ze,{ref:u,avatarUrl:$.avatarUrl,avatarBody:$.avatarBody,mood:$.mood,ttsLang:$.ttsLang,ttsService:$.ttsService,ttsVoice:$.ttsVoice,ttsApiKey:$.ttsApiKey,bodyMovement:$.bodyMovement,movementIntensity:$.movementIntensity,showFullAvatar:$.showFullAvatar,cameraView:"upper",animations:$.animations,onReady:Se,onLoading:()=>{},onError:I=>{console.error("Avatar error:",I)}})})});$e.displayName="CurriculumLearning";function wt({manifestPath:N="/animations/manifest.json",avatarBody:t="F",onAnimationPlay:e=null,onAnimationsSelected:n=null,onDeleteAnimations:o=null,style:s={}}){const[i,a]=x.useState([]),[c,l]=x.useState(new Set),[u,r]=x.useState(!0),[d,h]=x.useState(null),[g,v]=x.useState("all"),[R,S]=x.useState("");x.useEffect(()=>{(async()=>{r(!0),h(null);try{const T=await Ee(N),A=[];if(T._genderSpecific){const G=(t?.toUpperCase()||"F")==="M"?"male":"female";T._genderSpecific[G]&&Object.entries(T._genderSpecific[G]).forEach(([j,Y])=>{(Array.isArray(Y)?Y:[Y]).forEach(re=>{A.push({path:re,group:j,gender:G,name:re.split("/").pop().replace(".fbx","")})})}),T._genderSpecific.shared&&Object.entries(T._genderSpecific.shared).forEach(([j,Y])=>{(Array.isArray(Y)?Y:[Y]).forEach(re=>{A.push({path:re,group:j,gender:"shared",name:re.split("/").pop().replace(".fbx","")})})})}Object.entries(T).forEach(([W,G])=>{W!=="_genderSpecific"&&(Array.isArray(G)?G:[G]).forEach(Y=>{typeof Y=="string"&&A.push({path:Y,group:W,gender:"root",name:Y.split("/").pop().replace(".fbx","")})})}),a(A),r(!1)}catch(T){console.error("Failed to load animations:",T),h(T.message),r(!1)}})()},[N,t]);const M=["all",...new Set(i.map(k=>k.group))],p=i.filter(k=>{const T=g==="all"||k.group===g,A=R===""||k.name.toLowerCase().includes(R.toLowerCase())||k.path.toLowerCase().includes(R.toLowerCase());return T&&A}),O=k=>{const T=new Set(c);T.has(k)?T.delete(k):T.add(k),l(T),n&&n(Array.from(T))},P=()=>{const k=new Set(c);p.forEach(T=>{k.add(T.path)}),l(k),n&&n(Array.from(k))},y=()=>{const k=new Set(c);p.forEach(T=>{k.delete(T.path)}),l(k),n&&n(Array.from(k))},E=()=>{const T=i.filter(A=>!c.has(A.path)).map(A=>A.path);if(T.length===0){alert("No animations to delete. Select animations to keep, then delete will remove the unselected ones.");return}window.confirm(`Are you sure you want to delete ${T.length} animation(s)?
8
8
 
9
9
  This will delete:
10
10
  ${T.slice(0,5).join(`
package/dist/index.js CHANGED
@@ -6346,7 +6346,9 @@ class Qe {
6346
6346
  this.animationFinishedCallback && (this.animationFinishedCallback(), this.animationFinishedCallback = null), this.stopAnimation();
6347
6347
  };
6348
6348
  this.mixer.addEventListener("finished", u, { once: !0 });
6349
- const r = Math.ceil(n / c.clip.duration), d = this.mixer.clipAction(c.clip);
6349
+ let r = 1;
6350
+ n > 0 && (r = Math.ceil(n / c.clip.duration));
6351
+ const d = this.mixer.clipAction(c.clip);
6350
6352
  if (d.setLoop(f.LoopRepeat, r), d.clampWhenFinished = !0, this.currentFBXAction && this.currentFBXAction.isRunning()) {
6351
6353
  this.currentFBXAction.fadeOut(0.3), setTimeout(() => {
6352
6354
  this.currentFBXAction = d;
@@ -7629,7 +7631,7 @@ const Ct = Ze(({
7629
7631
  J.current = xe(N), ne.current = [];
7630
7632
  }
7631
7633
  const C = J.current.shift();
7632
- return ne.current.push(C), C;
7634
+ return C && ne.current.push(C), C;
7633
7635
  }, [oe, xe]), pe = D((k) => k ? k.split("/").pop().replace(".fbx", "").replace(/[-_]/g, " ") : "Unknown", []), Ie = D((k, C = !1, N = null) => {
7634
7636
  if (!x.current)
7635
7637
  return null;
@@ -7643,7 +7645,7 @@ const Ct = Ze(({
7643
7645
  Ie(k, C, N);
7644
7646
  }, 100) : N && N();
7645
7647
  };
7646
- return x.current.playAnimation(te, null, 10, 0, 0.01, C, Se), te;
7648
+ return x.current.playAnimation(te, null, 0, 0, 0.01, C, Se), te;
7647
7649
  } catch (K) {
7648
7650
  return console.error("Failed to play animation:", K), null;
7649
7651
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sage-rsc/talking-head-react",
3
- "version": "1.6.6",
3
+ "version": "1.6.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",
@@ -433,7 +433,7 @@ const SimpleTalkingAvatar = forwardRef(({
433
433
 
434
434
  // Helper function to get next animation from queue (or create new shuffled queue)
435
435
  const getNextAnimation = useCallback((groupName) => {
436
- // If queue is empty or we've played all animations, create a new shuffled queue
436
+ // If queue is empty, create a new shuffled queue
437
437
  if (animationQueueRef.current.length === 0) {
438
438
  const allAnimations = getAllAnimationsFromGroup(groupName);
439
439
  if (allAnimations.length === 0) {
@@ -446,7 +446,9 @@ const SimpleTalkingAvatar = forwardRef(({
446
446
 
447
447
  // Get next animation from queue
448
448
  const nextAnimation = animationQueueRef.current.shift();
449
- playedAnimationsRef.current.push(nextAnimation);
449
+ if (nextAnimation) {
450
+ playedAnimationsRef.current.push(nextAnimation);
451
+ }
450
452
 
451
453
  return nextAnimation;
452
454
  }, [getAllAnimationsFromGroup, shuffleArray]);
@@ -483,8 +485,8 @@ const SimpleTalkingAvatar = forwardRef(({
483
485
  }
484
486
  };
485
487
 
486
- // Play animation with callback - fade transitions are handled in playAnimation
487
- talkingHeadRef.current.playAnimation(animationPath, null, 10, 0, 0.01, disablePositionLock, animationFinishedCallback);
488
+ // Play animation once (dur=0 means play once, use clip's natural duration)
489
+ talkingHeadRef.current.playAnimation(animationPath, null, 0, 0, 0.01, disablePositionLock, animationFinishedCallback);
488
490
 
489
491
  return animationPath;
490
492
  } catch (error) {
@@ -5432,7 +5432,11 @@ class TalkingHead {
5432
5432
  this.mixer.addEventListener( 'finished', finishedHandler, { once: true });
5433
5433
 
5434
5434
  // Play action with error handling
5435
- const repeat = Math.ceil(dur / item.clip.duration);
5435
+ // If dur is 0 or negative, play animation once (use clip's natural duration)
5436
+ let repeat = 1;
5437
+ if (dur > 0) {
5438
+ repeat = Math.ceil(dur / item.clip.duration);
5439
+ }
5436
5440
  const action = this.mixer.clipAction(item.clip);
5437
5441
  action.setLoop( THREE.LoopRepeat, repeat );
5438
5442
  action.clampWhenFinished = true;