@breeze.blue/sdk 0.6.3 → 0.8.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 +17 -0
- package/README.md +53 -6
- package/dist/client.d.ts +24 -1
- package/dist/client.js +230 -29
- package/dist/errors.d.ts +5 -4
- package/dist/errors.js +5 -4
- package/dist/types.d.ts +10 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.8.0
|
|
6
|
+
|
|
7
|
+
- Realtime TTS sessions prefer the optional query-free direct WebSocket URL
|
|
8
|
+
and authenticate with a non-echoed token-bearing subprotocol. Existing
|
|
9
|
+
public session URLs remain the fallback for older deployments and clients.
|
|
10
|
+
|
|
11
|
+
## 0.7.0
|
|
12
|
+
|
|
13
|
+
- Realtime TTS connections can update synthesis instructions between turns
|
|
14
|
+
with `updateInstructions(...)`. The matching `session.updated` event remains
|
|
15
|
+
on the existing ordered stream, and managed connections carry confirmed
|
|
16
|
+
instructions across physical-session rotation and idle reconnects.
|
|
17
|
+
`BreezeBlueRealtimeError.recoverable` means the same connection remains
|
|
18
|
+
usable: most recoverable turn-command errors cancel the active turn, while
|
|
19
|
+
an active-turn `session.update` rejection leaves that turn running and
|
|
20
|
+
callers continue consuming it on the existing connection.
|
|
21
|
+
|
|
5
22
|
## 0.6.3
|
|
6
23
|
|
|
7
24
|
- Simplified model listings to Breeze-native fields: model ID, name, supported
|
package/README.md
CHANGED
|
@@ -152,7 +152,14 @@ a `BreezeBlueRealtimeError`. The `audio()` helper also throws
|
|
|
152
152
|
`BreezeBlueRealtimeError` for server `error` events and abnormal WebSocket
|
|
153
153
|
closes instead of silently ending.
|
|
154
154
|
|
|
155
|
-
|
|
155
|
+
When `BreezeBlueRealtimeError.recoverable` is `true`, the same connection
|
|
156
|
+
remains usable. Most recoverable turn-command errors cancel the active turn.
|
|
157
|
+
An active-turn `session.update` rejection is the exception: the turn keeps
|
|
158
|
+
running. If `connection.audio()` throws that error, start a fresh `audio()`
|
|
159
|
+
iterator immediately on the same connection and continue collecting the
|
|
160
|
+
current turn; do not start a replacement turn.
|
|
161
|
+
|
|
162
|
+
Events are a typed discriminated union (`session.ready`, `session.updated`, `turn.started`,
|
|
156
163
|
`audio.started`, `turn.done`, `turn.cancelled`, `usage.committed`,
|
|
157
164
|
`session.expiring`, `session.closed`, `error`, `pong`), with camelCase fields
|
|
158
165
|
such as `turnId`, `historyItemId`, `expiresAt`, and `ttfaMs`. Treat the union as
|
|
@@ -160,6 +167,32 @@ non-exhaustive: the server may add event types, and the SDK delivers unknown
|
|
|
160
167
|
JSON events unchanged — ignore event types you do not recognize instead of
|
|
161
168
|
switching exhaustively.
|
|
162
169
|
|
|
170
|
+
Between turns, update the synthesis instructions without replacing the
|
|
171
|
+
WebSocket. `updateInstructions(...)` only sends the request; the matching
|
|
172
|
+
`session.updated` acknowledgement remains on the same ordered iterator. Wait
|
|
173
|
+
for it before starting the next turn:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
connection.updateInstructions("Speak faster and with more energy.");
|
|
177
|
+
for await (const message of connection) {
|
|
178
|
+
if (message.type === "session.updated") {
|
|
179
|
+
if (message.instructions !== "Speak faster and with more energy.") {
|
|
180
|
+
throw new Error("Unexpected instructions acknowledgement");
|
|
181
|
+
}
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
connection.startTurn("turn_2");
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Instructions must be a non-empty string of at most 1,000 characters. Only one
|
|
190
|
+
update may await acknowledgement, and updates are accepted only while no turn
|
|
191
|
+
is active. An update attempted during a turn is rejected without interrupting
|
|
192
|
+
that turn. Keep one stream consumer: if a long-lived consumer owns the
|
|
193
|
+
iterator, have it signal your turn producer when `session.updated` arrives
|
|
194
|
+
instead of starting a second iterator.
|
|
195
|
+
|
|
163
196
|
Send a keepalive ping inside the session's `inactivityTimeoutSeconds` window;
|
|
164
197
|
the server answers with a `pong` event. Keep it running during active turns too:
|
|
165
198
|
a long TTFA or upstream stall with no audio does not pause the idle deadline.
|
|
@@ -245,9 +278,15 @@ const session = await client.textToSpeech.realtime.createSession("voc_...", {
|
|
|
245
278
|
// Later, when the first text is ready:
|
|
246
279
|
const connection = await client.textToSpeech.realtime.connect("voc_...", {
|
|
247
280
|
clientSecret: session.clientSecret,
|
|
281
|
+
websocketUrl: session.websocketUrl,
|
|
282
|
+
directWebsocketUrl: session.directWebsocketUrl,
|
|
248
283
|
});
|
|
249
284
|
```
|
|
250
285
|
|
|
286
|
+
When the session response includes `directWebsocketUrl`, the SDK prefers its
|
|
287
|
+
query-free origin and authenticates with the WebSocket subprotocol. It falls
|
|
288
|
+
back to `websocketUrl` for services that have not enabled the direct transport.
|
|
289
|
+
|
|
251
290
|
The same `clientSecret` handoff lets a browser connect without ever seeing your
|
|
252
291
|
API key: create the session on your server, hand `session.clientSecret` to the
|
|
253
292
|
page, and build a key-less client there:
|
|
@@ -282,7 +321,7 @@ const connection = await browserClient.textToSpeech.realtime.connectManaged("voc
|
|
|
282
321
|
reconnect,
|
|
283
322
|
});
|
|
284
323
|
}
|
|
285
|
-
return response.json(); // { clientSecret, websocketUrl? }
|
|
324
|
+
return response.json(); // { clientSecret, websocketUrl?, directWebsocketUrl? }
|
|
286
325
|
},
|
|
287
326
|
});
|
|
288
327
|
```
|
|
@@ -291,10 +330,18 @@ Honor the factory `signal`: the SDK aborts it when the logical connection closes
|
|
|
291
330
|
or the physical-epoch timeout expires. A custom callback that ignores the
|
|
292
331
|
signal must still enforce its own bounded request timeout.
|
|
293
332
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
333
|
+
After a dynamic instruction update receives `session.updated`, the managed SDK
|
|
334
|
+
carries the confirmed value to later physical epochs. SDK-created replacements
|
|
335
|
+
include it in their session request; factory-created replacements receive an
|
|
336
|
+
automatic `session.update` before their next `session.ready` is exposed to the
|
|
337
|
+
logical iterator.
|
|
338
|
+
|
|
339
|
+
`modelId`, `languageCode`, `voiceSettings`, `inactivityTimeoutSeconds`, and
|
|
340
|
+
`enableLogging` are fixed when the session is created. The initial
|
|
341
|
+
`instructions` value is also supplied at creation, but it can later be
|
|
342
|
+
replaced between turns with `updateInstructions(...)`. `connect` ignores
|
|
343
|
+
initial session options when `clientSecret` or `websocketUrl` is provided and
|
|
344
|
+
logs a warning.
|
|
298
345
|
|
|
299
346
|
If a realtime WebSocket is interrupted by a network change, service deployment,
|
|
300
347
|
or upstream realtime worker restart, `connectManaged(...)` handles bounded
|
package/dist/client.d.ts
CHANGED
|
@@ -48,17 +48,27 @@ export declare class RealtimeTextToSpeechConnection implements AsyncIterable<Rea
|
|
|
48
48
|
private closed;
|
|
49
49
|
private queuedBytes;
|
|
50
50
|
private failure;
|
|
51
|
+
private pendingInstructions;
|
|
51
52
|
private closeEventGraceTimer;
|
|
52
53
|
private constructor();
|
|
53
54
|
static open(socket: BreezeBlueWebSocket, options?: {
|
|
54
55
|
timeout?: number;
|
|
55
56
|
signal?: AbortSignal;
|
|
57
|
+
requiredSubprotocol?: string;
|
|
56
58
|
}): Promise<RealtimeTextToSpeechConnection>;
|
|
57
59
|
startTurn(turnId?: string): void;
|
|
58
60
|
appendText(text: string): void;
|
|
59
61
|
flush(): void;
|
|
60
62
|
endTurn(): void;
|
|
61
63
|
cancelTurn(): void;
|
|
64
|
+
/**
|
|
65
|
+
* Request new synthesis instructions for subsequent turns.
|
|
66
|
+
*
|
|
67
|
+
* The matching `session.updated` acknowledgement stays on this
|
|
68
|
+
* connection's existing message stream. Keep the single consumer running
|
|
69
|
+
* and wait for that event before starting the next turn.
|
|
70
|
+
*/
|
|
71
|
+
updateInstructions(instructions: string): void;
|
|
62
72
|
/** Send a keepalive ping. The server answers with a `pong` event. */
|
|
63
73
|
ping(): void;
|
|
64
74
|
close(): void;
|
|
@@ -73,6 +83,7 @@ export declare class RealtimeTextToSpeechConnection implements AsyncIterable<Rea
|
|
|
73
83
|
private fail;
|
|
74
84
|
private finish;
|
|
75
85
|
private clearCloseEventGraceTimer;
|
|
86
|
+
private handleInstructionsEvent;
|
|
76
87
|
}
|
|
77
88
|
type RealtimeConnectionFactory = (signal: AbortSignal) => Promise<RealtimeTextToSpeechConnection>;
|
|
78
89
|
/**
|
|
@@ -88,6 +99,7 @@ type RealtimeConnectionFactory = (signal: AbortSignal) => Promise<RealtimeTextTo
|
|
|
88
99
|
*/
|
|
89
100
|
export declare class ManagedRealtimeTextToSpeechConnection implements AsyncIterable<RealtimeTextToSpeechMessage> {
|
|
90
101
|
private readonly createConnection;
|
|
102
|
+
private readonly onInstructionsConfirmed;
|
|
91
103
|
private readonly queue;
|
|
92
104
|
private readonly drainingEpochs;
|
|
93
105
|
private readonly openAttempts;
|
|
@@ -99,6 +111,7 @@ export declare class ManagedRealtimeTextToSpeechConnection implements AsyncItera
|
|
|
99
111
|
private readonly maxPhysicalSessionMs;
|
|
100
112
|
private readonly maxReconnectAttempts;
|
|
101
113
|
private readonly reconnectBaseDelayMs;
|
|
114
|
+
private readonly reapplyInstructionsOnOpen;
|
|
102
115
|
private current;
|
|
103
116
|
private heartbeatTimer;
|
|
104
117
|
private heartbeatAckTimer;
|
|
@@ -109,11 +122,13 @@ export declare class ManagedRealtimeTextToSpeechConnection implements AsyncItera
|
|
|
109
122
|
private epochSequence;
|
|
110
123
|
private queuedBytes;
|
|
111
124
|
private activeTurn;
|
|
125
|
+
private instructions;
|
|
126
|
+
private pendingInstructions;
|
|
112
127
|
private rotationPending;
|
|
113
128
|
private closed;
|
|
114
129
|
private failure;
|
|
115
130
|
private constructor();
|
|
116
|
-
static open(createConnection: RealtimeConnectionFactory, options?: RealtimeTextToSpeechManagedConnectOptions): Promise<ManagedRealtimeTextToSpeechConnection>;
|
|
131
|
+
static open(createConnection: RealtimeConnectionFactory, options?: RealtimeTextToSpeechManagedConnectOptions, onInstructionsConfirmed?: (instructions: string) => void): Promise<ManagedRealtimeTextToSpeechConnection>;
|
|
117
132
|
startTurn(turnId?: string): void;
|
|
118
133
|
/**
|
|
119
134
|
* Start a turn on the current physical epoch, waiting only when an idle
|
|
@@ -129,6 +144,14 @@ export declare class ManagedRealtimeTextToSpeechConnection implements AsyncItera
|
|
|
129
144
|
flush(): void;
|
|
130
145
|
endTurn(): void;
|
|
131
146
|
cancelTurn(): void;
|
|
147
|
+
/**
|
|
148
|
+
* Update the synthesis instructions used by subsequent turns.
|
|
149
|
+
*
|
|
150
|
+
* The matching `session.updated` acknowledgement stays on this logical
|
|
151
|
+
* connection's ordered event stream. Confirmed instructions are carried to
|
|
152
|
+
* later physical WebSocket epochs.
|
|
153
|
+
*/
|
|
154
|
+
updateInstructions(instructions: string): void;
|
|
132
155
|
/** Send an immediate ping in addition to the managed keepalive schedule. */
|
|
133
156
|
ping(): void;
|
|
134
157
|
close(): void;
|
package/dist/client.js
CHANGED
|
@@ -14,6 +14,10 @@ const DEFAULT_REALTIME_RECONNECT_BASE_DELAY_MS = 250;
|
|
|
14
14
|
const MAX_REALTIME_RECONNECT_DELAY_MS = 4_000;
|
|
15
15
|
const REALTIME_SETTLEMENT_DRAIN_TIMEOUT_MS = 5_000;
|
|
16
16
|
const REALTIME_CLOSE_EVENT_GRACE_MS = 50;
|
|
17
|
+
const REALTIME_MAX_INSTRUCTIONS_CHARACTERS = 1_000;
|
|
18
|
+
const REALTIME_SAFE_SUBPROTOCOL = "breeze-realtime-v1";
|
|
19
|
+
const REALTIME_TOKEN_SUBPROTOCOL_PREFIX = "breeze-realtime-token.";
|
|
20
|
+
const REALTIME_MAX_TOKEN_SUBPROTOCOL_LENGTH = 7 * 1024;
|
|
17
21
|
const WEBSOCKET_OPEN = 1;
|
|
18
22
|
export class BreezeBlueClient {
|
|
19
23
|
apiKey;
|
|
@@ -128,30 +132,55 @@ class RealtimeTextToSpeechResource {
|
|
|
128
132
|
return this.client.requestJson("POST", `/v1/text-to-speech/${encodeURIComponent(voiceId)}/realtime-sessions`, undefined, request, options);
|
|
129
133
|
}
|
|
130
134
|
async connect(voiceId, options = {}) {
|
|
131
|
-
if ((options.clientSecret !== undefined ||
|
|
135
|
+
if ((options.clientSecret !== undefined ||
|
|
136
|
+
options.websocketUrl !== undefined ||
|
|
137
|
+
options.directWebsocketUrl !== undefined) && hasRealtimeSessionParams(options)) {
|
|
132
138
|
console.warn("@breeze.blue/sdk: realtime.connect ignores modelId, languageCode, instructions, voiceSettings, " +
|
|
133
|
-
"inactivityTimeoutSeconds, and enableLogging when
|
|
134
|
-
"Configure these when creating the realtime session instead."
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
139
|
+
"inactivityTimeoutSeconds, and enableLogging when session credentials are provided. " +
|
|
140
|
+
"Configure these initial values when creating the realtime session instead. " +
|
|
141
|
+
"To change instructions between turns, call connection.updateInstructions() and wait for session.updated.");
|
|
142
|
+
}
|
|
143
|
+
let clientSecret = options.clientSecret;
|
|
144
|
+
let websocketUrl = options.websocketUrl;
|
|
145
|
+
let directWebsocketUrl = options.directWebsocketUrl;
|
|
146
|
+
if (!clientSecret && !websocketUrl && !directWebsocketUrl) {
|
|
147
|
+
const session = await this.createSession(voiceId, {
|
|
148
|
+
modelId: options.modelId,
|
|
149
|
+
languageCode: options.languageCode,
|
|
150
|
+
instructions: options.instructions,
|
|
151
|
+
voiceSettings: options.voiceSettings,
|
|
152
|
+
inactivityTimeoutSeconds: options.inactivityTimeoutSeconds,
|
|
153
|
+
enableLogging: options.enableLogging,
|
|
154
|
+
}, {
|
|
155
|
+
signal: options.signal,
|
|
156
|
+
timeout: options.timeout,
|
|
157
|
+
});
|
|
158
|
+
clientSecret = session.clientSecret;
|
|
159
|
+
websocketUrl = session.websocketUrl;
|
|
160
|
+
directWebsocketUrl = session.directWebsocketUrl;
|
|
161
|
+
}
|
|
162
|
+
if (!websocketUrl && clientSecret) {
|
|
163
|
+
websocketUrl = buildUrl(this.client.baseUrl.replace(/^http/i, "ws"), `/v1/text-to-speech/${encodeURIComponent(voiceId)}/stream-input`, { client_secret: clientSecret });
|
|
164
|
+
}
|
|
165
|
+
let protocols;
|
|
166
|
+
let requiredSubprotocol;
|
|
167
|
+
if (directWebsocketUrl) {
|
|
168
|
+
if (!clientSecret) {
|
|
169
|
+
throw new BreezeBlueConfigurationError("realtime directWebsocketUrl requires a clientSecret.");
|
|
170
|
+
}
|
|
171
|
+
protocols = realtimeClientSecretSubprotocols(clientSecret);
|
|
172
|
+
requiredSubprotocol = REALTIME_SAFE_SUBPROTOCOL;
|
|
173
|
+
websocketUrl = directWebsocketUrl;
|
|
174
|
+
}
|
|
175
|
+
if (!websocketUrl) {
|
|
176
|
+
throw new BreezeBlueConfigurationError("realtime.connect requires session credentials or an API-created session.");
|
|
177
|
+
}
|
|
150
178
|
const WebSocketImpl = requireWebSocket(options.webSocket ?? this.client.webSocket);
|
|
151
|
-
const socket = new WebSocketImpl(websocketUrl);
|
|
179
|
+
const socket = new WebSocketImpl(websocketUrl, protocols);
|
|
152
180
|
return RealtimeTextToSpeechConnection.open(socket, {
|
|
153
181
|
timeout: options.timeout ?? this.client.timeout ?? DEFAULT_REALTIME_HANDSHAKE_TIMEOUT_MS,
|
|
154
182
|
signal: options.signal,
|
|
183
|
+
requiredSubprotocol,
|
|
155
184
|
});
|
|
156
185
|
}
|
|
157
186
|
/**
|
|
@@ -164,7 +193,8 @@ class RealtimeTextToSpeechResource {
|
|
|
164
193
|
if (options.sessionFactory !== undefined && hasRealtimeSessionParams(options)) {
|
|
165
194
|
console.warn("@breeze.blue/sdk: realtime.connectManaged cannot apply modelId, languageCode, instructions, " +
|
|
166
195
|
"voiceSettings, inactivityTimeoutSeconds, or enableLogging when sessionFactory is provided. " +
|
|
167
|
-
"Configure these when the factory creates each realtime session."
|
|
196
|
+
"Configure these when the factory creates each realtime session. After connecting, confirmed " +
|
|
197
|
+
"updateInstructions() values are carried across managed physical sessions.");
|
|
168
198
|
}
|
|
169
199
|
const sessionRequest = {
|
|
170
200
|
modelId: options.modelId,
|
|
@@ -188,6 +218,7 @@ class RealtimeTextToSpeechResource {
|
|
|
188
218
|
return this.connect(voiceId, {
|
|
189
219
|
clientSecret: credentials.clientSecret,
|
|
190
220
|
websocketUrl: credentials.websocketUrl,
|
|
221
|
+
directWebsocketUrl: credentials.directWebsocketUrl,
|
|
191
222
|
webSocket: options.webSocket,
|
|
192
223
|
signal,
|
|
193
224
|
timeout: connectionTimeout,
|
|
@@ -196,6 +227,8 @@ class RealtimeTextToSpeechResource {
|
|
|
196
227
|
return ManagedRealtimeTextToSpeechConnection.open(createConnection, {
|
|
197
228
|
...options,
|
|
198
229
|
timeout: connectionTimeout,
|
|
230
|
+
}, (instructions) => {
|
|
231
|
+
sessionRequest.instructions = instructions;
|
|
199
232
|
});
|
|
200
233
|
}
|
|
201
234
|
}
|
|
@@ -207,6 +240,16 @@ function hasRealtimeSessionParams(options) {
|
|
|
207
240
|
options.inactivityTimeoutSeconds !== undefined ||
|
|
208
241
|
options.enableLogging !== undefined);
|
|
209
242
|
}
|
|
243
|
+
function realtimeClientSecretSubprotocols(clientSecret) {
|
|
244
|
+
if (!/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]{43}$/.test(clientSecret)) {
|
|
245
|
+
throw new BreezeBlueConfigurationError("Realtime clientSecret cannot be encoded as a WebSocket subprotocol token.");
|
|
246
|
+
}
|
|
247
|
+
const tokenProtocol = `${REALTIME_TOKEN_SUBPROTOCOL_PREFIX}${clientSecret}`;
|
|
248
|
+
if (tokenProtocol.length > REALTIME_MAX_TOKEN_SUBPROTOCOL_LENGTH) {
|
|
249
|
+
throw new BreezeBlueConfigurationError("Realtime clientSecret WebSocket subprotocol is too long.");
|
|
250
|
+
}
|
|
251
|
+
return [REALTIME_SAFE_SUBPROTOCOL, tokenProtocol];
|
|
252
|
+
}
|
|
210
253
|
export class RealtimeTextToSpeechConnection {
|
|
211
254
|
socket;
|
|
212
255
|
queue = [];
|
|
@@ -214,6 +257,7 @@ export class RealtimeTextToSpeechConnection {
|
|
|
214
257
|
closed = false;
|
|
215
258
|
queuedBytes = 0;
|
|
216
259
|
failure;
|
|
260
|
+
pendingInstructions;
|
|
217
261
|
closeEventGraceTimer;
|
|
218
262
|
constructor(socket) {
|
|
219
263
|
this.socket = socket;
|
|
@@ -234,6 +278,11 @@ export class RealtimeTextToSpeechConnection {
|
|
|
234
278
|
const succeed = () => {
|
|
235
279
|
if (settled)
|
|
236
280
|
return;
|
|
281
|
+
if (options.requiredSubprotocol !== undefined &&
|
|
282
|
+
socket.protocol !== options.requiredSubprotocol) {
|
|
283
|
+
fail(new BreezeBlueRealtimeError(`Realtime TTS WebSocket did not negotiate ${options.requiredSubprotocol}.`, { code: "GENERATION_INVALID_RESPONSE" }));
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
237
286
|
settled = true;
|
|
238
287
|
cleanup();
|
|
239
288
|
resolve();
|
|
@@ -271,6 +320,9 @@ export class RealtimeTextToSpeechConnection {
|
|
|
271
320
|
return connection;
|
|
272
321
|
}
|
|
273
322
|
startTurn(turnId) {
|
|
323
|
+
if (this.pendingInstructions !== undefined) {
|
|
324
|
+
throw new BreezeBlueRealtimeError("Realtime TTS instructions update is awaiting session.updated; wait for that event before starting the next turn.", { code: "BAD_REQUEST", recoverable: true });
|
|
325
|
+
}
|
|
274
326
|
this.sendJson({ type: "turn.start", ...(turnId ? { turn_id: turnId } : {}) });
|
|
275
327
|
}
|
|
276
328
|
appendText(text) {
|
|
@@ -285,6 +337,29 @@ export class RealtimeTextToSpeechConnection {
|
|
|
285
337
|
cancelTurn() {
|
|
286
338
|
this.sendJson({ type: "turn.cancel" });
|
|
287
339
|
}
|
|
340
|
+
/**
|
|
341
|
+
* Request new synthesis instructions for subsequent turns.
|
|
342
|
+
*
|
|
343
|
+
* The matching `session.updated` acknowledgement stays on this
|
|
344
|
+
* connection's existing message stream. Keep the single consumer running
|
|
345
|
+
* and wait for that event before starting the next turn.
|
|
346
|
+
*/
|
|
347
|
+
updateInstructions(instructions) {
|
|
348
|
+
const validated = validateRealtimeInstructions(instructions);
|
|
349
|
+
if (this.pendingInstructions !== undefined) {
|
|
350
|
+
throw new BreezeBlueRealtimeError("Realtime TTS already has an instructions update awaiting session.updated.", { code: "BAD_REQUEST", recoverable: true });
|
|
351
|
+
}
|
|
352
|
+
this.pendingInstructions = validated;
|
|
353
|
+
try {
|
|
354
|
+
this.sendJson({ type: "session.update", instructions: validated });
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
if (this.pendingInstructions === validated) {
|
|
358
|
+
this.pendingInstructions = undefined;
|
|
359
|
+
}
|
|
360
|
+
throw error;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
288
363
|
/** Send a keepalive ping. The server answers with a `pong` event. */
|
|
289
364
|
ping() {
|
|
290
365
|
this.sendJson({ type: "ping" });
|
|
@@ -296,6 +371,7 @@ export class RealtimeTextToSpeechConnection {
|
|
|
296
371
|
if (this.socket.readyState === WEBSOCKET_OPEN) {
|
|
297
372
|
this.sendJson({ type: "session.close" });
|
|
298
373
|
}
|
|
374
|
+
this.pendingInstructions = undefined;
|
|
299
375
|
this.socket.close(1000);
|
|
300
376
|
this.finish();
|
|
301
377
|
}
|
|
@@ -351,7 +427,10 @@ export class RealtimeTextToSpeechConnection {
|
|
|
351
427
|
this.fail(new BreezeBlueRealtimeError("Realtime TTS returned an invalid JSON event.", { cause }));
|
|
352
428
|
return;
|
|
353
429
|
}
|
|
354
|
-
|
|
430
|
+
const message = camelizeKeys(parsed);
|
|
431
|
+
if (this.handleInstructionsEvent(message)) {
|
|
432
|
+
this.push(message, data.length);
|
|
433
|
+
}
|
|
355
434
|
return;
|
|
356
435
|
}
|
|
357
436
|
if (data instanceof ArrayBuffer) {
|
|
@@ -363,6 +442,7 @@ export class RealtimeTextToSpeechConnection {
|
|
|
363
442
|
}
|
|
364
443
|
}
|
|
365
444
|
handleClose(event) {
|
|
445
|
+
this.pendingInstructions = undefined;
|
|
366
446
|
this.clearCloseEventGraceTimer();
|
|
367
447
|
if (!this.closed && event.code !== 1000) {
|
|
368
448
|
this.push({
|
|
@@ -409,6 +489,7 @@ export class RealtimeTextToSpeechConnection {
|
|
|
409
489
|
this.clearCloseEventGraceTimer();
|
|
410
490
|
this.closed = true;
|
|
411
491
|
this.failure = error;
|
|
492
|
+
this.pendingInstructions = undefined;
|
|
412
493
|
this.queue.length = 0;
|
|
413
494
|
this.queuedBytes = 0;
|
|
414
495
|
this.socket.close(1000);
|
|
@@ -422,6 +503,7 @@ export class RealtimeTextToSpeechConnection {
|
|
|
422
503
|
}
|
|
423
504
|
this.clearCloseEventGraceTimer();
|
|
424
505
|
this.closed = true;
|
|
506
|
+
this.pendingInstructions = undefined;
|
|
425
507
|
for (const waiter of this.waiters.splice(0)) {
|
|
426
508
|
waiter.resolve({ value: undefined, done: true });
|
|
427
509
|
}
|
|
@@ -432,6 +514,25 @@ export class RealtimeTextToSpeechConnection {
|
|
|
432
514
|
this.closeEventGraceTimer = undefined;
|
|
433
515
|
}
|
|
434
516
|
}
|
|
517
|
+
handleInstructionsEvent(message) {
|
|
518
|
+
if (message.type === "session.updated") {
|
|
519
|
+
if (this.pendingInstructions === undefined ||
|
|
520
|
+
message.instructions !== this.pendingInstructions) {
|
|
521
|
+
this.fail(new BreezeBlueRealtimeError("Realtime TTS returned a session.updated acknowledgement that did not match the pending instructions update.", { code: "GENERATION_INVALID_RESPONSE" }));
|
|
522
|
+
return false;
|
|
523
|
+
}
|
|
524
|
+
this.pendingInstructions = undefined;
|
|
525
|
+
}
|
|
526
|
+
else if (message.type === "error" && this.pendingInstructions !== undefined) {
|
|
527
|
+
if (!realtimeErrorFromEvent(message).reconnect) {
|
|
528
|
+
this.pendingInstructions = undefined;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
else if (message.type === "session.closed") {
|
|
532
|
+
this.pendingInstructions = undefined;
|
|
533
|
+
}
|
|
534
|
+
return true;
|
|
535
|
+
}
|
|
435
536
|
}
|
|
436
537
|
/**
|
|
437
538
|
* A logical realtime TTS connection backed by bounded physical WebSocket
|
|
@@ -446,6 +547,7 @@ export class RealtimeTextToSpeechConnection {
|
|
|
446
547
|
*/
|
|
447
548
|
export class ManagedRealtimeTextToSpeechConnection {
|
|
448
549
|
createConnection;
|
|
550
|
+
onInstructionsConfirmed;
|
|
449
551
|
queue = [];
|
|
450
552
|
drainingEpochs = new Set();
|
|
451
553
|
openAttempts = new Set();
|
|
@@ -457,6 +559,7 @@ export class ManagedRealtimeTextToSpeechConnection {
|
|
|
457
559
|
maxPhysicalSessionMs;
|
|
458
560
|
maxReconnectAttempts;
|
|
459
561
|
reconnectBaseDelayMs;
|
|
562
|
+
reapplyInstructionsOnOpen;
|
|
460
563
|
current;
|
|
461
564
|
heartbeatTimer;
|
|
462
565
|
heartbeatAckTimer;
|
|
@@ -467,11 +570,16 @@ export class ManagedRealtimeTextToSpeechConnection {
|
|
|
467
570
|
epochSequence = 0;
|
|
468
571
|
queuedBytes = 0;
|
|
469
572
|
activeTurn = false;
|
|
573
|
+
instructions;
|
|
574
|
+
pendingInstructions;
|
|
470
575
|
rotationPending = false;
|
|
471
576
|
closed = false;
|
|
472
577
|
failure;
|
|
473
|
-
constructor(createConnection, options) {
|
|
578
|
+
constructor(createConnection, options, onInstructionsConfirmed) {
|
|
474
579
|
this.createConnection = createConnection;
|
|
580
|
+
this.onInstructionsConfirmed = onInstructionsConfirmed;
|
|
581
|
+
this.reapplyInstructionsOnOpen = options.sessionFactory !== undefined;
|
|
582
|
+
this.instructions = this.reapplyInstructionsOnOpen ? undefined : options.instructions;
|
|
475
583
|
this.readyTimeoutMs = positiveNumber(options.timeout ?? DEFAULT_REALTIME_HANDSHAKE_TIMEOUT_MS, "timeout");
|
|
476
584
|
this.heartbeatIntervalMs = optionalNonNegativeNumber(options.heartbeatIntervalMs, "heartbeatIntervalMs");
|
|
477
585
|
this.heartbeatTimeoutMs = positiveNumber(options.heartbeatTimeoutMs ?? DEFAULT_REALTIME_HEARTBEAT_TIMEOUT_MS, "heartbeatTimeoutMs");
|
|
@@ -480,17 +588,22 @@ export class ManagedRealtimeTextToSpeechConnection {
|
|
|
480
588
|
this.maxReconnectAttempts = nonNegativeInteger(options.maxReconnectAttempts ?? DEFAULT_REALTIME_MAX_RECONNECT_ATTEMPTS, "maxReconnectAttempts");
|
|
481
589
|
this.reconnectBaseDelayMs = nonNegativeNumber(options.reconnectBaseDelayMs ?? DEFAULT_REALTIME_RECONNECT_BASE_DELAY_MS, "reconnectBaseDelayMs");
|
|
482
590
|
}
|
|
483
|
-
static async open(createConnection, options = {}) {
|
|
484
|
-
const managed = new ManagedRealtimeTextToSpeechConnection(createConnection, options);
|
|
591
|
+
static async open(createConnection, options = {}, onInstructionsConfirmed = () => { }) {
|
|
592
|
+
const managed = new ManagedRealtimeTextToSpeechConnection(createConnection, options, onInstructionsConfirmed);
|
|
485
593
|
const epoch = await managed.openEpoch();
|
|
486
594
|
managed.installEpoch(epoch);
|
|
487
595
|
return managed;
|
|
488
596
|
}
|
|
489
597
|
startTurn(turnId) {
|
|
598
|
+
if (this.pendingInstructions !== undefined) {
|
|
599
|
+
throw new BreezeBlueRealtimeError("Realtime TTS instructions update is awaiting session.updated; wait for that event before starting the next turn.", { code: "BAD_REQUEST", recoverable: true });
|
|
600
|
+
}
|
|
490
601
|
const connection = this.requireCurrentConnection();
|
|
602
|
+
// Claim the turn before the first WebSocket write so a custom WebSocket
|
|
603
|
+
// implementation cannot re-enter updateInstructions() from send().
|
|
604
|
+
this.activeTurn = true;
|
|
491
605
|
try {
|
|
492
606
|
connection.startTurn(turnId);
|
|
493
|
-
this.activeTurn = true;
|
|
494
607
|
}
|
|
495
608
|
catch (error) {
|
|
496
609
|
this.activeTurn = false;
|
|
@@ -536,6 +649,36 @@ export class ManagedRealtimeTextToSpeechConnection {
|
|
|
536
649
|
cancelTurn() {
|
|
537
650
|
this.requireCurrentConnection().cancelTurn();
|
|
538
651
|
}
|
|
652
|
+
/**
|
|
653
|
+
* Update the synthesis instructions used by subsequent turns.
|
|
654
|
+
*
|
|
655
|
+
* The matching `session.updated` acknowledgement stays on this logical
|
|
656
|
+
* connection's ordered event stream. Confirmed instructions are carried to
|
|
657
|
+
* later physical WebSocket epochs.
|
|
658
|
+
*/
|
|
659
|
+
updateInstructions(instructions) {
|
|
660
|
+
const validated = validateRealtimeInstructions(instructions);
|
|
661
|
+
if (this.activeTurn) {
|
|
662
|
+
throw new BreezeBlueRealtimeError("Realtime TTS instructions can only be updated between turns.", { code: "BAD_REQUEST", recoverable: true });
|
|
663
|
+
}
|
|
664
|
+
if (this.pendingInstructions !== undefined) {
|
|
665
|
+
throw new BreezeBlueRealtimeError("Realtime TTS already has an instructions update awaiting session.updated.", { code: "BAD_REQUEST", recoverable: true });
|
|
666
|
+
}
|
|
667
|
+
if (this.transitionPromise !== undefined) {
|
|
668
|
+
throw new BreezeBlueRealtimeError("Realtime TTS is rotating its physical session; wait for the next session.ready event before updating instructions.", { code: "BAD_REQUEST", recoverable: true });
|
|
669
|
+
}
|
|
670
|
+
const connection = this.requireCurrentConnection();
|
|
671
|
+
this.pendingInstructions = validated;
|
|
672
|
+
try {
|
|
673
|
+
connection.updateInstructions(validated);
|
|
674
|
+
}
|
|
675
|
+
catch (error) {
|
|
676
|
+
if (this.pendingInstructions === validated) {
|
|
677
|
+
this.pendingInstructions = undefined;
|
|
678
|
+
}
|
|
679
|
+
throw error;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
539
682
|
/** Send an immediate ping in addition to the managed keepalive schedule. */
|
|
540
683
|
ping() {
|
|
541
684
|
this.requireCurrentConnection().ping();
|
|
@@ -545,6 +688,7 @@ export class ManagedRealtimeTextToSpeechConnection {
|
|
|
545
688
|
return;
|
|
546
689
|
}
|
|
547
690
|
this.closed = true;
|
|
691
|
+
this.pendingInstructions = undefined;
|
|
548
692
|
this.clearTimers();
|
|
549
693
|
this.cancelRetryDelay();
|
|
550
694
|
this.cancelOpenAttempts();
|
|
@@ -636,6 +780,26 @@ export class ManagedRealtimeTextToSpeechConnection {
|
|
|
636
780
|
}
|
|
637
781
|
const message = result.value;
|
|
638
782
|
if (message.type === "session.ready") {
|
|
783
|
+
if (this.reapplyInstructionsOnOpen && this.instructions !== undefined) {
|
|
784
|
+
connection.updateInstructions(this.instructions);
|
|
785
|
+
while (true) {
|
|
786
|
+
const confirmation = await Promise.race([iterator.next(), boundary]);
|
|
787
|
+
if (confirmation.done) {
|
|
788
|
+
throw new BreezeBlueRealtimeError("Realtime TTS connection closed before confirming the instructions update.", { reconnect: true });
|
|
789
|
+
}
|
|
790
|
+
const confirmationMessage = confirmation.value;
|
|
791
|
+
if (confirmationMessage.type === "session.updated") {
|
|
792
|
+
break;
|
|
793
|
+
}
|
|
794
|
+
if (confirmationMessage.type === "error") {
|
|
795
|
+
throw realtimeErrorFromEvent(confirmationMessage);
|
|
796
|
+
}
|
|
797
|
+
if (confirmationMessage.type === "session.closed") {
|
|
798
|
+
throw realtimeErrorFromClose(confirmationMessage);
|
|
799
|
+
}
|
|
800
|
+
pending.push(confirmationMessage);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
639
803
|
this.completeOpenAttempt(attempt);
|
|
640
804
|
return {
|
|
641
805
|
id: ++this.epochSequence,
|
|
@@ -748,6 +912,17 @@ export class ManagedRealtimeTextToSpeechConnection {
|
|
|
748
912
|
if (message.type === "turn.started") {
|
|
749
913
|
this.activeTurn = true;
|
|
750
914
|
}
|
|
915
|
+
else if (message.type === "session.updated") {
|
|
916
|
+
if (this.pendingInstructions === undefined ||
|
|
917
|
+
message.instructions !== this.pendingInstructions) {
|
|
918
|
+
this.fail(new BreezeBlueRealtimeError("Realtime TTS returned a session.updated acknowledgement that did not match the pending instructions update.", { code: "GENERATION_INVALID_RESPONSE" }));
|
|
919
|
+
return false;
|
|
920
|
+
}
|
|
921
|
+
const confirmedInstructions = this.pendingInstructions;
|
|
922
|
+
this.instructions = confirmedInstructions;
|
|
923
|
+
this.pendingInstructions = undefined;
|
|
924
|
+
this.onInstructionsConfirmed(confirmedInstructions);
|
|
925
|
+
}
|
|
751
926
|
else if (message.type === "turn.done") {
|
|
752
927
|
this.activeTurn = false;
|
|
753
928
|
epoch.pendingUsageHistoryIds.add(message.historyItemId);
|
|
@@ -767,13 +942,16 @@ export class ManagedRealtimeTextToSpeechConnection {
|
|
|
767
942
|
epoch.retryAfterMs = Math.max(epoch.retryAfterMs, message.meta.retryAfterMs);
|
|
768
943
|
}
|
|
769
944
|
if (error.recoverable) {
|
|
945
|
+
if (this.pendingInstructions !== undefined && !this.activeTurn) {
|
|
946
|
+
this.pendingInstructions = undefined;
|
|
947
|
+
}
|
|
770
948
|
if (message.code === "GENERATION_CONCURRENCY_EXCEEDED") {
|
|
771
949
|
// The server rejected turn.start before establishing an active turn,
|
|
772
950
|
// so no turn.cancelled event will follow to clear local state.
|
|
773
951
|
this.activeTurn = false;
|
|
774
952
|
}
|
|
775
953
|
this.push(message);
|
|
776
|
-
if (!this.activeTurn && this.rotationPending) {
|
|
954
|
+
if (!this.activeTurn && this.pendingInstructions === undefined && this.rotationPending) {
|
|
777
955
|
this.beginTransition("planned", epoch);
|
|
778
956
|
}
|
|
779
957
|
return true;
|
|
@@ -805,7 +983,7 @@ export class ManagedRealtimeTextToSpeechConnection {
|
|
|
805
983
|
return true;
|
|
806
984
|
}
|
|
807
985
|
this.push(message);
|
|
808
|
-
if (!this.activeTurn && this.rotationPending) {
|
|
986
|
+
if (!this.activeTurn && this.pendingInstructions === undefined && this.rotationPending) {
|
|
809
987
|
this.beginTransition("planned", epoch);
|
|
810
988
|
}
|
|
811
989
|
return true;
|
|
@@ -818,6 +996,7 @@ export class ManagedRealtimeTextToSpeechConnection {
|
|
|
818
996
|
this.fail(activeTurnInterruptedError());
|
|
819
997
|
return;
|
|
820
998
|
}
|
|
999
|
+
this.pendingInstructions = undefined;
|
|
821
1000
|
if (epoch.reconnectRequested) {
|
|
822
1001
|
this.requestUnexpectedReconnect(epoch);
|
|
823
1002
|
return;
|
|
@@ -834,6 +1013,7 @@ export class ManagedRealtimeTextToSpeechConnection {
|
|
|
834
1013
|
this.fail(activeTurnInterruptedError(cause));
|
|
835
1014
|
return;
|
|
836
1015
|
}
|
|
1016
|
+
this.pendingInstructions = undefined;
|
|
837
1017
|
this.clearTimers();
|
|
838
1018
|
this.current = undefined;
|
|
839
1019
|
epoch.connection.close();
|
|
@@ -843,6 +1023,10 @@ export class ManagedRealtimeTextToSpeechConnection {
|
|
|
843
1023
|
if (this.closed || this.transitionPromise !== undefined) {
|
|
844
1024
|
return;
|
|
845
1025
|
}
|
|
1026
|
+
if (kind === "planned" && this.pendingInstructions !== undefined) {
|
|
1027
|
+
this.rotationPending = true;
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
846
1030
|
this.transitionKind = kind;
|
|
847
1031
|
this.transitionPromise = (async () => {
|
|
848
1032
|
try {
|
|
@@ -855,7 +1039,11 @@ export class ManagedRealtimeTextToSpeechConnection {
|
|
|
855
1039
|
this.transitionPromise = undefined;
|
|
856
1040
|
this.transitionKind = undefined;
|
|
857
1041
|
const current = this.current;
|
|
858
|
-
if (!this.closed &&
|
|
1042
|
+
if (!this.closed &&
|
|
1043
|
+
this.rotationPending &&
|
|
1044
|
+
!this.activeTurn &&
|
|
1045
|
+
this.pendingInstructions === undefined &&
|
|
1046
|
+
current) {
|
|
859
1047
|
this.beginTransition("planned", current);
|
|
860
1048
|
}
|
|
861
1049
|
}
|
|
@@ -949,7 +1137,7 @@ export class ManagedRealtimeTextToSpeechConnection {
|
|
|
949
1137
|
return;
|
|
950
1138
|
}
|
|
951
1139
|
this.rotationPending = true;
|
|
952
|
-
if (!this.activeTurn) {
|
|
1140
|
+
if (!this.activeTurn && this.pendingInstructions === undefined) {
|
|
953
1141
|
this.beginTransition("planned", epoch);
|
|
954
1142
|
}
|
|
955
1143
|
}, rotationDelay);
|
|
@@ -1050,6 +1238,7 @@ export class ManagedRealtimeTextToSpeechConnection {
|
|
|
1050
1238
|
return;
|
|
1051
1239
|
}
|
|
1052
1240
|
this.failure = error;
|
|
1241
|
+
this.pendingInstructions = undefined;
|
|
1053
1242
|
this.closed = true;
|
|
1054
1243
|
this.clearTimers();
|
|
1055
1244
|
this.cancelRetryDelay();
|
|
@@ -1410,6 +1599,18 @@ function requireWebSocket(WebSocketImpl) {
|
|
|
1410
1599
|
}
|
|
1411
1600
|
return WebSocketImpl;
|
|
1412
1601
|
}
|
|
1602
|
+
function validateRealtimeInstructions(instructions) {
|
|
1603
|
+
if (typeof instructions !== "string") {
|
|
1604
|
+
throw new TypeError("instructions must be a string.");
|
|
1605
|
+
}
|
|
1606
|
+
if (instructions.trim().length === 0) {
|
|
1607
|
+
throw new RangeError("instructions must not be empty.");
|
|
1608
|
+
}
|
|
1609
|
+
if (Array.from(instructions).length > REALTIME_MAX_INSTRUCTIONS_CHARACTERS) {
|
|
1610
|
+
throw new RangeError(`instructions must be at most ${REALTIME_MAX_INSTRUCTIONS_CHARACTERS} characters.`);
|
|
1611
|
+
}
|
|
1612
|
+
return instructions;
|
|
1613
|
+
}
|
|
1413
1614
|
function normalizeBaseUrl(value) {
|
|
1414
1615
|
return value.replace(/\/+$/, "");
|
|
1415
1616
|
}
|
package/dist/errors.d.ts
CHANGED
|
@@ -16,10 +16,11 @@ export declare class BreezeBlueConfigurationError extends BreezeBlueError {
|
|
|
16
16
|
* message iterator rejects with this error.
|
|
17
17
|
*
|
|
18
18
|
* `recoverable` mirrors the Python SDK's `RealtimeError.recoverable`: it is
|
|
19
|
-
* `true`
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
19
|
+
* `true` when the same connection remains usable. Most recoverable
|
|
20
|
+
* turn-command errors cancel the active turn; a `session.update` rejected
|
|
21
|
+
* while a turn is active does not, so keep consuming that turn. SDK-detected
|
|
22
|
+
* connection failures are never recoverable. `reconnect` tells you whether
|
|
23
|
+
* opening a new session is expected to succeed.
|
|
23
24
|
*/
|
|
24
25
|
export declare class BreezeBlueRealtimeError extends BreezeBlueError {
|
|
25
26
|
readonly code: string | undefined;
|
package/dist/errors.js
CHANGED
|
@@ -12,10 +12,11 @@ export class BreezeBlueConfigurationError extends BreezeBlueError {
|
|
|
12
12
|
* message iterator rejects with this error.
|
|
13
13
|
*
|
|
14
14
|
* `recoverable` mirrors the Python SDK's `RealtimeError.recoverable`: it is
|
|
15
|
-
* `true`
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* `true` when the same connection remains usable. Most recoverable
|
|
16
|
+
* turn-command errors cancel the active turn; a `session.update` rejected
|
|
17
|
+
* while a turn is active does not, so keep consuming that turn. SDK-detected
|
|
18
|
+
* connection failures are never recoverable. `reconnect` tells you whether
|
|
19
|
+
* opening a new session is expected to succeed.
|
|
19
20
|
*/
|
|
20
21
|
export class BreezeBlueRealtimeError extends BreezeBlueError {
|
|
21
22
|
code;
|
package/dist/types.d.ts
CHANGED
|
@@ -28,6 +28,7 @@ export type BreezeBlueWebSocketConstructor = new (url: string, protocols?: strin
|
|
|
28
28
|
export interface BreezeBlueWebSocket {
|
|
29
29
|
binaryType: string;
|
|
30
30
|
readyState: number;
|
|
31
|
+
readonly protocol?: string;
|
|
31
32
|
onopen: ((event: Event) => void) | null;
|
|
32
33
|
onmessage: ((event: MessageEvent) => void) | null;
|
|
33
34
|
onerror: ((event: Event) => void) | null;
|
|
@@ -54,12 +55,14 @@ export interface RealtimeTextToSpeechAudioFormat {
|
|
|
54
55
|
export interface RealtimeTextToSpeechSession {
|
|
55
56
|
clientSecret: string;
|
|
56
57
|
websocketUrl: string;
|
|
58
|
+
directWebsocketUrl?: string;
|
|
57
59
|
expiresAt: string;
|
|
58
60
|
audioFormat: RealtimeTextToSpeechAudioFormat;
|
|
59
61
|
}
|
|
60
62
|
export interface RealtimeTextToSpeechConnectOptions extends RealtimeTextToSpeechSessionRequest {
|
|
61
63
|
clientSecret?: string;
|
|
62
64
|
websocketUrl?: string;
|
|
65
|
+
directWebsocketUrl?: string;
|
|
63
66
|
webSocket?: BreezeBlueWebSocketConstructor;
|
|
64
67
|
signal?: AbortSignal;
|
|
65
68
|
/** Handshake timeout in milliseconds. Defaults to the client `timeout`. */
|
|
@@ -68,9 +71,11 @@ export interface RealtimeTextToSpeechConnectOptions extends RealtimeTextToSpeech
|
|
|
68
71
|
export type RealtimeTextToSpeechSessionCredentials = {
|
|
69
72
|
clientSecret: string;
|
|
70
73
|
websocketUrl?: string;
|
|
74
|
+
directWebsocketUrl?: string;
|
|
71
75
|
} | {
|
|
72
76
|
clientSecret?: string;
|
|
73
77
|
websocketUrl: string;
|
|
78
|
+
directWebsocketUrl?: string;
|
|
74
79
|
};
|
|
75
80
|
export interface RealtimeTextToSpeechSessionFactoryContext {
|
|
76
81
|
/**
|
|
@@ -134,6 +139,10 @@ export interface RealtimeTextToSpeechSessionReadyEvent {
|
|
|
134
139
|
inactivityTimeoutSeconds: number;
|
|
135
140
|
maxSessionSeconds: number;
|
|
136
141
|
}
|
|
142
|
+
export interface RealtimeTextToSpeechSessionUpdatedEvent {
|
|
143
|
+
type: "session.updated";
|
|
144
|
+
instructions: string;
|
|
145
|
+
}
|
|
137
146
|
export interface RealtimeTextToSpeechSessionExpiringEvent {
|
|
138
147
|
type: "session.expiring";
|
|
139
148
|
sessionId: string;
|
|
@@ -209,7 +218,7 @@ export interface RealtimeTextToSpeechErrorEvent {
|
|
|
209
218
|
* (with camelCase keys). Do not rely on an exhaustive `switch` over `type`;
|
|
210
219
|
* ignore event types you do not recognize.
|
|
211
220
|
*/
|
|
212
|
-
export type RealtimeTextToSpeechEvent = RealtimeTextToSpeechSessionReadyEvent | RealtimeTextToSpeechSessionExpiringEvent | RealtimeTextToSpeechPongEvent | RealtimeTextToSpeechTurnStartedEvent | RealtimeTextToSpeechAudioStartedEvent | RealtimeTextToSpeechTurnDoneEvent | RealtimeTextToSpeechTurnCancelledEvent | RealtimeTextToSpeechUsageCommittedEvent | RealtimeTextToSpeechSessionClosedEvent | RealtimeTextToSpeechErrorEvent;
|
|
221
|
+
export type RealtimeTextToSpeechEvent = RealtimeTextToSpeechSessionReadyEvent | RealtimeTextToSpeechSessionUpdatedEvent | RealtimeTextToSpeechSessionExpiringEvent | RealtimeTextToSpeechPongEvent | RealtimeTextToSpeechTurnStartedEvent | RealtimeTextToSpeechAudioStartedEvent | RealtimeTextToSpeechTurnDoneEvent | RealtimeTextToSpeechTurnCancelledEvent | RealtimeTextToSpeechUsageCommittedEvent | RealtimeTextToSpeechSessionClosedEvent | RealtimeTextToSpeechErrorEvent;
|
|
213
222
|
export interface RealtimeTextToSpeechAudioMessage {
|
|
214
223
|
type: "audio";
|
|
215
224
|
audio: Uint8Array;
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const SDK_VERSION = "0.
|
|
1
|
+
export declare const SDK_VERSION = "0.8.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.8.0";
|
|
2
2
|
export const SDK_NAME = "@breeze.blue/sdk";
|