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.js CHANGED
@@ -39,7 +39,8 @@ __export(index_exports, {
39
39
  classifyWsHandshakeError: () => classifyWsHandshakeError,
40
40
  createWavBlob: () => createWavBlob,
41
41
  createWavFile: () => createWavFile,
42
- decodePCM16: () => decodePCM16
42
+ decodePCM16: () => decodePCM16,
43
+ parseSessionUsage: () => parseSessionUsage
43
44
  });
44
45
  module.exports = __toCommonJS(index_exports);
45
46
 
@@ -429,7 +430,30 @@ function classifyWsHandshakeError(err) {
429
430
  return build(status, void 0, typeof e.message === "string" ? e.message : "");
430
431
  }
431
432
 
433
+ // src/types.ts
434
+ function parseSessionUsage(data) {
435
+ const raw = data.usage;
436
+ const source = raw && typeof raw === "object" ? raw : data;
437
+ const audioSeconds = typeof source.audio_seconds === "number" ? source.audio_seconds : typeof data.total_audio_seconds === "number" ? data.total_audio_seconds : void 0;
438
+ if (audioSeconds === void 0) return null;
439
+ const costCents = typeof source.cost_cents === "number" ? source.cost_cents : null;
440
+ return {
441
+ audioSeconds,
442
+ costCents,
443
+ currency: typeof source.currency === "string" ? source.currency : void 0,
444
+ characters: typeof source.characters === "number" ? source.characters : void 0,
445
+ modelId: typeof source.model_id === "string" ? source.model_id : void 0,
446
+ costAvailable: costCents !== null
447
+ };
448
+ }
449
+
432
450
  // src/utils.ts
