@breeze.blue/sdk 0.3.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/dist/client.js ADDED
@@ -0,0 +1,331 @@
1
+ import { AudioResponse } from "./audio.js";
2
+ import { BreezeBlueConfigurationError, errorFromResponse } from "./errors.js";
3
+ import { camelizeKeys, snakeizeKeys } from "./_transform.js";
4
+ import { SDK_NAME, SDK_VERSION } from "./version.js";
5
+ const DEFAULT_BASE_URL = "https://api.breeze.blue";
6
+ const DEFAULT_TIMEOUT_MS = 60_000;
7
+ export class BreezeBlueClient {
8
+ apiKey;
9
+ baseUrl;
10
+ fetch;
11
+ headers;
12
+ timeout;
13
+ textToSpeech;
14
+ generationJobs;
15
+ voices;
16
+ history;
17
+ models;
18
+ account;
19
+ constructor(options = {}) {
20
+ const envApiKey = typeof process !== "undefined" ? process.env.BREEZE_API_KEY : undefined;
21
+ const envBaseUrl = typeof process !== "undefined" ? process.env.BREEZE_BASE_URL : undefined;
22
+ const apiKey = options.apiKey ?? envApiKey;
23
+ if (!apiKey) {
24
+ throw new BreezeBlueConfigurationError("Missing Breeze API key. Pass apiKey or set BREEZE_API_KEY.");
25
+ }
26
+ this.apiKey = apiKey;
27
+ this.baseUrl = normalizeBaseUrl(options.baseUrl ?? envBaseUrl ?? DEFAULT_BASE_URL);
28
+ this.fetch = options.fetch ?? globalThis.fetch;
29
+ this.headers = options.headers;
30
+ this.timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
31
+ this.textToSpeech = new TextToSpeechResource(this);
32
+ this.generationJobs = new GenerationJobsResource(this);
33
+ this.voices = new VoicesResource(this);
34
+ this.history = new HistoryResource(this);
35
+ this.models = new ModelsResource(this);
36
+ this.account = new AccountResource(this);
37
+ }
38
+ async requestJson(method, path, params, body, options) {
39
+ const response = await this.request(method, path, params, body, options);
40
+ const text = await response.text();
41
+ if (!text) {
42
+ return undefined;
43
+ }
44
+ return camelizeKeys(JSON.parse(text));
45
+ }
46
+ async requestAudio(method, path, params, body, options) {
47
+ const response = await this.request(method, path, params, body, options);
48
+ return new AudioResponse(response);
49
+ }
50
+ async request(method, path, params, body, options) {
51
+ const signal = buildAbortSignal(options?.signal, options?.timeout ?? this.timeout);
52
+ const wireParams = params ? snakeizeKeys(params) : undefined;
53
+ const wireBody = transformRequestBody(body);
54
+ try {
55
+ const response = await this.fetch(buildUrl(this.baseUrl, path, wireParams), {
56
+ method,
57
+ headers: this.buildHeaders(wireBody, options?.headers),
58
+ body: bodyToRequestBody(wireBody),
59
+ signal: signal.signal,
60
+ });
61
+ if (!response.ok) {
62
+ throw await errorFromResponse(response);
63
+ }
64
+ return response;
65
+ }
66
+ finally {
67
+ signal.cleanup();
68
+ }
69
+ }
70
+ buildHeaders(body, requestHeaders) {
71
+ const headers = new Headers(this.headers);
72
+ headers.set("xi-api-key", this.apiKey);
73
+ headers.set("x-breeze-sdk", `${SDK_NAME}/${SDK_VERSION}`);
74
+ mergeHeaders(headers, requestHeaders);
75
+ if (body !== undefined && !(body instanceof FormData) && !headers.has("content-type")) {
76
+ headers.set("content-type", "application/json");
77
+ }
78
+ return headers;
79
+ }
80
+ }
81
+ class TextToSpeechResource {
82
+ client;
83
+ constructor(client) {
84
+ this.client = client;
85
+ }
86
+ convert(voiceId, request, options = {}) {
87
+ return this.client.requestAudio("POST", `/v1/text-to-speech/${encodeURIComponent(voiceId)}`, { outputFormat: options.outputFormat }, request, options);
88
+ }
89
+ createJob(voiceId, request, options = {}) {
90
+ return this.client.requestJson("POST", `/v1/text-to-speech/${encodeURIComponent(voiceId)}`, { outputFormat: options.outputFormat, delivery: "async" }, request, options);
91
+ }
92
+ stream(voiceId, request, options = {}) {
93
+ return this.client.requestAudio("POST", `/v1/text-to-speech/${encodeURIComponent(voiceId)}/stream`, {
94
+ outputFormat: options.outputFormat,
95
+ optimizeStreamingLatency: options.optimizeStreamingLatency,
96
+ enableLogging: options.enableLogging,
97
+ }, request, options);
98
+ }
99
+ }
100
+ class GenerationJobsResource {
101
+ client;
102
+ constructor(client) {
103
+ this.client = client;
104
+ }
105
+ get(generationJobId, options) {
106
+ return this.client.requestJson("GET", `/v1/generation-jobs/${encodeURIComponent(generationJobId)}`, undefined, undefined, options);
107
+ }
108
+ downloadAudio(generationJobId, options) {
109
+ return this.client.requestAudio("GET", `/v1/generation-jobs/${encodeURIComponent(generationJobId)}/audio`, undefined, undefined, options);
110
+ }
111
+ }
112
+ /**
113
+ * Voice CRUD plus the two-step preview → save flow for Breeze voices.
114
+ *
115
+ * Breeze's voice creation is always two steps: produce a preview
116
+ * (`createClonePreview` from an audio sample, or `createDesignPreview` from
117
+ * a text description), then persist it with `savePreview`. Preview generation
118
+ * never creates a saved voice until `savePreview` is called.
119
+ */
120
+ class VoicesResource {
121
+ client;
122
+ constructor(client) {
123
+ this.client = client;
124
+ }
125
+ // -- listing / single voice CRUD --------------------------------------
126
+ search(params = {}, options) {
127
+ return this.client.requestJson("GET", "/v1/voices", params, undefined, options);
128
+ }
129
+ get(voiceId, options) {
130
+ return this.client.requestJson("GET", `/v1/voices/${encodeURIComponent(voiceId)}`, undefined, undefined, options);
131
+ }
132
+ edit(voiceId, request, options) {
133
+ return this.client.requestJson("PATCH", `/v1/voices/${encodeURIComponent(voiceId)}`, undefined, voiceEditForm(request), options);
134
+ }
135
+ delete(voiceId, options) {
136
+ return this.client.requestJson("DELETE", `/v1/voices/${encodeURIComponent(voiceId)}`, undefined, undefined, options);
137
+ }
138
+ // -- voice settings ---------------------------------------------------
139
+ getSettings(voiceId, options) {
140
+ return this.client.requestJson("GET", `/v1/voices/${encodeURIComponent(voiceId)}/settings`, undefined, undefined, options);
141
+ }
142
+ editSettings(voiceId, settings, options) {
143
+ return this.client.requestJson("PATCH", `/v1/voices/${encodeURIComponent(voiceId)}/settings`, undefined, settings, options);
144
+ }
145
+ // -- two-step preview → save flow -------------------------------------
146
+ /**
147
+ * Generate a clone preview from a single audio sample. Returns a
148
+ * `generatedVoiceId`; call {@link savePreview} after the user accepts
149
+ * the preview to persist it as a real voice.
150
+ */
151
+ async createClonePreview(request, options) {
152
+ return this.client.requestJson("POST", "/v1/voice-previews/clone", undefined, clonePreviewForm(request), options);
153
+ }
154
+ /**
155
+ * Design preview voices from a text prompt (no audio sample). Returns one
156
+ * or more previews; pass the chosen `generatedVoiceId` to
157
+ * {@link savePreview}.
158
+ */
159
+ createDesignPreview(request, options = {}) {
160
+ return this.client.requestJson("POST", "/v1/voice-previews/design", { outputFormat: options.outputFormat }, request, options);
161
+ }
162
+ /** Stream the audio of a clone or design preview before saving. */
163
+ streamPreview(generatedVoiceId, options = {}) {
164
+ return this.client.requestAudio("GET", `/v1/voice-previews/${encodeURIComponent(generatedVoiceId)}/stream`, { outputFormat: options.outputFormat }, undefined, options);
165
+ }
166
+ /** Persist a clone or design preview as a real voice asset. */
167
+ savePreview(request, options) {
168
+ return this.client.requestJson("POST", `/v1/voice-previews/${encodeURIComponent(request.generatedVoiceId)}/save`, undefined, {
169
+ voiceName: request.voiceName,
170
+ voiceDescription: request.voiceDescription,
171
+ labels: request.labels,
172
+ playedNotSelectedVoiceIds: request.playedNotSelectedVoiceIds,
173
+ }, options);
174
+ }
175
+ }
176
+ class HistoryResource {
177
+ client;
178
+ constructor(client) {
179
+ this.client = client;
180
+ }
181
+ list(params = {}, options) {
182
+ return this.client.requestJson("GET", "/v1/history", params, undefined, options);
183
+ }
184
+ get(historyItemId, options) {
185
+ return this.client.requestJson("GET", `/v1/history/${encodeURIComponent(historyItemId)}`, undefined, undefined, options);
186
+ }
187
+ downloadAudio(historyItemId, options) {
188
+ return this.client.requestAudio("GET", `/v1/history/${encodeURIComponent(historyItemId)}/audio`, undefined, undefined, options);
189
+ }
190
+ delete(historyItemId, options) {
191
+ return this.client.requestJson("DELETE", `/v1/history/${encodeURIComponent(historyItemId)}`, undefined, undefined, options);
192
+ }
193
+ }
194
+ class ModelsResource {
195
+ client;
196
+ constructor(client) {
197
+ this.client = client;
198
+ }
199
+ list(options) {
200
+ return this.client.requestJson("GET", "/v1/models", undefined, undefined, options);
201
+ }
202
+ }
203
+ class AccountResource {
204
+ client;
205
+ constructor(client) {
206
+ this.client = client;
207
+ }
208
+ balance(options) {
209
+ return this.client.requestJson("GET", "/v1/balance", undefined, undefined, options);
210
+ }
211
+ usage(params = {}, options) {
212
+ return this.client.requestJson("GET", "/v1/usage", params, undefined, options);
213
+ }
214
+ }
215
+ function normalizeBaseUrl(value) {
216
+ return value.replace(/\/+$/, "");
217
+ }
218
+ function buildAbortSignal(inputSignal, timeoutMs) {
219
+ if (!timeoutMs || timeoutMs <= 0) {
220
+ return { signal: inputSignal, cleanup: () => { } };
221
+ }
222
+ const controller = new AbortController();
223
+ const timeout = setTimeout(() => controller.abort(new Error(`Breeze request timed out after ${timeoutMs}ms.`)), timeoutMs);
224
+ let abortListener;
225
+ if (inputSignal) {
226
+ if (inputSignal.aborted) {
227
+ controller.abort(inputSignal.reason);
228
+ }
229
+ else {
230
+ abortListener = () => controller.abort(inputSignal.reason);
231
+ inputSignal.addEventListener("abort", abortListener, { once: true });
232
+ }
233
+ }
234
+ return {
235
+ signal: controller.signal,
236
+ cleanup: () => {
237
+ clearTimeout(timeout);
238
+ if (abortListener) {
239
+ inputSignal?.removeEventListener("abort", abortListener);
240
+ }
241
+ },
242
+ };
243
+ }
244
+ function buildUrl(baseUrl, path, params) {
245
+ const url = new URL(`${baseUrl}${path}`);
246
+ for (const [key, value] of Object.entries(params ?? {})) {
247
+ if (value === undefined || value === null)
248
+ continue;
249
+ url.searchParams.set(key, String(value));
250
+ }
251
+ return url.toString();
252
+ }
253
+ function bodyToRequestBody(body) {
254
+ if (body === undefined)
255
+ return undefined;
256
+ if (body instanceof FormData)
257
+ return body;
258
+ return JSON.stringify(body);
259
+ }
260
+ function transformRequestBody(body) {
261
+ if (body === undefined)
262
+ return undefined;
263
+ if (body instanceof FormData)
264
+ return body;
265
+ return snakeizeKeys(body);
266
+ }
267
+ function mergeHeaders(headers, input) {
268
+ if (!input)
269
+ return;
270
+ new Headers(input).forEach((value, key) => headers.set(key, value));
271
+ }
272
+ function clonePreviewForm(request) {
273
+ if (request.removeBackgroundNoise) {
274
+ throw new BreezeBlueConfigurationError("removeBackgroundNoise is not supported by the Breeze Developer API.");
275
+ }
276
+ const form = new FormData();
277
+ form.set("name", request.name);
278
+ appendOptional(form, "description", request.description);
279
+ appendOptional(form, "labels", request.labels ? JSON.stringify(request.labels) : undefined);
280
+ appendOptional(form, "text", request.text);
281
+ appendOptional(form, "instructions", request.instructions);
282
+ for (const file of normalizeFiles(request.file, request.files)) {
283
+ appendFile(form, "files", file);
284
+ }
285
+ return form;
286
+ }
287
+ function voiceEditForm(request) {
288
+ const form = new FormData();
289
+ form.set("name", request.name);
290
+ appendOptional(form, "description", request.description);
291
+ appendOptional(form, "labels", request.labels ? JSON.stringify(request.labels) : undefined);
292
+ for (const file of normalizeFiles(request.file, request.files)) {
293
+ appendFile(form, "files", file);
294
+ }
295
+ return form;
296
+ }
297
+ function appendOptional(form, key, value) {
298
+ if (value === undefined)
299
+ return;
300
+ form.set(key, String(value));
301
+ }
302
+ function normalizeFiles(file, files) {
303
+ if (files && files.length > 0)
304
+ return files;
305
+ return file === undefined ? [] : [file];
306
+ }
307
+ function appendFile(form, key, input) {
308
+ const upload = isUploadFile(input) ? input : { data: input };
309
+ const blob = toBlob(upload.data, upload.contentType);
310
+ const filename = upload.filename ?? filenameFromUpload(upload.data);
311
+ form.append(key, blob, filename);
312
+ }
313
+ function isUploadFile(value) {
314
+ return typeof value === "object" && value !== null && "data" in value;
315
+ }
316
+ function toBlob(data, contentType) {
317
+ if (data instanceof Blob)
318
+ return data;
319
+ if (typeof data === "string")
320
+ return new Blob([data], { type: contentType ?? "application/octet-stream" });
321
+ if (data instanceof ArrayBuffer)
322
+ return new Blob([data], { type: contentType ?? "application/octet-stream" });
323
+ const copy = new Uint8Array(data.byteLength);
324
+ copy.set(data);
325
+ return new Blob([copy.buffer], { type: contentType ?? "application/octet-stream" });
326
+ }
327
+ function filenameFromUpload(data) {
328
+ if (data instanceof File && data.name)
329
+ return data.name;
330
+ return "sample.wav";
331
+ }
@@ -0,0 +1,50 @@
1
+ export interface BreezeBlueErrorPayload {
2
+ ok?: false;
3
+ code?: string;
4
+ detail?: string;
5
+ error?: string;
6
+ meta?: Record<string, unknown> | null;
7
+ }
8
+ export declare class BreezeBlueError extends Error {
9
+ constructor(message: string, options?: ErrorOptions);
10
+ }
11
+ export declare class BreezeBlueConfigurationError extends BreezeBlueError {
12
+ }
13
+ export declare class BreezeBlueAPIError extends BreezeBlueError {
14
+ readonly status: number;
15
+ readonly code: string;
16
+ readonly detail: string;
17
+ readonly meta: Record<string, unknown> | null;
18
+ readonly headers: Headers;
19
+ constructor(params: {
20
+ status: number;
21
+ code: string;
22
+ detail: string;
23
+ meta?: Record<string, unknown> | null;
24
+ headers: Headers;
25
+ });
26
+ }
27
+ export declare class BreezeBlueAuthenticationError extends BreezeBlueAPIError {
28
+ }
29
+ export declare class BreezeBlueBadRequestError extends BreezeBlueAPIError {
30
+ }
31
+ export declare class BreezeBlueForbiddenError extends BreezeBlueAPIError {
32
+ }
33
+ export declare class BreezeBlueNotFoundError extends BreezeBlueAPIError {
34
+ }
35
+ export declare class BreezeBlueConflictError extends BreezeBlueAPIError {
36
+ }
37
+ export declare class BreezeBlueValidationError extends BreezeBlueAPIError {
38
+ }
39
+ export declare class BreezeBlueRateLimitError extends BreezeBlueAPIError {
40
+ }
41
+ export declare class BreezeBlueGenerationNotReadyError extends BreezeBlueAPIError {
42
+ get retryAfter(): number | null;
43
+ }
44
+ export declare class BreezeBlueInsufficientCreditsError extends BreezeBlueAPIError {
45
+ }
46
+ export declare class BreezeBlueUpstreamError extends BreezeBlueAPIError {
47
+ }
48
+ export declare class BreezeBlueServiceUnavailableError extends BreezeBlueAPIError {
49
+ }
50
+ export declare function errorFromResponse(response: Response): Promise<BreezeBlueAPIError>;
package/dist/errors.js ADDED
@@ -0,0 +1,151 @@
1
+ export class BreezeBlueError extends Error {
2
+ constructor(message, options) {
3
+ super(message, options);
4
+ this.name = new.target.name;
5
+ }
6
+ }
7
+ export class BreezeBlueConfigurationError extends BreezeBlueError {
8
+ }
9
+ export class BreezeBlueAPIError extends BreezeBlueError {
10
+ status;
11
+ code;
12
+ detail;
13
+ meta;
14
+ headers;
15
+ constructor(params) {
16
+ super(params.detail || params.code);
17
+ this.status = params.status;
18
+ this.code = params.code;
19
+ this.detail = params.detail;
20
+ this.meta = params.meta ?? null;
21
+ this.headers = params.headers;
22
+ }
23
+ }
24
+ export class BreezeBlueAuthenticationError extends BreezeBlueAPIError {
25
+ }
26
+ export class BreezeBlueBadRequestError extends BreezeBlueAPIError {
27
+ }
28
+ export class BreezeBlueForbiddenError extends BreezeBlueAPIError {
29
+ }
30
+ export class BreezeBlueNotFoundError extends BreezeBlueAPIError {
31
+ }
32
+ export class BreezeBlueConflictError extends BreezeBlueAPIError {
33
+ }
34
+ export class BreezeBlueValidationError extends BreezeBlueAPIError {
35
+ }
36
+ export class BreezeBlueRateLimitError extends BreezeBlueAPIError {
37
+ }
38
+ export class BreezeBlueGenerationNotReadyError extends BreezeBlueAPIError {
39
+ get retryAfter() {
40
+ const value = this.headers.get("retry-after");
41
+ if (!value)
42
+ return null;
43
+ const parsed = Number.parseInt(value, 10);
44
+ return Number.isFinite(parsed) ? parsed : null;
45
+ }
46
+ }
47
+ export class BreezeBlueInsufficientCreditsError extends BreezeBlueAPIError {
48
+ }
49
+ export class BreezeBlueUpstreamError extends BreezeBlueAPIError {
50
+ }
51
+ export class BreezeBlueServiceUnavailableError extends BreezeBlueAPIError {
52
+ }
53
+ const codeErrorMap = new Map([
54
+ ["BAD_REQUEST", BreezeBlueBadRequestError],
55
+ ["VALIDATION_ERROR", BreezeBlueValidationError],
56
+ ["AUTH_REQUIRED", BreezeBlueAuthenticationError],
57
+ ["AUTH_INVALID_SESSION", BreezeBlueAuthenticationError],
58
+ ["AUTH_SESSION_EXPIRED", BreezeBlueAuthenticationError],
59
+ ["AUTH_USER_UNAVAILABLE", BreezeBlueAuthenticationError],
60
+ ["AUTH_CONFIG_MISSING", BreezeBlueAuthenticationError],
61
+ ["FORBIDDEN", BreezeBlueForbiddenError],
62
+ ["RESOURCE_NOT_FOUND", BreezeBlueNotFoundError],
63
+ ["CONFLICT", BreezeBlueConflictError],
64
+ ["BILLING_INSUFFICIENT_CREDITS", BreezeBlueInsufficientCreditsError],
65
+ ["RATE_LIMITED", BreezeBlueRateLimitError],
66
+ ["GENERATION_CONCURRENCY_EXCEEDED", BreezeBlueRateLimitError],
67
+ ["GENERATION_NOT_READY", BreezeBlueGenerationNotReadyError],
68
+ ["UPSTREAM_BILLING_ERROR", BreezeBlueUpstreamError],
69
+ ["UPSTREAM_GENERATION_ERROR", BreezeBlueUpstreamError],
70
+ ["UPSTREAM_TIMEOUT", BreezeBlueUpstreamError],
71
+ ["UPSTREAM_GENERATION_SERVICE_ERROR", BreezeBlueUpstreamError],
72
+ ["UPSTREAM_ASSET_STORAGE_ERROR", BreezeBlueUpstreamError],
73
+ ["GENERATION_INVALID_RESPONSE", BreezeBlueUpstreamError],
74
+ ["GENERATION_FAILED", BreezeBlueUpstreamError],
75
+ ["VOICE_DESIGN_FAILED", BreezeBlueUpstreamError],
76
+ ["VOICE_CLONE_FAILED", BreezeBlueUpstreamError],
77
+ ["GENERATION_CAPACITY_EXCEEDED", BreezeBlueServiceUnavailableError],
78
+ ["LLM_CONFIG_NOT_FOUND", BreezeBlueServiceUnavailableError],
79
+ ]);
80
+ const statusErrorMap = new Map([
81
+ [400, BreezeBlueBadRequestError],
82
+ [401, BreezeBlueAuthenticationError],
83
+ [402, BreezeBlueInsufficientCreditsError],
84
+ [403, BreezeBlueForbiddenError],
85
+ [404, BreezeBlueNotFoundError],
86
+ [409, BreezeBlueConflictError],
87
+ [422, BreezeBlueValidationError],
88
+ [425, BreezeBlueGenerationNotReadyError],
89
+ [429, BreezeBlueRateLimitError],
90
+ [502, BreezeBlueUpstreamError],
91
+ [503, BreezeBlueServiceUnavailableError],
92
+ [504, BreezeBlueUpstreamError],
93
+ ]);
94
+ export async function errorFromResponse(response) {
95
+ const payload = await readErrorPayload(response);
96
+ const code = payload.code || fallbackCode(response.status);
97
+ const detail = payload.detail || payload.error || response.statusText || code;
98
+ const params = {
99
+ status: response.status,
100
+ code,
101
+ detail,
102
+ meta: payload.meta ?? null,
103
+ headers: response.headers,
104
+ };
105
+ const errorType = codeErrorMap.get(code) ?? statusErrorMap.get(response.status) ?? BreezeBlueAPIError;
106
+ return new errorType(params);
107
+ }
108
+ async function readErrorPayload(response) {
109
+ const contentType = response.headers.get("content-type") || "";
110
+ if (contentType.includes("application/json")) {
111
+ try {
112
+ const value = await response.json();
113
+ if (value && typeof value === "object") {
114
+ return value;
115
+ }
116
+ }
117
+ catch {
118
+ return {};
119
+ }
120
+ }
121
+ try {
122
+ const text = await response.text();
123
+ return text ? { detail: text, error: text } : {};
124
+ }
125
+ catch {
126
+ return {};
127
+ }
128
+ }
129
+ function fallbackCode(status) {
130
+ if (status === 401)
131
+ return "AUTH_REQUIRED";
132
+ if (status === 400)
133
+ return "BAD_REQUEST";
134
+ if (status === 403)
135
+ return "FORBIDDEN";
136
+ if (status === 404)
137
+ return "RESOURCE_NOT_FOUND";
138
+ if (status === 409)
139
+ return "CONFLICT";
140
+ if (status === 422)
141
+ return "VALIDATION_ERROR";
142
+ if (status === 425)
143
+ return "GENERATION_NOT_READY";
144
+ if (status === 502)
145
+ return "UPSTREAM_GENERATION_ERROR";
146
+ if (status === 503)
147
+ return "SERVICE_UNAVAILABLE";
148
+ if (status === 504)
149
+ return "UPSTREAM_TIMEOUT";
150
+ return "API_ERROR";
151
+ }
@@ -0,0 +1,5 @@
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";
4
+ export { SDK_NAME, SDK_VERSION } from "./version.js";
5
+ export type * from "./types.js";
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
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";
4
+ export { SDK_NAME, SDK_VERSION } from "./version.js";
package/dist/node.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { type AudioResponse, type PlayOptions, type StreamOptions, selectAudioPlaybackCommand, selectStreamPlaybackCommand } from "./audio.js";
2
+ export declare function save(audio: AudioResponse, path: string): Promise<void>;
3
+ export declare function play(audio: AudioResponse, options?: PlayOptions): Promise<void>;
4
+ export declare function stream(audio: AudioResponse, options?: StreamOptions): Promise<void>;
5
+ export { selectAudioPlaybackCommand, selectStreamPlaybackCommand };
package/dist/node.js ADDED
@@ -0,0 +1,74 @@
1
+ import { spawn, spawnSync } from "node:child_process";
2
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { Readable } from "node:stream";
6
+ import { extensionForContentType, selectAudioPlaybackCommand, selectStreamPlaybackCommand, } from "./audio.js";
7
+ export async function save(audio, path) {
8
+ await writeFile(path, Buffer.from(await audio.arrayBuffer()));
9
+ }
10
+ export async function play(audio, options = {}) {
11
+ const command = options.command
12
+ ? { command: options.command, args: options.args ?? [], mode: "stdin" }
13
+ : selectAudioPlaybackCommand({
14
+ availability: options.availability ?? systemCommandAvailability,
15
+ platform: options.platform ?? process.platform,
16
+ });
17
+ if (command.mode === "file") {
18
+ const dir = await mkdtemp(join(tmpdir(), "breeze-blue-audio-"));
19
+ const file = join(dir, audio.filename || `audio${extensionForContentType(audio.contentType)}`);
20
+ await writeFile(file, Buffer.from(await audio.arrayBuffer()));
21
+ try {
22
+ await spawnAndWait(command.command, [...command.args, file]);
23
+ }
24
+ finally {
25
+ await rm(dir, { force: true, recursive: true });
26
+ }
27
+ return;
28
+ }
29
+ await spawnAndPipeBytes(command.command, command.args, await audio.bytes());
30
+ }
31
+ export async function stream(audio, options = {}) {
32
+ const selected = selectStreamPlaybackCommand();
33
+ const command = options.command ?? selected.command;
34
+ const args = options.args ?? selected.args;
35
+ await spawnAndPipeReadable(command, args, audio.response.body);
36
+ }
37
+ export { selectAudioPlaybackCommand, selectStreamPlaybackCommand };
38
+ const systemCommandAvailability = {
39
+ has(command) {
40
+ const result = spawnSync(process.platform === "win32" ? "where" : "command", process.platform === "win32" ? [command] : ["-v", command], {
41
+ stdio: "ignore",
42
+ shell: process.platform !== "win32",
43
+ });
44
+ return result.status === 0;
45
+ },
46
+ };
47
+ async function spawnAndPipeBytes(command, args, bytes) {
48
+ const child = spawn(command, args, { stdio: ["pipe", "ignore", "inherit"] });
49
+ child.stdin.end(Buffer.from(bytes));
50
+ await waitForChild(child);
51
+ }
52
+ async function spawnAndPipeReadable(command, args, body) {
53
+ if (!body) {
54
+ throw new Error("Audio response has no stream body.");
55
+ }
56
+ const child = spawn(command, args, { stdio: ["pipe", "ignore", "inherit"] });
57
+ Readable.fromWeb(body).pipe(child.stdin);
58
+ await waitForChild(child);
59
+ }
60
+ async function spawnAndWait(command, args) {
61
+ await waitForChild(spawn(command, args, { stdio: "inherit" }));
62
+ }
63
+ async function waitForChild(child) {
64
+ await new Promise((resolve, reject) => {
65
+ child.once("error", reject);
66
+ child.once("exit", (code, signal) => {
67
+ if (code === 0) {
68
+ resolve();
69
+ return;
70
+ }
71
+ reject(new Error(`Audio player exited with ${signal || code}.`));
72
+ });
73
+ });
74
+ }