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/dist/index.mjs CHANGED
@@ -1,9 +1,25 @@
1
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
- }) : x)(function(x) {
4
- if (typeof require !== "undefined") return require.apply(this, arguments);
5
- throw Error('Dynamic require of "' + x + '" is not supported');
6
- });
1
+ import {
2
+ AuthenticationError,
3
+ ConnectionError,
4
+ ErrorCodes,
5
+ InsufficientCreditsError,
6
+ KugelAudioError,
7
+ NotFoundError,
8
+ RateLimitError,
9
+ ValidationError,
10
+ WsCloseCodes,
11
+ __require,
12
+ base64ToArrayBuffer,
13
+ clampCfgScale,
14
+ classifyHttpError,
15
+ classifyWsClose,
16
+ classifyWsFrame,
17
+ classifyWsHandshakeError,
18
+ createWavBlob,
19
+ createWavFile,
20
+ decodePCM16,
21
+ package_default
22
+ } from "./chunk-CB3B6T7F.mjs";
7
23
 
8
24
  // src/dictionaries.ts
9
25
  function mapDictionary(raw) {
@@ -186,265 +202,21 @@ var DictionariesResource = class {
186
202
  }
187
203
  };
188
204
 
189
- // src/errors.ts
190
- var ErrorCodes = {
191
- UNAUTHORIZED: "UNAUTHORIZED",
192
- RATE_LIMITED: "RATE_LIMITED",
193
- INSUFFICIENT_CREDITS: "INSUFFICIENT_CREDITS",
194
- MODEL_UNAVAILABLE: "MODEL_UNAVAILABLE",
195
- EMPTY_AUDIO: "EMPTY_AUDIO",
196
- VALIDATION: "VALIDATION_ERROR",
197
- INTERNAL: "INTERNAL_ERROR",
198
- NOT_FOUND: "NOT_FOUND",
199
- MISSING_VOICE_ID: "MISSING_VOICE_ID",
200
- TOO_MANY_CONTEXTS: "TOO_MANY_CONTEXTS"
201
- };
202
- var WsCloseCodes = {
203
- UNAUTHORIZED: 4001,
204
- INSUFFICIENT_CREDITS: 4003,
205
- RATE_LIMITED: 4029,
206
- MODEL_UNAVAILABLE: 4500
207
- };
208
- var API_KEYS_URL = "https://app.kugelaudio.com/settings/api-keys";
209
- var BILLING_URL = "https://app.kugelaudio.com/billing";
210
- var KugelAudioError = class _KugelAudioError extends Error {
211
- constructor(message, options = {}) {
212
- super(options.requestId ? `${message} (request_id: ${options.requestId})` : message);
213
- this.name = "KugelAudioError";
214
- this.statusCode = options.statusCode;
215
- this.errorCode = options.errorCode;
216
- this.requestId = options.requestId;
217
- this.retryAfter = options.retryAfter;
218
- Object.setPrototypeOf(this, _KugelAudioError.prototype);
219
- }
220
- };
221
- var AuthenticationError = class _AuthenticationError extends KugelAudioError {
222
- constructor(message, options = {}) {
223
- super(
224
- message ?? `KugelAudio rejected the API key. Check it is current at ${API_KEYS_URL}.`,
225
- { statusCode: 401, errorCode: ErrorCodes.UNAUTHORIZED, ...options }
226
- );
227
- this.name = "AuthenticationError";
228
- Object.setPrototypeOf(this, _AuthenticationError.prototype);
229
- }
230
- };
231
- var RateLimitError = class _RateLimitError extends KugelAudioError {
232
- constructor(message, options = {}) {
233
- const msg = message ?? (options.retryAfter ? `KugelAudio rate limit hit; retry after ${options.retryAfter}s.` : "KugelAudio rate limit hit; retry shortly.");
234
- super(msg, { statusCode: 429, errorCode: ErrorCodes.RATE_LIMITED, ...options });
235
- this.name = "RateLimitError";
236
- Object.setPrototypeOf(this, _RateLimitError.prototype);
237
- }
238
- };
239
- var InsufficientCreditsError = class _InsufficientCreditsError extends KugelAudioError {
240
- constructor(message, options = {}) {
241
- super(
242
- message ?? `Your KugelAudio account is out of credits. Top up at ${BILLING_URL}.`,
243
- { statusCode: 402, errorCode: ErrorCodes.INSUFFICIENT_CREDITS, ...options }
244
- );
245
- this.name = "InsufficientCreditsError";
246
- Object.setPrototypeOf(this, _InsufficientCreditsError.prototype);
247
- }
248
- };
249
- var ValidationError = class _ValidationError extends KugelAudioError {
250
- constructor(message, options = {}) {
251
- super(message, { statusCode: 400, errorCode: ErrorCodes.VALIDATION, ...options });
252
- this.name = "ValidationError";
253
- Object.setPrototypeOf(this, _ValidationError.prototype);
254
- }
255
- };
256
- var ConnectionError = class _ConnectionError extends KugelAudioError {
257
- constructor(message, options = {}) {
258
- super(message, { statusCode: 503, ...options });
259
- this.name = "ConnectionError";
260
- Object.setPrototypeOf(this, _ConnectionError.prototype);
261
- }
262
- };
263
- var NotFoundError = class _NotFoundError extends KugelAudioError {
264
- constructor(message, options = {}) {
265
- super(message ?? "Not found.", {
266
- statusCode: 404,
267
- errorCode: ErrorCodes.NOT_FOUND,
268
- ...options
269
- });
270
- this.name = "NotFoundError";
271
- Object.setPrototypeOf(this, _NotFoundError.prototype);
272
- }
273
- };
274
- function build(status, errorCode, message, opts = {}) {
275
- const common = { ...opts };
276
- if (status !== void 0) common.statusCode = status;
277
- if (errorCode !== void 0) common.errorCode = errorCode;
278
- if (errorCode === ErrorCodes.UNAUTHORIZED || status === 401) {
279
- return new AuthenticationError(message || void 0, common);
280
- }
281
- if (errorCode === ErrorCodes.INSUFFICIENT_CREDITS || status === 402) {
282
- return new InsufficientCreditsError(message || void 0, common);
283
- }
284
- if (errorCode === ErrorCodes.RATE_LIMITED || errorCode === ErrorCodes.TOO_MANY_CONTEXTS || status === 429) {
285
- return new RateLimitError(message || void 0, common);
286
- }
287
- if (errorCode === ErrorCodes.VALIDATION || errorCode === ErrorCodes.MISSING_VOICE_ID || status === 400) {
288
- return new ValidationError(message || "Request validation failed.", common);
289
- }
290
- if (errorCode === ErrorCodes.MODEL_UNAVAILABLE || status === 503) {
291
- const detail = message || "service temporarily unavailable";
292
- return new ConnectionError(
293
- `KugelAudio is temporarily unavailable: ${detail}. Retry shortly.`,
294
- common
295
- );
296
- }
297
- if (errorCode === ErrorCodes.NOT_FOUND || status === 404) {
298
- return new NotFoundError(message || void 0, common);
299
- }
300
- return new KugelAudioError(message || `HTTP ${status}`, common);
301
- }
302
- function readHeader(headers, name) {
303
- if (headers && typeof headers.get === "function") {
304
- return headers.get(name) ?? void 0;
305
- }
306
- const rec = headers;
307
- return rec[name] ?? rec[name.toLowerCase()] ?? void 0;
308
- }
309
- function classifyHttpError(status, bodyText, headers) {
310
- let errorCode;
311
- let message = "";
312
- let retryAfter;
313
- if (bodyText) {
314
- try {
315
- const body = JSON.parse(bodyText);
316
- if (body && typeof body === "object") {
317
- errorCode = typeof body.error_code === "string" ? body.error_code : void 0;
318
- const msg = body.error ?? body.detail;
319
- if (Array.isArray(msg)) {
320
- message = msg.map((m) => String(m)).join("; ");
321
- } else if (typeof msg === "string") {
322
- message = msg;
323
- }
324
- if (typeof body.retry_after === "number") {
325
- retryAfter = body.retry_after;
326
- }
327
- }
328
- } catch {
329
- }
330
- }
331
- if (retryAfter === void 0) {
332
- const header = readHeader(headers, "Retry-After") ?? readHeader(headers, "retry-after");
333
- if (header) {
334
- const n = Number(header);
335
- if (Number.isFinite(n)) retryAfter = n;
336
- }
337
- }
338
- const requestId = readHeader(headers, "x-request-id") ?? readHeader(headers, "X-Request-Id");
339
- if (!message) {
340
- message = (bodyText || "").trim();
341
- }
342
- return build(status, errorCode, message, { requestId, retryAfter });
343
- }
344
- function classifyWsFrame(data) {
345
- const errorCode = data.error_code;
346
- const message = data.error ?? "Server reported an error.";
347
- const status = typeof data.code === "number" ? data.code : void 0;
348
- const retryAfter = typeof data.retry_after === "number" ? data.retry_after : void 0;
349
- return build(status, errorCode, message, { retryAfter });
350
- }
351
- function classifyWsClose(code, reason) {
352
- const reasonTxt = (reason ?? "").trim();
353
- if (code === WsCloseCodes.UNAUTHORIZED) {
354
- let msg = `KugelAudio rejected the API key. Check it is current at ${API_KEYS_URL}.`;
355
- if (reasonTxt) msg = `${msg} (${reasonTxt})`;
356
- return new AuthenticationError(msg);
357
- }
358
- if (code === WsCloseCodes.INSUFFICIENT_CREDITS) {
359
- return new InsufficientCreditsError();
360
- }
361
- if (code === WsCloseCodes.RATE_LIMITED) {
362
- return new RateLimitError();
363
- }
364
- if (code === WsCloseCodes.MODEL_UNAVAILABLE) {
365
- const suffix = reasonTxt ? ` (${reasonTxt})` : "";
366
- return new ConnectionError(
367
- `KugelAudio model is temporarily unavailable. Retry shortly.${suffix}`
368
- );
369
- }
370
- const detail = reasonTxt || "no reason given";
371
- const codeStr = code !== void 0 ? ` (code ${code})` : "";
372
- return new ConnectionError(
373
- `KugelAudio WebSocket closed by server: ${detail}${codeStr}.`
374
- );
375
- }
376
- function classifyWsHandshakeError(err) {
377
- if (!err || typeof err !== "object") return null;
378
- const e = err;
379
- let status;
380
- if (typeof e.statusCode === "number") {
381
- status = e.statusCode;
382
- }
383
- if (status === void 0 && typeof e.message === "string") {
384
- const m = e.message.match(/Unexpected server response:\s*(\d{3})/i);
385
- if (m) status = Number(m[1]);
386
- }
387
- if (status === void 0) return null;
388
- if (status === 403) {
389
- return new AuthenticationError();
390
- }
391
- return build(status, void 0, typeof e.message === "string" ? e.message : "");
392
- }
393
-
394
- // src/utils.ts
395
- function base64ToArrayBuffer(base64) {
396
- if (typeof atob === "function") {
397
- const binary = atob(base64);
398
- const bytes = new Uint8Array(binary.length);
399
- for (let i = 0; i < binary.length; i++) {
400
- bytes[i] = binary.charCodeAt(i);
401
- }
402
- return bytes.buffer;
403
- } else {
404
- const buffer = Buffer.from(base64, "base64");
405
- return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
406
- }
407
- }
408
- function decodePCM16(base64) {
409
- const buffer = base64ToArrayBuffer(base64);
410
- const int16 = new Int16Array(buffer);
411
- const float32 = new Float32Array(int16.length);
412
- for (let i = 0; i < int16.length; i++) {
413
- float32[i] = int16[i] / 32768;
414
- }
415
- return float32;
416
- }
417
- function createWavFile(audio, sampleRate) {
418
- const dataSize = audio.byteLength;
419
- const fileSize = 44 + dataSize;
420
- const buffer = new ArrayBuffer(fileSize);
421
- const view = new DataView(buffer);
422
- writeString(view, 0, "RIFF");
423
- view.setUint32(4, fileSize - 8, true);
424
- writeString(view, 8, "WAVE");
425
- writeString(view, 12, "fmt ");
426
- view.setUint32(16, 16, true);
427
- view.setUint16(20, 1, true);
428
- view.setUint16(22, 1, true);
429
- view.setUint32(24, sampleRate, true);
430
- view.setUint32(28, sampleRate * 2, true);
431
- view.setUint16(32, 2, true);
432
- view.setUint16(34, 16, true);
433
- writeString(view, 36, "data");
434
- view.setUint32(40, dataSize, true);
435
- const audioBytes = new Uint8Array(audio);
436
- const wavBytes = new Uint8Array(buffer);
437
- wavBytes.set(audioBytes, 44);
438
- return buffer;
439
- }
440
- function writeString(view, offset, str) {
441
- for (let i = 0; i < str.length; i++) {
442
- view.setUint8(offset + i, str.charCodeAt(i));
443
- }
444
- }
445
- function createWavBlob(audio, sampleRate) {
446
- const wavBuffer = createWavFile(audio, sampleRate);
447
- return new Blob([wavBuffer], { type: "audio/wav" });
205
+ // src/types.ts
206
+ function parseSessionUsage(data) {
207
+ const raw = data.usage;
208
+ const source = raw && typeof raw === "object" ? raw : data;
209
+ const audioSeconds = typeof source.audio_seconds === "number" ? source.audio_seconds : typeof data.total_audio_seconds === "number" ? data.total_audio_seconds : void 0;
210
+ if (audioSeconds === void 0) return null;
211
+ const costCents = typeof source.cost_cents === "number" ? source.cost_cents : null;
212
+ return {
213
+ audioSeconds,
214
+ costCents,
215
+ currency: typeof source.currency === "string" ? source.currency : void 0,
216
+ characters: typeof source.characters === "number" ? source.characters : void 0,
217
+ modelId: typeof source.model_id === "string" ? source.model_id : void 0,
218
+ costAvailable: costCents !== null
219
+ };
448
220
  }
449
221
 
450
222
  // src/websocket.ts
@@ -474,69 +246,6 @@ function getWebSocket() {
474
246
  );
475
247
  }
