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
package/dist/index.mjs
CHANGED
|
@@ -1,9 +1,25 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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/
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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.
|
|
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.
|
|
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)
|
|
1288
|
+
if (voiceId) voiceSettings.voice_id = voiceId;
|
|
1442
1289
|
if (options?.voiceSettings) {
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
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.
|
|
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
|
};
|