@animated-waffle/client 0.1.5 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -8
- package/dist/chunk-M7Z7AFVA.js +1 -0
- package/dist/index.d.ts +55 -11
- package/dist/index.js +1 -1
- package/dist/internal.d.ts +1 -19
- package/dist/internal.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-LJ4KJFLQ.js +0 -1
package/README.md
CHANGED
|
@@ -35,10 +35,20 @@ const waffle = new WaffleClient({
|
|
|
35
35
|
})
|
|
36
36
|
|
|
37
37
|
waffle.on('transcript', ({ id, role, text, final }) => {
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
// Completed Narya conversation rows use the same id as persisted history.
|
|
39
|
+
if (final) console.log(id, role, text)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
waffle.on('speechSegment', ({ id, messageId, sequence, progressId, text, annotatedText }) => {
|
|
43
|
+
// Published only at this segment's queued audio start—not when text merely
|
|
44
|
+
// enters TTS or a transport-wide speaking callback fires.
|
|
45
|
+
console.log(id, messageId, sequence, progressId, text, annotatedText)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
waffle.on('speechProgress', ({ progressId, status, accumulatedText, remainingText }) => {
|
|
49
|
+
// Factual progress associated with the segment above. The browser owns any
|
|
50
|
+
// semantic interpretation of annotatedText.
|
|
51
|
+
console.log(progressId, status, accumulatedText, remainingText)
|
|
42
52
|
})
|
|
43
53
|
|
|
44
54
|
await waffle.connect({ inputMode: 'push-to-talk' })
|
|
@@ -57,17 +67,25 @@ const waffle = new WaffleClient({
|
|
|
57
67
|
})
|
|
58
68
|
```
|
|
59
69
|
|
|
70
|
+
At an input interruption the client immediately resets and temporarily blocks
|
|
71
|
+
speech facts. The runtime orders `conversation-speech-playout-cutoff` after all
|
|
72
|
+
old source identities and begins cancelling the old media queue; the client
|
|
73
|
+
then clears again and reopens the fact stream. An already-in-flight old marker
|
|
74
|
+
may arrive later, but it is an orphan and cannot revive cancelled performance.
|
|
75
|
+
|
|
60
76
|
## API surface
|
|
61
77
|
|
|
62
78
|
- `connect({ inputMode? })` — `'push-to-talk'` (default) or `'automatic'`.
|
|
63
79
|
- `disconnect()`, `sendText(text)`.
|
|
64
80
|
- `startPushToTalk()` / `commitPushToTalk()` / `cancelPushToTalk()`.
|
|
65
|
-
- `on('status' | 'session' | 'transcript' | '
|
|
66
|
-
returns the unsubscribe function.
|
|
81
|
+
- `on('status' | 'session' | 'transcript' | 'speechSegment' | 'speechProgress' |
|
|
82
|
+
'agentSpeaking' | 'error', fn)` — returns the unsubscribe function.
|
|
67
83
|
- Conversation transcripts carry the same required `id` as persisted history.
|
|
68
|
-
Assistant rows stream in place — accumulated `text` plus tag-preserving
|
|
69
|
-
`annotatedText`, then `final: true` — so replace by `id`, never append.
|
|
70
84
|
Provider sentence/progress framing never defines a public conversation row.
|
|
85
|
+
- Speech segments become public at their associated audio start. The SDK uses
|
|
86
|
+
raw `new` output only to correlate identity, never as a timing signal. Their
|
|
87
|
+
`annotatedText` drives local Avatar choreography while `text` remains clean
|
|
88
|
+
spoken text; `speechProgress` carries provider-paced accumulated facts.
|
|
71
89
|
- `status`, `session` getters; failures throw `WaffleError` with `code`.
|
|
72
90
|
|
|
73
91
|
`./internal` is a private contract consumed by `@animated-waffle/react`; it is
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var s=new WeakMap;function o(n,t,a){let e=s.get(n)??new Map,r=e.get(t)??new Set;return r.add(a),e.set(t,r),s.set(n,e),()=>r.delete(a)}function l(n,t,a){s.get(n)?.get(t)?.forEach(e=>{e(a)})}export{o as a,l as b};
|
package/dist/index.d.ts
CHANGED
|
@@ -12,22 +12,44 @@ interface WaffleTranscript {
|
|
|
12
12
|
/** The persisted conversation message id. */
|
|
13
13
|
id: string;
|
|
14
14
|
role: 'user' | 'assistant';
|
|
15
|
-
/** User-facing text with voice-control tags stripped. */
|
|
16
15
|
text: string;
|
|
17
16
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*/
|
|
22
|
-
annotatedText?: string;
|
|
23
|
-
/**
|
|
24
|
-
* False while the agent is still speaking this entry. The runtime re-sends
|
|
25
|
-
* the same id with accumulated text as each sentence is spoken, then once
|
|
26
|
-
* more with final: true when the turn completes. Replace by id, not append.
|
|
17
|
+
* False while this text is still provisional. Only interim speech recognition
|
|
18
|
+
* is provisional: a caller replaces the previous non-final entry from that
|
|
19
|
+
* speaker rather than appending. Agent text is always final.
|
|
27
20
|
*/
|
|
28
21
|
final: boolean;
|
|
29
22
|
timestamp?: string;
|
|
30
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* One Agent speech segment, published at its associated audio start. `text` is
|
|
26
|
+
* the clean spoken text while `annotatedText` retains inline performance tags
|
|
27
|
+
* for the browser-side Avatar choreographer. It is deliberately separate from
|
|
28
|
+
* the persisted conversation transcript.
|
|
29
|
+
*/
|
|
30
|
+
interface WaffleSpeechSegment {
|
|
31
|
+
/** Stable speech-segment id. */
|
|
32
|
+
id: string;
|
|
33
|
+
/** Persisted assistant message this segment belongs to. */
|
|
34
|
+
messageId: string;
|
|
35
|
+
/** Zero-based order within the assistant message. */
|
|
36
|
+
sequence: number;
|
|
37
|
+
/** Numeric identity shared with Pipecat's factual playout progress. */
|
|
38
|
+
progressId: number;
|
|
39
|
+
text: string;
|
|
40
|
+
annotatedText: string;
|
|
41
|
+
}
|
|
42
|
+
type WaffleSpeechProgressStatus = 'new' | 'in-progress' | 'completed';
|
|
43
|
+
/** Factual playout progress associated with one public speech segment. */
|
|
44
|
+
interface WaffleSpeechProgress {
|
|
45
|
+
segmentId: string;
|
|
46
|
+
messageId: string;
|
|
47
|
+
sequence: number;
|
|
48
|
+
progressId: number;
|
|
49
|
+
status: WaffleSpeechProgressStatus;
|
|
50
|
+
accumulatedText: string;
|
|
51
|
+
remainingText: string;
|
|
52
|
+
}
|
|
31
53
|
interface WaffleClientEvents {
|
|
32
54
|
connectionState: WaffleConnectionState;
|
|
33
55
|
/** @deprecated Subscribe to connectionState. */
|
|
@@ -37,6 +59,10 @@ interface WaffleClientEvents {
|
|
|
37
59
|
agentSpeaking: boolean;
|
|
38
60
|
remoteAudioTrack: MediaStreamTrack | null;
|
|
39
61
|
transcript: WaffleTranscript;
|
|
62
|
+
speechSegment: WaffleSpeechSegment;
|
|
63
|
+
speechProgress: WaffleSpeechProgress;
|
|
64
|
+
/** A user input boundary cancelled any pending Agent speech/performance. */
|
|
65
|
+
interrupted: undefined;
|
|
40
66
|
}
|
|
41
67
|
type WaffleSessionToken = () => string | Promise<string>;
|
|
42
68
|
interface WaffleSessionOverrides {
|
|
@@ -132,6 +158,14 @@ declare class WaffleClient implements WaffleSessionClient {
|
|
|
132
158
|
private nextTurnId;
|
|
133
159
|
private lifecycleVersion;
|
|
134
160
|
private recreateRealtimeOnConnect;
|
|
161
|
+
private readonly speechSegmentsByProgressId;
|
|
162
|
+
private readonly pendingSpeechProgress;
|
|
163
|
+
private readonly seenSpeechProgress;
|
|
164
|
+
private readonly releasedSpeechSegments;
|
|
165
|
+
private readonly pendingSpeechPlayoutStarts;
|
|
166
|
+
private botAudioActive;
|
|
167
|
+
private speechFactsBlocked;
|
|
168
|
+
private speechPlayoutCutoffVersion;
|
|
135
169
|
constructor(options: WaffleClientOptions);
|
|
136
170
|
private createRealtime;
|
|
137
171
|
get status(): WaffleStatus;
|
|
@@ -154,6 +188,16 @@ declare class WaffleClient implements WaffleSessionClient {
|
|
|
154
188
|
private handleTransportState;
|
|
155
189
|
private handleReady;
|
|
156
190
|
private handleDisconnected;
|
|
191
|
+
private handleSpeechSegment;
|
|
192
|
+
private handleSpeechPlayoutStart;
|
|
193
|
+
private handleBotOutput;
|
|
194
|
+
private handleBotStartedSpeaking;
|
|
195
|
+
private handleBotStoppedSpeaking;
|
|
196
|
+
private releaseSpeechSegment;
|
|
197
|
+
private emitSpeechProgress;
|
|
198
|
+
private resetSpeechProgress;
|
|
199
|
+
private interruptSpeech;
|
|
200
|
+
private handleSpeechPlayoutCutoff;
|
|
157
201
|
private setAudioTrack;
|
|
158
202
|
private stopAudio;
|
|
159
203
|
private setStatus;
|
|
@@ -163,4 +207,4 @@ declare class WaffleClient implements WaffleSessionClient {
|
|
|
163
207
|
private emit;
|
|
164
208
|
}
|
|
165
209
|
|
|
166
|
-
export { WaffleClient, type WaffleClientEvents, type WaffleClientOptions, type WaffleConnectOptions, type WaffleConnectionState, WaffleError, type WaffleInputMode, type WaffleSession, type WaffleSessionClient, type WaffleSessionOverrides, type WaffleSessionStartInput, type WaffleSessionStartOptions, type WaffleSessionStarter, type WaffleSessionToken, type WaffleStatus, type WaffleTranscript };
|
|
210
|
+
export { WaffleClient, type WaffleClientEvents, type WaffleClientOptions, type WaffleConnectOptions, type WaffleConnectionState, WaffleError, type WaffleInputMode, type WaffleSession, type WaffleSessionClient, type WaffleSessionOverrides, type WaffleSessionStartInput, type WaffleSessionStartOptions, type WaffleSessionStarter, type WaffleSessionToken, type WaffleSpeechProgress, type WaffleSpeechProgressStatus, type WaffleSpeechSegment, type WaffleStatus, type WaffleTranscript };
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{b as p}from"./chunk-LJ4KJFLQ.js";import{PipecatClient as T}from"@pipecat-ai/client-js";import{DailyTransport as y}from"@pipecat-ai/daily-transport";var w=15e3,k=3e4,W="https://animated-waffle.narya.ai",I=/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,C=/^sha256:[a-f0-9]{64}$/,i=class extends Error{constructor(t,n,o){super(t);this.status=n;this.code=o;this.name="WaffleError"}status;code};function h(s){return s!==null&&typeof s=="object"&&!Array.isArray(s)?s:null}function P(s=W){let e;try{e=new URL(s)}catch{throw new i("apiOrigin must be a valid URL.",void 0,"invalid_api_origin")}let t=e.hostname==="localhost"||e.hostname==="127.0.0.1"||e.hostname==="[::1]";if(e.protocol!=="https:"&&!(e.protocol==="http:"&&t)||e.username||e.password||e.pathname!=="/"&&e.pathname!==""||e.search||e.hash)throw new i("apiOrigin must be an HTTPS origin or an HTTP loopback origin.",void 0,"invalid_api_origin");return e.origin}function f(s,e){if(typeof s!="string"||s.trim().length===0)throw new i(`Session response is missing ${e}.`);return s}function b(s){let e=f(s,"avatar.asset_hash");if(!C.test(e))throw new i("Session response contains an invalid Avatar asset hash.");return e}function m(s){return typeof s=="string"&&s.length>0?s:void 0}function A(s,e,t){let n=h(s);if(!n)throw new i("Session response must be a JSON object.");if(n.protocol_version!==1)throw new i("Session response has an unsupported protocol version.");let o=h(n.connection);if(!o||o.transport!=="webrtc")throw new i("Session response is missing a WebRTC connection.");let u=f(o.url,"connection.url");try{if(new URL(u).protocol!=="https:")throw new Error}catch{throw new i("Session response contains an invalid connection URL.")}let c=f(n.agent_id,"agent_id");if(c!==t)throw new i("Session response contains a mismatched Agent id.");let d=m(n.avatar_id),l=h(n.avatar),v=null;if(d||l){if(!d||!l||f(l.id,"avatar.id")!==d)throw new i("Session response contains mismatched Avatar metadata.");let r=f(l.engine,"avatar.engine");if(r!=="vrm")throw new i("Session response contains an unsupported Avatar engine.");let a=f(l.model_url,"avatar.model_url");try{if(new URL(a).protocol!=="https:")throw new Error}catch{throw new i("Session response contains an invalid Avatar model URL.")}v={id:d,name:f(l.name,"avatar.name"),engine:r,modelUrl:a,assetHash:b(l.asset_hash),runtimeProfile:f(l.runtime_profile,"avatar.runtime_profile")}}return{id:f(n.session_id,"session_id"),agentId:c,...d?{avatarId:d}:{},avatar:v,inputMode:e,room:u,token:f(o.token,"connection.token")}}function _(s){let e=h(s),t=m(e?.tag),n=e?.kind;return e?.type!=="avatar-performance"||!t||n!=="emotion"&&n!=="vocal-action"?null:{phase:n,tag:t}}function O(s){let e=h(s),t=m(e?.messageId),n=e?.role;if(e?.type!=="conversation-transcript"||!t||n!=="user"&&n!=="assistant"||typeof e.text!="string"||typeof e.final!="boolean")return null;let o={id:t,role:n,text:e.text,final:e.final},u=m(e.annotatedText);return u!==void 0&&(o.annotatedText=u),o}function S(s){let{avatar:e,room:t,token:n,...o}=s;return o}var g=class{realtime;realtimeCallbacks;listeners=new Map;options;apiOrigin;currentStatus="disconnected";currentSession=null;startPromise=null;stopPromise=null;requestAbort=null;readyWaiter=null;audio=null;currentRemoteAudioTrack=null;activeInputMode="push-to-talk";turnId=null;nextTurnId=0;lifecycleVersion=0;recreateRealtimeOnConnect=!1;constructor(e){if(this.options=e,this.apiOrigin=P(e.apiOrigin),!I.test(e.agentId))throw new i("agentId must be a UUID.",void 0,"invalid_agent_id");if(!e.startSession&&typeof e.sessionToken!="function")throw new i("sessionToken must be a function.",void 0,"invalid_session_token");this.realtimeCallbacks={onTransportStateChanged:t=>this.handleTransportState(t),onBotReady:()=>this.handleReady(),onDisconnected:()=>this.handleDisconnected(),onBotStartedSpeaking:()=>this.emit("agentSpeaking",!0),onBotStoppedSpeaking:()=>this.emit("agentSpeaking",!1),onBotTtsText:t=>{t.text&&p(this,"spokenWord",{text:t.text})},onServerMessage:t=>{let n=O(t);n&&this.emit("transcript",n);let o=_(t);o&&p(this,"avatarPerformance",o)},onTrackStarted:(t,n)=>{t.kind==="audio"&&n?.local===!1&&this.setAudioTrack(t)},onTrackStopped:(t,n)=>{t.kind==="audio"&&n?.local===!1&&this.setAudioTrack(null)},onError:t=>this.fail(new i(h(t)?.message?.toString()??"Realtime connection failed."))},this.realtime=this.createRealtime()}createRealtime(){let e=new y({bufferLocalAudioUntilBotReady:!0,inputSettings:{audio:{processor:{type:"noise-cancellation"}}}});return new T({transport:e,enableCam:!1,enableMic:!1,disconnectOnBotDisconnect:!0,callbacks:this.realtimeCallbacks})}get status(){return this.currentStatus}get connectionState(){return this.currentStatus}get session(){return this.currentSession?S(this.currentSession):null}get remoteAudioTrack(){return this.currentRemoteAudioTrack}on(e,t){let n=this.listeners.get(e)??new Set;return n.add(t),this.listeners.set(e,n),()=>n.delete(t)}connect(e={}){if(this.currentSession&&this.status==="connected")return Promise.resolve(S(this.currentSession));if(this.startPromise)return this.startPromise;this.recreateRealtimeOnConnect&&(this.realtime=this.createRealtime(),this.recreateRealtimeOnConnect=!1);let t=++this.lifecycleVersion,n=this.startInternal(e,t).finally(()=>{this.startPromise===n&&(this.startPromise=null)});return this.startPromise=n,n}disconnect(){if(this.stopPromise)return this.stopPromise;let e=this.stopInternal().finally(()=>{this.stopPromise===e&&(this.stopPromise=null)});return this.stopPromise=e,e}async sendText(e){e.trim()&&(this.assertConnected(),await this.realtime.sendText(e,{audio_response:!0,run_immediately:!0}))}startPushToTalk(){if(this.turnId===null){this.nextTurnId+=1,this.turnId=this.nextTurnId;try{this.sendInputControl("start",this.turnId)}catch(e){throw this.turnId=null,e}}}commitPushToTalk(){this.turnId!==null&&(this.sendInputControl("stop",this.turnId),this.turnId=null)}stopPushToTalk(){this.commitPushToTalk()}cancelPushToTalk(){this.turnId!==null&&(this.sendInputControl("cancel",this.turnId),this.turnId=null)}async startInternal(e,t){this.activeInputMode=e.inputMode??"push-to-talk",this.setStatus("connecting");let n=new AbortController;this.requestAbort=n;let o;try{o=await this.withTimeout(this.options.startSession?Promise.resolve(this.options.startSession({agentId:this.options.agentId,inputMode:this.activeInputMode,...e.sessionId?{sessionId:e.sessionId}:{},...e.overrides?{overrides:e.overrides}:{},signal:n.signal})):this.resolveSessionToken().then(r=>fetch(`${this.apiOrigin}/v1/agents/${encodeURIComponent(this.options.agentId)}/sessions`,{method:"POST",headers:{Accept:"application/json","Content-Type":"application/json",Authorization:`Bearer ${r}`},signal:n.signal,body:JSON.stringify({input_mode:this.activeInputMode==="push-to-talk"?"push_to_talk":"automatic"})})),w,"Timed out creating the Waffle session.",()=>n.abort())}catch(r){let a=r instanceof Error?r:new i("Could not create session.");throw t!==this.lifecycleVersion?new i("The connection attempt was cancelled."):(this.fail(a),a)}finally{this.requestAbort===n&&(this.requestAbort=null)}if(t!==this.lifecycleVersion)throw new i("The connection attempt was cancelled.");let u=await o.json().catch(()=>null);if(!o.ok){let r=h(u),a=new i(m(r?.detail)??"Could not create session.",o.status,m(r?.code));throw this.fail(a),a}let c;try{c=A(u,this.activeInputMode,this.options.agentId)}catch(r){let a=r instanceof Error?r:new i("Could not parse the Waffle session.");throw this.fail(a),a}this.currentSession=c,this.emit("session",S(c)),p(this,"presentation",c.avatar),this.setStatus("connecting");let d,l,v=new Promise((r,a)=>{d=r,l=a});this.readyWaiter={reject:l,resolve:d,version:t};try{await this.withTimeout(Promise.all([this.realtime.connect({url:c.room,token:c.token}),v]).then(()=>{}),k,"Timed out waiting for the Waffle Agent.",()=>{this.realtime.disconnect().catch(()=>{})})}catch(r){await this.realtime.disconnect().catch(()=>{});let a=r instanceof Error?r:new i("Could not connect.");throw t!==this.lifecycleVersion?new i("The connection attempt was cancelled."):(this.fail(a),a)}finally{this.readyWaiter?.version===t&&(this.readyWaiter=null)}return this.realtime.enableMic(!0),this.setStatus("connected"),this.setAudioTrack(this.realtime.tracks().bot?.audio??null),S(c)}async stopInternal(){if(this.lifecycleVersion+=1,this.readyWaiter?.reject(new i("The connection attempt was cancelled.")),this.readyWaiter=null,this.requestAbort?.abort(),this.turnId=null,this.stopAudio(),this.currentStatus!=="disconnected"){this.setStatus("disconnecting");try{await this.realtime.disconnect()}finally{this.emit("agentSpeaking",!1),this.setStatus("disconnected")}}}sendInputControl(e,t){if(this.activeInputMode!=="push-to-talk")throw new i('Push-to-talk controls require inputMode: "push-to-talk".');this.assertConnected(),this.realtime.sendClientMessage("voice.input_control",{mode:"push_to_talk",state:e,turnId:t})}assertConnected(){if(this.status!=="connected")throw new i("Start the Waffle session before sending input.")}handleTransportState(e){e==="ready"?this.handleReady():e==="disconnecting"?this.setStatus("disconnecting"):e==="disconnected"?this.handleDisconnected():this.currentSession&&this.currentStatus!=="connected"&&this.setStatus("connecting")}handleReady(){this.currentSession&&this.readyWaiter?.resolve()}handleDisconnected(){this.readyWaiter?.reject(new i("The realtime session disconnected before it was ready.")),this.readyWaiter=null,this.stopAudio(),this.emit("agentSpeaking",!1),this.setStatus("disconnected")}setAudioTrack(e){if(this.currentRemoteAudioTrack=e,this.emit("remoteAudioTrack",e),p(this,"audioTrack",e),this.stopAudio(!1),!e||typeof document>"u")return;let t=document.createElement("audio");t.autoplay=!0,t.setAttribute("playsinline",""),t.srcObject=new MediaStream([e]),this.audio=t,t.play().catch(n=>{this.emit("error",n instanceof Error?n:new i("Browser blocked agent audio."))})}stopAudio(e=!0){this.audio&&(this.audio.pause(),this.audio.srcObject=null,this.audio=null),e&&this.currentRemoteAudioTrack&&(this.currentRemoteAudioTrack=null,this.emit("remoteAudioTrack",null),p(this,"audioTrack",null))}setStatus(e){e==="disconnected"&&(this.recreateRealtimeOnConnect=!0),e==="disconnected"&&this.currentSession&&(this.currentSession=null,this.emit("session",null),p(this,"presentation",null)),this.currentStatus!==e&&(this.currentStatus=e,this.emit("connectionState",e),this.emit("status",e))}fail(e){this.readyWaiter?.reject(e),this.readyWaiter=null,this.setStatus("disconnected"),this.emit("error",e)}async resolveSessionToken(){if(!this.options.sessionToken)throw new i("No session token provider is configured.");let e=await this.options.sessionToken();if(!e.trim())throw new i("sessionToken must not be empty.",void 0,"invalid_session_token");return e}withTimeout(e,t,n,o){return new Promise((u,c)=>{let d=setTimeout(()=>{o?.(),c(new i(n))},t);e.then(l=>{clearTimeout(d),u(l)},l=>{clearTimeout(d),c(l)})})}emit(e,t){this.listeners.get(e)?.forEach(n=>{n(t)})}};export{g as WaffleClient,i as WaffleError};
|
|
1
|
+
import{b as g}from"./chunk-M7Z7AFVA.js";import{PipecatClient as k}from"@pipecat-ai/client-js";import{DailyTransport as w}from"@pipecat-ai/daily-transport";var T=15e3,P=3e4,I="https://animated-waffle.narya.ai",W=/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,b=/^sha256:[a-f0-9]{64}$/,r=class extends Error{constructor(t,n,i){super(t);this.status=n;this.code=i;this.name="WaffleError"}status;code};function h(s){return s!==null&&typeof s=="object"&&!Array.isArray(s)?s:null}function C(s=I){let e;try{e=new URL(s)}catch{throw new r("apiOrigin must be a valid URL.",void 0,"invalid_api_origin")}let t=e.hostname==="localhost"||e.hostname==="127.0.0.1"||e.hostname==="[::1]";if(e.protocol!=="https:"&&!(e.protocol==="http:"&&t)||e.username||e.password||e.pathname!=="/"&&e.pathname!==""||e.search||e.hash)throw new r("apiOrigin must be an HTTPS origin or an HTTP loopback origin.",void 0,"invalid_api_origin");return e.origin}function p(s,e){if(typeof s!="string"||s.trim().length===0)throw new r(`Session response is missing ${e}.`);return s}function A(s){let e=p(s,"avatar.asset_hash");if(!b.test(e))throw new r("Session response contains an invalid Avatar asset hash.");return e}function v(s){return typeof s=="number"&&Number.isFinite(s)?s:void 0}function f(s){return typeof s=="string"&&s.length>0?s:void 0}function x(s,e,t){let n=h(s);if(!n)throw new r("Session response must be a JSON object.");if(n.protocol_version!==1)throw new r("Session response has an unsupported protocol version.");let i=h(n.connection);if(!i||i.transport!=="webrtc")throw new r("Session response is missing a WebRTC connection.");let a=p(i.url,"connection.url");try{if(new URL(a).protocol!=="https:")throw new Error}catch{throw new r("Session response contains an invalid connection URL.")}let o=p(n.agent_id,"agent_id");if(o!==t)throw new r("Session response contains a mismatched Agent id.");let u=f(n.avatar_id),d=h(n.avatar),m=null;if(u||d){if(!u||!d||p(d.id,"avatar.id")!==u)throw new r("Session response contains mismatched Avatar metadata.");let l=p(d.engine,"avatar.engine");if(l!=="vrm")throw new r("Session response contains an unsupported Avatar engine.");let c=p(d.model_url,"avatar.model_url");try{if(new URL(c).protocol!=="https:")throw new Error}catch{throw new r("Session response contains an invalid Avatar model URL.")}m={id:u,name:p(d.name,"avatar.name"),engine:l,modelUrl:c,assetHash:A(d.asset_hash),runtimeProfile:p(d.runtime_profile,"avatar.runtime_profile")}}return{id:p(n.session_id,"session_id"),agentId:o,...u?{avatarId:u}:{},avatar:m,inputMode:e,room:a,token:p(i.token,"connection.token")}}function _(s){let e=h(s),t=f(e?.messageId),n=e?.role;return e?.type!=="conversation-transcript"||!t||n!=="user"&&n!=="assistant"||typeof e.text!="string"||e.final!==!0?null:{id:t,role:n,text:e.text,final:!0}}function O(s){let e=h(s),t=f(e?.segmentId),n=f(e?.messageId),i=v(e?.sequence),a=v(e?.progressId);return e?.type!=="conversation-speech-segment"||!t||!n||i===void 0||!Number.isInteger(i)||i<0||a===void 0||!Number.isInteger(a)||a<0||typeof e.text!="string"||typeof e.annotatedText!="string"?null:{id:t,messageId:n,sequence:i,progressId:a,text:e.text,annotatedText:e.annotatedText}}function R(s){let e=h(s),t=v(e?.progressId);return e?.type!=="conversation-speech-playout-start"||t===void 0||!Number.isInteger(t)||t<0?null:t}function M(s){return h(s)?.type==="conversation-speech-playout-cutoff"}function B(s){let e=h(s),t=h(e?.spoken_progress),n=v(e?.segment_id),i=e?.spoken_status;return n===void 0||!Number.isInteger(n)||n<0||i!=="new"&&i!=="in-progress"&&i!=="completed"||typeof t?.accumulated_text!="string"||typeof t.remaining_text!="string"?null:{progressId:n,status:i,accumulatedText:t.accumulated_text,remainingText:t.remaining_text}}function E(s){return JSON.stringify([s.status,s.accumulatedText,s.remainingText])}function S(s){let{avatar:e,room:t,token:n,...i}=s;return i}var y=class{realtime;realtimeCallbacks;listeners=new Map;options;apiOrigin;currentStatus="disconnected";currentSession=null;startPromise=null;stopPromise=null;requestAbort=null;readyWaiter=null;audio=null;currentRemoteAudioTrack=null;activeInputMode="push-to-talk";turnId=null;nextTurnId=0;lifecycleVersion=0;recreateRealtimeOnConnect=!1;speechSegmentsByProgressId=new Map;pendingSpeechProgress=new Map;seenSpeechProgress=new Map;releasedSpeechSegments=new Set;pendingSpeechPlayoutStarts=new Set;botAudioActive=!1;speechFactsBlocked=!1;speechPlayoutCutoffVersion=0;constructor(e){if(this.options=e,this.apiOrigin=C(e.apiOrigin),!W.test(e.agentId))throw new r("agentId must be a UUID.",void 0,"invalid_agent_id");if(!e.startSession&&typeof e.sessionToken!="function")throw new r("sessionToken must be a function.",void 0,"invalid_session_token");this.realtimeCallbacks={onTransportStateChanged:t=>this.handleTransportState(t),onBotReady:()=>this.handleReady(),onDisconnected:()=>this.handleDisconnected(),onBotStartedSpeaking:()=>this.handleBotStartedSpeaking(),onBotStoppedSpeaking:()=>this.handleBotStoppedSpeaking(),onUserStartedSpeaking:()=>{this.interruptSpeech()},onBotOutput:t=>this.handleBotOutput(t),onServerMessage:t=>{if(M(t)){this.handleSpeechPlayoutCutoff();return}let n=_(t);n&&this.emit("transcript",n);let i=O(t);i&&this.handleSpeechSegment(i);let a=R(t);a!==null&&this.handleSpeechPlayoutStart(a)},onTrackStarted:(t,n)=>{t.kind==="audio"&&n?.local===!1&&this.setAudioTrack(t)},onTrackStopped:(t,n)=>{t.kind==="audio"&&n?.local===!1&&this.setAudioTrack(null)},onError:t=>this.fail(new r(h(t)?.message?.toString()??"Realtime connection failed."))},this.realtime=this.createRealtime()}createRealtime(){let e=new w({bufferLocalAudioUntilBotReady:!0,inputSettings:{audio:{processor:{type:"noise-cancellation"}}}});return new k({transport:e,enableCam:!1,enableMic:!1,disconnectOnBotDisconnect:!0,callbacks:this.realtimeCallbacks})}get status(){return this.currentStatus}get connectionState(){return this.currentStatus}get session(){return this.currentSession?S(this.currentSession):null}get remoteAudioTrack(){return this.currentRemoteAudioTrack}on(e,t){let n=this.listeners.get(e)??new Set;return n.add(t),this.listeners.set(e,n),()=>n.delete(t)}connect(e={}){if(this.currentSession&&this.status==="connected")return Promise.resolve(S(this.currentSession));if(this.startPromise)return this.startPromise;this.recreateRealtimeOnConnect&&(this.realtime=this.createRealtime(),this.recreateRealtimeOnConnect=!1);let t=++this.lifecycleVersion,n=this.startInternal(e,t).finally(()=>{this.startPromise===n&&(this.startPromise=null)});return this.startPromise=n,n}disconnect(){if(this.stopPromise)return this.stopPromise;let e=this.stopInternal().finally(()=>{this.stopPromise===e&&(this.stopPromise=null)});return this.stopPromise=e,e}async sendText(e){if(!e.trim())return;this.assertConnected();let t=this.speechFactsBlocked,n=this.speechPlayoutCutoffVersion;this.speechFactsBlocked=!0;try{await this.realtime.sendText(e,{audio_response:!0,run_immediately:!0})}catch(i){throw this.speechFactsBlocked=t,i}this.interruptSpeech(this.speechPlayoutCutoffVersion===n)}startPushToTalk(){if(this.turnId===null){this.nextTurnId+=1,this.turnId=this.nextTurnId;try{this.sendInputControl("start",this.turnId),this.interruptSpeech()}catch(e){throw this.turnId=null,e}}}commitPushToTalk(){this.turnId!==null&&(this.sendInputControl("stop",this.turnId),this.turnId=null)}stopPushToTalk(){this.commitPushToTalk()}cancelPushToTalk(){this.turnId!==null&&(this.sendInputControl("cancel",this.turnId),this.turnId=null)}async startInternal(e,t){this.resetSpeechProgress(),this.speechFactsBlocked=!1,this.speechPlayoutCutoffVersion=0,this.activeInputMode=e.inputMode??"push-to-talk",this.setStatus("connecting");let n=new AbortController;this.requestAbort=n;let i;try{i=await this.withTimeout(this.options.startSession?Promise.resolve(this.options.startSession({agentId:this.options.agentId,inputMode:this.activeInputMode,...e.sessionId?{sessionId:e.sessionId}:{},...e.overrides?{overrides:e.overrides}:{},signal:n.signal})):this.resolveSessionToken().then(l=>fetch(`${this.apiOrigin}/v1/agents/${encodeURIComponent(this.options.agentId)}/sessions`,{method:"POST",headers:{Accept:"application/json","Content-Type":"application/json",Authorization:`Bearer ${l}`},signal:n.signal,body:JSON.stringify({input_mode:this.activeInputMode==="push-to-talk"?"push_to_talk":"automatic"})})),T,"Timed out creating the Waffle session.",()=>n.abort())}catch(l){let c=l instanceof Error?l:new r("Could not create session.");throw t!==this.lifecycleVersion?new r("The connection attempt was cancelled."):(this.fail(c),c)}finally{this.requestAbort===n&&(this.requestAbort=null)}if(t!==this.lifecycleVersion)throw new r("The connection attempt was cancelled.");let a=await i.json().catch(()=>null);if(!i.ok){let l=h(a),c=new r(f(l?.detail)??"Could not create session.",i.status,f(l?.code));throw this.fail(c),c}let o;try{o=x(a,this.activeInputMode,this.options.agentId)}catch(l){let c=l instanceof Error?l:new r("Could not parse the Waffle session.");throw this.fail(c),c}this.currentSession=o,this.emit("session",S(o)),g(this,"presentation",o.avatar),this.setStatus("connecting");let u,d,m=new Promise((l,c)=>{u=l,d=c});this.readyWaiter={reject:d,resolve:u,version:t};try{await this.withTimeout(Promise.all([this.realtime.connect({url:o.room,token:o.token}),m]).then(()=>{}),P,"Timed out waiting for the Waffle Agent.",()=>{this.realtime.disconnect().catch(()=>{})})}catch(l){await this.realtime.disconnect().catch(()=>{});let c=l instanceof Error?l:new r("Could not connect.");throw t!==this.lifecycleVersion?new r("The connection attempt was cancelled."):(this.fail(c),c)}finally{this.readyWaiter?.version===t&&(this.readyWaiter=null)}return this.realtime.enableMic(!0),this.setStatus("connected"),this.setAudioTrack(this.realtime.tracks().bot?.audio??null),S(o)}async stopInternal(){if(this.lifecycleVersion+=1,this.readyWaiter?.reject(new r("The connection attempt was cancelled.")),this.readyWaiter=null,this.requestAbort?.abort(),this.turnId=null,this.stopAudio(),this.currentStatus!=="disconnected"){this.setStatus("disconnecting");try{await this.realtime.disconnect()}finally{this.emit("agentSpeaking",!1),this.setStatus("disconnected")}}}sendInputControl(e,t){if(this.activeInputMode!=="push-to-talk")throw new r('Push-to-talk controls require inputMode: "push-to-talk".');this.assertConnected(),this.realtime.sendClientMessage("voice.input_control",{mode:"push_to_talk",state:e,turnId:t})}assertConnected(){if(this.status!=="connected")throw new r("Start the Waffle session before sending input.")}handleTransportState(e){e==="ready"?this.handleReady():e==="disconnecting"?this.setStatus("disconnecting"):e==="disconnected"?this.handleDisconnected():this.currentSession&&this.currentStatus!=="connected"&&this.setStatus("connecting")}handleReady(){this.currentSession&&this.readyWaiter?.resolve()}handleDisconnected(){this.readyWaiter?.reject(new r("The realtime session disconnected before it was ready.")),this.readyWaiter=null,this.stopAudio(),this.resetSpeechProgress(),this.speechFactsBlocked=!1,this.emit("agentSpeaking",!1),this.setStatus("disconnected")}handleSpeechSegment(e){this.speechFactsBlocked||this.speechSegmentsByProgressId.has(e.progressId)||(this.speechSegmentsByProgressId.set(e.progressId,e),this.pendingSpeechPlayoutStarts.has(e.progressId)&&this.releaseSpeechSegment(e.progressId,!0))}handleSpeechPlayoutStart(e){this.speechFactsBlocked||this.releasedSpeechSegments.has(e)||(this.pendingSpeechPlayoutStarts.add(e),this.releaseSpeechSegment(e,!0))}handleBotOutput(e){if(this.speechFactsBlocked)return;let t=B(e);if(!t)return;let n=E(t),i=this.seenSpeechProgress.get(t.progressId)??new Set;if(i.has(n))return;i.add(n),this.seenSpeechProgress.set(t.progressId,i);let a=this.speechSegmentsByProgressId.get(t.progressId);if(a&&this.releasedSpeechSegments.has(t.progressId)){this.emitSpeechProgress(a,t);return}let o=this.pendingSpeechProgress.get(t.progressId)??[];o.push(t),this.pendingSpeechProgress.set(t.progressId,o),this.pendingSpeechPlayoutStarts.has(t.progressId)&&this.releaseSpeechSegment(t.progressId,!0)}handleBotStartedSpeaking(){this.speechFactsBlocked||(this.botAudioActive=!0,this.emit("agentSpeaking",!0))}handleBotStoppedSpeaking(){this.speechFactsBlocked||(this.botAudioActive=!1,this.emit("agentSpeaking",!1))}releaseSpeechSegment(e,t=!1){if(this.releasedSpeechSegments.has(e))return!1;let n=this.speechSegmentsByProgressId.get(e),i=this.pendingSpeechProgress.get(e)??[];if(!n||!i.some(o=>o.status==="new"))return!1;t&&!this.botAudioActive&&(this.botAudioActive=!0,this.emit("agentSpeaking",!0)),this.releasedSpeechSegments.add(e),this.pendingSpeechPlayoutStarts.delete(e),this.emit("speechSegment",n),this.pendingSpeechProgress.delete(e);let a=i.find(o=>o.status==="new");a&&this.emitSpeechProgress(n,a);for(let o of i)o!==a&&this.emitSpeechProgress(n,o);return!0}emitSpeechProgress(e,t){this.emit("speechProgress",{segmentId:e.id,messageId:e.messageId,sequence:e.sequence,progressId:t.progressId,status:t.status,accumulatedText:t.accumulatedText,remainingText:t.remainingText})}resetSpeechProgress(){this.speechSegmentsByProgressId.clear(),this.pendingSpeechProgress.clear(),this.seenSpeechProgress.clear(),this.releasedSpeechSegments.clear(),this.pendingSpeechPlayoutStarts.clear(),this.botAudioActive=!1}interruptSpeech(e=!0){this.resetSpeechProgress(),this.speechFactsBlocked=e,this.emit("interrupted",void 0)}handleSpeechPlayoutCutoff(){let e=this.speechFactsBlocked;this.resetSpeechProgress(),this.speechFactsBlocked=!1,this.speechPlayoutCutoffVersion+=1,e||this.emit("interrupted",void 0)}setAudioTrack(e){if(this.currentRemoteAudioTrack=e,this.emit("remoteAudioTrack",e),g(this,"audioTrack",e),this.stopAudio(!1),!e||typeof document>"u")return;let t=document.createElement("audio");t.autoplay=!0,t.setAttribute("playsinline",""),t.srcObject=new MediaStream([e]),this.audio=t,t.play().catch(n=>{this.emit("error",n instanceof Error?n:new r("Browser blocked agent audio."))})}stopAudio(e=!0){this.audio&&(this.audio.pause(),this.audio.srcObject=null,this.audio=null),e&&this.currentRemoteAudioTrack&&(this.currentRemoteAudioTrack=null,this.emit("remoteAudioTrack",null),g(this,"audioTrack",null))}setStatus(e){e==="disconnected"&&(this.recreateRealtimeOnConnect=!0,this.resetSpeechProgress(),this.speechFactsBlocked=!1),e==="disconnected"&&this.currentSession&&(this.currentSession=null,this.emit("session",null),g(this,"presentation",null)),this.currentStatus!==e&&(this.currentStatus=e,this.emit("connectionState",e),this.emit("status",e))}fail(e){this.readyWaiter?.reject(e),this.readyWaiter=null,this.setStatus("disconnected"),this.emit("error",e)}async resolveSessionToken(){if(!this.options.sessionToken)throw new r("No session token provider is configured.");let e=await this.options.sessionToken();if(!e.trim())throw new r("sessionToken must not be empty.",void 0,"invalid_session_token");return e}withTimeout(e,t,n,i){return new Promise((a,o)=>{let u=setTimeout(()=>{i?.(),o(new r(n))},t);e.then(d=>{clearTimeout(u),a(d)},d=>{clearTimeout(u),o(d)})})}emit(e,t){this.listeners.get(e)?.forEach(n=>{n(t)})}};export{y as WaffleClient,r as WaffleError};
|
package/dist/internal.d.ts
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
interface InternalAvatarPerformance {
|
|
2
|
-
phase: 'emotion' | 'vocal-action';
|
|
3
|
-
tag: string;
|
|
4
|
-
}
|
|
5
1
|
interface InternalAvatarPresentation {
|
|
6
2
|
id: string;
|
|
7
3
|
name: string;
|
|
@@ -11,25 +7,11 @@ interface InternalAvatarPresentation {
|
|
|
11
7
|
runtimeProfile: string;
|
|
12
8
|
}
|
|
13
9
|
interface InternalWaffleEvents {
|
|
14
|
-
/**
|
|
15
|
-
* One performance cue, delivered by the runtime at the exact playout moment
|
|
16
|
-
* it should be performed (the server schedules it on the audio clock, with
|
|
17
|
-
* the emotion pre-entry lead already applied).
|
|
18
|
-
*/
|
|
19
|
-
avatarPerformance: InternalAvatarPerformance;
|
|
20
10
|
audioTrack: MediaStreamTrack | null;
|
|
21
11
|
presentation: InternalAvatarPresentation | null;
|
|
22
|
-
/**
|
|
23
|
-
* One spoken word at the moment it becomes audible. Present only when the
|
|
24
|
-
* runtime's TTS provider supplies word timestamps; drives client-side
|
|
25
|
-
* performance choreography.
|
|
26
|
-
*/
|
|
27
|
-
spokenWord: {
|
|
28
|
-
text: string;
|
|
29
|
-
};
|
|
30
12
|
}
|
|
31
13
|
type InternalListener<K extends keyof InternalWaffleEvents> = (payload: InternalWaffleEvents[K]) => void;
|
|
32
14
|
declare function subscribeInternal<K extends keyof InternalWaffleEvents>(owner: object, event: K, listener: InternalListener<K>): () => void;
|
|
33
15
|
declare function emitInternal<K extends keyof InternalWaffleEvents>(owner: object, event: K, payload: InternalWaffleEvents[K]): void;
|
|
34
16
|
|
|
35
|
-
export { type
|
|
17
|
+
export { type InternalAvatarPresentation, type InternalWaffleEvents, emitInternal, subscribeInternal };
|
package/dist/internal.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{a,b}from"./chunk-
|
|
1
|
+
import{a,b}from"./chunk-M7Z7AFVA.js";export{b as emitInternal,a as subscribeInternal};
|
package/package.json
CHANGED
package/dist/chunk-LJ4KJFLQ.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
var o=new WeakMap;function s(n,t,a){let e=o.get(n)??new Map,r=e.get(t)??new Set;return r.add(a),e.set(t,r),o.set(n,e),()=>r.delete(a)}function l(n,t,a){o.get(n)?.get(t)?.forEach(e=>{e(a)})}export{s as a,l as b};
|