476
248
 
477
- // package.json
478
- var package_default = {
479
- name: "kugelaudio",
480
- version: "0.7.0",
481
- description: "Official JavaScript/TypeScript SDK for KugelAudio TTS API",
482
- main: "dist/index.js",
483
- module: "dist/index.mjs",
484
- types: "dist/index.d.ts",
485
- exports: {
486
- ".": {
487
- types: "./dist/index.d.ts",
488
- import: "./dist/index.mjs",
489
- require: "./dist/index.js"
490
- }
491
- },
492
- files: [
493
- "dist",
494
- "src",
495
- "LICENSE",
496
- "CHANGELOG.md"
497
- ],
498
- scripts: {
499
- build: "tsup src/index.ts --format cjs,esm --dts",
500
- dev: "tsup src/index.ts --format cjs,esm --dts --watch",
501
- lint: "eslint src/",
502
- test: "vitest run",
503
- "test:watch": "vitest",
504
- prepublishOnly: "npm run build"
505
- },
506
- keywords: [
507
- "tts",
508
- "text-to-speech",
509
- "audio",
510
- "streaming",
511
- "websocket",
512
- "kugelaudio"
513
- ],
514
- author: "KugelAudio <hello@kugelaudio.com>",
515
- license: "MIT",
516
- repository: {
517
- type: "git",
518
- url: "https://github.com/Kugelaudio/KugelAudio",
519
- directory: "sdks/js"
520
- },
521
- homepage: "https://kugelaudio.com",
522
- bugs: {
523
- url: "https://github.com/Kugelaudio/KugelAudio/issues"
524
- },
525
- devDependencies: {
526
- "@types/node": "^25.3.2",
527
- tsup: "^8.0.0",
528
- typescript: "^6.0.2",
529
- vitest: "^4.0.18"
530
- },
531
- engines: {
532
- node: ">=18.0.0"
533
- },
534
- dependencies: {
535
- tsx: "^4.21.0",
536
- ws: "^8.18.0"
537
- }
538
- };
539
-
540
249
  // src/client.ts
