@nuancelabs/js-sdk 0.14.1 → 0.15.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/index.d.ts +2 -2
- package/index.js +1 -1
- package/package.json +13 -1
- package/react/index.d.ts +52 -0
- package/react/index.js +166 -0
package/index.d.ts
CHANGED
|
@@ -739,8 +739,8 @@ export declare interface SessionPersonality {
|
|
|
739
739
|
|
|
740
740
|
/**
|
|
741
741
|
* What your server sends to `POST /session-tokens` with its API key. Every
|
|
742
|
-
* field is fixed for the conversation the token starts
|
|
743
|
-
* defaults.
|
|
742
|
+
* field is fixed for the conversation the token starts. Send `end_user_id`
|
|
743
|
+
* with every token; leave the other fields out for the defaults.
|
|
744
744
|
*/
|
|
745
745
|
export declare interface SessionTokenRequest {
|
|
746
746
|
/** Who the avatar is in the conversation. Leave it out for the default personality. */
|
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nuancelabs/js-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "JavaScript SDK for Nuance Labs conversation video AI avatar",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"repository": {
|
|
@@ -16,10 +16,22 @@
|
|
|
16
16
|
".": {
|
|
17
17
|
"types": "./index.d.ts",
|
|
18
18
|
"import": "./index.js"
|
|
19
|
+
},
|
|
20
|
+
"./react": {
|
|
21
|
+
"types": "./react/index.d.ts",
|
|
22
|
+
"import": "./react/index.js"
|
|
19
23
|
}
|
|
20
24
|
},
|
|
21
25
|
"sideEffects": false,
|
|
22
26
|
"dependencies": {
|
|
23
27
|
"livekit-client": "^2.18.4"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"react": ">=18"
|
|
31
|
+
},
|
|
32
|
+
"peerDependenciesMeta": {
|
|
33
|
+
"react": {
|
|
34
|
+
"optional": true
|
|
35
|
+
}
|
|
24
36
|
}
|
|
25
37
|
}
|
package/react/index.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Conversation } from '@nuancelabs/js-sdk';
|
|
2
|
+
import type { ConversationStateValue } from '@nuancelabs/js-sdk';
|
|
3
|
+
import type { CreateNuanceConversationOptions } from '@nuancelabs/js-sdk';
|
|
4
|
+
|
|
5
|
+
/** What {@link useNuanceConversation} returns. */
|
|
6
|
+
export declare interface NuanceConversationHook {
|
|
7
|
+
/** The conversation's state; `"idle"` before the first start. */
|
|
8
|
+
readonly state: ConversationStateValue;
|
|
9
|
+
/**
|
|
10
|
+
* Why the last start or end failed, as thrown: test it with `instanceof`
|
|
11
|
+
* (`ApiError`, `MediaAccessError`, ...). Null after a start succeeds.
|
|
12
|
+
*/
|
|
13
|
+
readonly error: Error | null;
|
|
14
|
+
/**
|
|
15
|
+
* The latest conversation, for anything the hook does not cover; kept after
|
|
16
|
+
* it ends, for its id. Null before the first start.
|
|
17
|
+
*/
|
|
18
|
+
readonly conversation: Conversation | null;
|
|
19
|
+
/** Pass to a `<video>`'s `ref`: the avatar's video plays there. */
|
|
20
|
+
readonly videoRef: (element: HTMLVideoElement | null) => void;
|
|
21
|
+
/** Pass to an `<audio autoPlay>`'s `ref`: the avatar's voice plays there. */
|
|
22
|
+
readonly audioRef: (element: HTMLAudioElement | null) => void;
|
|
23
|
+
/** Starts a conversation, ending any current one. Call it from a click. */
|
|
24
|
+
readonly start: () => Promise<void>;
|
|
25
|
+
/** Ends the current conversation. */
|
|
26
|
+
readonly end: () => Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* One conversation at a time in a React component: its state and error, refs
|
|
31
|
+
* for the elements the avatar plays in, and start and end. The hook leaves the
|
|
32
|
+
* conversation when the session ends (so the camera and microphone turn off)
|
|
33
|
+
* and when the component unmounts. Each start uses the options of the latest
|
|
34
|
+
* render.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```tsx
|
|
38
|
+
* const { state, error, videoRef, audioRef, start, end } =
|
|
39
|
+
* useNuanceConversation({ token: fetchToken });
|
|
40
|
+
*
|
|
41
|
+
* return (
|
|
42
|
+
* <>
|
|
43
|
+
* <video ref={videoRef} autoPlay playsInline />
|
|
44
|
+
* <audio ref={audioRef} autoPlay />
|
|
45
|
+
* <button onClick={start}>Start</button>
|
|
46
|
+
* </>
|
|
47
|
+
* );
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export declare function useNuanceConversation(options: CreateNuanceConversationOptions): NuanceConversationHook;
|
|
51
|
+
|
|
52
|
+
export { }
|
package/react/index.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// src/react/index.ts
|
|
2
|
+
import { useEffect, useState, useSyncExternalStore } from "react";
|
|
3
|
+
|
|
4
|
+
// src/react/store.ts
|
|
5
|
+
import {
|
|
6
|
+
createNuanceConversation
|
|
7
|
+
} from "@nuancelabs/js-sdk";
|
|
8
|
+
var IDLE = {
|
|
9
|
+
state: "idle",
|
|
10
|
+
error: null,
|
|
11
|
+
conversation: null
|
|
12
|
+
};
|
|
13
|
+
function asError(cause) {
|
|
14
|
+
return cause instanceof Error ? cause : new Error(String(cause));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
class ConversationStore {
|
|
18
|
+
options;
|
|
19
|
+
create;
|
|
20
|
+
snapshot = IDLE;
|
|
21
|
+
listeners = new Set;
|
|
22
|
+
current = null;
|
|
23
|
+
unsubscribe = null;
|
|
24
|
+
video = null;
|
|
25
|
+
audio = null;
|
|
26
|
+
playingVideo = null;
|
|
27
|
+
playingAudio = null;
|
|
28
|
+
leaving = Promise.resolve();
|
|
29
|
+
constructor(options, create = createNuanceConversation) {
|
|
30
|
+
this.options = options;
|
|
31
|
+
this.create = create;
|
|
32
|
+
}
|
|
33
|
+
setOptions(options) {
|
|
34
|
+
this.options = options;
|
|
35
|
+
}
|
|
36
|
+
subscribe = (listener) => {
|
|
37
|
+
this.listeners.add(listener);
|
|
38
|
+
return () => this.listeners.delete(listener);
|
|
39
|
+
};
|
|
40
|
+
getSnapshot = () => this.snapshot;
|
|
41
|
+
setVideo = (element) => {
|
|
42
|
+
if (element === this.video)
|
|
43
|
+
return;
|
|
44
|
+
this.playingVideo?.detach();
|
|
45
|
+
this.playingVideo = null;
|
|
46
|
+
this.video = element;
|
|
47
|
+
this.attach();
|
|
48
|
+
};
|
|
49
|
+
setAudio = (element) => {
|
|
50
|
+
if (element === this.audio)
|
|
51
|
+
return;
|
|
52
|
+
this.playingAudio?.detach();
|
|
53
|
+
this.playingAudio = null;
|
|
54
|
+
this.audio = element;
|
|
55
|
+
this.attach();
|
|
56
|
+
};
|
|
57
|
+
start = async () => {
|
|
58
|
+
let conversation;
|
|
59
|
+
try {
|
|
60
|
+
conversation = this.create(this.options);
|
|
61
|
+
} catch (cause) {
|
|
62
|
+
this.update({ error: asError(cause) });
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const previous = this.release();
|
|
66
|
+
this.current = conversation;
|
|
67
|
+
this.unsubscribe = conversation.onChange((event) => {
|
|
68
|
+
if (this.current !== conversation)
|
|
69
|
+
return;
|
|
70
|
+
if (event.type === "state-changed")
|
|
71
|
+
this.update({ state: event.state });
|
|
72
|
+
if (event.type === "tracks-changed")
|
|
73
|
+
this.attach();
|
|
74
|
+
if (event.type === "session-ended")
|
|
75
|
+
this.end();
|
|
76
|
+
});
|
|
77
|
+
this.update({ state: conversation.state, error: null, conversation });
|
|
78
|
+
if (previous)
|
|
79
|
+
this.leave(previous).catch(() => {});
|
|
80
|
+
await this.leaving;
|
|
81
|
+
if (this.current !== conversation)
|
|
82
|
+
return;
|
|
83
|
+
try {
|
|
84
|
+
await conversation.connect();
|
|
85
|
+
} catch (cause) {
|
|
86
|
+
if (this.current === conversation)
|
|
87
|
+
this.update({ error: asError(cause) });
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
end = async () => {
|
|
91
|
+
const conversation = this.release();
|
|
92
|
+
if (!conversation)
|
|
93
|
+
return;
|
|
94
|
+
try {
|
|
95
|
+
await this.leave(conversation);
|
|
96
|
+
} catch (cause) {
|
|
97
|
+
if (this.current === null)
|
|
98
|
+
this.update({ error: asError(cause) });
|
|
99
|
+
}
|
|
100
|
+
if (this.current === null)
|
|
101
|
+
this.update({ state: conversation.state });
|
|
102
|
+
};
|
|
103
|
+
leave(conversation) {
|
|
104
|
+
const done = conversation.disconnect();
|
|
105
|
+
this.leaving = Promise.all([this.leaving, done.catch(() => {})]);
|
|
106
|
+
return done;
|
|
107
|
+
}
|
|
108
|
+
release() {
|
|
109
|
+
const conversation = this.current;
|
|
110
|
+
this.unsubscribe?.();
|
|
111
|
+
this.unsubscribe = null;
|
|
112
|
+
this.current = null;
|
|
113
|
+
this.playingVideo?.detach();
|
|
114
|
+
this.playingAudio?.detach();
|
|
115
|
+
this.playingVideo = null;
|
|
116
|
+
this.playingAudio = null;
|
|
117
|
+
return conversation;
|
|
118
|
+
}
|
|
119
|
+
attach() {
|
|
120
|
+
const conversation = this.current;
|
|
121
|
+
if (!conversation)
|
|
122
|
+
return;
|
|
123
|
+
let avatar;
|
|
124
|
+
try {
|
|
125
|
+
avatar = conversation.avatar;
|
|
126
|
+
} catch {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const { videoTrack, audioTrack } = avatar;
|
|
130
|
+
if (this.video && videoTrack && videoTrack !== this.playingVideo) {
|
|
131
|
+
this.playingVideo?.detach();
|
|
132
|
+
videoTrack.attach(this.video);
|
|
133
|
+
this.playingVideo = videoTrack;
|
|
134
|
+
}
|
|
135
|
+
if (this.audio && audioTrack && audioTrack !== this.playingAudio) {
|
|
136
|
+
this.playingAudio?.detach();
|
|
137
|
+
audioTrack.attach(this.audio);
|
|
138
|
+
this.playingAudio = audioTrack;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
update(change) {
|
|
142
|
+
this.snapshot = { ...this.snapshot, ...change };
|
|
143
|
+
for (const listener of this.listeners)
|
|
144
|
+
listener();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// src/react/index.ts
|
|
149
|
+
function useNuanceConversation(options) {
|
|
150
|
+
const [store] = useState(() => new ConversationStore(options));
|
|
151
|
+
useEffect(() => store.setOptions(options));
|
|
152
|
+
const snapshot = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
|
|
153
|
+
useEffect(() => () => {
|
|
154
|
+
store.end();
|
|
155
|
+
}, [store]);
|
|
156
|
+
return {
|
|
157
|
+
...snapshot,
|
|
158
|
+
videoRef: store.setVideo,
|
|
159
|
+
audioRef: store.setAudio,
|
|
160
|
+
start: store.start,
|
|
161
|
+
end: store.end
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
export {
|
|
165
|
+
useNuanceConversation
|
|
166
|
+
};
|