@nuancelabs/js-sdk 0.0.0-stage → 0.1.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.
Files changed (4) hide show
  1. package/README.md +34 -2
  2. package/index.d.ts +480 -0
  3. package/index.js +2490 -0
  4. package/package.json +23 -4
package/README.md CHANGED
@@ -1,3 +1,35 @@
1
- # Temporary Holding Version
1
+ # @nuancelabs/js-sdk
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Put a live, face-to-face conversation with a Nuance avatar in your web page.
4
+
5
+ > **Preview.** Until 1.0, a minor version (0.2.0) may change the API; patch versions (0.1.1) only fix.
6
+
7
+ ```bash
8
+ npm install @nuancelabs/js-sdk
9
+ ```
10
+
11
+ Your server mints a short-lived session token with your API key, and the page
12
+ starts the conversation with it. The API key never reaches the browser.
13
+
14
+ ```ts
15
+ import { createNuanceConversation } from "@nuancelabs/js-sdk";
16
+
17
+ const conversation = createNuanceConversation({
18
+ // Your endpoint, which mints a session token with your API key.
19
+ token: async () => {
20
+ const response = await fetch("/api/nuance-token", { method: "POST" });
21
+ return (await response.json()).token;
22
+ },
23
+ });
24
+
25
+ conversation.onChange((event) => {
26
+ if (event.type === "tracks-changed") {
27
+ conversation.avatar.videoTrack?.attach(videoElement);
28
+ conversation.avatar.audioTrack?.attach(audioElement);
29
+ }
30
+ });
31
+
32
+ await conversation.connect();
33
+ ```
34
+
35
+ Guides, examples and the full reference: https://docs.nuancelabs.ai
package/index.d.ts ADDED
@@ -0,0 +1,480 @@
1
+ /**
2
+ * Raised by {@link Conversation.connect} when the caller already holds another
3
+ * active conversation session. The application must end that session before a
4
+ * new one can be created — retrying immediately will fail the same way.
5
+ */
6
+ export declare class ActiveSessionError extends Error {
7
+ constructor();
8
+ }
9
+
10
+ /**
11
+ * An audio track that can be attached to or detached from an audio element.
12
+ */
13
+ export declare interface AudioTrack {
14
+ /** Attaches the audio track to an HTML audio element. */
15
+ attach(element: HTMLAudioElement): void;
16
+ /** Detaches the track from its currently attached element, if any. */
17
+ detach(): void;
18
+ /** Whether the audio track is currently enabled. */
19
+ readonly isEnabled: boolean;
20
+ /** The underlying media track, when the provider exposes one — feeds
21
+ * local visualizations (e.g. an analyser-driven waveform). */
22
+ readonly mediaStreamTrack?: MediaStreamTrack | null;
23
+ }
24
+
25
+ /**
26
+ * The remote avatar participant in a conversation.
27
+ */
28
+ export declare interface AvatarParticipant {
29
+ /** Avatar video, when available. */
30
+ readonly videoTrack: VideoTrack | null;
31
+ /** Avatar audio, when available. */
32
+ readonly audioTrack: AudioTrack | null;
33
+ /** Mutes avatar audio playback locally. */
34
+ muteAudio(): Promise<void>;
35
+ /** Unmutes avatar audio playback locally. */
36
+ unmuteAudio(): Promise<void>;
37
+ /** Whether avatar audio playback is muted locally. */
38
+ readonly isMuted: boolean;
39
+ /** Whether avatar video is currently enabled. */
40
+ readonly isVideoEnabled: boolean;
41
+ /**
42
+ * Whether the avatar is ready to be shown — its first audiovisual output has
43
+ * reached the client. Audio playback is gated on this automatically (kept muted
44
+ * while attached so buffers warm, then unmuted on ready), so consumers only need
45
+ * this to gate visual presentation, e.g. revealing the video once in sync.
46
+ */
47
+ readonly isReady: boolean;
48
+ }
49
+
50
+ /** The direction a camera faces relative to the user. */
51
+ export declare type CameraFacing = "user" | "environment";
52
+
53
+ /**
54
+ * The runtime's answer to a command: `"applied"`, or `"rejected"` with the
55
+ * runtime's reason in `error`.
56
+ */
57
+ export declare interface CommandResult {
58
+ status: "applied" | "rejected";
59
+ error: string | null;
60
+ }
61
+
62
+
63
+ /**
64
+ * A live avatar conversation controlled by the SDK.
65
+ *
66
+ * A conversation owns the connection lifecycle and exposes the local user and
67
+ * remote avatar participants after it is connected.
68
+ */
69
+ export declare interface Conversation {
70
+ /**
71
+ * Starts the conversation.
72
+ *
73
+ * This creates or joins the underlying session and requests local camera and
74
+ * microphone access when required.
75
+ */
76
+ connect(): Promise<void>;
77
+ /**
78
+ * Ends the conversation and releases local connection resources.
79
+ */
80
+ disconnect(): Promise<void>;
81
+ /**
82
+ * Current lifecycle state for the conversation.
83
+ */
84
+ readonly state: ConversationStateValue;
85
+ /**
86
+ * Server-assigned conversation id, available once connected. Remains readable
87
+ * after disconnect so callers can reference the session (e.g. to submit
88
+ * feedback). Null before the conversation has been created.
89
+ */
90
+ readonly id: string | null;
91
+ /**
92
+ * Local user participant.
93
+ *
94
+ * @throws If accessed before the conversation is connected.
95
+ */
96
+ readonly user: UserParticipant;
97
+ /**
98
+ * Remote avatar participant.
99
+ *
100
+ * @throws If accessed before the conversation is connected.
101
+ */
102
+ readonly avatar: AvatarParticipant;
103
+ /**
104
+ * Switches the avatar's background to one the runtime serves, by id
105
+ * (`"original"` is the avatar's own backdrop). The runtime composites the
106
+ * video, so nothing changes on the client's side of it.
107
+ *
108
+ * Resolves with the runtime's answer: a runtime without a background
109
+ * library, or one that does not serve the id, rejects it.
110
+ *
111
+ * @throws If the id is not a background id, the conversation is not
112
+ * connected, the runtime takes no session commands, or it does not answer
113
+ * in time.
114
+ */
115
+ setBackground(background: string): Promise<CommandResult>;
116
+ /**
117
+ * Subscribes to conversation lifecycle and media events.
118
+ *
119
+ * @returns A function that removes the event handler.
120
+ */
121
+ onChange(handler: (event: ConversationEvent) => void): () => void;
122
+ }
123
+
124
+
125
+ /**
126
+ * Events emitted by {@link Conversation.onChange}.
127
+ */
128
+ export declare type ConversationEvent =
129
+ /** The conversation lifecycle state changed. */
130
+ {
131
+ type: "state-changed";
132
+ state: ConversationStateValue;
133
+ }
134
+ /**
135
+ * The server created the session and assigned {@link Conversation.id}.
136
+ * Fires before any wait for a slot, so a host can key UI on the id
137
+ * (a page for the conversation) while the connection is still underway.
138
+ */
139
+ | {
140
+ type: "session-created";
141
+ id: string;
142
+ }
143
+ /** The avatar participant joined the conversation. */
144
+ | {
145
+ type: "avatar-connected";
146
+ }
147
+ /** The avatar participant left the conversation. */
148
+ | {
149
+ type: "avatar-disconnected";
150
+ }
151
+ /**
152
+ * The avatar is ready to be shown ({@link AvatarParticipant.isReady}). Gate
153
+ * presentation of the avatar's video/audio on this so it is revealed in view
154
+ * and in sync.
155
+ */
156
+ | {
157
+ type: "avatar-ready";
158
+ }
159
+ /**
160
+ * The avatar is presented as she should be seen: her video has reached its
161
+ * full size and held it, or the wait for it ran out. Follows `avatar-ready`.
162
+ * The session opens on it: her first words, and its recording. Reveal her
163
+ * fully on this; between the two she is in view but not yet speaking.
164
+ */
165
+ | {
166
+ type: "avatar-presented";
167
+ }
168
+ /** User or avatar media tracks changed. */
169
+ | {
170
+ type: "tracks-changed";
171
+ }
172
+ /** The active microphone changed; read {@link UserParticipant.microphoneId}. */
173
+ | {
174
+ type: "microphone-changed";
175
+ }
176
+ /** The active camera changed; read {@link UserParticipant.cameraId}. */
177
+ | {
178
+ type: "camera-changed";
179
+ }
180
+ /** Speaking activity changed for the user or avatar. */
181
+ | {
182
+ type: "speaking-changed";
183
+ participant: "user" | "avatar";
184
+ isSpeaking: boolean;
185
+ }
186
+ /**
187
+ * The user's microphone is on in the app but nothing from it reaches the
188
+ * call: the browser sends no audio, or the system stopped or muted the
189
+ * capture. Never reported while the user has muted it in the app. Fires
190
+ * on each change, and clears when the conversation stops sampling.
191
+ */
192
+ | {
193
+ type: "mic-silent-changed";
194
+ silent: boolean;
195
+ }
196
+ /** The connection's {@link NetworkQuality} changed. Starts `"good"`. */
197
+ | {
198
+ type: "network-changed";
199
+ quality: NetworkQuality;
200
+ }
201
+ /**
202
+ * The runtime found this client's link to the avatar poor for a sustained
203
+ * span; sent at most once per session, and only by a runtime that offers it.
204
+ * A caller can offer the user to end the call.
205
+ */
206
+ | {
207
+ type: "link-poor";
208
+ }
209
+ /**
210
+ * The session's place in the queue moved, or its estimate changed.
211
+ *
212
+ * Emitted only while waiting. `estimatedWaitSeconds` never increases for a
213
+ * given session: a wait that grows reads as the service failing, and arriving
214
+ * early is forgiven. Null when there is no serving capacity to estimate from.
215
+ */
216
+ /**
217
+ * How long the live session has left, when it is capped.
218
+ *
219
+ * Emitted on each heartbeat, so it steps rather than ticks; a caller showing a
220
+ * countdown should interpolate between them.
221
+ */
222
+ | {
223
+ type: "time-remaining";
224
+ secondsRemaining: number;
225
+ }
226
+ /**
227
+ * The server considers the session over.
228
+ *
229
+ * A runtime that stops at its limit does not necessarily leave the transport,
230
+ * so a caller cannot infer this from the connection alone. `reason` is
231
+ * `"time-limit"` when the session was capped and ran out its clock,
232
+ * `"policy-violation"` when the runtime ended it for content.
233
+ */
234
+ | {
235
+ type: "session-ended";
236
+ reason: SessionEndReason;
237
+ } | {
238
+ type: "queue-changed";
239
+ position: number;
240
+ estimatedWaitSeconds: number | null;
241
+ poolSlots: number | null;
242
+ };
243
+
244
+ /**
245
+ * Lifecycle states emitted by a conversation.
246
+ */
247
+ export declare enum ConversationState {
248
+ /** The conversation has been created locally but has not started connecting. */
249
+ Idle = "idle",
250
+ /** Waiting for a runtime slot. Distinct from Connecting: nothing is wrong and
251
+ * there is nothing to retry — the session has a place in line and will start
252
+ * when one frees. */
253
+ Queued = "queued",
254
+ /** The SDK is creating or joining the underlying session. */
255
+ Connecting = "connecting",
256
+ /** The conversation is connected and media can flow. */
257
+ Connected = "connected",
258
+ /** The conversation was disconnected intentionally or by the remote service. */
259
+ Disconnected = "disconnected",
260
+ /** The conversation failed to connect or encountered a terminal error. */
261
+ Failed = "failed"
262
+ }
263
+
264
+ /**
265
+ * String value form of {@link ConversationState}.
266
+ */
267
+ export declare type ConversationStateValue = `${ConversationState}`;
268
+
269
+ /**
270
+ * Creates a Nuance avatar conversation.
271
+ *
272
+ * The returned conversation is idle until {@link Conversation.connect} is
273
+ * called. The SDK creates the backend conversation session, waits until it is
274
+ * ready, and connects using the connection details returned by the Nuance API.
275
+ *
276
+ * @example
277
+ * ```ts
278
+ * import { createNuanceConversation } from "@nuancelabs/js-sdk";
279
+ *
280
+ * const conversation = createNuanceConversation({
281
+ * // Your server mints a session token with its API key.
282
+ * token: async () => {
283
+ * const response = await fetch("/api/nuance-token", { method: "POST" });
284
+ * return (await response.json()).token;
285
+ * },
286
+ * });
287
+ *
288
+ * conversation.onChange((event) => {
289
+ * if (event.type === "tracks-changed") {
290
+ * conversation.avatar.videoTrack?.attach(videoElement);
291
+ * }
292
+ * });
293
+ *
294
+ * await conversation.connect();
295
+ * ```
296
+ */
297
+ export declare function createNuanceConversation(options?: CreateNuanceConversationOptions): Conversation;
298
+
299
+ export declare interface CreateNuanceConversationOptions {
300
+ /**
301
+ * Fetches a session token, for authenticating from a web page.
302
+ *
303
+ * Your server mints the token with its API key (`POST /session-tokens`) and
304
+ * returns it to the page. The SDK asks for a fresh one each time it starts a
305
+ * conversation, since a token creates one conversation and then reaches only
306
+ * that conversation. The conversation's settings are fixed when your
307
+ * server mints the token.
308
+ *
309
+ * @example
310
+ * ```ts
311
+ * token: async () => {
312
+ * const response = await fetch("/api/nuance-token", { method: "POST" });
313
+ * return (await response.json()).token;
314
+ * }
315
+ * ```
316
+ */
317
+ token?: () => Promise<string>;
318
+ /**
319
+ * Capture constraints for the user's camera.
320
+ *
321
+ * When omitted, the provider's capture defaults apply (16:9 720p for
322
+ * LiveKit, the browser's default for WebRTC).
323
+ */
324
+ videoCapture?: VideoCaptureConstraints;
325
+ /** Local capture preferences, resolved once for each connection attempt. */
326
+ mediaDevices?: MediaDevicePreferences | (() => MediaDevicePreferences);
327
+ }
328
+
329
+
330
+
331
+
332
+
333
+ /**
334
+ * Raised by {@link Conversation.connect} when the server refuses a new session
335
+ * because the account has not completed onboarding or accepted the current
336
+ * Terms of Service. The application must send the user through its legal
337
+ * consent flow; retrying without that will fail the same way.
338
+ */
339
+ export declare class LegalRequiredError extends Error {
340
+ constructor();
341
+ }
342
+
343
+ /**
344
+ * Raised by {@link Conversation.connect} when the server refuses a new session
345
+ * because the service is under maintenance. `message` is the notice the
346
+ * operators wrote, safe to show as is.
347
+ */
348
+ export declare class MaintenanceError extends Error {
349
+ constructor(message: string);
350
+ }
351
+
352
+ /**
353
+ * A local media failure raised by {@link Conversation.connect}. Exposes the
354
+ * affected device and a classified reason so the application can render (and
355
+ * localize) the user-facing copy and choose recovery UI. The `Error` message is
356
+ * terse and developer-facing — do not show it to users.
357
+ */
358
+ export declare class MediaAccessError extends Error {
359
+ readonly device: MediaDevice;
360
+ readonly reason: MediaErrorReason;
361
+ constructor(cause: unknown, device?: MediaDevice);
362
+ }
363
+
364
+ /**
365
+ * Which local capture device a media failure concerns.
366
+ */
367
+ export declare type MediaDevice = "camera" | "microphone" | "unknown";
368
+
369
+ /** Preferred local devices. Unavailable device IDs fall back to browser defaults. */
370
+ export declare interface MediaDevicePreferences {
371
+ microphoneId?: string;
372
+ cameraId?: string;
373
+ /** Whether to capture video on connect. Defaults to true; video can be enabled later. */
374
+ cameraEnabled?: boolean;
375
+ /** Required camera direction; takes precedence over cameraId. */
376
+ cameraFacing?: CameraFacing;
377
+ }
378
+
379
+ /**
380
+ * Why a local capture device could not be acquired.
381
+ */
382
+ export declare type MediaErrorReason = "permission-denied" | "device-not-found" | "device-in-use" | "unknown";
383
+
384
+ /**
385
+ * How well media is getting through: the worse of the user's own link and
386
+ * the avatar's, as the media server scores them. `"poor"`: degraded (loss,
387
+ * jitter, low bandwidth). `"lost"`: media has stopped arriving.
388
+ */
389
+ export declare type NetworkQuality = "good" | "poor" | "lost";
390
+
391
+
392
+
393
+ /**
394
+ * Raised by {@link Conversation.connect} when the server refuses a new session
395
+ * because the runtime recently ended too many of this account's sessions for
396
+ * content-policy violations. `retryAfterSeconds` is the server's estimate of
397
+ * when a new session may be started, when it sent one.
398
+ */
399
+ export declare class PolicyCurfewError extends Error {
400
+ readonly retryAfterSeconds: number | null;
401
+ constructor(retryAfterSeconds: number | null);
402
+ }
403
+
404
+
405
+ /**
406
+ * Why a session is over. `"time-limit"`: the capped session ran out its clock.
407
+ * `"policy-violation"`: the runtime ended it for content that breaks the usage
408
+ * policy. `"ended"`: any other server-side end.
409
+ */
410
+ export declare type SessionEndReason = "time-limit" | "ended" | "policy-violation" | "connection-lost";
411
+
412
+
413
+
414
+
415
+
416
+ /**
417
+ * The local user participant in a conversation.
418
+ */
419
+ export declare interface UserParticipant {
420
+ /** Local camera video, when available. */
421
+ readonly videoTrack: VideoTrack | null;
422
+ /** Local microphone audio, when available. */
423
+ readonly audioTrack: AudioTrack | null;
424
+ /** Current microphone device id, or the selected device while muted. */
425
+ readonly microphoneId: string | null;
426
+ /** Changes the microphone without ending the conversation or changing mute. */
427
+ setMicrophone(deviceId: string): Promise<void>;
428
+ /** Current camera device id, or the selected device while the camera is off. */
429
+ readonly cameraId: string | null;
430
+ /** Changes the camera without ending the conversation or enabling video. */
431
+ setCamera(deviceId: string): Promise<void>;
432
+ /** Current camera direction, or the selected direction while video is off. */
433
+ readonly cameraFacing: CameraFacing | null;
434
+ /** Selects the front or back camera without enabling video. */
435
+ setCameraFacing(facing: CameraFacing): Promise<void>;
436
+ /** Enables the local camera. */
437
+ enableVideo(): Promise<void>;
438
+ /** Disables the local camera. */
439
+ disableVideo(): Promise<void>;
440
+ /** Mutes the local microphone. */
441
+ muteAudio(): Promise<void>;
442
+ /** Unmutes the local microphone. */
443
+ unmuteAudio(): Promise<void>;
444
+ /** Whether the local microphone is muted. */
445
+ readonly isMuted: boolean;
446
+ /** Whether the local camera is enabled. */
447
+ readonly isVideoEnabled: boolean;
448
+ /** Whether the user is currently detected as speaking. */
449
+ readonly isSpeaking: boolean;
450
+ }
451
+
452
+ /**
453
+ * Capture constraints for the user's published camera track.
454
+ *
455
+ * Use this to match the capture to what your UI actually displays, so
456
+ * recordings and other downstream consumers receive the same frame the user
457
+ * sees in their self-view — e.g. a square self-view should capture square
458
+ * video. Values are applied as ideal constraints; a browser that cannot
459
+ * satisfy them falls back to its nearest supported format.
460
+ */
461
+ export declare interface VideoCaptureConstraints {
462
+ width: number;
463
+ height: number;
464
+ /** @defaultValue `width / height` */
465
+ aspectRatio?: number;
466
+ }
467
+
468
+ /**
469
+ * A video track that can be attached to or detached from a video element.
470
+ */
471
+ export declare interface VideoTrack {
472
+ /** Attaches the video track to an HTML video element. */
473
+ attach(element: HTMLVideoElement): void;
474
+ /** Detaches the track from its currently attached element, if any. */
475
+ detach(): void;
476
+ /** Whether the video track is currently enabled. */
477
+ readonly isEnabled: boolean;
478
+ }
479
+
480
+ export { }