@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 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 || options.websocketUrl !== undefined) && hasRealtimeSessionParams(options)) {
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 clientSecret or websocketUrl is provided. " +
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
- const websocketUrl = options.websocketUrl ??
139
- (options.clientSecret
140
- ? buildUrl(this.client.baseUrl.replace(/^http/i, "ws"), `/v1/text-to-speech/${encodeURIComponent(voiceId)}/stream-input`, { client_secret: options.clientSecret })
141
- : (await this.createSession(voiceId, {
142
- modelId: options.modelId,
143
- languageCode: options.languageCode,
144
- instructions: options.instructions,
145
- voiceSettings: options.voiceSettings,
146
- inactivityTimeoutSeconds: options.inactivityTimeoutSeconds,
147
- enableLogging: options.enableLogging,
148
- }, {
149
- signal: options.signal,
150
- timeout: options.timeout,
151
- })).websocketUrl);
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.7.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.7.0";
1
+ export const SDK_VERSION = "0.9.0";
2
2
  export const SDK_NAME = "@breeze.blue/sdk";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@breeze.blue/sdk",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "ESM-first TypeScript SDK for the Breeze Blue Developer API.",
5
5
  "license": "MIT",
6
6
  "author": "Breeze Blue <support@breeze.blue>",