541
250
  var DEFAULT_API_URL = "https://api.kugelaudio.com";
542
251
  var EU_API_URL = "https://api.eu.kugelaudio.com";
@@ -567,6 +276,105 @@ function createWs(url) {
567
276
  return new WS(url);
568
277
  }
569
278
  var WS_OPEN = 1;
279
+ var UPDATABLE_SETTINGS = {
280
+ cfgScale: "cfg_scale",
281
+ temperature: "temperature",
282
+ speed: "speed",
283
+ maxNewTokens: "max_new_tokens",
284
+ language: "language",
285
+ normalize: "normalize"
286
+ };
287
+ function buildSettingsUpdateBody(settings) {
288
+ const body = {};
289
+ for (const camel of Object.keys(UPDATABLE_SETTINGS)) {
290
+ const value = settings[camel];
291
+ if (value !== void 0) body[UPDATABLE_SETTINGS[camel]] = value;
292
+ }
293
+ if (Object.keys(body).length === 0) {
294
+ throw new KugelAudioError(
295
+ `updateSettings requires at least one parameter to change (one of ${Object.keys(UPDATABLE_SETTINGS).join(", ")})`
296
+ );
297
+ }
298
+ return body;
299
+ }
300
+ function parseEffectiveSettings(raw) {
301
+ const s = raw && typeof raw === "object" ? raw : {};
302
+ return {
303
+ cfgScale: s.cfg_scale,
304
+ temperature: s.temperature,
305
+ speed: s.speed,
306
+ maxNewTokens: s.max_new_tokens,
307
+ language: s.language,
308
+ normalize: s.normalize
309
+ };
310
+ }
311
+ function applyAcceptedSettingsUpdate(config, settings) {
312
+ for (const camel of Object.keys(UPDATABLE_SETTINGS)) {
313
+ if (settings[camel] !== void 0) {
314
+ config[camel] = settings[camel];
315
+ }
316
+ }
317
+ }
318
+ function sendUpdateSettings(ws, body) {
319
+ const QUIET_TIMEOUT_MS = 15e3;
320
+ return new Promise((resolve, reject) => {
321
+ let settled = false;
322
+ let timer;
323
+ const prevMessage = ws.onmessage;
324
+ const prevClose = ws.onclose;
325
+ const finish = (action) => {
326
+ if (settled) return;
327
+ settled = true;
328
+ clearTimeout(timer);
329
+ ws.onmessage = prevMessage;
330
+ ws.onclose = prevClose;
331
+ action();
332
+ };
333
+ const armQuietTimer = () => {
334
+ clearTimeout(timer);
335
+ timer = setTimeout(
336
+ () => finish(
337
+ () => reject(
338
+ new KugelAudioError(
339
+ "Timed out waiting for settings_updated acknowledgement"
340
+ )
341
+ )
342
+ ),
343
+ QUIET_TIMEOUT_MS
344
+ );
345
+ };
346
+ armQuietTimer();
347
+ ws.onmessage = (event) => {
348
+ armQuietTimer();
349
+ let data = null;
350
+ try {
351
+ const raw = typeof event.data === "string" ? event.data : event.data instanceof Buffer ? event.data.toString() : String(event.data);
352
+ data = JSON.parse(raw);
353
+ } catch {
354
+ }
355
+ if (data?.settings_updated) {
356
+ finish(() => resolve(parseEffectiveSettings(data.settings)));
357
+ return;
358
+ }
359
+ if (data?.error) {
360
+ finish(() => reject(new KugelAudioError(String(data.error))));
361
+ return;
362
+ }
363
+ if (prevMessage) prevMessage.call(ws, event);
364
+ };
365
+ ws.onclose = (event) => {
366
+ if (prevClose) prevClose.call(ws, event);
367
+ finish(
368
+ () => reject(
369
+ new ConnectionError(
370
+ "WebSocket closed before settings_updated acknowledgement"
371
+ )
372
+ )
373
+ );
374
+ };
375
+ ws.send(JSON.stringify({ update_settings: body }));
376
+ });
377
+ }
570
378
  var _languageWarningLogged = false;
