@breeze.blue/sdk 0.7.0 → 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 +6 -0
- package/README.md +7 -1
- package/dist/client.d.ts +1 -0
- package/dist/client.js +60 -17
- package/dist/types.d.ts +5 -0
- 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,12 @@
|
|
|
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
|
+
|
|
5
11
|
## 0.7.0
|
|
6
12
|
|
|
7
13
|
- Realtime TTS connections can update synthesis instructions between turns
|
package/README.md
CHANGED
|
@@ -278,9 +278,15 @@ const session = await client.textToSpeech.realtime.createSession("voc_...", {
|
|
|
278
278
|
// Later, when the first text is ready:
|
|
279
279
|
const connection = await client.textToSpeech.realtime.connect("voc_...", {
|
|
280
280
|
clientSecret: session.clientSecret,
|
|
281
|
+
websocketUrl: session.websocketUrl,
|
|
282
|
+
directWebsocketUrl: session.directWebsocketUrl,
|
|
281
283
|
});
|
|
282
284
|
```
|
|
283
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
|
+
|
|
284
290
|
The same `clientSecret` handoff lets a browser connect without ever seeing your
|
|
285
291
|
API key: create the session on your server, hand `session.clientSecret` to the
|
|
286
292
|
page, and build a key-less client there:
|
|
@@ -315,7 +321,7 @@ const connection = await browserClient.textToSpeech.realtime.connectManaged("voc
|
|
|
315
321
|
reconnect,
|
|
316
322
|
});
|
|
317
323
|
}
|
|
318
|
-
return response.json(); // { clientSecret, websocketUrl? }
|
|
324
|
+
return response.json(); // { clientSecret, websocketUrl?, directWebsocketUrl? }
|
|
319
325
|
},
|
|
320
326
|
});
|
|
321
327
|
```
|
package/dist/client.d.ts
CHANGED
|
@@ -54,6 +54,7 @@ export declare class RealtimeTextToSpeechConnection implements AsyncIterable<Rea
|
|
|
54
54
|
static open(socket: BreezeBlueWebSocket, options?: {
|
|
55
55
|
timeout?: number;
|
|
56
56
|
signal?: AbortSignal;
|
|
57
|
+
requiredSubprotocol?: string;
|
|
57
58
|
}): Promise<RealtimeTextToSpeechConnection>;
|
|
58
59
|
startTurn(turnId?: string): void;
|
|
59
60
|
appendText(text: string): void;
|
package/dist/client.js
CHANGED
|
@@ -15,6 +15,9 @@ 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
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;
|
|
18
21
|
const WEBSOCKET_OPEN = 1;
|
|
19
22
|
export class BreezeBlueClient {
|
|
20
23
|
apiKey;
|
|
@@ -129,31 +132,55 @@ class RealtimeTextToSpeechResource {
|
|
|
129
132
|
return this.client.requestJson("POST", `/v1/text-to-speech/${encodeURIComponent(voiceId)}/realtime-sessions`, undefined, request, options);
|
|
130
133
|
}
|
|
131
134
|
async connect(voiceId, options = {}) {
|
|
132
|
-
if ((options.clientSecret !== undefined ||
|
|
135
|
+
if ((options.clientSecret !== undefined ||
|
|
136
|
+
options.websocketUrl !== undefined ||
|
|
137
|
+
options.directWebsocketUrl !== undefined) && hasRealtimeSessionParams(options)) {
|
|
133
138
|
console.warn("@breeze.blue/sdk: realtime.connect ignores modelId, languageCode, instructions, voiceSettings, " +
|
|
134
|
-
"inactivityTimeoutSeconds, and enableLogging when
|
|
139
|
+
"inactivityTimeoutSeconds, and enableLogging when session credentials are provided. " +
|
|
135
140
|
"Configure these initial values when creating the realtime session instead. " +
|
|
136
141
|
"To change instructions between turns, call connection.updateInstructions() and wait for session.updated.");
|
|
137
142
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
+
}
|
|
152
178
|
const WebSocketImpl = requireWebSocket(options.webSocket ?? this.client.webSocket);
|
|
153
|
-
const socket = new WebSocketImpl(websocketUrl);
|
|
179
|
+
const socket = new WebSocketImpl(websocketUrl, protocols);
|
|
154
180
|
return RealtimeTextToSpeechConnection.open(socket, {
|
|
155
181
|
timeout: options.timeout ?? this.client.timeout ?? DEFAULT_REALTIME_HANDSHAKE_TIMEOUT_MS,
|
|
156
182
|
signal: options.signal,
|
|
183
|
+
requiredSubprotocol,
|
|
157
184
|
});
|
|
158
185
|
}
|
|
159
186
|
/**
|
|
@@ -191,6 +218,7 @@ class RealtimeTextToSpeechResource {
|
|
|
191
218
|
return this.connect(voiceId, {
|
|
192
219
|
clientSecret: credentials.clientSecret,
|
|
193
220
|
websocketUrl: credentials.websocketUrl,
|
|
221
|
+
directWebsocketUrl: credentials.directWebsocketUrl,
|
|
194
222
|
webSocket: options.webSocket,
|
|
195
223
|
signal,
|
|
196
224
|
timeout: connectionTimeout,
|
|
@@ -212,6 +240,16 @@ function hasRealtimeSessionParams(options) {
|
|
|
212
240
|
options.inactivityTimeoutSeconds !== undefined ||
|
|
213
241
|
options.enableLogging !== undefined);
|
|
214
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
|
+
}
|
|
215
253
|
export class RealtimeTextToSpeechConnection {
|
|
216
254
|
socket;
|
|
217
255
|
queue = [];
|
|
@@ -240,6 +278,11 @@ export class RealtimeTextToSpeechConnection {
|
|
|
240
278
|
const succeed = () => {
|
|
241
279
|
if (settled)
|
|
242
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
|
+
}
|
|
243
286
|
settled = true;
|
|
244
287
|
cleanup();
|
|
245
288
|
resolve();
|
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
|
/**
|
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";
|