kugelaudio 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +41 -3
- package/dist/chunk-CB3B6T7F.mjs +391 -0
- package/dist/chunk-D57AKD2S.mjs +391 -0
- package/dist/index.d.mts +200 -16
- package/dist/index.d.ts +200 -16
- package/dist/index.js +287 -23
- package/dist/index.mjs +276 -347
- package/dist/livekit/index.d.mts +222 -0
- package/dist/livekit/index.d.ts +222 -0
- package/dist/livekit/index.js +994 -0
- package/dist/livekit/index.mjs +746 -0
- package/package.json +23 -3
- package/src/cfg-scale.test.ts +33 -0
- package/src/client.test.ts +466 -2
- package/src/client.ts +308 -17
- package/src/index.ts +4 -0
- package/src/livekit/index.ts +32 -0
- package/src/livekit/models.test.ts +30 -0
- package/src/livekit/models.ts +84 -0
- package/src/livekit/tts.functional.test.ts +348 -0
- package/src/livekit/tts.ts +833 -0
- package/src/livekit/wire.test.ts +112 -0
- package/src/livekit/wire.ts +126 -0
- package/src/types.ts +171 -13
- package/src/utils.ts +16 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
appendSdkQuery,
|
|
4
|
+
buildClosePayload,
|
|
5
|
+
buildMultiWsUrl,
|
|
6
|
+
buildTextPayload,
|
|
7
|
+
wordTimestampsToTimed,
|
|
8
|
+
type WireOptions,
|
|
9
|
+
} from './wire';
|
|
10
|
+
|
|
11
|
+
const opts: WireOptions = {
|
|
12
|
+
model: 'kugel-3',
|
|
13
|
+
voiceId: 1071,
|
|
14
|
+
sampleRate: 24000,
|
|
15
|
+
cfgScale: 2.0,
|
|
16
|
+
maxNewTokens: 2048,
|
|
17
|
+
wordTimestamps: false,
|
|
18
|
+
normalize: true,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
describe('buildMultiWsUrl', () => {
|
|
22
|
+
it('rewrites https to wss and targets /ws/tts/multi with the api key', () => {
|
|
23
|
+
const url = buildMultiWsUrl('https://api.kugelaudio.com', 'secret');
|
|
24
|
+
expect(url).toContain('wss://api.kugelaudio.com/ws/tts/multi');
|
|
25
|
+
expect(url).toContain('api_key=secret');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('rewrites http to ws for local dev', () => {
|
|
29
|
+
expect(buildMultiWsUrl('http://localhost:8000', 'k')).toContain('ws://localhost:8000/ws/tts/multi');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('appends sdk telemetry params', () => {
|
|
33
|
+
const url = buildMultiWsUrl('https://api.kugelaudio.com', 'k');
|
|
34
|
+
expect(url).toContain('sdk=js');
|
|
35
|
+
expect(url).toContain('sdk_version=');
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('appendSdkQuery', () => {
|
|
40
|
+
it('uses ? for the first param and & thereafter', () => {
|
|
41
|
+
expect(appendSdkQuery('https://x/y')).toContain('?sdk=js');
|
|
42
|
+
expect(appendSdkQuery('https://x/y?a=b')).toContain('&sdk=js');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('buildTextPayload', () => {
|
|
47
|
+
it('sends a bare text frame when config is not requested', () => {
|
|
48
|
+
const msg = buildTextPayload('ctx1', 'hello', opts);
|
|
49
|
+
expect(msg).toEqual({ text: 'hello', context_id: 'ctx1' });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('marks flush when requested', () => {
|
|
53
|
+
expect(buildTextPayload('ctx1', 'hi', opts, { flush: true })).toMatchObject({ flush: true });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('attaches session + voice config on the first frame', () => {
|
|
57
|
+
const msg = buildTextPayload('ctx1', 'hi', opts, { includeConfig: true });
|
|
58
|
+
expect(msg).toMatchObject({
|
|
59
|
+
model_id: 'kugel-3',
|
|
60
|
+
sample_rate: 24000,
|
|
61
|
+
word_timestamps: false,
|
|
62
|
+
normalize: true,
|
|
63
|
+
voice_settings: { voice_id: 1071 },
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('omits default generation params from voice_settings', () => {
|
|
68
|
+
const msg = buildTextPayload('ctx1', 'hi', opts, { includeConfig: true });
|
|
69
|
+
expect(msg.voice_settings).toEqual({ voice_id: 1071 });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('includes non-default cfg_scale and max_new_tokens', () => {
|
|
73
|
+
const msg = buildTextPayload('ctx1', 'hi', { ...opts, cfgScale: 1.5, maxNewTokens: 4096 }, {
|
|
74
|
+
includeConfig: true,
|
|
75
|
+
});
|
|
76
|
+
expect(msg.voice_settings).toMatchObject({ cfg_scale: 1.5, max_new_tokens: 4096 });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('omits voice_settings entirely when the voice is the server default', () => {
|
|
80
|
+
const msg = buildTextPayload('ctx1', 'hi', { ...opts, voiceId: null }, { includeConfig: true });
|
|
81
|
+
expect(msg).not.toHaveProperty('voice_settings');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('includes language only when set', () => {
|
|
85
|
+
expect(buildTextPayload('c', 'x', opts, { includeConfig: true })).not.toHaveProperty('language');
|
|
86
|
+
const withLang = buildTextPayload('c', 'x', { ...opts, language: 'de' }, { includeConfig: true });
|
|
87
|
+
expect(withLang).toMatchObject({ language: 'de' });
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('buildClosePayload', () => {
|
|
92
|
+
it('drains by default', () => {
|
|
93
|
+
expect(buildClosePayload('ctx1')).toEqual({ close_context: true, context_id: 'ctx1' });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('sets immediate for barge-in', () => {
|
|
97
|
+
expect(buildClosePayload('ctx1', true)).toMatchObject({ immediate: true });
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('wordTimestampsToTimed', () => {
|
|
102
|
+
it('converts server milliseconds to seconds', () => {
|
|
103
|
+
const timed = wordTimestampsToTimed([
|
|
104
|
+
{ word: 'Hallo', start_ms: 0, end_ms: 500 },
|
|
105
|
+
{ word: 'Welt', start_ms: 500, end_ms: 1200 },
|
|
106
|
+
]);
|
|
107
|
+
expect(timed).toEqual([
|
|
108
|
+
{ text: 'Hallo', startTime: 0, endTime: 0.5 },
|
|
109
|
+
{ text: 'Welt', startTime: 0.5, endTime: 1.2 },
|
|
110
|
+
]);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure wire-protocol helpers for the KugelAudio `/ws/tts/multi` endpoint used
|
|
3
|
+
* by the LiveKit plugin.
|
|
4
|
+
*
|
|
5
|
+
* Kept free of any `@livekit/agents` import so the message-shaping logic can be
|
|
6
|
+
* unit-tested without the LiveKit runtime. Mirrors the send loop of
|
|
7
|
+
* `kugelaudio.livekit.tts._Connection` in the Python SDK.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import packageJson from '../../package.json';
|
|
11
|
+
|
|
12
|
+
const SDK_NAME = 'js';
|
|
13
|
+
const SDK_VERSION = packageJson.version;
|
|
14
|
+
|
|
15
|
+
/** Generation parameters carried on the first message of each context. */
|
|
16
|
+
export interface WireOptions {
|
|
17
|
+
model: string;
|
|
18
|
+
voiceId: number | null;
|
|
19
|
+
sampleRate: number;
|
|
20
|
+
cfgScale: number;
|
|
21
|
+
maxNewTokens: number;
|
|
22
|
+
wordTimestamps: boolean;
|
|
23
|
+
normalize: boolean;
|
|
24
|
+
language?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Append the `sdk` / `sdk_version` query params used for server-side telemetry. */
|
|
28
|
+
export function appendSdkQuery(url: string): string {
|
|
29
|
+
const separator = url.includes('?') ? '&' : '?';
|
|
30
|
+
return (
|
|
31
|
+
`${url}${separator}sdk=${encodeURIComponent(SDK_NAME)}` +
|
|
32
|
+
`&sdk_version=${encodeURIComponent(SDK_VERSION)}`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Build the `wss://.../ws/tts/multi` URL from an `https://`/`http://` base URL.
|
|
38
|
+
* The API key is passed as a query parameter, matching the Python plugin.
|
|
39
|
+
*/
|
|
40
|
+
export function buildMultiWsUrl(baseUrl: string, apiKey: string): string {
|
|
41
|
+
const wsBase = baseUrl.replace('https://', 'wss://').replace('http://', 'ws://');
|
|
42
|
+
return appendSdkQuery(`${wsBase}/ws/tts/multi?api_key=${apiKey}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Build the JSON payload for a text message to a context.
|
|
47
|
+
*
|
|
48
|
+
* When `includeConfig` is true (the first message sent for a context), the
|
|
49
|
+
* session + voice configuration is attached, exactly as the Python plugin does
|
|
50
|
+
* on the first frame of a new `context_id`.
|
|
51
|
+
*/
|
|
52
|
+
export function buildTextPayload(
|
|
53
|
+
contextId: string,
|
|
54
|
+
text: string,
|
|
55
|
+
opts: WireOptions,
|
|
56
|
+
{ flush = false, includeConfig = false }: { flush?: boolean; includeConfig?: boolean } = {},
|
|
57
|
+
): Record<string, unknown> {
|
|
58
|
+
const msg: Record<string, unknown> = { text, context_id: contextId };
|
|
59
|
+
if (flush) msg.flush = true;
|
|
60
|
+
|
|
61
|
+
if (includeConfig) {
|
|
62
|
+
msg.model_id = opts.model;
|
|
63
|
+
msg.sample_rate = opts.sampleRate;
|
|
64
|
+
msg.word_timestamps = opts.wordTimestamps;
|
|
65
|
+
msg.normalize = opts.normalize;
|
|
66
|
+
if (opts.language !== undefined) msg.language = opts.language;
|
|
67
|
+
|
|
68
|
+
const voiceSettings: Record<string, unknown> = {};
|
|
69
|
+
if (opts.voiceId !== null && opts.voiceId !== undefined) {
|
|
70
|
+
voiceSettings.voice_id = opts.voiceId;
|
|
71
|
+
}
|
|
72
|
+
// Only send non-default generation params, mirroring the Python plugin.
|
|
73
|
+
if (opts.cfgScale !== 2.0) voiceSettings.cfg_scale = opts.cfgScale;
|
|
74
|
+
if (opts.maxNewTokens !== 2048) voiceSettings.max_new_tokens = opts.maxNewTokens;
|
|
75
|
+
if (Object.keys(voiceSettings).length > 0) msg.voice_settings = voiceSettings;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return msg;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Build the JSON payload that closes a context.
|
|
83
|
+
*
|
|
84
|
+
* `immediate` cancels in-flight generation (barge-in); omitting it drains any
|
|
85
|
+
* queued audio first.
|
|
86
|
+
*/
|
|
87
|
+
export function buildClosePayload(
|
|
88
|
+
contextId: string,
|
|
89
|
+
immediate = false,
|
|
90
|
+
): Record<string, unknown> {
|
|
91
|
+
const msg: Record<string, unknown> = {
|
|
92
|
+
close_context: true,
|
|
93
|
+
context_id: contextId,
|
|
94
|
+
};
|
|
95
|
+
if (immediate) msg.immediate = true;
|
|
96
|
+
return msg;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** A single word-timing entry as delivered by the server (`start_ms`/`end_ms`). */
|
|
100
|
+
export interface RawWordTimestamp {
|
|
101
|
+
word: string;
|
|
102
|
+
start_ms: number;
|
|
103
|
+
end_ms: number;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** A framework-agnostic word timing in seconds. */
|
|
107
|
+
export interface TimedWord {
|
|
108
|
+
text: string;
|
|
109
|
+
startTime: number;
|
|
110
|
+
endTime: number;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Convert the server's `word_timestamps` payload (integer milliseconds) into
|
|
115
|
+
* seconds-based {@link TimedWord}s, matching the Python plugin's
|
|
116
|
+
* `_word_timestamps_to_timed`.
|
|
117
|
+
*/
|
|
118
|
+
export function wordTimestampsToTimed(
|
|
119
|
+
timestamps: RawWordTimestamp[],
|
|
120
|
+
): TimedWord[] {
|
|
121
|
+
return timestamps.map((ts) => ({
|
|
122
|
+
text: ts.word,
|
|
123
|
+
startTime: ts.start_ms / 1000,
|
|
124
|
+
endTime: ts.end_ms / 1000,
|
|
125
|
+
}));
|
|
126
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -39,6 +39,7 @@ export interface Voice {
|
|
|
39
39
|
category?: VoiceCategory;
|
|
40
40
|
sex?: VoiceSex;
|
|
41
41
|
age?: VoiceAge;
|
|
42
|
+
quality?: string;
|
|
42
43
|
supportedLanguages: string[];
|
|
43
44
|
sampleText?: string;
|
|
44
45
|
avatarUrl?: string;
|
|
@@ -244,7 +245,7 @@ export interface GenerateOptions {
|
|
|
244
245
|
modelId?: string;
|
|
245
246
|
/** Voice ID to use */
|
|
246
247
|
voiceId?: number;
|
|
247
|
-
/**
|
|
248
|
+
/** Classifier-free guidance scale. Clamped to [1.2, 2.5] (default: 2.0). */
|
|
248
249
|
cfgScale?: number;
|
|
249
250
|
/**
|
|
250
251
|
* Sampling variance. Range [0.0, 1.0]. 0 = most stable (near-greedy),
|
|
@@ -258,7 +259,13 @@ export interface GenerateOptions {
|
|
|
258
259
|
maxNewTokens?: number;
|
|
259
260
|
/** Output sample rate (default: 24000) */
|
|
260
261
|
sampleRate?: number;
|
|
261
|
-
/**
|
|
262
|
+
/**
|
|
263
|
+
* Combined codec+rate token, e.g. 'ulaw_8000' / 'alaw_8000' / 'pcm_8000'.
|
|
264
|
+
* Opt-in; when set it is authoritative and must not contradict sampleRate.
|
|
265
|
+
* Absent ⇒ legacy PCM16 at sampleRate.
|
|
266
|
+
*/
|
|
267
|
+
outputFormat?: string;
|
|
268
|
+
/**
|
|
262
269
|
* Enable text normalization (converts numbers, dates, etc. to spoken words).
|
|
263
270
|
* When true, text will be normalized before TTS generation.
|
|
264
271
|
* Default: true
|
|
@@ -286,8 +293,9 @@ export interface GenerateOptions {
|
|
|
286
293
|
/**
|
|
287
294
|
* Playback speed multiplier (0.8 = slower, 1.0 = normal, 1.2 = faster).
|
|
288
295
|
*
|
|
289
|
-
* Uses pitch-preserving time-stretching (WSOLA)
|
|
290
|
-
*
|
|
296
|
+
* Uses pitch-preserving time-stretching (WSOLA); applies to the whole
|
|
297
|
+
* request. Wrap text in `<prosody rate="slow|medium|fast|0.8-1.2">` to
|
|
298
|
+
* override the rate for a span (the span rate wins inside the span).
|
|
291
299
|
* Range: [0.8, 1.2]. Default: 1.0.
|
|
292
300
|
*/
|
|
293
301
|
speed?: number;
|
|
@@ -298,6 +306,14 @@ export interface GenerateOptions {
|
|
|
298
306
|
* server treats the value as trusted once received.
|
|
299
307
|
*/
|
|
300
308
|
projectId?: number;
|
|
309
|
+
/**
|
|
310
|
+
* Per-request dictionary selection. Omit for the default behavior (all
|
|
311
|
+
* active dictionaries of the project apply, filtered by language). An
|
|
312
|
+
* empty array disables dictionaries for this request. A list of
|
|
313
|
+
* dictionary IDs applies exactly those dictionaries — including
|
|
314
|
+
* inactive ones — bypassing the language filter.
|
|
315
|
+
*/
|
|
316
|
+
dictionaryIds?: number[];
|
|
301
317
|
}
|
|
302
318
|
|
|
303
319
|
/**
|
|
@@ -322,7 +338,7 @@ export interface StreamConfig {
|
|
|
322
338
|
voiceId?: number;
|
|
323
339
|
/** Model ID. Default: 'kugel-3'. Legacy ids still accepted; they alias to kugel-3 server-side. */
|
|
324
340
|
modelId?: string;
|
|
325
|
-
/**
|
|
341
|
+
/** Classifier-free guidance scale. Clamped to [1.2, 2.5] (default: 2.0). */
|
|
326
342
|
cfgScale?: number;
|
|
327
343
|
/**
|
|
328
344
|
* Sampling variance. Range [0.0, 1.0]. 0 = most stable, 1 = most variance.
|
|
@@ -333,6 +349,8 @@ export interface StreamConfig {
|
|
|
333
349
|
maxNewTokens?: number;
|
|
334
350
|
/** Output sample rate */
|
|
335
351
|
sampleRate?: number;
|
|
352
|
+
/** Combined codec+rate token (e.g. 'ulaw_8000'); opt-in, set-once per session. */
|
|
353
|
+
outputFormat?: string;
|
|
336
354
|
/** Auto-flush timeout in milliseconds */
|
|
337
355
|
flushTimeoutMs?: number;
|
|
338
356
|
/** Maximum buffer length */
|
|
@@ -377,11 +395,57 @@ export interface StreamConfig {
|
|
|
377
395
|
/**
|
|
378
396
|
* Playback speed multiplier (0.8 = slower, 1.0 = normal, 1.2 = faster).
|
|
379
397
|
*
|
|
380
|
-
* Uses pitch-preserving time-stretching (WSOLA)
|
|
381
|
-
*
|
|
398
|
+
* Uses pitch-preserving time-stretching (WSOLA); applies to the whole
|
|
399
|
+
* request. Wrap text in `<prosody rate="slow|medium|fast|0.8-1.2">` to
|
|
400
|
+
* override the rate for a span (the span rate wins inside the span).
|
|
382
401
|
* Range: [0.8, 1.2]. Default: 1.0.
|
|
383
402
|
*/
|
|
384
403
|
speed?: number;
|
|
404
|
+
/**
|
|
405
|
+
* Per-request dictionary selection. Omit for the default behavior (all
|
|
406
|
+
* active dictionaries of the project apply, filtered by language). An
|
|
407
|
+
* empty array disables dictionaries for this request. A list of
|
|
408
|
+
* dictionary IDs applies exactly those dictionaries — including
|
|
409
|
+
* inactive ones — bypassing the language filter.
|
|
410
|
+
*/
|
|
411
|
+
dictionaryIds?: number[];
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Generation parameters changeable mid-connection via
|
|
416
|
+
* {@link StreamingSession.updateSettings} / {@link MultiContextSession.updateSettings}
|
|
417
|
+
* (KUG-1166). Every field is optional; an update changes only the fields it
|
|
418
|
+
* carries. Identity / audio-format fields (`voiceId`, `modelId`, `sampleRate`,
|
|
419
|
+
* `outputFormat`, `dictionaryIds`) are NOT here — they are fixed for the
|
|
420
|
+
* connection's lifetime and the server rejects them in an update.
|
|
421
|
+
*/
|
|
422
|
+
export interface SettingsUpdate {
|
|
423
|
+
/** Classifier-free guidance scale (0.0–10.0). */
|
|
424
|
+
cfgScale?: number;
|
|
425
|
+
/** Sampling variance (0.0–1.0). */
|
|
426
|
+
temperature?: number;
|
|
427
|
+
/** Playback speed multiplier (0.8–1.2). */
|
|
428
|
+
speed?: number;
|
|
429
|
+
/** Maximum tokens per generation (1–8192). */
|
|
430
|
+
maxNewTokens?: number;
|
|
431
|
+
/** Language code for normalization (e.g. `'de'`). */
|
|
432
|
+
language?: string;
|
|
433
|
+
/** Enable text normalization. */
|
|
434
|
+
normalize?: boolean;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* The generation parameters in effect after an
|
|
439
|
+
* {@link StreamingSession.updateSettings} call — the server's echo. Fields the
|
|
440
|
+
* session never set come back `null` (e.g. `temperature`, `language`).
|
|
441
|
+
*/
|
|
442
|
+
export interface EffectiveSettings {
|
|
443
|
+
cfgScale?: number;
|
|
444
|
+
temperature?: number | null;
|
|
445
|
+
speed?: number;
|
|
446
|
+
maxNewTokens?: number;
|
|
447
|
+
language?: string | null;
|
|
448
|
+
normalize?: boolean;
|
|
385
449
|
}
|
|
386
450
|
|
|
387
451
|
/**
|
|
@@ -399,9 +463,18 @@ export interface StreamingSessionCallbacks {
|
|
|
399
463
|
* Carries the segment index, total audio duration, and generation time.
|
|
400
464
|
*/
|
|
401
465
|
onChunkComplete?: (chunkId: number, audioSeconds: number, genMs: number) => void;
|
|
466
|
+
/**
|
|
467
|
+
* Called when the server marks the end of a turn's audio
|
|
468
|
+
* (`{"final": true, ...}` — sent after the last audio frame of every
|
|
469
|
+
* gracefully completed turn, right before `session_closed`). The
|
|
470
|
+
* ElevenLabs `isFinal` equivalent: once this fires, no further audio
|
|
471
|
+
* for the turn will arrive. Not fired on a barge-in cancel — that
|
|
472
|
+
* path fires {@link onInterrupted} instead.
|
|
473
|
+
*/
|
|
474
|
+
onFinal?: (totalAudioSeconds: number, totalTextChunks: number, totalAudioChunks: number) => void;
|
|
402
475
|
/**
|
|
403
476
|
* Called when the session is fully closed (after `session.close()`).
|
|
404
|
-
*
|
|
477
|
+
* Fires right after {@link onFinal} and additionally carries usage.
|
|
405
478
|
*/
|
|
406
479
|
onSessionClosed?: (totalAudioSeconds: number, totalTextChunks: number, totalAudioChunks: number) => void;
|
|
407
480
|
/** Called when the server begins generating audio for a text segment. */
|
|
@@ -419,14 +492,71 @@ export interface StreamingSessionCallbacks {
|
|
|
419
492
|
onError?: (error: Error) => void;
|
|
420
493
|
}
|
|
421
494
|
|
|
495
|
+
/**
|
|
496
|
+
* Per-session usage reported in the `session_closed` frame (KUG-1192).
|
|
497
|
+
*
|
|
498
|
+
* Lets you bill your own customers per conversation. `costCents` is the
|
|
499
|
+
* actual amount charged in **EUR cents**. When the charge could not be
|
|
500
|
+
* determined at session end (e.g. a transient billing error) `costCents` is
|
|
501
|
+
* `null` and `costAvailable` is `false` — never a misleading `0`.
|
|
502
|
+
* `audioSeconds` is always reported. On `/ws/tts/multi` usage is reported per
|
|
503
|
+
* context (per conversation) on each `context_closed` frame, not aggregated
|
|
504
|
+
* across contexts.
|
|
505
|
+
*/
|
|
506
|
+
export interface SessionUsage {
|
|
507
|
+
/** Total audio generated this session, in seconds (the unit we bill on). */
|
|
508
|
+
audioSeconds: number;
|
|
509
|
+
/** Actual amount charged in EUR cents, or `null` if undetermined. */
|
|
510
|
+
costCents: number | null;
|
|
511
|
+
/** Currency of `costCents` (`"eur"`); present only when `costCents` is set. */
|
|
512
|
+
currency?: string;
|
|
513
|
+
/** Total input characters submitted this session, if reported. */
|
|
514
|
+
characters?: number;
|
|
515
|
+
/** Model that produced the audio, if reported. */
|
|
516
|
+
modelId?: string;
|
|
517
|
+
/** `true` when an authoritative charge was returned for this session. */
|
|
518
|
+
costAvailable: boolean;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Parse the raw `usage` object (or a legacy `session_closed` payload without
|
|
523
|
+
* one) into a typed {@link SessionUsage}. Returns `null` when no usage info
|
|
524
|
+
* is present.
|
|
525
|
+
*/
|
|
526
|
+
export function parseSessionUsage(
|
|
527
|
+
data: Record<string, unknown>,
|
|
528
|
+
): SessionUsage | null {
|
|
529
|
+
const raw = data.usage as Record<string, unknown> | undefined;
|
|
530
|
+
const source = raw && typeof raw === 'object' ? raw : data;
|
|
531
|
+
const audioSeconds =
|
|
532
|
+
typeof source.audio_seconds === 'number'
|
|
533
|
+
? source.audio_seconds
|
|
534
|
+
: typeof data.total_audio_seconds === 'number'
|
|
535
|
+
? data.total_audio_seconds
|
|
536
|
+
: undefined;
|
|
537
|
+
if (audioSeconds === undefined) return null;
|
|
538
|
+
const costCents =
|
|
539
|
+
typeof source.cost_cents === 'number' ? source.cost_cents : null;
|
|
540
|
+
return {
|
|
541
|
+
audioSeconds,
|
|
542
|
+
costCents,
|
|
543
|
+
currency:
|
|
544
|
+
typeof source.currency === 'string' ? source.currency : undefined,
|
|
545
|
+
characters:
|
|
546
|
+
typeof source.characters === 'number' ? source.characters : undefined,
|
|
547
|
+
modelId: typeof source.model_id === 'string' ? source.model_id : undefined,
|
|
548
|
+
costAvailable: costCents !== null,
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
422
552
|
/**
|
|
423
553
|
* Audio chunk from streaming TTS.
|
|
424
554
|
*/
|
|
425
555
|
export interface AudioChunk {
|
|
426
556
|
/** Raw PCM16 audio as base64 */
|
|
427
557
|
audio: string;
|
|
428
|
-
/** Encoding format */
|
|
429
|
-
encoding: 'pcm_s16le';
|
|
558
|
+
/** Encoding format. 'mulaw' / 'alaw' only when output_format requested G.711. */
|
|
559
|
+
encoding: 'pcm_s16le' | 'mulaw' | 'alaw';
|
|
430
560
|
/** Chunk index */
|
|
431
561
|
index: number;
|
|
432
562
|
/** Sample rate */
|
|
@@ -453,6 +583,12 @@ export interface GenerationStats {
|
|
|
453
583
|
rtf: number;
|
|
454
584
|
/** Error message if any */
|
|
455
585
|
error?: string;
|
|
586
|
+
/**
|
|
587
|
+
* Per-request usage (audio time + amount charged), for billing your own
|
|
588
|
+
* customers. Undefined when the server reports no usage. See
|
|
589
|
+
* {@link SessionUsage}.
|
|
590
|
+
*/
|
|
591
|
+
usage?: SessionUsage;
|
|
456
592
|
}
|
|
457
593
|
|
|
458
594
|
/**
|
|
@@ -546,7 +682,9 @@ export interface MultiContextConfig {
|
|
|
546
682
|
defaultVoiceId?: number;
|
|
547
683
|
/** Output sample rate (default: 24000) */
|
|
548
684
|
sampleRate?: number;
|
|
549
|
-
/**
|
|
685
|
+
/** Combined codec+rate token (e.g. 'ulaw_8000'); opt-in, set-once per context. */
|
|
686
|
+
outputFormat?: string;
|
|
687
|
+
/** Classifier-free guidance scale. Clamped to [1.2, 2.5] (default: 2.0). */
|
|
550
688
|
cfgScale?: number;
|
|
551
689
|
/**
|
|
552
690
|
* Sampling variance. Range [0.0, 1.0]. 0 = most stable, 1 = most variance.
|
|
@@ -563,6 +701,14 @@ export interface MultiContextConfig {
|
|
|
563
701
|
* the language, which adds ~60-150ms to time-to-first-audio.
|
|
564
702
|
*/
|
|
565
703
|
language?: string;
|
|
704
|
+
/**
|
|
705
|
+
* Per-request dictionary selection. Omit for the default behavior (all
|
|
706
|
+
* active dictionaries of the project apply, filtered by language). An
|
|
707
|
+
* empty array disables dictionaries for this request. A list of
|
|
708
|
+
* dictionary IDs applies exactly those dictionaries — including
|
|
709
|
+
* inactive ones — bypassing the language filter.
|
|
710
|
+
*/
|
|
711
|
+
dictionaryIds?: number[];
|
|
566
712
|
/** Seconds before context auto-closes (default: 20.0) */
|
|
567
713
|
inactivityTimeout?: number;
|
|
568
714
|
}
|
|
@@ -601,8 +747,20 @@ export interface MultiContextCallbacks {
|
|
|
601
747
|
onContextCreated?: (contextId: string) => void;
|
|
602
748
|
/** Called when an audio chunk is received */
|
|
603
749
|
onChunk?: (chunk: MultiContextAudioChunk) => void;
|
|
604
|
-
/**
|
|
605
|
-
|
|
750
|
+
/**
|
|
751
|
+
* Called when all audio admitted before a `{flush: true}` has been
|
|
752
|
+
* delivered for a context (`{"final": true, "context_id": ...}`), and
|
|
753
|
+
* once more before {@link onContextClosed} on a graceful close. The
|
|
754
|
+
* ElevenLabs multi-context `is_final` equivalent. Not fired on an
|
|
755
|
+
* immediate (barge-in) close.
|
|
756
|
+
*/
|
|
757
|
+
onFinal?: (contextId: string) => void;
|
|
758
|
+
/**
|
|
759
|
+
* Called when a context is closed (terminal). `usage` carries this
|
|
760
|
+
* conversation's audio time + amount charged (undefined if not reported).
|
|
761
|
+
* See {@link SessionUsage}.
|
|
762
|
+
*/
|
|
763
|
+
onContextClosed?: (contextId: string, usage?: SessionUsage) => void;
|
|
606
764
|
/** Called when a context times out */
|
|
607
765
|
onContextTimeout?: (contextId: string) => void;
|
|
608
766
|
/** Called when session is closed */
|
package/src/utils.ts
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
* Utility functions for KugelAudio SDK.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Accepted classifier-free guidance band. Values outside [MIN, MAX] are
|
|
7
|
+
* clamped into the band (both client-side and by the server).
|
|
8
|
+
*/
|
|
9
|
+
export const MIN_CFG_SCALE = 1.2;
|
|
10
|
+
export const MAX_CFG_SCALE = 2.5;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Clamp `cfgScale` into [1.2, 2.5]. `undefined` passes through unchanged
|
|
14
|
+
* (the server default is applied).
|
|
15
|
+
*/
|
|
16
|
+
export function clampCfgScale(cfgScale: number | undefined): number | undefined {
|
|
17
|
+
if (cfgScale === undefined) return undefined;
|
|
18
|
+
return Math.min(MAX_CFG_SCALE, Math.max(MIN_CFG_SCALE, cfgScale));
|
|
19
|
+
}
|
|
20
|
+
|
|
5
21
|
/**
|
|
6
22
|
* Decode base64 string to ArrayBuffer.
|
|
7
23
|
*/
|