Juan Jimenez Carrero Claude Opus 4.8 commited on
Commit
f71bdf7
Β·
1 Parent(s): 8c09fe3

fix: smooth head-only motion, no audio duplication, mic diagnostics

Browse files

- Movement: port the reachy-stories approach β€” a 20 Hz (not 60 Hz) client-side
loop that wobbles the HEAD only (roll/pitch/yaw) from smoothed speech energy
while talking, gentle head breath when idle. Antennas are no longer driven
from the wobble β€” oscillating them at audio rate was the buzz. Daemon wobbler
stays off; motion is client-side (it doesn't react to streamed WebRTC audio).
- Duplicate audio: play TTS on the phone speaker ONLY when the robot isn't
connected; when the robot plays it, phone output is muted (no double sound,
no phone-mic feedback loop).
- 'Reachy can't hear me': added mic-level logging (avg sample magnitude) to
reveal whether the robot mic track actually carries audio; keep the worklet
in the render graph via a 0-gain sink so process() reliably runs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

assets/{embed-VZIqWv3F.js β†’ embed-ByAC4o7k.js} RENAMED
The diff for this file is too large to render. See raw diff
 
assets/{index-Bpu0WOHF.js β†’ index-DGQc0Yr2.js} RENAMED
@@ -1,4 +1,4 @@
1
- const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/auto-CaTD3xKq.js","assets/sub-paths-F6TP7MGR-8fYyfeHC.js","assets/embed-VZIqWv3F.js","assets/embed-BmrOAEFs.css","assets/landing-B7BE48AY.js","assets/landing-BJE7fNCb.css"])))=>i.map(i=>d[i]);
2
  var G=Object.defineProperty;var q=(o,a,e)=>a in o?G(o,a,{enumerable:!0,configurable:!0,writable:!0,value:e}):o[a]=e;var d=(o,a,e)=>q(o,typeof a!="symbol"?a+"":a,e);(function(){const a=document.createElement("link").relList;if(a&&a.supports&&a.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))t(i);new MutationObserver(i=>{for(const s of i)if(s.type==="childList")for(const n of s.addedNodes)n.tagName==="LINK"&&n.rel==="modulepreload"&&t(n)}).observe(document,{childList:!0,subtree:!0});function e(i){const s={};return i.integrity&&(s.integrity=i.integrity),i.referrerPolicy&&(s.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?s.credentials="include":i.crossOrigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function t(i){if(i.ep)return;i.ep=!0;const s=e(i);fetch(i.href,s)}})();const W="modulepreload",z=function(o){return"/"+o},O={},U=function(a,e,t){let i=Promise.resolve();if(e&&e.length>0){document.getElementsByTagName("link");const n=document.querySelector("meta[property=csp-nonce]"),r=(n==null?void 0:n.nonce)||(n==null?void 0:n.getAttribute("nonce"));i=Promise.allSettled(e.map(c=>{if(c=z(c),c in O)return;O[c]=!0;const l=c.endsWith(".css"),h=l?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${c}"]${h}`))return;const u=document.createElement("link");if(u.rel=l?"stylesheet":W,l||(u.as="script"),u.crossOrigin="",u.href=c,r&&u.setAttribute("nonce",r),document.head.appendChild(u),l)return new Promise((p,g)=>{u.addEventListener("load",p),u.addEventListener("error",()=>g(new Error(`Unable to preload CSS for ${c}`)))})}))}function s(n){const r=new Event("vite:preloadError",{cancelable:!0});if(r.payload=n,window.dispatchEvent(r),!r.defaultPrevented)throw n}return i.then(n=>{for(const r of n||[])r.status==="rejected"&&s(r.reason);return a().catch(s)})};var J=Object.defineProperty,K=(o,a,e)=>a in o?J(o,a,{enumerable:!0,configurable:!0,writable:!0,value:e}):o[a]=e,k=(o,a,e)=>(K(o,typeof a!="symbol"?a+"":a,e),e),B="https://huggingface.co";async function T(o,a){var e,t;const i=new X(o.url,o.status,(e=o.headers.get("X-Request-Id"))!=null?e:void 0);i.message=`Api error with status ${i.statusCode}`;const s=[`URL: ${i.url}`,i.requestId?`Request ID: ${i.requestId}`:void 0].filter(Boolean).join(". ");if((t=o.headers.get("Content-Type"))!=null&&t.startsWith("application/json")){const n=await o.json();i.message=n.error||n.message||i.message,i.data=n}else i.data={message:await o.text()};throw i.message+=`. ${s}`,i}var X=class extends Error{constructor(o,a,e,t){super(t),k(this,"statusCode"),k(this,"url"),k(this,"requestId"),k(this,"data"),this.statusCode=a,this.requestId=e,this.url=o}};new Promise(o=>{});function Y(o){if(globalThis.Buffer)return globalThis.Buffer.from(o).toString("base64");{const a=[];return o.forEach(e=>{a.push(String.fromCharCode(e))}),globalThis.btoa(a.join(""))}}async function $(o){var a,e;if(typeof window>"u")throw new Error("oauthHandleRedirect is only available in the browser");const t=new URLSearchParams(window.location.search),[i,s]=[t.get("error"),t.get("error_description")];if(i)throw new Error(`${i}: ${s}`);const n=t.get("code"),r=localStorage.getItem("huggingface.co:oauth:nonce");if(!n)throw new Error("Missing oauth code from query parameters in redirected URL");if(!r)throw new Error("Missing oauth nonce from localStorage");const c=localStorage.getItem("huggingface.co:oauth:code_verifier");if(!c)throw new Error("Missing oauth code_verifier from localStorage");const l=t.get("state");if(!l)throw new Error("Missing oauth state from query parameters in redirected URL");let h;try{h=JSON.parse(l)}catch{throw new Error("Invalid oauth state in redirected URL, unable to parse JSON: "+l)}if(h.nonce!==r)throw new Error("Invalid oauth state in redirected URL");const u=B,p=`${new URL(u).origin}/.well-known/openid-configuration`,g=await fetch(p,{headers:{Accept:"application/json"}});if(!g.ok)throw await T(g);const w=await g.json(),m=await fetch(w.token_endpoint,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({grant_type:"authorization_code",code:n,redirect_uri:h.redirectUri,code_verifier:c}).toString()});if(localStorage.removeItem("huggingface.co:oauth:code_verifier"),localStorage.removeItem("huggingface.co:oauth:nonce"),!m.ok)throw await T(m);const f=await m.json(),y=new Date(Date.now()+f.expires_in*1e3),v=await fetch(w.userinfo_endpoint,{headers:{Authorization:`Bearer ${f.access_token}`}});if(!v.ok)throw await T(v);const b=await v.json();return{accessToken:f.access_token,accessTokenExpiresAt:y,userInfo:{id:b.sub,name:b.name,fullname:b.preferred_username,email:b.email,emailVerified:b.email_verified,avatarUrl:b.picture,websiteUrl:b.website,isPro:b.isPro,orgs:(e=(a=b.orgs)==null?void 0:a.map(R=>({id:R.sub,name:R.name,fullname:R.name,isEnterprise:R.isEnterprise,canPay:R.canPay,avatarUrl:R.picture,roleInOrg:R.roleInOrg})))!=null?e:[]},state:h.state,scope:f.scope}}async function Q(o){if(typeof window>"u")throw new Error("oauthHandleRedirect is only available in the browser");const a=new URLSearchParams(window.location.search);return a.has("error")?$():a.has("code")?localStorage.getItem("huggingface.co:oauth:nonce")?$():(console.warn("Missing oauth nonce from localStorage. This can happen when the user refreshes the page after logging in, without changing the URL."),!1):!1}async function Z(o){var a,e;if(typeof window>"u")throw new Error("oauthLogin is only available in the browser");const t=(o==null?void 0:o.hubUrl)||B,i=`${new URL(t).origin}/.well-known/openid-configuration`,s=await fetch(i,{headers:{Accept:"application/json"}});if(!s.ok)throw await T(s);const n=await s.json(),r=globalThis.crypto.randomUUID(),c=globalThis.crypto.randomUUID()+globalThis.crypto.randomUUID();localStorage.setItem("huggingface.co:oauth:nonce",r),localStorage.setItem("huggingface.co:oauth:code_verifier",c);const l=(o==null?void 0:o.redirectUrl)||window.location.href,h=JSON.stringify({nonce:r,redirectUri:l,state:o==null?void 0:o.state}),u=(e=(a=window==null?void 0:window.huggingface)==null?void 0:a.variables)!=null?e:null,p=(o==null?void 0:o.clientId)||(u==null?void 0:u.OAUTH_CLIENT_ID);if(!p)throw u?new Error("Missing clientId, please add hf_oauth: true to the README.md's metadata in your static Space"):new Error("Missing clientId");const g=Y(new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256",new TextEncoder().encode(c)))).replace(/[+]/g,"-").replace(/[/]/g,"_").replace(/=/g,"");return`${n.authorization_endpoint}?${new URLSearchParams({client_id:p,scope:(o==null?void 0:o.scopes)||(u==null?void 0:u.OAUTH_SCOPES)||"openid profile",response_type:"code",redirect_uri:l,state:h,code_challenge:g,code_challenge_method:"S256"}).toString()}`}function C(o){return o*Math.PI/180}function _e(o){return o*180/Math.PI}function ee(o,a,e){const t=C(o),i=C(a),s=C(e),n=Math.cos(s),r=Math.sin(s),c=Math.cos(i),l=Math.sin(i),h=Math.cos(t),u=Math.sin(t);return[[n*c,n*l*u-r*h,n*l*h+r*u,0],[r*c,r*l*u+n*h,r*l*h-n*u,0],[-l,c*u,c*h,0],[0,0,0,1]]}function te(){if(typeof window>"u"||!window.location.hash)return;const o=window.location.hash.startsWith("#")?window.location.hash.slice(1):window.location.hash;let a;try{a=new URLSearchParams(o)}catch{return}const e=a.get("hf_token");if(!e)return;const t=a.get("hf_username")||"user",i=a.get("hf_token_expires"),s=i&&!Number.isNaN(new Date(i).getTime())?i:new Date(Date.now()+365*24*60*60*1e3).toISOString();try{sessionStorage.setItem("hf_token",e),sessionStorage.setItem("hf_username",t),sessionStorage.setItem("hf_token_expires",s)}catch(c){console.warn("[reachy-mini] could not persist pre-seeded HF credentials:",c)}a.delete("hf_token"),a.delete("hf_username"),a.delete("hf_token_expires");const n=a.toString(),r=window.location.pathname+window.location.search+(n?"#"+n:"");try{window.history.replaceState(null,"",r)}catch{}}function ie(){if(typeof window>"u")return null;if(window.location.hash){const o=window.location.hash.startsWith("#")?window.location.hash.slice(1):window.location.hash;try{const e=new URLSearchParams(o).get("robot_peer_id");if(e)return e}catch{}}if(window.location.search)try{const a=new URLSearchParams(window.location.search).get("robot_peer_id");if(a)return a}catch{}return null}function se(o){const a=o.split(`\r
3
  `);let e=!1;for(const t of a)if(t.startsWith("m=audio")?e=!0:t.startsWith("m=")&&(e=!1),e&&t==="a=sendrecv")return!0;return!1}const S=12*1024,L=1*1024*1024,ne=512*1024;function oe(){return typeof CompressionStream<"u"}function x(){return"u"+Math.random().toString(36).slice(2,11)+Date.now().toString(36)}function P(o){let a="";for(let t=0;t<o.length;t+=32768)a+=String.fromCharCode.apply(null,Array.from(o.subarray(t,t+32768)));return btoa(a)}async function re(o){const a=new TextEncoder().encode(o),e=await new Response(new Blob([a]).stream().pipeThrough(new CompressionStream("gzip"))).arrayBuffer();return P(new Uint8Array(e))}function N(o){const a=Math.round(Number(o)||0);return Math.max(0,Math.min(100,a))}const H=3e3,V=1e3,ae=6e4;class le extends EventTarget{constructor(e={}){super();d(this,"_signalingUrl");d(this,"_clientId");d(this,"_appName");d(this,"_videoJitterBufferTargetMs");d(this,"_autoStartFromUrl");d(this,"_autoStartAttempted");d(this,"_state","disconnected");d(this,"_robots",[]);d(this,"_robotState",{});d(this,"_preselectedRobotId");d(this,"_token",null);d(this,"_username",null);d(this,"_tokenExpires",null);d(this,"_sseAbortController",null);d(this,"_pc",null);d(this,"_dc",null);d(this,"_sessionId",null);d(this,"_selectedRobotId",null);d(this,"_pendingRemoteIce",[]);d(this,"_micStream",null);d(this,"_micMuted",!0);d(this,"_audioMuted",!0);d(this,"_micSupported",!1);d(this,"_latencyMonitorId",null);d(this,"_stateRefreshInterval",null);d(this,"_versionResolve",null);d(this,"_hardwareIdResolve",null);d(this,"_volumeResolve",null);d(this,"_micVolumeResolve",null);d(this,"_applyAudioConfigResolve",null);d(this,"_readAudioParameterResolve",null);d(this,"_logSubscribers",new Set);d(this,"_broadcastWaiters",[]);d(this,"_activeMoveUploadId",null);d(this,"_activeAudioUploadId",null);d(this,"_sessionResolve",null);d(this,"_sessionReject",null);d(this,"_iceConnected",!1);d(this,"_dcOpen",!1);d(this,"_iceGraceTimer",null);d(this,"_iceGraceReason",null);d(this,"_pendingVisibilityHandler",null);d(this,"_onlineHandler",null);d(this,"_offlineHandler",null);d(this,"_connectionChangeHandler",null);d(this,"_pendingMotionCompletions",{wake_up:[],goto_sleep:[]});d(this,"_videoElement",null);this._signalingUrl=e.signalingUrl||"https://pollen-robotics-reachy-mini-central.hf.space",this._clientId=e.clientId||null,this._appName=e.appName||"unknown",this._videoJitterBufferTargetMs=e.videoJitterBufferTargetMs??0,this._autoStartFromUrl=e.autoStartFromUrl===!0,this._autoStartAttempted=!1,this._preselectedRobotId=ie()}get state(){return this._state}get robots(){return this._robots}get robotState(){return this._robotState}get username(){return this._username}get isAuthenticated(){return!!this._token}get micSupported(){return this._micSupported}get micMuted(){return this._micMuted}get audioMuted(){return this._audioMuted}get preselectedRobotId(){return this._preselectedRobotId}get isEmbedded(){return this._preselectedRobotId!==null}_maybeAutoStart(){if(!this._autoStartFromUrl||this._autoStartAttempted||!this._preselectedRobotId||this._state!=="connected"||!this._robots.find(i=>i.id===this._preselectedRobotId))return;this._autoStartAttempted=!0;const t=this._preselectedRobotId;setTimeout(()=>{this._state==="connected"&&this.startSession(t).catch(i=>{console.warn("[reachy-mini] autoStartFromUrl: startSession rejected:",i)})},0)}async authenticate(){try{te();const e=await Q();if(e)return this._username=e.userInfo.preferred_username||e.userInfo.name||null,this._token=e.accessToken,this._tokenExpires=e.accessTokenExpiresAt,sessionStorage.setItem("hf_token",this._token),sessionStorage.setItem("hf_username",this._username??""),sessionStorage.setItem("hf_token_expires",typeof this._tokenExpires=="string"?this._tokenExpires:this._tokenExpires.toISOString()),!0;const t=sessionStorage.getItem("hf_token"),i=sessionStorage.getItem("hf_username"),s=sessionStorage.getItem("hf_token_expires");return t&&i&&s&&new Date(s)>new Date?(this._token=t,this._username=i,this._tokenExpires=s,!0):!1}catch(e){return console.error("Auth error:",e),!1}}async login(){const e={};this._clientId&&(e.clientId=this._clientId),window.location.href=await Z(e)}logout(){sessionStorage.removeItem("hf_token"),sessionStorage.removeItem("hf_username"),sessionStorage.removeItem("hf_token_expires"),this._username=null,this._tokenExpires=null,this.disconnect()}async connect(e){if(this._state!=="disconnected")throw new Error("Already connected");if(e&&(this._token=e),!this._token)throw new Error("No token β€” call authenticate() first or pass a token");this._sseAbortController=new AbortController;let t;try{t=await fetch(`${this._signalingUrl}/events`,{signal:this._sseAbortController.signal,headers:{Authorization:`Bearer ${this._token}`}})}catch(i){throw this._sseAbortController=null,i}if(!t.ok)throw this._sseAbortController=null,new Error(`HTTP ${t.status}`);return new Promise((i,s)=>{let n=!1;const r=t.body.getReader(),c=new TextDecoder;let l="";(async()=>{try{for(;;){const{done:u,value:p}=await r.read();if(u)break;l+=c.decode(p,{stream:!0});const g=l.split(`
4
- `);l=g.pop()??"";for(const w of g)if(w.startsWith("data:"))try{const m=JSON.parse(w.slice(5).trim());!n&&m.type==="welcome"&&m.peerId&&(n=!0,this._state="connected",await this._sendToServer({type:"setPeerStatus",roles:["listener"],meta:{name:this._appName}}),this._emit("connected",{peerId:m.peerId}),i()),this._handleSignalingMessage(m)}catch{}}}catch(u){if(u.name!=="AbortError"&&this._emit("error",{source:"signaling",error:u}),!n){s(u);return}}this._state!=="disconnected"&&(this._state="disconnected",this._emit("disconnected",{reason:"SSE closed"})),n||s(new Error("Connection closed before welcome"))})()})}async autoConnect(e={}){var l,h,u,p,g;const{token:t,pickRobot:i,autoPickIfSingle:s=!0,filterBusy:n=!0,wakeOnConnect:r=!0}=e;if(this._state==="streaming"){const w=(l=this._robots)==null?void 0:l.find(m=>m.id===this._selectedRobotId);return{robotId:this._selectedRobotId,robotName:((h=w==null?void 0:w.meta)==null?void 0:h.name)??null,isEmbedded:this.isEmbedded,alreadyStreaming:!0}}const c=this._autoStartFromUrl;this._autoStartFromUrl=!1;try{if(t)this._token=t;else if(!this._token&&!await this.authenticate())throw new Error("Not authenticated β€” call login() or pass a token");this._state==="disconnected"&&await this.connect();let w,m=null;if(this.isEmbedded){w=this._preselectedRobotId;try{await this._waitForRobotInList(w,5e3)}catch{}const f=(u=this._robots)==null?void 0:u.find(y=>y.id===w);m=((p=f==null?void 0:f.meta)==null?void 0:p.name)??null}else{const f=await this._fetchOwnedRobots({filterBusy:n});if(f.length===0)throw new Error("No reachable robots");if(s&&f.length===1&&!f[0].busy)w=f[0].id,m=f[0].name;else if(i){const y=await i(f);if(!y)throw new Error("Robot selection cancelled");w=y,m=((g=f.find(v=>v.id===y))==null?void 0:g.name)??null}else throw new Error("Multiple robots available β€” pass a pickRobot callback to autoConnect()")}if(await this.startSession(w),r&&typeof this.ensureAwake=="function")try{await this.ensureAwake()}catch(f){console.warn("[reachy-mini] autoConnect: ensureAwake failed:",f)}return{robotId:w,robotName:m,isEmbedded:this.isEmbedded}}finally{this._autoStartFromUrl=c}}async _fetchOwnedRobots({filterBusy:e=!0}={}){var t,i,s;try{const n=await fetch(`${this._signalingUrl}/api/robot-status`,{headers:{Authorization:`Bearer ${this._token}`}});if(!n.ok)throw new Error(`HTTP ${n.status}`);const r=await n.json(),c=new Map;for(const l of r.robots||[]){if(e&&l.busy)continue;const h=((t=l.meta)==null?void 0:t.install_id)??((i=l.meta)==null?void 0:i.hardware_id)??l.peerId;c.set(h,{id:l.peerId,name:l.robotName??((s=l.meta)==null?void 0:s.name)??null,busy:!!l.busy,activeApp:l.activeApp??null,meta:l.meta??{},lastSeenAgeSeconds:l.last_seen_age_seconds??null})}return Array.from(c.values()).sort((l,h)=>(l.lastSeenAgeSeconds??1/0)-(h.lastSeenAgeSeconds??1/0))}catch(n){return console.warn("[reachy-mini] /api/robot-status unavailable, using SSE list:",n),(this._robots||[]).map(r=>{var c;return{id:r.id,name:((c=r.meta)==null?void 0:c.name)??null,busy:!1,activeApp:null,meta:r.meta??{},lastSeenAgeSeconds:null}})}}_waitForRobotInList(e,t){var i;return(i=this._robots)!=null&&i.find(s=>s.id===e)?Promise.resolve():new Promise((s,n)=>{const r=()=>{var l;(l=this._robots)!=null&&l.find(h=>h.id===e)&&(this.removeEventListener("robotsChanged",r),clearTimeout(c),s())},c=setTimeout(()=>{this.removeEventListener("robotsChanged",r),n(new Error(`Timeout waiting for robot ${e} in list`))},t);this.addEventListener("robotsChanged",r)})}async startSession(e){if(this._state!=="connected")throw new Error("Not connected");this._selectedRobotId=e,this._iceConnected=!1,this._dcOpen=!1,this._micSupported=!1,this._pendingRemoteIce=[];try{const t=window,i=t.AudioContext??t.webkitAudioContext;if(!i)throw new Error("AudioContext not supported");const s=new i,n=s.createMediaStreamDestination(),r=s.createOscillator(),c=s.createGain();c.gain.value=0,r.connect(c).connect(n),r.start();const l=n.stream;l.getAudioTracks().forEach(h=>{h.enabled=!1}),this._micStream=l,this._micMuted=!0}catch(t){console.warn("Audio sender placeholder setup failed:",t),this._micStream=null}return this._pc=new RTCPeerConnection({iceServers:[{urls:"stun:stun.l.google.com:19302"}]}),this._installNetworkListeners(),new Promise((t,i)=>{this._sessionResolve=t,this._sessionReject=i,this._pc.ontrack=s=>{if(s.track.kind==="video"){const n=this._videoJitterBufferTargetMs;try{s.receiver.jitterBufferTarget=n}catch{}try{s.receiver.playoutDelayHint=n/1e3}catch{}this._emit("videoTrack",{track:s.track,stream:s.streams[0]})}},this._pc.onicecandidate=async s=>{s.candidate&&this._sessionId&&await this._sendToServer({type:"peer",sessionId:this._sessionId,ice:{candidate:s.candidate.candidate,sdpMLineIndex:s.candidate.sdpMLineIndex,sdpMid:s.candidate.sdpMid}})},this._pc.oniceconnectionstatechange=()=>{var n;const s=(n=this._pc)==null?void 0:n.iceConnectionState;if(s){if(this._emit("iceStateChange",{state:s}),s==="connected"||s==="completed"){this._clearIceGrace(),this._iceConnected=!0,this._checkSessionReady();return}if(s==="disconnected"){typeof document<"u"&&document.hidden?this._armIceGraceOnVisibility():this._scheduleIceGrace(H,"disconnected");return}if(s==="failed"){this._scheduleIceGrace(V,"failed");return}}},this._pc.ondatachannel=s=>{this._dc=s.channel,this._dc.onopen=()=>{this._dcOpen=!0,this._checkSessionReady()},this._dc.onmessage=n=>this._handleRobotMessage(JSON.parse(n.data))},this._sendToServer({type:"startSession",peerId:e}).then(s=>{if((s==null?void 0:s.type)==="sessionRejected"){this._failSessionRejected(s);return}s!=null&&s.sessionId&&(this._sessionId=s.sessionId)})})}_failSessionRejected(e){const t=new Error(e.reason==="robot_busy"?`Robot is busy: "${e.activeApp||"another app"}" is already connected`:`Session rejected: ${e.reason||"unknown reason"}`);if(t.reason=e.reason??null,t.activeApp=e.activeApp??null,this._clearIceGrace(),this._uninstallNetworkListeners(),this._pc&&(this._pc.close(),this._pc=null),this._micStream&&(this._micStream.getTracks().forEach(i=>i.stop()),this._micStream=null),this._iceConnected=!1,this._dcOpen=!1,this._micMuted=!0,this._micSupported=!1,this._emit("sessionRejected",{reason:e.reason,activeApp:e.activeApp}),this._sessionReject){const i=this._sessionReject;this._sessionResolve=null,this._sessionReject=null,i(t)}}async stopSession(){this._versionResolve&&(this._versionResolve(null),this._versionResolve=null),this._hardwareIdResolve&&(this._hardwareIdResolve(null),this._hardwareIdResolve=null),this._volumeResolve&&(this._volumeResolve(null),this._volumeResolve=null),this._micVolumeResolve&&(this._micVolumeResolve(null),this._micVolumeResolve=null),this._applyAudioConfigResolve&&(this._applyAudioConfigResolve(!1),this._applyAudioConfigResolve=null),this._readAudioParameterResolve&&(this._readAudioParameterResolve(null),this._readAudioParameterResolve=null),this._logSubscribers.clear(),this._rejectPendingMotionCompletions(new Error("Session stopped")),this._clearIceGrace(),this._uninstallNetworkListeners(),this._sessionReject&&(this._sessionReject(new Error("Session stopped")),this._sessionResolve=null,this._sessionReject=null),this._stateRefreshInterval&&(clearInterval(this._stateRefreshInterval),this._stateRefreshInterval=null),this._latencyMonitorId&&(clearInterval(this._latencyMonitorId),this._latencyMonitorId=null),this._sessionId&&await this._sendToServer({type:"endSession",sessionId:this._sessionId}),this._micStream&&(this._micStream.getTracks().forEach(t=>t.stop()),this._micStream=null),this._micMuted=!0,this._micSupported=!1,this._pc&&(this._pc.close(),this._pc=null),this._dc&&(this._dc.close(),this._dc=null),this._sessionId=null,this._iceConnected=!1,this._dcOpen=!1,this._state==="streaming"&&(this._state="connected",this._emit("sessionStopped",{reason:"user"}))}disconnect(){this._sseAbortController&&(this._sseAbortController.abort(),this._sseAbortController=null),this._versionResolve&&(this._versionResolve(null),this._versionResolve=null),this._hardwareIdResolve&&(this._hardwareIdResolve(null),this._hardwareIdResolve=null),this._volumeResolve&&(this._volumeResolve(null),this._volumeResolve=null),this._micVolumeResolve&&(this._micVolumeResolve(null),this._micVolumeResolve=null),this._applyAudioConfigResolve&&(this._applyAudioConfigResolve(!1),this._applyAudioConfigResolve=null),this._readAudioParameterResolve&&(this._readAudioParameterResolve(null),this._readAudioParameterResolve=null),this._logSubscribers.clear(),this._rejectPendingMotionCompletions(new Error("Disconnected")),this._clearIceGrace(),this._uninstallNetworkListeners(),this._sessionReject&&(this._sessionReject(new Error("Disconnected")),this._sessionResolve=null,this._sessionReject=null),this._stateRefreshInterval&&(clearInterval(this._stateRefreshInterval),this._stateRefreshInterval=null),this._latencyMonitorId&&(clearInterval(this._latencyMonitorId),this._latencyMonitorId=null),this._sessionId&&this._token&&this._sendToServer({type:"endSession",sessionId:this._sessionId}),this._micStream&&(this._micStream.getTracks().forEach(e=>e.stop()),this._micStream=null),this._pc&&(this._pc.close(),this._pc=null),this._dc&&(this._dc.close(),this._dc=null),this._sessionId=null,this._micMuted=!0,this._micSupported=!1,this._iceConnected=!1,this._dcOpen=!1,this._robots=[],this._state="disconnected",this._emit("disconnected",{reason:"user"})}_clearIceGrace(){this._iceGraceTimer!==null&&(clearTimeout(this._iceGraceTimer),this._iceGraceTimer=null),this._iceGraceReason=null,this._pendingVisibilityHandler&&typeof document<"u"&&document.removeEventListener("visibilitychange",this._pendingVisibilityHandler),this._pendingVisibilityHandler=null}_scheduleIceGrace(e,t){if(this._iceGraceTimer!==null){if(this._iceGraceReason===t)return;clearTimeout(this._iceGraceTimer)}this._iceGraceReason=t,this._iceGraceTimer=setTimeout(()=>{var n;this._iceGraceTimer=null;const i=this._iceGraceReason;this._iceGraceReason=null;const s=(n=this._pc)==null?void 0:n.iceConnectionState;if(!(s==="connected"||s==="completed")){if(i==="disconnected"&&s==="disconnected"){this._emit("error",{source:"webrtc",error:new Error(`ICE stuck in 'disconnected' for > ${e}ms`)});return}if(i==="failed"||s==="failed"){const r=new Error("ICE connection failed");this._sessionReject&&(this._sessionReject(r),this._sessionResolve=null,this._sessionReject=null),this._emit("error",{source:"webrtc",error:r})}}},e)}_armIceGraceOnVisibility(){if(this._pendingVisibilityHandler)return;const e=Date.now(),t=()=>{if(typeof document<"u"&&document.hidden||(document.removeEventListener("visibilitychange",t),this._pendingVisibilityHandler=null,!this._pc))return;const i=this._pc.iceConnectionState;if(!(i==="connected"||i==="completed")){if(Date.now()-e>ae){const s=new Error("Session expired while tab was backgrounded");this._sessionReject&&(this._sessionReject(s),this._sessionResolve=null,this._sessionReject=null),this._emit("error",{source:"webrtc",error:s});return}if(i==="failed"){this._scheduleIceGrace(V,"failed");return}this._scheduleIceGrace(H,"disconnected")}};document.addEventListener("visibilitychange",t),this._pendingVisibilityHandler=t}_installNetworkListeners(){if(this._onlineHandler||typeof window>"u")return;const e=()=>this._emit("networkOnline",{}),t=()=>this._emit("networkOffline",{});window.addEventListener("online",e),window.addEventListener("offline",t),this._onlineHandler=e,this._offlineHandler=t;const i=navigator.connection;if(i&&typeof i.addEventListener=="function"){const s=()=>this._emit("networkChange",{effectiveType:i.effectiveType,downlink:i.downlink,rtt:i.rtt,saveData:i.saveData});i.addEventListener("change",s),this._connectionChangeHandler=s}}_uninstallNetworkListeners(){typeof window<"u"&&(this._onlineHandler&&window.removeEventListener("online",this._onlineHandler),this._offlineHandler&&window.removeEventListener("offline",this._offlineHandler));const e=navigator.connection;e&&this._connectionChangeHandler&&typeof e.removeEventListener=="function"&&e.removeEventListener("change",this._connectionChangeHandler),this._onlineHandler=null,this._offlineHandler=null,this._connectionChangeHandler=null}setTarget({head:e,antennas:t,body_yaw:i}={}){const s={type:"set_full_target"};if(e!==void 0){if(!Array.isArray(e)||e.length!==16||!e.every(n=>Number.isFinite(n)))throw new TypeError(`setTarget: head must be a 16-element flat row-major 4Γ—4 matrix of finite numbers; got ${Array.isArray(e)?`Array(${e.length})`:typeof e}`);s.head=e}if(t!==void 0){if(!Array.isArray(t)||t.length!==2||!t.every(n=>Number.isFinite(n)))throw new TypeError(`setTarget: antennas must be [rightRad, leftRad] (2 finite numbers); got ${Array.isArray(t)?`Array(${t.length})`:typeof t}`);s.antennas=t}if(i!==void 0){if(!Number.isFinite(i))throw new TypeError(`setTarget: body_yaw must be a finite number (radians); got ${i}`);s.body_yaw=i}return this._sendCommand(s)}gotoTarget({head:e,antennas:t,body_yaw:i,duration:s}){const n={type:"goto_target"};if(e!==void 0){if(!Array.isArray(e)||e.length!==16||!e.every(r=>Number.isFinite(r)))throw new TypeError(`gotoTarget: head must be a 16-element flat row-major 4Γ—4 matrix of finite numbers; got ${Array.isArray(e)?`Array(${e.length})`:typeof e}`);n.head=e}if(t!==void 0){if(!Array.isArray(t)||t.length!==2||!t.every(r=>Number.isFinite(r)))throw new TypeError(`gotoTarget: antennas must be [rightRad, leftRad] (2 finite numbers); got ${Array.isArray(t)?`Array(${t.length})`:typeof t}`);n.antennas=t}if(i!==void 0){if(!Number.isFinite(i))throw new TypeError(`gotoTarget: body_yaw must be a finite number (radians); got ${i}`);n.body_yaw=i}if(!Number.isFinite(s)||s<=0)throw new TypeError(`gotoTarget: duration must be a positive finite number (seconds); got ${s}`);return n.duration=s,this._sendCommand(n)}setHeadRpyDeg(e,t,i){return this.setTarget({head:ee(e,t,i).flat()})}setAntennasDeg(e,t){return this.setTarget({antennas:[C(e),C(t)]})}setBodyYawDeg(e){return this.setTarget({body_yaw:C(e)})}playSound(e){return this._sendCommand({type:"play_sound",file:e})}clearIncomingAudio(){return this._sendCommand({type:"clear_incoming_audio"})}setMotorMode(e){return this._sendCommand({type:"set_motor_mode",mode:e})}setMotorTorque(e,t=null){return this._sendCommand({type:"set_torque",on:e,ids:t})}wakeUp({timeoutMs:e=8e3}={}){return this._sendCommand({type:"set_motor_mode",mode:"enabled"}),this._sendCommandAwaitCompletion("wake_up",e)}gotoSleep({timeoutMs:e=8e3}={}){return this._sendCommandAwaitCompletion("goto_sleep",e)}_sendCommandAwaitCompletion(e,t){return this._sendCommand({type:e})?new Promise((i,s)=>{const n={resolve:i,reject:s,timer:setTimeout(()=>{const r=this._pendingMotionCompletions[e],c=r.indexOf(n);c!==-1&&r.splice(c,1),s(new Error(`${e} timed out after ${t}ms`))},t)};this._pendingMotionCompletions[e].push(n)}):Promise.reject(new Error(`${e}: data channel not open`))}_rejectPendingMotionCompletions(e){for(const t of Object.keys(this._pendingMotionCompletions)){const i=this._pendingMotionCompletions[t];for(;i.length;){const s=i.shift();clearTimeout(s.timer),s.reject(e)}}}isAwake(){var t;const e=(t=this._robotState)==null?void 0:t.motor_mode;return e==="enabled"||e==="gravity_compensation"}async ensureAwake(e=1e3){var t;return((t=this._robotState)==null?void 0:t.motor_mode)===void 0&&await new Promise(i=>{const s=()=>{this.removeEventListener("state",s),clearTimeout(n),i()},n=setTimeout(s,e);this.addEventListener("state",s),this.requestState()}),this.isAwake()||this.wakeUp().catch(()=>{}),!0}getVersion(){return new Promise((e,t)=>{if(!this._dc||this._dc.readyState!=="open"){t(new Error("Data channel not open"));return}this._versionResolve&&this._versionResolve(null),this._versionResolve=e,this._sendCommand({type:"get_version"})})}getHardwareId(){return new Promise((e,t)=>{if(!this._dc||this._dc.readyState!=="open"){t(new Error("Data channel not open"));return}this._hardwareIdResolve&&this._hardwareIdResolve(null),this._hardwareIdResolve=e,this._sendCommand({type:"get_hardware_id"})})}getVolume(){return this._slotRoundtrip(()=>this._volumeResolve,e=>{this._volumeResolve=e},{type:"get_volume"})}setVolume(e){return this._slotRoundtrip(()=>this._volumeResolve,t=>{this._volumeResolve=t},{type:"set_volume",volume:N(e)})}getMicrophoneVolume(){return this._slotRoundtrip(()=>this._micVolumeResolve,e=>{this._micVolumeResolve=e},{type:"get_microphone_volume"})}setMicrophoneVolume(e){return this._slotRoundtrip(()=>this._micVolumeResolve,t=>{this._micVolumeResolve=t},{type:"set_microphone_volume",volume:N(e)})}applyAudioConfig(e,{verify:t=!0}={}){return this._slotRoundtrip(()=>this._applyAudioConfigResolve,i=>{this._applyAudioConfigResolve=i},{type:"apply_audio_config",config:e,verify:t}).then(i=>i===!0)}readAudioParameter(e){return this._slotRoundtrip(()=>this._readAudioParameterResolve,t=>{this._readAudioParameterResolve=t},{type:"read_audio_parameter",name:e})}_slotRoundtrip(e,t,i){return new Promise((s,n)=>{if(!this._dc||this._dc.readyState!=="open"){n(new Error("Data channel not open"));return}const r=e();r&&r(null),t(s),this._sendCommand(i)})}sendRaw(e){return this._sendCommand(e)}subscribeLogs({onLine:e,onError:t}){if(typeof e!="function")throw new TypeError("subscribeLogs: onLine callback is required");const i={onLine:e,onError:t},s=this._logSubscribers.size===0;this._logSubscribers.add(i),s&&this._sendCommand({type:"subscribe_logs"});let n=!1;return()=>{n||(n=!0,this._logSubscribers.delete(i),this._logSubscribers.size===0&&this._sendCommand({type:"unsubscribe_logs"}))}}requestState(){return this._sendCommand({type:"get_state"})}setAudioMuted(e){this._audioMuted=e,this._videoElement&&(this._videoElement.muted=e)}setMicMuted(e){this._micMuted=e,this._micStream&&this._micStream.getAudioTracks().forEach(t=>{t.enabled=!e})}attachVideo(e){this._videoElement=e,e.muted=this._audioMuted;const t=s=>{const n=s;e.srcObject=n.detail.stream,e.playsInline=!0,"requestVideoFrameCallback"in e&&this._startLatencyMonitor(e)},i=()=>{e.srcObject=null};return this.addEventListener("videoTrack",t),this.addEventListener("sessionStopped",i),()=>{this.removeEventListener("videoTrack",t),this.removeEventListener("sessionStopped",i),this._latencyMonitorId&&(clearInterval(this._latencyMonitorId),this._latencyMonitorId=null),e.srcObject=null,this._videoElement=null}}async playMove(e,{audioBlob:t=null,audioLeadMs:i=-100,description:s="move",encoding:n="gzip+base64",playFrequency:r=100,initialGotoDuration:c=0,startTimeoutMs:l=8e3,onProgress:h=()=>{},onStarted:u=()=>{}}={}){var R,j;if(!this._dc||this._dc.readyState!=="open")throw new Error("data channel not open");if(!((R=e==null?void 0:e.time)!=null&&R.length)||!((j=e==null?void 0:e.set_target_data)!=null&&j.length))throw new Error("playMove: motion must have time + set_target_data");const p=x();this._activeMoveUploadId=p;const g={description:s,time:e.time,set_target_data:e.set_target_data},w=JSON.stringify(g);let m,f;n==="gzip+base64"&&oe()?(m=await re(w),f="gzip+base64"):(m=w,f="json");const y=Math.ceil(m.length/S)||1;h({phase:"starting",sent:0,total:y,bytes:m.length,encoding:f}),this._sendCommand({type:"upload_move_start",upload_id:p,total_chunks:y,description:s,encoding:f});for(let _=0;_<y;_++){this._dc.bufferedAmount>L&&await this._awaitDataChannelDrain();const I=_*S;this._sendCommand({type:"upload_move_chunk",upload_id:p,chunk_index:_,chunk:m.slice(I,I+S)}),h({phase:"upload",sent:_+1,total:y})}if(this._sendCommand({type:"upload_move_finish",upload_id:p}),h({phase:"uploaded",sent:y,total:y}),t){const _=new Uint8Array(await t.arrayBuffer()),I=P(_),E=Math.ceil(I.length/S)||1;h({phase:"audio-starting",sent:0,total:E,bytes:I.length}),this._sendCommand({type:"upload_audio_start",upload_id:p,total_chunks:E,encoding:"wav-base64",description:s});for(let A=0;A<E;A++){this._dc.bufferedAmount>L&&await this._awaitDataChannelDrain();const D=A*S;this._sendCommand({type:"upload_audio_chunk",upload_id:p,chunk_index:A,chunk:I.slice(D,D+S)}),h({phase:"audio-upload",sent:A+1,total:E})}this._sendCommand({type:"upload_audio_finish",upload_id:p}),h({phase:"audio-uploaded",sent:E,total:E})}this._sendCommand({type:"play_uploaded_move",upload_id:p,play_frequency:r,initial_goto_duration:c,audio_lead_ms:i});let v;try{v=await this._waitForBroadcast(_=>(_==null?void 0:_.type)==="play_uploaded_move"&&(_==null?void 0:_.upload_id)===p&&(_.started===!0||typeof _.error=="string"),{timeoutMs:l,debugLabel:"play_uploaded_move started"})}catch(_){throw new Error(`Daemon did not respond to play_uploaded_move (requires the reachy_mini daemon with feature/daemon-side-move-upload). Underlying: ${_.message}`)}if(typeof v.error=="string")throw new Error(`play_uploaded_move: ${v.error}`);try{u({duration_s:v.duration_s,has_audio:v.has_audio===!0})}catch(_){console.warn("playMove.onStarted threw:",_)}h({phase:"playing",duration_s:v.duration_s});const b=await this._waitForBroadcast(_=>(_==null?void 0:_.type)==="play_uploaded_move"&&(_==null?void 0:_.upload_id)===p&&(_.finished===!0||_.cancelled===!0||typeof _.error=="string"),{timeoutMs:(v.duration_s+30)*1e3,debugLabel:"play_uploaded_move final"});return this._activeMoveUploadId===p&&(this._activeMoveUploadId=null),b}cancelMove(e=null){const t=e??this._activeMoveUploadId;return t?this._sendCommand({type:"cancel_move",upload_id:t}):!1}async uploadAudio(e,{description:t="audio",onProgress:i=()=>{}}={}){if(!this._dc||this._dc.readyState!=="open")throw new Error("data channel not open");if(!(e instanceof Blob))throw new TypeError("uploadAudio: expected a Blob");const s=x(),n=new Uint8Array(await e.arrayBuffer()),r=P(n),c=Math.ceil(r.length/S)||1;i({phase:"audio-starting",sent:0,total:c,bytes:r.length}),this._sendCommand({type:"upload_audio_start",upload_id:s,total_chunks:c,encoding:"wav-base64",description:t});for(let l=0;l<c;l++){this._dc.bufferedAmount>L&&await this._awaitDataChannelDrain();const h=l*S;this._sendCommand({type:"upload_audio_chunk",upload_id:s,chunk_index:l,chunk:r.slice(h,h+S)}),i({phase:"audio-upload",sent:l+1,total:c})}return this._sendCommand({type:"upload_audio_finish",upload_id:s}),i({phase:"audio-uploaded",sent:c,total:c}),s}async playUploadedAudio(e,{timeoutMs:t=8e3}={}){if(!this._dc||this._dc.readyState!=="open")throw new Error("data channel not open");const i=this._waitForBroadcast(n=>(n==null?void 0:n.type)==="play_uploaded_audio"&&(n==null?void 0:n.upload_id)===e&&(n.started===!0||typeof n.error=="string"),{timeoutMs:t,debugLabel:"play_uploaded_audio started"});this._sendCommand({type:"play_uploaded_audio",upload_id:e});const s=await i;if(typeof s.error=="string")throw new Error(s.error);return this._activeAudioUploadId=e,s}cancelAudio(e=null){const t=e??this._activeAudioUploadId;return t?(this._activeAudioUploadId===t&&(this._activeAudioUploadId=null),this._sendCommand({type:"cancel_audio",upload_id:t})):!1}_emit(e,t){this.dispatchEvent(new CustomEvent(e,{detail:t}))}_waitForBroadcast(e,{timeoutMs:t=5e3,debugLabel:i=""}={}){return new Promise((s,n)=>{const r={predicate:e,resolve:s,timer:setTimeout(()=>{const c=this._broadcastWaiters.indexOf(r);c!==-1&&this._broadcastWaiters.splice(c,1),n(new Error(`broadcast timeout (${t} ms): ${i}`))},t)};this._broadcastWaiters.push(r)})}async _awaitDataChannelDrain(){for(;this._dc&&this._dc.bufferedAmount>ne;)if(await new Promise(e=>setTimeout(e,30)),!this._dc||this._dc.readyState!=="open")throw new Error("data channel closed mid-upload")}async _sendToServer(e){if(!this._token)throw new Error("No token β€” authenticate() first");try{const t=await fetch(`${this._signalingUrl}/send`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this._token}`},body:JSON.stringify(e)});if(!t.ok){let i="";try{i=await t.text()}catch{}return console.warn(`[reachy-mini] /send rejected (${t.status}) for type=${e==null?void 0:e.type}; body=${i||"<empty>"}`),null}return await t.json()}catch(t){return console.error("Send error:",t),null}}_sendCommand(e){return!this._dc||this._dc.readyState!=="open"?!1:(this._dc.send(JSON.stringify(e)),!0)}_checkSessionReady(){this._iceConnected&&this._dcOpen&&this._sessionResolve&&(this._state="streaming",this.requestState(),this._stateRefreshInterval=setInterval(()=>this.requestState(),500),this._emit("streaming",{sessionId:this._sessionId,robotId:this._selectedRobotId}),this._sessionResolve(),this._sessionResolve=null,this._sessionReject=null)}async _handleSignalingMessage(e){switch(e.type){case"welcome":break;case"list":this._robots=e.producers||[],this._emit("robotsChanged",{robots:this._robots}),this._maybeAutoStart();break;case"peerStatusChanged":{const t=await this._sendToServer({type:"list"});t!=null&&t.producers&&(this._robots=t.producers,this._emit("robotsChanged",{robots:this._robots}),this._maybeAutoStart());break}case"sessionStarted":this._sessionId=e.sessionId??null;break;case"sessionRejected":this._failSessionRejected(e);break;case"endSession":this._handleEndSession(e);break;case"peer":this._handlePeerMessage(e);break}}_handleEndSession(e){const t=e.reason,i=t==="robot_busy_local_app"?"Robot is busy: a local Python app is running":t==="local_app_started"?"Disconnected: a local Python app started on the robot":t==="robot_busy_local"?"Robot is busy: another session is already active":null;if(this._sessionReject){const s=new Error(i||`Session ended before it could start: ${t||"unknown reason"}`);s.reason=t??null,this._emit("sessionRejected",{reason:t,activeApp:null}),this._clearIceGrace(),this._uninstallNetworkListeners(),this._pc&&(this._pc.close(),this._pc=null),this._micStream&&(this._micStream.getTracks().forEach(r=>r.stop()),this._micStream=null),this._iceConnected=!1,this._dcOpen=!1,this._micMuted=!0,this._micSupported=!1;const n=this._sessionReject;this._sessionResolve=null,this._sessionReject=null,n(s);return}this._state==="streaming"&&(this._emit("sessionStopped",{reason:t||"remote_end",message:i}),this.stopSession().catch(()=>{}))}async _handlePeerMessage(e){if(this._pc)try{if(e.sdp){const t=e.sdp;if(t.type==="offer"){const s=se(t.sdp);if(this._micSupported=s,this._emit("micSupported",{supported:s}),s&&this._micStream)for(const r of this._micStream.getAudioTracks())this._pc.addTrack(r,this._micStream);await this._pc.setRemoteDescription(new RTCSessionDescription(t));const n=await this._pc.createAnswer();await this._pc.setLocalDescription(n),await this._sendToServer({type:"peer",sessionId:this._sessionId,sdp:{type:"answer",sdp:n.sdp}})}else await this._pc.setRemoteDescription(new RTCSessionDescription(t));const i=this._pendingRemoteIce;if(i&&i.length){this._pendingRemoteIce=[];for(const s of i)try{await this._pc.addIceCandidate(new RTCIceCandidate(s))}catch(n){console.warn("[reachy-mini] buffered ICE candidate rejected:",n)}}}if(e.ice){if(!e.ice.candidate)return;this._pc.remoteDescription?await this._pc.addIceCandidate(new RTCIceCandidate(e.ice)):(this._pendingRemoteIce||(this._pendingRemoteIce=[]),this._pendingRemoteIce.push(e.ice))}}catch(t){console.error("WebRTC error:",t),this._emit("error",{source:"webrtc",error:t})}}_handleRobotMessage(e){if("version"in e&&this._versionResolve){this._versionResolve(e.version),this._versionResolve=null;return}if("hardware_id"in e&&this._hardwareIdResolve){this._hardwareIdResolve(e.hardware_id),this._hardwareIdResolve=null;return}if(e.command==="get_volume"||e.command==="set_volume"){this._volumeResolve&&(this._volumeResolve(e.status==="error"?null:e.volume),this._volumeResolve=null);return}if(e.command==="get_microphone_volume"||e.command==="set_microphone_volume"){this._micVolumeResolve&&(this._micVolumeResolve(e.status==="error"?null:e.volume),this._micVolumeResolve=null);return}if(e.command==="apply_audio_config"){this._applyAudioConfigResolve&&(this._applyAudioConfigResolve(e.error?!1:!!e.applied),this._applyAudioConfigResolve=null);return}if(e.command==="read_audio_parameter"){this._readAudioParameterResolve&&(this._readAudioParameterResolve(e.error?null:e.values??null),this._readAudioParameterResolve=null);return}if((e.command==="wake_up"||e.command==="goto_sleep")&&this._pendingMotionCompletions&&this._pendingMotionCompletions[e.command]){const t=this._pendingMotionCompletions[e.command];if(e.completed===!0&&t.length>0){const i=t.shift();clearTimeout(i.timer),i.resolve();return}if(e.error&&t.length>0){const i=t.shift();clearTimeout(i.timer),i.reject(new Error(`${e.command}: ${e.error}`));return}}if(e.type==="log_line"){for(const t of this._logSubscribers)try{t.onLine({timestamp:e.timestamp,line:e.line})}catch(i){console.error("subscribeLogs onLine threw:",i)}return}if(e.type==="log_stream_error"){for(const t of this._logSubscribers)if(typeof t.onError=="function")try{t.onError(e.error)}catch(i){console.error("subscribeLogs onError threw:",i)}return}if(e.state){const t=e.state;t.head_pose&&(this._robotState.head=t.head_pose.flat()),t.antennas&&(this._robotState.antennas=[t.antennas[0],t.antennas[1]]),typeof t.body_yaw=="number"&&(this._robotState.body_yaw=t.body_yaw),t.motor_mode&&(this._robotState.motor_mode=t.motor_mode),typeof t.is_move_running=="boolean"&&(this._robotState.is_move_running=t.is_move_running),this._emit("state",{...this._robotState})}if(e.error&&this._emit("error",{source:"robot",error:e.error}),this._broadcastWaiters.length>0)for(let t=this._broadcastWaiters.length-1;t>=0;t--){const i=this._broadcastWaiters[t];if(i.predicate(e)){this._broadcastWaiters.splice(t,1),clearTimeout(i.timer),i.resolve(e);return}}}_startLatencyMonitor(e){this._latencyMonitorId&&clearInterval(this._latencyMonitorId),this._latencyMonitorId=setInterval(()=>{if(!e.srcObject||e.paused)return;const t=e.buffered;if(t.length>0){const i=t.end(t.length-1),s=i-e.currentTime;s>.5&&(console.log(`Latency correction: was ${s.toFixed(2)}s behind`),e.currentTime=i-.1)}},2e3)}}window.ReachyMini=le;window.dispatchEvent(new Event("reachymini:ready"));const M=new URLSearchParams(window.location.search),ce=M.get("embedded")==="1"||M.get("embed")==="1",de=M.has("code")||M.has("error"),he=sessionStorage.getItem("cookaiware:launched")==="1";function F(){sessionStorage.setItem("cookaiware:launched","1");const o=document.getElementById("root");return o&&(o.innerHTML=""),U(async()=>{const{mountHost:a}=await import("./auto-CaTD3xKq.js");return{mountHost:a}},__vite__mapDeps([0,1])).then(({mountHost:a})=>{a({appName:"CookAIware",appIconUrl:"/icon.png",appEmoji:"🍲",enableMicrophone:!0,devToken:void 0,clientId:void 0})})}ce?U(()=>import("./embed-VZIqWv3F.js"),__vite__mapDeps([2,1,3])):de||he?F():U(async()=>{const{renderLanding:o}=await import("./landing-B7BE48AY.js");return{renderLanding:o}},__vite__mapDeps([4,5])).then(({renderLanding:o})=>o(()=>void F()));export{U as _,_e as r};
 
1
+ const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/auto-CaTD3xKq.js","assets/sub-paths-F6TP7MGR-8fYyfeHC.js","assets/embed-ByAC4o7k.js","assets/embed-BmrOAEFs.css","assets/landing-B7BE48AY.js","assets/landing-BJE7fNCb.css"])))=>i.map(i=>d[i]);
2
  var G=Object.defineProperty;var q=(o,a,e)=>a in o?G(o,a,{enumerable:!0,configurable:!0,writable:!0,value:e}):o[a]=e;var d=(o,a,e)=>q(o,typeof a!="symbol"?a+"":a,e);(function(){const a=document.createElement("link").relList;if(a&&a.supports&&a.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))t(i);new MutationObserver(i=>{for(const s of i)if(s.type==="childList")for(const n of s.addedNodes)n.tagName==="LINK"&&n.rel==="modulepreload"&&t(n)}).observe(document,{childList:!0,subtree:!0});function e(i){const s={};return i.integrity&&(s.integrity=i.integrity),i.referrerPolicy&&(s.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?s.credentials="include":i.crossOrigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function t(i){if(i.ep)return;i.ep=!0;const s=e(i);fetch(i.href,s)}})();const W="modulepreload",z=function(o){return"/"+o},O={},U=function(a,e,t){let i=Promise.resolve();if(e&&e.length>0){document.getElementsByTagName("link");const n=document.querySelector("meta[property=csp-nonce]"),r=(n==null?void 0:n.nonce)||(n==null?void 0:n.getAttribute("nonce"));i=Promise.allSettled(e.map(c=>{if(c=z(c),c in O)return;O[c]=!0;const l=c.endsWith(".css"),h=l?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${c}"]${h}`))return;const u=document.createElement("link");if(u.rel=l?"stylesheet":W,l||(u.as="script"),u.crossOrigin="",u.href=c,r&&u.setAttribute("nonce",r),document.head.appendChild(u),l)return new Promise((p,g)=>{u.addEventListener("load",p),u.addEventListener("error",()=>g(new Error(`Unable to preload CSS for ${c}`)))})}))}function s(n){const r=new Event("vite:preloadError",{cancelable:!0});if(r.payload=n,window.dispatchEvent(r),!r.defaultPrevented)throw n}return i.then(n=>{for(const r of n||[])r.status==="rejected"&&s(r.reason);return a().catch(s)})};var J=Object.defineProperty,K=(o,a,e)=>a in o?J(o,a,{enumerable:!0,configurable:!0,writable:!0,value:e}):o[a]=e,k=(o,a,e)=>(K(o,typeof a!="symbol"?a+"":a,e),e),B="https://huggingface.co";async function T(o,a){var e,t;const i=new X(o.url,o.status,(e=o.headers.get("X-Request-Id"))!=null?e:void 0);i.message=`Api error with status ${i.statusCode}`;const s=[`URL: ${i.url}`,i.requestId?`Request ID: ${i.requestId}`:void 0].filter(Boolean).join(". ");if((t=o.headers.get("Content-Type"))!=null&&t.startsWith("application/json")){const n=await o.json();i.message=n.error||n.message||i.message,i.data=n}else i.data={message:await o.text()};throw i.message+=`. ${s}`,i}var X=class extends Error{constructor(o,a,e,t){super(t),k(this,"statusCode"),k(this,"url"),k(this,"requestId"),k(this,"data"),this.statusCode=a,this.requestId=e,this.url=o}};new Promise(o=>{});function Y(o){if(globalThis.Buffer)return globalThis.Buffer.from(o).toString("base64");{const a=[];return o.forEach(e=>{a.push(String.fromCharCode(e))}),globalThis.btoa(a.join(""))}}async function $(o){var a,e;if(typeof window>"u")throw new Error("oauthHandleRedirect is only available in the browser");const t=new URLSearchParams(window.location.search),[i,s]=[t.get("error"),t.get("error_description")];if(i)throw new Error(`${i}: ${s}`);const n=t.get("code"),r=localStorage.getItem("huggingface.co:oauth:nonce");if(!n)throw new Error("Missing oauth code from query parameters in redirected URL");if(!r)throw new Error("Missing oauth nonce from localStorage");const c=localStorage.getItem("huggingface.co:oauth:code_verifier");if(!c)throw new Error("Missing oauth code_verifier from localStorage");const l=t.get("state");if(!l)throw new Error("Missing oauth state from query parameters in redirected URL");let h;try{h=JSON.parse(l)}catch{throw new Error("Invalid oauth state in redirected URL, unable to parse JSON: "+l)}if(h.nonce!==r)throw new Error("Invalid oauth state in redirected URL");const u=B,p=`${new URL(u).origin}/.well-known/openid-configuration`,g=await fetch(p,{headers:{Accept:"application/json"}});if(!g.ok)throw await T(g);const w=await g.json(),m=await fetch(w.token_endpoint,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({grant_type:"authorization_code",code:n,redirect_uri:h.redirectUri,code_verifier:c}).toString()});if(localStorage.removeItem("huggingface.co:oauth:code_verifier"),localStorage.removeItem("huggingface.co:oauth:nonce"),!m.ok)throw await T(m);const f=await m.json(),y=new Date(Date.now()+f.expires_in*1e3),v=await fetch(w.userinfo_endpoint,{headers:{Authorization:`Bearer ${f.access_token}`}});if(!v.ok)throw await T(v);const b=await v.json();return{accessToken:f.access_token,accessTokenExpiresAt:y,userInfo:{id:b.sub,name:b.name,fullname:b.preferred_username,email:b.email,emailVerified:b.email_verified,avatarUrl:b.picture,websiteUrl:b.website,isPro:b.isPro,orgs:(e=(a=b.orgs)==null?void 0:a.map(R=>({id:R.sub,name:R.name,fullname:R.name,isEnterprise:R.isEnterprise,canPay:R.canPay,avatarUrl:R.picture,roleInOrg:R.roleInOrg})))!=null?e:[]},state:h.state,scope:f.scope}}async function Q(o){if(typeof window>"u")throw new Error("oauthHandleRedirect is only available in the browser");const a=new URLSearchParams(window.location.search);return a.has("error")?$():a.has("code")?localStorage.getItem("huggingface.co:oauth:nonce")?$():(console.warn("Missing oauth nonce from localStorage. This can happen when the user refreshes the page after logging in, without changing the URL."),!1):!1}async function Z(o){var a,e;if(typeof window>"u")throw new Error("oauthLogin is only available in the browser");const t=(o==null?void 0:o.hubUrl)||B,i=`${new URL(t).origin}/.well-known/openid-configuration`,s=await fetch(i,{headers:{Accept:"application/json"}});if(!s.ok)throw await T(s);const n=await s.json(),r=globalThis.crypto.randomUUID(),c=globalThis.crypto.randomUUID()+globalThis.crypto.randomUUID();localStorage.setItem("huggingface.co:oauth:nonce",r),localStorage.setItem("huggingface.co:oauth:code_verifier",c);const l=(o==null?void 0:o.redirectUrl)||window.location.href,h=JSON.stringify({nonce:r,redirectUri:l,state:o==null?void 0:o.state}),u=(e=(a=window==null?void 0:window.huggingface)==null?void 0:a.variables)!=null?e:null,p=(o==null?void 0:o.clientId)||(u==null?void 0:u.OAUTH_CLIENT_ID);if(!p)throw u?new Error("Missing clientId, please add hf_oauth: true to the README.md's metadata in your static Space"):new Error("Missing clientId");const g=Y(new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256",new TextEncoder().encode(c)))).replace(/[+]/g,"-").replace(/[/]/g,"_").replace(/=/g,"");return`${n.authorization_endpoint}?${new URLSearchParams({client_id:p,scope:(o==null?void 0:o.scopes)||(u==null?void 0:u.OAUTH_SCOPES)||"openid profile",response_type:"code",redirect_uri:l,state:h,code_challenge:g,code_challenge_method:"S256"}).toString()}`}function C(o){return o*Math.PI/180}function _e(o){return o*180/Math.PI}function ee(o,a,e){const t=C(o),i=C(a),s=C(e),n=Math.cos(s),r=Math.sin(s),c=Math.cos(i),l=Math.sin(i),h=Math.cos(t),u=Math.sin(t);return[[n*c,n*l*u-r*h,n*l*h+r*u,0],[r*c,r*l*u+n*h,r*l*h-n*u,0],[-l,c*u,c*h,0],[0,0,0,1]]}function te(){if(typeof window>"u"||!window.location.hash)return;const o=window.location.hash.startsWith("#")?window.location.hash.slice(1):window.location.hash;let a;try{a=new URLSearchParams(o)}catch{return}const e=a.get("hf_token");if(!e)return;const t=a.get("hf_username")||"user",i=a.get("hf_token_expires"),s=i&&!Number.isNaN(new Date(i).getTime())?i:new Date(Date.now()+365*24*60*60*1e3).toISOString();try{sessionStorage.setItem("hf_token",e),sessionStorage.setItem("hf_username",t),sessionStorage.setItem("hf_token_expires",s)}catch(c){console.warn("[reachy-mini] could not persist pre-seeded HF credentials:",c)}a.delete("hf_token"),a.delete("hf_username"),a.delete("hf_token_expires");const n=a.toString(),r=window.location.pathname+window.location.search+(n?"#"+n:"");try{window.history.replaceState(null,"",r)}catch{}}function ie(){if(typeof window>"u")return null;if(window.location.hash){const o=window.location.hash.startsWith("#")?window.location.hash.slice(1):window.location.hash;try{const e=new URLSearchParams(o).get("robot_peer_id");if(e)return e}catch{}}if(window.location.search)try{const a=new URLSearchParams(window.location.search).get("robot_peer_id");if(a)return a}catch{}return null}function se(o){const a=o.split(`\r
3
  `);let e=!1;for(const t of a)if(t.startsWith("m=audio")?e=!0:t.startsWith("m=")&&(e=!1),e&&t==="a=sendrecv")return!0;return!1}const S=12*1024,L=1*1024*1024,ne=512*1024;function oe(){return typeof CompressionStream<"u"}function x(){return"u"+Math.random().toString(36).slice(2,11)+Date.now().toString(36)}function P(o){let a="";for(let t=0;t<o.length;t+=32768)a+=String.fromCharCode.apply(null,Array.from(o.subarray(t,t+32768)));return btoa(a)}async function re(o){const a=new TextEncoder().encode(o),e=await new Response(new Blob([a]).stream().pipeThrough(new CompressionStream("gzip"))).arrayBuffer();return P(new Uint8Array(e))}function N(o){const a=Math.round(Number(o)||0);return Math.max(0,Math.min(100,a))}const H=3e3,V=1e3,ae=6e4;class le extends EventTarget{constructor(e={}){super();d(this,"_signalingUrl");d(this,"_clientId");d(this,"_appName");d(this,"_videoJitterBufferTargetMs");d(this,"_autoStartFromUrl");d(this,"_autoStartAttempted");d(this,"_state","disconnected");d(this,"_robots",[]);d(this,"_robotState",{});d(this,"_preselectedRobotId");d(this,"_token",null);d(this,"_username",null);d(this,"_tokenExpires",null);d(this,"_sseAbortController",null);d(this,"_pc",null);d(this,"_dc",null);d(this,"_sessionId",null);d(this,"_selectedRobotId",null);d(this,"_pendingRemoteIce",[]);d(this,"_micStream",null);d(this,"_micMuted",!0);d(this,"_audioMuted",!0);d(this,"_micSupported",!1);d(this,"_latencyMonitorId",null);d(this,"_stateRefreshInterval",null);d(this,"_versionResolve",null);d(this,"_hardwareIdResolve",null);d(this,"_volumeResolve",null);d(this,"_micVolumeResolve",null);d(this,"_applyAudioConfigResolve",null);d(this,"_readAudioParameterResolve",null);d(this,"_logSubscribers",new Set);d(this,"_broadcastWaiters",[]);d(this,"_activeMoveUploadId",null);d(this,"_activeAudioUploadId",null);d(this,"_sessionResolve",null);d(this,"_sessionReject",null);d(this,"_iceConnected",!1);d(this,"_dcOpen",!1);d(this,"_iceGraceTimer",null);d(this,"_iceGraceReason",null);d(this,"_pendingVisibilityHandler",null);d(this,"_onlineHandler",null);d(this,"_offlineHandler",null);d(this,"_connectionChangeHandler",null);d(this,"_pendingMotionCompletions",{wake_up:[],goto_sleep:[]});d(this,"_videoElement",null);this._signalingUrl=e.signalingUrl||"https://pollen-robotics-reachy-mini-central.hf.space",this._clientId=e.clientId||null,this._appName=e.appName||"unknown",this._videoJitterBufferTargetMs=e.videoJitterBufferTargetMs??0,this._autoStartFromUrl=e.autoStartFromUrl===!0,this._autoStartAttempted=!1,this._preselectedRobotId=ie()}get state(){return this._state}get robots(){return this._robots}get robotState(){return this._robotState}get username(){return this._username}get isAuthenticated(){return!!this._token}get micSupported(){return this._micSupported}get micMuted(){return this._micMuted}get audioMuted(){return this._audioMuted}get preselectedRobotId(){return this._preselectedRobotId}get isEmbedded(){return this._preselectedRobotId!==null}_maybeAutoStart(){if(!this._autoStartFromUrl||this._autoStartAttempted||!this._preselectedRobotId||this._state!=="connected"||!this._robots.find(i=>i.id===this._preselectedRobotId))return;this._autoStartAttempted=!0;const t=this._preselectedRobotId;setTimeout(()=>{this._state==="connected"&&this.startSession(t).catch(i=>{console.warn("[reachy-mini] autoStartFromUrl: startSession rejected:",i)})},0)}async authenticate(){try{te();const e=await Q();if(e)return this._username=e.userInfo.preferred_username||e.userInfo.name||null,this._token=e.accessToken,this._tokenExpires=e.accessTokenExpiresAt,sessionStorage.setItem("hf_token",this._token),sessionStorage.setItem("hf_username",this._username??""),sessionStorage.setItem("hf_token_expires",typeof this._tokenExpires=="string"?this._tokenExpires:this._tokenExpires.toISOString()),!0;const t=sessionStorage.getItem("hf_token"),i=sessionStorage.getItem("hf_username"),s=sessionStorage.getItem("hf_token_expires");return t&&i&&s&&new Date(s)>new Date?(this._token=t,this._username=i,this._tokenExpires=s,!0):!1}catch(e){return console.error("Auth error:",e),!1}}async login(){const e={};this._clientId&&(e.clientId=this._clientId),window.location.href=await Z(e)}logout(){sessionStorage.removeItem("hf_token"),sessionStorage.removeItem("hf_username"),sessionStorage.removeItem("hf_token_expires"),this._username=null,this._tokenExpires=null,this.disconnect()}async connect(e){if(this._state!=="disconnected")throw new Error("Already connected");if(e&&(this._token=e),!this._token)throw new Error("No token β€” call authenticate() first or pass a token");this._sseAbortController=new AbortController;let t;try{t=await fetch(`${this._signalingUrl}/events`,{signal:this._sseAbortController.signal,headers:{Authorization:`Bearer ${this._token}`}})}catch(i){throw this._sseAbortController=null,i}if(!t.ok)throw this._sseAbortController=null,new Error(`HTTP ${t.status}`);return new Promise((i,s)=>{let n=!1;const r=t.body.getReader(),c=new TextDecoder;let l="";(async()=>{try{for(;;){const{done:u,value:p}=await r.read();if(u)break;l+=c.decode(p,{stream:!0});const g=l.split(`
4
+ `);l=g.pop()??"";for(const w of g)if(w.startsWith("data:"))try{const m=JSON.parse(w.slice(5).trim());!n&&m.type==="welcome"&&m.peerId&&(n=!0,this._state="connected",await this._sendToServer({type:"setPeerStatus",roles:["listener"],meta:{name:this._appName}}),this._emit("connected",{peerId:m.peerId}),i()),this._handleSignalingMessage(m)}catch{}}}catch(u){if(u.name!=="AbortError"&&this._emit("error",{source:"signaling",error:u}),!n){s(u);return}}this._state!=="disconnected"&&(this._state="disconnected",this._emit("disconnected",{reason:"SSE closed"})),n||s(new Error("Connection closed before welcome"))})()})}async autoConnect(e={}){var l,h,u,p,g;const{token:t,pickRobot:i,autoPickIfSingle:s=!0,filterBusy:n=!0,wakeOnConnect:r=!0}=e;if(this._state==="streaming"){const w=(l=this._robots)==null?void 0:l.find(m=>m.id===this._selectedRobotId);return{robotId:this._selectedRobotId,robotName:((h=w==null?void 0:w.meta)==null?void 0:h.name)??null,isEmbedded:this.isEmbedded,alreadyStreaming:!0}}const c=this._autoStartFromUrl;this._autoStartFromUrl=!1;try{if(t)this._token=t;else if(!this._token&&!await this.authenticate())throw new Error("Not authenticated β€” call login() or pass a token");this._state==="disconnected"&&await this.connect();let w,m=null;if(this.isEmbedded){w=this._preselectedRobotId;try{await this._waitForRobotInList(w,5e3)}catch{}const f=(u=this._robots)==null?void 0:u.find(y=>y.id===w);m=((p=f==null?void 0:f.meta)==null?void 0:p.name)??null}else{const f=await this._fetchOwnedRobots({filterBusy:n});if(f.length===0)throw new Error("No reachable robots");if(s&&f.length===1&&!f[0].busy)w=f[0].id,m=f[0].name;else if(i){const y=await i(f);if(!y)throw new Error("Robot selection cancelled");w=y,m=((g=f.find(v=>v.id===y))==null?void 0:g.name)??null}else throw new Error("Multiple robots available β€” pass a pickRobot callback to autoConnect()")}if(await this.startSession(w),r&&typeof this.ensureAwake=="function")try{await this.ensureAwake()}catch(f){console.warn("[reachy-mini] autoConnect: ensureAwake failed:",f)}return{robotId:w,robotName:m,isEmbedded:this.isEmbedded}}finally{this._autoStartFromUrl=c}}async _fetchOwnedRobots({filterBusy:e=!0}={}){var t,i,s;try{const n=await fetch(`${this._signalingUrl}/api/robot-status`,{headers:{Authorization:`Bearer ${this._token}`}});if(!n.ok)throw new Error(`HTTP ${n.status}`);const r=await n.json(),c=new Map;for(const l of r.robots||[]){if(e&&l.busy)continue;const h=((t=l.meta)==null?void 0:t.install_id)??((i=l.meta)==null?void 0:i.hardware_id)??l.peerId;c.set(h,{id:l.peerId,name:l.robotName??((s=l.meta)==null?void 0:s.name)??null,busy:!!l.busy,activeApp:l.activeApp??null,meta:l.meta??{},lastSeenAgeSeconds:l.last_seen_age_seconds??null})}return Array.from(c.values()).sort((l,h)=>(l.lastSeenAgeSeconds??1/0)-(h.lastSeenAgeSeconds??1/0))}catch(n){return console.warn("[reachy-mini] /api/robot-status unavailable, using SSE list:",n),(this._robots||[]).map(r=>{var c;return{id:r.id,name:((c=r.meta)==null?void 0:c.name)??null,busy:!1,activeApp:null,meta:r.meta??{},lastSeenAgeSeconds:null}})}}_waitForRobotInList(e,t){var i;return(i=this._robots)!=null&&i.find(s=>s.id===e)?Promise.resolve():new Promise((s,n)=>{const r=()=>{var l;(l=this._robots)!=null&&l.find(h=>h.id===e)&&(this.removeEventListener("robotsChanged",r),clearTimeout(c),s())},c=setTimeout(()=>{this.removeEventListener("robotsChanged",r),n(new Error(`Timeout waiting for robot ${e} in list`))},t);this.addEventListener("robotsChanged",r)})}async startSession(e){if(this._state!=="connected")throw new Error("Not connected");this._selectedRobotId=e,this._iceConnected=!1,this._dcOpen=!1,this._micSupported=!1,this._pendingRemoteIce=[];try{const t=window,i=t.AudioContext??t.webkitAudioContext;if(!i)throw new Error("AudioContext not supported");const s=new i,n=s.createMediaStreamDestination(),r=s.createOscillator(),c=s.createGain();c.gain.value=0,r.connect(c).connect(n),r.start();const l=n.stream;l.getAudioTracks().forEach(h=>{h.enabled=!1}),this._micStream=l,this._micMuted=!0}catch(t){console.warn("Audio sender placeholder setup failed:",t),this._micStream=null}return this._pc=new RTCPeerConnection({iceServers:[{urls:"stun:stun.l.google.com:19302"}]}),this._installNetworkListeners(),new Promise((t,i)=>{this._sessionResolve=t,this._sessionReject=i,this._pc.ontrack=s=>{if(s.track.kind==="video"){const n=this._videoJitterBufferTargetMs;try{s.receiver.jitterBufferTarget=n}catch{}try{s.receiver.playoutDelayHint=n/1e3}catch{}this._emit("videoTrack",{track:s.track,stream:s.streams[0]})}},this._pc.onicecandidate=async s=>{s.candidate&&this._sessionId&&await this._sendToServer({type:"peer",sessionId:this._sessionId,ice:{candidate:s.candidate.candidate,sdpMLineIndex:s.candidate.sdpMLineIndex,sdpMid:s.candidate.sdpMid}})},this._pc.oniceconnectionstatechange=()=>{var n;const s=(n=this._pc)==null?void 0:n.iceConnectionState;if(s){if(this._emit("iceStateChange",{state:s}),s==="connected"||s==="completed"){this._clearIceGrace(),this._iceConnected=!0,this._checkSessionReady();return}if(s==="disconnected"){typeof document<"u"&&document.hidden?this._armIceGraceOnVisibility():this._scheduleIceGrace(H,"disconnected");return}if(s==="failed"){this._scheduleIceGrace(V,"failed");return}}},this._pc.ondatachannel=s=>{this._dc=s.channel,this._dc.onopen=()=>{this._dcOpen=!0,this._checkSessionReady()},this._dc.onmessage=n=>this._handleRobotMessage(JSON.parse(n.data))},this._sendToServer({type:"startSession",peerId:e}).then(s=>{if((s==null?void 0:s.type)==="sessionRejected"){this._failSessionRejected(s);return}s!=null&&s.sessionId&&(this._sessionId=s.sessionId)})})}_failSessionRejected(e){const t=new Error(e.reason==="robot_busy"?`Robot is busy: "${e.activeApp||"another app"}" is already connected`:`Session rejected: ${e.reason||"unknown reason"}`);if(t.reason=e.reason??null,t.activeApp=e.activeApp??null,this._clearIceGrace(),this._uninstallNetworkListeners(),this._pc&&(this._pc.close(),this._pc=null),this._micStream&&(this._micStream.getTracks().forEach(i=>i.stop()),this._micStream=null),this._iceConnected=!1,this._dcOpen=!1,this._micMuted=!0,this._micSupported=!1,this._emit("sessionRejected",{reason:e.reason,activeApp:e.activeApp}),this._sessionReject){const i=this._sessionReject;this._sessionResolve=null,this._sessionReject=null,i(t)}}async stopSession(){this._versionResolve&&(this._versionResolve(null),this._versionResolve=null),this._hardwareIdResolve&&(this._hardwareIdResolve(null),this._hardwareIdResolve=null),this._volumeResolve&&(this._volumeResolve(null),this._volumeResolve=null),this._micVolumeResolve&&(this._micVolumeResolve(null),this._micVolumeResolve=null),this._applyAudioConfigResolve&&(this._applyAudioConfigResolve(!1),this._applyAudioConfigResolve=null),this._readAudioParameterResolve&&(this._readAudioParameterResolve(null),this._readAudioParameterResolve=null),this._logSubscribers.clear(),this._rejectPendingMotionCompletions(new Error("Session stopped")),this._clearIceGrace(),this._uninstallNetworkListeners(),this._sessionReject&&(this._sessionReject(new Error("Session stopped")),this._sessionResolve=null,this._sessionReject=null),this._stateRefreshInterval&&(clearInterval(this._stateRefreshInterval),this._stateRefreshInterval=null),this._latencyMonitorId&&(clearInterval(this._latencyMonitorId),this._latencyMonitorId=null),this._sessionId&&await this._sendToServer({type:"endSession",sessionId:this._sessionId}),this._micStream&&(this._micStream.getTracks().forEach(t=>t.stop()),this._micStream=null),this._micMuted=!0,this._micSupported=!1,this._pc&&(this._pc.close(),this._pc=null),this._dc&&(this._dc.close(),this._dc=null),this._sessionId=null,this._iceConnected=!1,this._dcOpen=!1,this._state==="streaming"&&(this._state="connected",this._emit("sessionStopped",{reason:"user"}))}disconnect(){this._sseAbortController&&(this._sseAbortController.abort(),this._sseAbortController=null),this._versionResolve&&(this._versionResolve(null),this._versionResolve=null),this._hardwareIdResolve&&(this._hardwareIdResolve(null),this._hardwareIdResolve=null),this._volumeResolve&&(this._volumeResolve(null),this._volumeResolve=null),this._micVolumeResolve&&(this._micVolumeResolve(null),this._micVolumeResolve=null),this._applyAudioConfigResolve&&(this._applyAudioConfigResolve(!1),this._applyAudioConfigResolve=null),this._readAudioParameterResolve&&(this._readAudioParameterResolve(null),this._readAudioParameterResolve=null),this._logSubscribers.clear(),this._rejectPendingMotionCompletions(new Error("Disconnected")),this._clearIceGrace(),this._uninstallNetworkListeners(),this._sessionReject&&(this._sessionReject(new Error("Disconnected")),this._sessionResolve=null,this._sessionReject=null),this._stateRefreshInterval&&(clearInterval(this._stateRefreshInterval),this._stateRefreshInterval=null),this._latencyMonitorId&&(clearInterval(this._latencyMonitorId),this._latencyMonitorId=null),this._sessionId&&this._token&&this._sendToServer({type:"endSession",sessionId:this._sessionId}),this._micStream&&(this._micStream.getTracks().forEach(e=>e.stop()),this._micStream=null),this._pc&&(this._pc.close(),this._pc=null),this._dc&&(this._dc.close(),this._dc=null),this._sessionId=null,this._micMuted=!0,this._micSupported=!1,this._iceConnected=!1,this._dcOpen=!1,this._robots=[],this._state="disconnected",this._emit("disconnected",{reason:"user"})}_clearIceGrace(){this._iceGraceTimer!==null&&(clearTimeout(this._iceGraceTimer),this._iceGraceTimer=null),this._iceGraceReason=null,this._pendingVisibilityHandler&&typeof document<"u"&&document.removeEventListener("visibilitychange",this._pendingVisibilityHandler),this._pendingVisibilityHandler=null}_scheduleIceGrace(e,t){if(this._iceGraceTimer!==null){if(this._iceGraceReason===t)return;clearTimeout(this._iceGraceTimer)}this._iceGraceReason=t,this._iceGraceTimer=setTimeout(()=>{var n;this._iceGraceTimer=null;const i=this._iceGraceReason;this._iceGraceReason=null;const s=(n=this._pc)==null?void 0:n.iceConnectionState;if(!(s==="connected"||s==="completed")){if(i==="disconnected"&&s==="disconnected"){this._emit("error",{source:"webrtc",error:new Error(`ICE stuck in 'disconnected' for > ${e}ms`)});return}if(i==="failed"||s==="failed"){const r=new Error("ICE connection failed");this._sessionReject&&(this._sessionReject(r),this._sessionResolve=null,this._sessionReject=null),this._emit("error",{source:"webrtc",error:r})}}},e)}_armIceGraceOnVisibility(){if(this._pendingVisibilityHandler)return;const e=Date.now(),t=()=>{if(typeof document<"u"&&document.hidden||(document.removeEventListener("visibilitychange",t),this._pendingVisibilityHandler=null,!this._pc))return;const i=this._pc.iceConnectionState;if(!(i==="connected"||i==="completed")){if(Date.now()-e>ae){const s=new Error("Session expired while tab was backgrounded");this._sessionReject&&(this._sessionReject(s),this._sessionResolve=null,this._sessionReject=null),this._emit("error",{source:"webrtc",error:s});return}if(i==="failed"){this._scheduleIceGrace(V,"failed");return}this._scheduleIceGrace(H,"disconnected")}};document.addEventListener("visibilitychange",t),this._pendingVisibilityHandler=t}_installNetworkListeners(){if(this._onlineHandler||typeof window>"u")return;const e=()=>this._emit("networkOnline",{}),t=()=>this._emit("networkOffline",{});window.addEventListener("online",e),window.addEventListener("offline",t),this._onlineHandler=e,this._offlineHandler=t;const i=navigator.connection;if(i&&typeof i.addEventListener=="function"){const s=()=>this._emit("networkChange",{effectiveType:i.effectiveType,downlink:i.downlink,rtt:i.rtt,saveData:i.saveData});i.addEventListener("change",s),this._connectionChangeHandler=s}}_uninstallNetworkListeners(){typeof window<"u"&&(this._onlineHandler&&window.removeEventListener("online",this._onlineHandler),this._offlineHandler&&window.removeEventListener("offline",this._offlineHandler));const e=navigator.connection;e&&this._connectionChangeHandler&&typeof e.removeEventListener=="function"&&e.removeEventListener("change",this._connectionChangeHandler),this._onlineHandler=null,this._offlineHandler=null,this._connectionChangeHandler=null}setTarget({head:e,antennas:t,body_yaw:i}={}){const s={type:"set_full_target"};if(e!==void 0){if(!Array.isArray(e)||e.length!==16||!e.every(n=>Number.isFinite(n)))throw new TypeError(`setTarget: head must be a 16-element flat row-major 4Γ—4 matrix of finite numbers; got ${Array.isArray(e)?`Array(${e.length})`:typeof e}`);s.head=e}if(t!==void 0){if(!Array.isArray(t)||t.length!==2||!t.every(n=>Number.isFinite(n)))throw new TypeError(`setTarget: antennas must be [rightRad, leftRad] (2 finite numbers); got ${Array.isArray(t)?`Array(${t.length})`:typeof t}`);s.antennas=t}if(i!==void 0){if(!Number.isFinite(i))throw new TypeError(`setTarget: body_yaw must be a finite number (radians); got ${i}`);s.body_yaw=i}return this._sendCommand(s)}gotoTarget({head:e,antennas:t,body_yaw:i,duration:s}){const n={type:"goto_target"};if(e!==void 0){if(!Array.isArray(e)||e.length!==16||!e.every(r=>Number.isFinite(r)))throw new TypeError(`gotoTarget: head must be a 16-element flat row-major 4Γ—4 matrix of finite numbers; got ${Array.isArray(e)?`Array(${e.length})`:typeof e}`);n.head=e}if(t!==void 0){if(!Array.isArray(t)||t.length!==2||!t.every(r=>Number.isFinite(r)))throw new TypeError(`gotoTarget: antennas must be [rightRad, leftRad] (2 finite numbers); got ${Array.isArray(t)?`Array(${t.length})`:typeof t}`);n.antennas=t}if(i!==void 0){if(!Number.isFinite(i))throw new TypeError(`gotoTarget: body_yaw must be a finite number (radians); got ${i}`);n.body_yaw=i}if(!Number.isFinite(s)||s<=0)throw new TypeError(`gotoTarget: duration must be a positive finite number (seconds); got ${s}`);return n.duration=s,this._sendCommand(n)}setHeadRpyDeg(e,t,i){return this.setTarget({head:ee(e,t,i).flat()})}setAntennasDeg(e,t){return this.setTarget({antennas:[C(e),C(t)]})}setBodyYawDeg(e){return this.setTarget({body_yaw:C(e)})}playSound(e){return this._sendCommand({type:"play_sound",file:e})}clearIncomingAudio(){return this._sendCommand({type:"clear_incoming_audio"})}setMotorMode(e){return this._sendCommand({type:"set_motor_mode",mode:e})}setMotorTorque(e,t=null){return this._sendCommand({type:"set_torque",on:e,ids:t})}wakeUp({timeoutMs:e=8e3}={}){return this._sendCommand({type:"set_motor_mode",mode:"enabled"}),this._sendCommandAwaitCompletion("wake_up",e)}gotoSleep({timeoutMs:e=8e3}={}){return this._sendCommandAwaitCompletion("goto_sleep",e)}_sendCommandAwaitCompletion(e,t){return this._sendCommand({type:e})?new Promise((i,s)=>{const n={resolve:i,reject:s,timer:setTimeout(()=>{const r=this._pendingMotionCompletions[e],c=r.indexOf(n);c!==-1&&r.splice(c,1),s(new Error(`${e} timed out after ${t}ms`))},t)};this._pendingMotionCompletions[e].push(n)}):Promise.reject(new Error(`${e}: data channel not open`))}_rejectPendingMotionCompletions(e){for(const t of Object.keys(this._pendingMotionCompletions)){const i=this._pendingMotionCompletions[t];for(;i.length;){const s=i.shift();clearTimeout(s.timer),s.reject(e)}}}isAwake(){var t;const e=(t=this._robotState)==null?void 0:t.motor_mode;return e==="enabled"||e==="gravity_compensation"}async ensureAwake(e=1e3){var t;return((t=this._robotState)==null?void 0:t.motor_mode)===void 0&&await new Promise(i=>{const s=()=>{this.removeEventListener("state",s),clearTimeout(n),i()},n=setTimeout(s,e);this.addEventListener("state",s),this.requestState()}),this.isAwake()||this.wakeUp().catch(()=>{}),!0}getVersion(){return new Promise((e,t)=>{if(!this._dc||this._dc.readyState!=="open"){t(new Error("Data channel not open"));return}this._versionResolve&&this._versionResolve(null),this._versionResolve=e,this._sendCommand({type:"get_version"})})}getHardwareId(){return new Promise((e,t)=>{if(!this._dc||this._dc.readyState!=="open"){t(new Error("Data channel not open"));return}this._hardwareIdResolve&&this._hardwareIdResolve(null),this._hardwareIdResolve=e,this._sendCommand({type:"get_hardware_id"})})}getVolume(){return this._slotRoundtrip(()=>this._volumeResolve,e=>{this._volumeResolve=e},{type:"get_volume"})}setVolume(e){return this._slotRoundtrip(()=>this._volumeResolve,t=>{this._volumeResolve=t},{type:"set_volume",volume:N(e)})}getMicrophoneVolume(){return this._slotRoundtrip(()=>this._micVolumeResolve,e=>{this._micVolumeResolve=e},{type:"get_microphone_volume"})}setMicrophoneVolume(e){return this._slotRoundtrip(()=>this._micVolumeResolve,t=>{this._micVolumeResolve=t},{type:"set_microphone_volume",volume:N(e)})}applyAudioConfig(e,{verify:t=!0}={}){return this._slotRoundtrip(()=>this._applyAudioConfigResolve,i=>{this._applyAudioConfigResolve=i},{type:"apply_audio_config",config:e,verify:t}).then(i=>i===!0)}readAudioParameter(e){return this._slotRoundtrip(()=>this._readAudioParameterResolve,t=>{this._readAudioParameterResolve=t},{type:"read_audio_parameter",name:e})}_slotRoundtrip(e,t,i){return new Promise((s,n)=>{if(!this._dc||this._dc.readyState!=="open"){n(new Error("Data channel not open"));return}const r=e();r&&r(null),t(s),this._sendCommand(i)})}sendRaw(e){return this._sendCommand(e)}subscribeLogs({onLine:e,onError:t}){if(typeof e!="function")throw new TypeError("subscribeLogs: onLine callback is required");const i={onLine:e,onError:t},s=this._logSubscribers.size===0;this._logSubscribers.add(i),s&&this._sendCommand({type:"subscribe_logs"});let n=!1;return()=>{n||(n=!0,this._logSubscribers.delete(i),this._logSubscribers.size===0&&this._sendCommand({type:"unsubscribe_logs"}))}}requestState(){return this._sendCommand({type:"get_state"})}setAudioMuted(e){this._audioMuted=e,this._videoElement&&(this._videoElement.muted=e)}setMicMuted(e){this._micMuted=e,this._micStream&&this._micStream.getAudioTracks().forEach(t=>{t.enabled=!e})}attachVideo(e){this._videoElement=e,e.muted=this._audioMuted;const t=s=>{const n=s;e.srcObject=n.detail.stream,e.playsInline=!0,"requestVideoFrameCallback"in e&&this._startLatencyMonitor(e)},i=()=>{e.srcObject=null};return this.addEventListener("videoTrack",t),this.addEventListener("sessionStopped",i),()=>{this.removeEventListener("videoTrack",t),this.removeEventListener("sessionStopped",i),this._latencyMonitorId&&(clearInterval(this._latencyMonitorId),this._latencyMonitorId=null),e.srcObject=null,this._videoElement=null}}async playMove(e,{audioBlob:t=null,audioLeadMs:i=-100,description:s="move",encoding:n="gzip+base64",playFrequency:r=100,initialGotoDuration:c=0,startTimeoutMs:l=8e3,onProgress:h=()=>{},onStarted:u=()=>{}}={}){var R,j;if(!this._dc||this._dc.readyState!=="open")throw new Error("data channel not open");if(!((R=e==null?void 0:e.time)!=null&&R.length)||!((j=e==null?void 0:e.set_target_data)!=null&&j.length))throw new Error("playMove: motion must have time + set_target_data");const p=x();this._activeMoveUploadId=p;const g={description:s,time:e.time,set_target_data:e.set_target_data},w=JSON.stringify(g);let m,f;n==="gzip+base64"&&oe()?(m=await re(w),f="gzip+base64"):(m=w,f="json");const y=Math.ceil(m.length/S)||1;h({phase:"starting",sent:0,total:y,bytes:m.length,encoding:f}),this._sendCommand({type:"upload_move_start",upload_id:p,total_chunks:y,description:s,encoding:f});for(let _=0;_<y;_++){this._dc.bufferedAmount>L&&await this._awaitDataChannelDrain();const I=_*S;this._sendCommand({type:"upload_move_chunk",upload_id:p,chunk_index:_,chunk:m.slice(I,I+S)}),h({phase:"upload",sent:_+1,total:y})}if(this._sendCommand({type:"upload_move_finish",upload_id:p}),h({phase:"uploaded",sent:y,total:y}),t){const _=new Uint8Array(await t.arrayBuffer()),I=P(_),E=Math.ceil(I.length/S)||1;h({phase:"audio-starting",sent:0,total:E,bytes:I.length}),this._sendCommand({type:"upload_audio_start",upload_id:p,total_chunks:E,encoding:"wav-base64",description:s});for(let A=0;A<E;A++){this._dc.bufferedAmount>L&&await this._awaitDataChannelDrain();const D=A*S;this._sendCommand({type:"upload_audio_chunk",upload_id:p,chunk_index:A,chunk:I.slice(D,D+S)}),h({phase:"audio-upload",sent:A+1,total:E})}this._sendCommand({type:"upload_audio_finish",upload_id:p}),h({phase:"audio-uploaded",sent:E,total:E})}this._sendCommand({type:"play_uploaded_move",upload_id:p,play_frequency:r,initial_goto_duration:c,audio_lead_ms:i});let v;try{v=await this._waitForBroadcast(_=>(_==null?void 0:_.type)==="play_uploaded_move"&&(_==null?void 0:_.upload_id)===p&&(_.started===!0||typeof _.error=="string"),{timeoutMs:l,debugLabel:"play_uploaded_move started"})}catch(_){throw new Error(`Daemon did not respond to play_uploaded_move (requires the reachy_mini daemon with feature/daemon-side-move-upload). Underlying: ${_.message}`)}if(typeof v.error=="string")throw new Error(`play_uploaded_move: ${v.error}`);try{u({duration_s:v.duration_s,has_audio:v.has_audio===!0})}catch(_){console.warn("playMove.onStarted threw:",_)}h({phase:"playing",duration_s:v.duration_s});const b=await this._waitForBroadcast(_=>(_==null?void 0:_.type)==="play_uploaded_move"&&(_==null?void 0:_.upload_id)===p&&(_.finished===!0||_.cancelled===!0||typeof _.error=="string"),{timeoutMs:(v.duration_s+30)*1e3,debugLabel:"play_uploaded_move final"});return this._activeMoveUploadId===p&&(this._activeMoveUploadId=null),b}cancelMove(e=null){const t=e??this._activeMoveUploadId;return t?this._sendCommand({type:"cancel_move",upload_id:t}):!1}async uploadAudio(e,{description:t="audio",onProgress:i=()=>{}}={}){if(!this._dc||this._dc.readyState!=="open")throw new Error("data channel not open");if(!(e instanceof Blob))throw new TypeError("uploadAudio: expected a Blob");const s=x(),n=new Uint8Array(await e.arrayBuffer()),r=P(n),c=Math.ceil(r.length/S)||1;i({phase:"audio-starting",sent:0,total:c,bytes:r.length}),this._sendCommand({type:"upload_audio_start",upload_id:s,total_chunks:c,encoding:"wav-base64",description:t});for(let l=0;l<c;l++){this._dc.bufferedAmount>L&&await this._awaitDataChannelDrain();const h=l*S;this._sendCommand({type:"upload_audio_chunk",upload_id:s,chunk_index:l,chunk:r.slice(h,h+S)}),i({phase:"audio-upload",sent:l+1,total:c})}return this._sendCommand({type:"upload_audio_finish",upload_id:s}),i({phase:"audio-uploaded",sent:c,total:c}),s}async playUploadedAudio(e,{timeoutMs:t=8e3}={}){if(!this._dc||this._dc.readyState!=="open")throw new Error("data channel not open");const i=this._waitForBroadcast(n=>(n==null?void 0:n.type)==="play_uploaded_audio"&&(n==null?void 0:n.upload_id)===e&&(n.started===!0||typeof n.error=="string"),{timeoutMs:t,debugLabel:"play_uploaded_audio started"});this._sendCommand({type:"play_uploaded_audio",upload_id:e});const s=await i;if(typeof s.error=="string")throw new Error(s.error);return this._activeAudioUploadId=e,s}cancelAudio(e=null){const t=e??this._activeAudioUploadId;return t?(this._activeAudioUploadId===t&&(this._activeAudioUploadId=null),this._sendCommand({type:"cancel_audio",upload_id:t})):!1}_emit(e,t){this.dispatchEvent(new CustomEvent(e,{detail:t}))}_waitForBroadcast(e,{timeoutMs:t=5e3,debugLabel:i=""}={}){return new Promise((s,n)=>{const r={predicate:e,resolve:s,timer:setTimeout(()=>{const c=this._broadcastWaiters.indexOf(r);c!==-1&&this._broadcastWaiters.splice(c,1),n(new Error(`broadcast timeout (${t} ms): ${i}`))},t)};this._broadcastWaiters.push(r)})}async _awaitDataChannelDrain(){for(;this._dc&&this._dc.bufferedAmount>ne;)if(await new Promise(e=>setTimeout(e,30)),!this._dc||this._dc.readyState!=="open")throw new Error("data channel closed mid-upload")}async _sendToServer(e){if(!this._token)throw new Error("No token β€” authenticate() first");try{const t=await fetch(`${this._signalingUrl}/send`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this._token}`},body:JSON.stringify(e)});if(!t.ok){let i="";try{i=await t.text()}catch{}return console.warn(`[reachy-mini] /send rejected (${t.status}) for type=${e==null?void 0:e.type}; body=${i||"<empty>"}`),null}return await t.json()}catch(t){return console.error("Send error:",t),null}}_sendCommand(e){return!this._dc||this._dc.readyState!=="open"?!1:(this._dc.send(JSON.stringify(e)),!0)}_checkSessionReady(){this._iceConnected&&this._dcOpen&&this._sessionResolve&&(this._state="streaming",this.requestState(),this._stateRefreshInterval=setInterval(()=>this.requestState(),500),this._emit("streaming",{sessionId:this._sessionId,robotId:this._selectedRobotId}),this._sessionResolve(),this._sessionResolve=null,this._sessionReject=null)}async _handleSignalingMessage(e){switch(e.type){case"welcome":break;case"list":this._robots=e.producers||[],this._emit("robotsChanged",{robots:this._robots}),this._maybeAutoStart();break;case"peerStatusChanged":{const t=await this._sendToServer({type:"list"});t!=null&&t.producers&&(this._robots=t.producers,this._emit("robotsChanged",{robots:this._robots}),this._maybeAutoStart());break}case"sessionStarted":this._sessionId=e.sessionId??null;break;case"sessionRejected":this._failSessionRejected(e);break;case"endSession":this._handleEndSession(e);break;case"peer":this._handlePeerMessage(e);break}}_handleEndSession(e){const t=e.reason,i=t==="robot_busy_local_app"?"Robot is busy: a local Python app is running":t==="local_app_started"?"Disconnected: a local Python app started on the robot":t==="robot_busy_local"?"Robot is busy: another session is already active":null;if(this._sessionReject){const s=new Error(i||`Session ended before it could start: ${t||"unknown reason"}`);s.reason=t??null,this._emit("sessionRejected",{reason:t,activeApp:null}),this._clearIceGrace(),this._uninstallNetworkListeners(),this._pc&&(this._pc.close(),this._pc=null),this._micStream&&(this._micStream.getTracks().forEach(r=>r.stop()),this._micStream=null),this._iceConnected=!1,this._dcOpen=!1,this._micMuted=!0,this._micSupported=!1;const n=this._sessionReject;this._sessionResolve=null,this._sessionReject=null,n(s);return}this._state==="streaming"&&(this._emit("sessionStopped",{reason:t||"remote_end",message:i}),this.stopSession().catch(()=>{}))}async _handlePeerMessage(e){if(this._pc)try{if(e.sdp){const t=e.sdp;if(t.type==="offer"){const s=se(t.sdp);if(this._micSupported=s,this._emit("micSupported",{supported:s}),s&&this._micStream)for(const r of this._micStream.getAudioTracks())this._pc.addTrack(r,this._micStream);await this._pc.setRemoteDescription(new RTCSessionDescription(t));const n=await this._pc.createAnswer();await this._pc.setLocalDescription(n),await this._sendToServer({type:"peer",sessionId:this._sessionId,sdp:{type:"answer",sdp:n.sdp}})}else await this._pc.setRemoteDescription(new RTCSessionDescription(t));const i=this._pendingRemoteIce;if(i&&i.length){this._pendingRemoteIce=[];for(const s of i)try{await this._pc.addIceCandidate(new RTCIceCandidate(s))}catch(n){console.warn("[reachy-mini] buffered ICE candidate rejected:",n)}}}if(e.ice){if(!e.ice.candidate)return;this._pc.remoteDescription?await this._pc.addIceCandidate(new RTCIceCandidate(e.ice)):(this._pendingRemoteIce||(this._pendingRemoteIce=[]),this._pendingRemoteIce.push(e.ice))}}catch(t){console.error("WebRTC error:",t),this._emit("error",{source:"webrtc",error:t})}}_handleRobotMessage(e){if("version"in e&&this._versionResolve){this._versionResolve(e.version),this._versionResolve=null;return}if("hardware_id"in e&&this._hardwareIdResolve){this._hardwareIdResolve(e.hardware_id),this._hardwareIdResolve=null;return}if(e.command==="get_volume"||e.command==="set_volume"){this._volumeResolve&&(this._volumeResolve(e.status==="error"?null:e.volume),this._volumeResolve=null);return}if(e.command==="get_microphone_volume"||e.command==="set_microphone_volume"){this._micVolumeResolve&&(this._micVolumeResolve(e.status==="error"?null:e.volume),this._micVolumeResolve=null);return}if(e.command==="apply_audio_config"){this._applyAudioConfigResolve&&(this._applyAudioConfigResolve(e.error?!1:!!e.applied),this._applyAudioConfigResolve=null);return}if(e.command==="read_audio_parameter"){this._readAudioParameterResolve&&(this._readAudioParameterResolve(e.error?null:e.values??null),this._readAudioParameterResolve=null);return}if((e.command==="wake_up"||e.command==="goto_sleep")&&this._pendingMotionCompletions&&this._pendingMotionCompletions[e.command]){const t=this._pendingMotionCompletions[e.command];if(e.completed===!0&&t.length>0){const i=t.shift();clearTimeout(i.timer),i.resolve();return}if(e.error&&t.length>0){const i=t.shift();clearTimeout(i.timer),i.reject(new Error(`${e.command}: ${e.error}`));return}}if(e.type==="log_line"){for(const t of this._logSubscribers)try{t.onLine({timestamp:e.timestamp,line:e.line})}catch(i){console.error("subscribeLogs onLine threw:",i)}return}if(e.type==="log_stream_error"){for(const t of this._logSubscribers)if(typeof t.onError=="function")try{t.onError(e.error)}catch(i){console.error("subscribeLogs onError threw:",i)}return}if(e.state){const t=e.state;t.head_pose&&(this._robotState.head=t.head_pose.flat()),t.antennas&&(this._robotState.antennas=[t.antennas[0],t.antennas[1]]),typeof t.body_yaw=="number"&&(this._robotState.body_yaw=t.body_yaw),t.motor_mode&&(this._robotState.motor_mode=t.motor_mode),typeof t.is_move_running=="boolean"&&(this._robotState.is_move_running=t.is_move_running),this._emit("state",{...this._robotState})}if(e.error&&this._emit("error",{source:"robot",error:e.error}),this._broadcastWaiters.length>0)for(let t=this._broadcastWaiters.length-1;t>=0;t--){const i=this._broadcastWaiters[t];if(i.predicate(e)){this._broadcastWaiters.splice(t,1),clearTimeout(i.timer),i.resolve(e);return}}}_startLatencyMonitor(e){this._latencyMonitorId&&clearInterval(this._latencyMonitorId),this._latencyMonitorId=setInterval(()=>{if(!e.srcObject||e.paused)return;const t=e.buffered;if(t.length>0){const i=t.end(t.length-1),s=i-e.currentTime;s>.5&&(console.log(`Latency correction: was ${s.toFixed(2)}s behind`),e.currentTime=i-.1)}},2e3)}}window.ReachyMini=le;window.dispatchEvent(new Event("reachymini:ready"));const M=new URLSearchParams(window.location.search),ce=M.get("embedded")==="1"||M.get("embed")==="1",de=M.has("code")||M.has("error"),he=sessionStorage.getItem("cookaiware:launched")==="1";function F(){sessionStorage.setItem("cookaiware:launched","1");const o=document.getElementById("root");return o&&(o.innerHTML=""),U(async()=>{const{mountHost:a}=await import("./auto-CaTD3xKq.js");return{mountHost:a}},__vite__mapDeps([0,1])).then(({mountHost:a})=>{a({appName:"CookAIware",appIconUrl:"/icon.png",appEmoji:"🍲",enableMicrophone:!0,devToken:void 0,clientId:void 0})})}ce?U(()=>import("./embed-ByAC4o7k.js"),__vite__mapDeps([2,1,3])):de||he?F():U(async()=>{const{renderLanding:o}=await import("./landing-B7BE48AY.js");return{renderLanding:o}},__vite__mapDeps([4,5])).then(({renderLanding:o})=>o(()=>void F()));export{U as _,_e as r};
index.html CHANGED
@@ -59,7 +59,7 @@
59
  }
