@nuancelabs/js-sdk 0.14.1 → 0.33.2
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/LICENSE +202 -0
- package/NOTICE +4 -0
- package/index.d.ts +262 -350
- package/index.js +859 -577
- package/package.json +14 -2
- package/react/index.d.ts +52 -0
- package/react/index.js +171 -0
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nuancelabs/js-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.2",
|
|
4
4
|
"description": "JavaScript SDK for Nuance Labs conversation video AI avatar",
|
|
5
|
-
"license": "
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "git+https://github.com/nuance-labs-ai/nuance.git",
|
|
@@ -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
|
+
* even with `stayConnectedOnEnd`, and when the component unmounts. Each start
|
|
34
|
+
* uses the options of the latest 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,171 @@
|
|
|
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
|
+
if (event.error) {
|
|
76
|
+
this.update({ error: event.error });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
this.end();
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
this.update({ state: conversation.state, error: null, conversation });
|
|
83
|
+
if (previous)
|
|
84
|
+
this.leave(previous).catch(() => {});
|
|
85
|
+
await this.leaving;
|
|
86
|
+
if (this.current !== conversation)
|
|
87
|
+
return;
|
|
88
|
+
try {
|
|
89
|
+
await conversation.connect();
|
|
90
|
+
} catch (cause) {
|
|
91
|
+
if (this.current === conversation)
|
|
92
|
+
this.update({ error: asError(cause) });
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
end = async () => {
|
|
96
|
+
const conversation = this.release();
|
|
97
|
+
if (!conversation)
|
|
98
|
+
return;
|
|
99
|
+
try {
|
|
100
|
+
await this.leave(conversation);
|
|
101
|
+
} catch (cause) {
|
|
102
|
+
if (this.current === null)
|
|
103
|
+
this.update({ error: asError(cause) });
|
|
104
|
+
}
|
|
105
|
+
if (this.current === null)
|
|
106
|
+
this.update({ state: conversation.state });
|
|
107
|
+
};
|
|
108
|
+
leave(conversation) {
|
|
109
|
+
const done = conversation.disconnect();
|
|
110
|
+
this.leaving = Promise.all([this.leaving, done.catch(() => {})]);
|
|
111
|
+
return done;
|
|
112
|
+
}
|
|
113
|
+
release() {
|
|
114
|
+
const conversation = this.current;
|
|
115
|
+
this.unsubscribe?.();
|
|
116
|
+
this.unsubscribe = null;
|
|
117
|
+
this.current = null;
|
|
118
|
+
this.playingVideo?.detach();
|
|
119
|
+
this.playingAudio?.detach();
|
|
120
|
+
this.playingVideo = null;
|
|
121
|
+
this.playingAudio = null;
|
|
122
|
+
return conversation;
|
|
123
|
+
}
|
|
124
|
+
attach() {
|
|
125
|
+
const conversation = this.current;
|
|
126
|
+
if (!conversation)
|
|
127
|
+
return;
|
|
128
|
+
let avatar;
|
|
129
|
+
try {
|
|
130
|
+
avatar = conversation.avatar;
|
|
131
|
+
} catch {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const { videoTrack, audioTrack } = avatar;
|
|
135
|
+
if (this.video && videoTrack && videoTrack !== this.playingVideo) {
|
|
136
|
+
this.playingVideo?.detach();
|
|
137
|
+
videoTrack.attach(this.video);
|
|
138
|
+
this.playingVideo = videoTrack;
|
|
139
|
+
}
|
|
140
|
+
if (this.audio && audioTrack && audioTrack !== this.playingAudio) {
|
|
141
|
+
this.playingAudio?.detach();
|
|
142
|
+
audioTrack.attach(this.audio);
|
|
143
|
+
this.playingAudio = audioTrack;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
update(change) {
|
|
147
|
+
this.snapshot = { ...this.snapshot, ...change };
|
|
148
|
+
for (const listener of this.listeners)
|
|
149
|
+
listener();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/react/index.ts
|
|
154
|
+
function useNuanceConversation(options) {
|
|
155
|
+
const [store] = useState(() => new ConversationStore(options));
|
|
156
|
+
useEffect(() => store.setOptions(options));
|
|
157
|
+
const snapshot = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
|
|
158
|
+
useEffect(() => () => {
|
|
159
|
+
store.end();
|
|
160
|
+
}, [store]);
|
|
161
|
+
return {
|
|
162
|
+
...snapshot,
|
|
163
|
+
videoRef: store.setVideo,
|
|
164
|
+
audioRef: store.setAudio,
|
|
165
|
+
start: store.start,
|
|
166
|
+
end: store.end
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
export {
|
|
170
|
+
useNuanceConversation
|
|
171
|
+
};
|