@flamai/finesse 0.0.0-stage → 0.1.1
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 +43 -0
- package/LICENSE +202 -0
- package/README.md +576 -2
- package/dist/index.cjs +1785 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +828 -0
- package/dist/index.d.ts +828 -0
- package/dist/index.js +1722 -0
- package/dist/index.js.map +1 -0
- package/package.json +71 -4
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1785 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
APIConnectionError: () => APIConnectionError,
|
|
34
|
+
APIStatusError: () => APIStatusError,
|
|
35
|
+
APITimeoutError: () => APITimeoutError,
|
|
36
|
+
AuthenticationError: () => AuthenticationError,
|
|
37
|
+
BadRequestError: () => BadRequestError,
|
|
38
|
+
CloneFailedError: () => CloneFailedError,
|
|
39
|
+
CloneTimeoutError: () => CloneTimeoutError,
|
|
40
|
+
ConflictError: () => ConflictError,
|
|
41
|
+
Finesse: () => Finesse,
|
|
42
|
+
FinesseError: () => FinesseError,
|
|
43
|
+
InternalServerError: () => InternalServerError,
|
|
44
|
+
NotFoundError: () => NotFoundError,
|
|
45
|
+
PayloadTooLargeError: () => PayloadTooLargeError,
|
|
46
|
+
PaymentRequiredError: () => PaymentRequiredError,
|
|
47
|
+
PermissionDeniedError: () => PermissionDeniedError,
|
|
48
|
+
RateLimitError: () => RateLimitError,
|
|
49
|
+
RealtimeContext: () => RealtimeContext,
|
|
50
|
+
RealtimeError: () => RealtimeError,
|
|
51
|
+
RealtimeSession: () => RealtimeSession,
|
|
52
|
+
SpeechStream: () => SpeechStream,
|
|
53
|
+
StreamError: () => StreamError,
|
|
54
|
+
UnprocessableEntityError: () => UnprocessableEntityError,
|
|
55
|
+
VERSION: () => VERSION,
|
|
56
|
+
VoicePage: () => VoicePage,
|
|
57
|
+
VoicePagePromise: () => VoicePagePromise,
|
|
58
|
+
fileFromPath: () => fileFromPath,
|
|
59
|
+
isReady: () => isReady
|
|
60
|
+
});
|
|
61
|
+
module.exports = __toCommonJS(index_exports);
|
|
62
|
+
|
|
63
|
+
// src/errors.ts
|
|
64
|
+
var FinesseError = class extends Error {
|
|
65
|
+
constructor(message, options) {
|
|
66
|
+
super(message, options);
|
|
67
|
+
this.name = new.target.name;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
var APIConnectionError = class extends FinesseError {
|
|
71
|
+
};
|
|
72
|
+
var APITimeoutError = class extends APIConnectionError {
|
|
73
|
+
constructor(options) {
|
|
74
|
+
super("Request timed out.", options);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
var APIStatusError = class extends FinesseError {
|
|
78
|
+
/** HTTP status. */
|
|
79
|
+
status;
|
|
80
|
+
/** Machine-readable code. Branch on this, never on the message. Envelope codes:
|
|
81
|
+
* `invalid_request`, `unauthorized`, `key_expired`, `token_invalid`, `payment_required`,
|
|
82
|
+
* `forbidden`, `insufficient_scope`, `voice_not_allowed`, `not_found`, `conflict`,
|
|
83
|
+
* `idempotency_in_progress`, `payload_too_large`, `invalid_input`, `idempotency_key_reused`,
|
|
84
|
+
* `rate_limited`, `concurrency_limited`, `internal_error`, `upstream_error`, `upstream_busy`,
|
|
85
|
+
* `unavailable`; plan denials: `insufficient_credits`, `quota_exhausted`, `feature_disabled`,
|
|
86
|
+
* `plan_limit_reached`; realtime: `too_many_sessions`. `null` when absent. */
|
|
87
|
+
code;
|
|
88
|
+
/** The decoded JSON body, or the raw text when it is not JSON. */
|
|
89
|
+
body;
|
|
90
|
+
headers;
|
|
91
|
+
/** `X-Billing-Remaining` as a number, when sent. */
|
|
92
|
+
billingRemaining;
|
|
93
|
+
/** The service's `X-Request-Id` for the failed request; quote it to support. */
|
|
94
|
+
requestId;
|
|
95
|
+
constructor(status, message, body2, code, headers) {
|
|
96
|
+
super(code && code !== message ? `${status}: ${message} (${code})` : `${status}: ${message}`);
|
|
97
|
+
this.status = status;
|
|
98
|
+
this.code = code;
|
|
99
|
+
this.body = body2;
|
|
100
|
+
this.headers = headers;
|
|
101
|
+
this.billingRemaining = intOrNull(headers.get("x-billing-remaining"));
|
|
102
|
+
const fromBody = body2?.request_id;
|
|
103
|
+
this.requestId = headers.get("x-request-id") ?? (typeof fromBody === "string" ? fromBody : null);
|
|
104
|
+
}
|
|
105
|
+
/** Seconds the server asked you to wait (`Retry-After`), when it sent one. */
|
|
106
|
+
get retryAfter() {
|
|
107
|
+
const v = this.headers.get("retry-after");
|
|
108
|
+
if (v === null) return null;
|
|
109
|
+
const n = Number(v);
|
|
110
|
+
return Number.isFinite(n) ? n : null;
|
|
111
|
+
}
|
|
112
|
+
/** Plan limit from an entitlement or plan-limit denial, when present. */
|
|
113
|
+
get limit() {
|
|
114
|
+
return numberField(this.body, "limit");
|
|
115
|
+
}
|
|
116
|
+
/** Amount already used, from an entitlement denial, when present. */
|
|
117
|
+
get used() {
|
|
118
|
+
return numberField(this.body, "used");
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
var BadRequestError = class extends APIStatusError {
|
|
122
|
+
};
|
|
123
|
+
var AuthenticationError = class extends APIStatusError {
|
|
124
|
+
};
|
|
125
|
+
var PaymentRequiredError = class extends APIStatusError {
|
|
126
|
+
};
|
|
127
|
+
var PermissionDeniedError = class extends APIStatusError {
|
|
128
|
+
/** The capped feature for `plan_limit_reached`, e.g. `max_voices`. */
|
|
129
|
+
get feature() {
|
|
130
|
+
const b = this.body;
|
|
131
|
+
return b && typeof b === "object" && typeof b.feature === "string" ? b.feature : null;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
var NotFoundError = class extends APIStatusError {
|
|
135
|
+
};
|
|
136
|
+
var ConflictError = class extends APIStatusError {
|
|
137
|
+
};
|
|
138
|
+
var PayloadTooLargeError = class extends APIStatusError {
|
|
139
|
+
};
|
|
140
|
+
var UnprocessableEntityError = class extends APIStatusError {
|
|
141
|
+
};
|
|
142
|
+
var RateLimitError = class extends APIStatusError {
|
|
143
|
+
};
|
|
144
|
+
var InternalServerError = class extends APIStatusError {
|
|
145
|
+
};
|
|
146
|
+
var StreamError = class extends FinesseError {
|
|
147
|
+
code;
|
|
148
|
+
constructor(code, message = "") {
|
|
149
|
+
super(message ? `${message} (${code})` : code);
|
|
150
|
+
this.code = code;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
var CloneFailedError = class extends FinesseError {
|
|
154
|
+
job;
|
|
155
|
+
constructor(job) {
|
|
156
|
+
const reason = typeof job.error?.message === "string" ? `: ${job.error.message}` : "";
|
|
157
|
+
super(`Clone job ${job.id} ${job.status}${reason}`);
|
|
158
|
+
this.job = job;
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
var CloneTimeoutError = class extends FinesseError {
|
|
162
|
+
constructor(jobId, timeoutMs) {
|
|
163
|
+
super(`Clone job ${jobId} did not finish within ${timeoutMs / 1e3}s`);
|
|
164
|
+
this.jobId = jobId;
|
|
165
|
+
this.timeoutMs = timeoutMs;
|
|
166
|
+
}
|
|
167
|
+
jobId;
|
|
168
|
+
timeoutMs;
|
|
169
|
+
};
|
|
170
|
+
var MAX_MESSAGE = 300;
|
|
171
|
+
var STATUS_ERRORS = {
|
|
172
|
+
400: BadRequestError,
|
|
173
|
+
401: AuthenticationError,
|
|
174
|
+
402: PaymentRequiredError,
|
|
175
|
+
403: PermissionDeniedError,
|
|
176
|
+
404: NotFoundError,
|
|
177
|
+
409: ConflictError,
|
|
178
|
+
413: PayloadTooLargeError,
|
|
179
|
+
422: UnprocessableEntityError,
|
|
180
|
+
429: RateLimitError
|
|
181
|
+
};
|
|
182
|
+
function makeStatusError(status, text, headers, statusText = "") {
|
|
183
|
+
let body2 = text;
|
|
184
|
+
try {
|
|
185
|
+
body2 = text ? JSON.parse(text) : text;
|
|
186
|
+
} catch {
|
|
187
|
+
}
|
|
188
|
+
let message = null;
|
|
189
|
+
let code = null;
|
|
190
|
+
if (body2 && typeof body2 === "object" && !Array.isArray(body2)) {
|
|
191
|
+
const b = body2;
|
|
192
|
+
if (typeof b.message === "string" && b.message) {
|
|
193
|
+
message = b.message;
|
|
194
|
+
if (/^[a-z]+(_[a-z]+)+$/.test(b.message)) code = b.message;
|
|
195
|
+
}
|
|
196
|
+
if (typeof b.code === "string" && b.code) {
|
|
197
|
+
if (!(status === 402 && b.message === "insufficient_credits")) code = b.code;
|
|
198
|
+
} else if (typeof b.error === "string" && b.error) {
|
|
199
|
+
code = b.error === "plan limit reached" ? "plan_limit_reached" : b.error;
|
|
200
|
+
message ??= b.error.replace(/_/g, " ");
|
|
201
|
+
} else if (b.error && typeof b.error === "object") {
|
|
202
|
+
const e = b.error;
|
|
203
|
+
if (typeof e.message === "string" && e.message) message = e.message;
|
|
204
|
+
if (typeof e.code === "string" && e.code) code = e.code;
|
|
205
|
+
}
|
|
206
|
+
} else if (typeof body2 === "string" && body2.trim()) {
|
|
207
|
+
const flat = body2.split(/\s+/).join(" ").trim();
|
|
208
|
+
message = flat.length <= MAX_MESSAGE ? flat : flat.slice(0, MAX_MESSAGE - 3) + "...";
|
|
209
|
+
}
|
|
210
|
+
message ||= statusText || `HTTP ${status}`;
|
|
211
|
+
const Cls = STATUS_ERRORS[status] ?? (status >= 500 ? InternalServerError : APIStatusError);
|
|
212
|
+
return new Cls(status, message, body2, code, headers);
|
|
213
|
+
}
|
|
214
|
+
function intOrNull(value) {
|
|
215
|
+
if (value === null) return null;
|
|
216
|
+
const n = Number(value);
|
|
217
|
+
return Number.isInteger(n) ? n : null;
|
|
218
|
+
}
|
|
219
|
+
function numberField(body2, key) {
|
|
220
|
+
if (body2 && typeof body2 === "object") {
|
|
221
|
+
const v = body2[key];
|
|
222
|
+
if (typeof v === "number") return v;
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// src/version.ts
|
|
228
|
+
var VERSION = "0.1.1";
|
|
229
|
+
|
|
230
|
+
// src/core.ts
|
|
231
|
+
var DEFAULT_BASE_URL = "https://fi.production.flamapis.com";
|
|
232
|
+
var API_PREFIX = "/flam-voice-svc/api/v1/public";
|
|
233
|
+
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
234
|
+
var DEFAULT_MAX_RETRIES = 2;
|
|
235
|
+
var CLIENT_NAME = `flam-finesse-node/${VERSION}`;
|
|
236
|
+
var RETRY_STATUSES = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
|
|
237
|
+
var POST_RETRY_STATUSES = /* @__PURE__ */ new Set([429, 502, 503]);
|
|
238
|
+
var TOKEN_RETRY_STATUSES = /* @__PURE__ */ new Set([429]);
|
|
239
|
+
var UNSENT_CODES = /* @__PURE__ */ new Set(["ECONNREFUSED", "ENOTFOUND", "EAI_AGAIN", "UND_ERR_CONNECT_TIMEOUT"]);
|
|
240
|
+
var MAX_BACKOFF_MS = 8e3;
|
|
241
|
+
function env(name) {
|
|
242
|
+
const proc = globalThis.process;
|
|
243
|
+
return proc?.env?.[name];
|
|
244
|
+
}
|
|
245
|
+
function isBrowser() {
|
|
246
|
+
return typeof globalThis.window !== "undefined" && typeof globalThis.document !== "undefined";
|
|
247
|
+
}
|
|
248
|
+
var Core = class {
|
|
249
|
+
apiKey;
|
|
250
|
+
accessToken;
|
|
251
|
+
baseURL;
|
|
252
|
+
timeout;
|
|
253
|
+
maxRetries;
|
|
254
|
+
headers;
|
|
255
|
+
fetch;
|
|
256
|
+
logLevel;
|
|
257
|
+
logger;
|
|
258
|
+
/** @internal What this client learned about the server; shared by `withOptions` copies. */
|
|
259
|
+
server = { honoursIdempotencyKeys: false };
|
|
260
|
+
/** @internal Called with each successful response (used by `withRawResponse`). */
|
|
261
|
+
onResponse = null;
|
|
262
|
+
constructor(options = {}) {
|
|
263
|
+
if (options.apiKey && options.accessToken) {
|
|
264
|
+
throw new FinesseError("Pass { apiKey } or { accessToken }, not both.");
|
|
265
|
+
}
|
|
266
|
+
const accessToken = options.accessToken || null;
|
|
267
|
+
const apiKey = accessToken ? null : options.apiKey ?? env("FINESSE_API_KEY") ?? null;
|
|
268
|
+
if (!accessToken && !apiKey) {
|
|
269
|
+
throw new FinesseError(
|
|
270
|
+
"No API key. Pass { apiKey } or set the FINESSE_API_KEY environment variable (or pass { accessToken } for a short-lived token)."
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
if (apiKey && isBrowser() && !options.dangerouslyAllowBrowser) {
|
|
274
|
+
throw new FinesseError(
|
|
275
|
+
"Running in a browser would expose your API key. Mint a short-lived token on your server (tokens.create) and pass { accessToken }, or pass { dangerouslyAllowBrowser: true } for internal tools."
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
this.apiKey = apiKey;
|
|
279
|
+
this.accessToken = accessToken;
|
|
280
|
+
this.baseURL = (options.baseURL || env("FINESSE_BASE_URL") || DEFAULT_BASE_URL).replace(
|
|
281
|
+
/\/+$/,
|
|
282
|
+
""
|
|
283
|
+
);
|
|
284
|
+
this.timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
285
|
+
this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
286
|
+
this.headers = {
|
|
287
|
+
...accessToken ? { Authorization: `Bearer ${accessToken}` } : { "X-API-Key": apiKey },
|
|
288
|
+
"X-Finesse-Client": CLIENT_NAME,
|
|
289
|
+
Accept: "application/json",
|
|
290
|
+
...isBrowser() ? {} : { "User-Agent": CLIENT_NAME },
|
|
291
|
+
...options.defaultHeaders
|
|
292
|
+
};
|
|
293
|
+
const f = options.fetch ?? globalThis.fetch;
|
|
294
|
+
if (!f)
|
|
295
|
+
throw new FinesseError("No fetch available. Pass { fetch } (Node 18+ has one built in).");
|
|
296
|
+
this.fetch = f.bind(globalThis);
|
|
297
|
+
const level = options.logLevel ?? env("FINESSE_LOG")?.toLowerCase() ?? "off";
|
|
298
|
+
this.logLevel = level === "debug" || level === "info" ? level : "off";
|
|
299
|
+
this.logger = options.logger ?? console;
|
|
300
|
+
}
|
|
301
|
+
/** @internal A view of this core with other defaults, sharing everything else. */
|
|
302
|
+
withOptions(overrides) {
|
|
303
|
+
const view = Object.create(Object.getPrototypeOf(this));
|
|
304
|
+
Object.assign(view, this, {
|
|
305
|
+
timeout: overrides.timeout ?? this.timeout,
|
|
306
|
+
maxRetries: overrides.maxRetries ?? this.maxRetries,
|
|
307
|
+
headers: { ...this.headers, ...overrides.headers }
|
|
308
|
+
});
|
|
309
|
+
return view;
|
|
310
|
+
}
|
|
311
|
+
debug(message) {
|
|
312
|
+
if (this.logLevel === "debug") this.logger.debug(`[finesse] ${message}`);
|
|
313
|
+
}
|
|
314
|
+
info(message) {
|
|
315
|
+
if (this.logLevel !== "off") this.logger.info(`[finesse] ${message}`);
|
|
316
|
+
}
|
|
317
|
+
url(path, query) {
|
|
318
|
+
const url = new URL(`${this.baseURL}${API_PREFIX}${path}`);
|
|
319
|
+
for (const [k, v] of Object.entries(query ?? {})) url.searchParams.set(k, String(v));
|
|
320
|
+
return url.toString();
|
|
321
|
+
}
|
|
322
|
+
get wsBaseURL() {
|
|
323
|
+
return this.baseURL.replace(/^http(s?):\/\//, "ws$1://");
|
|
324
|
+
}
|
|
325
|
+
/** Send a request, retrying transient failures, and throw on an error status. With
|
|
326
|
+
* `stream: true` the caller owns the response body. A caller `signal` stops the request and
|
|
327
|
+
* any further retries, and rejects with the signal's reason. */
|
|
328
|
+
async request(method, path, opts = {}) {
|
|
329
|
+
const { signal } = opts;
|
|
330
|
+
const timeout = opts.timeout ?? this.timeout;
|
|
331
|
+
const maxRetries = opts.maxRetries ?? this.maxRetries;
|
|
332
|
+
const baseHeaders = { ...this.headers, ...opts.headers };
|
|
333
|
+
let key = null;
|
|
334
|
+
if (opts.idempotent && method === "POST") {
|
|
335
|
+
const own = Object.keys(baseHeaders).find((k) => k.toLowerCase() === "idempotency-key");
|
|
336
|
+
key = own ? baseHeaders[own] : randomUUID();
|
|
337
|
+
if (!own) baseHeaders["Idempotency-Key"] = key;
|
|
338
|
+
}
|
|
339
|
+
const retryAs = (response) => {
|
|
340
|
+
if (key === null) return method;
|
|
341
|
+
if (response?.headers.get("idempotency-key") === key)
|
|
342
|
+
this.server.honoursIdempotencyKeys = true;
|
|
343
|
+
return this.server.honoursIdempotencyKeys ? "GET" : method;
|
|
344
|
+
};
|
|
345
|
+
let inProgressUntil = null;
|
|
346
|
+
for (let attempt = 0; ; attempt++) {
|
|
347
|
+
throwIfAborted(signal);
|
|
348
|
+
const headers = { ...baseHeaders };
|
|
349
|
+
let body2;
|
|
350
|
+
if (opts.json !== void 0) {
|
|
351
|
+
headers["Content-Type"] = "application/json";
|
|
352
|
+
body2 = JSON.stringify(opts.json);
|
|
353
|
+
} else if (opts.form) {
|
|
354
|
+
body2 = opts.form();
|
|
355
|
+
}
|
|
356
|
+
const controller = new AbortController();
|
|
357
|
+
const timer = setTimeout(() => controller.abort(new TimeoutSignal()), timeout);
|
|
358
|
+
const onAbort = () => controller.abort(signal?.reason);
|
|
359
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
360
|
+
const release = () => {
|
|
361
|
+
clearTimeout(timer);
|
|
362
|
+
signal?.removeEventListener("abort", onAbort);
|
|
363
|
+
};
|
|
364
|
+
const retryAfterError = async (err, sent) => {
|
|
365
|
+
throwIfAborted(signal);
|
|
366
|
+
const timedOut = controller.signal.reason instanceof TimeoutSignal;
|
|
367
|
+
const cautious = this.accessToken !== null || retryAs(null) === "POST";
|
|
368
|
+
const retry = !cautious || !sent && !timedOut && isUnsent(err);
|
|
369
|
+
if (attempt < maxRetries && retry) {
|
|
370
|
+
await sleep(retryDelay(attempt, null), signal);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
if (timedOut) throw new APITimeoutError({ cause: err });
|
|
374
|
+
throw new APIConnectionError(errorMessage(err), { cause: err });
|
|
375
|
+
};
|
|
376
|
+
const url = this.url(path, opts.query);
|
|
377
|
+
const where = `${method} ${url.split("?")[0]}`;
|
|
378
|
+
const started = Date.now();
|
|
379
|
+
let response;
|
|
380
|
+
try {
|
|
381
|
+
response = await this.fetch(url, {
|
|
382
|
+
method,
|
|
383
|
+
headers,
|
|
384
|
+
body: body2,
|
|
385
|
+
signal: controller.signal
|
|
386
|
+
});
|
|
387
|
+
} catch (err) {
|
|
388
|
+
release();
|
|
389
|
+
await retryAfterError(err, false);
|
|
390
|
+
this.info(`retrying ${where} (attempt ${attempt + 1} failed: ${errorMessage(err)})`);
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
retryAs(response);
|
|
394
|
+
this.debug(
|
|
395
|
+
`${where} -> ${response.status} in ${Date.now() - started} ms` + (attempt ? `, attempt ${attempt + 1}` : "") + ` (request id ${response.headers.get("x-request-id") ?? "-"})`
|
|
396
|
+
);
|
|
397
|
+
if (response.ok && opts.stream) {
|
|
398
|
+
clearTimeout(timer);
|
|
399
|
+
streamCleanups.set(response, () => signal?.removeEventListener("abort", onAbort));
|
|
400
|
+
this.onResponse?.(response);
|
|
401
|
+
return response;
|
|
402
|
+
}
|
|
403
|
+
let buffer;
|
|
404
|
+
try {
|
|
405
|
+
buffer = await response.arrayBuffer();
|
|
406
|
+
} catch (err) {
|
|
407
|
+
release();
|
|
408
|
+
await retryAfterError(err, true);
|
|
409
|
+
this.info(`retrying ${where} (attempt ${attempt + 1} failed: ${errorMessage(err)})`);
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
release();
|
|
413
|
+
if (response.ok) {
|
|
414
|
+
const empty = response.status === 204 || response.status === 205 || response.status === 304;
|
|
415
|
+
const buffered = new Response(empty ? null : buffer, {
|
|
416
|
+
status: response.status,
|
|
417
|
+
headers: response.headers
|
|
418
|
+
});
|
|
419
|
+
this.onResponse?.(buffered);
|
|
420
|
+
return buffered;
|
|
421
|
+
}
|
|
422
|
+
const text = new TextDecoder().decode(buffer);
|
|
423
|
+
if (key !== null && response.status === 409 && isInProgress(text)) {
|
|
424
|
+
inProgressUntil ??= Date.now() + timeout;
|
|
425
|
+
if (Date.now() < inProgressUntil) {
|
|
426
|
+
const delay = retryDelay(0, response);
|
|
427
|
+
this.info(`waiting for ${where}: the first attempt is still running`);
|
|
428
|
+
await sleep(delay, signal);
|
|
429
|
+
attempt--;
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
const statuses = this.accessToken !== null ? TOKEN_RETRY_STATUSES : retryAs(response) === "POST" ? POST_RETRY_STATUSES : RETRY_STATUSES;
|
|
434
|
+
if (attempt < maxRetries && statuses.has(response.status)) {
|
|
435
|
+
const delay = retryDelay(attempt, response);
|
|
436
|
+
this.info(
|
|
437
|
+
`retrying ${where} in ${Math.round(delay)} ms (attempt ${attempt + 1} failed: HTTP ${response.status})`
|
|
438
|
+
);
|
|
439
|
+
await sleep(delay, signal);
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
throw makeStatusError(response.status, text, response.headers, response.statusText);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
/** Request and decode a JSON success body, stripping the `{status, error, data}` envelope. */
|
|
446
|
+
async json(method, path, opts = {}) {
|
|
447
|
+
const response = await this.request(method, path, opts);
|
|
448
|
+
return unwrap(await response.json());
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
var streamCleanups = /* @__PURE__ */ new WeakMap();
|
|
452
|
+
function releaseStream(response) {
|
|
453
|
+
streamCleanups.get(response)?.();
|
|
454
|
+
streamCleanups.delete(response);
|
|
455
|
+
}
|
|
456
|
+
var TimeoutSignal = class extends Error {
|
|
457
|
+
};
|
|
458
|
+
function isInProgress(text) {
|
|
459
|
+
try {
|
|
460
|
+
return JSON.parse(text)?.code === "idempotency_in_progress";
|
|
461
|
+
} catch {
|
|
462
|
+
return false;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
function randomUUID() {
|
|
466
|
+
const c = globalThis.crypto;
|
|
467
|
+
if (c?.randomUUID) return c.randomUUID();
|
|
468
|
+
const b = new Uint8Array(16);
|
|
469
|
+
if (c?.getRandomValues) c.getRandomValues(b);
|
|
470
|
+
else for (let i = 0; i < 16; i++) b[i] = Math.floor(Math.random() * 256);
|
|
471
|
+
b[6] = b[6] & 15 | 64;
|
|
472
|
+
b[8] = b[8] & 63 | 128;
|
|
473
|
+
const h = Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
|
|
474
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
|
|
475
|
+
}
|
|
476
|
+
function unwrap(body2) {
|
|
477
|
+
if (body2 && typeof body2 === "object" && "data" in body2 && body2.error === false) {
|
|
478
|
+
return body2.data;
|
|
479
|
+
}
|
|
480
|
+
return body2;
|
|
481
|
+
}
|
|
482
|
+
function isUnsent(err) {
|
|
483
|
+
let e = err;
|
|
484
|
+
for (let i = 0; i < 4 && e; i++) {
|
|
485
|
+
const code = e.code;
|
|
486
|
+
if (typeof code === "string" && UNSENT_CODES.has(code)) return true;
|
|
487
|
+
e = e.cause;
|
|
488
|
+
}
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
function errorMessage(err) {
|
|
492
|
+
const cause = err?.cause?.message;
|
|
493
|
+
return cause || (err instanceof Error ? err.message : "") || "Connection error.";
|
|
494
|
+
}
|
|
495
|
+
function retryDelay(attempt, response) {
|
|
496
|
+
const retryAfter = response?.headers.get("retry-after");
|
|
497
|
+
if (retryAfter) {
|
|
498
|
+
let ms = Number(retryAfter) * 1e3;
|
|
499
|
+
if (Number.isNaN(ms)) ms = Date.parse(retryAfter) - Date.now();
|
|
500
|
+
if (ms >= 0 && ms <= 6e4) return ms;
|
|
501
|
+
}
|
|
502
|
+
const base = Math.min(500 * 2 ** attempt, MAX_BACKOFF_MS);
|
|
503
|
+
return base * (1 - 0.25 * Math.random());
|
|
504
|
+
}
|
|
505
|
+
var wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
506
|
+
async function sleep(ms, signal) {
|
|
507
|
+
throwIfAborted(signal);
|
|
508
|
+
if (!signal) return wait(ms);
|
|
509
|
+
let onAbort = () => void 0;
|
|
510
|
+
const aborted = new Promise((_, reject) => {
|
|
511
|
+
onAbort = () => reject(abortReason(signal));
|
|
512
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
513
|
+
});
|
|
514
|
+
try {
|
|
515
|
+
await Promise.race([wait(ms), aborted]);
|
|
516
|
+
} finally {
|
|
517
|
+
signal.removeEventListener("abort", onAbort);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
function abortReason(signal) {
|
|
521
|
+
return signal.reason ?? new DOMException("This operation was aborted.", "AbortError");
|
|
522
|
+
}
|
|
523
|
+
function throwIfAborted(signal) {
|
|
524
|
+
if (signal?.aborted) throw abortReason(signal);
|
|
525
|
+
}
|
|
526
|
+
function pathId(value) {
|
|
527
|
+
if (value === "" || value === "." || value === "..") {
|
|
528
|
+
throw new FinesseError(`Invalid id: ${JSON.stringify(value)}`);
|
|
529
|
+
}
|
|
530
|
+
return encodeURIComponent(value);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// src/raw.ts
|
|
534
|
+
function rawClient(core, build) {
|
|
535
|
+
const namespace = (name) => new Proxy(
|
|
536
|
+
{},
|
|
537
|
+
{
|
|
538
|
+
get(_target, method) {
|
|
539
|
+
if (typeof method !== "string") return void 0;
|
|
540
|
+
if (method === "stream") {
|
|
541
|
+
throw new FinesseError(
|
|
542
|
+
"speech.stream has no raw form: read headers from the stream itself (stream.billingRemaining, stream.done) instead."
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
return async (...args) => {
|
|
546
|
+
const seen = [];
|
|
547
|
+
const view = core.withOptions({});
|
|
548
|
+
view.onResponse = (r) => seen.push(r);
|
|
549
|
+
const resource = build(view)[name];
|
|
550
|
+
const fn = resource[method];
|
|
551
|
+
if (typeof fn !== "function")
|
|
552
|
+
throw new FinesseError(`No method ${String(name)}.${method}`);
|
|
553
|
+
const data = await fn.apply(resource, args);
|
|
554
|
+
const response = seen[0];
|
|
555
|
+
if (!response) throw new FinesseError("This call made no HTTP request.");
|
|
556
|
+
return {
|
|
557
|
+
data,
|
|
558
|
+
response,
|
|
559
|
+
status: response.status,
|
|
560
|
+
headers: response.headers,
|
|
561
|
+
requestId: response.headers.get("x-request-id")
|
|
562
|
+
};
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
);
|
|
567
|
+
const names = Object.keys(build(core));
|
|
568
|
+
return Object.fromEntries(names.map((name) => [name, namespace(name)]));
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// src/realtime.ts
|
|
572
|
+
var DEFAULT_OPEN_TIMEOUT_MS = 6e4;
|
|
573
|
+
var SUBPROTOCOL = "finesse.v1";
|
|
574
|
+
var CLOSE_TIMEOUT_MS = 5e3;
|
|
575
|
+
var MAX_FRAME_BYTES = 1 << 20;
|
|
576
|
+
var RETRYABLE_CLOSES = /* @__PURE__ */ new Set([1001, 1006, 1011, 1012, 1013, 4429]);
|
|
577
|
+
var RETRYABLE_CODES = /* @__PURE__ */ new Set([
|
|
578
|
+
"upstream_closed",
|
|
579
|
+
"upstream_error",
|
|
580
|
+
"upstream_busy",
|
|
581
|
+
"upstream_unavailable",
|
|
582
|
+
"connection_lost",
|
|
583
|
+
"service_busy",
|
|
584
|
+
"stream_billing_error"
|
|
585
|
+
]);
|
|
586
|
+
var RealtimeError = class extends FinesseError {
|
|
587
|
+
/** Per context: `voice_not_found`, `voice_not_allowed` (the token is limited to other
|
|
588
|
+
* voices), `invalid_request` (a parameter out of range), a `task_failed` code. Whole session:
|
|
589
|
+
* `insufficient_credits`, `upstream_unavailable` (the engine could not be reached),
|
|
590
|
+
* `upstream_busy` (it is at capacity), `upstream_error`, `upstream_closed`,
|
|
591
|
+
* `connection_lost`, `token_spent`, ... */
|
|
592
|
+
code;
|
|
593
|
+
/** The context it applies to, or `null` for the whole session. */
|
|
594
|
+
contextId;
|
|
595
|
+
/** The WebSocket close code, when the socket closed. */
|
|
596
|
+
closeCode;
|
|
597
|
+
constructor(code, message = "", options = {}) {
|
|
598
|
+
super(message && message !== code ? `${message} (${code})` : code);
|
|
599
|
+
this.code = code;
|
|
600
|
+
this.contextId = options.contextId ?? null;
|
|
601
|
+
this.closeCode = options.closeCode ?? null;
|
|
602
|
+
}
|
|
603
|
+
/** Whether reconnecting and trying again can succeed. */
|
|
604
|
+
get retryable() {
|
|
605
|
+
return RETRYABLE_CODES.has(this.code) || this.closeCode !== null && RETRYABLE_CLOSES.has(this.closeCode);
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
function closeError(code, reason) {
|
|
609
|
+
const c = { closeCode: code };
|
|
610
|
+
if (code === 1e3) return null;
|
|
611
|
+
if (code === 1008) return new RealtimeError(reason || "policy_violation", reason, c);
|
|
612
|
+
if (code === 1009) return new RealtimeError("message_too_big", "A message was too large.", c);
|
|
613
|
+
if (code === 1011) return new RealtimeError(reason || "upstream_closed", reason, c);
|
|
614
|
+
if (code === 1013 && (reason === "upstream_busy" || reason === "upstream_unavailable")) {
|
|
615
|
+
return new RealtimeError(
|
|
616
|
+
reason,
|
|
617
|
+
reason === "upstream_busy" ? "The synthesis service is at capacity." : "The synthesis service could not be reached.",
|
|
618
|
+
c
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
if (code === 4401) {
|
|
622
|
+
return new RealtimeError(
|
|
623
|
+
"service_auth_failed",
|
|
624
|
+
"The service could not start the session (a problem on our side).",
|
|
625
|
+
c
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
if (code === 4429) {
|
|
629
|
+
return new RealtimeError(
|
|
630
|
+
"service_busy",
|
|
631
|
+
"The service is at its connection limit. Try again shortly.",
|
|
632
|
+
c
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
if (code === null || code === 1005 || code === 1006) {
|
|
636
|
+
return new RealtimeError("connection_lost", "The connection dropped.", { closeCode: 1006 });
|
|
637
|
+
}
|
|
638
|
+
if (code === 1001 || code === 1012 || code === 1013) {
|
|
639
|
+
return new RealtimeError(
|
|
640
|
+
"connection_lost",
|
|
641
|
+
"The service closed the connection (restart or overload).",
|
|
642
|
+
c
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
return new RealtimeError(reason || "connection_closed", reason, c);
|
|
646
|
+
}
|
|
647
|
+
var END = /* @__PURE__ */ Symbol("end");
|
|
648
|
+
var KNOWN_EVENTS = /* @__PURE__ */ new Set(["task_started", "sentence_done", "task_done", "task_cancelled"]);
|
|
649
|
+
var RealtimeContext = class {
|
|
650
|
+
constructor(session, options, id) {
|
|
651
|
+
this.session = session;
|
|
652
|
+
this.options = options;
|
|
653
|
+
this.id = id;
|
|
654
|
+
}
|
|
655
|
+
session;
|
|
656
|
+
options;
|
|
657
|
+
id;
|
|
658
|
+
cancelled = false;
|
|
659
|
+
sampleRate = null;
|
|
660
|
+
audioFormat = null;
|
|
661
|
+
/** @internal */
|
|
662
|
+
started = false;
|
|
663
|
+
/** @internal The socket this context runs on. */
|
|
664
|
+
socket = null;
|
|
665
|
+
/** @internal */
|
|
666
|
+
ended = false;
|
|
667
|
+
finished = false;
|
|
668
|
+
items = [];
|
|
669
|
+
waiter = null;
|
|
670
|
+
sending = Promise.resolve();
|
|
671
|
+
/** Send text. Text is billed per character as it is sent. Pass `{ finalize: true }` with the
|
|
672
|
+
* turn's last text (or call {@link finish}). */
|
|
673
|
+
send(text, options = {}) {
|
|
674
|
+
return this.serial(async () => {
|
|
675
|
+
if (this.cancelled) {
|
|
676
|
+
throw new RealtimeError("context_cancelled", "This context was cancelled.", {
|
|
677
|
+
contextId: this.id
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
if (this.finished) {
|
|
681
|
+
throw new RealtimeError("context_finished", "This context was already finalized.", {
|
|
682
|
+
contextId: this.id
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
const finalize = options.finalize ?? false;
|
|
686
|
+
if (!this.started) {
|
|
687
|
+
this.started = true;
|
|
688
|
+
try {
|
|
689
|
+
await this.session._start(this, text, finalize);
|
|
690
|
+
} catch (err) {
|
|
691
|
+
this.started = false;
|
|
692
|
+
throw err;
|
|
693
|
+
}
|
|
694
|
+
} else {
|
|
695
|
+
this.session._sendText({ type: "task_continue", context_id: this.id }, text, finalize);
|
|
696
|
+
}
|
|
697
|
+
if (finalize) this.finished = true;
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
/** No more text for this turn: speak what is buffered, then end the context. */
|
|
701
|
+
finish() {
|
|
702
|
+
return this.serial(async () => {
|
|
703
|
+
if (!this.started) {
|
|
704
|
+
throw new RealtimeError("context_not_started", "Send text before finishing.", {
|
|
705
|
+
contextId: this.id
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
if (!this.finished) {
|
|
709
|
+
this.session._send({ type: "task_continue", context_id: this.id, finalize: true });
|
|
710
|
+
this.finished = true;
|
|
711
|
+
}
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
/** Stop this context now. Audio still in flight for it is dropped; iterating it ends with
|
|
715
|
+
* `task_cancelled`. */
|
|
716
|
+
async cancel() {
|
|
717
|
+
if (this.ended || this.cancelled) return;
|
|
718
|
+
this.cancelled = true;
|
|
719
|
+
this.finished = true;
|
|
720
|
+
await this.serial(async () => {
|
|
721
|
+
if (!this.started) {
|
|
722
|
+
this._end();
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
try {
|
|
726
|
+
this.session._send({ type: "task_cancel", context_id: this.id });
|
|
727
|
+
} catch {
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
async *[Symbol.asyncIterator]() {
|
|
732
|
+
for (; ; ) {
|
|
733
|
+
while (this.items.length === 0) {
|
|
734
|
+
await new Promise((resolve) => this.waiter = resolve);
|
|
735
|
+
}
|
|
736
|
+
const item = this.items.shift();
|
|
737
|
+
if (item === END) return;
|
|
738
|
+
if (item instanceof RealtimeError) throw item;
|
|
739
|
+
yield item;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
/** Iterate only the PCM of this context. */
|
|
743
|
+
async *audio() {
|
|
744
|
+
for await (const event of this) if (event.type === "audio") yield event.pcm;
|
|
745
|
+
}
|
|
746
|
+
/** @internal */
|
|
747
|
+
_put(event) {
|
|
748
|
+
if (event.type === "task_started") {
|
|
749
|
+
this.sampleRate = typeof event.sample_rate === "number" ? event.sample_rate : null;
|
|
750
|
+
this.audioFormat = typeof event.audio_format === "string" ? event.audio_format : null;
|
|
751
|
+
}
|
|
752
|
+
this.push(event);
|
|
753
|
+
if (event.type === "task_done" || event.type === "task_cancelled") this._end();
|
|
754
|
+
}
|
|
755
|
+
/** @internal */
|
|
756
|
+
_fail(error) {
|
|
757
|
+
if (this.ended) return;
|
|
758
|
+
this.push(error);
|
|
759
|
+
this._end();
|
|
760
|
+
}
|
|
761
|
+
/** @internal */
|
|
762
|
+
_end() {
|
|
763
|
+
if (this.ended) return;
|
|
764
|
+
this.ended = true;
|
|
765
|
+
this.push(END);
|
|
766
|
+
this.session._forget(this);
|
|
767
|
+
}
|
|
768
|
+
push(item) {
|
|
769
|
+
this.items.push(item);
|
|
770
|
+
const w = this.waiter;
|
|
771
|
+
this.waiter = null;
|
|
772
|
+
w?.();
|
|
773
|
+
}
|
|
774
|
+
serial(fn) {
|
|
775
|
+
const run = this.sending.then(fn, fn);
|
|
776
|
+
this.sending = run.catch(() => void 0);
|
|
777
|
+
return run;
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
var OPEN = 1;
|
|
781
|
+
function versions() {
|
|
782
|
+
return globalThis.process?.versions ?? {};
|
|
783
|
+
}
|
|
784
|
+
function globalDialer(headers) {
|
|
785
|
+
const G = globalThis.WebSocket;
|
|
786
|
+
if (!G) throw new FinesseError("No WebSocket implementation available.");
|
|
787
|
+
return {
|
|
788
|
+
headers,
|
|
789
|
+
// Bun's WebSocket takes headers as a second-argument option. A token session needs no
|
|
790
|
+
// headers, so it can use the standard protocols argument everywhere.
|
|
791
|
+
open: (url, h, _t, protocols) => protocols ? new G(url, protocols) : h ? new G(url, { headers: h }) : new G(url)
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
function denoHasHeaders(version) {
|
|
795
|
+
const [major = 0, minor = 0] = (version ?? "").split(".").map(Number);
|
|
796
|
+
return major > 2 || major === 2 && minor >= 5;
|
|
797
|
+
}
|
|
798
|
+
async function defaultDialer() {
|
|
799
|
+
if (isBrowser()) return globalDialer(false);
|
|
800
|
+
const v = versions();
|
|
801
|
+
if (v.bun) return globalDialer(true);
|
|
802
|
+
const deno = globalThis.Deno;
|
|
803
|
+
if (deno) return globalDialer(denoHasHeaders(deno.version?.deno));
|
|
804
|
+
if (!v.node) return globalDialer(false);
|
|
805
|
+
let mod;
|
|
806
|
+
try {
|
|
807
|
+
mod = await import("ws");
|
|
808
|
+
} catch (err) {
|
|
809
|
+
const code = err.code;
|
|
810
|
+
if (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") {
|
|
811
|
+
throw new FinesseError("Realtime in Node needs the `ws` package: npm install ws");
|
|
812
|
+
}
|
|
813
|
+
throw err;
|
|
814
|
+
}
|
|
815
|
+
const WS = mod.default ?? mod.WebSocket;
|
|
816
|
+
return {
|
|
817
|
+
headers: true,
|
|
818
|
+
open: (url, h, timeoutMs, protocols) => new WS(url, protocols, { headers: h, handshakeTimeout: timeoutMs })
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
var RealtimeSession = class {
|
|
822
|
+
constructor(core, options = {}) {
|
|
823
|
+
this.core = core;
|
|
824
|
+
this.options = options;
|
|
825
|
+
this.staticToken = options.accessToken ?? core.accessToken;
|
|
826
|
+
}
|
|
827
|
+
core;
|
|
828
|
+
options;
|
|
829
|
+
ws = null;
|
|
830
|
+
opening = null;
|
|
831
|
+
contexts = /* @__PURE__ */ new Map();
|
|
832
|
+
pendingAudio = null;
|
|
833
|
+
fatal = null;
|
|
834
|
+
error = null;
|
|
835
|
+
openedOnce = false;
|
|
836
|
+
closed = false;
|
|
837
|
+
closedWaiters = [];
|
|
838
|
+
staticToken;
|
|
839
|
+
tokenSpent = false;
|
|
840
|
+
get url() {
|
|
841
|
+
return `${this.core.wsBaseURL}${API_PREFIX}/stream`;
|
|
842
|
+
}
|
|
843
|
+
get isOpen() {
|
|
844
|
+
return this.ws !== null && this.ws.readyState === OPEN;
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Open the socket (no-op when open). Throws `AuthenticationError`, `PaymentRequiredError` or
|
|
848
|
+
* `PermissionDeniedError` when the handshake is refused, `RateLimitError` with
|
|
849
|
+
* `code === "too_many_sessions"` when the workspace already has its maximum of sessions,
|
|
850
|
+
* and `APIConnectionError` / `APITimeoutError` when the service can't be reached.
|
|
851
|
+
*/
|
|
852
|
+
open() {
|
|
853
|
+
if (this.isOpen) return Promise.resolve();
|
|
854
|
+
if (this.closed)
|
|
855
|
+
return Promise.reject(new RealtimeError("session_closed", "This session was closed."));
|
|
856
|
+
if (this.openedOnce && this.options.autoReconnect === false) {
|
|
857
|
+
return Promise.reject(
|
|
858
|
+
this.error ?? new RealtimeError("connection_closed", "The session is closed.")
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
this.opening ??= this.dial().finally(() => this.opening = null);
|
|
862
|
+
return this.opening;
|
|
863
|
+
}
|
|
864
|
+
/** Create a context. Nothing is sent until its first `send`. */
|
|
865
|
+
context(options) {
|
|
866
|
+
const { context_id, ...rest } = options;
|
|
867
|
+
const opts = { output_format: "s16le" };
|
|
868
|
+
for (const [k, v] of Object.entries(rest)) if (v !== void 0) opts[k] = v;
|
|
869
|
+
return new RealtimeContext(this, opts, context_id ?? randomId());
|
|
870
|
+
}
|
|
871
|
+
/** Speak `text` as one complete turn and yield its events. Breaking out of the loop cancels
|
|
872
|
+
* the turn. */
|
|
873
|
+
async *say(options) {
|
|
874
|
+
const { text, ...rest } = options;
|
|
875
|
+
const ctx = this.context(rest);
|
|
876
|
+
await ctx.send(text, { finalize: true });
|
|
877
|
+
try {
|
|
878
|
+
yield* ctx;
|
|
879
|
+
} finally {
|
|
880
|
+
if (!ctx.ended) await ctx.cancel();
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
/** End the session: tell the service, then close the socket. */
|
|
884
|
+
async close() {
|
|
885
|
+
this.closed = true;
|
|
886
|
+
await this.opening?.catch(() => void 0);
|
|
887
|
+
const ws = this.ws;
|
|
888
|
+
if (ws && ws.readyState === OPEN) {
|
|
889
|
+
try {
|
|
890
|
+
ws.send(JSON.stringify({ type: "session_end" }));
|
|
891
|
+
} catch {
|
|
892
|
+
}
|
|
893
|
+
await new Promise((resolve) => {
|
|
894
|
+
const timer = setTimeout(resolve, CLOSE_TIMEOUT_MS);
|
|
895
|
+
this.closedWaiters.push(() => {
|
|
896
|
+
clearTimeout(timer);
|
|
897
|
+
resolve();
|
|
898
|
+
});
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
if (this.ws && this.ws.readyState <= OPEN) this.ws.close(1e3);
|
|
902
|
+
}
|
|
903
|
+
// -- internals --
|
|
904
|
+
/** @internal */
|
|
905
|
+
async _start(ctx, text, finalize) {
|
|
906
|
+
await this.open();
|
|
907
|
+
const live = this.contexts.get(ctx.id);
|
|
908
|
+
if (live && live !== ctx) {
|
|
909
|
+
throw new RealtimeError("context_already_exists", "A running context already uses this id.", {
|
|
910
|
+
contextId: ctx.id
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
this.contexts.set(ctx.id, ctx);
|
|
914
|
+
ctx.socket = this.ws;
|
|
915
|
+
try {
|
|
916
|
+
this._sendText({ type: "task_start", context_id: ctx.id, ...ctx.options }, text, finalize);
|
|
917
|
+
} catch (err) {
|
|
918
|
+
this.contexts.delete(ctx.id);
|
|
919
|
+
throw err;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
/** @internal */
|
|
923
|
+
_send(msg) {
|
|
924
|
+
if (!this.ws || !this.isOpen) {
|
|
925
|
+
throw this.error ?? new RealtimeError("connection_closed", "The session is not open.");
|
|
926
|
+
}
|
|
927
|
+
this.ws.send(JSON.stringify(msg));
|
|
928
|
+
}
|
|
929
|
+
/** @internal Send a `task_start` / `task_continue` frame with `text`. Text that would make
|
|
930
|
+
* the frame larger than {@link MAX_FRAME_BYTES} is split across `task_continue` frames for
|
|
931
|
+
* the same context; only the last one carries `finalize`. */
|
|
932
|
+
_sendText(msg, text, finalize) {
|
|
933
|
+
const frame = { ...msg, text, finalize };
|
|
934
|
+
const json = JSON.stringify(frame);
|
|
935
|
+
if (json.length * 3 <= MAX_FRAME_BYTES || utf8Bytes(json) <= MAX_FRAME_BYTES) {
|
|
936
|
+
this._send(frame);
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
const next = { type: "task_continue", context_id: msg.context_id };
|
|
940
|
+
const overhead = Math.max(
|
|
941
|
+
utf8Bytes(JSON.stringify({ ...msg, text: "", finalize: false })),
|
|
942
|
+
utf8Bytes(JSON.stringify({ ...next, text: "", finalize: false }))
|
|
943
|
+
);
|
|
944
|
+
const pieces = splitText(text, MAX_FRAME_BYTES - overhead);
|
|
945
|
+
pieces.forEach(
|
|
946
|
+
(piece, i) => this._send({
|
|
947
|
+
...i ? next : msg,
|
|
948
|
+
text: piece,
|
|
949
|
+
finalize: finalize && i === pieces.length - 1
|
|
950
|
+
})
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
/** @internal */
|
|
954
|
+
_forget(ctx) {
|
|
955
|
+
if (this.contexts.get(ctx.id) === ctx) this.contexts.delete(ctx.id);
|
|
956
|
+
}
|
|
957
|
+
/** The token for the next handshake, or `null` to use the API key. A static token is used
|
|
958
|
+
* first; once a handshake has spent it, `getAccessToken` supplies one per reconnect. */
|
|
959
|
+
async nextToken() {
|
|
960
|
+
if (this.staticToken && !this.tokenSpent) return this.staticToken;
|
|
961
|
+
if (this.options.getAccessToken) {
|
|
962
|
+
const token = await this.options.getAccessToken();
|
|
963
|
+
if (typeof token !== "string" || !token) {
|
|
964
|
+
throw new FinesseError("getAccessToken must return a non-empty token string.");
|
|
965
|
+
}
|
|
966
|
+
return token;
|
|
967
|
+
}
|
|
968
|
+
if (this.staticToken) {
|
|
969
|
+
throw new RealtimeError(
|
|
970
|
+
"token_spent",
|
|
971
|
+
"This session's access token was used by an earlier connection. Pass { getAccessToken } so a reconnect can fetch a fresh one."
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
return null;
|
|
975
|
+
}
|
|
976
|
+
async dial() {
|
|
977
|
+
const timeout = this.options.openTimeout ?? DEFAULT_OPEN_TIMEOUT_MS;
|
|
978
|
+
const custom = this.options.WebSocket;
|
|
979
|
+
const dialer = custom ? {
|
|
980
|
+
headers: !isBrowser(),
|
|
981
|
+
open: (url2, h, t, protocols2) => h ? new custom(url2, protocols2, { headers: h, handshakeTimeout: t }) : new custom(url2, protocols2)
|
|
982
|
+
} : await defaultDialer();
|
|
983
|
+
const token = await this.nextToken();
|
|
984
|
+
const protocols = token ? [SUBPROTOCOL, token] : void 0;
|
|
985
|
+
let url = this.url;
|
|
986
|
+
let headers = null;
|
|
987
|
+
if (token) {
|
|
988
|
+
if (dialer.headers) {
|
|
989
|
+
headers = { ...this.core.headers };
|
|
990
|
+
delete headers.Accept;
|
|
991
|
+
delete headers["X-API-Key"];
|
|
992
|
+
delete headers.Authorization;
|
|
993
|
+
}
|
|
994
|
+
const u = new URL(url);
|
|
995
|
+
u.searchParams.set("client", this.core.headers["X-Finesse-Client"] ?? "");
|
|
996
|
+
url = u.toString();
|
|
997
|
+
} else if (dialer.headers) {
|
|
998
|
+
headers = { ...this.core.headers };
|
|
999
|
+
delete headers.Accept;
|
|
1000
|
+
} else {
|
|
1001
|
+
if (this.core.logLevel !== "off") warnQueryKey(this.core.logger);
|
|
1002
|
+
const u = new URL(url);
|
|
1003
|
+
u.searchParams.set("api_key", this.core.apiKey ?? "");
|
|
1004
|
+
u.searchParams.set("client", this.core.headers["X-Finesse-Client"] ?? "");
|
|
1005
|
+
url = u.toString();
|
|
1006
|
+
}
|
|
1007
|
+
const ws = dialer.open(url, headers, timeout, protocols);
|
|
1008
|
+
ws.binaryType = "arraybuffer";
|
|
1009
|
+
await new Promise((resolve, reject) => {
|
|
1010
|
+
let settled = false;
|
|
1011
|
+
const done = (err) => {
|
|
1012
|
+
if (settled) return;
|
|
1013
|
+
settled = true;
|
|
1014
|
+
clearTimeout(timer);
|
|
1015
|
+
if (err) {
|
|
1016
|
+
ws.onopen = ws.onclose = null;
|
|
1017
|
+
ws.onerror = () => void 0;
|
|
1018
|
+
try {
|
|
1019
|
+
ws.close();
|
|
1020
|
+
} catch {
|
|
1021
|
+
}
|
|
1022
|
+
reject(err);
|
|
1023
|
+
} else resolve();
|
|
1024
|
+
};
|
|
1025
|
+
const timer = setTimeout(() => done(new APITimeoutError()), timeout);
|
|
1026
|
+
ws.on?.("unexpected-response", (_req, res) => {
|
|
1027
|
+
readResponse(res).then(
|
|
1028
|
+
({ status, text, headers: headers2 }) => done(makeStatusError(status, text, headers2)),
|
|
1029
|
+
() => done(new APIConnectionError("The service refused the connection."))
|
|
1030
|
+
);
|
|
1031
|
+
});
|
|
1032
|
+
ws.onopen = () => done();
|
|
1033
|
+
let lastError = "";
|
|
1034
|
+
ws.onerror = (ev) => {
|
|
1035
|
+
const e = ev;
|
|
1036
|
+
lastError = e?.error?.message || e?.message || "";
|
|
1037
|
+
setTimeout(() => done(new APIConnectionError(lastError || "Connection error.")), 0);
|
|
1038
|
+
};
|
|
1039
|
+
ws.onclose = (ev) => done(new APIConnectionError(lastError || `Connection closed (${ev.code}).`));
|
|
1040
|
+
});
|
|
1041
|
+
if (this.closed) {
|
|
1042
|
+
ws.close(1e3);
|
|
1043
|
+
throw new RealtimeError("session_closed", "This session was closed.");
|
|
1044
|
+
}
|
|
1045
|
+
if (token && token === this.staticToken) this.tokenSpent = true;
|
|
1046
|
+
this.ws = ws;
|
|
1047
|
+
this.pendingAudio = null;
|
|
1048
|
+
this.fatal = null;
|
|
1049
|
+
this.error = null;
|
|
1050
|
+
this.openedOnce = true;
|
|
1051
|
+
ws.onerror = () => void 0;
|
|
1052
|
+
ws.onmessage = (ev) => {
|
|
1053
|
+
if (this.ws !== ws) return;
|
|
1054
|
+
try {
|
|
1055
|
+
if (typeof ev.data === "string") this.onMessage(ev.data);
|
|
1056
|
+
else this.onAudio(toBytes(ev.data));
|
|
1057
|
+
} catch {
|
|
1058
|
+
}
|
|
1059
|
+
};
|
|
1060
|
+
ws.onclose = (ev) => this.onClose(ws, ev.code, ev.reason);
|
|
1061
|
+
}
|
|
1062
|
+
onClose(ws, code, reason) {
|
|
1063
|
+
if (this.ws !== null && this.ws !== ws) {
|
|
1064
|
+
const err = closeError(code, reason ?? "") ?? new RealtimeError("connection_closed");
|
|
1065
|
+
for (const ctx of [...this.contexts.values()]) if (ctx.socket === ws) ctx._fail(err);
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
this.ws = null;
|
|
1069
|
+
let error = this.fatal;
|
|
1070
|
+
if (error) error.closeCode ??= code;
|
|
1071
|
+
else error = closeError(code, reason ?? "");
|
|
1072
|
+
if (!error && this.contexts.size) {
|
|
1073
|
+
error = new RealtimeError("connection_closed", "The session ended.", { closeCode: code });
|
|
1074
|
+
}
|
|
1075
|
+
this.error = error;
|
|
1076
|
+
for (const ctx of [...this.contexts.values()]) {
|
|
1077
|
+
ctx._fail(error ?? new RealtimeError("connection_closed"));
|
|
1078
|
+
}
|
|
1079
|
+
for (const w of this.closedWaiters.splice(0)) w();
|
|
1080
|
+
}
|
|
1081
|
+
onAudio(frame) {
|
|
1082
|
+
const header = this.pendingAudio;
|
|
1083
|
+
this.pendingAudio = null;
|
|
1084
|
+
if (!header) return;
|
|
1085
|
+
const ctx = this.contexts.get(String(header.context_id));
|
|
1086
|
+
if (!ctx || ctx.cancelled) return;
|
|
1087
|
+
ctx._put({
|
|
1088
|
+
type: "audio",
|
|
1089
|
+
context_id: ctx.id,
|
|
1090
|
+
pcm: frame,
|
|
1091
|
+
sample_rate: ctx.sampleRate,
|
|
1092
|
+
audio_format: ctx.audioFormat,
|
|
1093
|
+
...numbers(header, ["sentence_index", "chunk_index", "audio_duration_s"])
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
onMessage(frame) {
|
|
1097
|
+
let data;
|
|
1098
|
+
try {
|
|
1099
|
+
data = JSON.parse(frame);
|
|
1100
|
+
} catch {
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) return;
|
|
1104
|
+
const d = data;
|
|
1105
|
+
const kind = d.type;
|
|
1106
|
+
if (kind === "audio_chunk") {
|
|
1107
|
+
this.pendingAudio = d;
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
const cid = typeof d.context_id === "string" ? d.context_id : null;
|
|
1111
|
+
const ctx = cid ? this.contexts.get(cid) : void 0;
|
|
1112
|
+
if (kind === "error") {
|
|
1113
|
+
const err = new RealtimeError(String(d.code || "error"), String(d.message ?? ""), {
|
|
1114
|
+
contextId: cid
|
|
1115
|
+
});
|
|
1116
|
+
if (d.fatal) this.fatal ??= err;
|
|
1117
|
+
else ctx?._fail(err);
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
if (kind === "session_ended") {
|
|
1121
|
+
this.fatal ??= new RealtimeError("session_ended", String(d.reason || "The session ended."));
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
if (!ctx || typeof kind !== "string") return;
|
|
1125
|
+
if (kind === "task_failed") {
|
|
1126
|
+
ctx._fail(
|
|
1127
|
+
new RealtimeError(String(d.code || "task_failed"), String(d.message ?? ""), {
|
|
1128
|
+
contextId: ctx.id
|
|
1129
|
+
})
|
|
1130
|
+
);
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
ctx._put(
|
|
1134
|
+
KNOWN_EVENTS.has(kind) ? { ...d, type: kind, context_id: ctx.id } : { type: "unknown", context_id: ctx.id, event_type: kind, data: d }
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
1137
|
+
};
|
|
1138
|
+
var Realtime = class {
|
|
1139
|
+
constructor(core) {
|
|
1140
|
+
this.core = core;
|
|
1141
|
+
}
|
|
1142
|
+
core;
|
|
1143
|
+
/** Open a session. */
|
|
1144
|
+
async connect(options = {}) {
|
|
1145
|
+
const session = new RealtimeSession(this.core, options);
|
|
1146
|
+
await session.open();
|
|
1147
|
+
return session;
|
|
1148
|
+
}
|
|
1149
|
+
/** Create a session without opening it; it opens on first use. */
|
|
1150
|
+
session(options = {}) {
|
|
1151
|
+
return new RealtimeSession(this.core, options);
|
|
1152
|
+
}
|
|
1153
|
+
};
|
|
1154
|
+
var warnedLoggers = /* @__PURE__ */ new WeakSet();
|
|
1155
|
+
function warnQueryKey(logger) {
|
|
1156
|
+
if (warnedLoggers.has(logger)) return;
|
|
1157
|
+
warnedLoggers.add(logger);
|
|
1158
|
+
logger.info(
|
|
1159
|
+
"[finesse] This runtime cannot send WebSocket headers, so the API key goes in the URL (?api_key=), which the service has deprecated. Mint a short-lived token on your server (tokens.create) and connect with { accessToken } or { getAccessToken } instead."
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
function utf8Bytes(s) {
|
|
1163
|
+
return new TextEncoder().encode(s).length;
|
|
1164
|
+
}
|
|
1165
|
+
function splitText(text, budget) {
|
|
1166
|
+
const pieces = [];
|
|
1167
|
+
let start = 0;
|
|
1168
|
+
let size = 0;
|
|
1169
|
+
for (let i = 0; i < text.length; ) {
|
|
1170
|
+
const c = text.charCodeAt(i);
|
|
1171
|
+
const pair = c >= 55296 && c <= 56319 && (text.charCodeAt(i + 1) & 64512) === 56320 ? 2 : 1;
|
|
1172
|
+
const bytes = pair === 2 ? 4 : escapedBytes(c);
|
|
1173
|
+
if (size + bytes > budget && i > start) {
|
|
1174
|
+
pieces.push(text.slice(start, i));
|
|
1175
|
+
start = i;
|
|
1176
|
+
size = 0;
|
|
1177
|
+
}
|
|
1178
|
+
size += bytes;
|
|
1179
|
+
i += pair;
|
|
1180
|
+
}
|
|
1181
|
+
pieces.push(text.slice(start));
|
|
1182
|
+
return pieces;
|
|
1183
|
+
}
|
|
1184
|
+
function escapedBytes(c) {
|
|
1185
|
+
if (c === 34 || c === 92) return 2;
|
|
1186
|
+
if (c < 32) return c === 8 || c === 9 || c === 10 || c === 12 || c === 13 ? 2 : 6;
|
|
1187
|
+
if (c < 128) return 1;
|
|
1188
|
+
if (c < 2048) return 2;
|
|
1189
|
+
if (c >= 55296 && c <= 57343) return 6;
|
|
1190
|
+
return 3;
|
|
1191
|
+
}
|
|
1192
|
+
function randomId() {
|
|
1193
|
+
const c = globalThis.crypto;
|
|
1194
|
+
if (c?.randomUUID) return c.randomUUID().replace(/-/g, "");
|
|
1195
|
+
const bytes = new Uint8Array(16);
|
|
1196
|
+
if (c?.getRandomValues) c.getRandomValues(bytes);
|
|
1197
|
+
else for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256);
|
|
1198
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
1199
|
+
}
|
|
1200
|
+
function toBytes(data) {
|
|
1201
|
+
if (data instanceof ArrayBuffer) return new Uint8Array(data);
|
|
1202
|
+
if (ArrayBuffer.isView(data))
|
|
1203
|
+
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
1204
|
+
if (Array.isArray(data)) {
|
|
1205
|
+
const parts = data.map(toBytes);
|
|
1206
|
+
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
1207
|
+
let o = 0;
|
|
1208
|
+
for (const p of parts) {
|
|
1209
|
+
out.set(p, o);
|
|
1210
|
+
o += p.length;
|
|
1211
|
+
}
|
|
1212
|
+
return out;
|
|
1213
|
+
}
|
|
1214
|
+
throw new TypeError("unexpected frame");
|
|
1215
|
+
}
|
|
1216
|
+
function numbers(obj, keys) {
|
|
1217
|
+
const out = {};
|
|
1218
|
+
for (const k of keys) if (typeof obj[k] === "number") out[k] = obj[k];
|
|
1219
|
+
return out;
|
|
1220
|
+
}
|
|
1221
|
+
async function readResponse(res) {
|
|
1222
|
+
const r = res;
|
|
1223
|
+
const headers = new Headers();
|
|
1224
|
+
for (const [k, v] of Object.entries(r.headers)) {
|
|
1225
|
+
if (v !== void 0) headers.set(k, Array.isArray(v) ? v.join(", ") : v);
|
|
1226
|
+
}
|
|
1227
|
+
const chunks = [];
|
|
1228
|
+
await new Promise((resolve, reject) => {
|
|
1229
|
+
r.on("data", (c) => chunks.push(c));
|
|
1230
|
+
r.on("end", () => resolve());
|
|
1231
|
+
r.on("error", (e) => reject(e));
|
|
1232
|
+
});
|
|
1233
|
+
const text = new TextDecoder().decode(toBytes(chunks));
|
|
1234
|
+
return { status: r.statusCode, text, headers };
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
// src/resources/jobs.ts
|
|
1238
|
+
var DEFAULT_WAIT_TIMEOUT_MS = 3e5;
|
|
1239
|
+
var DEFAULT_POLL_INTERVAL_MS = 2e3;
|
|
1240
|
+
var FINISHED = /* @__PURE__ */ new Set(["succeeded", "failed", "cancelled", "expired"]);
|
|
1241
|
+
var Jobs = class {
|
|
1242
|
+
constructor(core) {
|
|
1243
|
+
this.core = core;
|
|
1244
|
+
}
|
|
1245
|
+
core;
|
|
1246
|
+
get(jobId, options = {}) {
|
|
1247
|
+
return this.core.json("GET", `/jobs/${pathId(jobId)}`, options);
|
|
1248
|
+
}
|
|
1249
|
+
/** Poll until the job finishes and return it. Throws {@link CloneFailedError} if it ends
|
|
1250
|
+
* `failed`, `cancelled` or `expired`, and {@link CloneTimeoutError} on timeout. */
|
|
1251
|
+
async wait(jobId, options = {}) {
|
|
1252
|
+
const timeout = options.timeout ?? DEFAULT_WAIT_TIMEOUT_MS;
|
|
1253
|
+
const interval = options.pollInterval ?? DEFAULT_POLL_INTERVAL_MS;
|
|
1254
|
+
const deadline = Date.now() + timeout;
|
|
1255
|
+
for (; ; ) {
|
|
1256
|
+
const job = await this.get(jobId, { signal: options.signal });
|
|
1257
|
+
options.onUpdate?.(job);
|
|
1258
|
+
if (FINISHED.has(job.status)) {
|
|
1259
|
+
if (job.status !== "succeeded") throw new CloneFailedError(job);
|
|
1260
|
+
return job;
|
|
1261
|
+
}
|
|
1262
|
+
const remaining = deadline - Date.now();
|
|
1263
|
+
if (remaining <= 0) throw new CloneTimeoutError(jobId, timeout);
|
|
1264
|
+
await sleep(Math.min(interval, remaining), options.signal);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
};
|
|
1268
|
+
|
|
1269
|
+
// src/sse.ts
|
|
1270
|
+
async function* iterSSE(body2, idleTimeoutMs) {
|
|
1271
|
+
const decoder = new TextDecoder();
|
|
1272
|
+
const reader = body2.getReader();
|
|
1273
|
+
let buffer = "";
|
|
1274
|
+
let event = null;
|
|
1275
|
+
let data = [];
|
|
1276
|
+
function* feed(line) {
|
|
1277
|
+
if (line.endsWith("\r")) line = line.slice(0, -1);
|
|
1278
|
+
if (line === "") {
|
|
1279
|
+
if (data.length || event !== null) yield { event: event ?? "message", data: data.join("\n") };
|
|
1280
|
+
event = null;
|
|
1281
|
+
data = [];
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
if (line.startsWith(":")) return;
|
|
1285
|
+
const i = line.indexOf(":");
|
|
1286
|
+
const field = i === -1 ? line : line.slice(0, i);
|
|
1287
|
+
let value = i === -1 ? "" : line.slice(i + 1);
|
|
1288
|
+
if (value.startsWith(" ")) value = value.slice(1);
|
|
1289
|
+
if (field === "event") event = value;
|
|
1290
|
+
else if (field === "data") data.push(value);
|
|
1291
|
+
}
|
|
1292
|
+
try {
|
|
1293
|
+
for (; ; ) {
|
|
1294
|
+
let timer;
|
|
1295
|
+
const idle = new Promise((_, reject) => {
|
|
1296
|
+
timer = setTimeout(() => reject(new APITimeoutError()), idleTimeoutMs);
|
|
1297
|
+
});
|
|
1298
|
+
let result;
|
|
1299
|
+
try {
|
|
1300
|
+
result = await Promise.race([reader.read(), idle]);
|
|
1301
|
+
} finally {
|
|
1302
|
+
clearTimeout(timer);
|
|
1303
|
+
}
|
|
1304
|
+
const { done, value } = result;
|
|
1305
|
+
if (done) break;
|
|
1306
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1307
|
+
let nl;
|
|
1308
|
+
while ((nl = buffer.indexOf("\n")) !== -1) {
|
|
1309
|
+
const line = buffer.slice(0, nl);
|
|
1310
|
+
buffer = buffer.slice(nl + 1);
|
|
1311
|
+
yield* feed(line);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
buffer += decoder.decode();
|
|
1315
|
+
if (buffer) yield* feed(buffer);
|
|
1316
|
+
yield* feed("");
|
|
1317
|
+
} finally {
|
|
1318
|
+
await reader.cancel().catch(() => void 0);
|
|
1319
|
+
reader.releaseLock();
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
// src/resources/speech.ts
|
|
1324
|
+
function body(params) {
|
|
1325
|
+
const { voice_id: _ignored, ...rest } = params;
|
|
1326
|
+
return Object.fromEntries(Object.entries(rest).filter(([, v]) => v !== void 0));
|
|
1327
|
+
}
|
|
1328
|
+
function intHeader(headers, name) {
|
|
1329
|
+
const v = headers.get(name);
|
|
1330
|
+
if (v === null) return null;
|
|
1331
|
+
const n = Number(v);
|
|
1332
|
+
return Number.isInteger(n) ? n : null;
|
|
1333
|
+
}
|
|
1334
|
+
function base64ToBytes(b64) {
|
|
1335
|
+
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(b64) || b64.length % 4 === 1) {
|
|
1336
|
+
throw new StreamError("bad_chunk", "The stream sent an undecodable audio chunk.");
|
|
1337
|
+
}
|
|
1338
|
+
const bin = atob(b64);
|
|
1339
|
+
const out = new Uint8Array(bin.length);
|
|
1340
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
1341
|
+
return out;
|
|
1342
|
+
}
|
|
1343
|
+
var SpeechStream = class {
|
|
1344
|
+
constructor(core, path, json, options = {}) {
|
|
1345
|
+
this.core = core;
|
|
1346
|
+
this.path = path;
|
|
1347
|
+
this.json = json;
|
|
1348
|
+
this.options = options;
|
|
1349
|
+
}
|
|
1350
|
+
core;
|
|
1351
|
+
path;
|
|
1352
|
+
json;
|
|
1353
|
+
options;
|
|
1354
|
+
sampleRate = null;
|
|
1355
|
+
audioFormat = null;
|
|
1356
|
+
/** Final event (duration, generation id, stored clip URL), once the stream finished. */
|
|
1357
|
+
done = null;
|
|
1358
|
+
billingRemaining = null;
|
|
1359
|
+
consumed = false;
|
|
1360
|
+
async *[Symbol.asyncIterator]() {
|
|
1361
|
+
if (this.consumed) throw new FinesseError("This stream was already consumed; start a new one.");
|
|
1362
|
+
this.consumed = true;
|
|
1363
|
+
const response = await this.core.request("POST", this.path, {
|
|
1364
|
+
...this.options,
|
|
1365
|
+
json: this.json,
|
|
1366
|
+
stream: true
|
|
1367
|
+
});
|
|
1368
|
+
this.billingRemaining = intHeader(response.headers, "x-billing-remaining");
|
|
1369
|
+
try {
|
|
1370
|
+
if (!response.body) throw new StreamError("stream_incomplete", "The response had no body.");
|
|
1371
|
+
const idle = this.options.timeout ?? this.core.timeout;
|
|
1372
|
+
for await (const ev of iterSSE(response.body, idle)) {
|
|
1373
|
+
const chunk = this.handle(ev.event, ev.data);
|
|
1374
|
+
if (chunk) yield chunk;
|
|
1375
|
+
}
|
|
1376
|
+
if (!this.done) {
|
|
1377
|
+
throw new StreamError("stream_incomplete", "The stream ended before it finished.");
|
|
1378
|
+
}
|
|
1379
|
+
} catch (err) {
|
|
1380
|
+
throwIfAborted(this.options.signal);
|
|
1381
|
+
if (err instanceof FinesseError) throw err;
|
|
1382
|
+
throw new APIConnectionError(
|
|
1383
|
+
err instanceof Error ? err.message : "The stream was interrupted.",
|
|
1384
|
+
{ cause: err }
|
|
1385
|
+
);
|
|
1386
|
+
} finally {
|
|
1387
|
+
releaseStream(response);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
/** Consume the stream and return all PCM joined together. */
|
|
1391
|
+
async readAll() {
|
|
1392
|
+
const parts = [];
|
|
1393
|
+
for await (const chunk of this) parts.push(chunk.pcm);
|
|
1394
|
+
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
1395
|
+
let offset = 0;
|
|
1396
|
+
for (const p of parts) {
|
|
1397
|
+
out.set(p, offset);
|
|
1398
|
+
offset += p.length;
|
|
1399
|
+
}
|
|
1400
|
+
return out;
|
|
1401
|
+
}
|
|
1402
|
+
handle(event, data) {
|
|
1403
|
+
let payload;
|
|
1404
|
+
try {
|
|
1405
|
+
payload = data ? JSON.parse(data) : {};
|
|
1406
|
+
} catch {
|
|
1407
|
+
return null;
|
|
1408
|
+
}
|
|
1409
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return null;
|
|
1410
|
+
const p = payload;
|
|
1411
|
+
const kind = typeof p.type === "string" && p.type || event;
|
|
1412
|
+
if (kind === "start") {
|
|
1413
|
+
if (typeof p.sample_rate === "number") this.sampleRate = p.sample_rate;
|
|
1414
|
+
if (typeof p.audio_format === "string") this.audioFormat = p.audio_format;
|
|
1415
|
+
} else if (kind === "chunk") {
|
|
1416
|
+
return {
|
|
1417
|
+
pcm: base64ToBytes(typeof p.audio_b64 === "string" ? p.audio_b64 : ""),
|
|
1418
|
+
sampleRate: typeof p.sample_rate === "number" ? p.sample_rate : this.sampleRate,
|
|
1419
|
+
audioFormat: typeof p.audio_format === "string" ? p.audio_format : this.audioFormat,
|
|
1420
|
+
index: typeof p.index === "number" ? p.index : null
|
|
1421
|
+
};
|
|
1422
|
+
} else if (kind === "done") {
|
|
1423
|
+
this.done = p;
|
|
1424
|
+
} else if (kind === "error") {
|
|
1425
|
+
throw new StreamError(
|
|
1426
|
+
typeof p.code === "string" && p.code ? p.code : "stream_error",
|
|
1427
|
+
typeof p.message === "string" ? p.message : ""
|
|
1428
|
+
);
|
|
1429
|
+
}
|
|
1430
|
+
return null;
|
|
1431
|
+
}
|
|
1432
|
+
};
|
|
1433
|
+
var Speech = class {
|
|
1434
|
+
constructor(core) {
|
|
1435
|
+
this.core = core;
|
|
1436
|
+
}
|
|
1437
|
+
core;
|
|
1438
|
+
/** Synthesize `text` and return the whole clip (`wav` unless `output_format` says otherwise). */
|
|
1439
|
+
async create(params, options = {}) {
|
|
1440
|
+
const response = await this.core.request("POST", `/voices/${pathId(params.voice_id)}/speak`, {
|
|
1441
|
+
...options,
|
|
1442
|
+
idempotent: true,
|
|
1443
|
+
json: body(params)
|
|
1444
|
+
});
|
|
1445
|
+
const h = response.headers;
|
|
1446
|
+
return {
|
|
1447
|
+
data: new Uint8Array(await response.arrayBuffer()),
|
|
1448
|
+
contentType: h.get("content-type") ?? "audio/wav",
|
|
1449
|
+
sampleRate: intHeader(h, "x-sample-rate"),
|
|
1450
|
+
generationId: h.get("x-generation-id") || null,
|
|
1451
|
+
audioURL: h.get("x-audio-url") || null,
|
|
1452
|
+
billingRemaining: intHeader(h, "x-billing-remaining")
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
/** Stream speech as raw PCM chunks (`s16le` unless `output_format` says otherwise):
|
|
1456
|
+
*
|
|
1457
|
+
* ```ts
|
|
1458
|
+
* for await (const chunk of client.speech.stream({ voice_id, text: "Hello" })) play(chunk.pcm);
|
|
1459
|
+
* ```
|
|
1460
|
+
*/
|
|
1461
|
+
stream(params, options = {}) {
|
|
1462
|
+
return new SpeechStream(
|
|
1463
|
+
this.core,
|
|
1464
|
+
`/voices/${pathId(params.voice_id)}/stream`,
|
|
1465
|
+
body(params),
|
|
1466
|
+
options
|
|
1467
|
+
);
|
|
1468
|
+
}
|
|
1469
|
+
};
|
|
1470
|
+
|
|
1471
|
+
// src/resources/tokens.ts
|
|
1472
|
+
var Tokens = class {
|
|
1473
|
+
constructor(core) {
|
|
1474
|
+
this.core = core;
|
|
1475
|
+
}
|
|
1476
|
+
core;
|
|
1477
|
+
/** Mint a single-use token. Call this on your server with an API key that has the `tts`
|
|
1478
|
+
* scope, and send only the token to the client:
|
|
1479
|
+
*
|
|
1480
|
+
* ```ts
|
|
1481
|
+
* const { token } = await client.tokens.create({ ttl_seconds: 60, voice_ids: [voiceId] });
|
|
1482
|
+
* ```
|
|
1483
|
+
*/
|
|
1484
|
+
async create(params = {}, options = {}) {
|
|
1485
|
+
const json = {};
|
|
1486
|
+
if (params.ttl_seconds !== void 0) json.ttl_seconds = params.ttl_seconds;
|
|
1487
|
+
if (params.voice_ids !== void 0) json.voice_ids = params.voice_ids;
|
|
1488
|
+
return this.core.json("POST", "/tokens", { ...options, json });
|
|
1489
|
+
}
|
|
1490
|
+
};
|
|
1491
|
+
|
|
1492
|
+
// src/files.ts
|
|
1493
|
+
var TYPES = {
|
|
1494
|
+
wav: "audio/wav",
|
|
1495
|
+
mp3: "audio/mpeg",
|
|
1496
|
+
m4a: "audio/mp4",
|
|
1497
|
+
ogg: "audio/ogg",
|
|
1498
|
+
flac: "audio/flac",
|
|
1499
|
+
webm: "audio/webm"
|
|
1500
|
+
};
|
|
1501
|
+
function guessType(name) {
|
|
1502
|
+
return TYPES[name.split(".").pop()?.toLowerCase() ?? ""] ?? "application/octet-stream";
|
|
1503
|
+
}
|
|
1504
|
+
function toBlob(file) {
|
|
1505
|
+
if (typeof Blob !== "undefined" && file instanceof Blob) {
|
|
1506
|
+
const name = file.name;
|
|
1507
|
+
return { blob: file, filename: typeof name === "string" && name ? name : "audio.wav" };
|
|
1508
|
+
}
|
|
1509
|
+
if (file instanceof Uint8Array || file instanceof ArrayBuffer) {
|
|
1510
|
+
return { blob: new Blob([file]), filename: "audio.wav" };
|
|
1511
|
+
}
|
|
1512
|
+
const { data, filename = "audio.wav" } = file;
|
|
1513
|
+
const contentType = file.contentType ?? guessType(filename);
|
|
1514
|
+
const blob = data instanceof Blob && data.type ? data : new Blob([data], { type: contentType });
|
|
1515
|
+
return { blob, filename };
|
|
1516
|
+
}
|
|
1517
|
+
async function fileFromPath(path) {
|
|
1518
|
+
const { readFile } = await import("fs/promises");
|
|
1519
|
+
const { basename } = await import("path");
|
|
1520
|
+
const data = await readFile(path);
|
|
1521
|
+
const filename = basename(path);
|
|
1522
|
+
return { data: new Uint8Array(data), filename, contentType: guessType(filename) };
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
// src/types.ts
|
|
1526
|
+
function toVoice(raw) {
|
|
1527
|
+
const v = { ...raw };
|
|
1528
|
+
const labels = v.labels;
|
|
1529
|
+
if (Array.isArray(labels)) {
|
|
1530
|
+
const map = {};
|
|
1531
|
+
for (const item of labels) {
|
|
1532
|
+
if (item && typeof item === "object" && "key" in item) {
|
|
1533
|
+
map[String(item.key)] = String(
|
|
1534
|
+
item.value ?? ""
|
|
1535
|
+
);
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
v.labels = map;
|
|
1539
|
+
} else if (!labels || typeof labels !== "object") {
|
|
1540
|
+
v.labels = {};
|
|
1541
|
+
}
|
|
1542
|
+
return v;
|
|
1543
|
+
}
|
|
1544
|
+
var isReady = (voice) => voice.status === "ready";
|
|
1545
|
+
|
|
1546
|
+
// src/resources/voices.ts
|
|
1547
|
+
var VoicePage = class {
|
|
1548
|
+
constructor(data, fetchPage) {
|
|
1549
|
+
this.fetchPage = fetchPage;
|
|
1550
|
+
this.voices = (data.voices ?? []).map(toVoice);
|
|
1551
|
+
this.page = Number(data.page ?? 1);
|
|
1552
|
+
this.page_size = Number(data.page_size ?? this.voices.length);
|
|
1553
|
+
this.total = Number(data.total ?? this.voices.length);
|
|
1554
|
+
this.has_more = Boolean(data.has_more);
|
|
1555
|
+
}
|
|
1556
|
+
fetchPage;
|
|
1557
|
+
voices;
|
|
1558
|
+
page;
|
|
1559
|
+
page_size;
|
|
1560
|
+
total;
|
|
1561
|
+
has_more;
|
|
1562
|
+
nextPage() {
|
|
1563
|
+
return this.fetchPage(this.page + 1);
|
|
1564
|
+
}
|
|
1565
|
+
async *iterPages() {
|
|
1566
|
+
let page = this;
|
|
1567
|
+
for (; ; ) {
|
|
1568
|
+
yield page;
|
|
1569
|
+
if (!page.has_more || page.voices.length === 0) return;
|
|
1570
|
+
page = await page.nextPage();
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
async *[Symbol.asyncIterator]() {
|
|
1574
|
+
for await (const page of this.iterPages()) yield* page.voices;
|
|
1575
|
+
}
|
|
1576
|
+
};
|
|
1577
|
+
function consent(params) {
|
|
1578
|
+
if (params.consent !== true) {
|
|
1579
|
+
throw new FinesseError(
|
|
1580
|
+
"Cloning requires consent: true: you attest that you are the speaker or hold their explicit permission to clone their voice."
|
|
1581
|
+
);
|
|
1582
|
+
}
|
|
1583
|
+
return "true";
|
|
1584
|
+
}
|
|
1585
|
+
function defined(obj) {
|
|
1586
|
+
return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== void 0));
|
|
1587
|
+
}
|
|
1588
|
+
function form(fields, file) {
|
|
1589
|
+
const { blob, filename } = toBlob(file);
|
|
1590
|
+
return () => {
|
|
1591
|
+
const fd = new FormData();
|
|
1592
|
+
for (const [k, v] of Object.entries(fields)) if (v !== void 0) fd.append(k, v);
|
|
1593
|
+
fd.append("file", blob, filename);
|
|
1594
|
+
return fd;
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
var VoicePagePromise = class {
|
|
1598
|
+
constructor(load) {
|
|
1599
|
+
this.load = load;
|
|
1600
|
+
}
|
|
1601
|
+
load;
|
|
1602
|
+
first = null;
|
|
1603
|
+
page() {
|
|
1604
|
+
return this.first ??= this.load();
|
|
1605
|
+
}
|
|
1606
|
+
then(onfulfilled, onrejected) {
|
|
1607
|
+
return this.page().then(onfulfilled, onrejected);
|
|
1608
|
+
}
|
|
1609
|
+
catch(onrejected) {
|
|
1610
|
+
return this.page().catch(onrejected);
|
|
1611
|
+
}
|
|
1612
|
+
finally(onfinally) {
|
|
1613
|
+
return this.page().finally(onfinally);
|
|
1614
|
+
}
|
|
1615
|
+
async *[Symbol.asyncIterator]() {
|
|
1616
|
+
yield* await this.page();
|
|
1617
|
+
}
|
|
1618
|
+
};
|
|
1619
|
+
var Voices = class {
|
|
1620
|
+
constructor(core, jobs) {
|
|
1621
|
+
this.core = core;
|
|
1622
|
+
this.jobs = jobs;
|
|
1623
|
+
}
|
|
1624
|
+
core;
|
|
1625
|
+
jobs;
|
|
1626
|
+
/** Your voices (newest first), then the public platform voices.
|
|
1627
|
+
* `for await (const voice of client.voices.list())` walks every page;
|
|
1628
|
+
* `await client.voices.list()` returns just the first page. */
|
|
1629
|
+
list(params = {}, options = {}) {
|
|
1630
|
+
const pageSize = params.page_size ?? 50;
|
|
1631
|
+
const fetchPage = async (page) => {
|
|
1632
|
+
const data = await this.core.json("GET", "/voices", {
|
|
1633
|
+
...options,
|
|
1634
|
+
query: { page, page_size: pageSize }
|
|
1635
|
+
});
|
|
1636
|
+
return new VoicePage(data, fetchPage);
|
|
1637
|
+
};
|
|
1638
|
+
return new VoicePagePromise(() => fetchPage(params.page ?? 1));
|
|
1639
|
+
}
|
|
1640
|
+
/** One of your voices or a public platform voice. */
|
|
1641
|
+
async get(voiceId, options = {}) {
|
|
1642
|
+
return toVoice(await this.core.json("GET", `/voices/${pathId(voiceId)}`, options));
|
|
1643
|
+
}
|
|
1644
|
+
/** Create an empty `draft` voice. Give it a clip with {@link enroll}, or use {@link clone}. */
|
|
1645
|
+
async create(params, options = {}) {
|
|
1646
|
+
const json = defined({ ...params });
|
|
1647
|
+
return toVoice(await this.core.json("POST", "/voices", { ...options, json, idempotent: true }));
|
|
1648
|
+
}
|
|
1649
|
+
/** Change a voice's metadata. Only the fields you pass change. */
|
|
1650
|
+
async update(voiceId, params, options = {}) {
|
|
1651
|
+
const json = defined({ ...params });
|
|
1652
|
+
return toVoice(
|
|
1653
|
+
await this.core.json("PATCH", `/voices/${pathId(voiceId)}`, { ...options, json })
|
|
1654
|
+
);
|
|
1655
|
+
}
|
|
1656
|
+
/** Delete one of your voices. */
|
|
1657
|
+
async delete(voiceId, options = {}) {
|
|
1658
|
+
await this.core.request("DELETE", `/voices/${pathId(voiceId)}`, options);
|
|
1659
|
+
}
|
|
1660
|
+
/** Create a voice from a reference clip. Returns at once with the voice in `processing` and a
|
|
1661
|
+
* `job_id`; see {@link waitUntilReady} or {@link cloneAndWait}. Requires `consent: true`. */
|
|
1662
|
+
async clone(params, options = {}) {
|
|
1663
|
+
const fields = {
|
|
1664
|
+
consent: consent(params),
|
|
1665
|
+
name: params.name,
|
|
1666
|
+
description: params.description,
|
|
1667
|
+
language: params.language,
|
|
1668
|
+
ref_text: params.ref_text,
|
|
1669
|
+
labels: params.labels ? JSON.stringify(params.labels) : void 0,
|
|
1670
|
+
remove_background_noise: params.remove_background_noise === void 0 ? void 0 : String(params.remove_background_noise)
|
|
1671
|
+
};
|
|
1672
|
+
const result = await this.core.json("POST", "/voices/clone", {
|
|
1673
|
+
...options,
|
|
1674
|
+
idempotent: true,
|
|
1675
|
+
form: form(fields, params.file)
|
|
1676
|
+
});
|
|
1677
|
+
return { ...result, voice: toVoice(result.voice) };
|
|
1678
|
+
}
|
|
1679
|
+
/** Clone a reference clip into an existing voice of yours. Returns at once. Requires
|
|
1680
|
+
* `consent: true`. */
|
|
1681
|
+
async enroll(voiceId, params, options = {}) {
|
|
1682
|
+
const fields = {
|
|
1683
|
+
consent: consent(params),
|
|
1684
|
+
ref_text: params.ref_text,
|
|
1685
|
+
language: params.language
|
|
1686
|
+
};
|
|
1687
|
+
const result = await this.core.json("POST", `/voices/${pathId(voiceId)}/enroll`, {
|
|
1688
|
+
...options,
|
|
1689
|
+
idempotent: true,
|
|
1690
|
+
form: form(fields, params.file)
|
|
1691
|
+
});
|
|
1692
|
+
return { ...result, voice: toVoice(result.voice) };
|
|
1693
|
+
}
|
|
1694
|
+
/** Wait for a clone job to finish and return the voice, now `ready`. */
|
|
1695
|
+
async waitUntilReady(clone, options = {}) {
|
|
1696
|
+
await this.jobs.wait(clone.job_id, options);
|
|
1697
|
+
return this.get(clone.voice.id, { signal: options.signal });
|
|
1698
|
+
}
|
|
1699
|
+
/** {@link clone}, then {@link waitUntilReady}. Returns the ready voice. */
|
|
1700
|
+
async cloneAndWait(params, options = {}) {
|
|
1701
|
+
return this.waitUntilReady(await this.clone(params), options);
|
|
1702
|
+
}
|
|
1703
|
+
};
|
|
1704
|
+
|
|
1705
|
+
// src/client.ts
|
|
1706
|
+
function resources(core) {
|
|
1707
|
+
const jobs = new Jobs(core);
|
|
1708
|
+
return {
|
|
1709
|
+
jobs,
|
|
1710
|
+
voices: new Voices(core, jobs),
|
|
1711
|
+
speech: new Speech(core),
|
|
1712
|
+
tokens: new Tokens(core)
|
|
1713
|
+
};
|
|
1714
|
+
}
|
|
1715
|
+
var Finesse = class _Finesse {
|
|
1716
|
+
voices;
|
|
1717
|
+
jobs;
|
|
1718
|
+
speech;
|
|
1719
|
+
tokens;
|
|
1720
|
+
realtime;
|
|
1721
|
+
/** @internal */
|
|
1722
|
+
_core;
|
|
1723
|
+
constructor(options = {}) {
|
|
1724
|
+
this._core = "_core" in options ? options._core : new Core(options);
|
|
1725
|
+
const r = resources(this._core);
|
|
1726
|
+
this.jobs = r.jobs;
|
|
1727
|
+
this.voices = r.voices;
|
|
1728
|
+
this.speech = r.speech;
|
|
1729
|
+
this.tokens = r.tokens;
|
|
1730
|
+
this.realtime = new Realtime(this._core);
|
|
1731
|
+
}
|
|
1732
|
+
get baseURL() {
|
|
1733
|
+
return this._core.baseURL;
|
|
1734
|
+
}
|
|
1735
|
+
/** A copy of this client with different defaults. Cheap: it shares everything else. */
|
|
1736
|
+
withOptions(options) {
|
|
1737
|
+
return new _Finesse({
|
|
1738
|
+
_core: this._core.withOptions({
|
|
1739
|
+
timeout: options.timeout,
|
|
1740
|
+
maxRetries: options.maxRetries,
|
|
1741
|
+
headers: options.defaultHeaders
|
|
1742
|
+
})
|
|
1743
|
+
});
|
|
1744
|
+
}
|
|
1745
|
+
/** The same calls, each resolving to `{ data, response, status, headers, requestId }`:
|
|
1746
|
+
*
|
|
1747
|
+
* ```ts
|
|
1748
|
+
* const { data: voice, requestId } = await client.withRawResponse.voices.get(id);
|
|
1749
|
+
* ```
|
|
1750
|
+
*/
|
|
1751
|
+
get withRawResponse() {
|
|
1752
|
+
return rawClient(this._core, resources);
|
|
1753
|
+
}
|
|
1754
|
+
};
|
|
1755
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1756
|
+
0 && (module.exports = {
|
|
1757
|
+
APIConnectionError,
|
|
1758
|
+
APIStatusError,
|
|
1759
|
+
APITimeoutError,
|
|
1760
|
+
AuthenticationError,
|
|
1761
|
+
BadRequestError,
|
|
1762
|
+
CloneFailedError,
|
|
1763
|
+
CloneTimeoutError,
|
|
1764
|
+
ConflictError,
|
|
1765
|
+
Finesse,
|
|
1766
|
+
FinesseError,
|
|
1767
|
+
InternalServerError,
|
|
1768
|
+
NotFoundError,
|
|
1769
|
+
PayloadTooLargeError,
|
|
1770
|
+
PaymentRequiredError,
|
|
1771
|
+
PermissionDeniedError,
|
|
1772
|
+
RateLimitError,
|
|
1773
|
+
RealtimeContext,
|
|
1774
|
+
RealtimeError,
|
|
1775
|
+
RealtimeSession,
|
|
1776
|
+
SpeechStream,
|
|
1777
|
+
StreamError,
|
|
1778
|
+
UnprocessableEntityError,
|
|
1779
|
+
VERSION,
|
|
1780
|
+
VoicePage,
|
|
1781
|
+
VoicePagePromise,
|
|
1782
|
+
fileFromPath,
|
|
1783
|
+
isReady
|
|
1784
|
+
});
|
|
1785
|
+
//# sourceMappingURL=index.cjs.map
|