571
379
  function warnIfNoLanguage(language, normalize) {
572
380
  const normEnabled = normalize === void 0 || normalize;
@@ -622,6 +430,7 @@ var VoicesResource = class {
622
430
  category: v.category,
623
431
  sex: v.sex,
624
432
  age: v.age,
433
+ quality: v.quality,
625
434
  supportedLanguages: v.supported_languages || [],
626
435
  sampleText: v.sample_text,
627
436
  avatarUrl: v.avatar_url,
@@ -993,7 +802,8 @@ var TTSResource = class {
993
802
  durationMs: data.dur_ms,
994
803
  generationMs: data.gen_ms,
995
804
  rtf: data.rtf,
996
- error: data.error
805
+ error: data.error,
806
+ usage: parseSessionUsage(data) ?? void 0
997
807
  };
998
808
  pending.callbacks.onFinal?.(stats);
999
809
  this.pendingRequests.delete(requestId);
@@ -1080,15 +890,19 @@ var TTSResource = class {
1080
890
  text: options.text,
1081
891
  model_id: options.modelId || "kugel-3",
1082
892
  voice_id: options.voiceId,
1083
- cfg_scale: options.cfgScale ?? 2,
893
+ cfg_scale: clampCfgScale(options.cfgScale) ?? 2,
1084
894
  ...options.temperature !== void 0 && { temperature: options.temperature },
1085
895
  max_new_tokens: options.maxNewTokens ?? 2048,
1086
896
  sample_rate: options.sampleRate ?? 24e3,
897
+ ...options.outputFormat && { output_format: options.outputFormat },
1087
898
  normalize: options.normalize ?? true,
1088
899
  ...options.language && { language: options.language },
1089
900
  ...options.wordTimestamps && { word_timestamps: true },
1090
901
  ...options.speed !== void 0 && { speed: options.speed },
1091
- ...options.projectId !== void 0 && { project_id: options.projectId }
902
+ ...options.projectId !== void 0 && { project_id: options.projectId },
903
+ // [] is meaningful (explicit opt-out) and must be sent; only
904
+ // undefined (use the project default) is omitted.
905
+ ...options.dictionaryIds !== void 0 && { dictionary_ids: options.dictionaryIds }
1092
906
  }));
