@flamai/finesse 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -1
- package/README.md +110 -14
- package/dist/index.cjs +107 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +212 -129
- package/dist/index.d.ts +212 -129
- package/dist/index.js +106 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
- Direct realtime sessions: `realtime.sessions.create({ voice_ids, budget_chars })` and
|
|
6
|
+
`realtime.connect({ session })` / `{ getSession }` run the socket straight against the
|
|
7
|
+
synthesis GPU in the account's server region, for lower latency. `RealtimeSession.directSession`
|
|
8
|
+
reports its region and budget.
|
|
9
|
+
- Realtime: on a direct session, close code `4401` is `RealtimeError` with
|
|
10
|
+
`code === "unauthorized"` (the token was invalid, already used or past `connect_by`), not
|
|
11
|
+
`service_auth_failed`. Reusing a spent session throws `session_spent`; one past `connect_by`
|
|
12
|
+
throws `session_expired` without dialing.
|
|
13
|
+
|
|
14
|
+
## 0.1.2
|
|
15
|
+
|
|
16
|
+
- `output_format: "mulaw"` (raw G.711 µ-law) for telephony, with `sample_rate: 8000`, on
|
|
17
|
+
`speech` and realtime contexts. 8 kHz is now produced as asked; the README shows Twilio Media
|
|
18
|
+
Streams.
|
|
19
|
+
|
|
20
|
+
## 0.1.1
|
|
4
21
|
|
|
5
22
|
- Published on npm as `@flamai/finesse` (was private, installed from GitHub).
|
|
6
23
|
- Token clients (`accessToken`) retry only `429` and connections that never opened. The
|
package/README.md
CHANGED
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
</picture>
|
|
6
6
|
|
|
7
7
|
<p>
|
|
8
|
+
<a href="https://www.npmjs.com/package/@flamai/finesse"><img alt="npm version" src="https://img.shields.io/npm/v/@flamai/finesse?style=flat&labelColor=14151A&color=2547F0"></a>
|
|
8
9
|
<img alt="Node 18+" src="https://img.shields.io/badge/node-18%2B-14151A?style=flat&labelColor=14151A&color=2547F0">
|
|
9
10
|
<img alt="Bun and Deno" src="https://img.shields.io/badge/also-Bun%20%C2%B7%20Deno-14151A?style=flat&labelColor=14151A&color=2547F0">
|
|
10
11
|
<img alt="TypeScript types included" src="https://img.shields.io/badge/types-included-14151A?style=flat&labelColor=14151A&color=2547F0">
|
|
11
12
|
<img alt="Status: beta" src="https://img.shields.io/badge/status-beta-14151A?style=flat&labelColor=14151A&color=63656A">
|
|
13
|
+
<a href="./LICENSE"><img alt="License: Apache-2.0" src="https://img.shields.io/badge/license-Apache--2.0-14151A?style=flat&labelColor=14151A&color=63656A"></a>
|
|
12
14
|
</p>
|
|
13
15
|
|
|
14
16
|
# Finesse TypeScript SDK
|
|
@@ -37,6 +39,7 @@ and stream it in realtime to voice agents, with a few lines of code.
|
|
|
37
39
|
- [Text to speech](#text-to-speech)
|
|
38
40
|
- [Streaming speech](#streaming-speech)
|
|
39
41
|
- [Realtime](#realtime)
|
|
42
|
+
- [Direct sessions (lowest latency)](#direct-sessions-lowest-latency)
|
|
40
43
|
- [Browsers and short-lived tokens](#browsers-and-short-lived-tokens)
|
|
41
44
|
- [Which method should I use?](#which-method-should-i-use)
|
|
42
45
|
- [Audio formats](#audio-formats)
|
|
@@ -47,6 +50,11 @@ and stream it in realtime to voice agents, with a few lines of code.
|
|
|
47
50
|
- [Request IDs](#request-ids)
|
|
48
51
|
- [Advanced](#advanced)
|
|
49
52
|
- [Runtimes](#runtimes)
|
|
53
|
+
- [Requirements](#requirements)
|
|
54
|
+
- [Versioning](#versioning)
|
|
55
|
+
- [Getting help](#getting-help)
|
|
56
|
+
- [Security](#security)
|
|
57
|
+
- [Contributing](#contributing)
|
|
50
58
|
|
|
51
59
|
## Installation
|
|
52
60
|
|
|
@@ -56,8 +64,14 @@ Realtime in Node also needs the `ws` package:
|
|
|
56
64
|
npm install @flamai/finesse ws
|
|
57
65
|
# or
|
|
58
66
|
pnpm add @flamai/finesse ws
|
|
67
|
+
# or
|
|
68
|
+
yarn add @flamai/finesse ws
|
|
69
|
+
# or
|
|
70
|
+
bun add @flamai/finesse
|
|
59
71
|
```
|
|
60
72
|
|
|
73
|
+
`ws` is only needed for realtime in Node. Bun, Deno and browsers have a built-in `WebSocket`.
|
|
74
|
+
|
|
61
75
|
## Get an API key
|
|
62
76
|
|
|
63
77
|
1. Sign in to the [Finesse dashboard](https://finesse.flamapp.ai/dashboard).
|
|
@@ -181,7 +195,7 @@ const audio = await client.speech.create({
|
|
|
181
195
|
voice_id: voice.id,
|
|
182
196
|
text: "The quick brown fox jumps over the lazy dog.",
|
|
183
197
|
language: "en", // optional; defaults to the voice's language
|
|
184
|
-
output_format: "wav", // default; or raw PCM: "s16le", "s24le", "s32le", "f32le", "f64le"
|
|
198
|
+
output_format: "wav", // default; or raw PCM: "s16le", "s24le", "s32le", "f32le", "f64le", or "mulaw"
|
|
185
199
|
num_step: 32, // optional; 1-32, default 32; fewer steps is faster
|
|
186
200
|
});
|
|
187
201
|
|
|
@@ -284,6 +298,52 @@ A turn that fails throws `RealtimeError` (for example `code === "voice_not_found
|
|
|
284
298
|
- Text is billed per character as it is sent. A session reserves credits in blocks of up to
|
|
285
299
|
1,000 characters and returns what it doesn't use when it ends.
|
|
286
300
|
|
|
301
|
+
## Direct sessions (lowest latency)
|
|
302
|
+
|
|
303
|
+
A direct session runs the realtime socket **straight against the synthesis GPU** in your
|
|
304
|
+
account's server region, so audio never passes through the API and each sentence starts sooner.
|
|
305
|
+
Contexts, events, cancel and limits work exactly as above.
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
// On a server, with your API key: a new session for the first connection and every reconnect.
|
|
309
|
+
const rt = await client.realtime.connect({
|
|
310
|
+
getSession: () => client.realtime.sessions.create({ voice_ids: [voiceId] }),
|
|
311
|
+
});
|
|
312
|
+
for await (const event of rt.say({ voice_id: voiceId, text: "Hello, straight from the GPU." })) {
|
|
313
|
+
if (event.type === "audio") play(event.pcm);
|
|
314
|
+
}
|
|
315
|
+
console.log(rt.directSession?.region); // "ap-south" or "us-east"
|
|
316
|
+
await rt.close();
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
In a browser, create the session on your server and return it (it holds no API key), then
|
|
320
|
+
connect with it:
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
// On your server
|
|
324
|
+
const session = await client.realtime.sessions.create({ voice_ids: [voiceId] });
|
|
325
|
+
return Response.json(session);
|
|
326
|
+
|
|
327
|
+
// In the browser
|
|
328
|
+
import { connectDirect } from "@flamai/finesse";
|
|
329
|
+
|
|
330
|
+
const getSession = async () => (await fetch("/api/finesse-session", { method: "POST" })).json();
|
|
331
|
+
const rt = await connectDirect({ getSession });
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
How it differs from a session through the API:
|
|
335
|
+
|
|
336
|
+
- **Voices:** only the `voice_ids` you list (1-16). A context for any other voice fails with
|
|
337
|
+
`voice_not_found`; the session keeps running.
|
|
338
|
+
- **Credits:** `budget_chars` (default 20,000) is reserved when the session is created and the
|
|
339
|
+
rest is returned when it ends. Going past it ends the session with `insufficient_credits`.
|
|
340
|
+
- **Lifetime:** connect within 60 s of creating it (`connect_by`), and a session runs at most an
|
|
341
|
+
hour (`ttl_seconds`), then ends with `session_ended`.
|
|
342
|
+
- **Single use:** the session's token works for one connection. With `{ session }` alone, a
|
|
343
|
+
reconnect throws `RealtimeError` with `code === "session_spent"`; pass `getSession` so each
|
|
344
|
+
reconnect gets a new session. A refused token throws `code === "unauthorized"`.
|
|
345
|
+
- **Region:** set by the account that owns the API key, not per request.
|
|
346
|
+
|
|
287
347
|
## Browsers and short-lived tokens
|
|
288
348
|
|
|
289
349
|
Never ship an API key to a browser. Your server mints a short-lived, single-use token with a key
|
|
@@ -331,11 +391,26 @@ outside `voice_ids` throws `PermissionDeniedError` with `code === "voice_not_all
|
|
|
331
391
|
| `s16le` | Raw PCM, 16-bit signed little-endian | `speech.stream`, realtime |
|
|
332
392
|
| `s24le`, `s32le` | Raw PCM, 24- or 32-bit signed | |
|
|
333
393
|
| `f32le`, `f64le` | Raw PCM, 32- or 64-bit float | |
|
|
394
|
+
| `mulaw` | Raw G.711 µ-law, for telephony | needs `sample_rate: 8000` |
|
|
334
395
|
| `opus` | Opus | realtime only |
|
|
335
396
|
|
|
336
|
-
All audio is mono. The engine's native rate is 24 kHz. You can ask for a higher `sample_rate`,
|
|
337
|
-
|
|
338
|
-
`audio.sampleRate`, `stream.sampleRate` or the `task_started` event.
|
|
397
|
+
All audio is mono. The engine's native rate is 24 kHz. You can ask for a higher `sample_rate`, or
|
|
398
|
+
for 8000 (telephony); other lower rates still come back at 24 kHz, so always read the actual rate
|
|
399
|
+
from `audio.sampleRate`, `stream.sampleRate` or the `task_started` event.
|
|
400
|
+
|
|
401
|
+
### Phone calls (Twilio)
|
|
402
|
+
|
|
403
|
+
Twilio Media Streams play 8 kHz µ-law. Ask for exactly that and send each chunk as the media
|
|
404
|
+
payload, base64-encoded; there is no WAV header to strip and nothing to resample:
|
|
405
|
+
|
|
406
|
+
```ts
|
|
407
|
+
const ctx = rt.context({ voice_id: voiceId, output_format: "mulaw", sample_rate: 8000 });
|
|
408
|
+
|
|
409
|
+
for await (const audio of ctx.audio()) {
|
|
410
|
+
const payload = Buffer.from(audio).toString("base64");
|
|
411
|
+
twilioWs.send(JSON.stringify({ event: "media", streamSid, media: { payload } }));
|
|
412
|
+
}
|
|
413
|
+
```
|
|
339
414
|
|
|
340
415
|
## Handling errors
|
|
341
416
|
|
|
@@ -486,22 +561,12 @@ const quick = client.withOptions({ timeout: 10_000, maxRetries: 0 });
|
|
|
486
561
|
await quick.voices.get(voiceId);
|
|
487
562
|
```
|
|
488
563
|
|
|
489
|
-
### Environments
|
|
490
|
-
|
|
491
|
-
The client talks to production by default. Point it elsewhere with `baseURL` or
|
|
492
|
-
`FINESSE_BASE_URL`:
|
|
493
|
-
|
|
494
|
-
```ts
|
|
495
|
-
const client = new Finesse({ baseURL: "https://finesse-proxy.example.com" });
|
|
496
|
-
```
|
|
497
|
-
|
|
498
564
|
### Configuration
|
|
499
565
|
|
|
500
566
|
| Option | Environment variable | Default |
|
|
501
567
|
| ------------------------- | -------------------- | ------------------------------------- |
|
|
502
568
|
| `apiKey` | `FINESSE_API_KEY` | required, unless `accessToken` |
|
|
503
569
|
| `accessToken` | | none (a short-lived `fin_tok_` token) |
|
|
504
|
-
| `baseURL` | `FINESSE_BASE_URL` | `https://fi.production.flamapis.com` |
|
|
505
570
|
| `timeout` | | 120 000 ms |
|
|
506
571
|
| `maxRetries` | | 2 |
|
|
507
572
|
| `defaultHeaders` | | none |
|
|
@@ -510,6 +575,8 @@ const client = new Finesse({ baseURL: "https://finesse-proxy.example.com" });
|
|
|
510
575
|
| `logLevel` | `FINESSE_LOG` | `"off"` |
|
|
511
576
|
| `logger` | | `console` |
|
|
512
577
|
|
|
578
|
+
An option passed to `new Finesse({...})` always wins over its environment variable.
|
|
579
|
+
|
|
513
580
|
### Custom fetch
|
|
514
581
|
|
|
515
582
|
Pass your own `fetch` for proxies, logging or tests:
|
|
@@ -542,11 +609,28 @@ Browsers can't keep an API key secret, so a client holding a key refuses to run
|
|
|
542
609
|
deprecated URL parameter, because browsers can't set WebSocket headers (as does Deno before
|
|
543
610
|
2.5), and logs a warning once.
|
|
544
611
|
|
|
612
|
+
## TypeScript types
|
|
613
|
+
|
|
614
|
+
Every request and response type is exported, so you can type your own code against the API:
|
|
615
|
+
|
|
616
|
+
```ts
|
|
617
|
+
import type { Voice } from "@flamai/finesse";
|
|
618
|
+
|
|
619
|
+
function label(voice: Voice) {
|
|
620
|
+
return `${voice.name} (${voice.id})`;
|
|
621
|
+
}
|
|
622
|
+
```
|
|
623
|
+
|
|
545
624
|
## Examples
|
|
546
625
|
|
|
547
626
|
Runnable scripts are in [`examples/`](./examples): [`speak.ts`](./examples/speak.ts) and
|
|
548
627
|
[`realtime.ts`](./examples/realtime.ts).
|
|
549
628
|
|
|
629
|
+
## Requirements
|
|
630
|
+
|
|
631
|
+
- Node 18 or later, Bun, Deno, or a modern browser (see [Runtimes](#runtimes)).
|
|
632
|
+
- TypeScript 4.7 or later, if you use TypeScript.
|
|
633
|
+
|
|
550
634
|
## Versioning
|
|
551
635
|
|
|
552
636
|
The SDK follows [semantic versioning](https://semver.org). Before 1.0, minor versions may
|
|
@@ -556,6 +640,18 @@ include breaking changes; they are listed in the [changelog](./CHANGELOG.md).
|
|
|
556
640
|
import { VERSION } from "@flamai/finesse";
|
|
557
641
|
```
|
|
558
642
|
|
|
643
|
+
## Getting help
|
|
644
|
+
|
|
645
|
+
- [Documentation](https://docs.finesse.flamapp.ai) for guides and the API reference.
|
|
646
|
+
- [GitHub issues](https://github.com/homingos/finesse-node/issues) for bugs and feature requests
|
|
647
|
+
in this SDK. Include the `requestId` from the error (see [Request IDs](#request-ids)) when
|
|
648
|
+
a request failed.
|
|
649
|
+
|
|
650
|
+
## Security
|
|
651
|
+
|
|
652
|
+
Please don't report security problems in a public issue. See [SECURITY.md](./SECURITY.md) for
|
|
653
|
+
how to report one privately.
|
|
654
|
+
|
|
559
655
|
## Contributing
|
|
560
656
|
|
|
561
657
|
```sh
|
package/dist/index.cjs
CHANGED
|
@@ -55,6 +55,7 @@ __export(index_exports, {
|
|
|
55
55
|
VERSION: () => VERSION,
|
|
56
56
|
VoicePage: () => VoicePage,
|
|
57
57
|
VoicePagePromise: () => VoicePagePromise,
|
|
58
|
+
connectDirect: () => connectDirect,
|
|
58
59
|
fileFromPath: () => fileFromPath,
|
|
59
60
|
isReady: () => isReady
|
|
60
61
|
});
|
|
@@ -225,7 +226,7 @@ function numberField(body2, key) {
|
|
|
225
226
|
}
|
|
226
227
|
|
|
227
228
|
// src/version.ts
|
|
228
|
-
var VERSION = "0.
|
|
229
|
+
var VERSION = "0.2.0";
|
|
229
230
|
|
|
230
231
|
// src/core.ts
|
|
231
232
|
var DEFAULT_BASE_URL = "https://fi.production.flamapis.com";
|
|
@@ -588,7 +589,8 @@ var RealtimeError = class extends FinesseError {
|
|
|
588
589
|
* voices), `invalid_request` (a parameter out of range), a `task_failed` code. Whole session:
|
|
589
590
|
* `insufficient_credits`, `upstream_unavailable` (the engine could not be reached),
|
|
590
591
|
* `upstream_busy` (it is at capacity), `upstream_error`, `upstream_closed`,
|
|
591
|
-
* `connection_lost`, `token_spent`,
|
|
592
|
+
* `connection_lost`, `token_spent`, `session_spent`, `session_expired`, `unauthorized` (a
|
|
593
|
+
* direct session's token was refused), ... */
|
|
592
594
|
code;
|
|
593
595
|
/** The context it applies to, or `null` for the whole session. */
|
|
594
596
|
contextId;
|
|
@@ -605,8 +607,15 @@ var RealtimeError = class extends FinesseError {
|
|
|
605
607
|
return RETRYABLE_CODES.has(this.code) || this.closeCode !== null && RETRYABLE_CLOSES.has(this.closeCode);
|
|
606
608
|
}
|
|
607
609
|
};
|
|
608
|
-
function closeError(code, reason) {
|
|
610
|
+
function closeError(code, reason, direct = false) {
|
|
609
611
|
const c = { closeCode: code };
|
|
612
|
+
if (direct && code === 4401) {
|
|
613
|
+
return new RealtimeError(
|
|
614
|
+
"unauthorized",
|
|
615
|
+
"The session token was refused (invalid, already used, or past connect_by). Create a new session.",
|
|
616
|
+
c
|
|
617
|
+
);
|
|
618
|
+
}
|
|
610
619
|
if (code === 1e3) return null;
|
|
611
620
|
if (code === 1008) return new RealtimeError(reason || "policy_violation", reason, c);
|
|
612
621
|
if (code === 1009) return new RealtimeError("message_too_big", "A message was too large.", c);
|
|
@@ -822,7 +831,13 @@ var RealtimeSession = class {
|
|
|
822
831
|
constructor(core, options = {}) {
|
|
823
832
|
this.core = core;
|
|
824
833
|
this.options = options;
|
|
825
|
-
this.
|
|
834
|
+
this.directMode = Boolean(options.session || options.getSession);
|
|
835
|
+
if (this.directMode && (options.accessToken || options.getAccessToken)) {
|
|
836
|
+
throw new FinesseError(
|
|
837
|
+
"Pass { session } / { getSession } or { accessToken } / { getAccessToken }, not both."
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
this.staticToken = this.directMode ? null : options.accessToken ?? core.accessToken;
|
|
826
841
|
}
|
|
827
842
|
core;
|
|
828
843
|
options;
|
|
@@ -837,9 +852,19 @@ var RealtimeSession = class {
|
|
|
837
852
|
closedWaiters = [];
|
|
838
853
|
staticToken;
|
|
839
854
|
tokenSpent = false;
|
|
855
|
+
directMode;
|
|
856
|
+
staticSessionSpent = false;
|
|
857
|
+
current = null;
|
|
858
|
+
/** Where the socket connects: the API's `/stream`, or a direct session's GPU URL. */
|
|
840
859
|
get url() {
|
|
860
|
+
if (this.directMode) return this.current?.url ?? this.options.session?.url ?? "";
|
|
841
861
|
return `${this.core.wsBaseURL}${API_PREFIX}/stream`;
|
|
842
862
|
}
|
|
863
|
+
/** The direct session the socket runs on (its region and budget), or `null` when the session
|
|
864
|
+
* goes through the API. */
|
|
865
|
+
get directSession() {
|
|
866
|
+
return this.current;
|
|
867
|
+
}
|
|
843
868
|
get isOpen() {
|
|
844
869
|
return this.ws !== null && this.ws.readyState === OPEN;
|
|
845
870
|
}
|
|
@@ -973,6 +998,31 @@ var RealtimeSession = class {
|
|
|
973
998
|
}
|
|
974
999
|
return null;
|
|
975
1000
|
}
|
|
1001
|
+
/** The direct session for the next handshake. Like a token, a static session is used first;
|
|
1002
|
+
* once a handshake has spent it, `getSession` supplies one per reconnect. */
|
|
1003
|
+
async nextSession() {
|
|
1004
|
+
const fixed = this.options.session;
|
|
1005
|
+
if (fixed && !this.staticSessionSpent) {
|
|
1006
|
+
if (Date.parse(fixed.connect_by) <= Date.now()) {
|
|
1007
|
+
throw new RealtimeError(
|
|
1008
|
+
"session_expired",
|
|
1009
|
+
"This direct session's connect_by has passed. Create a new session."
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
return fixed;
|
|
1013
|
+
}
|
|
1014
|
+
if (this.options.getSession) {
|
|
1015
|
+
const s = await this.options.getSession();
|
|
1016
|
+
if (!s || typeof s.url !== "string" || !s.url || typeof s.token !== "string" || !s.token) {
|
|
1017
|
+
throw new FinesseError("getSession must return a session with a url and a token.");
|
|
1018
|
+
}
|
|
1019
|
+
return s;
|
|
1020
|
+
}
|
|
1021
|
+
throw new RealtimeError(
|
|
1022
|
+
"session_spent",
|
|
1023
|
+
"This direct session was used by an earlier connection. Pass { getSession } so a reconnect can create a new one."
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
976
1026
|
async dial() {
|
|
977
1027
|
const timeout = this.options.openTimeout ?? DEFAULT_OPEN_TIMEOUT_MS;
|
|
978
1028
|
const custom = this.options.WebSocket;
|
|
@@ -980,11 +1030,21 @@ var RealtimeSession = class {
|
|
|
980
1030
|
headers: !isBrowser(),
|
|
981
1031
|
open: (url2, h, t, protocols2) => h ? new custom(url2, protocols2, { headers: h, handshakeTimeout: t }) : new custom(url2, protocols2)
|
|
982
1032
|
} : await defaultDialer();
|
|
983
|
-
const
|
|
984
|
-
const
|
|
1033
|
+
const direct = this.directMode ? await this.nextSession() : null;
|
|
1034
|
+
const token = direct ? null : await this.nextToken();
|
|
1035
|
+
let protocols = token ? [SUBPROTOCOL, token] : void 0;
|
|
985
1036
|
let url = this.url;
|
|
986
1037
|
let headers = null;
|
|
987
|
-
if (
|
|
1038
|
+
if (direct) {
|
|
1039
|
+
protocols = [direct.protocol || SUBPROTOCOL, direct.token];
|
|
1040
|
+
url = direct.url;
|
|
1041
|
+
if (dialer.headers) {
|
|
1042
|
+
headers = { ...this.core.headers };
|
|
1043
|
+
delete headers.Accept;
|
|
1044
|
+
delete headers["X-API-Key"];
|
|
1045
|
+
delete headers.Authorization;
|
|
1046
|
+
}
|
|
1047
|
+
} else if (token) {
|
|
988
1048
|
if (dialer.headers) {
|
|
989
1049
|
headers = { ...this.core.headers };
|
|
990
1050
|
delete headers.Accept;
|
|
@@ -1043,6 +1103,10 @@ var RealtimeSession = class {
|
|
|
1043
1103
|
throw new RealtimeError("session_closed", "This session was closed.");
|
|
1044
1104
|
}
|
|
1045
1105
|
if (token && token === this.staticToken) this.tokenSpent = true;
|
|
1106
|
+
if (direct) {
|
|
1107
|
+
if (direct === this.options.session) this.staticSessionSpent = true;
|
|
1108
|
+
this.current = direct;
|
|
1109
|
+
}
|
|
1046
1110
|
this.ws = ws;
|
|
1047
1111
|
this.pendingAudio = null;
|
|
1048
1112
|
this.fatal = null;
|
|
@@ -1061,14 +1125,14 @@ var RealtimeSession = class {
|
|
|
1061
1125
|
}
|
|
1062
1126
|
onClose(ws, code, reason) {
|
|
1063
1127
|
if (this.ws !== null && this.ws !== ws) {
|
|
1064
|
-
const err = closeError(code, reason ?? "") ?? new RealtimeError("connection_closed");
|
|
1128
|
+
const err = closeError(code, reason ?? "", this.directMode) ?? new RealtimeError("connection_closed");
|
|
1065
1129
|
for (const ctx of [...this.contexts.values()]) if (ctx.socket === ws) ctx._fail(err);
|
|
1066
1130
|
return;
|
|
1067
1131
|
}
|
|
1068
1132
|
this.ws = null;
|
|
1069
1133
|
let error = this.fatal;
|
|
1070
1134
|
if (error) error.closeCode ??= code;
|
|
1071
|
-
else error = closeError(code, reason ?? "");
|
|
1135
|
+
else error = closeError(code, reason ?? "", this.directMode);
|
|
1072
1136
|
if (!error && this.contexts.size) {
|
|
1073
1137
|
error = new RealtimeError("connection_closed", "The session ended.", { closeCode: code });
|
|
1074
1138
|
}
|
|
@@ -1135,11 +1199,35 @@ var RealtimeSession = class {
|
|
|
1135
1199
|
);
|
|
1136
1200
|
}
|
|
1137
1201
|
};
|
|
1202
|
+
var DirectSessions = class {
|
|
1203
|
+
constructor(core) {
|
|
1204
|
+
this.core = core;
|
|
1205
|
+
}
|
|
1206
|
+
core;
|
|
1207
|
+
/** Create a direct session. Call this on your server with an API key that has the `tts`
|
|
1208
|
+
* scope; it reserves `budget_chars` from your balance now. Connect within 60 s
|
|
1209
|
+
* (`connect_by`), with `realtime.connect({ session })`, or hand `url`, `protocol` and `token`
|
|
1210
|
+
* to a browser:
|
|
1211
|
+
*
|
|
1212
|
+
* ```ts
|
|
1213
|
+
* const session = await client.realtime.sessions.create({ voice_ids: [voiceId] });
|
|
1214
|
+
* const rt = await client.realtime.connect({ session });
|
|
1215
|
+
* ```
|
|
1216
|
+
*/
|
|
1217
|
+
async create(params, options = {}) {
|
|
1218
|
+
const json = { voice_ids: params.voice_ids };
|
|
1219
|
+
if (params.budget_chars !== void 0) json.budget_chars = params.budget_chars;
|
|
1220
|
+
return this.core.json("POST", "/realtime/sessions", { ...options, json });
|
|
1221
|
+
}
|
|
1222
|
+
};
|
|
1138
1223
|
var Realtime = class {
|
|
1139
1224
|
constructor(core) {
|
|
1140
1225
|
this.core = core;
|
|
1226
|
+
this.sessions = new DirectSessions(core);
|
|
1141
1227
|
}
|
|
1142
1228
|
core;
|
|
1229
|
+
/** Direct sessions: lower latency, straight to the GPU. */
|
|
1230
|
+
sessions;
|
|
1143
1231
|
/** Open a session. */
|
|
1144
1232
|
async connect(options = {}) {
|
|
1145
1233
|
const session = new RealtimeSession(this.core, options);
|
|
@@ -1151,6 +1239,15 @@ var Realtime = class {
|
|
|
1151
1239
|
return new RealtimeSession(this.core, options);
|
|
1152
1240
|
}
|
|
1153
1241
|
};
|
|
1242
|
+
async function connectDirect(options) {
|
|
1243
|
+
if (!options.session && !options.getSession) {
|
|
1244
|
+
throw new FinesseError("connectDirect needs { session } or { getSession }.");
|
|
1245
|
+
}
|
|
1246
|
+
const core = new Core({ accessToken: "direct-session-only" });
|
|
1247
|
+
const session = new RealtimeSession(core, options);
|
|
1248
|
+
await session.open();
|
|
1249
|
+
return session;
|
|
1250
|
+
}
|
|
1154
1251
|
var warnedLoggers = /* @__PURE__ */ new WeakSet();
|
|
1155
1252
|
function warnQueryKey(logger) {
|
|
1156
1253
|
if (warnedLoggers.has(logger)) return;
|
|
@@ -1779,6 +1876,7 @@ var Finesse = class _Finesse {
|
|
|
1779
1876
|
VERSION,
|
|
1780
1877
|
VoicePage,
|
|
1781
1878
|
VoicePagePromise,
|
|
1879
|
+
connectDirect,
|
|
1782
1880
|
fileFromPath,
|
|
1783
1881
|
isReady
|
|
1784
1882
|
});
|