kugelaudio 0.8.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,211 +202,6 @@ 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
205
  // src/types.ts
395
206
  function parseSessionUsage(data) {
396
207
  const raw = data.usage;
@@ -408,62 +219,6 @@ function parseSessionUsage(data) {
408
219
  };
409
220
  }
410
221
 
411
- // src/utils.ts
412
- function base64ToArrayBuffer(base64) {
413
- if (typeof atob === "function") {
414
- const binary = atob(base64);
415
- const bytes = new Uint8Array(binary.length);
416
- for (let i = 0; i < binary.length; i++) {
417
- bytes[i] = binary.charCodeAt(i);
418
- }
419
- return bytes.buffer;
420
- } else {
421
- const buffer = Buffer.from(base64, "base64");
422
- return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
423
- }
424
- }
425
- function decodePCM16(base64) {
426
- const buffer = base64ToArrayBuffer(base64);
427
- const int16 = new Int16Array(buffer);
428
- const float32 = new Float32Array(int16.length);
429
- for (let i = 0; i < int16.length; i++) {
430
- float32[i] = int16[i] / 32768;
431
- }
432
- return float32;
433
- }
434
- function createWavFile(audio, sampleRate) {
435
- const dataSize = audio.byteLength;
436
- const fileSize = 44 + dataSize;
437
- const buffer = new ArrayBuffer(fileSize);
438
- const view = new DataView(buffer);
439
- writeString(view, 0, "RIFF");
440
- view.setUint32(4, fileSize - 8, true);
441
- writeString(view, 8, "WAVE");
442
- writeString(view, 12, "fmt ");
443
- view.setUint32(16, 16, true);
444
- view.setUint16(20, 1, true);
445
- view.setUint16(22, 1, true);
446
- view.setUint32(24, sampleRate, true);
447
- view.setUint32(28, sampleRate * 2, true);
448
- view.setUint16(32, 2, true);
449
- view.setUint16(34, 16, true);
450
- writeString(view, 36, "data");
451
- view.setUint32(40, dataSize, true);
452
- const audioBytes = new Uint8Array(audio);
453
- const wavBytes = new Uint8Array(buffer);
454
- wavBytes.set(audioBytes, 44);
455
- return buffer;
456
- }
457
- function writeString(view, offset, str) {
458
- for (let i = 0; i < str.length; i++) {
459
- view.setUint8(offset + i, str.charCodeAt(i));
460
- }
461
- }
462
- function createWavBlob(audio, sampleRate) {
463
- const wavBuffer = createWavFile(audio, sampleRate);
464
- return new Blob([wavBuffer], { type: "audio/wav" });
465
- }
466
-
467
222
  // src/websocket.ts
468
223
  var _cachedWs = null;
469
224
  function isNodeJs() {
@@ -491,69 +246,6 @@ function getWebSocket() {
491
246
  );
492
247
  }
493
248
 
494
- // package.json
495
- var package_default = {
496
- name: "kugelaudio",
497
- version: "0.8.0",
498
- description: "Official JavaScript/TypeScript SDK for KugelAudio TTS API",
499
- main: "dist/index.js",
500
- module: "dist/index.mjs",
501
- types: "dist/index.d.ts",
502
- exports: {
503
- ".": {
504
- types: "./dist/index.d.ts",
505
- import: "./dist/index.mjs",
506
- require: "./dist/index.js"
507
- }
508
- },
509
- files: [
510
- "dist",
511
- "src",
512
- "LICENSE",
513
- "CHANGELOG.md"
514
- ],
515
- scripts: {
516
- build: "tsup src/index.ts --format cjs,esm --dts",
517
- dev: "tsup src/index.ts --format cjs,esm --dts --watch",
518
- lint: "eslint src/",
519
- test: "vitest run",
520
- "test:watch": "vitest",
521
- prepublishOnly: "npm run build"
522
- },
523
- keywords: [
524
- "tts",
525
- "text-to-speech",
526
- "audio",
527
- "streaming",
528
- "websocket",
529
- "kugelaudio"
530
- ],
531
- author: "KugelAudio <hello@kugelaudio.com>",
532
- license: "MIT",
533
- repository: {
534
- type: "git",
535
- url: "https://github.com/Kugelaudio/KugelAudio",
536
- directory: "sdks/js"
537
- },
538
- homepage: "https://kugelaudio.com",
539
- bugs: {
540
- url: "https://github.com/Kugelaudio/KugelAudio/issues"
541
- },
542
- devDependencies: {
543
- "@types/node": "^25.3.2",
544
- tsup: "^8.0.0",
545
- typescript: "^6.0.2",
546
- vitest: "^4.0.18"
547
- },
548
- engines: {
549
- node: ">=18.0.0"
550
- },
551
- dependencies: {
552
- tsx: "^4.21.0",
553
- ws: "^8.18.0"
554
- }
555
- };
556
-
557
249
  // src/client.ts
