@flamai/finesse 0.0.0-stage → 0.1.2

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.
@@ -0,0 +1,831 @@
1
+ type Fetch = typeof globalThis.fetch;
2
+ interface ClientOptions {
3
+ /** Workspace API key (`flv_...`). Defaults to `FINESSE_API_KEY`. Keep it on your server. */
4
+ apiKey?: string;
5
+ /** A short-lived token (`fin_tok_...`) from `tokens.create`, instead of an API key. It is
6
+ * safe in a browser: it carries the `tts` scope only and is good for one `speech.create` /
7
+ * `speech.stream` call or one realtime session. Sent as `Authorization: Bearer`. */
8
+ accessToken?: string;
9
+ /** API host. Defaults to `FINESSE_BASE_URL`, then production. */
10
+ baseURL?: string;
11
+ /** Per-request timeout in ms. Default 120 000. */
12
+ timeout?: number;
13
+ /** Retries for transient failures. Default 2. Reads, updates and deletes retry on network
14
+ * errors, timeouts, 408, 429 and 5xx. Creates, uploads and synthesis retry only when the
15
+ * server did no work: the connection never opened, 429, 502 or 503. With an `accessToken`
16
+ * only 429 and a connection that never opened are retried, since the token is single use. */
17
+ maxRetries?: number;
18
+ /** Extra headers sent on every request. */
19
+ defaultHeaders?: Record<string, string>;
20
+ /** A custom `fetch` (proxies, tests). */
21
+ fetch?: Fetch;
22
+ /** Allow an API key in a browser. The key would be visible to anyone using the page, so
23
+ * only do this for internal tools; otherwise use an `accessToken`. */
24
+ dangerouslyAllowBrowser?: boolean;
25
+ /** Log requests and retries: `"debug"`, `"info"` or `"off"`. Defaults to `FINESSE_LOG`, then
26
+ * `"off"`. The API key, bodies and audio are never logged. */
27
+ logLevel?: LogLevel;
28
+ /** Where log lines go. Defaults to `console`. */
29
+ logger?: Logger;
30
+ }
31
+ type LogLevel = "debug" | "info" | "off";
32
+ interface Logger {
33
+ debug(message: string): void;
34
+ info(message: string): void;
35
+ }
36
+ /** Per-call overrides that every request method accepts as its last argument. */
37
+ interface RequestOptions {
38
+ /** Abort the request, its retries and any waiting. Rejects with the signal's reason. */
39
+ signal?: AbortSignal;
40
+ /** Timeout in ms for this call (for streams, the longest wait between chunks). */
41
+ timeout?: number;
42
+ /** Retries for this call. */
43
+ maxRetries?: number;
44
+ /** Extra headers for this call; they override the client's. */
45
+ headers?: Record<string, string>;
46
+ }
47
+ interface CallOptions extends RequestOptions {
48
+ /** Send one Idempotency-Key for every attempt, so the call can be retried like a read. */
49
+ idempotent?: boolean;
50
+ query?: Record<string, string | number>;
51
+ json?: unknown;
52
+ form?: () => FormData;
53
+ stream?: boolean;
54
+ }
55
+ declare class Core {
56
+ readonly apiKey: string | null;
57
+ readonly accessToken: string | null;
58
+ readonly baseURL: string;
59
+ readonly timeout: number;
60
+ readonly maxRetries: number;
61
+ readonly headers: Record<string, string>;
62
+ readonly fetch: Fetch;
63
+ readonly logLevel: LogLevel;
64
+ readonly logger: Logger;
65
+ /** @internal What this client learned about the server; shared by `withOptions` copies. */
66
+ readonly server: {
67
+ honoursIdempotencyKeys: boolean;
68
+ };
69
+ /** @internal Called with each successful response (used by `withRawResponse`). */
70
+ onResponse: ((response: Response) => void) | null;
71
+ constructor(options?: ClientOptions);
72
+ /** @internal A view of this core with other defaults, sharing everything else. */
73
+ withOptions(overrides: {
74
+ timeout?: number;
75
+ maxRetries?: number;
76
+ headers?: Record<string, string>;
77
+ }): Core;
78
+ private debug;
79
+ private info;
80
+ url(path: string, query?: Record<string, string | number>): string;
81
+ get wsBaseURL(): string;
82
+ /** Send a request, retrying transient failures, and throw on an error status. With
83
+ * `stream: true` the caller owns the response body. A caller `signal` stops the request and
84
+ * any further retries, and rejects with the signal's reason. */
85
+ request(method: string, path: string, opts?: CallOptions): Promise<Response>;
86
+ /** Request and decode a JSON success body, stripping the `{status, error, data}` envelope. */
87
+ json<T>(method: string, path: string, opts?: CallOptions): Promise<T>;
88
+ }
89
+
90
+ /** The parsed result of a call plus the HTTP response it came from. For calls that make several
91
+ * requests (`voices.list` walking pages, `cloneAndWait` polling), `response` is the first one. */
92
+ interface RawResponse<T> {
93
+ data: T;
94
+ response: Response;
95
+ status: number;
96
+ headers: Headers;
97
+ /** The service's `X-Request-Id` for this request, when sent. */
98
+ requestId: string | null;
99
+ }
100
+ type Fn = (...args: never[]) => unknown;
101
+ type RawMethods<R> = {
102
+ [M in keyof R as R[M] extends Fn ? (M extends "stream" ? never : M) : never]: R[M] extends (...args: infer A) => infer Ret ? (...args: A) => Promise<RawResponse<Awaited<Ret>>> : never;
103
+ };
104
+ interface Resources {
105
+ voices: object;
106
+ jobs: object;
107
+ speech: object;
108
+ }
109
+ /** `client.withRawResponse`: the same methods, each resolving to a {@link RawResponse}. */
110
+ type RawClient<R extends Resources> = {
111
+ [K in keyof R]: RawMethods<R[K]>;
112
+ };
113
+
114
+ /**
115
+ * Errors thrown by the SDK. Every error extends {@link FinesseError}; HTTP errors extend
116
+ * {@link APIStatusError} and are split by status so you can catch exactly what you handle:
117
+ *
118
+ * ```ts
119
+ * try {
120
+ * await client.speech.create({ voice_id, text: "Hi" });
121
+ * } catch (e) {
122
+ * if (e instanceof PaymentRequiredError) console.log("Out of credits", e.billingRemaining);
123
+ * }
124
+ * ```
125
+ */
126
+ declare class FinesseError extends Error {
127
+ constructor(message: string, options?: {
128
+ cause?: unknown;
129
+ });
130
+ }
131
+ /** The request never got a response: DNS, TLS, connection reset, and so on. */
132
+ declare class APIConnectionError extends FinesseError {
133
+ }
134
+ /** The request timed out. */
135
+ declare class APITimeoutError extends APIConnectionError {
136
+ constructor(options?: {
137
+ cause?: unknown;
138
+ });
139
+ }
140
+ /**
141
+ * The API answered with a 4xx or 5xx status. The API's three error body shapes are normalised:
142
+ *
143
+ * - `{"status": 404, "error": true, "message": "Voice not found", "code": "not_found"}`
144
+ * - `{"error": "insufficient_credits", "unit": "character", "limit": 100, "used": 100}`
145
+ * - `{"error": "plan limit reached", "feature": "max_voices", "limit": 3}`
146
+ */
147
+ declare class APIStatusError extends FinesseError {
148
+ /** HTTP status. */
149
+ readonly status: number;
150
+ /** Machine-readable code. Branch on this, never on the message. Envelope codes:
151
+ * `invalid_request`, `unauthorized`, `key_expired`, `token_invalid`, `payment_required`,
152
+ * `forbidden`, `insufficient_scope`, `voice_not_allowed`, `not_found`, `conflict`,
153
+ * `idempotency_in_progress`, `payload_too_large`, `invalid_input`, `idempotency_key_reused`,
154
+ * `rate_limited`, `concurrency_limited`, `internal_error`, `upstream_error`, `upstream_busy`,
155
+ * `unavailable`; plan denials: `insufficient_credits`, `quota_exhausted`, `feature_disabled`,
156
+ * `plan_limit_reached`; realtime: `too_many_sessions`. `null` when absent. */
157
+ readonly code: string | null;
158
+ /** The decoded JSON body, or the raw text when it is not JSON. */
159
+ readonly body: unknown;
160
+ readonly headers: Headers;
161
+ /** `X-Billing-Remaining` as a number, when sent. */
162
+ readonly billingRemaining: number | null;
163
+ /** The service's `X-Request-Id` for the failed request; quote it to support. */
164
+ readonly requestId: string | null;
165
+ constructor(status: number, message: string, body: unknown, code: string | null, headers: Headers);
166
+ /** Seconds the server asked you to wait (`Retry-After`), when it sent one. */
167
+ get retryAfter(): number | null;
168
+ /** Plan limit from an entitlement or plan-limit denial, when present. */
169
+ get limit(): number | null;
170
+ /** Amount already used, from an entitlement denial, when present. */
171
+ get used(): number | null;
172
+ }
173
+ /** 400: invalid request (missing field, text too long, voice not enrolled, ...). */
174
+ declare class BadRequestError extends APIStatusError {
175
+ }
176
+ /** 401: the API key is missing, unknown or revoked; `key_expired`; or a short-lived token is
177
+ * invalid, expired or already used (`token_invalid`). */
178
+ declare class AuthenticationError extends APIStatusError {
179
+ }
180
+ /** 402: out of credits (`insufficient_credits`) or quota (`quota_exhausted`), or another
181
+ * payment problem (`payment_required`). */
182
+ declare class PaymentRequiredError extends APIStatusError {
183
+ }
184
+ /** 403: the key lacks the route's scope (`insufficient_scope`), a token is limited to other
185
+ * voices (`voice_not_allowed`), the plan lacks the feature (`feature_disabled`), or a cap is
186
+ * reached (`plan_limit_reached`). */
187
+ declare class PermissionDeniedError extends APIStatusError {
188
+ /** The capped feature for `plan_limit_reached`, e.g. `max_voices`. */
189
+ get feature(): string | null;
190
+ }
191
+ /** 404: no such resource, or not visible to this workspace. */
192
+ declare class NotFoundError extends APIStatusError {
193
+ }
194
+ /** 409: e.g. a request with the same `Idempotency-Key` is still running. */
195
+ declare class ConflictError extends APIStatusError {
196
+ }
197
+ /** 413: the request body is over 21 MiB (`payload_too_large`). */
198
+ declare class PayloadTooLargeError extends APIStatusError {
199
+ }
200
+ /** 422: the synthesis service could not use the input (`invalid_input`), or an
201
+ * `Idempotency-Key` was reused for a different request (`idempotency_key_reused`). */
202
+ declare class UnprocessableEntityError extends APIStatusError {
203
+ }
204
+ /** 429: over the plan's requests per second (`rate_limited`) or concurrent synthesis
205
+ * (`concurrency_limited`), or (realtime) `too_many_sessions`. Wait `retryAfter` seconds. */
206
+ declare class RateLimitError extends APIStatusError {
207
+ }
208
+ /** 5xx: a server error, or the synthesis service failing (`upstream_error`, 502) or at
209
+ * capacity (`upstream_busy`, 503, with `retryAfter`). Usually safe to retry. */
210
+ declare class InternalServerError extends APIStatusError {
211
+ }
212
+ /** A speech stream failed after it started. */
213
+ declare class StreamError extends FinesseError {
214
+ readonly code: string;
215
+ constructor(code: string, message?: string);
216
+ }
217
+ /** A clone job ended without producing a ready voice. */
218
+ declare class CloneFailedError extends FinesseError {
219
+ readonly job: {
220
+ id: string;
221
+ status: string;
222
+ error?: Record<string, unknown> | null;
223
+ };
224
+ constructor(job: {
225
+ id: string;
226
+ status: string;
227
+ error?: Record<string, unknown> | null;
228
+ });
229
+ }
230
+ /** Waiting for a clone job took longer than the timeout. The job may still finish. */
231
+ declare class CloneTimeoutError extends FinesseError {
232
+ readonly jobId: string;
233
+ readonly timeoutMs: number;
234
+ constructor(jobId: string, timeoutMs: number);
235
+ }
236
+
237
+ /**
238
+ * Realtime speech over one WebSocket. One session is one socket; each turn of speech runs in a
239
+ * *context*, and a socket can run several contexts at once (up to 4). Audio arrives as raw PCM,
240
+ * `s16le` at 24 kHz unless you ask for something else.
241
+ *
242
+ * ```ts
243
+ * const rt = await client.realtime.connect();
244
+ * for await (const event of rt.say({ voice_id, text: "Hello there." })) {
245
+ * if (event.type === "audio") play(event.pcm);
246
+ * }
247
+ * await rt.close();
248
+ * ```
249
+ *
250
+ * In Node this needs the `ws` package (`npm i ws`), which sends the API key as a header. Bun's
251
+ * built-in WebSocket does too, and so does Deno's from 2.5.
252
+ *
253
+ * In a browser, never use the API key: mint a short-lived token on your server
254
+ * (`tokens.create`) and connect with `{ accessToken }` or `{ getAccessToken }`. The token rides
255
+ * in the WebSocket subprotocol list (`["finesse.v1", token]`), the one place a browser can put
256
+ * a credential on a handshake. (With a key, browsers and Deno before 2.5 fall back to a
257
+ * deprecated URL parameter, and the SDK logs a warning once.)
258
+ */
259
+
260
+ /** A realtime context or session failed. */
261
+ declare class RealtimeError extends FinesseError {
262
+ /** Per context: `voice_not_found`, `voice_not_allowed` (the token is limited to other
263
+ * voices), `invalid_request` (a parameter out of range), a `task_failed` code. Whole session:
264
+ * `insufficient_credits`, `upstream_unavailable` (the engine could not be reached),
265
+ * `upstream_busy` (it is at capacity), `upstream_error`, `upstream_closed`,
266
+ * `connection_lost`, `token_spent`, ... */
267
+ readonly code: string;
268
+ /** The context it applies to, or `null` for the whole session. */
269
+ readonly contextId: string | null;
270
+ /** The WebSocket close code, when the socket closed. */
271
+ closeCode: number | null;
272
+ constructor(code: string, message?: string, options?: {
273
+ contextId?: string | null;
274
+ closeCode?: number | null;
275
+ });
276
+ /** Whether reconnecting and trying again can succeed. */
277
+ get retryable(): boolean;
278
+ }
279
+ interface TaskStarted {
280
+ type: "task_started";
281
+ context_id: string;
282
+ sample_rate?: number;
283
+ audio_format?: string;
284
+ }
285
+ interface AudioEvent {
286
+ type: "audio";
287
+ context_id: string;
288
+ /** Raw PCM in the context's `audio_format` / `sample_rate`. */
289
+ pcm: Uint8Array;
290
+ sample_rate: number | null;
291
+ audio_format: string | null;
292
+ sentence_index?: number;
293
+ chunk_index?: number;
294
+ audio_duration_s?: number;
295
+ }
296
+ interface SentenceDone {
297
+ type: "sentence_done";
298
+ context_id: string;
299
+ sentence_index?: number;
300
+ text?: string;
301
+ audio_duration_s?: number;
302
+ }
303
+ /** The context finished. Last event of a context. */
304
+ interface TaskDone {
305
+ type: "task_done";
306
+ context_id: string;
307
+ num_sentences?: number;
308
+ total_audio_duration_s?: number;
309
+ profile?: Record<string, unknown>;
310
+ trace_id?: string;
311
+ }
312
+ /** The context was cancelled. Last event of a cancelled context. */
313
+ interface TaskCancelled {
314
+ type: "task_cancelled";
315
+ context_id: string;
316
+ sentences_completed?: number;
317
+ sentences_dropped?: number;
318
+ trace_id?: string;
319
+ }
320
+ /** An engine event this SDK version does not know yet. `event_type` is its `type` on the wire
321
+ * and `data` the whole frame. */
322
+ interface UnknownEvent {
323
+ type: "unknown";
324
+ context_id: string;
325
+ event_type: string;
326
+ data: Record<string, unknown>;
327
+ }
328
+ /** Every event a context yields. Narrow it on `type`. */
329
+ type RealtimeEvent = TaskStarted | AudioEvent | SentenceDone | TaskDone | TaskCancelled | UnknownEvent;
330
+ interface ContextOptions {
331
+ /** One of your voices or a public platform voice. */
332
+ voice_id: string;
333
+ context_id?: string;
334
+ language?: string;
335
+ /** `s16le` (default), `s24le`, `s32le`, `f32le`, `f64le`, `opus`, or `mulaw` (G.711 µ-law
336
+ * for telephony, with `sample_rate: 8000`). */
337
+ output_format?: "s16le" | "s24le" | "s32le" | "f32le" | "f64le" | "opus" | "mulaw";
338
+ /** Output rate in Hz, 8000-48000; the native rate is 24000. 8000 is produced as asked, other
339
+ * lower values still produce 24000. */
340
+ sample_rate?: number;
341
+ /** Generation steps, 1-32 (default 32); fewer is faster. Values over 32 are capped. */
342
+ num_step?: number;
343
+ guidance_scale?: number;
344
+ /** Speaking rate. */
345
+ length_scale?: number;
346
+ /** Speak a pending sentence after this many ms without new text. */
347
+ idle_flush_ms?: number;
348
+ }
349
+ interface ConnectOptions {
350
+ /** Reconnect on the next context after a retryable drop. Default true. */
351
+ autoReconnect?: boolean;
352
+ /** Ms to open the socket. Default 60 000. */
353
+ openTimeout?: number;
354
+ /** A short-lived token (`fin_tok_...`) for this session instead of the API key (defaults to
355
+ * the client's `accessToken`). Single use: the handshake spends it, so pass
356
+ * `getAccessToken` too if the session should reconnect. */
357
+ accessToken?: string;
358
+ /** Returns a fresh token, e.g. from your own backend running `tokens.create`. Used for the
359
+ * first handshake when there is no `accessToken`, and for every reconnect. */
360
+ getAccessToken?: () => string | Promise<string>;
361
+ /** A WebSocket implementation (defaults to the `ws` package in Node, the global elsewhere). */
362
+ WebSocket?: unknown;
363
+ }
364
+ /**
365
+ * One turn of speech. Create it with {@link RealtimeSession.context}. `send` text (the first
366
+ * call opens the context), `finish` when the turn's text is complete, and iterate it for its
367
+ * events. Iteration ends after `task_done` or `task_cancelled`, and throws
368
+ * {@link RealtimeError} if the context fails.
369
+ *
370
+ * Events are buffered without limit so that one slow context never stalls the others on the
371
+ * same socket. Iterate every context you start, or `cancel()` the ones you abandon. Iterate a
372
+ * context from one place only: events are consumed as they are read.
373
+ */
374
+ declare class RealtimeContext implements AsyncIterable<RealtimeEvent> {
375
+ private readonly session;
376
+ /** @internal */ readonly options: Record<string, unknown>;
377
+ readonly id: string;
378
+ cancelled: boolean;
379
+ sampleRate: number | null;
380
+ audioFormat: string | null;
381
+ /** @internal */ started: boolean;
382
+ /** @internal The socket this context runs on. */ socket: unknown;
383
+ /** @internal */ ended: boolean;
384
+ private finished;
385
+ private readonly items;
386
+ private waiter;
387
+ private sending;
388
+ constructor(session: RealtimeSession,
389
+ /** @internal */ options: Record<string, unknown>, id: string);
390
+ /** Send text. Text is billed per character as it is sent. Pass `{ finalize: true }` with the
391
+ * turn's last text (or call {@link finish}). */
392
+ send(text: string, options?: {
393
+ finalize?: boolean;
394
+ }): Promise<void>;
395
+ /** No more text for this turn: speak what is buffered, then end the context. */
396
+ finish(): Promise<void>;
397
+ /** Stop this context now. Audio still in flight for it is dropped; iterating it ends with
398
+ * `task_cancelled`. */
399
+ cancel(): Promise<void>;
400
+ [Symbol.asyncIterator](): AsyncIterator<RealtimeEvent>;
401
+ /** Iterate only the PCM of this context. */
402
+ audio(): AsyncGenerator<Uint8Array>;
403
+ /** @internal */
404
+ _put(event: RealtimeEvent): void;
405
+ /** @internal */
406
+ _fail(error: RealtimeError): void;
407
+ /** @internal */
408
+ _end(): void;
409
+ private push;
410
+ private serial;
411
+ }
412
+ /**
413
+ * A realtime session. Get one with `await client.realtime.connect()` and `close()` it when done.
414
+ * After a retryable drop the next context reconnects automatically (unless
415
+ * `autoReconnect: false`); contexts running at the time fail with {@link RealtimeError}.
416
+ */
417
+ declare class RealtimeSession {
418
+ private readonly core;
419
+ private readonly options;
420
+ private ws;
421
+ private opening;
422
+ private readonly contexts;
423
+ private pendingAudio;
424
+ private fatal;
425
+ private error;
426
+ private openedOnce;
427
+ private closed;
428
+ private closedWaiters;
429
+ private readonly staticToken;
430
+ private tokenSpent;
431
+ constructor(core: Core, options?: ConnectOptions);
432
+ get url(): string;
433
+ get isOpen(): boolean;
434
+ /**
435
+ * Open the socket (no-op when open). Throws `AuthenticationError`, `PaymentRequiredError` or
436
+ * `PermissionDeniedError` when the handshake is refused, `RateLimitError` with
437
+ * `code === "too_many_sessions"` when the workspace already has its maximum of sessions,
438
+ * and `APIConnectionError` / `APITimeoutError` when the service can't be reached.
439
+ */
440
+ open(): Promise<void>;
441
+ /** Create a context. Nothing is sent until its first `send`. */
442
+ context(options: ContextOptions): RealtimeContext;
443
+ /** Speak `text` as one complete turn and yield its events. Breaking out of the loop cancels
444
+ * the turn. */
445
+ say(options: ContextOptions & {
446
+ text: string;
447
+ }): AsyncGenerator<RealtimeEvent>;
448
+ /** End the session: tell the service, then close the socket. */
449
+ close(): Promise<void>;
450
+ /** @internal */
451
+ _start(ctx: RealtimeContext, text: string, finalize: boolean): Promise<void>;
452
+ /** @internal */
453
+ _send(msg: Record<string, unknown>): void;
454
+ /** @internal Send a `task_start` / `task_continue` frame with `text`. Text that would make
455
+ * the frame larger than {@link MAX_FRAME_BYTES} is split across `task_continue` frames for
456
+ * the same context; only the last one carries `finalize`. */
457
+ _sendText(msg: Record<string, unknown>, text: string, finalize: boolean): void;
458
+ /** @internal */
459
+ _forget(ctx: RealtimeContext): void;
460
+ /** The token for the next handshake, or `null` to use the API key. A static token is used
461
+ * first; once a handshake has spent it, `getAccessToken` supplies one per reconnect. */
462
+ private nextToken;
463
+ private dial;
464
+ private onClose;
465
+ private onAudio;
466
+ private onMessage;
467
+ }
468
+ /** `client.realtime`: open realtime sessions. */
469
+ declare class Realtime {
470
+ private readonly core;
471
+ constructor(core: Core);
472
+ /** Open a session. */
473
+ connect(options?: ConnectOptions): Promise<RealtimeSession>;
474
+ /** Create a session without opening it; it opens on first use. */
475
+ session(options?: ConnectOptions): RealtimeSession;
476
+ }
477
+
478
+ /** Response shapes. Field names match the API (snake_case); timestamps are Unix milliseconds.
479
+ * Media URLs (`original_audio_url`, `sample_audio_url`, `avatar_url`, `audio_url`) may be
480
+ * short-lived signed URLs: download them promptly, or fetch the object again for a fresh one. */
481
+ interface VoiceSettings {
482
+ stability?: number;
483
+ similarity?: number;
484
+ style?: number;
485
+ speed?: number;
486
+ pitch?: number;
487
+ volume?: number;
488
+ use_speaker_boost?: boolean;
489
+ emotion?: string;
490
+ engine_params?: Record<string, unknown>;
491
+ }
492
+ /** A voice. It can synthesize once `status === "ready"`. */
493
+ interface Voice {
494
+ id: string;
495
+ org_id: string;
496
+ name: string;
497
+ /** `draft`, `processing`, `ready`, `failed`, ... */
498
+ status: string;
499
+ clone_method: string;
500
+ /** `private` for your voices, `public` for platform catalog voices. */
501
+ visibility: string;
502
+ primary_language: string;
503
+ description?: string;
504
+ model?: string;
505
+ /** Human-friendly handle, also usable as `voice` on the OpenAI-compatible endpoint. */
506
+ alias?: string;
507
+ source?: string;
508
+ language?: string;
509
+ gender?: string;
510
+ /** Labels as a plain object (the API sends `[{key, value}]`; the SDK converts it). */
511
+ labels: Record<string, string>;
512
+ settings?: VoiceSettings;
513
+ external_ref?: string;
514
+ original_audio_url?: string;
515
+ sample_audio_url?: string;
516
+ avatar_url?: string;
517
+ base_voice_id?: string;
518
+ created_at?: number;
519
+ updated_at?: number;
520
+ last_used_at?: number;
521
+ expires_at?: number;
522
+ /** Prefix of the API key that created the voice; only on your own voices. */
523
+ created_via_key_prefix?: string;
524
+ [key: string]: unknown;
525
+ }
526
+ /** An asynchronous clone job. */
527
+ interface CloneJob {
528
+ id: string;
529
+ org_id: string;
530
+ /** `pending`, `running`, `succeeded`, `failed`, `cancelled` or `expired`. */
531
+ status: string;
532
+ job_type: string;
533
+ voice_id?: string;
534
+ /** Percent complete. Not updated while running; 100 once succeeded. */
535
+ progress: number;
536
+ attempts: number;
537
+ /** Failure detail, `{message, status_code}`. */
538
+ error?: Record<string, unknown> | null;
539
+ started_at?: number;
540
+ finished_at?: number;
541
+ created_at?: number;
542
+ [key: string]: unknown;
543
+ }
544
+ /** What `voices.clone` / `voices.enroll` return: the voice (`processing`) and its job id. */
545
+ interface CloneResult {
546
+ voice: Voice;
547
+ job_id: string;
548
+ }
549
+ /** A synthesized clip from `speech.create`. */
550
+ interface Audio {
551
+ data: Uint8Array;
552
+ /** `audio/wav` for WAV, `application/octet-stream` for raw PCM. */
553
+ contentType: string;
554
+ sampleRate: number | null;
555
+ /** Id of the generation history record (when object storage is enabled). */
556
+ generationId: string | null;
557
+ /** The stored clip, when object storage is enabled; may be a short-lived signed URL. */
558
+ audioURL: string | null;
559
+ billingRemaining: number | null;
560
+ }
561
+ /** One chunk of raw PCM from `speech.stream`. */
562
+ interface AudioChunk {
563
+ pcm: Uint8Array;
564
+ sampleRate: number | null;
565
+ /** PCM encoding, e.g. `s16le`. */
566
+ audioFormat: string | null;
567
+ index: number | null;
568
+ }
569
+ /** The final event of `speech.stream`. */
570
+ interface StreamDone {
571
+ audio_duration_s?: number;
572
+ generation_id?: string;
573
+ audio_url?: string;
574
+ }
575
+ /** `wav` (one-shot default) or raw little-endian PCM. */
576
+ /** A short-lived, single-use token from `tokens.create`. Hand `token` to a browser or device:
577
+ * it is good for one `speech.create` / `speech.stream` call or one realtime session, carries
578
+ * the `tts` scope only, and dies when the key that minted it is revoked or expires. */
579
+ interface AccessToken {
580
+ token: string;
581
+ /** Unix epoch ms. */
582
+ expires_at: number;
583
+ ttl_seconds: number;
584
+ }
585
+ /** `mulaw` is raw G.711 µ-law for telephony and needs `sample_rate: 8000`. */
586
+ type OutputFormat = "wav" | "s16le" | "s24le" | "s32le" | "f32le" | "f64le" | "mulaw";
587
+ interface SpeechParams {
588
+ /** One of your voices or a public platform voice; it must be ready. */
589
+ voice_id: string;
590
+ /** At most 10,000 characters. Billed per character. */
591
+ text: string;
592
+ language?: string;
593
+ output_format?: OutputFormat;
594
+ /** Output rate in Hz, 8000-48000. The native rate is 24000; 8000 is produced as asked, other
595
+ * lower values still return 24000. */
596
+ sample_rate?: number;
597
+ /** Generation steps, 1-32 (default and maximum 32; over 32 is a 400); fewer is faster. */
598
+ num_step?: number;
599
+ }
600
+ declare const isReady: (voice: Voice) => boolean;
601
+
602
+ interface WaitOptions {
603
+ /** Give up after this many ms. Default 300 000 (the first clone after the cloner has been
604
+ * idle can take a couple of minutes). */
605
+ timeout?: number;
606
+ /** Poll every this many ms. Default 2 000. */
607
+ pollInterval?: number;
608
+ /** Called with the job after every poll. */
609
+ onUpdate?: (job: CloneJob) => void;
610
+ /** Stop waiting (rejects with the signal's reason). The job itself keeps running. */
611
+ signal?: AbortSignal;
612
+ }
613
+ /** Clone jobs started by `voices.clone` and `voices.enroll`. */
614
+ declare class Jobs {
615
+ private readonly core;
616
+ constructor(core: Core);
617
+ get(jobId: string, options?: RequestOptions): Promise<CloneJob>;
618
+ /** Poll until the job finishes and return it. Throws {@link CloneFailedError} if it ends
619
+ * `failed`, `cancelled` or `expired`, and {@link CloneTimeoutError} on timeout. */
620
+ wait(jobId: string, options?: WaitOptions): Promise<CloneJob>;
621
+ }
622
+
623
+ /** Speech streamed as raw PCM chunks: `for await (const chunk of stream)`. `done` is set once
624
+ * it finishes. The request is sent when iteration starts. Errors before the stream starts
625
+ * throw the usual `APIStatusError`; failures after it throw {@link StreamError}. */
626
+ declare class SpeechStream implements AsyncIterable<AudioChunk> {
627
+ private readonly core;
628
+ private readonly path;
629
+ private readonly json;
630
+ private readonly options;
631
+ sampleRate: number | null;
632
+ audioFormat: string | null;
633
+ /** Final event (duration, generation id, stored clip URL), once the stream finished. */
634
+ done: StreamDone | null;
635
+ billingRemaining: number | null;
636
+ private consumed;
637
+ constructor(core: Core, path: string, json: Record<string, unknown>, options?: RequestOptions);
638
+ [Symbol.asyncIterator](): AsyncIterator<AudioChunk>;
639
+ /** Consume the stream and return all PCM joined together. */
640
+ readAll(): Promise<Uint8Array>;
641
+ private handle;
642
+ }
643
+ /** Text-to-speech. */
644
+ declare class Speech {
645
+ private readonly core;
646
+ constructor(core: Core);
647
+ /** Synthesize `text` and return the whole clip (`wav` unless `output_format` says otherwise). */
648
+ create(params: SpeechParams, options?: RequestOptions): Promise<Audio>;
649
+ /** Stream speech as raw PCM chunks (`s16le` unless `output_format` says otherwise):
650
+ *
651
+ * ```ts
652
+ * for await (const chunk of client.speech.stream({ voice_id, text: "Hello" })) play(chunk.pcm);
653
+ * ```
654
+ */
655
+ stream(params: SpeechParams, options?: RequestOptions): SpeechStream;
656
+ }
657
+
658
+ interface CreateTokenParams {
659
+ /** How long the token stays valid if unused, 10-3600 seconds. Default 300. */
660
+ ttl_seconds?: number;
661
+ /** Limit the token to these voices (yours or public platform voices). */
662
+ voice_ids?: string[];
663
+ }
664
+ /** Short-lived tokens, so a browser or device can synthesize without your API key. */
665
+ declare class Tokens {
666
+ private readonly core;
667
+ constructor(core: Core);
668
+ /** Mint a single-use token. Call this on your server with an API key that has the `tts`
669
+ * scope, and send only the token to the client:
670
+ *
671
+ * ```ts
672
+ * const { token } = await client.tokens.create({ ttl_seconds: 60, voice_ids: [voiceId] });
673
+ * ```
674
+ */
675
+ create(params?: CreateTokenParams, options?: RequestOptions): Promise<AccessToken>;
676
+ }
677
+
678
+ /** A reference clip: a `Blob`/`File`, raw bytes, or `{ data, filename, contentType }`.
679
+ * In Node, `await fileFromPath("ref.wav")` reads a file from disk. */
680
+ type FileInput = Blob | Uint8Array | ArrayBuffer | {
681
+ data: Blob | Uint8Array | ArrayBuffer;
682
+ filename?: string;
683
+ contentType?: string;
684
+ };
685
+ /** Node only: read a file from disk into a {@link FileInput}. */
686
+ declare function fileFromPath(path: string): Promise<FileInput>;
687
+
688
+ interface ListVoicesParams {
689
+ /** 1-based page. Default 1. */
690
+ page?: number;
691
+ /** Voices per page, at most 100. Default 50. */
692
+ page_size?: number;
693
+ }
694
+ interface CreateVoiceParams {
695
+ name: string;
696
+ description?: string;
697
+ language?: string;
698
+ labels?: Record<string, string>;
699
+ model?: string;
700
+ }
701
+ interface UpdateVoiceParams {
702
+ name?: string;
703
+ description?: string;
704
+ language?: string;
705
+ /** Replaces the whole label set (`{}` clears it). */
706
+ labels?: Record<string, string>;
707
+ }
708
+ /** Cloning requires `consent: true`: you attest that you are the speaker or hold their explicit
709
+ * permission to clone their voice. The attestation is recorded on the voice. */
710
+ interface ConsentParams {
711
+ consent: true;
712
+ }
713
+ interface CloneVoiceParams extends ConsentParams {
714
+ name: string;
715
+ /** Reference clip, at most 20 MiB: at least 10 s of clear speech (10-30 s works best). */
716
+ file: FileInput;
717
+ description?: string;
718
+ language?: string;
719
+ labels?: Record<string, string>;
720
+ /** Transcript of the reference clip, at most 4000 characters. */
721
+ ref_text?: string;
722
+ remove_background_noise?: boolean;
723
+ }
724
+ interface EnrollVoiceParams extends ConsentParams {
725
+ /** Reference clip, at most 20 MiB: at least 10 s of clear speech (10-30 s works best). */
726
+ file: FileInput;
727
+ /** Transcript of the reference clip, at most 4000 characters. */
728
+ ref_text?: string;
729
+ language?: string;
730
+ }
731
+ /** One page of voices. `for await (const voice of page)` walks this page and every page after
732
+ * it, fetching as needed. */
733
+ declare class VoicePage implements AsyncIterable<Voice> {
734
+ private readonly fetchPage;
735
+ readonly voices: Voice[];
736
+ readonly page: number;
737
+ readonly page_size: number;
738
+ readonly total: number;
739
+ readonly has_more: boolean;
740
+ constructor(data: Record<string, unknown>, fetchPage: (page: number) => Promise<VoicePage>);
741
+ nextPage(): Promise<VoicePage>;
742
+ iterPages(): AsyncGenerator<VoicePage>;
743
+ [Symbol.asyncIterator](): AsyncIterator<Voice>;
744
+ }
745
+ /** What `voices.list()` returns: `await` it for the first page, or iterate it directly with
746
+ * `for await` to walk every voice. The first page is fetched once, on first use. */
747
+ declare class VoicePagePromise implements PromiseLike<VoicePage>, AsyncIterable<Voice> {
748
+ private readonly load;
749
+ private first;
750
+ constructor(load: () => Promise<VoicePage>);
751
+ private page;
752
+ then<A = VoicePage, B = never>(onfulfilled?: ((value: VoicePage) => A | PromiseLike<A>) | null, onrejected?: ((reason: unknown) => B | PromiseLike<B>) | null): Promise<A | B>;
753
+ catch<B = never>(onrejected?: ((reason: unknown) => B | PromiseLike<B>) | null): Promise<VoicePage | B>;
754
+ finally(onfinally?: (() => void) | null): Promise<VoicePage>;
755
+ [Symbol.asyncIterator](): AsyncIterator<Voice>;
756
+ }
757
+ /** Create, list, clone and manage voices. */
758
+ declare class Voices {
759
+ private readonly core;
760
+ private readonly jobs;
761
+ constructor(core: Core, jobs: Jobs);
762
+ /** Your voices (newest first), then the public platform voices.
763
+ * `for await (const voice of client.voices.list())` walks every page;
764
+ * `await client.voices.list()` returns just the first page. */
765
+ list(params?: ListVoicesParams, options?: RequestOptions): VoicePagePromise;
766
+ /** One of your voices or a public platform voice. */
767
+ get(voiceId: string, options?: RequestOptions): Promise<Voice>;
768
+ /** Create an empty `draft` voice. Give it a clip with {@link enroll}, or use {@link clone}. */
769
+ create(params: CreateVoiceParams, options?: RequestOptions): Promise<Voice>;
770
+ /** Change a voice's metadata. Only the fields you pass change. */
771
+ update(voiceId: string, params: UpdateVoiceParams, options?: RequestOptions): Promise<Voice>;
772
+ /** Delete one of your voices. */
773
+ delete(voiceId: string, options?: RequestOptions): Promise<void>;
774
+ /** Create a voice from a reference clip. Returns at once with the voice in `processing` and a
775
+ * `job_id`; see {@link waitUntilReady} or {@link cloneAndWait}. Requires `consent: true`. */
776
+ clone(params: CloneVoiceParams, options?: RequestOptions): Promise<CloneResult>;
777
+ /** Clone a reference clip into an existing voice of yours. Returns at once. Requires
778
+ * `consent: true`. */
779
+ enroll(voiceId: string, params: EnrollVoiceParams, options?: RequestOptions): Promise<CloneResult>;
780
+ /** Wait for a clone job to finish and return the voice, now `ready`. */
781
+ waitUntilReady(clone: CloneResult, options?: WaitOptions): Promise<Voice>;
782
+ /** {@link clone}, then {@link waitUntilReady}. Returns the ready voice. */
783
+ cloneAndWait(params: CloneVoiceParams, options?: WaitOptions): Promise<Voice>;
784
+ }
785
+
786
+ interface FinesseResources {
787
+ voices: Voices;
788
+ jobs: Jobs;
789
+ speech: Speech;
790
+ tokens: Tokens;
791
+ }
792
+ /**
793
+ * Client for the Finesse API.
794
+ *
795
+ * ```ts
796
+ * import { Finesse } from "@flamai/finesse";
797
+ *
798
+ * const client = new Finesse(); // reads FINESSE_API_KEY
799
+ * const audio = await client.speech.create({ voice_id: "665f1a2b3c4d5e6f7a8b9c0d", text: "Hello!" });
800
+ * ```
801
+ */
802
+ declare class Finesse {
803
+ readonly voices: Voices;
804
+ readonly jobs: Jobs;
805
+ readonly speech: Speech;
806
+ readonly tokens: Tokens;
807
+ readonly realtime: Realtime;
808
+ /** @internal */
809
+ readonly _core: Core;
810
+ constructor(options?: ClientOptions | {
811
+ _core: Core;
812
+ });
813
+ get baseURL(): string;
814
+ /** A copy of this client with different defaults. Cheap: it shares everything else. */
815
+ withOptions(options: {
816
+ timeout?: number;
817
+ maxRetries?: number;
818
+ defaultHeaders?: Record<string, string>;
819
+ }): Finesse;
820
+ /** The same calls, each resolving to `{ data, response, status, headers, requestId }`:
821
+ *
822
+ * ```ts
823
+ * const { data: voice, requestId } = await client.withRawResponse.voices.get(id);
824
+ * ```
825
+ */
826
+ get withRawResponse(): RawClient<FinesseResources>;
827
+ }
828
+
829
+ declare const VERSION = "0.1.2";
830
+
831
+ export { APIConnectionError, APIStatusError, APITimeoutError, type AccessToken, type Audio, type AudioChunk, type AudioEvent, AuthenticationError, BadRequestError, type ClientOptions, CloneFailedError, type CloneJob, type CloneResult, CloneTimeoutError, type CloneVoiceParams, ConflictError, type ConnectOptions, type ConsentParams, type ContextOptions, type CreateTokenParams, type CreateVoiceParams, type EnrollVoiceParams, type FileInput, Finesse, FinesseError, InternalServerError, type ListVoicesParams, type LogLevel, type Logger, NotFoundError, type OutputFormat, PayloadTooLargeError, PaymentRequiredError, PermissionDeniedError, RateLimitError, type RawClient, type RawResponse, RealtimeContext, RealtimeError, type RealtimeEvent, RealtimeSession, type RequestOptions, type SentenceDone, type SpeechParams, SpeechStream, type StreamDone, StreamError, type TaskCancelled, type TaskDone, type TaskStarted, type UnknownEvent, UnprocessableEntityError, type UpdateVoiceParams, VERSION, type Voice, VoicePage, VoicePagePromise, type VoiceSettings, type WaitOptions, fileFromPath, isReady };