@breeze.blue/sdk 0.7.0 → 0.9.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 +11 -0
- package/README.md +9 -2
- package/dist/client.d.ts +1 -0
- package/dist/client.js +62 -17
- package/dist/types.d.ts +10 -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,17 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.9.0
|
|
6
|
+
|
|
7
|
+
- Added language-aware voice search and language metadata for voice results,
|
|
8
|
+
preview saves, and saved-voice edits.
|
|
9
|
+
|
|
10
|
+
## 0.8.0
|
|
11
|
+
|
|
12
|
+
- Realtime TTS sessions prefer the optional query-free direct WebSocket URL
|
|
13
|
+
and authenticate with a non-echoed token-bearing subprotocol. Existing
|
|
14
|
+
public session URLs remain the fallback for older deployments and clients.
|
|
15
|
+
|
|
5
16
|
## 0.7.0
|
|
6
17
|
|
|
7
18
|
- 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
|
```
|
|
@@ -443,13 +449,14 @@ await save(audio, "preview.mp3");
|
|
|
443
449
|
const saved = await client.voices.savePreview({
|
|
444
450
|
generatedVoiceId,
|
|
445
451
|
voiceName: "Documentary narrator",
|
|
452
|
+
languageCode: "en",
|
|
446
453
|
});
|
|
447
454
|
```
|
|
448
455
|
|
|
449
456
|
Edit, tune settings, or delete a saved voice:
|
|
450
457
|
|
|
451
458
|
```ts
|
|
452
|
-
await client.voices.edit(saved.voiceId, { name: "Renamed narrator" });
|
|
459
|
+
await client.voices.edit(saved.voiceId, { name: "Renamed narrator", languageCode: "en" });
|
|
453
460
|
await client.voices.editSettings(saved.voiceId, { guidanceScale: 1.2 });
|
|
454
461
|
await client.voices.delete(saved.voiceId);
|
|
455
462
|
```
|
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();
|
|
@@ -1503,6 +1546,7 @@ class VoicesResource {
|
|
|
1503
1546
|
primaryCategoryCode: request.primaryCategoryCode,
|
|
1504
1547
|
visibility: request.visibility,
|
|
1505
1548
|
playedNotSelectedVoiceIds: request.playedNotSelectedVoiceIds,
|
|
1549
|
+
languageCode: request.languageCode,
|
|
1506
1550
|
}, options);
|
|
1507
1551
|
}
|
|
1508
1552
|
}
|
|
@@ -1651,6 +1695,7 @@ function voiceEditForm(request) {
|
|
|
1651
1695
|
appendOptional(form, "labels", request.labels ? JSON.stringify(request.labels) : undefined);
|
|
1652
1696
|
appendOptional(form, "primary_category_code", request.primaryCategoryCode);
|
|
1653
1697
|
appendOptional(form, "visibility", request.visibility);
|
|
1698
|
+
appendOptional(form, "language_code", request.languageCode);
|
|
1654
1699
|
for (const file of normalizeFiles(request.file, request.files, { required: false })) {
|
|
1655
1700
|
appendFile(form, "files", file);
|
|
1656
1701
|
}
|
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
|
/**
|
|
@@ -304,6 +309,7 @@ export interface Voice {
|
|
|
304
309
|
createdAtUnix?: number | null;
|
|
305
310
|
primaryCategoryCode?: string | null;
|
|
306
311
|
visibility?: string | null;
|
|
312
|
+
languageCode: string;
|
|
307
313
|
[key: string]: unknown;
|
|
308
314
|
}
|
|
309
315
|
export interface SavedVoice {
|
|
@@ -315,6 +321,7 @@ export interface SavedVoice {
|
|
|
315
321
|
createdAtUnix?: number | null;
|
|
316
322
|
primaryCategoryCode?: string | null;
|
|
317
323
|
visibility?: string | null;
|
|
324
|
+
languageCode: string;
|
|
318
325
|
}
|
|
319
326
|
export interface VoiceList {
|
|
320
327
|
voices: Voice[];
|
|
@@ -326,6 +333,7 @@ export interface VoiceList {
|
|
|
326
333
|
}
|
|
327
334
|
export interface VoiceSearchParams {
|
|
328
335
|
search?: string;
|
|
336
|
+
languageCode?: string;
|
|
329
337
|
origin?: "designed" | "cloned" | string;
|
|
330
338
|
voiceType?: "all" | "default" | "personal" | string;
|
|
331
339
|
sort?: "created_at_unix" | "name" | string;
|
|
@@ -363,6 +371,7 @@ export interface VoiceEditRequest {
|
|
|
363
371
|
labels?: Record<string, string>;
|
|
364
372
|
primaryCategoryCode?: string;
|
|
365
373
|
visibility?: "private" | "public" | string;
|
|
374
|
+
languageCode?: string;
|
|
366
375
|
file?: UploadData | UploadFile;
|
|
367
376
|
files?: Array<UploadData | UploadFile>;
|
|
368
377
|
}
|
|
@@ -396,6 +405,7 @@ export interface SaveVoiceRequest {
|
|
|
396
405
|
primaryCategoryCode?: string;
|
|
397
406
|
visibility?: "private" | "public" | string;
|
|
398
407
|
playedNotSelectedVoiceIds?: string[];
|
|
408
|
+
languageCode?: string;
|
|
399
409
|
}
|
|
400
410
|
export interface HistoryItem {
|
|
401
411
|
historyItemId: 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.9.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.9.0";
|
|
2
2
|
export const SDK_NAME = "@breeze.blue/sdk";
|