558
250
  var DEFAULT_API_URL = "https://api.kugelaudio.com";
559
251
  var EU_API_URL = "https://api.eu.kugelaudio.com";
@@ -584,6 +276,105 @@ function createWs(url) {
584
276
  return new WS(url);
585
277
  }
586
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
+ }
587
378
  var _languageWarningLogged = false;
588
379
  function warnIfNoLanguage(language, normalize) {
589
380
  const normEnabled = normalize === void 0 || normalize;
@@ -639,6 +430,7 @@ var VoicesResource = class {
639
430
  category: v.category,
640
431
  sex: v.sex,
641
432
  age: v.age,
433
+ quality: v.quality,
642
434
  supportedLanguages: v.supported_languages || [],
643
435
  sampleText: v.sample_text,
644
436
  avatarUrl: v.avatar_url,
@@ -1098,7 +890,7 @@ var TTSResource = class {
1098
890
  text: options.text,
1099
891
  model_id: options.modelId || "kugel-3",
1100
892
  voice_id: options.voiceId,
1101
- cfg_scale: options.cfgScale ?? 2,
893
+ cfg_scale: clampCfgScale(options.cfgScale) ?? 2,
1102
894
  ...options.temperature !== void 0 && { temperature: options.temperature },
1103
895
  max_new_tokens: options.maxNewTokens ?? 2048,
1104
896
  sample_rate: options.sampleRate ?? 24e3,
@@ -1128,7 +920,7 @@ var TTSResource = class {
1128
920
  text: options.text,
1129
921
  model_id: options.modelId || "kugel-3",
1130
922
  voice_id: options.voiceId,
1131
- cfg_scale: options.cfgScale ?? 2,
923
+ cfg_scale: clampCfgScale(options.cfgScale) ?? 2,
1132
924
  max_new_tokens: options.maxNewTokens ?? 2048,
1133
925
  sample_rate: options.sampleRate ?? 24e3,
1134
926
  ...options.outputFormat && { output_format: options.outputFormat },
@@ -1483,7 +1275,7 @@ var MultiContextSession = class {
1483
1275
  warnIfNoLanguage(this.config.language, this.config.normalize);
1484
1276
  if (this.config.sampleRate) msg.sample_rate = this.config.sampleRate;
1485
1277
  if (this.config.outputFormat) msg.output_format = this.config.outputFormat;
1486
- if (this.config.cfgScale) msg.cfg_scale = this.config.cfgScale;
1278
+ if (this.config.cfgScale !== void 0) msg.cfg_scale = clampCfgScale(this.config.cfgScale);
1487
1279
  if (this.config.temperature !== void 0) msg.temperature = this.config.temperature;
1488
1280
  if (this.config.maxNewTokens) msg.max_new_tokens = this.config.maxNewTokens;
1489
1281
  if (this.config.normalize !== void 0) msg.normalize = this.config.normalize;
@@ -1560,6 +1352,33 @@ var MultiContextSession = class {
1560
1352
  context_id: contextId
1561
1353
  }));
1562
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
+ }
1563
1382
  /**
1564
1383
  * Close the session and all contexts.
1565
1384
  */
@@ -1738,7 +1557,7 @@ var StreamingSession = class {
1738
1557
  if (!this.configSent) {
1739
1558
  if (this.config.voiceId !== void 0) msg.voice_id = this.config.voiceId;
1740
1559
  if (this.config.modelId !== void 0) msg.model_id = this.config.modelId;
1741
- 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);
1742
1561
  if (this.config.temperature !== void 0) msg.temperature = this.config.temperature;
1743
1562
  if (this.config.maxNewTokens !== void 0) msg.max_new_tokens = this.config.maxNewTokens;
1744
1563
  if (this.config.sampleRate !== void 0) msg.sample_rate = this.config.sampleRate;
@@ -1887,6 +1706,39 @@ var StreamingSession = class {
1887
1706
  Object.assign(this.config, config);
1888
1707
  this.configSent = false;
1889
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
+ }
1890
1742
  /**
1891
1743
  * Close the session and the WebSocket connection.
1892
1744
  *