60
  })();
61
  </script>
62
- <script type="module" crossorigin src="/assets/index-Bpu0WOHF.js"></script>
63
  </head>
64
  <body>
65
  <!-- Capture the robot's inbound audio track BEFORE the Reachy SDK module
 
59
  }
60
  })();
61
  </script>
62
+ <script type="module" crossorigin src="/assets/index-DGQc0Yr2.js"></script>
63
  </head>
64
  <body>
65
  <!-- Capture the robot's inbound audio track BEFORE the Reachy SDK module
web/src/voice/controller.ts CHANGED
@@ -1,6 +1,6 @@
1
  import type { ReachyMiniInstance } from "@pollen-robotics/reachy-mini-sdk";
2
  import { RealtimeSession } from "./realtime";
3
- import { getPhoneMicTrack, getRobotMicTrack, getRobotSpeakerTrack, resumeAudio, setPhoneOutput } from "./audio";
4
  import type { ToolContext } from "./tools";
5
  import type { RobotGestures } from "../robot/gestures";
6
  import type { Lang } from "../i18n";
@@ -63,18 +63,12 @@ export class VoiceController {
63
  if (source === "robot") {
64
  micTrack = getRobotMicTrack(this.opts.getReachy());
65
  if (!micTrack) {
66
- // Robot mic genuinely unavailable β€” fall back to the phone mic so the
67
- // conversation still works instead of failing.
68
  console.warn("[voice] robot mic track not found; falling back to phone mic");
69
  this.opts.onError("Reachy's mic wasn't available β€” using your phone's mic.");
70
  micTrack = await getPhoneMicTrack();
71
- setPhoneOutput(true);
72
- } else {
73
- setPhoneOutput(false); // robot speaks
74
  }
75
  } else {
76
  micTrack = await getPhoneMicTrack();
77
- setPhoneOutput(true); // you hear it on the phone (robot too if connected)
78
  }
79
  } catch {
80
  this.opts.onError("Couldn't access the microphone.");
@@ -85,17 +79,20 @@ export class VoiceController {
85
  this.opts.gestures?.cancel();
86
 
87
  // Send our TTS to the robot speaker. SDK 1.8 no longer calls getUserMedia;
88
- // instead it puts a silent placeholder on the audio sender and expects us
89
- // to replaceTrack() our real audio onto it. Then enable the daemon's
90
- // wobbler so it animates the head from the speech now playing.
91
- const reachyForAudio = this.opts.getReachy();
 
 
 
92
  try {
93
- this.injectSpeakerTrack(reachyForAudio);
94
- reachyForAudio?.setMicMuted(false);
95
- reachyForAudio?.sendRaw({ type: "set_wobbling", enabled: true });
96
- } catch (err) {
97
- console.warn("[voice] could not attach robot speaker track", err);
98
  }
 
99
 
100
  this.session = new RealtimeSession({
101
  apiKey,
@@ -110,7 +107,7 @@ export class VoiceController {
110
  onUserText: (t) => this.opts.onTranscript("user", t),
111
  onAssistantText: (t) => this.opts.onTranscript("assistant", t),
112
  onSpeakingChange: (speaking) => {
113
- // Status only β€” the daemon animates the head from the speaker audio.
114
  this.setStatus(speaking ? "speaking" : "listening");
115
  },
116
  onClosed: () => {
@@ -125,9 +122,9 @@ export class VoiceController {
125
  stop(): void {
126
  this.session?.close();
127
  this.session = null;
 
 
128
  const reachy = this.opts.getReachy();
129
- // Restore the silent placeholder on the sender and turn the daemon wobbler
130
- // off so the idle robot stays calm (robot mic stays available for next time).
131
  try {
132
  this.restoreSpeakerTrack();
133
  reachy?.setMicMuted(true);
@@ -142,15 +139,17 @@ export class VoiceController {
142
  private audioSender: RTCRtpSender | null = null;
143
  private placeholderTrack: MediaStreamTrack | null = null;
144
 
145
- private injectSpeakerTrack(reachy: ReachyMiniInstance | null): void {
 
146
  const pc = reachy?._pc ?? null;
147
  const ttsTrack = getRobotSpeakerTrack();
148
- if (!pc || !ttsTrack) return;
149
  const sender = pc.getSenders().find((s) => s.track && s.track.kind === "audio") ?? null;
150
- if (!sender) return;
151
  this.audioSender = sender;
152
  this.placeholderTrack = sender.track; // the SDK's silent placeholder
153
  void sender.replaceTrack(ttsTrack);
 
154
  }
155
 
156
  private restoreSpeakerTrack(): void {
@@ -160,4 +159,57 @@ export class VoiceController {
160
  this.placeholderTrack = null;
161
  }
162
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
  }
 
1
  import type { ReachyMiniInstance } from "@pollen-robotics/reachy-mini-sdk";
2
  import { RealtimeSession } from "./realtime";
3
+ import { getPhoneMicTrack, getRobotMicTrack, getRobotSpeakerTrack, resumeAudio, setPhoneOutput, ttsLevel } from "./audio";
4
  import type { ToolContext } from "./tools";
5
  import type { RobotGestures } from "../robot/gestures";
6
  import type { Lang } from "../i18n";
 
63
  if (source === "robot") {
64
  micTrack = getRobotMicTrack(this.opts.getReachy());
65
  if (!micTrack) {
 
 
66
  console.warn("[voice] robot mic track not found; falling back to phone mic");
67
  this.opts.onError("Reachy's mic wasn't available β€” using your phone's mic.");
68
  micTrack = await getPhoneMicTrack();
 
 
 
69
  }
70
  } else {
71
  micTrack = await getPhoneMicTrack();
 
72
  }
73
  } catch {
74
  this.opts.onError("Couldn't access the microphone.");
 
79
  this.opts.gestures?.cancel();
80
 
81
  // Send our TTS to the robot speaker. SDK 1.8 no longer calls getUserMedia;
82
+ // it puts a silent placeholder on the audio sender and expects us to
83
+ // replaceTrack() our real audio onto it. Play on the PHONE only when the
84
+ // robot isn't connected (otherwise you'd hear it twice). We animate the
85
+ // head ourselves (client-side), so keep the daemon wobbler off.
86
+ const reachy = this.opts.getReachy();
87
+ const robotPlaying = this.injectSpeakerTrack(reachy);
88
+ setPhoneOutput(!robotPlaying);
89
  try {
90
+ reachy?.setMicMuted(false);
91
+ reachy?.sendRaw({ type: "set_wobbling", enabled: false });
92
+ } catch {
93
+ /* no audio path */
 
94
  }
95
+ this.startMotion();
96
 
97
  this.session = new RealtimeSession({
98
  apiKey,
 
107
  onUserText: (t) => this.opts.onTranscript("user", t),
108
  onAssistantText: (t) => this.opts.onTranscript("assistant", t),
109
  onSpeakingChange: (speaking) => {
110
+ this.speaking = speaking;
111
  this.setStatus(speaking ? "speaking" : "listening");
112
  },
113
  onClosed: () => {
 
122
  stop(): void {
123
  this.session?.close();
124
  this.session = null;
125
+ this.speaking = false;
126
+ this.stopMotion();
127
  const reachy = this.opts.getReachy();
 
 
128
  try {
129
  this.restoreSpeakerTrack();
130
  reachy?.setMicMuted(true);
 
139
  private audioSender: RTCRtpSender | null = null;
140
  private placeholderTrack: MediaStreamTrack | null = null;
141
 
142
+ /** Returns true if our TTS track was attached to the robot's audio sender. */
143
+ private injectSpeakerTrack(reachy: ReachyMiniInstance | null): boolean {
144
  const pc = reachy?._pc ?? null;
145
  const ttsTrack = getRobotSpeakerTrack();
146
+ if (!pc || !ttsTrack) return false;
147
  const sender = pc.getSenders().find((s) => s.track && s.track.kind === "audio") ?? null;
148
+ if (!sender) return false;
149
  this.audioSender = sender;
150
  this.placeholderTrack = sender.track; // the SDK's silent placeholder
151
  void sender.replaceTrack(ttsTrack);
152
+ return true;
153
  }
154
 
155
  private restoreSpeakerTrack(): void {
 
159
  this.placeholderTrack = null;
160
  }
161
  }
162
+
163
+ // ── Client-side motion (head only, ~20 Hz) ─────────────────────────────────
164
+ // Ported from the reachy-stories reference: a 20 Hz loop that wobbles the
165
+ // HEAD (roll/pitch/yaw) from smoothed speech energy while talking, and does a
166
+ // gentle breath when idle. Antennas are deliberately NOT driven from the
167
+ // wobble β€” oscillating them at audio rate was the high-frequency buzz.
168
+ private speaking = false;
169
+ private motionRaf: number | null = null;
170
+ private env = 0;
171
+ private lastCmd = 0;
172
+
173
+ private startMotion(): void {
174
+ if (this.motionRaf !== null) return;
175
+ const loop = (now: number) => {
176
+ this.motionRaf = requestAnimationFrame(loop);
177
+ if (now - this.lastCmd < 50) return; // throttle to ~20 Hz
178
+ this.lastCmd = now;
179
+ const reachy = this.opts.getReachy();
180
+ if (!reachy) return;
181
+ const t = now / 1000;
182
+ try {
183
+ if (this.speaking) {
184
+ const level = Math.min(ttsLevel(), 1);
185
+ this.env += (level - this.env) * 0.3; // smooth envelope
186
+ const e = this.env;
187
+ reachy.setHeadRpyDeg(
188
+ e * 3 * Math.sin(t * 3.3),
189
+ e * 5 * Math.sin(t * 2.7) + Math.sin(t * 0.8) * 1.2,
190
+ e * 4 * Math.sin(t * 2.1),
191
+ );
192
+ } else {
193
+ const breath = Math.sin(t * 0.9);
194
+ reachy.setHeadRpyDeg(0, breath * 1.8, Math.sin(t * 0.5) * 2);
195
+ }
196
+ } catch {
197
+ /* channel closed */
198
+ }
199
+ };
200
+ this.motionRaf = requestAnimationFrame(loop);
201
+ }
202
+
203
+ private stopMotion(): void {
204
+ if (this.motionRaf !== null) {
205
+ cancelAnimationFrame(this.motionRaf);
206
+ this.motionRaf = null;
207
+ }
208
+ this.env = 0;
209
+ try {
210
+ this.opts.getReachy()?.setHeadRpyDeg(0, 0, 0);
211
+ } catch {
212
+ /* channel closed */
213
+ }
214
+ }
215
  }
web/src/voice/realtime.ts CHANGED
@@ -126,6 +126,7 @@ export class RealtimeSession {
126
  await ac.audioWorklet.addModule(blobUrl);
127
  URL.revokeObjectURL(blobUrl);
128
  const node = new AudioWorkletNode(ac, "pcm-pump");
 
129
  node.port.onmessage = (e: MessageEvent<ArrayBuffer>) => {
130
  // Mute the pump while the assistant talks β€” otherwise the robot mic
131
  // hears the robot's own speaker and feeds it back into the VAD.
@@ -134,6 +135,16 @@ export class RealtimeSession {
134
  return;
135
  }
136
  const incoming = new Uint8Array(e.data);
 
 
 
 
 
 
 
 
 
 
137
  const merged = new Uint8Array(this.micBytes.length + incoming.length);
138
  merged.set(this.micBytes);
139
  merged.set(incoming, this.micBytes.length);
@@ -144,6 +155,11 @@ export class RealtimeSession {
144
  }
145
  };
146
  src.connect(node);
 
 
 
 
 
147
  this.micWorklet = node;
148
  }
149
 
 
126
  await ac.audioWorklet.addModule(blobUrl);
127
  URL.revokeObjectURL(blobUrl);
128
  const node = new AudioWorkletNode(ac, "pcm-pump");
129
+ let chunkCount = 0;
130
  node.port.onmessage = (e: MessageEvent<ArrayBuffer>) => {
131
  // Mute the pump while the assistant talks β€” otherwise the robot mic
132
  // hears the robot's own speaker and feeds it back into the VAD.
 
135
  return;
136
  }
137
  const incoming = new Uint8Array(e.data);
138
+ // Diagnostic: log the first chunks' average magnitude so we can tell
139
+ // whether the chosen mic is actually producing audio (β‰ˆ0 = silent track).
140
+ chunkCount++;
141
+ if (chunkCount <= 5 || chunkCount % 100 === 0) {
142
+ const view = new Int16Array(incoming.buffer, incoming.byteOffset, incoming.byteLength / 2);
143
+ let sum = 0;
144
+ for (let i = 0; i < view.length; i++) sum += Math.abs(view[i]);
145
+ const avg = view.length ? Math.round(sum / view.length) : 0;
146
+ console.log(`[voice] mic chunk #${chunkCount} avg|sample|=${avg}${avg < 30 ? " ⚠ very quiet/silent" : ""}`);
147
+ }
148
  const merged = new Uint8Array(this.micBytes.length + incoming.length);
149
  merged.set(this.micBytes);
150
  merged.set(incoming, this.micBytes.length);
 
155
  }
156
  };
157
  src.connect(node);
158
+ // Keep the worklet in the render graph (no audible output) so process() runs.
159
+ const sink = ac.createGain();
160
+ sink.gain.value = 0;
161
+ node.connect(sink);
162
+ sink.connect(ac.destination);
163
  this.micWorklet = node;
164
  }
165