@cartesia/cartesia-js 1.0.0 → 1.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.
Files changed (62) hide show
  1. package/.turbo/turbo-build.log +50 -50
  2. package/CHANGELOG.md +12 -0
  3. package/LICENSE.md +21 -0
  4. package/README.md +92 -19
  5. package/dist/{chunk-PQ6CIPFW.js → chunk-6YQ6KDIQ.js} +44 -5
  6. package/dist/{chunk-RO7TY474.js → chunk-BHY7MNGT.js} +11 -6
  7. package/dist/{chunk-F4QWVJY3.js → chunk-EDAAHENY.js} +2 -2
  8. package/dist/{chunk-WIFMLPT5.js → chunk-GHY2WEOK.js} +13 -0
  9. package/dist/{chunk-FN7BK4PS.js → chunk-IZBPLCGW.js} +97 -75
  10. package/dist/{chunk-JYLAM6VU.js → chunk-LZO6K34D.js} +2 -2
  11. package/dist/{chunk-3FL2SNIR.js → chunk-NQVZNVOU.js} +1 -1
  12. package/dist/{chunk-IEN4NCER.js → chunk-NVOCUUOF.js} +3 -3
  13. package/dist/chunk-PISCPZK4.js +40 -0
  14. package/dist/{chunk-SGXUEFII.js → chunk-UCYL2SOX.js} +18 -15
  15. package/dist/index.cjs +186 -103
  16. package/dist/index.d.cts +4 -4
  17. package/dist/index.d.ts +4 -4
  18. package/dist/index.js +15 -9
  19. package/dist/lib/client.cjs +35 -10
  20. package/dist/lib/client.d.cts +2 -2
  21. package/dist/lib/client.d.ts +2 -2
  22. package/dist/lib/client.js +2 -2
  23. package/dist/lib/constants.js +1 -1
  24. package/dist/lib/index.cjs +181 -102
  25. package/dist/lib/index.js +8 -8
  26. package/dist/react/index.cjs +286 -158
  27. package/dist/react/index.d.cts +5 -4
  28. package/dist/react/index.d.ts +5 -4
  29. package/dist/react/index.js +115 -66
  30. package/dist/react/utils.js +2 -2
  31. package/dist/tts/index.cjs +165 -89
  32. package/dist/tts/index.js +6 -6
  33. package/dist/tts/player.cjs +5 -0
  34. package/dist/tts/player.js +4 -3
  35. package/dist/tts/source.cjs +50 -4
  36. package/dist/tts/source.d.cts +16 -6
  37. package/dist/tts/source.d.ts +16 -6
  38. package/dist/tts/source.js +4 -2
  39. package/dist/tts/utils.cjs +18 -6
  40. package/dist/tts/utils.d.cts +7 -5
  41. package/dist/tts/utils.d.ts +7 -5
  42. package/dist/tts/utils.js +3 -2
  43. package/dist/tts/websocket.cjs +165 -89
  44. package/dist/tts/websocket.d.cts +12 -8
  45. package/dist/tts/websocket.d.ts +12 -8
  46. package/dist/tts/websocket.js +5 -5
  47. package/dist/types/index.d.cts +65 -5
  48. package/dist/types/index.d.ts +65 -5
  49. package/dist/voices/index.cjs +31 -23
  50. package/dist/voices/index.d.cts +2 -1
  51. package/dist/voices/index.d.ts +2 -1
  52. package/dist/voices/index.js +3 -3
  53. package/package.json +1 -1
  54. package/src/index.ts +2 -0
  55. package/src/lib/client.ts +10 -10
  56. package/src/react/index.ts +115 -64
  57. package/src/tts/source.ts +53 -7
  58. package/src/tts/utils.ts +26 -12
  59. package/src/tts/websocket.ts +42 -23
  60. package/src/types/index.ts +89 -4
  61. package/src/voices/index.ts +22 -15
  62. package/dist/chunk-PQ5EVEEH.js +0 -34
