@breeze.blue/sdk 0.4.0 → 0.5.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/CHANGELOG.md +64 -2
- package/README.md +105 -1
- package/dist/audio.d.ts +7 -1
- package/dist/audio.js +11 -2
- package/dist/client.d.ts +41 -4
- package/dist/client.js +263 -10
- package/dist/errors.d.ts +26 -0
- package/dist/errors.js +26 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/node.js +9 -1
- package/dist/types.d.ts +151 -7
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,71 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.0
|
|
4
|
+
|
|
5
|
+
- Fixed `textToSpeech.realtime.connect(...)` so a failed WebSocket handshake
|
|
6
|
+
rejects instead of hanging: added a handshake timeout (connect `timeout`,
|
|
7
|
+
falling back to the client `timeout`), rejected clean closes that arrive
|
|
8
|
+
before the socket opens, and closed the socket on every failure path.
|
|
9
|
+
- Breaking (compile time): the client constructor accepts a missing API key,
|
|
10
|
+
so `BreezeBlueClient.apiKey` is now typed `string | undefined` instead of
|
|
11
|
+
`string`. The constructor no longer throws on a missing key; the first REST
|
|
12
|
+
call throws `BreezeBlueConfigurationError` instead, so browser clients can
|
|
13
|
+
use `textToSpeech.realtime.connect({ clientSecret })` without a key. Code
|
|
14
|
+
that read `client.apiKey` as a `string` must narrow it first, and code that
|
|
15
|
+
relied on the constructor throwing must handle the error at the first
|
|
16
|
+
request.
|
|
17
|
+
- Bounded the realtime receive buffer at 16 MiB. If messages are not consumed,
|
|
18
|
+
the SDK closes the connection and the message iterator rejects with a
|
|
19
|
+
`BreezeBlueRealtimeError`.
|
|
20
|
+
- Added `BreezeBlueRealtimeError` for realtime connection failures the SDK
|
|
21
|
+
detects (invalid server frames, receive-buffer overflow). It carries the
|
|
22
|
+
same fields as the Python SDK's `RealtimeError` — `code`, `meta`,
|
|
23
|
+
`closeCode`, `reconnect`, and `recoverable` (`true` only when just the
|
|
24
|
+
active turn was cancelled and the connection stays usable; SDK-detected
|
|
25
|
+
connection failures are never recoverable).
|
|
26
|
+
- Fixed invalid JSON text frames on a realtime connection: instead of the
|
|
27
|
+
`JSON.parse` exception escaping from the WebSocket `onmessage` callback
|
|
28
|
+
(leaving the connection neither failed nor finished), the SDK now fails the
|
|
29
|
+
connection and the message iterator rejects with a
|
|
30
|
+
`BreezeBlueRealtimeError`, matching the Python SDK.
|
|
31
|
+
- Added `connection.ping()` for realtime keepalive; the server answers with a
|
|
32
|
+
`pong` event.
|
|
33
|
+
- Guarded `connection.close()` so it no longer throws when the underlying
|
|
34
|
+
WebSocket is not open.
|
|
35
|
+
- Breaking (compile time): `RealtimeTextToSpeechEvent` changed from an open
|
|
36
|
+
record (`{ type: string; [key: string]: unknown }`) to a discriminated
|
|
37
|
+
union of per-event interfaces with camelCase fields (`turnId`,
|
|
38
|
+
`historyItemId`, `ttfaMs`, ...). Code that indexed arbitrary keys on an
|
|
39
|
+
event must narrow on `event.type` first (or cast). The union is
|
|
40
|
+
non-exhaustive by design: unknown future event types are still delivered
|
|
41
|
+
at runtime, so do not rely on an exhaustive `switch` — ignore event types
|
|
42
|
+
you do not recognize.
|
|
43
|
+
- Exported the `RealtimeTextToSpeechConnection` class from the root
|
|
44
|
+
entrypoint.
|
|
45
|
+
- `realtime.connect` now warns when session parameters (`modelId`,
|
|
46
|
+
`voiceSettings`, ...) are passed together with `clientSecret` or
|
|
47
|
+
`websocketUrl`, which ignore them.
|
|
48
|
+
- Clarified the missing-WebSocket error and README: realtime needs a global
|
|
49
|
+
WebSocket (browsers, edge runtimes, Node 22+) or a `webSocket` constructor
|
|
50
|
+
passed to the client.
|
|
51
|
+
- Reworked the README realtime examples into a consume-and-close loop and
|
|
52
|
+
added keepalive, pre-created session, and browser `clientSecret` handoff
|
|
53
|
+
examples.
|
|
54
|
+
|
|
55
|
+
## 0.4.1
|
|
56
|
+
|
|
57
|
+
- Renamed the instruction-only text-to-speech helper to
|
|
58
|
+
`client.textToSpeech.enhance(...)` and aligned it with
|
|
59
|
+
`/v1/text-to-speech/enhance`.
|
|
60
|
+
- Documented the streaming text-to-speech default as `pcm`, added `wav`
|
|
61
|
+
streaming examples, and taught the Node `stream(...)` helper to pass raw PCM
|
|
62
|
+
playback options to mpv.
|
|
63
|
+
- Added realtime conversation text-to-speech helpers under
|
|
64
|
+
`client.textToSpeech.realtime` for fixed-PCM WebSocket sessions.
|
|
65
|
+
|
|
3
66
|
## 0.4.0
|
|
4
67
|
|
|
5
|
-
- Added
|
|
6
|
-
text-to-speech AI Rewrite.
|
|
68
|
+
- Added an instruction-only text-to-speech helper.
|
|
7
69
|
- Added `client.voices.random()` for fetching one visible voice to use as a
|
|
8
70
|
text-to-speech reference voice.
|
|
9
71
|
- Added account usage filters for API key, status class, client type, and date
|
package/README.md
CHANGED
|
@@ -30,6 +30,10 @@ export BREEZE_API_KEY=brz_...
|
|
|
30
30
|
The SDK sends the key with the `xi-api-key` header. It also sends
|
|
31
31
|
`x-breeze-sdk` for server-side observability.
|
|
32
32
|
|
|
33
|
+
An API key is required for every REST method. The one exception is
|
|
34
|
+
`textToSpeech.realtime.connect(...)` with a `clientSecret`, which lets a
|
|
35
|
+
browser client run without a key (see the realtime section below).
|
|
36
|
+
|
|
33
37
|
## Quickstart
|
|
34
38
|
|
|
35
39
|
```ts
|
|
@@ -74,7 +78,7 @@ const audioStream = await client.textToSpeech.stream("voc_...", {
|
|
|
74
78
|
text: "Stream this line.",
|
|
75
79
|
});
|
|
76
80
|
|
|
77
|
-
const
|
|
81
|
+
const enhanced = await client.textToSpeech.enhance({
|
|
78
82
|
instruction: "Calm, warm, bedtime narration.",
|
|
79
83
|
languageCode: "en",
|
|
80
84
|
});
|
|
@@ -100,6 +104,102 @@ if (status.status === "ready") {
|
|
|
100
104
|
If the job is still active, `downloadAudio(...)` rejects with
|
|
101
105
|
`BreezeBlueGenerationNotReadyError`; read `error.retryAfter` before retrying.
|
|
102
106
|
|
|
107
|
+
Use realtime text-to-speech when one WebSocket connection should handle multiple
|
|
108
|
+
conversation turns. Realtime audio is fixed to raw `pcm_s16le`, 24000 Hz, mono,
|
|
109
|
+
16-bit frames.
|
|
110
|
+
|
|
111
|
+
Realtime needs a `WebSocket` implementation. Browsers, edge runtimes, and
|
|
112
|
+
Node 22+ provide a global one; on Node 20, pass a constructor (for example the
|
|
113
|
+
`ws` package's `WebSocket`) via `new BreezeBlueClient({ webSocket })`.
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
const connection = await client.textToSpeech.realtime.connect("voc_...", {
|
|
117
|
+
modelId: "bluebell-v1",
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
connection.startTurn("turn_1");
|
|
121
|
+
connection.appendText("Hello from Breeze.");
|
|
122
|
+
connection.flush();
|
|
123
|
+
connection.endTurn();
|
|
124
|
+
|
|
125
|
+
const pcmChunks: Uint8Array[] = [];
|
|
126
|
+
for await (const message of connection) {
|
|
127
|
+
if (message.type === "audio") {
|
|
128
|
+
pcmChunks.push(message.audio);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (message.type === "error") {
|
|
132
|
+
throw new Error(`Realtime TTS failed: ${message.code}: ${message.message}`);
|
|
133
|
+
}
|
|
134
|
+
if (message.type === "turn.done") {
|
|
135
|
+
break; // all audio for this turn has been delivered
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
connection.close();
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Always consume the connection (or its `audio()` / `events()` iterators) while a
|
|
142
|
+
turn is active, and call `close()` when you are done. When the SDK detects a
|
|
143
|
+
connection failure — an invalid server frame, or more than 16 MiB of messages
|
|
144
|
+
piling up unconsumed — it closes the connection and the iterator rejects with
|
|
145
|
+
a `BreezeBlueRealtimeError`.
|
|
146
|
+
|
|
147
|
+
Events are a typed discriminated union (`session.ready`, `turn.started`,
|
|
148
|
+
`audio.started`, `turn.done`, `turn.cancelled`, `usage.committed`,
|
|
149
|
+
`session.closed`, `error`, `pong`), with camelCase fields such as `turnId`,
|
|
150
|
+
`historyItemId`, and `ttfaMs`. Treat the union as non-exhaustive: the server
|
|
151
|
+
may add event types, and the SDK delivers unknown JSON events unchanged —
|
|
152
|
+
ignore event types you do not recognize instead of switching exhaustively.
|
|
153
|
+
|
|
154
|
+
For connections that sit idle between turns, send a keepalive ping inside the
|
|
155
|
+
session's `inactivityTimeoutSeconds` window; the server answers with a `pong`
|
|
156
|
+
event:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
const keepalive = setInterval(() => connection.ping(), 30_000);
|
|
160
|
+
// ... run turns ...
|
|
161
|
+
clearInterval(keepalive);
|
|
162
|
+
connection.close();
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
To cut time to first audio, create the session ahead of time (for example while
|
|
166
|
+
your app is still preparing the turn) and connect with its `clientSecret` when
|
|
167
|
+
the first text is ready — only the WebSocket handshake remains:
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
const session = await client.textToSpeech.realtime.createSession("voc_...", {
|
|
171
|
+
modelId: "bluebell-v1",
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Later, when the first text is ready:
|
|
175
|
+
const connection = await client.textToSpeech.realtime.connect("voc_...", {
|
|
176
|
+
clientSecret: session.clientSecret,
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
The same `clientSecret` handoff lets a browser connect without ever seeing your
|
|
181
|
+
API key: create the session on your server, hand `session.clientSecret` to the
|
|
182
|
+
page, and build a key-less client there:
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
// Browser — no API key required for clientSecret connections.
|
|
186
|
+
const browserClient = new BreezeBlueClient();
|
|
187
|
+
const connection = await browserClient.textToSpeech.realtime.connect("voc_...", {
|
|
188
|
+
clientSecret, // received from your server
|
|
189
|
+
});
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Session parameters (`modelId`, `languageCode`, `instructions`, `voiceSettings`,
|
|
193
|
+
`inactivityTimeoutSeconds`, `enableLogging`) are fixed when the session is
|
|
194
|
+
created. `connect` ignores them when `clientSecret` or `websocketUrl` is
|
|
195
|
+
provided and logs a warning.
|
|
196
|
+
|
|
197
|
+
If a realtime WebSocket is interrupted by a network change, service deployment,
|
|
198
|
+
or upstream realtime worker restart, handle `error.meta.reconnect === true` or a
|
|
199
|
+
`session.closed` event with `reconnect === true` by creating a new connection and
|
|
200
|
+
starting a new turn from your own conversation state. Active turns are not
|
|
201
|
+
resumed in place.
|
|
202
|
+
|
|
103
203
|
The API uses the default text-to-speech model when `modelId` is omitted. If
|
|
104
204
|
you need to select a model explicitly, call `client.models.list()` and pass one
|
|
105
205
|
of the returned `modelId` values.
|
|
@@ -130,6 +230,10 @@ await save(audio, "x.mp3");
|
|
|
130
230
|
await stream(audioStream); // mpv
|
|
131
231
|
```
|
|
132
232
|
|
|
233
|
+
Streaming text-to-speech defaults to `pcm` to reduce time to first audio. Pass
|
|
234
|
+
`{ outputFormat: "wav" }` or `{ outputFormat: "mp3" }` when you need that wire
|
|
235
|
+
format explicitly.
|
|
236
|
+
|
|
133
237
|
## Voices
|
|
134
238
|
|
|
135
239
|
Browse existing voices and inspect a single voice:
|
package/dist/audio.d.ts
CHANGED
|
@@ -10,6 +10,12 @@ export interface StreamPlaybackCommand {
|
|
|
10
10
|
command: string;
|
|
11
11
|
args: string[];
|
|
12
12
|
}
|
|
13
|
+
export interface StreamPlaybackCommandOptions {
|
|
14
|
+
contentType?: string | null;
|
|
15
|
+
sampleRate?: number | null;
|
|
16
|
+
channels?: number | null;
|
|
17
|
+
sampleFormat?: string | null;
|
|
18
|
+
}
|
|
13
19
|
export interface AudioResponseOptions {
|
|
14
20
|
filename?: string | null;
|
|
15
21
|
}
|
|
@@ -45,5 +51,5 @@ export declare function selectAudioPlaybackCommand(params: {
|
|
|
45
51
|
availability: CommandAvailability;
|
|
46
52
|
platform: string;
|
|
47
53
|
}): PlaybackCommand;
|
|
48
|
-
export declare function selectStreamPlaybackCommand(): StreamPlaybackCommand;
|
|
54
|
+
export declare function selectStreamPlaybackCommand(options?: StreamPlaybackCommandOptions): StreamPlaybackCommand;
|
|
49
55
|
export declare function extensionForContentType(contentType: string): string;
|
package/dist/audio.js
CHANGED
|
@@ -55,8 +55,17 @@ export function selectAudioPlaybackCommand(params) {
|
|
|
55
55
|
}
|
|
56
56
|
throw new Error("No supported audio player found. Install ffplay, or use afplay on macOS.");
|
|
57
57
|
}
|
|
58
|
-
export function selectStreamPlaybackCommand() {
|
|
59
|
-
|
|
58
|
+
export function selectStreamPlaybackCommand(options = {}) {
|
|
59
|
+
const args = ["--no-terminal", "--really-quiet"];
|
|
60
|
+
if (isPcmContentType(options.contentType)) {
|
|
61
|
+
args.push("--demuxer=rawaudio", `--demuxer-rawaudio-rate=${options.sampleRate ?? 24000}`, `--demuxer-rawaudio-channels=${options.channels ?? 1}`, `--demuxer-rawaudio-format=${options.sampleFormat ?? "s16le"}`);
|
|
62
|
+
}
|
|
63
|
+
args.push("-");
|
|
64
|
+
return { command: "mpv", args };
|
|
65
|
+
}
|
|
66
|
+
function isPcmContentType(contentType) {
|
|
67
|
+
const mediaType = (contentType ?? "").split(";", 1)[0].trim().toLowerCase();
|
|
68
|
+
return mediaType === "audio/pcm" || mediaType === "audio/x-pcm" || mediaType.endsWith("/pcm");
|
|
60
69
|
}
|
|
61
70
|
function nodeHelperError(action) {
|
|
62
71
|
return new Error(`Use @breeze.blue/sdk/node to ${action} in Node.js runtimes.`);
|
package/dist/client.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { AudioResponse } from "./audio.js";
|
|
2
|
-
import type { AudioRequestOptions, AsyncTextToSpeechJob, Balance, BreezeBlueClientOptions, GenericStatus, GenerationJob, HistoryItem, HistoryList, HistoryListParams, Model, RequestOptions, SaveVoiceRequest, StreamTextToSpeechOptions, TextToSpeechRequest,
|
|
2
|
+
import type { AudioRequestOptions, BreezeBlueWebSocket, AsyncTextToSpeechJob, Balance, BreezeBlueClientOptions, GenericStatus, GenerationJob, HistoryItem, HistoryList, HistoryListParams, Model, RequestOptions, RealtimeTextToSpeechConnectOptions, RealtimeTextToSpeechEvent, RealtimeTextToSpeechMessage, RealtimeTextToSpeechSession, RealtimeTextToSpeechSessionRequest, SavedVoice, SaveVoiceRequest, StreamTextToSpeechOptions, TextToSpeechRequest, TtsEnhanceRequest, TtsEnhanceResponse, Usage, UsageParams, Voice, VoiceClonePreview, VoiceClonePreviewRequest, VoiceDesignRequest, VoiceDesignResponse, VoiceEditRequest, VoiceList, VoiceSearchParams, VoiceSettings } from "./types.js";
|
|
3
3
|
export declare class BreezeBlueClient {
|
|
4
|
-
readonly apiKey: string;
|
|
4
|
+
readonly apiKey: string | undefined;
|
|
5
5
|
readonly baseUrl: string;
|
|
6
6
|
readonly fetch: typeof fetch;
|
|
7
|
+
readonly webSocket: BreezeBlueClientOptions["webSocket"];
|
|
7
8
|
readonly headers: HeadersInit | undefined;
|
|
8
9
|
readonly timeout: number | undefined;
|
|
9
10
|
readonly textToSpeech: TextToSpeechResource;
|
|
@@ -20,11 +21,47 @@ export declare class BreezeBlueClient {
|
|
|
20
21
|
}
|
|
21
22
|
declare class TextToSpeechResource {
|
|
22
23
|
private readonly client;
|
|
24
|
+
readonly realtime: RealtimeTextToSpeechResource;
|
|
23
25
|
constructor(client: BreezeBlueClient);
|
|
24
26
|
convert(voiceId: string, request: TextToSpeechRequest, options?: AudioRequestOptions): Promise<AudioResponse>;
|
|
25
27
|
createJob(voiceId: string, request: TextToSpeechRequest, options?: AudioRequestOptions): Promise<AsyncTextToSpeechJob>;
|
|
26
28
|
stream(voiceId: string, request: TextToSpeechRequest, options?: StreamTextToSpeechOptions): Promise<AudioResponse>;
|
|
27
|
-
|
|
29
|
+
enhance(request: TtsEnhanceRequest, options?: RequestOptions): Promise<TtsEnhanceResponse>;
|
|
30
|
+
}
|
|
31
|
+
declare class RealtimeTextToSpeechResource {
|
|
32
|
+
private readonly client;
|
|
33
|
+
constructor(client: BreezeBlueClient);
|
|
34
|
+
createSession(voiceId: string, request?: RealtimeTextToSpeechSessionRequest, options?: RequestOptions): Promise<RealtimeTextToSpeechSession>;
|
|
35
|
+
connect(voiceId: string, options?: RealtimeTextToSpeechConnectOptions): Promise<RealtimeTextToSpeechConnection>;
|
|
36
|
+
}
|
|
37
|
+
export declare class RealtimeTextToSpeechConnection implements AsyncIterable<RealtimeTextToSpeechMessage> {
|
|
38
|
+
private readonly socket;
|
|
39
|
+
private readonly queue;
|
|
40
|
+
private readonly waiters;
|
|
41
|
+
private closed;
|
|
42
|
+
private queuedBytes;
|
|
43
|
+
private failure;
|
|
44
|
+
private constructor();
|
|
45
|
+
static open(socket: BreezeBlueWebSocket, options?: {
|
|
46
|
+
timeout?: number;
|
|
47
|
+
}): Promise<RealtimeTextToSpeechConnection>;
|
|
48
|
+
startTurn(turnId?: string): void;
|
|
49
|
+
appendText(text: string): void;
|
|
50
|
+
flush(): void;
|
|
51
|
+
endTurn(): void;
|
|
52
|
+
cancelTurn(): void;
|
|
53
|
+
/** Send a keepalive ping. The server answers with a `pong` event. */
|
|
54
|
+
ping(): void;
|
|
55
|
+
close(): void;
|
|
56
|
+
events(): AsyncIterable<RealtimeTextToSpeechEvent>;
|
|
57
|
+
audio(): AsyncIterable<Uint8Array>;
|
|
58
|
+
[Symbol.asyncIterator](): AsyncIterator<RealtimeTextToSpeechMessage>;
|
|
59
|
+
private sendJson;
|
|
60
|
+
private handleMessage;
|
|
61
|
+
private handleClose;
|
|
62
|
+
private push;
|
|
63
|
+
private fail;
|
|
64
|
+
private finish;
|
|
28
65
|
}
|
|
29
66
|
declare class GenerationJobsResource {
|
|
30
67
|
private readonly client;
|
|
@@ -65,7 +102,7 @@ declare class VoicesResource {
|
|
|
65
102
|
/** Stream the audio of a clone or design preview before saving. */
|
|
66
103
|
streamPreview(generatedVoiceId: string, options?: AudioRequestOptions): Promise<AudioResponse>;
|
|
67
104
|
/** Persist a clone or design preview as a real voice asset. */
|
|
68
|
-
savePreview(request: SaveVoiceRequest, options?: RequestOptions): Promise<
|
|
105
|
+
savePreview(request: SaveVoiceRequest, options?: RequestOptions): Promise<SavedVoice>;
|
|
69
106
|
}
|
|
70
107
|
declare class HistoryResource {
|
|
71
108
|
private readonly client;
|
package/dist/client.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { AudioResponse } from "./audio.js";
|
|
2
|
-
import { BreezeBlueConfigurationError, errorFromResponse } from "./errors.js";
|
|
2
|
+
import { BreezeBlueConfigurationError, BreezeBlueRealtimeError, errorFromResponse } from "./errors.js";
|
|
3
3
|
import { camelizeKeys, snakeizeKeys } from "./_transform.js";
|
|
4
4
|
import { SDK_NAME, SDK_VERSION } from "./version.js";
|
|
5
5
|
const DEFAULT_BASE_URL = "https://api.breeze.blue";
|
|
6
6
|
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
7
|
+
const DEFAULT_REALTIME_HANDSHAKE_TIMEOUT_MS = 30_000;
|
|
8
|
+
const REALTIME_MAX_BUFFERED_BYTES = 16 * 1024 * 1024;
|
|
9
|
+
const WEBSOCKET_OPEN = 1;
|
|
7
10
|
export class BreezeBlueClient {
|
|
8
11
|
apiKey;
|
|
9
12
|
baseUrl;
|
|
10
13
|
fetch;
|
|
14
|
+
webSocket;
|
|
11
15
|
headers;
|
|
12
16
|
timeout;
|
|
13
17
|
textToSpeech;
|
|
@@ -19,13 +23,12 @@ export class BreezeBlueClient {
|
|
|
19
23
|
constructor(options = {}) {
|
|
20
24
|
const envApiKey = typeof process !== "undefined" ? process.env.BREEZE_API_KEY : undefined;
|
|
21
25
|
const envBaseUrl = typeof process !== "undefined" ? process.env.BREEZE_BASE_URL : undefined;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
this.apiKey = apiKey;
|
|
26
|
+
// An API key is only required for REST calls; realtime.connect with a
|
|
27
|
+
// clientSecret works without one, so missing keys are reported lazily.
|
|
28
|
+
this.apiKey = options.apiKey ?? envApiKey;
|
|
27
29
|
this.baseUrl = normalizeBaseUrl(options.baseUrl ?? envBaseUrl ?? DEFAULT_BASE_URL);
|
|
28
30
|
this.fetch = options.fetch ?? globalThis.fetch;
|
|
31
|
+
this.webSocket = options.webSocket ?? readGlobalWebSocket();
|
|
29
32
|
this.headers = options.headers;
|
|
30
33
|
this.timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
31
34
|
this.textToSpeech = new TextToSpeechResource(this);
|
|
@@ -48,6 +51,10 @@ export class BreezeBlueClient {
|
|
|
48
51
|
return new AudioResponse(response);
|
|
49
52
|
}
|
|
50
53
|
async request(method, path, params, body, options) {
|
|
54
|
+
if (!this.apiKey) {
|
|
55
|
+
throw new BreezeBlueConfigurationError("Missing Breeze API key. Pass apiKey or set BREEZE_API_KEY. " +
|
|
56
|
+
"(Only textToSpeech.realtime.connect with a clientSecret works without one.)");
|
|
57
|
+
}
|
|
51
58
|
const signal = buildAbortSignal(options?.signal, options?.timeout ?? this.timeout);
|
|
52
59
|
const wireParams = params ? snakeizeKeys(params) : undefined;
|
|
53
60
|
const wireBody = transformRequestBody(body);
|
|
@@ -69,7 +76,9 @@ export class BreezeBlueClient {
|
|
|
69
76
|
}
|
|
70
77
|
buildHeaders(body, requestHeaders) {
|
|
71
78
|
const headers = new Headers(this.headers);
|
|
72
|
-
|
|
79
|
+
if (this.apiKey) {
|
|
80
|
+
headers.set("xi-api-key", this.apiKey);
|
|
81
|
+
}
|
|
73
82
|
headers.set("x-breeze-sdk", `${SDK_NAME}/${SDK_VERSION}`);
|
|
74
83
|
mergeHeaders(headers, requestHeaders);
|
|
75
84
|
if (body !== undefined && !(body instanceof FormData) && !headers.has("content-type")) {
|
|
@@ -80,8 +89,10 @@ export class BreezeBlueClient {
|
|
|
80
89
|
}
|
|
81
90
|
class TextToSpeechResource {
|
|
82
91
|
client;
|
|
92
|
+
realtime;
|
|
83
93
|
constructor(client) {
|
|
84
94
|
this.client = client;
|
|
95
|
+
this.realtime = new RealtimeTextToSpeechResource(client);
|
|
85
96
|
}
|
|
86
97
|
convert(voiceId, request, options = {}) {
|
|
87
98
|
return this.client.requestAudio("POST", `/v1/text-to-speech/${encodeURIComponent(voiceId)}`, { outputFormat: options.outputFormat }, request, options);
|
|
@@ -96,8 +107,235 @@ class TextToSpeechResource {
|
|
|
96
107
|
enableLogging: options.enableLogging,
|
|
97
108
|
}, request, options);
|
|
98
109
|
}
|
|
99
|
-
|
|
100
|
-
return this.client.requestJson("POST", "/v1/text-to-speech/
|
|
110
|
+
enhance(request, options) {
|
|
111
|
+
return this.client.requestJson("POST", "/v1/text-to-speech/enhance", undefined, request, options);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
class RealtimeTextToSpeechResource {
|
|
115
|
+
client;
|
|
116
|
+
constructor(client) {
|
|
117
|
+
this.client = client;
|
|
118
|
+
}
|
|
119
|
+
createSession(voiceId, request = {}, options) {
|
|
120
|
+
return this.client.requestJson("POST", `/v1/text-to-speech/${encodeURIComponent(voiceId)}/realtime-sessions`, undefined, request, options);
|
|
121
|
+
}
|
|
122
|
+
async connect(voiceId, options = {}) {
|
|
123
|
+
if ((options.clientSecret !== undefined || options.websocketUrl !== undefined) && hasRealtimeSessionParams(options)) {
|
|
124
|
+
console.warn("@breeze.blue/sdk: realtime.connect ignores modelId, languageCode, instructions, voiceSettings, " +
|
|
125
|
+
"inactivityTimeoutSeconds, and enableLogging when clientSecret or websocketUrl is provided. " +
|
|
126
|
+
"Configure these when creating the realtime session instead.");
|
|
127
|
+
}
|
|
128
|
+
const websocketUrl = options.websocketUrl ??
|
|
129
|
+
(options.clientSecret
|
|
130
|
+
? buildUrl(this.client.baseUrl.replace(/^http/i, "ws"), `/v1/text-to-speech/${encodeURIComponent(voiceId)}/stream-input`, { client_secret: options.clientSecret })
|
|
131
|
+
: (await this.createSession(voiceId, {
|
|
132
|
+
modelId: options.modelId,
|
|
133
|
+
languageCode: options.languageCode,
|
|
134
|
+
instructions: options.instructions,
|
|
135
|
+
voiceSettings: options.voiceSettings,
|
|
136
|
+
inactivityTimeoutSeconds: options.inactivityTimeoutSeconds,
|
|
137
|
+
enableLogging: options.enableLogging,
|
|
138
|
+
})).websocketUrl);
|
|
139
|
+
const WebSocketImpl = requireWebSocket(options.webSocket ?? this.client.webSocket);
|
|
140
|
+
const socket = new WebSocketImpl(websocketUrl);
|
|
141
|
+
return RealtimeTextToSpeechConnection.open(socket, {
|
|
142
|
+
timeout: options.timeout ?? this.client.timeout ?? DEFAULT_REALTIME_HANDSHAKE_TIMEOUT_MS,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function hasRealtimeSessionParams(options) {
|
|
147
|
+
return (options.modelId !== undefined ||
|
|
148
|
+
options.languageCode !== undefined ||
|
|
149
|
+
options.instructions !== undefined ||
|
|
150
|
+
options.voiceSettings !== undefined ||
|
|
151
|
+
options.inactivityTimeoutSeconds !== undefined ||
|
|
152
|
+
options.enableLogging !== undefined);
|
|
153
|
+
}
|
|
154
|
+
export class RealtimeTextToSpeechConnection {
|
|
155
|
+
socket;
|
|
156
|
+
queue = [];
|
|
157
|
+
waiters = [];
|
|
158
|
+
closed = false;
|
|
159
|
+
queuedBytes = 0;
|
|
160
|
+
failure;
|
|
161
|
+
constructor(socket) {
|
|
162
|
+
this.socket = socket;
|
|
163
|
+
}
|
|
164
|
+
static async open(socket, options = {}) {
|
|
165
|
+
const connection = new RealtimeTextToSpeechConnection(socket);
|
|
166
|
+
const timeoutMs = options.timeout ?? DEFAULT_REALTIME_HANDSHAKE_TIMEOUT_MS;
|
|
167
|
+
await new Promise((resolve, reject) => {
|
|
168
|
+
let settled = false;
|
|
169
|
+
let timer;
|
|
170
|
+
const succeed = () => {
|
|
171
|
+
if (settled)
|
|
172
|
+
return;
|
|
173
|
+
settled = true;
|
|
174
|
+
if (timer !== undefined)
|
|
175
|
+
clearTimeout(timer);
|
|
176
|
+
resolve();
|
|
177
|
+
};
|
|
178
|
+
const fail = (error) => {
|
|
179
|
+
if (settled)
|
|
180
|
+
return;
|
|
181
|
+
settled = true;
|
|
182
|
+
if (timer !== undefined)
|
|
183
|
+
clearTimeout(timer);
|
|
184
|
+
try {
|
|
185
|
+
socket.close(1000);
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
// The socket may already be closed; opening failed either way.
|
|
189
|
+
}
|
|
190
|
+
reject(error);
|
|
191
|
+
};
|
|
192
|
+
if (timeoutMs > 0) {
|
|
193
|
+
timer = setTimeout(() => fail(new Error(`Realtime TTS WebSocket did not open within ${timeoutMs}ms.`)), timeoutMs);
|
|
194
|
+
}
|
|
195
|
+
socket.binaryType = "arraybuffer";
|
|
196
|
+
socket.onopen = () => succeed();
|
|
197
|
+
socket.onerror = (event) => fail(new Error(`Realtime TTS WebSocket failed to open: ${String(event.type)}`));
|
|
198
|
+
socket.onclose = (event) => fail(new Error(`Realtime TTS WebSocket closed before opening: ${event.code} ${event.reason}`.trim()));
|
|
199
|
+
});
|
|
200
|
+
socket.onmessage = (event) => connection.handleMessage(event.data);
|
|
201
|
+
socket.onclose = (event) => connection.handleClose(event);
|
|
202
|
+
socket.onerror = () => connection.finish();
|
|
203
|
+
return connection;
|
|
204
|
+
}
|
|
205
|
+
startTurn(turnId) {
|
|
206
|
+
this.sendJson({ type: "turn.start", ...(turnId ? { turn_id: turnId } : {}) });
|
|
207
|
+
}
|
|
208
|
+
appendText(text) {
|
|
209
|
+
this.sendJson({ type: "text.append", text });
|
|
210
|
+
}
|
|
211
|
+
flush() {
|
|
212
|
+
this.sendJson({ type: "text.flush" });
|
|
213
|
+
}
|
|
214
|
+
endTurn() {
|
|
215
|
+
this.sendJson({ type: "turn.end" });
|
|
216
|
+
}
|
|
217
|
+
cancelTurn() {
|
|
218
|
+
this.sendJson({ type: "turn.cancel" });
|
|
219
|
+
}
|
|
220
|
+
/** Send a keepalive ping. The server answers with a `pong` event. */
|
|
221
|
+
ping() {
|
|
222
|
+
this.sendJson({ type: "ping" });
|
|
223
|
+
}
|
|
224
|
+
close() {
|
|
225
|
+
if (this.closed) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (this.socket.readyState === WEBSOCKET_OPEN) {
|
|
229
|
+
this.sendJson({ type: "session.close" });
|
|
230
|
+
}
|
|
231
|
+
this.socket.close(1000);
|
|
232
|
+
this.finish();
|
|
233
|
+
}
|
|
234
|
+
async *events() {
|
|
235
|
+
for await (const message of this) {
|
|
236
|
+
if (message.type !== "audio") {
|
|
237
|
+
yield message;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
async *audio() {
|
|
242
|
+
for await (const message of this) {
|
|
243
|
+
if (message.type === "audio") {
|
|
244
|
+
yield message.audio;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
[Symbol.asyncIterator]() {
|
|
249
|
+
return {
|
|
250
|
+
next: () => {
|
|
251
|
+
const entry = this.queue.shift();
|
|
252
|
+
if (entry) {
|
|
253
|
+
this.queuedBytes -= entry.size;
|
|
254
|
+
return Promise.resolve({ value: entry.message, done: false });
|
|
255
|
+
}
|
|
256
|
+
if (this.failure) {
|
|
257
|
+
return Promise.reject(this.failure);
|
|
258
|
+
}
|
|
259
|
+
if (this.closed) {
|
|
260
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
261
|
+
}
|
|
262
|
+
return new Promise((resolve, reject) => this.waiters.push({ resolve, reject }));
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
sendJson(payload) {
|
|
267
|
+
this.socket.send(JSON.stringify(payload));
|
|
268
|
+
}
|
|
269
|
+
handleMessage(data) {
|
|
270
|
+
if (typeof data === "string") {
|
|
271
|
+
let parsed;
|
|
272
|
+
try {
|
|
273
|
+
parsed = JSON.parse(data);
|
|
274
|
+
}
|
|
275
|
+
catch (cause) {
|
|
276
|
+
this.fail(new BreezeBlueRealtimeError("Realtime TTS returned an invalid JSON event.", { cause }));
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
this.push(camelizeKeys(parsed), data.length);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (data instanceof ArrayBuffer) {
|
|
283
|
+
this.push({ type: "audio", audio: new Uint8Array(data) }, data.byteLength);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (ArrayBuffer.isView(data)) {
|
|
287
|
+
this.push({ type: "audio", audio: new Uint8Array(data.buffer, data.byteOffset, data.byteLength) }, data.byteLength);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
handleClose(event) {
|
|
291
|
+
if (!this.closed && event.code !== 1000) {
|
|
292
|
+
this.push({
|
|
293
|
+
type: "session.closed",
|
|
294
|
+
code: event.code,
|
|
295
|
+
reason: event.reason,
|
|
296
|
+
reconnect: event.code === 1012,
|
|
297
|
+
}, 0);
|
|
298
|
+
}
|
|
299
|
+
this.finish();
|
|
300
|
+
}
|
|
301
|
+
push(message, size) {
|
|
302
|
+
if (this.closed) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const waiter = this.waiters.shift();
|
|
306
|
+
if (waiter) {
|
|
307
|
+
waiter.resolve({ value: message, done: false });
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
this.queue.push({ message, size });
|
|
311
|
+
this.queuedBytes += size;
|
|
312
|
+
if (this.queuedBytes > REALTIME_MAX_BUFFERED_BYTES) {
|
|
313
|
+
this.fail(new BreezeBlueRealtimeError(`Realtime TTS connection buffered more than ${REALTIME_MAX_BUFFERED_BYTES} bytes without being ` +
|
|
314
|
+
"consumed, so the connection was closed. Iterate the connection (or its audio()/events() " +
|
|
315
|
+
"helpers) while the session is active."));
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
fail(error) {
|
|
319
|
+
if (this.closed) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
this.closed = true;
|
|
323
|
+
this.failure = error;
|
|
324
|
+
this.queue.length = 0;
|
|
325
|
+
this.queuedBytes = 0;
|
|
326
|
+
this.socket.close(1000);
|
|
327
|
+
for (const waiter of this.waiters.splice(0)) {
|
|
328
|
+
waiter.reject(error);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
finish() {
|
|
332
|
+
if (this.closed) {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
this.closed = true;
|
|
336
|
+
for (const waiter of this.waiters.splice(0)) {
|
|
337
|
+
waiter.resolve({ value: undefined, done: true });
|
|
338
|
+
}
|
|
101
339
|
}
|
|
102
340
|
}
|
|
103
341
|
class GenerationJobsResource {
|
|
@@ -175,6 +413,8 @@ class VoicesResource {
|
|
|
175
413
|
voiceName: request.voiceName,
|
|
176
414
|
voiceDescription: request.voiceDescription,
|
|
177
415
|
labels: request.labels,
|
|
416
|
+
primaryCategoryCode: request.primaryCategoryCode,
|
|
417
|
+
visibility: request.visibility,
|
|
178
418
|
playedNotSelectedVoiceIds: request.playedNotSelectedVoiceIds,
|
|
179
419
|
}, options);
|
|
180
420
|
}
|
|
@@ -218,6 +458,17 @@ class AccountResource {
|
|
|
218
458
|
return this.client.requestJson("GET", "/v1/usage", params, undefined, options);
|
|
219
459
|
}
|
|
220
460
|
}
|
|
461
|
+
function readGlobalWebSocket() {
|
|
462
|
+
return globalThis.WebSocket;
|
|
463
|
+
}
|
|
464
|
+
function requireWebSocket(WebSocketImpl) {
|
|
465
|
+
if (!WebSocketImpl) {
|
|
466
|
+
throw new BreezeBlueConfigurationError("Missing WebSocket implementation. Realtime text-to-speech needs a global WebSocket " +
|
|
467
|
+
"(available in browsers, edge runtimes, and Node 22+) or a webSocket constructor passed to " +
|
|
468
|
+
"BreezeBlueClient (for example from the 'ws' package on Node 20).");
|
|
469
|
+
}
|
|
470
|
+
return WebSocketImpl;
|
|
471
|
+
}
|
|
221
472
|
function normalizeBaseUrl(value) {
|
|
222
473
|
return value.replace(/\/+$/, "");
|
|
223
474
|
}
|
|
@@ -292,9 +543,11 @@ function clonePreviewForm(request) {
|
|
|
292
543
|
}
|
|
293
544
|
function voiceEditForm(request) {
|
|
294
545
|
const form = new FormData();
|
|
295
|
-
form
|
|
546
|
+
appendOptional(form, "name", request.name);
|
|
296
547
|
appendOptional(form, "description", request.description);
|
|
297
548
|
appendOptional(form, "labels", request.labels ? JSON.stringify(request.labels) : undefined);
|
|
549
|
+
appendOptional(form, "primary_category_code", request.primaryCategoryCode);
|
|
550
|
+
appendOptional(form, "visibility", request.visibility);
|
|
298
551
|
for (const file of normalizeFiles(request.file, request.files, { required: false })) {
|
|
299
552
|
appendFile(form, "files", file);
|
|
300
553
|
}
|
package/dist/errors.d.ts
CHANGED
|
@@ -10,6 +10,32 @@ export declare class BreezeBlueError extends Error {
|
|
|
10
10
|
}
|
|
11
11
|
export declare class BreezeBlueConfigurationError extends BreezeBlueError {
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* A realtime text-to-speech connection failure: the SDK failed the
|
|
15
|
+
* connection (invalid server frame, unconsumed-buffer overflow, ...) and the
|
|
16
|
+
* message iterator rejects with this error.
|
|
17
|
+
*
|
|
18
|
+
* `recoverable` mirrors the Python SDK's `RealtimeError.recoverable`: it is
|
|
19
|
+
* `true` only when the failure cancelled just the active turn and the
|
|
20
|
+
* connection stays usable. SDK-detected connection failures are never
|
|
21
|
+
* recoverable. `reconnect` tells you whether opening a new session is
|
|
22
|
+
* expected to succeed.
|
|
23
|
+
*/
|
|
24
|
+
export declare class BreezeBlueRealtimeError extends BreezeBlueError {
|
|
25
|
+
readonly code: string | undefined;
|
|
26
|
+
readonly meta: Record<string, unknown> | null;
|
|
27
|
+
readonly closeCode: number | undefined;
|
|
28
|
+
readonly reconnect: boolean;
|
|
29
|
+
readonly recoverable: boolean;
|
|
30
|
+
constructor(message: string, params?: {
|
|
31
|
+
code?: string;
|
|
32
|
+
meta?: Record<string, unknown> | null;
|
|
33
|
+
closeCode?: number;
|
|
34
|
+
reconnect?: boolean;
|
|
35
|
+
recoverable?: boolean;
|
|
36
|
+
cause?: unknown;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
13
39
|
export declare class BreezeBlueAPIError extends BreezeBlueError {
|
|
14
40
|
readonly status: number;
|
|
15
41
|
readonly code: string;
|
package/dist/errors.js
CHANGED
|
@@ -6,6 +6,32 @@ export class BreezeBlueError extends Error {
|
|
|
6
6
|
}
|
|
7
7
|
export class BreezeBlueConfigurationError extends BreezeBlueError {
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* A realtime text-to-speech connection failure: the SDK failed the
|
|
11
|
+
* connection (invalid server frame, unconsumed-buffer overflow, ...) and the
|
|
12
|
+
* message iterator rejects with this error.
|
|
13
|
+
*
|
|
14
|
+
* `recoverable` mirrors the Python SDK's `RealtimeError.recoverable`: it is
|
|
15
|
+
* `true` only when the failure cancelled just the active turn and the
|
|
16
|
+
* connection stays usable. SDK-detected connection failures are never
|
|
17
|
+
* recoverable. `reconnect` tells you whether opening a new session is
|
|
18
|
+
* expected to succeed.
|
|
19
|
+
*/
|
|
20
|
+
export class BreezeBlueRealtimeError extends BreezeBlueError {
|
|
21
|
+
code;
|
|
22
|
+
meta;
|
|
23
|
+
closeCode;
|
|
24
|
+
reconnect;
|
|
25
|
+
recoverable;
|
|
26
|
+
constructor(message, params = {}) {
|
|
27
|
+
super(message, params.cause === undefined ? undefined : { cause: params.cause });
|
|
28
|
+
this.code = params.code;
|
|
29
|
+
this.meta = params.meta ?? null;
|
|
30
|
+
this.closeCode = params.closeCode;
|
|
31
|
+
this.reconnect = params.reconnect ?? false;
|
|
32
|
+
this.recoverable = params.recoverable ?? false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
9
35
|
export class BreezeBlueAPIError extends BreezeBlueError {
|
|
10
36
|
status;
|
|
11
37
|
code;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { AudioResponse } from "./audio.js";
|
|
2
|
-
export { BreezeBlueAPIError, BreezeBlueAuthenticationError, BreezeBlueBadRequestError, BreezeBlueConfigurationError, BreezeBlueConflictError, BreezeBlueError, BreezeBlueForbiddenError, BreezeBlueGenerationNotReadyError, BreezeBlueInsufficientCreditsError, BreezeBlueNotFoundError, BreezeBlueRateLimitError, BreezeBlueServiceUnavailableError, BreezeBlueUpstreamError, BreezeBlueValidationError, } from "./errors.js";
|
|
3
|
-
export { BreezeBlueClient } from "./client.js";
|
|
2
|
+
export { BreezeBlueAPIError, BreezeBlueAuthenticationError, BreezeBlueBadRequestError, BreezeBlueConfigurationError, BreezeBlueConflictError, BreezeBlueError, BreezeBlueForbiddenError, BreezeBlueGenerationNotReadyError, BreezeBlueInsufficientCreditsError, BreezeBlueNotFoundError, BreezeBlueRateLimitError, BreezeBlueRealtimeError, BreezeBlueServiceUnavailableError, BreezeBlueUpstreamError, BreezeBlueValidationError, } from "./errors.js";
|
|
3
|
+
export { BreezeBlueClient, RealtimeTextToSpeechConnection } from "./client.js";
|
|
4
4
|
export { SDK_NAME, SDK_VERSION } from "./version.js";
|
|
5
5
|
export type * from "./types.js";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { AudioResponse } from "./audio.js";
|
|
2
|
-
export { BreezeBlueAPIError, BreezeBlueAuthenticationError, BreezeBlueBadRequestError, BreezeBlueConfigurationError, BreezeBlueConflictError, BreezeBlueError, BreezeBlueForbiddenError, BreezeBlueGenerationNotReadyError, BreezeBlueInsufficientCreditsError, BreezeBlueNotFoundError, BreezeBlueRateLimitError, BreezeBlueServiceUnavailableError, BreezeBlueUpstreamError, BreezeBlueValidationError, } from "./errors.js";
|
|
3
|
-
export { BreezeBlueClient } from "./client.js";
|
|
2
|
+
export { BreezeBlueAPIError, BreezeBlueAuthenticationError, BreezeBlueBadRequestError, BreezeBlueConfigurationError, BreezeBlueConflictError, BreezeBlueError, BreezeBlueForbiddenError, BreezeBlueGenerationNotReadyError, BreezeBlueInsufficientCreditsError, BreezeBlueNotFoundError, BreezeBlueRateLimitError, BreezeBlueRealtimeError, BreezeBlueServiceUnavailableError, BreezeBlueUpstreamError, BreezeBlueValidationError, } from "./errors.js";
|
|
3
|
+
export { BreezeBlueClient, RealtimeTextToSpeechConnection } from "./client.js";
|
|
4
4
|
export { SDK_NAME, SDK_VERSION } from "./version.js";
|
package/dist/node.js
CHANGED
|
@@ -29,7 +29,11 @@ export async function play(audio, options = {}) {
|
|
|
29
29
|
await spawnAndPipeBytes(command.command, command.args, await audio.bytes());
|
|
30
30
|
}
|
|
31
31
|
export async function stream(audio, options = {}) {
|
|
32
|
-
const selected = selectStreamPlaybackCommand(
|
|
32
|
+
const selected = selectStreamPlaybackCommand({
|
|
33
|
+
contentType: audio.contentType,
|
|
34
|
+
sampleRate: numberHeader(audio, "x-breeze-sample-rate"),
|
|
35
|
+
channels: numberHeader(audio, "x-breeze-channels"),
|
|
36
|
+
});
|
|
33
37
|
const command = options.command ?? selected.command;
|
|
34
38
|
const args = options.args ?? selected.args;
|
|
35
39
|
await spawnAndPipeReadable(command, args, audio.response.body);
|
|
@@ -57,6 +61,10 @@ async function spawnAndPipeReadable(command, args, body) {
|
|
|
57
61
|
Readable.fromWeb(body).pipe(child.stdin);
|
|
58
62
|
await waitForChild(child);
|
|
59
63
|
}
|
|
64
|
+
function numberHeader(audio, header) {
|
|
65
|
+
const value = Number.parseInt(audio.headers.get(header) ?? "", 10);
|
|
66
|
+
return Number.isFinite(value) && value > 0 ? value : null;
|
|
67
|
+
}
|
|
60
68
|
async function spawnAndWait(command, args) {
|
|
61
69
|
await waitForChild(spawn(command, args, { stdio: "inherit" }));
|
|
62
70
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
export type JsonObject = Record<string, unknown>;
|
|
2
2
|
export type BreezeBlueFetch = typeof fetch;
|
|
3
3
|
export interface BreezeBlueClientOptions {
|
|
4
|
+
/**
|
|
5
|
+
* API key for REST calls. Optional: browser and edge callers that only use
|
|
6
|
+
* `textToSpeech.realtime.connect` with a `clientSecret` do not need one.
|
|
7
|
+
*/
|
|
4
8
|
apiKey?: string;
|
|
5
9
|
baseUrl?: string;
|
|
6
10
|
fetch?: BreezeBlueFetch;
|
|
7
11
|
headers?: HeadersInit;
|
|
8
12
|
timeout?: number;
|
|
13
|
+
webSocket?: BreezeBlueWebSocketConstructor;
|
|
9
14
|
}
|
|
10
15
|
export interface RequestOptions {
|
|
11
16
|
headers?: HeadersInit;
|
|
@@ -19,6 +24,129 @@ export interface StreamTextToSpeechOptions extends AudioRequestOptions {
|
|
|
19
24
|
optimizeStreamingLatency?: number;
|
|
20
25
|
enableLogging?: boolean;
|
|
21
26
|
}
|
|
27
|
+
export type BreezeBlueWebSocketConstructor = new (url: string, protocols?: string | string[]) => BreezeBlueWebSocket;
|
|
28
|
+
export interface BreezeBlueWebSocket {
|
|
29
|
+
binaryType: string;
|
|
30
|
+
readyState: number;
|
|
31
|
+
onopen: ((event: Event) => void) | null;
|
|
32
|
+
onmessage: ((event: MessageEvent) => void) | null;
|
|
33
|
+
onerror: ((event: Event) => void) | null;
|
|
34
|
+
onclose: ((event: CloseEvent) => void) | null;
|
|
35
|
+
send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void;
|
|
36
|
+
close(code?: number, reason?: string): void;
|
|
37
|
+
}
|
|
38
|
+
export interface RealtimeTextToSpeechSessionRequest {
|
|
39
|
+
modelId?: string;
|
|
40
|
+
languageCode?: string;
|
|
41
|
+
instructions?: string;
|
|
42
|
+
voiceSettings?: {
|
|
43
|
+
guidanceScale?: number | null;
|
|
44
|
+
};
|
|
45
|
+
inactivityTimeoutSeconds?: number;
|
|
46
|
+
enableLogging?: boolean;
|
|
47
|
+
}
|
|
48
|
+
export interface RealtimeTextToSpeechAudioFormat {
|
|
49
|
+
codec: "pcm_s16le" | string;
|
|
50
|
+
sampleRate: number;
|
|
51
|
+
channels: number;
|
|
52
|
+
sampleWidthBits: number;
|
|
53
|
+
}
|
|
54
|
+
export interface RealtimeTextToSpeechSession {
|
|
55
|
+
clientSecret: string;
|
|
56
|
+
websocketUrl: string;
|
|
57
|
+
expiresAt: string;
|
|
58
|
+
audioFormat: RealtimeTextToSpeechAudioFormat;
|
|
59
|
+
}
|
|
60
|
+
export interface RealtimeTextToSpeechConnectOptions extends RealtimeTextToSpeechSessionRequest {
|
|
61
|
+
clientSecret?: string;
|
|
62
|
+
websocketUrl?: string;
|
|
63
|
+
webSocket?: BreezeBlueWebSocketConstructor;
|
|
64
|
+
/** Handshake timeout in milliseconds. Defaults to the client `timeout`. */
|
|
65
|
+
timeout?: number;
|
|
66
|
+
}
|
|
67
|
+
export interface RealtimeTextToSpeechSessionReadyEvent {
|
|
68
|
+
type: "session.ready";
|
|
69
|
+
sessionId: string;
|
|
70
|
+
audioFormat: RealtimeTextToSpeechAudioFormat;
|
|
71
|
+
maxAppendBytes: number;
|
|
72
|
+
maxTurnCharacters: number;
|
|
73
|
+
inactivityTimeoutSeconds: number;
|
|
74
|
+
maxSessionSeconds: number;
|
|
75
|
+
}
|
|
76
|
+
export interface RealtimeTextToSpeechPongEvent {
|
|
77
|
+
type: "pong";
|
|
78
|
+
}
|
|
79
|
+
export interface RealtimeTextToSpeechTurnStartedEvent {
|
|
80
|
+
type: "turn.started";
|
|
81
|
+
turnId: string;
|
|
82
|
+
historyItemId: string;
|
|
83
|
+
}
|
|
84
|
+
export interface RealtimeTextToSpeechAudioStartedEvent {
|
|
85
|
+
type: "audio.started";
|
|
86
|
+
turnId: string;
|
|
87
|
+
ttfaMs: number;
|
|
88
|
+
audioFormat: RealtimeTextToSpeechAudioFormat;
|
|
89
|
+
}
|
|
90
|
+
export interface RealtimeTextToSpeechTurnSummary {
|
|
91
|
+
turnId: string;
|
|
92
|
+
historyItemId: string;
|
|
93
|
+
status: string;
|
|
94
|
+
textCharacters: number;
|
|
95
|
+
audioBytes: number;
|
|
96
|
+
durationMs: number | null;
|
|
97
|
+
ttfaMs: number | null;
|
|
98
|
+
}
|
|
99
|
+
export interface RealtimeTextToSpeechTurnDoneEvent extends RealtimeTextToSpeechTurnSummary {
|
|
100
|
+
type: "turn.done";
|
|
101
|
+
}
|
|
102
|
+
export interface RealtimeTextToSpeechTurnCancelledEvent extends RealtimeTextToSpeechTurnSummary {
|
|
103
|
+
type: "turn.cancelled";
|
|
104
|
+
}
|
|
105
|
+
export interface RealtimeTextToSpeechUsageCommittedEvent {
|
|
106
|
+
type: "usage.committed";
|
|
107
|
+
turnId: string;
|
|
108
|
+
historyItemId: string;
|
|
109
|
+
textCharacters: number;
|
|
110
|
+
outputFormat: string;
|
|
111
|
+
ttfaMs: number | null;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Sent by the server after a clean `session.close` (with `sessionId`), or
|
|
115
|
+
* synthesized by the SDK when the WebSocket closes abnormally (with `code`,
|
|
116
|
+
* `reason`, and `reconnect`).
|
|
117
|
+
*/
|
|
118
|
+
export interface RealtimeTextToSpeechSessionClosedEvent {
|
|
119
|
+
type: "session.closed";
|
|
120
|
+
sessionId?: string;
|
|
121
|
+
code?: number;
|
|
122
|
+
reason?: string;
|
|
123
|
+
reconnect?: boolean;
|
|
124
|
+
}
|
|
125
|
+
export interface RealtimeTextToSpeechErrorEvent {
|
|
126
|
+
type: "error";
|
|
127
|
+
code: string;
|
|
128
|
+
message: string;
|
|
129
|
+
meta?: {
|
|
130
|
+
reconnect?: boolean;
|
|
131
|
+
reason?: string;
|
|
132
|
+
turnId?: string;
|
|
133
|
+
[key: string]: unknown;
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Server events currently sent on a realtime text-to-speech connection.
|
|
138
|
+
*
|
|
139
|
+
* Treat this union as non-exhaustive: the server may add new event types,
|
|
140
|
+
* and the SDK passes unknown JSON events through to the iterator unchanged
|
|
141
|
+
* (with camelCase keys). Do not rely on an exhaustive `switch` over `type`;
|
|
142
|
+
* ignore event types you do not recognize.
|
|
143
|
+
*/
|
|
144
|
+
export type RealtimeTextToSpeechEvent = RealtimeTextToSpeechSessionReadyEvent | RealtimeTextToSpeechPongEvent | RealtimeTextToSpeechTurnStartedEvent | RealtimeTextToSpeechAudioStartedEvent | RealtimeTextToSpeechTurnDoneEvent | RealtimeTextToSpeechTurnCancelledEvent | RealtimeTextToSpeechUsageCommittedEvent | RealtimeTextToSpeechSessionClosedEvent | RealtimeTextToSpeechErrorEvent;
|
|
145
|
+
export interface RealtimeTextToSpeechAudioMessage {
|
|
146
|
+
type: "audio";
|
|
147
|
+
audio: Uint8Array;
|
|
148
|
+
}
|
|
149
|
+
export type RealtimeTextToSpeechMessage = RealtimeTextToSpeechEvent | RealtimeTextToSpeechAudioMessage;
|
|
22
150
|
export interface VoiceSettings {
|
|
23
151
|
stability?: number;
|
|
24
152
|
similarityBoost?: number;
|
|
@@ -46,11 +174,11 @@ export interface TextToSpeechRequest {
|
|
|
46
174
|
applyTextNormalization?: string;
|
|
47
175
|
applyLanguageTextNormalization?: boolean;
|
|
48
176
|
}
|
|
49
|
-
export interface
|
|
177
|
+
export interface TtsEnhanceRequest {
|
|
50
178
|
instruction: string;
|
|
51
179
|
languageCode?: string | null;
|
|
52
180
|
}
|
|
53
|
-
export interface
|
|
181
|
+
export interface TtsEnhanceResponse {
|
|
54
182
|
instruction: string;
|
|
55
183
|
languageCode: string;
|
|
56
184
|
}
|
|
@@ -71,7 +199,7 @@ export interface GenerationJob {
|
|
|
71
199
|
source: string;
|
|
72
200
|
voiceId?: string | null;
|
|
73
201
|
voiceName?: string | null;
|
|
74
|
-
|
|
202
|
+
origin?: string | null;
|
|
75
203
|
modelId?: string | null;
|
|
76
204
|
text?: string | null;
|
|
77
205
|
contentType?: string | null;
|
|
@@ -100,15 +228,27 @@ export interface Model {
|
|
|
100
228
|
export interface Voice {
|
|
101
229
|
voiceId: string;
|
|
102
230
|
name: string;
|
|
103
|
-
|
|
231
|
+
origin: string;
|
|
104
232
|
voiceType: string;
|
|
105
233
|
labels: Record<string, string>;
|
|
106
234
|
previewUrl?: string | null;
|
|
107
235
|
settings: Required<VoiceSettings>;
|
|
108
236
|
description?: string | null;
|
|
109
237
|
createdAtUnix?: number | null;
|
|
238
|
+
primaryCategoryCode?: string | null;
|
|
239
|
+
visibility?: string | null;
|
|
110
240
|
[key: string]: unknown;
|
|
111
241
|
}
|
|
242
|
+
export interface SavedVoice {
|
|
243
|
+
voiceId: string;
|
|
244
|
+
name: string;
|
|
245
|
+
origin: string;
|
|
246
|
+
previewUrl?: string | null;
|
|
247
|
+
description?: string | null;
|
|
248
|
+
createdAtUnix?: number | null;
|
|
249
|
+
primaryCategoryCode?: string | null;
|
|
250
|
+
visibility?: string | null;
|
|
251
|
+
}
|
|
112
252
|
export interface VoiceList {
|
|
113
253
|
voices: Voice[];
|
|
114
254
|
hasMore: boolean;
|
|
@@ -119,7 +259,7 @@ export interface VoiceList {
|
|
|
119
259
|
}
|
|
120
260
|
export interface VoiceSearchParams {
|
|
121
261
|
search?: string;
|
|
122
|
-
|
|
262
|
+
origin?: "designed" | "cloned" | string;
|
|
123
263
|
voiceType?: "all" | "default" | "personal" | string;
|
|
124
264
|
sort?: "created_at_unix" | "name" | string;
|
|
125
265
|
sortDirection?: "asc" | "desc";
|
|
@@ -151,9 +291,11 @@ export interface VoiceClonePreview {
|
|
|
151
291
|
requiresVerification: boolean;
|
|
152
292
|
}
|
|
153
293
|
export interface VoiceEditRequest {
|
|
154
|
-
name
|
|
294
|
+
name?: string;
|
|
155
295
|
description?: string;
|
|
156
296
|
labels?: Record<string, string>;
|
|
297
|
+
primaryCategoryCode?: string;
|
|
298
|
+
visibility?: "private" | "public" | string;
|
|
157
299
|
file?: UploadData | UploadFile;
|
|
158
300
|
files?: Array<UploadData | UploadFile>;
|
|
159
301
|
}
|
|
@@ -184,13 +326,15 @@ export interface SaveVoiceRequest {
|
|
|
184
326
|
voiceName: string;
|
|
185
327
|
voiceDescription?: string | null;
|
|
186
328
|
labels?: Record<string, string>;
|
|
329
|
+
primaryCategoryCode?: string;
|
|
330
|
+
visibility?: "private" | "public" | string;
|
|
187
331
|
playedNotSelectedVoiceIds?: string[];
|
|
188
332
|
}
|
|
189
333
|
export interface HistoryItem {
|
|
190
334
|
historyItemId: string;
|
|
191
335
|
voiceId?: string | null;
|
|
192
336
|
voiceName?: string | null;
|
|
193
|
-
|
|
337
|
+
origin?: string | null;
|
|
194
338
|
modelId?: string | null;
|
|
195
339
|
text?: string | null;
|
|
196
340
|
source: string;
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const SDK_VERSION = "0.
|
|
1
|
+
export declare const SDK_VERSION = "0.5.0";
|
|
2
2
|
export declare const SDK_NAME = "@breeze.blue/sdk";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const SDK_VERSION = "0.
|
|
1
|
+
export const SDK_VERSION = "0.5.0";
|
|
2
2
|
export const SDK_NAME = "@breeze.blue/sdk";
|