@cartesia/cartesia-js 0.0.1 → 0.0.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/.turbo/turbo-build.log +45 -47
- package/CHANGELOG.md +6 -0
- package/README.md +17 -2
- package/dist/audio/index.d.mts +2 -1
- package/dist/audio/index.d.ts +2 -1
- package/dist/audio/index.js +90 -48
- package/dist/audio/index.mjs +4 -4
- package/dist/audio/utils.d.mts +2 -1
- package/dist/audio/utils.d.ts +2 -1
- package/dist/audio/utils.js +44 -0
- package/dist/audio/utils.mjs +6 -2
- package/dist/{chunk-5RMUZJV7.mjs → chunk-3CYTAFLF.mjs} +72 -50
- package/dist/{chunk-BTFHUVNH.mjs → chunk-FRIBQZPN.mjs} +44 -2
- package/dist/{chunk-35HX6ML3.mjs → chunk-HNLIBHEN.mjs} +18 -1
- package/dist/{chunk-ERFCRIWU.mjs → chunk-XSFPHPPG.mjs} +1 -1
- package/dist/{index-Dt9A_pEb.d.mts → index-DSBmfK9-.d.mts} +39 -8
- package/dist/{index-Ds4LDkmk.d.ts → index-qwAyxV5I.d.ts} +39 -8
- package/dist/lib/client.mjs +2 -2
- package/dist/lib/constants.js +1 -1
- package/dist/lib/constants.mjs +1 -1
- package/dist/lib/index.d.mts +2 -1
- package/dist/lib/index.d.ts +2 -1
- package/dist/lib/index.js +90 -48
- package/dist/lib/index.mjs +4 -4
- package/dist/react/index.d.mts +9 -5
- package/dist/react/index.d.ts +9 -5
- package/dist/react/index.js +178 -60
- package/dist/react/index.mjs +64 -16
- package/package.json +5 -4
- package/src/audio/index.ts +86 -46
- package/src/audio/utils.ts +82 -0
- package/src/lib/constants.ts +1 -1
- package/src/react/index.ts +61 -12
package/dist/react/index.mjs
CHANGED
|
@@ -1,42 +1,58 @@
|
|
|
1
1
|
import {
|
|
2
2
|
audio_default
|
|
3
|
-
} from "../chunk-
|
|
4
|
-
import "../chunk-BTFHUVNH.mjs";
|
|
5
|
-
import "../chunk-ERFCRIWU.mjs";
|
|
3
|
+
} from "../chunk-3CYTAFLF.mjs";
|
|
6
4
|
import {
|
|
5
|
+
base64ToArray,
|
|
6
|
+
bufferToWav
|
|
7
|
+
} from "../chunk-FRIBQZPN.mjs";
|
|
8
|
+
import "../chunk-XSFPHPPG.mjs";
|
|
9
|
+
import {
|
|
10
|
+
SAMPLE_RATE,
|
|
7
11
|
__async
|
|
8
|
-
} from "../chunk-
|
|
12
|
+
} from "../chunk-HNLIBHEN.mjs";
|
|
9
13
|
|
|
10
14
|
// src/react/index.ts
|
|
11
15
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
12
16
|
function useAudio({ apiKey, baseUrl }) {
|
|
13
|
-
if (typeof window === "undefined"
|
|
17
|
+
if (typeof window === "undefined") {
|
|
14
18
|
return {
|
|
15
19
|
stream: () => {
|
|
16
20
|
},
|
|
17
21
|
play: () => __async(this, null, function* () {
|
|
18
22
|
}),
|
|
23
|
+
download: () => null,
|
|
24
|
+
isConnected: false,
|
|
19
25
|
isPlaying: false,
|
|
26
|
+
isStreamed: false,
|
|
20
27
|
chunks: [],
|
|
21
28
|
messages: []
|
|
22
29
|
};
|
|
23
30
|
}
|
|
24
31
|
const audio = useMemo(() => {
|
|
32
|
+
if (!apiKey) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
25
35
|
const audio2 = new audio_default({ apiKey, baseUrl });
|
|
26
36
|
return audio2;
|
|
27
37
|
}, [apiKey, baseUrl]);
|
|
28
38
|
const streamReturn = useRef(null);
|
|
39
|
+
const [isStreamed, setIsStreamed] = useState(false);
|
|
29
40
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
41
|
+
const [isConnected, setIsConnected] = useState(false);
|
|
30
42
|
const [chunks, setChunks] = useState([]);
|
|
31
43
|
const [messages, setMessages] = useState([]);
|
|
32
44
|
const stream = useCallback(
|
|
33
|
-
(options) => {
|
|
45
|
+
(options) => __async(this, null, function* () {
|
|
34
46
|
var _a;
|
|
35
47
|
streamReturn.current = (_a = audio == null ? void 0 : audio.stream(options)) != null ? _a : null;
|
|
48
|
+
if (!streamReturn.current) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
setMessages([]);
|
|
36
52
|
streamReturn.current.on(
|
|
37
53
|
"chunk",
|
|
38
|
-
({ chunks:
|
|
39
|
-
setChunks(
|
|
54
|
+
({ chunks: chunks3 }) => {
|
|
55
|
+
setChunks(chunks3);
|
|
40
56
|
}
|
|
41
57
|
);
|
|
42
58
|
streamReturn.current.on(
|
|
@@ -45,23 +61,46 @@ function useAudio({ apiKey, baseUrl }) {
|
|
|
45
61
|
setMessages((messages2) => [...messages2, message]);
|
|
46
62
|
}
|
|
47
63
|
);
|
|
48
|
-
|
|
64
|
+
const { chunks: chunks2 } = yield streamReturn.current.once("streamed");
|
|
65
|
+
setChunks(chunks2);
|
|
66
|
+
setIsStreamed(true);
|
|
67
|
+
}),
|
|
49
68
|
[audio]
|
|
50
69
|
);
|
|
70
|
+
const download = useCallback(() => {
|
|
71
|
+
if (!isStreamed) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const audio2 = bufferToWav(SAMPLE_RATE, [base64ToArray(chunks)]);
|
|
75
|
+
return new Blob([audio2], { type: "audio/wav" });
|
|
76
|
+
}, [isStreamed, chunks]);
|
|
51
77
|
useEffect(() => {
|
|
52
|
-
|
|
78
|
+
let cleanup = () => {
|
|
79
|
+
};
|
|
80
|
+
function setupConnection() {
|
|
53
81
|
return __async(this, null, function* () {
|
|
54
82
|
try {
|
|
55
|
-
yield audio == null ? void 0 : audio.connect();
|
|
83
|
+
const connection = yield audio == null ? void 0 : audio.connect();
|
|
84
|
+
if (!connection) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
setIsConnected(true);
|
|
88
|
+
const unsubscribe = connection.on("close", () => {
|
|
89
|
+
setIsConnected(false);
|
|
90
|
+
});
|
|
91
|
+
return () => {
|
|
92
|
+
unsubscribe();
|
|
93
|
+
audio == null ? void 0 : audio.disconnect();
|
|
94
|
+
};
|
|
56
95
|
} catch (e) {
|
|
57
96
|
console.error(e);
|
|
58
97
|
}
|
|
59
|
-
return () => {
|
|
60
|
-
audio == null ? void 0 : audio.disconnect();
|
|
61
|
-
};
|
|
62
98
|
});
|
|
63
99
|
}
|
|
64
|
-
|
|
100
|
+
setupConnection().then((cleanupConnection) => {
|
|
101
|
+
cleanup = cleanupConnection;
|
|
102
|
+
});
|
|
103
|
+
return () => cleanup == null ? void 0 : cleanup();
|
|
65
104
|
}, [audio]);
|
|
66
105
|
const play = useCallback(
|
|
67
106
|
(bufferDuration = 0) => __async(this, null, function* () {
|
|
@@ -75,7 +114,16 @@ function useAudio({ apiKey, baseUrl }) {
|
|
|
75
114
|
}),
|
|
76
115
|
[isPlaying]
|
|
77
116
|
);
|
|
78
|
-
return {
|
|
117
|
+
return {
|
|
118
|
+
stream,
|
|
119
|
+
play,
|
|
120
|
+
download,
|
|
121
|
+
isPlaying,
|
|
122
|
+
isConnected,
|
|
123
|
+
isStreamed,
|
|
124
|
+
chunks,
|
|
125
|
+
messages
|
|
126
|
+
};
|
|
79
127
|
}
|
|
80
128
|
export {
|
|
81
129
|
useAudio
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"name": "Cartesia",
|
|
5
5
|
"url": "https://cartesia.ai"
|
|
6
6
|
},
|
|
7
|
-
"version": "0.0.
|
|
7
|
+
"version": "0.0.2",
|
|
8
8
|
"description": "Client for the Cartesia API.",
|
|
9
9
|
"main": "./dist/index.js",
|
|
10
10
|
"module": "./dist/index.mjs",
|
|
@@ -22,13 +22,14 @@
|
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"base64-js": "^1.5.1",
|
|
24
24
|
"emittery": "^1.0.3",
|
|
25
|
-
"human-id": "^4.1.1"
|
|
25
|
+
"human-id": "^4.1.1",
|
|
26
|
+
"partysocket": "^1.0.1"
|
|
26
27
|
},
|
|
27
28
|
"publishConfig": {
|
|
28
|
-
"access": "
|
|
29
|
+
"access": "public"
|
|
29
30
|
},
|
|
30
31
|
"scripts": {
|
|
31
|
-
"build": "tsup src/ --format cjs,esm --dts
|
|
32
|
+
"build": "tsup src/ --format cjs,esm --dts",
|
|
32
33
|
"dev": "bun run build -- --watch"
|
|
33
34
|
},
|
|
34
35
|
"peerDependencies": {
|
package/src/audio/index.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import Emittery from "emittery";
|
|
2
2
|
import { humanId } from "human-id";
|
|
3
|
+
import { WebSocket } from "partysocket";
|
|
3
4
|
import { Client } from "../lib/client";
|
|
4
5
|
import { SAMPLE_RATE, constructWebsocketUrl } from "../lib/constants";
|
|
5
6
|
import {
|
|
7
|
+
type EmitteryCallbacks,
|
|
6
8
|
type Sentinel,
|
|
7
9
|
createMessageHandlerForContextId,
|
|
8
10
|
getBufferDuration,
|
|
11
|
+
getEmitteryCallbacks,
|
|
9
12
|
isComplete,
|
|
10
13
|
isSentinel,
|
|
11
14
|
playAudioBuffer,
|
|
@@ -17,8 +20,22 @@ export type StreamEventData = {
|
|
|
17
20
|
chunk: Chunk;
|
|
18
21
|
chunks: Chunk[];
|
|
19
22
|
};
|
|
23
|
+
streamed: {
|
|
24
|
+
chunks: Chunk[];
|
|
25
|
+
};
|
|
20
26
|
message: unknown;
|
|
21
27
|
};
|
|
28
|
+
export type ConnectionEventData = {
|
|
29
|
+
open: never;
|
|
30
|
+
close: never;
|
|
31
|
+
};
|
|
32
|
+
export type StreamRequest = {
|
|
33
|
+
inputs: object;
|
|
34
|
+
options: {
|
|
35
|
+
timeout?: number;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
|
|
22
39
|
export default class extends Client {
|
|
23
40
|
socket?: WebSocket;
|
|
24
41
|
isConnected = false;
|
|
@@ -35,7 +52,10 @@ export default class extends Client {
|
|
|
35
52
|
* that plays the audio as it arrives, with `bufferDuration` seconds of audio buffered before
|
|
36
53
|
* starting playback.
|
|
37
54
|
*/
|
|
38
|
-
stream(
|
|
55
|
+
stream(
|
|
56
|
+
inputs: StreamRequest["inputs"],
|
|
57
|
+
{ timeout = 0 }: StreamRequest["options"] = {},
|
|
58
|
+
) {
|
|
39
59
|
if (!this.isConnected) {
|
|
40
60
|
throw new Error("Not connected to WebSocket. Call .connect() first.");
|
|
41
61
|
}
|
|
@@ -72,6 +92,9 @@ export default class extends Client {
|
|
|
72
92
|
});
|
|
73
93
|
await emitter.emit("message", message);
|
|
74
94
|
if (isSentinel(chunk)) {
|
|
95
|
+
await emitter.emit("streamed", {
|
|
96
|
+
chunks,
|
|
97
|
+
});
|
|
75
98
|
streamCompleteController.abort();
|
|
76
99
|
} else if (timeoutId) {
|
|
77
100
|
clearTimeout(timeoutId);
|
|
@@ -82,13 +105,28 @@ export default class extends Client {
|
|
|
82
105
|
this.socket?.addEventListener("message", handleMessage, {
|
|
83
106
|
signal: streamCompleteController.signal,
|
|
84
107
|
});
|
|
85
|
-
this.socket?.addEventListener(
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
108
|
+
this.socket?.addEventListener(
|
|
109
|
+
"close",
|
|
110
|
+
() => {
|
|
111
|
+
streamCompleteController.abort();
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
once: true,
|
|
115
|
+
},
|
|
116
|
+
);
|
|
117
|
+
this.socket?.addEventListener(
|
|
118
|
+
"error",
|
|
119
|
+
() => {
|
|
120
|
+
streamCompleteController.abort();
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
once: true,
|
|
124
|
+
},
|
|
125
|
+
);
|
|
91
126
|
streamCompleteController.signal.addEventListener("abort", () => {
|
|
127
|
+
if (timeoutId) {
|
|
128
|
+
clearTimeout(timeoutId);
|
|
129
|
+
}
|
|
92
130
|
emitter.clearListeners();
|
|
93
131
|
});
|
|
94
132
|
|
|
@@ -158,10 +196,7 @@ export default class extends Client {
|
|
|
158
196
|
|
|
159
197
|
return {
|
|
160
198
|
play,
|
|
161
|
-
|
|
162
|
-
off: emitter.off.bind(emitter),
|
|
163
|
-
once: emitter.once.bind(emitter),
|
|
164
|
-
events: emitter.events.bind(emitter),
|
|
199
|
+
...getEmitteryCallbacks(emitter),
|
|
165
200
|
};
|
|
166
201
|
}
|
|
167
202
|
|
|
@@ -189,48 +224,53 @@ export default class extends Client {
|
|
|
189
224
|
connect() {
|
|
190
225
|
const url = constructWebsocketUrl(this.baseUrl);
|
|
191
226
|
url.searchParams.set("api_key", this.apiKey);
|
|
192
|
-
|
|
227
|
+
const emitter = new Emittery<ConnectionEventData>();
|
|
228
|
+
this.socket = new WebSocket(url.toString());
|
|
193
229
|
this.socket.onopen = () => {
|
|
194
230
|
this.isConnected = true;
|
|
231
|
+
emitter.emit("open");
|
|
195
232
|
};
|
|
196
233
|
this.socket.onclose = () => {
|
|
197
234
|
this.isConnected = false;
|
|
235
|
+
emitter.emit("close");
|
|
198
236
|
};
|
|
199
237
|
|
|
200
|
-
return new Promise<
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
238
|
+
return new Promise<EmitteryCallbacks<ConnectionEventData>>(
|
|
239
|
+
(resolve, reject) => {
|
|
240
|
+
this.socket?.addEventListener(
|
|
241
|
+
"open",
|
|
242
|
+
() => {
|
|
243
|
+
resolve(getEmitteryCallbacks(emitter));
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
once: true,
|
|
247
|
+
},
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
const aborter = new AbortController();
|
|
251
|
+
this.socket?.addEventListener(
|
|
252
|
+
"error",
|
|
253
|
+
() => {
|
|
254
|
+
aborter.abort();
|
|
255
|
+
reject(new Error("WebSocket failed to connect."));
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
signal: aborter.signal,
|
|
259
|
+
},
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
this.socket?.addEventListener(
|
|
263
|
+
"close",
|
|
264
|
+
() => {
|
|
265
|
+
aborter.abort();
|
|
266
|
+
reject(new Error("WebSocket closed before it could connect."));
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
signal: aborter.signal,
|
|
270
|
+
},
|
|
271
|
+
);
|
|
272
|
+
},
|
|
273
|
+
);
|
|
234
274
|
}
|
|
235
275
|
|
|
236
276
|
/**
|
package/src/audio/utils.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import base64 from "base64-js";
|
|
2
|
+
import type Emittery from "emittery";
|
|
2
3
|
import type { Chunk, StreamEventData } from ".";
|
|
3
4
|
import { SAMPLE_RATE } from "../lib/constants";
|
|
4
5
|
|
|
@@ -136,3 +137,84 @@ export function filterSentinel<T>(collection: T[]): Exclude<T, Sentinel>[] {
|
|
|
136
137
|
export function isComplete(chunks: Chunk[]) {
|
|
137
138
|
return isSentinel(chunks[chunks.length - 1]);
|
|
138
139
|
}
|
|
140
|
+
|
|
141
|
+
export type EmitteryCallbacks<T> = {
|
|
142
|
+
on: Emittery<T>["on"];
|
|
143
|
+
off: Emittery<T>["off"];
|
|
144
|
+
once: Emittery<T>["once"];
|
|
145
|
+
events: Emittery<T>["events"];
|
|
146
|
+
};
|
|
147
|
+
/**
|
|
148
|
+
* Get user-facing emitter callbacks for an Emittery instance.
|
|
149
|
+
* @param emitter The Emittery instance to get callbacks for.
|
|
150
|
+
* @returns User-facing emitter callbacks.
|
|
151
|
+
*/
|
|
152
|
+
export function getEmitteryCallbacks<T>(
|
|
153
|
+
emitter: Emittery<T>,
|
|
154
|
+
): EmitteryCallbacks<T> {
|
|
155
|
+
return {
|
|
156
|
+
on: emitter.on.bind(emitter),
|
|
157
|
+
off: emitter.off.bind(emitter),
|
|
158
|
+
once: emitter.once.bind(emitter),
|
|
159
|
+
events: emitter.events.bind(emitter),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Converts a base64-encoded audio buffer to a WAV file.
|
|
165
|
+
* Source: https://gist.github.com/Daninet/22edc59cf2aee0b9a90c18e553e49297
|
|
166
|
+
* @param b64 The base64-encoded audio buffer to convert to a WAV file.
|
|
167
|
+
*/
|
|
168
|
+
export function bufferToWav(
|
|
169
|
+
sampleRate: number,
|
|
170
|
+
channelBuffers: Float32Array[],
|
|
171
|
+
) {
|
|
172
|
+
const totalSamples = channelBuffers[0].length * channelBuffers.length;
|
|
173
|
+
|
|
174
|
+
const buffer = new ArrayBuffer(44 + totalSamples * 2);
|
|
175
|
+
const view = new DataView(buffer);
|
|
176
|
+
|
|
177
|
+
const writeString = (view: DataView, offset: number, string: string) => {
|
|
178
|
+
for (let i = 0; i < string.length; i++) {
|
|
179
|
+
view.setUint8(offset + i, string.charCodeAt(i));
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/* RIFF identifier */
|
|
184
|
+
writeString(view, 0, "RIFF");
|
|
185
|
+
/* RIFF chunk length */
|
|
186
|
+
view.setUint32(4, 36 + totalSamples * 2, true);
|
|
187
|
+
/* RIFF type */
|
|
188
|
+
writeString(view, 8, "WAVE");
|
|
189
|
+
/* format chunk identifier */
|
|
190
|
+
writeString(view, 12, "fmt ");
|
|
191
|
+
/* format chunk length */
|
|
192
|
+
view.setUint32(16, 16, true);
|
|
193
|
+
/* sample format (raw) */
|
|
194
|
+
view.setUint16(20, 1, true);
|
|
195
|
+
/* channel count */
|
|
196
|
+
view.setUint16(22, channelBuffers.length, true);
|
|
197
|
+
/* sample rate */
|
|
198
|
+
view.setUint32(24, sampleRate, true);
|
|
199
|
+
/* byte rate (sample rate * block align) */
|
|
200
|
+
view.setUint32(28, sampleRate * 4, true);
|
|
201
|
+
/* block align (channel count * bytes per sample) */
|
|
202
|
+
view.setUint16(32, channelBuffers.length * 2, true);
|
|
203
|
+
/* bits per sample */
|
|
204
|
+
view.setUint16(34, 16, true);
|
|
205
|
+
/* data chunk identifier */
|
|
206
|
+
writeString(view, 36, "data");
|
|
207
|
+
/* data chunk length */
|
|
208
|
+
view.setUint32(40, totalSamples * 2, true);
|
|
209
|
+
|
|
210
|
+
let offset = 44;
|
|
211
|
+
for (let i = 0; i < channelBuffers[0].length; i++) {
|
|
212
|
+
for (let channel = 0; channel < channelBuffers.length; channel++) {
|
|
213
|
+
const s = Math.max(-1, Math.min(1, channelBuffers[channel][i]));
|
|
214
|
+
view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true);
|
|
215
|
+
offset += 2;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return buffer;
|
|
220
|
+
}
|
package/src/lib/constants.ts
CHANGED
|
@@ -2,5 +2,5 @@ export const BASE_URL = "https://api.cartesia.ai/v0";
|
|
|
2
2
|
export const SAMPLE_RATE = 44100;
|
|
3
3
|
|
|
4
4
|
export const constructWebsocketUrl = (baseUrl: string) => {
|
|
5
|
-
return new URL(`${baseUrl.replace(/^http/, "ws")}/
|
|
5
|
+
return new URL(`${baseUrl.replace(/^http/, "ws")}/audio/websocket`);
|
|
6
6
|
};
|
package/src/react/index.ts
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import CartesiaAudio, { type Chunk, type StreamEventData } from "../audio";
|
|
3
|
+
import { base64ToArray, bufferToWav } from "../audio/utils";
|
|
4
|
+
import { SAMPLE_RATE } from "../lib/constants";
|
|
3
5
|
|
|
4
|
-
|
|
5
|
-
apiKey: string;
|
|
6
|
+
export type UseAudioOptions = {
|
|
7
|
+
apiKey: string | null;
|
|
6
8
|
baseUrl?: string;
|
|
7
|
-
}
|
|
9
|
+
};
|
|
8
10
|
|
|
9
11
|
interface UseAudioReturn {
|
|
10
12
|
stream: (options: object) => void;
|
|
11
13
|
play: (bufferDuration?: number) => Promise<void>;
|
|
14
|
+
download: () => Blob | null;
|
|
12
15
|
isPlaying: boolean;
|
|
16
|
+
isConnected: boolean;
|
|
17
|
+
isStreamed: boolean;
|
|
13
18
|
chunks: Chunk[];
|
|
14
19
|
messages: StreamEventData["message"][];
|
|
15
20
|
}
|
|
@@ -17,28 +22,40 @@ interface UseAudioReturn {
|
|
|
17
22
|
* React hook to use the Cartesia audio API.
|
|
18
23
|
*/
|
|
19
24
|
export function useAudio({ apiKey, baseUrl }: UseAudioOptions): UseAudioReturn {
|
|
20
|
-
if (typeof window === "undefined"
|
|
25
|
+
if (typeof window === "undefined") {
|
|
21
26
|
return {
|
|
22
27
|
stream: () => {},
|
|
23
28
|
play: async () => {},
|
|
29
|
+
download: () => null,
|
|
30
|
+
isConnected: false,
|
|
24
31
|
isPlaying: false,
|
|
32
|
+
isStreamed: false,
|
|
25
33
|
chunks: [],
|
|
26
34
|
messages: [],
|
|
27
35
|
};
|
|
28
36
|
}
|
|
29
37
|
|
|
30
38
|
const audio = useMemo(() => {
|
|
39
|
+
if (!apiKey) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
31
42
|
const audio = new CartesiaAudio({ apiKey, baseUrl });
|
|
32
43
|
return audio;
|
|
33
44
|
}, [apiKey, baseUrl]);
|
|
34
45
|
const streamReturn = useRef<ReturnType<CartesiaAudio["stream"]> | null>(null);
|
|
46
|
+
const [isStreamed, setIsStreamed] = useState(false);
|
|
35
47
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
48
|
+
const [isConnected, setIsConnected] = useState(false);
|
|
36
49
|
const [chunks, setChunks] = useState<Chunk[]>([]);
|
|
37
50
|
const [messages, setMessages] = useState<StreamEventData["message"][]>([]);
|
|
38
51
|
|
|
39
52
|
const stream = useCallback(
|
|
40
|
-
(options: object) => {
|
|
53
|
+
async (options: object) => {
|
|
41
54
|
streamReturn.current = audio?.stream(options) ?? null;
|
|
55
|
+
if (!streamReturn.current) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
setMessages([]);
|
|
42
59
|
streamReturn.current.on(
|
|
43
60
|
"chunk",
|
|
44
61
|
({ chunks }: StreamEventData["chunk"]) => {
|
|
@@ -51,22 +68,45 @@ export function useAudio({ apiKey, baseUrl }: UseAudioOptions): UseAudioReturn {
|
|
|
51
68
|
setMessages((messages) => [...messages, message]);
|
|
52
69
|
},
|
|
53
70
|
);
|
|
71
|
+
const { chunks } = await streamReturn.current.once("streamed");
|
|
72
|
+
setChunks(chunks);
|
|
73
|
+
setIsStreamed(true);
|
|
54
74
|
},
|
|
55
75
|
[audio],
|
|
56
76
|
);
|
|
57
77
|
|
|
78
|
+
const download = useCallback(() => {
|
|
79
|
+
if (!isStreamed) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
const audio = bufferToWav(SAMPLE_RATE, [base64ToArray(chunks)]);
|
|
83
|
+
return new Blob([audio], { type: "audio/wav" });
|
|
84
|
+
}, [isStreamed, chunks]);
|
|
85
|
+
|
|
58
86
|
useEffect(() => {
|
|
59
|
-
|
|
87
|
+
let cleanup: (() => void) | undefined = () => {};
|
|
88
|
+
async function setupConnection() {
|
|
60
89
|
try {
|
|
61
|
-
await audio?.connect();
|
|
90
|
+
const connection = await audio?.connect();
|
|
91
|
+
if (!connection) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
setIsConnected(true);
|
|
95
|
+
const unsubscribe = connection.on("close", () => {
|
|
96
|
+
setIsConnected(false);
|
|
97
|
+
});
|
|
98
|
+
return () => {
|
|
99
|
+
unsubscribe();
|
|
100
|
+
audio?.disconnect();
|
|
101
|
+
};
|
|
62
102
|
} catch (e) {
|
|
63
103
|
console.error(e);
|
|
64
104
|
}
|
|
65
|
-
return () => {
|
|
66
|
-
audio?.disconnect();
|
|
67
|
-
};
|
|
68
105
|
}
|
|
69
|
-
|
|
106
|
+
setupConnection().then((cleanupConnection) => {
|
|
107
|
+
cleanup = cleanupConnection;
|
|
108
|
+
});
|
|
109
|
+
return () => cleanup?.();
|
|
70
110
|
}, [audio]);
|
|
71
111
|
|
|
72
112
|
const play = useCallback(
|
|
@@ -87,5 +127,14 @@ export function useAudio({ apiKey, baseUrl }: UseAudioOptions): UseAudioReturn {
|
|
|
87
127
|
// - [] Seek to a specific time.
|
|
88
128
|
// These are probably best implemented by adding event listener
|
|
89
129
|
// functionality to the base library.
|
|
90
|
-
return {
|
|
130
|
+
return {
|
|
131
|
+
stream,
|
|
132
|
+
play,
|
|
133
|
+
download,
|
|
134
|
+
isPlaying,
|
|
135
|
+
isConnected,
|
|
136
|
+
isStreamed,
|
|
137
|
+
chunks,
|
|
138
|
+
messages,
|
|
139
|
+
};
|
|
91
140
|
}
|