@@ -1,7 +1,7 @@
1
1
  import emittery__default from 'emittery';
2
2
 
3
3
  interface ClientOptions {
4
- apiKey?: string;
4
+ apiKey?: string | (() => Promise<string>);
5
5
  baseUrl?: string;
6
6
  }
7
7
  type Sentinel = null;
@@ -10,12 +10,64 @@ type ConnectionEventData = {
10
10
  open: never;
11
11
  close: never;
12
12
  };
13
+ type VoiceSpecifier = {
14
+ mode: "id";
15
+ id: string;
16
+ } | {
17
+ mode: "embedding";
18
+ embedding: number[];
19
+ };
20
+ type Emotion = "anger" | "sadness" | "positivity" | "curiosity" | "surprise";
21
+ type Intensity = "lowest" | "low" | "high" | "highest";
22
+ type EmotionControl = Emotion | `${Emotion}:${Intensity}`;
23
+ type VoiceOptions = VoiceSpecifier & {
24
+ __experimental_controls?: {
25
+ speed?: "slowest" | "slow" | "normal" | "fast" | "fastest";
26
+ emotion?: EmotionControl[];
27
+ };
28
+ };
13
29
  type StreamRequest = {
14
- inputs: object;
15
- options: {
16
- timeout?: number;
30
+ model_id: string;
31
+ transcript: string;
32
+ voice: VoiceOptions;
33
+ output_format?: {
34
+ container: string;
35
+ encoding: string;
36
+ sample_rate: number;
17
37
  };
38
+ context_id?: string;
39
+ continue?: boolean;
40
+ duration?: number;
41
+ language?: string;
42
+ add_timestamps?: boolean;
43
+ };
44
+ type StreamOptions = {
45
+ timeout?: number;
46
+ };
47
+ type WebSocketBaseResponse = {
48
+ context_id: string;
49
+ status_code: number;
50
+ done: boolean;
51
+ };
52
+ type WordTimestamps = {
53
+ words: string[];
54
+ start: number[];
55
+ end: number[];
56
+ };
57
+ type WebSocketTimestampsResponse = WebSocketBaseResponse & {
58
+ type: "timestamps";
59
+ word_timestamps: WordTimestamps;
60
+ };
61
+ type WebSocketChunkResponse = WebSocketBaseResponse & {
62
+ type: "chunk";
63
+ data: string;
64
+ step_time: number;
65
+ };
66
+ type WebSocketErrorResponse = WebSocketBaseResponse & {
67
+ type: "error";
68
+ error: string;
18
69
  };
70
+ type WebSocketResponse = WebSocketTimestampsResponse | WebSocketChunkResponse | WebSocketErrorResponse;
19
71
  type EmitteryCallbacks<T> = {
20
72
  on: emittery__default<T>["on"];
21
73
  off: emittery__default<T>["off"];
@@ -25,9 +77,11 @@ type EmitteryCallbacks<T> = {
25
77
  type CloneOptions = {
26
78
  mode: "url";
27
79
  link: string;
80
+ enhance?: boolean;
28
81
  } | {
29
82
  mode: "clip";
30
83
  clip: Blob;
84
+ enhance?: boolean;
31
85
  };
32
86
  type Voice = {
33
87
  id: string;
@@ -37,12 +91,16 @@ type Voice = {
37
91
  is_public: boolean;
38
92
  user_id: string;
39
93
  created_at: string;
94
+ language: string;
40
95
  };
41
96
  type CreateVoice = Pick<Voice, "name" | "description" | "embedding"> & Partial<Omit<Voice, "name" | "description" | "embedding">>;
97
+ type UpdateVoice = Partial<Pick<Voice, "name" | "description" | "embedding">>;
42
98
  type CloneResponse = {
43
99
  embedding: number[];
44
100
  };
45
101
  type WebSocketOptions = {
102
+ container?: string;
103
+ encoding?: string;
46
104
  sampleRate: number;
47
105
  };
48
106
  type SourceEventData = {
@@ -51,5 +109,7 @@ type SourceEventData = {
51
109
  wait: never;
52
110
  read: never;
53
111
  };
112
+ type TypedArray = Float32Array | Int16Array | Uint8Array;
113
+ type Encoding = "pcm_f32le" | "pcm_s16le" | "pcm_alaw" | "pcm_mulaw";
54
114
 
55
- export type { Chunk, ClientOptions, CloneOptions, CloneResponse, ConnectionEventData, CreateVoice, EmitteryCallbacks, Sentinel, SourceEventData, StreamRequest, Voice, WebSocketOptions };
115
+ export type { Chunk, ClientOptions, CloneOptions, CloneResponse, ConnectionEventData, CreateVoice, EmitteryCallbacks, Emotion, EmotionControl, Encoding, Intensity, Sentinel, SourceEventData, StreamOptions, StreamRequest, TypedArray, UpdateVoice, Voice, VoiceOptions, VoiceSpecifier, WebSocketBaseResponse, WebSocketChunkResponse, WebSocketErrorResponse, WebSocketOptions, WebSocketResponse, WebSocketTimestampsResponse, WordTimestamps };
@@ -1,7 +1,7 @@
1
1
  import emittery__default from 'emittery';
2
2
 
3
3
  interface ClientOptions {
4
- apiKey?: string;
4
+ apiKey?: string | (() => Promise<string>);
5
5
  baseUrl?: string;
6
6
  }
7
7
  type Sentinel = null;
@@ -10,12 +10,64 @@ type ConnectionEventData = {
10
10
  open: never;
11
11
  close: never;
12
12
  };
13
+ type VoiceSpecifier = {
14
+ mode: "id";
15
+ id: string;
16
+ } | {
17
+ mode: "embedding";
18
+ embedding: number[];
19
+ };
20
+ type Emotion = "anger" | "sadness" | "positivity" | "curiosity" | "surprise";
21
+ type Intensity = "lowest" | "low" | "high" | "highest";
22
+ type EmotionControl = Emotion | `${Emotion}:${Intensity}`;
23
+ type VoiceOptions = VoiceSpecifier & {
24
+ __experimental_controls?: {
25
+ speed?: "slowest" | "slow" | "normal" | "fast" | "fastest";
26
+ emotion?: EmotionControl[];
27
+ };
28
+ };
13
29
  type StreamRequest = {
14
- inputs: object;
15
- options: {
16
- timeout?: number;
30
+ model_id: string;
31
+ transcript: string;
32
+ voice: VoiceOptions;
33
+ output_format?: {
34
+ container: string;
35
+ encoding: string;
36
+ sample_rate: number;
17
37
  };
38
+ context_id?: string;
39
+ continue?: boolean;
40
+ duration?: number;
41
+ language?: string;
42
+ add_timestamps?: boolean;
43
+ };
44
+ type StreamOptions = {
45
+ timeout?: number;
46
+ };
47
+ type WebSocketBaseResponse = {
48
+ context_id: string;
49
+ status_code: number;
50
+ done: boolean;
51
+ };
52
+ type WordTimestamps = {
53
+ words: string[];
54
+ start: number[];
55
+ end: number[];
56
+ };
57
+ type WebSocketTimestampsResponse = WebSocketBaseResponse & {
58
+ type: "timestamps";
59
+ word_timestamps: WordTimestamps;
60
+ };
61
+ type WebSocketChunkResponse = WebSocketBaseResponse & {
62
+ type: "chunk";
63
+ data: string;
64
+ step_time: number;
65
+ };
66
+ type WebSocketErrorResponse = WebSocketBaseResponse & {
67
+ type: "error";
68
+ error: string;
18
69
  };
70
+ type WebSocketResponse = WebSocketTimestampsResponse | WebSocketChunkResponse | WebSocketErrorResponse;
19
71
  type EmitteryCallbacks<T> = {
20
72
  on: emittery__default<T>["on"];
21
73
  off: emittery__default<T>["off"];
@@ -25,9 +77,11 @@ type EmitteryCallbacks<T> = {
25
77
  type CloneOptions = {
26
78
  mode: "url";
27
79
  link: string;
80
+ enhance?: boolean;
28
81
  } | {
29
82
  mode: "clip";
30
83
  clip: Blob;
84
+ enhance?: boolean;
31
85
  };
32
86
  type Voice = {
33
87
  id: string;
@@ -37,12 +91,16 @@ type Voice = {
37
91
  is_public: boolean;
38
92
  user_id: string;
39
93
  created_at: string;
94
+ language: string;
40
95
  };
41
96
  type CreateVoice = Pick<Voice, "name" | "description" | "embedding"> & Partial<Omit<Voice, "name" | "description" | "embedding">>;
97
+ type UpdateVoice = Partial<Pick<Voice, "name" | "description" | "embedding">>;
42
98
  type CloneResponse = {
43
99
  embedding: number[];
44
100
  };
45
101
  type WebSocketOptions = {
102
+ container?: string;
103
+ encoding?: string;
46
104
  sampleRate: number;
47
105
  };
48
106
  type SourceEventData = {
@@ -51,5 +109,7 @@ type SourceEventData = {
51
109
  wait: never;
52
110
  read: never;
53
111
  };
112
+ type TypedArray = Float32Array | Int16Array | Uint8Array;
113
+ type Encoding = "pcm_f32le" | "pcm_s16le" | "pcm_alaw" | "pcm_mulaw";
54
114
 
55
- export type { Chunk, ClientOptions, CloneOptions, CloneResponse, ConnectionEventData, CreateVoice, EmitteryCallbacks, Sentinel, SourceEventData, StreamRequest, Voice, WebSocketOptions };
115
+ export type { Chunk, ClientOptions, CloneOptions, CloneResponse, ConnectionEventData, CreateVoice, EmitteryCallbacks, Emotion, EmotionControl, Encoding, Intensity, Sentinel, SourceEventData, StreamOptions, StreamRequest, TypedArray, UpdateVoice, Voice, VoiceOptions, VoiceSpecifier, WebSocketBaseResponse, WebSocketChunkResponse, WebSocketErrorResponse, WebSocketOptions, WebSocketResponse, WebSocketTimestampsResponse, WordTimestamps };
@@ -88,20 +88,25 @@ var constructApiUrl = (baseUrl, path, { websocket = false } = {}) => {
88
88
  // src/lib/client.ts
89
89
  var Client = class {
90
90
  constructor(options = {}) {
91
- if (!(options.apiKey || process.env.CARTESIA_API_KEY)) {
91
+ const apiKey = options.apiKey || process.env.CARTESIA_API_KEY;
92
+ if (!apiKey) {
92
93
  throw new Error("Missing Cartesia API key.");
93
94
  }
94
- this.apiKey = options.apiKey || process.env.CARTESIA_API_KEY;
95
+ this.apiKey = typeof apiKey === "function" ? apiKey : () => __async(this, null, function* () {
96
+ return apiKey;
97
+ });
95
98
  this.baseUrl = options.baseUrl || BASE_URL;
96
99
  }
97
- fetch(path, options = {}) {
98
- const url = constructApiUrl(this.baseUrl, path);
99
- return (0, import_cross_fetch.default)(url.toString(), __spreadProps(__spreadValues({}, options), {
100
- headers: __spreadValues({
101
- "X-API-Key": this.apiKey,
102
- "Cartesia-Version": CARTESIA_VERSION
103
- }, options.headers)
104
- }));
100
+ _fetch(_0) {
101
+ return __async(this, arguments, function* (path, options = {}) {
102
+ const url = constructApiUrl(this.baseUrl, path);
103
+ const headers = new Headers(options.headers);
104
+ headers.set("X-API-Key", yield this.apiKey());
105
+ headers.set("Cartesia-Version", CARTESIA_VERSION);
106
+ return (0, import_cross_fetch.default)(url.toString(), __spreadProps(__spreadValues({}, options), {
107
+ headers
108
+ }));
109
+ });
105
110
  }
106
111
  };
107
112
 
@@ -109,40 +114,43 @@ var Client = class {
109
114
  var Voices = class extends Client {
110
115
  list() {
111
116
  return __async(this, null, function* () {
112
- const response = yield this.fetch("/voices");
117
+ const response = yield this._fetch("/voices");
113
118
  return response.json();
114
119
  });
115
120
  }
116
121
  get(voiceId) {
117
122
  return __async(this, null, function* () {
118
- const response = yield this.fetch(`/voices/${voiceId}`);
123
+ const response = yield this._fetch(`/voices/${voiceId}`);
119
124
  return response.json();
120
125
  });
121
126
  }
122
127
  create(voice) {
123
128
  return __async(this, null, function* () {
124
- const response = yield this.fetch("/voices", {
129
+ const response = yield this._fetch("/voices", {
125
130
  method: "POST",
126
131
  body: JSON.stringify(voice)
127
132
  });
128
133
  return response.json();
129
134
  });
130
135
  }
136
+ update(id, voice) {
137
+ return __async(this, null, function* () {
138
+ const response = yield this._fetch(`/voices/${id}`, {
139
+ method: "PATCH",
140
+ body: JSON.stringify(voice)
141
+ });
142
+ return response.json();
143
+ });
144
+ }
131
145
  clone(options) {
132
146
  return __async(this, null, function* () {
133
- if (options.mode === "url") {
134
- const response = yield this.fetch(
135
- `/voices/clone/url?link=${options.link}`,
136
- {
137
- method: "POST"
138
- }
139
- );
140
- return response.json();
141
- }
142
147
  if (options.mode === "clip") {
143
148
  const formData = new FormData();
144
149
  formData.append("clip", options.clip);
145
- const response = yield this.fetch("/voices/clone/clip", {
150
+ if (options.enhance !== void 0) {
151
+ formData.append("enhance", options.enhance.toString());
152
+ }
153
+ const response = yield this._fetch("/voices/clone/clip", {
146
154
  method: "POST",
147
155
  body: formData
148
156
  });
@@ -1,11 +1,12 @@
1
1
  import { Client } from '../lib/client.cjs';
2
- import { Voice, CreateVoice, CloneOptions, CloneResponse } from '../types/index.cjs';
2
+ import { Voice, CreateVoice, UpdateVoice, CloneOptions, CloneResponse } from '../types/index.cjs';
3
3
  import 'emittery';
4
4
 
5
5
  declare class Voices extends Client {
6
6
  list(): Promise<Voice[]>;
7
7
  get(voiceId: string): Promise<Voice>;
8
8
  create(voice: CreateVoice): Promise<Voice>;
9
+ update(id: string, voice: UpdateVoice): Promise<Voice>;
9
10
  clone(options: CloneOptions): Promise<CloneResponse>;
10
11
  }
11
12
 
@@ -1,11 +1,12 @@
1
1
  import { Client } from '../lib/client.js';
2
- import { Voice, CreateVoice, CloneOptions, CloneResponse } from '../types/index.js';
2
+ import { Voice, CreateVoice, UpdateVoice, CloneOptions, CloneResponse } from '../types/index.js';
3
3
  import 'emittery';
4
4
 
5
5
  declare class Voices extends Client {
6
6
  list(): Promise<Voice[]>;
7
7
  get(voiceId: string): Promise<Voice>;
8
8
  create(voice: CreateVoice): Promise<Voice>;
9
+ update(id: string, voice: UpdateVoice): Promise<Voice>;
9
10
  clone(options: CloneOptions): Promise<CloneResponse>;
10
11
  }
11
12
 
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  Voices
3
- } from "../chunk-SGXUEFII.js";
4
- import "../chunk-PQ5EVEEH.js";
3
+ } from "../chunk-UCYL2SOX.js";
4
+ import "../chunk-PISCPZK4.js";
5
5
  import "../chunk-2BFEKY3F.js";
6
- import "../chunk-WIFMLPT5.js";
6
+ import "../chunk-GHY2WEOK.js";
7
7
  export {
8
8
  Voices as default
9
9
  };
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "name": "Cartesia",
5
5
  "url": "https://cartesia.ai"
6
6
  },
7
- "version": "1.0.0",
7
+ "version": "1.0.2",
8
8
  "description": "Client for the Cartesia API.",
9
9
  "type": "module",
10
10
  "module": "./dist/index.js",
package/src/index.ts CHANGED
@@ -2,3 +2,5 @@ export { Cartesia as default } from "./lib";
2
2
  export * from "./lib";
3
3
  export * from "./types";
4
4
  export { default as WebPlayer } from "./tts/player";
5
+ export { default as Source } from "./tts/source";
6
+ export { default as WebSocket } from "./tts/websocket";
package/src/lib/client.ts CHANGED
@@ -3,29 +3,29 @@ import type { ClientOptions } from "../types";
3
3
  import { BASE_URL, CARTESIA_VERSION, constructApiUrl } from "./constants";
4
4
 
5
5
  export class Client {
6
- apiKey: string;
6
+ apiKey: () => Promise<string>;
7
7
  baseUrl: string;
8
8
 
9
9
  constructor(options: ClientOptions = {}) {
10
- if (!(options.apiKey || process.env.CARTESIA_API_KEY)) {
10
+ const apiKey = options.apiKey || process.env.CARTESIA_API_KEY;
11
+ if (!apiKey) {
11
12
  throw new Error("Missing Cartesia API key.");
12
13
  }
13
14
 
14
- // biome-ignore lint/style/noNonNullAssertion: Guaranteed to be defined by the check above.
15
- this.apiKey = (options.apiKey || process.env.CARTESIA_API_KEY)!;
15
+ this.apiKey = typeof apiKey === "function" ? apiKey : async () => apiKey;
16
16
  this.baseUrl = options.baseUrl || BASE_URL;
17
17
  }
18
18
 
19
- fetch(path: string, options: RequestInit = {}) {
19
+ protected async _fetch(path: string, options: RequestInit = {}) {
20
20
  const url = constructApiUrl(this.baseUrl, path);
21
+ const headers = new Headers(options.headers);
22
+
23
+ headers.set("X-API-Key", await this.apiKey());
24
+ headers.set("Cartesia-Version", CARTESIA_VERSION);
21
25
 
22
26
  return fetch(url.toString(), {
23
27
  ...options,
24
- headers: {
25
- "X-API-Key": this.apiKey,
26
- "Cartesia-Version": CARTESIA_VERSION,
27
- ...options.headers,
28
- },
28
+ headers,
29
29
  });
30
30
  }
31
31
  }
@@ -4,12 +4,14 @@ import { Cartesia } from "../lib";
4
4
  import Player from "../tts/player";
5
5
  import type Source from "../tts/source";
6
6
  import type WebSocket from "../tts/websocket";
7
+ import type { StreamRequest } from "../types";
7
8
  import { pingServer } from "./utils";
8
9
 
9
10
  export type UseTTSOptions = {
10
- apiKey: string | null;
11
+ apiKey: string | (() => Promise<string>) | null;
11
12
  baseUrl?: string;
12
13
  sampleRate: number;
14
+ onError?: (error: Error) => void;
13
15
  };
14
16
 
15
17
  export type PlaybackStatus = "inactive" | "playing" | "paused" | "finished";
@@ -20,7 +22,7 @@ export type Metrics = {
20
22
  };
21
23
 
22
24
  export interface UseTTSReturn {
23
- buffer: (options: object) => Promise<void>;
25
+ buffer: (options: StreamRequest) => Promise<void>;
24
26
  play: (bufferDuration?: number) => Promise<void>;
25
27
  pause: () => Promise<void>;
26
28
  resume: () => Promise<void>;
@@ -47,6 +49,7 @@ export function useTTS({
47
49
  apiKey,
48
50
  baseUrl,
49
51
  sampleRate,
52
+ onError,
50
53
  }: UseTTSOptions): UseTTSReturn {
51
54
  if (typeof window === "undefined") {
52
55
  return {
@@ -72,7 +75,11 @@ export function useTTS({
72
75
  }
73
76
  const cartesia = new Cartesia({ apiKey, baseUrl });
74
77
  baseUrl = baseUrl ?? cartesia.baseUrl;
75
- return cartesia.tts.websocket({ sampleRate });
78
+ return cartesia.tts.websocket({
79
+ container: "raw",
80
+ encoding: "pcm_f32le",
81
+ sampleRate,
82
+ });
76
83
  }, [apiKey, baseUrl, sampleRate]);
77
84
  const websocketReturn = useRef<ReturnType<WebSocket["send"]> | null>(null);
78
85
  const player = useRef<Player | null>(null);
@@ -85,23 +92,35 @@ export function useTTS({
85
92
  const [messages, setMessages] = useState<Message[]>([]);
86
93
 
87
94
  const buffer = useCallback(
88
- async (options: object) => {
95
+ async (options: StreamRequest) => {
89
96
  websocketReturn.current?.stop(); // Abort the previous request if it exists.
90
97
 
91
- setMessages([]);
92
- setBufferStatus("buffering");
93
- websocketReturn.current = websocket?.send(options) ?? null;
94
- if (!websocketReturn.current) {
95
- return;
98
+ try {
99
+ setMessages([]);
100
+ setBufferStatus("buffering");
101
+ websocketReturn.current = websocket?.send(options) ?? null;
102
+ if (!websocketReturn.current) {
103
+ return;
104
+ }
105
+ const unsubscribe = websocketReturn.current.on("message", (message) => {
106
+ const parsedMessage = JSON.parse(message);
107
+ setMessages((messages) => [...messages, parsedMessage]);
108
+ if (parsedMessage.error) {
109
+ onError?.(new Error(parsedMessage.error));
110
+ }
111
+ });
112
+ await websocketReturn.current.source.once("close");
113
+ setBufferStatus("buffered");
114
+ unsubscribe();
115
+ } catch (error) {
116
+ if (error instanceof Error) {
117
+ onError?.(error);
118
+ } else {
119
+ console.error(error);
120
+ }
96
121
  }
97
- const unsubscribe = websocketReturn.current.on("message", (message) => {
98
- setMessages((messages) => [...messages, JSON.parse(message)]);
99
- });
100
- await websocketReturn.current.source.once("close");
101
- setBufferStatus("buffered");
102
- unsubscribe();
103
122
  },
104
- [websocket],
123
+ [websocket, onError],
105
124
  );
106
125
 
107
126
  const metrics = useMemo(() => {
@@ -173,64 +192,96 @@ export function useTTS({
173
192
  }, [websocket, baseUrl]);
174
193
 
175
194
  const play = useCallback(async () => {
176
- if (playbackStatus === "playing" || !websocketReturn.current) {
177
- return;
178
- }
179
- if (player.current) {
180
- // Stop the current player if it exists.
181
- await player.current.stop();
182
- }
195
+ try {
196
+ if (playbackStatus === "playing" || !websocketReturn.current) {
197
+ return;
198
+ }
199
+ if (player.current) {
200
+ // Stop the current player if it exists.
201
+ await player.current.stop();
202
+ }
183
203
 
184
- setPlaybackStatus("playing");
185
-
186
- const unsubscribes = [];
187
- unsubscribes.push(
188
- websocketReturn.current.source.on("wait", () => {
189
- setIsWaiting(true);
190
- }),
191
- );
192
- unsubscribes.push(
193
- websocketReturn.current.source.on("read", () => {
194
- setIsWaiting(false);
195
- }),
196
- );
197
-
198
- player.current = new Player({
199
- bufferDuration: bufferDuration ?? DEFAULT_BUFFER_DURATION,
200
- });
201
- // Wait for the playback to finish before setting isPlaying to false.
202
- await player.current.play(websocketReturn.current.source);
204
+ setPlaybackStatus("playing");
203
205
 
204
- for (const unsubscribe of unsubscribes) {
205
- // Deregister the event listeners (.on()) that we registered above to avoid memory leaks.
206
- unsubscribe();
207
- }
206
+ const unsubscribes = [];
207
+ unsubscribes.push(
208
+ websocketReturn.current.source.on("wait", () => {
209
+ setIsWaiting(true);
210
+ }),
211
+ );
212
+ unsubscribes.push(
213
+ websocketReturn.current.source.on("read", () => {
214
+ setIsWaiting(false);
215
+ }),
216
+ );
208
217
 
209
- setPlaybackStatus("finished");
210
- }, [playbackStatus, bufferDuration]);
218
+ player.current = new Player({
219
+ bufferDuration: bufferDuration ?? DEFAULT_BUFFER_DURATION,
220
+ });
221
+ // Wait for the playback to finish before setting isPlaying to false.
222
+ await player.current.play(websocketReturn.current.source);
223
+
224
+ for (const unsubscribe of unsubscribes) {
225
+ // Deregister the event listeners (.on()) that we registered above to avoid memory leaks.
226
+ unsubscribe();
227
+ }
228
+
229
+ setPlaybackStatus("finished");
230
+ } catch (error) {
231
+ if (error instanceof Error) {
232
+ onError?.(error);
233
+ } else {
234
+ console.error(error);
235
+ }
236
+ }
237
+ }, [playbackStatus, bufferDuration, onError]);
211
238
 
212
239
  const pause = useCallback(async () => {
213
- await player.current?.pause();
214
- setPlaybackStatus("paused");
215
- }, []);
240
+ try {
241
+ await player.current?.pause();
242
+ setPlaybackStatus("paused");
243
+ } catch (error) {
244
+ if (error instanceof Error) {
245
+ onError?.(error);
246
+ } else {
247
+ console.error(error);
248
+ }
249
+ }
250
+ }, [onError]);
216
251
 
217
252
  const resume = useCallback(async () => {
218
- await player.current?.resume();
219
- setPlaybackStatus("playing");
220
- }, []);
253
+ try {
254
+ await player.current?.resume();
255
+ setPlaybackStatus("playing");
256
+ } catch (error) {
257
+ if (error instanceof Error) {
258
+ onError?.(error);
259
+ } else {
260
+ console.error(error);
261
+ }
262
+ }
263
+ }, [onError]);
221
264
 
222
265
  const toggle = useCallback(async () => {
223
- await player.current?.toggle();
224
- setPlaybackStatus((status) => {
225
- if (status === "playing") {
226
- return "paused";
227
- }
228
- if (status === "paused") {
229
- return "playing";
266
+ try {
267
+ await player.current?.toggle();
268
+ setPlaybackStatus((status) => {
269
+ if (status === "playing") {
270
+ return "paused";
271
+ }
272
+ if (status === "paused") {
273
+ return "playing";
274
+ }
275
+ return status;
276
+ });
277
+ } catch (error) {
278
+ if (error instanceof Error) {
279
+ onError?.(error);
280
+ } else {
281
+ console.error(error);
230
282
  }
231
- return status;
232
- });
233
- }, []);
283
+ }
284
+ }, [onError]);
234
285
 
235
286
  return {
236
287
  buffer,