@animated-waffle/client 0.2.6 → 0.3.1
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/CHANGELOG.md +15 -0
- package/README.md +38 -7
- package/dist/index.d.ts +34 -22
- package/dist/index.js +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.3.1
|
|
4
|
+
|
|
5
|
+
- Pass SDK-owned request/session identities to managed starters and optional
|
|
6
|
+
end-user context through the default token start.
|
|
7
|
+
|
|
8
|
+
## 0.3.0
|
|
9
|
+
|
|
10
|
+
- Require managed Agent session protocol v2 and connect to LiveKit WebRTC.
|
|
11
|
+
- Add ephemeral `transcriptProgress` captions while keeping `transcript` as completed persisted messages.
|
|
12
|
+
- Preserve the public connection, text, push-to-talk, audio-track, and reconnect lifecycle without transport-specific types.
|
|
13
|
+
- Breaking: remove `openaiModel` and `openaiVoice` from managed `WaffleSessionOverrides`; the managed SDK is provider-neutral. The separate Live Conversation OpenAI Realtime implementation is unchanged.
|
|
14
|
+
|
|
15
|
+
0.3.0 must be published before a managed backend is cut over to protocol v2.
|
package/README.md
CHANGED
|
@@ -6,7 +6,18 @@ transcripts. React applications should use
|
|
|
6
6
|
[`@animated-waffle/react`](../waffle-react), which re-exports this entire API
|
|
7
7
|
and adds the Avatar renderer.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Install the current npm release without copying a mutable registry version:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pnpm add @animated-waffle/client@latest
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The managed-session identity and `sessionContext` APIs described here were
|
|
16
|
+
introduced in version 0.3.1. The workspace [`package.json`](./package.json) is
|
|
17
|
+
the source of truth for this repository's version; `@latest` resolves the
|
|
18
|
+
current npm release.
|
|
19
|
+
|
|
20
|
+
The Agent owns its avatar, voice, and prompt. Media runtime, API routes, and
|
|
10
21
|
avatar asset URLs are SDK implementation details. Production is the default
|
|
11
22
|
API origin; tests and non-production integrations may pass `apiOrigin`.
|
|
12
23
|
|
|
@@ -25,6 +36,10 @@ const waffle = new WaffleClient({
|
|
|
25
36
|
agentId,
|
|
26
37
|
// Set false for text-triggered, output-only applications.
|
|
27
38
|
inputAudioEnabled: false,
|
|
39
|
+
sessionContext: {
|
|
40
|
+
userName: ' Space é ',
|
|
41
|
+
timeZone: 'Asia/Shanghai',
|
|
42
|
+
},
|
|
28
43
|
sessionToken: async () => {
|
|
29
44
|
const response = await fetch('/api/narya-session-token', {
|
|
30
45
|
method: 'POST',
|
|
@@ -40,6 +55,9 @@ waffle.on('transcript', ({ id, role, text, final }) => {
|
|
|
40
55
|
// Completed Narya conversation rows use the same id as persisted history.
|
|
41
56
|
if (final) console.log(id, role, text)
|
|
42
57
|
})
|
|
58
|
+
waffle.on('transcriptProgress', ({ segmentId, role, text, complete }) => {
|
|
59
|
+
// Ephemeral ASR/playout caption; never persist segmentId as a message id.
|
|
60
|
+
})
|
|
43
61
|
|
|
44
62
|
await waffle.connect()
|
|
45
63
|
```
|
|
@@ -59,7 +77,13 @@ const waffle = new WaffleClient({
|
|
|
59
77
|
|
|
60
78
|
## API surface
|
|
61
79
|
|
|
62
|
-
- `
|
|
80
|
+
- `sessionContext?: { userName?: string; timeZone?: string }` — available only
|
|
81
|
+
with the default `sessionToken` starter. Nonblank values are sent exactly as
|
|
82
|
+
provided; blank values are omitted. It is mutually exclusive with the
|
|
83
|
+
advanced `startSession` callback, so token-path consumers do not recreate
|
|
84
|
+
the raw managed-session request.
|
|
85
|
+
- `connect({ inputMode?, sessionId?, overrides? })` — `'push-to-talk'`
|
|
86
|
+
(default) or `'automatic'`; resolves only when the Agent runtime is ready.
|
|
63
87
|
- `inputAudioEnabled: false` — never requests or enables the browser microphone;
|
|
64
88
|
text input, Agent audio, transcripts, and reconnects continue to work. Push-
|
|
65
89
|
to-talk controls fail closed with `input_audio_disabled`. The default is
|
|
@@ -70,13 +94,20 @@ const waffle = new WaffleClient({
|
|
|
70
94
|
`remoteAudioTrack` yourself; an application that routes the voice through its
|
|
71
95
|
own element, mixer or Web Audio graph must turn this off, or the voice is
|
|
72
96
|
heard twice. `muted` then has nothing to do.
|
|
73
|
-
- `disconnect()`, `sendText(text)`.
|
|
97
|
+
- `disconnect()`, `sendText(text, { audioResponse? })`. `audioResponse` defaults
|
|
98
|
+
to `true`; `false` keeps the typed turn and transcript but suppresses reply
|
|
99
|
+
audio for that turn.
|
|
74
100
|
- `startPushToTalk()` / `commitPushToTalk()` / `cancelPushToTalk()`.
|
|
75
|
-
- `on('
|
|
101
|
+
- `on('connectionState' | 'session' | 'remoteAudioTrack' | 'transcriptProgress' | 'transcript' |
|
|
102
|
+
'agentSpeaking' | 'error', fn)` —
|
|
76
103
|
returns the unsubscribe function.
|
|
77
|
-
- Conversation transcripts carry the same required
|
|
78
|
-
|
|
79
|
-
|
|
104
|
+
- Conversation transcripts are completed persisted rows, carry the same required
|
|
105
|
+
`id` as history, and currently always have `final: true`. Provider
|
|
106
|
+
sentence/progress framing never defines a public conversation row.
|
|
107
|
+
- Transcript progress is ephemeral and keyed by a runtime `segmentId`; user ASR
|
|
108
|
+
and assistant playout update it incrementally until `complete` closes it.
|
|
109
|
+
- `connectionState`, `session`, `remoteAudioTrack` getters; `status` is the
|
|
110
|
+
deprecated 0.1 alias. Failures throw `WaffleError` with `code`.
|
|
80
111
|
|
|
81
112
|
`./internal` is a private contract consumed by `@animated-waffle/react`; it is
|
|
82
113
|
not part of the public API and may change without notice.
|
package/dist/index.d.ts
CHANGED
|
@@ -13,14 +13,19 @@ interface WaffleTranscript {
|
|
|
13
13
|
id: string;
|
|
14
14
|
role: 'user' | 'assistant';
|
|
15
15
|
text: string;
|
|
16
|
-
/**
|
|
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.
|
|
20
|
-
*/
|
|
16
|
+
/** Currently always true: transcript events are completed persisted rows. */
|
|
21
17
|
final: boolean;
|
|
22
18
|
timestamp?: string;
|
|
23
19
|
}
|
|
20
|
+
/** Session-scoped caption progress. It is never a persisted conversation row. */
|
|
21
|
+
interface WaffleTranscriptProgress {
|
|
22
|
+
/** Runtime-owned segment identity; not a database message id. */
|
|
23
|
+
segmentId: string;
|
|
24
|
+
role: 'user' | 'assistant';
|
|
25
|
+
text: string;
|
|
26
|
+
/** The runtime closed this ephemeral segment. */
|
|
27
|
+
complete: boolean;
|
|
28
|
+
}
|
|
24
29
|
interface WaffleClientEvents {
|
|
25
30
|
connectionState: WaffleConnectionState;
|
|
26
31
|
/** @deprecated Subscribe to connectionState. */
|
|
@@ -29,6 +34,7 @@ interface WaffleClientEvents {
|
|
|
29
34
|
error: Error;
|
|
30
35
|
agentSpeaking: boolean;
|
|
31
36
|
remoteAudioTrack: MediaStreamTrack | null;
|
|
37
|
+
transcriptProgress: WaffleTranscriptProgress;
|
|
32
38
|
transcript: WaffleTranscript;
|
|
33
39
|
}
|
|
34
40
|
type WaffleSessionToken = () => string | Promise<string>;
|
|
@@ -49,8 +55,6 @@ interface WaffleSessionOverrides {
|
|
|
49
55
|
defaultDeviceProfileId?: string | null;
|
|
50
56
|
avatarId?: string;
|
|
51
57
|
avatarEnabled?: boolean;
|
|
52
|
-
openaiModel?: string | null;
|
|
53
|
-
openaiVoice?: string;
|
|
54
58
|
}
|
|
55
59
|
interface WaffleClientBaseOptions {
|
|
56
60
|
/** The published Agent to connect to. */
|
|
@@ -68,6 +72,11 @@ interface WaffleClientBaseOptions {
|
|
|
68
72
|
interface WaffleTokenClientOptions extends WaffleClientBaseOptions {
|
|
69
73
|
/** A short-lived token minted for this Agent by the Narya API. */
|
|
70
74
|
sessionToken: WaffleSessionToken;
|
|
75
|
+
/** Optional end-user context for the default managed-session request. */
|
|
76
|
+
sessionContext?: {
|
|
77
|
+
readonly userName?: string;
|
|
78
|
+
readonly timeZone?: string;
|
|
79
|
+
};
|
|
71
80
|
/** Override the Narya API origin for a non-production environment. */
|
|
72
81
|
apiOrigin?: string;
|
|
73
82
|
startSession?: never;
|
|
@@ -76,18 +85,15 @@ interface WaffleManagedClientOptions extends WaffleClientBaseOptions {
|
|
|
76
85
|
/** Start a session through the embedding application's authenticated backend. */
|
|
77
86
|
startSession: WaffleSessionStarter;
|
|
78
87
|
sessionToken?: never;
|
|
88
|
+
sessionContext?: never;
|
|
79
89
|
apiOrigin?: never;
|
|
80
90
|
}
|
|
81
91
|
type WaffleClientOptions = WaffleTokenClientOptions | WaffleManagedClientOptions;
|
|
82
92
|
interface WaffleSendTextOptions {
|
|
83
93
|
/**
|
|
84
|
-
* Whether the Agent speaks its reply.
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
* false makes one turn text-only: the reply still streams as transcript and
|
|
88
|
-
* still lands in the conversation context, but no audio is synthesized —
|
|
89
|
-
* what a typed exchange inside an otherwise spoken session wants, and what a
|
|
90
|
-
* message thread wants for every turn.
|
|
94
|
+
* Whether the Agent speaks its reply. Defaults to true. When false, the
|
|
95
|
+
* reply still streams as transcript and is persisted in the conversation,
|
|
96
|
+
* but no audio is synthesized.
|
|
91
97
|
*/
|
|
92
98
|
readonly audioResponse?: boolean;
|
|
93
99
|
}
|
|
@@ -102,7 +108,8 @@ type WaffleConnectOptions = WaffleSessionStartOptions;
|
|
|
102
108
|
interface WaffleSessionStartInput {
|
|
103
109
|
agentId: string;
|
|
104
110
|
inputMode: WaffleInputMode;
|
|
105
|
-
|
|
111
|
+
requestId: string;
|
|
112
|
+
sessionId: string;
|
|
106
113
|
overrides?: Readonly<WaffleSessionOverrides>;
|
|
107
114
|
signal: AbortSignal;
|
|
108
115
|
}
|
|
@@ -135,8 +142,7 @@ declare class WaffleError extends Error {
|
|
|
135
142
|
constructor(message: string, status?: number | undefined, code?: string | undefined);
|
|
136
143
|
}
|
|
137
144
|
declare class WaffleClient implements WaffleSessionClient {
|
|
138
|
-
private
|
|
139
|
-
private readonly realtimeCallbacks;
|
|
145
|
+
private room;
|
|
140
146
|
private readonly listeners;
|
|
141
147
|
private readonly options;
|
|
142
148
|
private readonly apiOrigin;
|
|
@@ -150,12 +156,17 @@ declare class WaffleClient implements WaffleSessionClient {
|
|
|
150
156
|
private audioMuted;
|
|
151
157
|
private currentRemoteAudioTrack;
|
|
152
158
|
private activeInputMode;
|
|
159
|
+
private agentIdentity;
|
|
160
|
+
private readonly progressExecutions;
|
|
161
|
+
private readonly progressText;
|
|
162
|
+
private readonly completedProgressExecutions;
|
|
153
163
|
private turnId;
|
|
154
164
|
private nextTurnId;
|
|
165
|
+
private turnControl;
|
|
155
166
|
private lifecycleVersion;
|
|
156
|
-
private
|
|
167
|
+
private recreateRoomOnConnect;
|
|
157
168
|
constructor(options: WaffleClientOptions);
|
|
158
|
-
private
|
|
169
|
+
private createRoom;
|
|
159
170
|
get status(): WaffleStatus;
|
|
160
171
|
get connectionState(): WaffleConnectionState;
|
|
161
172
|
get session(): WaffleSession | null;
|
|
@@ -176,11 +187,12 @@ declare class WaffleClient implements WaffleSessionClient {
|
|
|
176
187
|
private sendInputControl;
|
|
177
188
|
private assertPushToTalkAvailable;
|
|
178
189
|
private assertConnected;
|
|
179
|
-
private
|
|
180
|
-
private handleReady;
|
|
190
|
+
private handleParticipant;
|
|
181
191
|
private handleDisconnected;
|
|
182
192
|
private setAudioTrack;
|
|
183
193
|
private stopAudio;
|
|
194
|
+
private completeTranscriptExecution;
|
|
195
|
+
private clearTranscriptProgress;
|
|
184
196
|
private setStatus;
|
|
185
197
|
private fail;
|
|
186
198
|
private resolveSessionToken;
|
|
@@ -188,4 +200,4 @@ declare class WaffleClient implements WaffleSessionClient {
|
|
|
188
200
|
private emit;
|
|
189
201
|
}
|
|
190
202
|
|
|
191
|
-
export { WaffleClient, type WaffleClientEvents, type WaffleClientOptions, type WaffleConnectOptions, type WaffleConnectionState, WaffleError, type WaffleInputMode, type WaffleSendTextOptions, type WaffleSession, type WaffleSessionClient, type WaffleSessionOverrides, type WaffleSessionStartInput, type WaffleSessionStartOptions, type WaffleSessionStarter, type WaffleSessionToken, type WaffleStatus, type WaffleTranscript };
|
|
203
|
+
export { WaffleClient, type WaffleClientEvents, type WaffleClientOptions, type WaffleConnectOptions, type WaffleConnectionState, WaffleError, type WaffleInputMode, type WaffleSendTextOptions, type WaffleSession, type WaffleSessionClient, type WaffleSessionOverrides, type WaffleSessionStartInput, type WaffleSessionStartOptions, type WaffleSessionStarter, type WaffleSessionToken, type WaffleStatus, type WaffleTranscript, type WaffleTranscriptProgress };
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{b as m}from"./chunk-BXRNROI7.js";import{PipecatClient as y}from"@pipecat-ai/client-js";import{DailyTransport as w}from"@pipecat-ai/daily-transport";var k=15e3,W=3e4,b="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,P=/^sha256:[a-f0-9]{64}$/,i=class extends Error{constructor(t,n,r){super(t);this.status=n;this.code=r;this.name="WaffleError"}status;code};function h(s){return s!==null&&typeof s=="object"&&!Array.isArray(s)?s:null}function A(s=b){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 u(s,e){if(typeof s!="string"||s.trim().length===0)throw new i(`Session response is missing ${e}.`);return s}function C(s){let e=u(s,"avatar.asset_hash");if(!P.test(e))throw new i("Session response contains an invalid Avatar asset hash.");return e}function g(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 _(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 r=h(n.connection);if(!r||r.transport!=="webrtc")throw new i("Session response is missing a WebRTC connection.");let p=u(r.url,"connection.url");try{if(new URL(p).protocol!=="https:")throw new Error}catch{throw new i("Session response contains an invalid connection URL.")}let c=u(n.agent_id,"agent_id");if(c!==t)throw new i("Session response contains a mismatched Agent id.");let d=f(n.avatar_id),l=h(n.avatar),v=null;if(d||l){if(!d||!l||u(l.id,"avatar.id")!==d)throw new i("Session response contains mismatched Avatar metadata.");let o=u(l.engine,"avatar.engine");if(o!=="vrm")throw new i("Session response contains an unsupported Avatar engine.");let a=u(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:u(l.name,"avatar.name"),engine:o,modelUrl:a,assetHash:C(l.asset_hash),runtimeProfile:u(l.runtime_profile,"avatar.runtime_profile")}}return{id:u(n.session_id,"session_id"),agentId:c,...d?{avatarId:d}:{},avatar:v,inputMode:e,room:p,token:u(r.token,"connection.token")}}function O(s){let e=h(s);if(!e||typeof e.type!="string")return null;let t={contextId:f(e.contextId)??null,segmentId:f(e.segmentId),occurrence:g(e.occurrence),audioTime:g(e.audioTime)};if(e.type==="avatar-emotion"){let n=f(e.emotion);return n?{phase:"emotion",emotion:n,...t}:null}if(e.type==="avatar-vocal-action"){let n=f(e.action);return n?{phase:"vocal-action",action:n,...t}:null}return null}function E(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 S(s){let{avatar:e,room:t,token:n,...r}=s;return r}var T=class{realtime;realtimeCallbacks;listeners=new Map;options;apiOrigin;currentStatus="disconnected";currentSession=null;startPromise=null;stopPromise=null;requestAbort=null;readyWaiter=null;audio=null;audioMuted=!1;currentRemoteAudioTrack=null;activeInputMode="push-to-talk";turnId=null;nextTurnId=0;lifecycleVersion=0;recreateRealtimeOnConnect=!1;constructor(e){if(this.options=e,this.apiOrigin=A(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),onServerMessage:t=>{let n=E(t);n&&this.emit("transcript",n);let r=O(t);r&&m(this,"avatar",r)},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 w({bufferLocalAudioUntilBotReady:!0,inputSettings:{audio:{processor:{type:this.options.inputAudioEnabled===!1?"none":"noise-cancellation"}}}});return new y({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}get muted(){return this.audioMuted}set muted(e){this.audioMuted=e,this.audio&&(this.audio.muted=e)}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,t){e.trim()&&(this.assertConnected(),await this.realtime.sendText(e,{audio_response:t?.audioResponse??!0,run_immediately:!0}))}startPushToTalk(){if(this.assertPushToTalkAvailable(),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.assertPushToTalkAvailable(),this.turnId!==null&&(this.sendInputControl("stop",this.turnId),this.turnId=null)}stopPushToTalk(){this.commitPushToTalk()}cancelPushToTalk(){this.assertPushToTalkAvailable(),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 r;try{r=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(o=>fetch(`${this.apiOrigin}/v1/agents/${encodeURIComponent(this.options.agentId)}/sessions`,{method:"POST",headers:{Accept:"application/json","Content-Type":"application/json",Authorization:`Bearer ${o}`},signal:n.signal,body:JSON.stringify({input_mode:this.activeInputMode==="push-to-talk"?"push_to_talk":"automatic"})})),k,"Timed out creating the Waffle session.",()=>n.abort())}catch(o){let a=o instanceof Error?o: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 p=await r.json().catch(()=>null);if(!r.ok){let o=h(p),a=new i(f(o?.detail)??"Could not create session.",r.status,f(o?.code));throw this.fail(a),a}let c;try{c=_(p,this.activeInputMode,this.options.agentId)}catch(o){let a=o instanceof Error?o:new i("Could not parse the Waffle session.");throw this.fail(a),a}this.currentSession=c,this.emit("session",S(c)),m(this,"presentation",c.avatar),this.setStatus("connecting");let d,l,v=new Promise((o,a)=>{d=o,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(()=>{}),W,"Timed out waiting for the Waffle Agent.",()=>{this.realtime.disconnect().catch(()=>{})})}catch(o){await this.realtime.disconnect().catch(()=>{});let a=o instanceof Error?o: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.options.inputAudioEnabled!==!1&&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})}assertPushToTalkAvailable(){if(this.options.inputAudioEnabled===!1)throw new i("Push-to-talk requires input audio to be enabled.",void 0,"input_audio_disabled")}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),m(this,"audioTrack",e),this.stopAudio(!1),!e||typeof document>"u"||this.options.audioPlayback===!1)return;let t=document.createElement("audio");t.autoplay=!0,t.muted=this.audioMuted,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),m(this,"audioTrack",null))}setStatus(e){e==="disconnected"&&(this.recreateRealtimeOnConnect=!0),e==="disconnected"&&this.currentSession&&(this.currentSession=null,this.emit("session",null),m(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,r){return new Promise((p,c)=>{let d=setTimeout(()=>{r?.(),c(new i(n))},t);e.then(l=>{clearTimeout(d),p(l)},l=>{clearTimeout(d),c(l)})})}emit(e,t){this.listeners.get(e)?.forEach(n=>{n(t)})}};export{T as WaffleClient,i as WaffleError};
|
|
1
|
+
import{b as k}from"./chunk-BXRNROI7.js";import{DataPacket_Kind as C,ParticipantKind as y,Room as R,RoomEvent as S,Track as A}from"livekit-client";var O=15e3,M=3e4,N="https://animated-waffle.narya.ai",U=/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,D=/^sha256:[a-f0-9]{64}$/,i=class extends Error{constructor(e,n,s){super(e);this.status=n;this.code=s;this.name="WaffleError"}status;code};function P(o){return o!==null&&typeof o=="object"&&!Array.isArray(o)?o:null}function K(o=N){let t;try{t=new URL(o)}catch{throw new i("apiOrigin must be a valid URL.",void 0,"invalid_api_origin")}let e=t.hostname==="localhost"||t.hostname==="127.0.0.1"||t.hostname==="[::1]";if(t.protocol!=="https:"&&!(t.protocol==="http:"&&e)||t.username||t.password||t.pathname!=="/"&&t.pathname!==""||t.search||t.hash)throw new i("apiOrigin must be an HTTPS origin or an HTTP loopback origin.",void 0,"invalid_api_origin");return t.origin}function T(o,t){if(typeof o!="string"||o.trim().length===0)throw new i(`Session response is missing ${t}.`);return o}function L(o){let t=T(o,"avatar.asset_hash");if(!D.test(t))throw new i("Session response contains an invalid Avatar asset hash.");return t}function E(o){return typeof o=="number"&&Number.isFinite(o)?o:void 0}function g(o){return typeof o=="string"&&o.length>0?o:void 0}function x(o,t){let e=g(o[`${t}.speech_id`]),n=Number(o[`${t}.speech_step_index`]);return e&&Number.isSafeInteger(n)&&n>0?JSON.stringify([e,n]):null}function j(o,t,e){let n=P(o);if(!n)throw new i("Session response must be a JSON object.");if(n.protocol_version!==2)throw new i("Session response has an unsupported protocol version.");let s=P(n.connection);if(!s||s.transport!=="webrtc")throw new i("Session response is missing a WebRTC connection.");let c=T(s.url,"connection.url");try{let h=new URL(c),m=h.hostname==="localhost"||h.hostname==="127.0.0.1"||h.hostname==="[::1]";if(h.protocol!=="wss:"&&!(h.protocol==="ws:"&&m))throw new Error}catch{throw new i("Session response contains an invalid connection URL.")}let a=T(n.agent_id,"agent_id");if(a!==e)throw new i("Session response contains a mismatched Agent id.");let l=g(n.avatar_id),d=P(n.avatar),p=null;if(l||d){if(!l||!d||T(d.id,"avatar.id")!==l)throw new i("Session response contains mismatched Avatar metadata.");let h=T(d.engine,"avatar.engine");if(h!=="vrm")throw new i("Session response contains an unsupported Avatar engine.");let m=T(d.model_url,"avatar.model_url");try{let u=new URL(m),v=u.hostname==="localhost"||u.hostname==="127.0.0.1"||u.hostname==="[::1]";if(u.protocol!=="https:"&&!(u.protocol==="http:"&&v))throw new Error}catch{throw new i("Session response contains an invalid Avatar model URL.")}p={id:l,name:T(d.name,"avatar.name"),engine:h,modelUrl:m,assetHash:L(d.asset_hash),runtimeProfile:T(d.runtime_profile,"avatar.runtime_profile")}}return{id:T(n.session_id,"session_id"),agentId:a,...l?{avatarId:l}:{},avatar:p,inputMode:t,room:c,token:T(s.token,"connection.token")}}function q(o){let t=P(o);if(!t||typeof t.type!="string")return null;let e={contextId:g(t.contextId)??null,segmentId:g(t.segmentId),occurrence:E(t.occurrence),audioTime:E(t.audioTime)};if(t.type==="avatar-emotion"){let n=g(t.emotion);return n?{phase:"emotion",emotion:n,...e}:null}if(t.type==="avatar-vocal-action"){let n=g(t.action);return n?{phase:"vocal-action",action:n,...e}:null}return null}function W(o){let{avatar:t,room:e,token:n,...s}=o;return s}var _=class{room;listeners=new Map;options;apiOrigin;currentStatus="disconnected";currentSession=null;startPromise=null;stopPromise=null;requestAbort=null;readyWaiter=null;audio=null;audioMuted=!1;currentRemoteAudioTrack=null;activeInputMode="push-to-talk";agentIdentity=null;progressExecutions=new Map;progressText=new Map;completedProgressExecutions=new Set;turnId=null;nextTurnId=0;turnControl=Promise.resolve();lifecycleVersion=0;recreateRoomOnConnect=!1;constructor(t){if(this.options=t,this.apiOrigin=K(t.apiOrigin),!U.test(t.agentId))throw new i("agentId must be a UUID.",void 0,"invalid_agent_id");if(!t.startSession&&typeof t.sessionToken!="function")throw new i("sessionToken must be a function.",void 0,"invalid_session_token");this.room=this.createRoom()}createRoom(){let t=new R({adaptiveStream:!0,dynacast:!0,audioCaptureDefaults:{voiceIsolation:!1}});return t.on(S.ParticipantConnected,e=>{t===this.room&&this.handleParticipant(e)}),t.on(S.ParticipantDisconnected,e=>{t!==this.room||e.kind!==y.AGENT||(this.agentIdentity=null,this.currentStatus!=="disconnecting"&&(this.handleDisconnected("The Waffle Agent disconnected."),t.disconnect().catch(n=>{this.emit("error",n instanceof Error?n:new i("Could not close the Agent room."))})))}),t.on(S.ParticipantAttributesChanged,(e,n)=>{t===this.room&&this.handleParticipant(n)}),t.on(S.TrackSubscribed,(e,n,s)=>{t===this.room&&s.kind===y.AGENT&&e.kind===A.Kind.Audio&&this.setAudioTrack(e.mediaStreamTrack)}),t.on(S.TrackUnsubscribed,(e,n,s)=>{t===this.room&&s.kind===y.AGENT&&e.kind===A.Kind.Audio&&this.setAudioTrack(null)}),t.on(S.DataReceived,(e,n,s,c)=>{if(!(t!==this.room||n?.kind!==y.AGENT||s!==C.RELIABLE||c!=="avatar.performance"))try{let a=q(JSON.parse(new TextDecoder().decode(e)));a&&k(this,"avatar",a)}catch{}}),t.on(S.Disconnected,()=>{t===this.room&&this.handleDisconnected("The Agent session disconnected before it was ready.")}),t.registerTextStreamHandler("lk.transcription",(e,n)=>{(async()=>{let s=this.agentIdentity?t.remoteParticipants.get(this.agentIdentity):void 0;if(t!==this.room||this.currentStatus!=="connected"||s?.kind!==y.AGENT)return;let c=e.info.attributes??{},a=g(c["lk.segment_id"]),l=g(c["lk.transcribed_track_id"]),d=c["lk.transcription_final"];if(!a||!l||d!=="true"&&d!=="false")return;let p=t.localParticipant.audioTrackPublications.has(l),h=s.audioTrackPublications.has(l);if(p===h)return;let m=p?"user":"assistant";if(n.identity!==(p?t.localParticipant.identity:s.identity))return;let u=m==="assistant"?x(c,"lk"):null;if(m==="assistant"&&!u||u&&(this.progressExecutions.set(a,u),this.completedProgressExecutions.has(u)))return;let v="";for await(let I of e){if(v+=I,t!==this.room||this.currentStatus!=="connected"||u&&this.completedProgressExecutions.has(u))return;if(v.length>0){let b={segmentId:a,role:m,text:v,complete:!1};this.progressText.set(a,b),this.emit("transcriptProgress",b)}}d==="true"&&v.length>0&&(this.emit("transcriptProgress",{segmentId:a,role:m,text:v,complete:!0}),this.progressExecutions.delete(a),this.progressText.delete(a))})().catch(s=>{this.emit("error",s instanceof Error?s:new i("Transcript stream failed."))})}),t.registerTextStreamHandler("animated-waffle.transcript",(e,n)=>{e.readAll().then(s=>{if(t!==this.room||n.identity!==this.agentIdentity||!s)return;let c=e.info.attributes??{},a=g(c["animated-waffle.message_id"]),l=c["animated-waffle.role"];if(!a||l!=="user"&&l!=="assistant"||c["animated-waffle.final"]!=="true")return;let d=l==="assistant"?x(c,"animated-waffle"):null;d&&this.completeTranscriptExecution(d);let p=Number(e.info.timestamp);this.emit("transcript",{id:a,role:l,text:s,final:!0,...Number.isFinite(p)&&p>0?{timestamp:new Date(p).toISOString()}:{}})}).catch(s=>{this.emit("error",s instanceof Error?s:new i("Transcript stream failed."))})}),t}get status(){return this.currentStatus}get connectionState(){return this.currentStatus}get session(){return this.currentSession?W(this.currentSession):null}get remoteAudioTrack(){return this.currentRemoteAudioTrack}get muted(){return this.audioMuted}set muted(t){this.audioMuted=t,this.audio&&(this.audio.muted=t)}on(t,e){let n=this.listeners.get(t)??new Set;return n.add(e),this.listeners.set(t,n),()=>n.delete(e)}connect(t={}){if(this.currentSession&&this.status==="connected")return Promise.resolve(W(this.currentSession));if(this.startPromise)return this.startPromise;this.recreateRoomOnConnect&&(this.room=this.createRoom(),this.recreateRoomOnConnect=!1);let e=++this.lifecycleVersion,n=this.startInternal(t,e).finally(()=>{this.startPromise===n&&(this.startPromise=null)});return this.startPromise=n,n}disconnect(){if(this.stopPromise)return this.stopPromise;let t=this.stopInternal().finally(()=>{this.stopPromise===t&&(this.stopPromise=null)});return this.stopPromise=t,t}async sendText(t,e){if(!t.trim())return;this.assertConnected();let n=e?.audioResponse===!1?{"animated-waffle.audio_response":"false"}:void 0;await this.room.localParticipant.sendText(t,{topic:"lk.chat",...n?{attributes:n}:{}})}startPushToTalk(){if(this.assertPushToTalkAvailable(),this.turnId===null){this.nextTurnId+=1,this.turnId=this.nextTurnId;try{this.sendInputControl("start_turn")}catch(t){throw this.turnId=null,t}}}commitPushToTalk(){this.assertPushToTalkAvailable(),this.turnId!==null&&(this.sendInputControl("end_turn"),this.turnId=null)}stopPushToTalk(){this.commitPushToTalk()}cancelPushToTalk(){this.assertPushToTalkAvailable(),this.turnId!==null&&(this.sendInputControl("cancel_turn"),this.turnId=null)}async startInternal(t,e){this.activeInputMode=t.inputMode??"push-to-talk",this.setStatus("connecting");let n=new AbortController;this.requestAbort=n;let s=t.sessionId??crypto.randomUUID(),c=crypto.randomUUID(),a=this.options.startSession,l=a?void 0:this.options.sessionContext,d=a?void 0:JSON.stringify({chat_session_id:s,input_mode:this.activeInputMode==="push-to-talk"?"push_to_talk":"automatic",...t.overrides?{overrides:t.overrides}:{},...l?.userName?.trim()?{user_name:l.userName}:{},...l?.timeZone?.trim()?{time_zone:l.timeZone}:{}}),p=()=>this.options.startSession?Promise.resolve(this.options.startSession({agentId:this.options.agentId,inputMode:this.activeInputMode,requestId:c,sessionId:s,...t.overrides?{overrides:t.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,"Idempotency-Key":c},signal:n.signal,body:d})),h;try{h=await this.withTimeout(a?p():p().catch(r=>{if(n.signal.aborted)throw r;return p()}),O,"Timed out creating the Waffle session.",()=>n.abort())}catch(r){let f=r instanceof Error?r:new i("Could not create session.");throw e!==this.lifecycleVersion?new i("The connection attempt was cancelled."):(this.fail(f),f)}finally{this.requestAbort===n&&(this.requestAbort=null)}if(e!==this.lifecycleVersion)throw new i("The connection attempt was cancelled.");let m=await h.json().catch(()=>null);if(!h.ok){let r=P(m),f=new i(g(r?.detail)??"Could not create session.",h.status,g(r?.code));throw this.fail(f),f}let u;try{u=j(m,this.activeInputMode,this.options.agentId)}catch(r){let f=r instanceof Error?r:new i("Could not parse the Waffle session.");throw this.fail(f),f}this.currentSession=u,this.emit("session",W(u)),k(this,"presentation",u.avatar),this.setStatus("connecting");let v,I,b=new Promise((r,f)=>{v=r,I=f});this.readyWaiter={reject:I,resolve:v,version:e};let w=this.room;try{if(await w.connect(u.room,u.token),await w.startAudio(),this.options.inputAudioEnabled!==!1){let r=await w.localParticipant.setMicrophoneEnabled(!0),f=r?.audioTrack?.mediaStreamTrack;if(!r||r.isMuted||r.isUpstreamPaused||!f||!f.enabled||f.muted||f.readyState!=="live")throw new i("The microphone track did not publish.")}for(let r of w.remoteParticipants.values())this.handleParticipant(r);await this.withTimeout(b,M,"Timed out waiting for the Waffle Agent.",()=>{w.disconnect().catch(()=>{})})}catch(r){this.readyWaiter?.version===e&&(this.readyWaiter=null),this.currentStatus!=="disconnected"&&await w.disconnect().catch(()=>{});let f=r instanceof Error?r:new i("Could not connect.");throw e!==this.lifecycleVersion?new i("The connection attempt was cancelled."):(this.fail(f),f)}finally{this.readyWaiter?.version===e&&(this.readyWaiter=null)}if(e!==this.lifecycleVersion)throw new i("The connection attempt was cancelled.");return this.setStatus("connected"),W(u)}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.turnControl=Promise.resolve(),this.agentIdentity=null,this.clearTranscriptProgress(),this.stopAudio(),this.currentStatus!=="disconnected"){this.setStatus("disconnecting");try{await this.room.disconnect()}finally{this.emit("agentSpeaking",!1),this.setStatus("disconnected")}}}sendInputControl(t){if(this.activeInputMode!=="push-to-talk")throw new i('Push-to-talk controls require inputMode: "push-to-talk".');this.assertConnected();let e=this.agentIdentity;if(!e)throw new i("The Waffle Agent is not ready.");let n=this.room,s=this.turnControl.then(()=>n.localParticipant.performRpc({destinationIdentity:e,method:t,payload:""}));this.turnControl=s.then(()=>{},c=>{this.emit("error",c instanceof Error?c:new i("Push-to-talk control failed."))})}assertPushToTalkAvailable(){if(this.options.inputAudioEnabled===!1)throw new i("Push-to-talk requires input audio to be enabled.",void 0,"input_audio_disabled")}assertConnected(){if(this.status!=="connected")throw new i("Start the Waffle session before sending input.")}handleParticipant(t){if(t.kind===y.AGENT){this.agentIdentity=t.identity,this.emit("agentSpeaking",t.attributes["lk.agent.state"]==="speaking"),t.attributes["animated-waffle.runtime.ready"]==="true"&&this.readyWaiter?.resolve();for(let e of t.audioTrackPublications.values()){let n=e.track;if(n?.kind===A.Kind.Audio){this.setAudioTrack(n.mediaStreamTrack);break}}}}handleDisconnected(t){this.readyWaiter?.reject(new i(t)),this.readyWaiter=null,this.agentIdentity=null,this.clearTranscriptProgress(),this.stopAudio(),this.emit("agentSpeaking",!1),this.setStatus("disconnected")}setAudioTrack(t){if(t===this.currentRemoteAudioTrack||(this.stopAudio(!1),this.currentRemoteAudioTrack=t,this.emit("remoteAudioTrack",t),k(this,"audioTrack",t),!t||typeof document>"u")||this.options.audioPlayback===!1)return;let e=document.createElement("audio");e.autoplay=!0,e.muted=this.audioMuted,e.setAttribute("playsinline",""),e.srcObject=new MediaStream([t]),this.audio=e,e.play().catch(n=>{this.emit("error",n instanceof Error?n:new i("Browser blocked agent audio."))})}stopAudio(t=!0){this.audio&&(this.audio.pause(),this.audio.srcObject=null,this.audio=null),t&&this.currentRemoteAudioTrack&&(this.currentRemoteAudioTrack=null,this.emit("remoteAudioTrack",null),k(this,"audioTrack",null))}completeTranscriptExecution(t){this.completedProgressExecutions.add(t);for(let[e,n]of this.progressText)this.progressExecutions.get(e)===t&&(this.emit("transcriptProgress",{...n,complete:!0}),this.progressExecutions.delete(e),this.progressText.delete(e))}clearTranscriptProgress(){this.progressExecutions.clear(),this.progressText.clear(),this.completedProgressExecutions.clear()}setStatus(t){t==="disconnected"&&(this.recreateRoomOnConnect=!0),t==="disconnected"&&this.currentSession&&(this.currentSession=null,this.emit("session",null),k(this,"presentation",null)),this.currentStatus!==t&&(this.currentStatus=t,this.emit("connectionState",t),this.emit("status",t))}fail(t){this.readyWaiter?.reject(t),this.readyWaiter=null,this.stopAudio(),this.emit("agentSpeaking",!1),this.setStatus("disconnected"),this.emit("error",t)}async resolveSessionToken(){if(!this.options.sessionToken)throw new i("No session token provider is configured.");let t=await this.options.sessionToken();if(!t.trim())throw new i("sessionToken must not be empty.",void 0,"invalid_session_token");return t}withTimeout(t,e,n,s){return new Promise((c,a)=>{let l=setTimeout(()=>{s?.(),a(new i(n))},e);t.then(d=>{clearTimeout(l),c(d)},d=>{clearTimeout(l),a(d)})})}emit(t,e){this.listeners.get(t)?.forEach(n=>{n(e)})}};export{_ as WaffleClient,i as WaffleError};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@animated-waffle/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Framework-independent browser SDK for Narya Animated Waffle talking Agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
15
|
"dist",
|
|
16
|
+
"CHANGELOG.md",
|
|
16
17
|
"README.md",
|
|
17
18
|
"LICENSE"
|
|
18
19
|
],
|
|
@@ -33,8 +34,7 @@
|
|
|
33
34
|
"access": "public"
|
|
34
35
|
},
|
|
35
36
|
"dependencies": {
|
|
36
|
-
"
|
|
37
|
-
"@pipecat-ai/daily-transport": "1.6.7"
|
|
37
|
+
"livekit-client": "2.21.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"tsup": "^8.5.0",
|