1093
907
  });
1094
908
  }
@@ -1106,14 +920,18 @@ var TTSResource = class {
1106
920
  text: options.text,
1107
921
  model_id: options.modelId || "kugel-3",
1108
922
  voice_id: options.voiceId,
1109
- cfg_scale: options.cfgScale ?? 2,
923
+ cfg_scale: clampCfgScale(options.cfgScale) ?? 2,
1110
924
  max_new_tokens: options.maxNewTokens ?? 2048,
1111
925
  sample_rate: options.sampleRate ?? 24e3,
926
+ ...options.outputFormat && { output_format: options.outputFormat },
1112
927
  normalize: options.normalize ?? true,
1113
928
  ...options.language && { language: options.language },
1114
929
  ...options.wordTimestamps && { word_timestamps: true },
1115
930
  ...options.speed !== void 0 && { speed: options.speed },
1116
- ...options.projectId !== void 0 && { project_id: options.projectId }
931
+ ...options.projectId !== void 0 && { project_id: options.projectId },
932
+ // [] is meaningful (explicit opt-out) and must be sent; only
933
+ // undefined (use the project default) is omitted.
934
+ ...options.dictionaryIds !== void 0 && { dictionary_ids: options.dictionaryIds }
1117
935
  }));
1118
936
  };