451
+ var MIN_CFG_SCALE = 1.2;
452
+ var MAX_CFG_SCALE = 2.5;
453
+ function clampCfgScale(cfgScale) {
454
+ if (cfgScale === void 0) return void 0;
455
+ return Math.min(MAX_CFG_SCALE, Math.max(MIN_CFG_SCALE, cfgScale));
456
+ }
433
457
  function base64ToArrayBuffer(base64) {
434
458
  if (typeof atob === "function") {
435
459
  const binary = atob(base64);
@@ -515,7 +539,7 @@ function getWebSocket() {
515
539
  // package.json
516
540
  var package_default = {
517
541
  name: "kugelaudio",
518
- version: "0.7.0",
542
+ version: "0.9.0",
519
543
  description: "Official JavaScript/TypeScript SDK for KugelAudio TTS API",
520
544
  main: "dist/index.js",
521
545
  module: "dist/index.mjs",
@@ -525,6 +549,11 @@ var package_default = {
525
549
  types: "./dist/index.d.ts",
526
550
  import: "./dist/index.mjs",
527
551
  require: "./dist/index.js"
552
+ },
553
+ "./livekit": {
554
+ types: "./dist/livekit/index.d.ts",
555
+ import: "./dist/livekit/index.mjs",
556
+ require: "./dist/livekit/index.js"
528
557
  }
529
558
  },
530
559
  files: [
@@ -534,8 +563,8 @@ var package_default = {
534
563
  "CHANGELOG.md"
535
564
  ],
536
565
  scripts: {
537
- build: "tsup src/index.ts --format cjs,esm --dts",
538
- dev: "tsup src/index.ts --format cjs,esm --dts --watch",
566
+ build: "tsup src/index.ts src/livekit/index.ts --format cjs,esm --dts",
567
+ dev: "tsup src/index.ts src/livekit/index.ts --format cjs,esm --dts --watch",
539
568
  lint: "eslint src/",
540
569
  test: "vitest run",
541
570
  "test:watch": "vitest",
@@ -561,11 +590,26 @@ var package_default = {
561
590
  url: "https://github.com/Kugelaudio/KugelAudio/issues"
562
591
  },
563
592
  devDependencies: {
593
+ "@livekit/agents": "^1.4.11",
594
+ "@livekit/rtc-node": "^0.13.30",
564
595
  "@types/node": "^25.3.2",
596
+ "@types/ws": "^8.18.1",
565
597
  tsup: "^8.0.0",
566
598
  typescript: "^6.0.2",
567
599
  vitest: "^4.0.18"
568
600
  },
601
+ peerDependencies: {
602
+ "@livekit/agents": ">=1.0.0",
603
+ "@livekit/rtc-node": ">=0.13.0"
604
+ },
605
+ peerDependenciesMeta: {
606
+ "@livekit/agents": {
607
+ optional: true
608
+ },
609
+ "@livekit/rtc-node": {
610
+ optional: true
611
+ }
612
+ },
569
613
  engines: {
570
614
  node: ">=18.0.0"
571
615
  },
@@ -605,6 +649,105 @@ function createWs(url) {
605
649
  return new WS(url);
606
650
  }
607
651
  var WS_OPEN = 1;
652
+ var UPDATABLE_SETTINGS = {
653
+ cfgScale: "cfg_scale",
654
+ temperature: "temperature",
655
+ speed: "speed",
656
+ maxNewTokens: "max_new_tokens",
657
+ language: "language",
658
+ normalize: "normalize"
659
+ };
660
+ function buildSettingsUpdateBody(settings) {
661
+ const body = {};
662
+ for (const camel of Object.keys(UPDATABLE_SETTINGS)) {
663
+ const value = settings[camel];
664
+ if (value !== void 0) body[UPDATABLE_SETTINGS[camel]] = value;
665
+ }
666
+ if (Object.keys(body).length === 0) {
667
+ throw new KugelAudioError(
668
+ `updateSettings requires at least one parameter to change (one of ${Object.keys(UPDATABLE_SETTINGS).join(", ")})`
669
+ );
670
+ }
671
+ return body;
672
+ }
673
+ function parseEffectiveSettings(raw) {
674
+ const s = raw && typeof raw === "object" ? raw : {};
675
+ return {
676
+ cfgScale: s.cfg_scale,
677
+ temperature: s.temperature,
678
+ speed: s.speed,
679
+ maxNewTokens: s.max_new_tokens,
680
+ language: s.language,
681
+ normalize: s.normalize
682
+ };
683
+ }
684
+ function applyAcceptedSettingsUpdate(config, settings) {
685
+ for (const camel of Object.keys(UPDATABLE_SETTINGS)) {
686
+ if (settings[camel] !== void 0) {
687
+ config[camel] = settings[camel];
688
+ }
689
+ }
690
+ }
691
+ function sendUpdateSettings(ws, body) {
692
+ const QUIET_TIMEOUT_MS = 15e3;
693
+ return new Promise((resolve, reject) => {
694
+ let settled = false;
695
+ let timer;
696
+ const prevMessage = ws.onmessage;
697
+ const prevClose = ws.onclose;
698
+ const finish = (action) => {
699
+ if (settled) return;
700
+ settled = true;
701
+ clearTimeout(timer);
702
+ ws.onmessage = prevMessage;
703
+ ws.onclose = prevClose;
704
+ action();
705
+ };
706
+ const armQuietTimer = () => {
707
+ clearTimeout(timer);
708
+ timer = setTimeout(
709
+ () => finish(
710
+ () => reject(
711
+ new KugelAudioError(
712
+ "Timed out waiting for settings_updated acknowledgement"
713
+ )
714
+ )
715
+ ),
716
+ QUIET_TIMEOUT_MS
717
+ );
718
+ };
719
+ armQuietTimer();
720
+ ws.onmessage = (event) => {
721
+ armQuietTimer();
722
+ let data = null;
723
+ try {
724
+ const raw = typeof event.data === "string" ? event.data : event.data instanceof Buffer ? event.data.toString() : String(event.data);
725
+ data = JSON.parse(raw);
726
+ } catch {
727
+ }
728
+ if (data?.settings_updated) {
729
+ finish(() => resolve(parseEffectiveSettings(data.settings)));
730
+ return;
731
+ }
732
+ if (data?.error) {
733
+ finish(() => reject(new KugelAudioError(String(data.error))));
734
+ return;
735
+ }
736
+ if (prevMessage) prevMessage.call(ws, event);
737
+ };
738
+ ws.onclose = (event) => {
739
+ if (prevClose) prevClose.call(ws, event);
740
+ finish(
741
+ () => reject(
742
+ new ConnectionError(
743
+ "WebSocket closed before settings_updated acknowledgement"
744
+ )
745
+ )
746
+ );
747
+ };
748
+ ws.send(JSON.stringify({ update_settings: body }));
749
+ });
750
+ }
608
751
  var _languageWarningLogged = false;
609
752
  function warnIfNoLanguage(language, normalize) {
610
753
  const normEnabled = normalize === void 0 || normalize;
@@ -660,6 +803,7 @@ var VoicesResource = class {
660
803
  category: v.category,
661
804
  sex: v.sex,
662
805
  age: v.age,
806
+ quality: v.quality,
663
807
  supportedLanguages: v.supported_languages || [],
664
808
  sampleText: v.sample_text,
665
809
  avatarUrl: v.avatar_url,
@@ -1031,7 +1175,8 @@ var TTSResource = class {
1031
1175
  durationMs: data.dur_ms,
1032
1176
  generationMs: data.gen_ms,
1033
1177
  rtf: data.rtf,
1034
- error: data.error
1178
+ error: data.error,
1179
+ usage: parseSessionUsage(data) ?? void 0
1035
1180
  };
1036
1181
  pending.callbacks.onFinal?.(stats);
1037
1182
  this.pendingRequests.delete(requestId);
@@ -1118,15 +1263,19 @@ var TTSResource = class {
1118
1263
  text: options.text,
1119
1264
  model_id: options.modelId || "kugel-3",
1120
1265
  voice_id: options.voiceId,
1121
- cfg_scale: options.cfgScale ?? 2,
1266
+ cfg_scale: clampCfgScale(options.cfgScale) ?? 2,
1122
1267
  ...options.temperature !== void 0 && { temperature: options.temperature },
1123
1268
  max_new_tokens: options.maxNewTokens ?? 2048,
1124
1269
  sample_rate: options.sampleRate ?? 24e3,
1270
+ ...options.outputFormat && { output_format: options.outputFormat },
1125
1271
  normalize: options.normalize ?? true,
1126
1272
  ...options.language && { language: options.language },
1127
1273
  ...options.wordTimestamps && { word_timestamps: true },
1128
1274
  ...options.speed !== void 0 && { speed: options.speed },
1129
- ...options.projectId !== void 0 && { project_id: options.projectId }
1275
+ ...options.projectId !== void 0 && { project_id: options.projectId },
1276
+ // [] is meaningful (explicit opt-out) and must be sent; only
1277
+ // undefined (use the project default) is omitted.
1278
+ ...options.dictionaryIds !== void 0 && { dictionary_ids: options.dictionaryIds }
1130
1279
  }));
1131
1280
  });
1132
1281
  }
@@ -1144,14 +1293,18 @@ var TTSResource = class {
1144
1293
  text: options.text,
1145
1294
  model_id: options.modelId || "kugel-3",
1146
1295
  voice_id: options.voiceId,
1147
- cfg_scale: options.cfgScale ?? 2,
1296
+ cfg_scale: clampCfgScale(options.cfgScale) ?? 2,
1148
1297
  max_new_tokens: options.maxNewTokens ?? 2048,
1149
1298
  sample_rate: options.sampleRate ?? 24e3,
1299
+ ...options.outputFormat && { output_format: options.outputFormat },
1150
1300
  normalize: options.normalize ?? true,
1151
1301
  ...options.language && { language: options.language },
1152
1302
  ...options.wordTimestamps && { word_timestamps: true },
1153
1303
  ...options.speed !== void 0 && { speed: options.speed },
1154
- ...options.projectId !== void 0 && { project_id: options.projectId }
1304
+ ...options.projectId !== void 0 && { project_id: options.projectId },
1305
+ // [] is meaningful (explicit opt-out) and must be sent; only
1306
+ // undefined (use the project default) is omitted.
1307
+ ...options.dictionaryIds !== void 0 && { dictionary_ids: options.dictionaryIds }
1155
1308
  }));
1156
1309
  };
1157
1310
  ws.onmessage = (event) => {
@@ -1173,7 +1326,8 @@ var TTSResource = class {
1173
1326
  durationMs: data.dur_ms,
1174
1327
  generationMs: data.gen_ms,
1175
1328
  rtf: data.rtf,
1176
- error: data.error
1329
+ error: data.error,
1330
+ usage: parseSessionUsage(data) ?? void 0
1177
1331
  };
1178
1332
  callbacks.onFinal?.(stats);
1179
1333
  ws.close();
@@ -1343,7 +1497,11 @@ var MultiContextSession = class {
1343
1497
  this.ws = null;
1344
1498
  this.callbacks = {};
1345
1499
  this.contexts = /* @__PURE__ */ new Set();
1500
+ /** Contexts a create message has been sent for (not yet necessarily
1501
+ * confirmed by the server via context_created). */
1502
+ this.requestedContexts = /* @__PURE__ */ new Set();
1346
1503
  this._sessionId = null;
1504
+ this._contextUsage = /* @__PURE__ */ new Map();
1347
1505
  this.isStarted = false;
1348
1506
  this.config = config || {};
1349
1507
  }
@@ -1353,6 +1511,18 @@ var MultiContextSession = class {
1353
1511
  get sessionId() {
1354
1512
  return this._sessionId;
1355
1513
  }
1514
+ /**
1515
+ * Per-context usage (audio time + amount charged) for a closed context, or
1516
+ * null if that context hasn't closed yet. Each context is its own
1517
+ * conversation — use this to bill per conversation. See {@link SessionUsage}.
1518
+ */
1519
+ usageFor(contextId) {
1520
+ return this._contextUsage.get(contextId) ?? null;
1521
+ }
1522
+ /** Map of context_id → per-context usage for all closed contexts. */
1523
+ get contextUsage() {
1524
+ return new Map(this._contextUsage);
1525
+ }
1356
1526
  /**
1357
1527
  * Connect to the multi-context WebSocket endpoint.
1358
1528
  *
@@ -1406,12 +1576,19 @@ var MultiContextSession = class {
1406
1576
  };
1407
1577
  this.callbacks.onChunk?.(chunk);
1408
1578
  }
1579
+ if (data.final && data.context_id) {
1580
+ this.callbacks.onFinal?.(data.context_id);
1581
+ }
1409
1582
  if (data.context_closed) {
1410
1583
  this.contexts.delete(data.context_id);
1411
- this.callbacks.onContextClosed?.(data.context_id);
1584
+ this.requestedContexts.delete(data.context_id);
1585
+ const ctxUsage = parseSessionUsage(data) ?? void 0;
1586
+ if (ctxUsage) this._contextUsage.set(data.context_id, ctxUsage);
1587
+ this.callbacks.onContextClosed?.(data.context_id, ctxUsage);
1412
1588
  }
1413
1589
  if (data.context_timeout) {
1414
1590
  this.contexts.delete(data.context_id);
1591
+ this.requestedContexts.delete(data.context_id);
1415
1592
  this.callbacks.onContextTimeout?.(data.context_id);
1416
1593
  }
1417
1594
  if (data.session_closed) {
@@ -1451,6 +1628,7 @@ var MultiContextSession = class {
1451
1628
  this.ws = null;
1452
1629
  this.isStarted = false;
1453
1630
  this.contexts.clear();
1631
+ this.requestedContexts.clear();
1454
1632
  };
1455
1633
  });
1456
1634
  }
@@ -1461,6 +1639,7 @@ var MultiContextSession = class {
1461
1639
  if (!this.ws || this.ws.readyState !== WS_OPEN) {
1462
1640
  throw new KugelAudioError("WebSocket not connected");
1463
1641
  }
1642
+ this.requestedContexts.add(contextId);
1464
1643
  const msg = {
1465
1644
  text: " ",
1466
1645
  context_id: contextId
@@ -1468,23 +1647,27 @@ var MultiContextSession = class {
1468
1647
  if (!this.isStarted) {
1469
1648
  warnIfNoLanguage(this.config.language, this.config.normalize);
1470
1649
  if (this.config.sampleRate) msg.sample_rate = this.config.sampleRate;
1471
- if (this.config.cfgScale) msg.cfg_scale = this.config.cfgScale;
1650
+ if (this.config.outputFormat) msg.output_format = this.config.outputFormat;
1651
+ if (this.config.cfgScale !== void 0) msg.cfg_scale = clampCfgScale(this.config.cfgScale);
1472
1652
  if (this.config.temperature !== void 0) msg.temperature = this.config.temperature;
1473
1653
  if (this.config.maxNewTokens) msg.max_new_tokens = this.config.maxNewTokens;
1474
1654
  if (this.config.normalize !== void 0) msg.normalize = this.config.normalize;
1475
1655
  if (this.config.language) msg.language = this.config.language;
1656
+ if (this.config.dictionaryIds !== void 0) msg.dictionary_ids = this.config.dictionaryIds;
1476
1657
  if (this.config.inactivityTimeout) msg.inactivity_timeout = this.config.inactivityTimeout;
1477
1658
  }
1659
+ const voiceSettings = {};
1478
1660
  const voiceId = options?.voiceId || this.config.defaultVoiceId;
1479
- if (voiceId) msg.voice_id = voiceId;
1661
+ if (voiceId) voiceSettings.voice_id = voiceId;
1480
1662
  if (options?.voiceSettings) {
1481
- msg.voice_settings = {
1482
- stability: options.voiceSettings.stability,
1483
- similarity_boost: options.voiceSettings.similarityBoost,
1484
- style: options.voiceSettings.style,
1485
- use_speaker_boost: options.voiceSettings.useSpeakerBoost,
1486
- speed: options.voiceSettings.speed
1487
- };
1663
+ voiceSettings.stability = options.voiceSettings.stability;
1664
+ voiceSettings.similarity_boost = options.voiceSettings.similarityBoost;
1665
+ voiceSettings.style = options.voiceSettings.style;
1666
+ voiceSettings.use_speaker_boost = options.voiceSettings.useSpeakerBoost;
1667
+ voiceSettings.speed = options.voiceSettings.speed;
1668
+ }
1669
+ if (Object.keys(voiceSettings).length > 0) {
1670
+ msg.voice_settings = voiceSettings;
1488
1671
  }
1489
1672
  this.ws.send(JSON.stringify(msg));
1490
1673
  }
@@ -1495,7 +1678,7 @@ var MultiContextSession = class {
1495
1678
  if (!this.ws || this.ws.readyState !== WS_OPEN) {
1496
1679
  throw new KugelAudioError("WebSocket not connected");
1497
1680
  }
1498
- if (!this.contexts.has(contextId) && !this.isStarted) {
1681
+ if (!this.requestedContexts.has(contextId) && !this.contexts.has(contextId)) {
1499
1682
  this.createContext(contextId);
1500
1683
  }
1501
1684
  this.ws.send(JSON.stringify({
@@ -1542,6 +1725,33 @@ var MultiContextSession = class {
1542
1725
  context_id: contextId
1543
1726
  }));
1544
1727
  }
1728
+ /**
1729
+ * Change the session's generation parameters mid-connection (KUG-1166).
1730
+ *
1731
+ * Session-scoped (there is no `contextId`): the update applies to contexts
1732
+ * started **after** it — a context already streaming keeps the settings it
1733
+ * began with, since generation parameters are bound when a context's engine
1734
+ * session opens. With the common one-context-per-turn pattern that means it
1735
+ * takes effect on the next turn. Per-context `cfgScale` / `maxNewTokens`
1736
+ * passed to {@link createContext} still win for that context.
1737
+ *
1738
+ * Resolves with the generation parameters now in effect (the server's echo).
1739
+ *
1740
+ * @throws {KugelAudioError} if no field is provided, the socket is not open,
1741
+ * or the server rejects the update.
1742
+ */
1743
+ updateSettings(settings) {
1744
+ if (!this.ws || this.ws.readyState !== WS_OPEN) {
1745
+ throw new KugelAudioError(
1746
+ "MultiContextSession not connected. Call connect() first."
1747
+ );
1748
+ }
1749
+ const body = buildSettingsUpdateBody(settings);
1750
+ return sendUpdateSettings(this.ws, body).then((effective) => {
1751
+ applyAcceptedSettingsUpdate(this.config, settings);
1752
+ return effective;
1753
+ });
1754
+ }
1545
1755
  /**
1546
1756
  * Close the session and all contexts.
1547
1757
  */
@@ -1553,6 +1763,7 @@ var MultiContextSession = class {
1553
1763
  this.ws = null;
1554
1764
  this.isStarted = false;
1555
1765
  this.contexts.clear();
1766
+ this.requestedContexts.clear();
1556
1767
  }
1557
1768
  /**
1558
1769
  * Get active context IDs.
@@ -1571,10 +1782,19 @@ var StreamingSession = class {
1571
1782
  constructor(client, config, callbacks) {
1572
1783
  this.ws = null;
1573
1784
  this.configSent = false;
1785
+ this._lastUsage = null;
1574
1786
  this.client = client;
1575
1787
  this.config = config;
1576
1788
  this.callbacks = callbacks;
1577
1789
  }
1790
+ /**
1791
+ * Per-session usage from the most recently closed session, or null before
1792
+ * the first session closes. Use this to bill your own customers per
1793
+ * conversation. See {@link SessionUsage}.
1794
+ */
1795
+ get lastUsage() {
1796
+ return this._lastUsage;
1797
+ }
1578
1798
  /**
1579
1799
  * Open the WebSocket connection and authenticate.
1580
1800
  *
@@ -1638,7 +1858,15 @@ var StreamingSession = class {
1638
1858
  if (data.interrupted) {
1639
1859
  this.callbacks.onInterrupted?.();
1640
1860
  }
1861
+ if (data.final) {
1862
+ this.callbacks.onFinal?.(
1863
+ data.total_audio_seconds ?? 0,
1864
+ data.total_text_chunks ?? 0,
1865
+ data.total_audio_chunks ?? 0
1866
+ );
1867
+ }
1641
1868
  if (data.session_closed) {
1869
+ this._lastUsage = parseSessionUsage(data);
1642
1870
  this.callbacks.onSessionClosed?.(
1643
1871
  data.total_audio_seconds ?? 0,
1644
1872
  data.total_text_chunks ?? 0,
@@ -1702,10 +1930,11 @@ var StreamingSession = class {
1702
1930
  if (!this.configSent) {
1703
1931
  if (this.config.voiceId !== void 0) msg.voice_id = this.config.voiceId;
1704
1932
  if (this.config.modelId !== void 0) msg.model_id = this.config.modelId;
1705
- if (this.config.cfgScale !== void 0) msg.cfg_scale = this.config.cfgScale;
1933
+ if (this.config.cfgScale !== void 0) msg.cfg_scale = clampCfgScale(this.config.cfgScale);
1706
1934
  if (this.config.temperature !== void 0) msg.temperature = this.config.temperature;
1707
1935
  if (this.config.maxNewTokens !== void 0) msg.max_new_tokens = this.config.maxNewTokens;
1708
1936
  if (this.config.sampleRate !== void 0) msg.sample_rate = this.config.sampleRate;
1937
+ if (this.config.outputFormat !== void 0) msg.output_format = this.config.outputFormat;
1709
1938
  if (this.config.flushTimeoutMs !== void 0) msg.flush_timeout_ms = this.config.flushTimeoutMs;
1710
1939
  if (this.config.maxBufferLength !== void 0) msg.max_buffer_length = this.config.maxBufferLength;
1711
1940
  if (this.config.normalize !== void 0) msg.normalize = this.config.normalize;
@@ -1714,6 +1943,7 @@ var StreamingSession = class {
1714
1943
  if (this.config.autoMode !== void 0) msg.auto_mode = this.config.autoMode;
1715
1944
  if (this.config.chunkLengthSchedule?.length) msg.chunk_length_schedule = this.config.chunkLengthSchedule;
1716
1945
  if (this.config.speed !== void 0) msg.speed = this.config.speed;
1946
+ if (this.config.dictionaryIds !== void 0) msg.dictionary_ids = this.config.dictionaryIds;
1717
1947
  this.configSent = true;
1718
1948
  }
1719
1949
  this.ws.send(JSON.stringify(msg));
@@ -1849,6 +2079,39 @@ var StreamingSession = class {
1849
2079
  Object.assign(this.config, config);
1850
2080
  this.configSent = false;
1851
2081
  }
2082
+ /**
2083
+ * Change generation parameters mid-connection without reconnecting (KUG-1166).
2084
+ *
2085
+ * Sends an `update_settings` message and resolves with the generation
2086
+ * parameters now in effect (the server's echo). Only the six fields of
2087
+ * {@link SettingsUpdate} are updatable; identity / audio-format fields
2088
+ * (`voiceId`, `modelId`, `sampleRate`, `outputFormat`, `dictionaryIds`) are
2089
+ * fixed for the connection — change those with {@link updateConfig} after
2090
+ * {@link endSession} instead.
2091
+ *
2092
+ * The change applies to the **next turn**: a turn already streaming keeps the
2093
+ * settings it started with, so call this between turns.
2094
+ *
2095
+ * @example
2096
+ * ```typescript
2097
+ * const effective = await session.updateSettings({ cfgScale: 1.5, speed: 1.1 });
2098
+ * ```
2099
+ *
2100
+ * @throws {KugelAudioError} if no field is provided, the socket is not open,
2101
+ * or the server rejects the update (e.g. a value out of range).
2102
+ */
2103
+ updateSettings(settings) {
2104
+ if (!this.ws || this.ws.readyState !== WS_OPEN) {
2105
+ throw new KugelAudioError(
2106
+ "StreamingSession not connected. Call connect() first."
2107
+ );
2108
+ }
2109
+ const body = buildSettingsUpdateBody(settings);
2110
+ return sendUpdateSettings(this.ws, body).then((effective) => {
2111
+ applyAcceptedSettingsUpdate(this.config, settings);
2112
+ return effective;
2113
+ });
2114
+ }
1852
2115
  /**
1853
2116
  * Close the session and the WebSocket connection.
1854
2117
  *
@@ -2093,5 +2356,6 @@ var KugelAudio = class _KugelAudio {
2093
2356
  classifyWsHandshakeError,
2094
2357
  createWavBlob,
2095
2358
  createWavFile,
2096
- decodePCM16
2359
+ decodePCM16,
2360
+ parseSessionUsage
2097
2361
  });