1119
937
  ws.onmessage = (event) => {
@@ -1135,7 +953,8 @@ var TTSResource = class {
1135
953
  durationMs: data.dur_ms,
1136
954
  generationMs: data.gen_ms,
1137
955
  rtf: data.rtf,
1138
- error: data.error
956
+ error: data.error,
957
+ usage: parseSessionUsage(data) ?? void 0
1139
958
  };
1140
959
  callbacks.onFinal?.(stats);
1141
960
  ws.close();
@@ -1305,7 +1124,11 @@ var MultiContextSession = class {
1305
1124
  this.ws = null;
1306
1125
  this.callbacks = {};
1307
1126
  this.contexts = /* @__PURE__ */ new Set();
1127
+ /** Contexts a create message has been sent for (not yet necessarily
1128
+ * confirmed by the server via context_created). */
1129
+ this.requestedContexts = /* @__PURE__ */ new Set();
1308
1130
  this._sessionId = null;
1131
+ this._contextUsage = /* @__PURE__ */ new Map();
1309
1132
  this.isStarted = false;
1310
1133
  this.config = config || {};
1311
1134
  }
@@ -1315,6 +1138,18 @@ var MultiContextSession = class {
1315
1138
  get sessionId() {
1316
1139
  return this._sessionId;
1317
1140
  }
1141
+ /**
1142
+ * Per-context usage (audio time + amount charged) for a closed context, or
1143
+ * null if that context hasn't closed yet. Each context is its own
1144
+ * conversation — use this to bill per conversation. See {@link SessionUsage}.
1145
+ */
1146
+ usageFor(contextId) {
1147
+ return this._contextUsage.get(contextId) ?? null;
1148
+ }
1149
+ /** Map of context_id → per-context usage for all closed contexts. */
1150
+ get contextUsage() {
1151
+ return new Map(this._contextUsage);
1152
+ }
1318
1153
  /**
1319
1154
  * Connect to the multi-context WebSocket endpoint.
1320
1155
  *
@@ -1368,12 +1203,19 @@ var MultiContextSession = class {
1368
1203
  };
1369
1204
  this.callbacks.onChunk?.(chunk);
1370
1205
  }
1206
+ if (data.final && data.context_id) {
1207
+ this.callbacks.onFinal?.(data.context_id);
1208
+ }
1371
1209
  if (data.context_closed) {
1372
1210
  this.contexts.delete(data.context_id);
1373
- this.callbacks.onContextClosed?.(data.context_id);
1211
+ this.requestedContexts.delete(data.context_id);
1212
+ const ctxUsage = parseSessionUsage(data) ?? void 0;
1213
+ if (ctxUsage) this._contextUsage.set(data.context_id, ctxUsage);
1214
+ this.callbacks.onContextClosed?.(data.context_id, ctxUsage);
1374
1215
  }
1375
1216
  if (data.context_timeout) {
1376
1217
  this.contexts.delete(data.context_id);
1218
+ this.requestedContexts.delete(data.context_id);
1377
1219
  this.callbacks.onContextTimeout?.(data.context_id);
1378
1220
  }
1379
1221
  if (data.session_closed) {
@@ -1413,6 +1255,7 @@ var MultiContextSession = class {
1413
1255
  this.ws = null;
1414
1256
  this.isStarted = false;
1415
1257
  this.contexts.clear();
1258
+ this.requestedContexts.clear();
1416
1259
  };
1417
1260
  });
1418
1261
  }
@@ -1423,6 +1266,7 @@ var MultiContextSession = class {
1423
1266
  if (!this.ws || this.ws.readyState !== WS_OPEN) {
1424
1267
  throw new KugelAudioError("WebSocket not connected");
1425
1268
  }
1269
+ this.requestedContexts.add(contextId);
1426
1270
  const msg = {
1427
1271
  text: " ",
1428
1272
  context_id: contextId
@@ -1430,23 +1274,27 @@ var MultiContextSession = class {
1430
1274
  if (!this.isStarted) {
1431
1275
  warnIfNoLanguage(this.config.language, this.config.normalize);
1432
1276
  if (this.config.sampleRate) msg.sample_rate = this.config.sampleRate;
1433
- if (this.config.cfgScale) msg.cfg_scale = this.config.cfgScale;
1277
+ if (this.config.outputFormat) msg.output_format = this.config.outputFormat;
1278
+ if (this.config.cfgScale !== void 0) msg.cfg_scale = clampCfgScale(this.config.cfgScale);
1434
1279
  if (this.config.temperature !== void 0) msg.temperature = this.config.temperature;
1435
1280
  if (this.config.maxNewTokens) msg.max_new_tokens = this.config.maxNewTokens;
1436
1281
  if (this.config.normalize !== void 0) msg.normalize = this.config.normalize;
1437
1282
  if (this.config.language) msg.language = this.config.language;
1283
+ if (this.config.dictionaryIds !== void 0) msg.dictionary_ids = this.config.dictionaryIds;
1438
1284
  if (this.config.inactivityTimeout) msg.inactivity_timeout = this.config.inactivityTimeout;
1439
1285
  }
1286
+ const voiceSettings = {};
1440
1287
  const voiceId = options?.voiceId || this.config.defaultVoiceId;
1441
- if (voiceId) msg.voice_id = voiceId;
1288
+ if (voiceId) voiceSettings.voice_id = voiceId;
1442
1289
  if (options?.voiceSettings) {
1443
- msg.voice_settings = {
1444
- stability: options.voiceSettings.stability,
1445
- similarity_boost: options.voiceSettings.similarityBoost,
1446
- style: options.voiceSettings.style,
1447
- use_speaker_boost: options.voiceSettings.useSpeakerBoost,
1448
- speed: options.voiceSettings.speed
1449
- };
1290
+ voiceSettings.stability = options.voiceSettings.stability;
1291
+ voiceSettings.similarity_boost = options.voiceSettings.similarityBoost;
1292
+ voiceSettings.style = options.voiceSettings.style;
1293
+ voiceSettings.use_speaker_boost = options.voiceSettings.useSpeakerBoost;
1294
+ voiceSettings.speed = options.voiceSettings.speed;
1295
+ }
1296
+ if (Object.keys(voiceSettings).length > 0) {
1297
+ msg.voice_settings = voiceSettings;
1450
1298
  }
1451
1299
  this.ws.send(JSON.stringify(msg));
1452
1300
  }
@@ -1457,7 +1305,7 @@ var MultiContextSession = class {
1457
1305
  if (!this.ws || this.ws.readyState !== WS_OPEN) {
1458
1306
  throw new KugelAudioError("WebSocket not connected");
1459
1307
  }
1460
- if (!this.contexts.has(contextId) && !this.isStarted) {
1308
+ if (!this.requestedContexts.has(contextId) && !this.contexts.has(contextId)) {
1461
1309
  this.createContext(contextId);
1462
1310
  }
1463
1311
  this.ws.send(JSON.stringify({
@@ -1504,6 +1352,33 @@ var MultiContextSession = class {
1504
1352
  context_id: contextId
1505
1353
  }));
1506
1354
  }
1355
+ /**
1356
+ * Change the session's generation parameters mid-connection (KUG-1166).
1357
+ *
1358
+ * Session-scoped (there is no `contextId`): the update applies to contexts
1359
+ * started **after** it — a context already streaming keeps the settings it
1360
+ * began with, since generation parameters are bound when a context's engine
1361
+ * session opens. With the common one-context-per-turn pattern that means it
1362
+ * takes effect on the next turn. Per-context `cfgScale` / `maxNewTokens`
1363
+ * passed to {@link createContext} still win for that context.
1364
+ *
1365
+ * Resolves with the generation parameters now in effect (the server's echo).
1366
+ *
1367
+ * @throws {KugelAudioError} if no field is provided, the socket is not open,
1368
+ * or the server rejects the update.
1369
+ */
1370
+ updateSettings(settings) {
1371
+ if (!this.ws || this.ws.readyState !== WS_OPEN) {
1372
+ throw new KugelAudioError(
1373
+ "MultiContextSession not connected. Call connect() first."
1374
+ );
1375
+ }
1376
+ const body = buildSettingsUpdateBody(settings);
1377
+ return sendUpdateSettings(this.ws, body).then((effective) => {
1378
+ applyAcceptedSettingsUpdate(this.config, settings);
1379
+ return effective;
1380
+ });
1381
+ }
1507
1382
  /**
1508
1383
  * Close the session and all contexts.
1509
1384
  */
@@ -1515,6 +1390,7 @@ var MultiContextSession = class {
1515
1390
  this.ws = null;
1516
1391
  this.isStarted = false;
1517
1392
  this.contexts.clear();
1393
+ this.requestedContexts.clear();
1518
1394
  }
1519
1395
  /**
1520
1396
  * Get active context IDs.
@@ -1533,10 +1409,19 @@ var StreamingSession = class {
1533
1409
  constructor(client, config, callbacks) {
1534
1410
  this.ws = null;
1535
1411
  this.configSent = false;
1412
+ this._lastUsage = null;
1536
1413
  this.client = client;
1537
1414
  this.config = config;
1538
1415
  this.callbacks = callbacks;
1539
1416
  }
1417
+ /**
1418
+ * Per-session usage from the most recently closed session, or null before
1419
+ * the first session closes. Use this to bill your own customers per
1420
+ * conversation. See {@link SessionUsage}.
1421
+ */
1422
+ get lastUsage() {
1423
+ return this._lastUsage;
1424
+ }
1540
1425
  /**
1541
1426
  * Open the WebSocket connection and authenticate.
1542
1427
  *
@@ -1600,7 +1485,15 @@ var StreamingSession = class {
1600
1485
  if (data.interrupted) {
1601
1486
  this.callbacks.onInterrupted?.();
1602
1487
  }
1488
+ if (data.final) {
1489
+ this.callbacks.onFinal?.(
1490
+ data.total_audio_seconds ?? 0,
1491
+ data.total_text_chunks ?? 0,
1492
+ data.total_audio_chunks ?? 0
1493
+ );
1494
+ }
1603
1495
  if (data.session_closed) {
1496
+ this._lastUsage = parseSessionUsage(data);
1604
1497
  this.callbacks.onSessionClosed?.(
1605
1498
  data.total_audio_seconds ?? 0,
1606
1499
  data.total_text_chunks ?? 0,
@@ -1664,10 +1557,11 @@ var StreamingSession = class {
1664
1557
  if (!this.configSent) {
1665
1558
  if (this.config.voiceId !== void 0) msg.voice_id = this.config.voiceId;
1666
1559
  if (this.config.modelId !== void 0) msg.model_id = this.config.modelId;
1667
- if (this.config.cfgScale !== void 0) msg.cfg_scale = this.config.cfgScale;
1560
+ if (this.config.cfgScale !== void 0) msg.cfg_scale = clampCfgScale(this.config.cfgScale);
1668
1561
  if (this.config.temperature !== void 0) msg.temperature = this.config.temperature;
1669
1562
  if (this.config.maxNewTokens !== void 0) msg.max_new_tokens = this.config.maxNewTokens;
1670
1563
  if (this.config.sampleRate !== void 0) msg.sample_rate = this.config.sampleRate;
1564
+ if (this.config.outputFormat !== void 0) msg.output_format = this.config.outputFormat;
1671
1565
  if (this.config.flushTimeoutMs !== void 0) msg.flush_timeout_ms = this.config.flushTimeoutMs;
1672
1566
  if (this.config.maxBufferLength !== void 0) msg.max_buffer_length = this.config.maxBufferLength;
1673
1567
  if (this.config.normalize !== void 0) msg.normalize = this.config.normalize;
@@ -1676,6 +1570,7 @@ var StreamingSession = class {
1676
1570
  if (this.config.autoMode !== void 0) msg.auto_mode = this.config.autoMode;
1677
1571
  if (this.config.chunkLengthSchedule?.length) msg.chunk_length_schedule = this.config.chunkLengthSchedule;
1678
1572
  if (this.config.speed !== void 0) msg.speed = this.config.speed;
1573
+ if (this.config.dictionaryIds !== void 0) msg.dictionary_ids = this.config.dictionaryIds;
1679
1574
  this.configSent = true;
1680
1575
  }
1681
1576
  this.ws.send(JSON.stringify(msg));
@@ -1811,6 +1706,39 @@ var StreamingSession = class {
1811
1706
  Object.assign(this.config, config);
1812
1707
  this.configSent = false;
1813
1708
  }
1709
+ /**
1710
+ * Change generation parameters mid-connection without reconnecting (KUG-1166).
1711
+ *
1712
+ * Sends an `update_settings` message and resolves with the generation
1713
+ * parameters now in effect (the server's echo). Only the six fields of
1714
+ * {@link SettingsUpdate} are updatable; identity / audio-format fields
1715
+ * (`voiceId`, `modelId`, `sampleRate`, `outputFormat`, `dictionaryIds`) are
1716
+ * fixed for the connection — change those with {@link updateConfig} after
1717
+ * {@link endSession} instead.
1718
+ *
1719
+ * The change applies to the **next turn**: a turn already streaming keeps the
1720
+ * settings it started with, so call this between turns.
1721
+ *
1722
+ * @example
1723
+ * ```typescript
1724
+ * const effective = await session.updateSettings({ cfgScale: 1.5, speed: 1.1 });
1725
+ * ```
1726
+ *
1727
+ * @throws {KugelAudioError} if no field is provided, the socket is not open,
1728
+ * or the server rejects the update (e.g. a value out of range).
1729
+ */
1730
+ updateSettings(settings) {
1731
+ if (!this.ws || this.ws.readyState !== WS_OPEN) {
1732
+ throw new KugelAudioError(
1733
+ "StreamingSession not connected. Call connect() first."
1734
+ );
1735
+ }
1736
+ const body = buildSettingsUpdateBody(settings);
1737
+ return sendUpdateSettings(this.ws, body).then((effective) => {
1738
+ applyAcceptedSettingsUpdate(this.config, settings);
1739
+ return effective;
1740
+ });
1741
+ }
1814
1742
  /**
1815
1743
  * Close the session and the WebSocket connection.
1816
1744
  *
@@ -2054,5 +1982,6 @@ export {
2054
1982
  classifyWsHandshakeError,
2055
1983
  createWavBlob,
2056
1984
  createWavFile,
2057
- decodePCM16
1985
+ decodePCM16,
1986
+ parseSessionUsage
2058
1987
  };