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.
@@ -0,0 +1,994 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __typeError = (msg) => {
7
+ throw TypeError(msg);
8
+ };
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
22
+ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
23
+ var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
24
+ var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
25
+ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
26
+ var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
27
+
28
+ // src/livekit/index.ts
29
+ var livekit_exports = {};
30
+ __export(livekit_exports, {
31
+ ChunkedStream: () => ChunkedStream,
32
+ DEFAULT_CFG_SCALE: () => DEFAULT_CFG_SCALE,
33
+ DEFAULT_MAX_NEW_TOKENS: () => DEFAULT_MAX_NEW_TOKENS,
34
+ DEFAULT_MODEL: () => DEFAULT_MODEL,
35
+ DEFAULT_SAMPLE_RATE: () => DEFAULT_SAMPLE_RATE,
36
+ DEFAULT_VOICE_ID: () => DEFAULT_VOICE_ID,
37
+ SUPPORTED_LANGUAGES: () => SUPPORTED_LANGUAGES,
38
+ SUPPORTED_SAMPLE_RATES: () => SUPPORTED_SAMPLE_RATES,
39
+ SynthesizeStream: () => SynthesizeStream,
40
+ TTS: () => TTS,
41
+ validateLanguage: () => validateLanguage
42
+ });
43
+ module.exports = __toCommonJS(livekit_exports);
44
+
45
+ // src/livekit/tts.ts
46
+ var import_agents = require("@livekit/agents");
47
+ var import_ws = require("ws");
48
+
49
+ // src/errors.ts
50
+ var ErrorCodes = {
51
+ UNAUTHORIZED: "UNAUTHORIZED",
52
+ RATE_LIMITED: "RATE_LIMITED",
53
+ INSUFFICIENT_CREDITS: "INSUFFICIENT_CREDITS",
54
+ MODEL_UNAVAILABLE: "MODEL_UNAVAILABLE",
55
+ EMPTY_AUDIO: "EMPTY_AUDIO",
56
+ VALIDATION: "VALIDATION_ERROR",
57
+ INTERNAL: "INTERNAL_ERROR",
58
+ NOT_FOUND: "NOT_FOUND",
59
+ MISSING_VOICE_ID: "MISSING_VOICE_ID",
60
+ TOO_MANY_CONTEXTS: "TOO_MANY_CONTEXTS"
61
+ };
62
+ var API_KEYS_URL = "https://app.kugelaudio.com/settings/api-keys";
63
+ var BILLING_URL = "https://app.kugelaudio.com/billing";
64
+ var KugelAudioError = class _KugelAudioError extends Error {
65
+ constructor(message, options = {}) {
66
+ super(options.requestId ? `${message} (request_id: ${options.requestId})` : message);
67
+ this.name = "KugelAudioError";
68
+ this.statusCode = options.statusCode;
69
+ this.errorCode = options.errorCode;
70
+ this.requestId = options.requestId;
71
+ this.retryAfter = options.retryAfter;
72
+ Object.setPrototypeOf(this, _KugelAudioError.prototype);
73
+ }
74
+ };
75
+ var AuthenticationError = class _AuthenticationError extends KugelAudioError {
76
+ constructor(message, options = {}) {
77
+ super(
78
+ message ?? `KugelAudio rejected the API key. Check it is current at ${API_KEYS_URL}.`,
79
+ { statusCode: 401, errorCode: ErrorCodes.UNAUTHORIZED, ...options }
80
+ );
81
+ this.name = "AuthenticationError";
82
+ Object.setPrototypeOf(this, _AuthenticationError.prototype);
83
+ }
84
+ };
85
+ var RateLimitError = class _RateLimitError extends KugelAudioError {
86
+ constructor(message, options = {}) {
87
+ const msg = message ?? (options.retryAfter ? `KugelAudio rate limit hit; retry after ${options.retryAfter}s.` : "KugelAudio rate limit hit; retry shortly.");
88
+ super(msg, { statusCode: 429, errorCode: ErrorCodes.RATE_LIMITED, ...options });
89
+ this.name = "RateLimitError";
90
+ Object.setPrototypeOf(this, _RateLimitError.prototype);
91
+ }
92
+ };
93
+ var InsufficientCreditsError = class _InsufficientCreditsError extends KugelAudioError {
94
+ constructor(message, options = {}) {
95
+ super(
96
+ message ?? `Your KugelAudio account is out of credits. Top up at ${BILLING_URL}.`,
97
+ { statusCode: 402, errorCode: ErrorCodes.INSUFFICIENT_CREDITS, ...options }
98
+ );
99
+ this.name = "InsufficientCreditsError";
100
+ Object.setPrototypeOf(this, _InsufficientCreditsError.prototype);
101
+ }
102
+ };
103
+ var ValidationError = class _ValidationError extends KugelAudioError {
104
+ constructor(message, options = {}) {
105
+ super(message, { statusCode: 400, errorCode: ErrorCodes.VALIDATION, ...options });
106
+ this.name = "ValidationError";
107
+ Object.setPrototypeOf(this, _ValidationError.prototype);
108
+ }
109
+ };
110
+ var ConnectionError = class _ConnectionError extends KugelAudioError {
111
+ constructor(message, options = {}) {
112
+ super(message, { statusCode: 503, ...options });
113
+ this.name = "ConnectionError";
114
+ Object.setPrototypeOf(this, _ConnectionError.prototype);
115
+ }
116
+ };
117
+ var NotFoundError = class _NotFoundError extends KugelAudioError {
118
+ constructor(message, options = {}) {
119
+ super(message ?? "Not found.", {
120
+ statusCode: 404,
121
+ errorCode: ErrorCodes.NOT_FOUND,
122
+ ...options
123
+ });
124
+ this.name = "NotFoundError";
125
+ Object.setPrototypeOf(this, _NotFoundError.prototype);
126
+ }
127
+ };
128
+ function build(status, errorCode, message, opts = {}) {
129
+ const common = { ...opts };
130
+ if (status !== void 0) common.statusCode = status;
131
+ if (errorCode !== void 0) common.errorCode = errorCode;
132
+ if (errorCode === ErrorCodes.UNAUTHORIZED || status === 401) {
133
+ return new AuthenticationError(message || void 0, common);
134
+ }
135
+ if (errorCode === ErrorCodes.INSUFFICIENT_CREDITS || status === 402) {
136
+ return new InsufficientCreditsError(message || void 0, common);
137
+ }
138
+ if (errorCode === ErrorCodes.RATE_LIMITED || errorCode === ErrorCodes.TOO_MANY_CONTEXTS || status === 429) {
139
+ return new RateLimitError(message || void 0, common);
140
+ }
141
+ if (errorCode === ErrorCodes.VALIDATION || errorCode === ErrorCodes.MISSING_VOICE_ID || status === 400) {
142
+ return new ValidationError(message || "Request validation failed.", common);
143
+ }
144
+ if (errorCode === ErrorCodes.MODEL_UNAVAILABLE || status === 503) {
145
+ const detail = message || "service temporarily unavailable";
146
+ return new ConnectionError(
147
+ `KugelAudio is temporarily unavailable: ${detail}. Retry shortly.`,
148
+ common
149
+ );
150
+ }
151
+ if (errorCode === ErrorCodes.NOT_FOUND || status === 404) {
152
+ return new NotFoundError(message || void 0, common);
153
+ }
154
+ return new KugelAudioError(message || `HTTP ${status}`, common);
155
+ }
156
+ function classifyWsFrame(data) {
157
+ const errorCode = data.error_code;
158
+ const message = data.error ?? "Server reported an error.";
159
+ const status = typeof data.code === "number" ? data.code : void 0;
160
+ const retryAfter = typeof data.retry_after === "number" ? data.retry_after : void 0;
161
+ return build(status, errorCode, message, { retryAfter });
162
+ }
163
+ function classifyWsHandshakeError(err) {
164
+ if (!err || typeof err !== "object") return null;
165
+ const e = err;
166
+ let status;
167
+ if (typeof e.statusCode === "number") {
168
+ status = e.statusCode;
169
+ }
170
+ if (status === void 0 && typeof e.message === "string") {
171
+ const m = e.message.match(/Unexpected server response:\s*(\d{3})/i);
172
+ if (m) status = Number(m[1]);
173
+ }
174
+ if (status === void 0) return null;
175
+ if (status === 403) {
176
+ return new AuthenticationError();
177
+ }
178
+ return build(status, void 0, typeof e.message === "string" ? e.message : "");
179
+ }
180
+
181
+ // src/utils.ts
182
+ var MIN_CFG_SCALE = 1.2;
183
+ var MAX_CFG_SCALE = 2.5;
184
+ function clampCfgScale(cfgScale) {
185
+ if (cfgScale === void 0) return void 0;
186
+ return Math.min(MAX_CFG_SCALE, Math.max(MIN_CFG_SCALE, cfgScale));
187
+ }
188
+
189
+ // src/livekit/models.ts
190
+ var DEFAULT_MODEL = "kugel-3";
191
+ var SUPPORTED_SAMPLE_RATES = [24e3, 22050, 16e3, 8e3];
192
+ var DEFAULT_SAMPLE_RATE = 24e3;
193
+ var DEFAULT_VOICE_ID = null;
194
+ var DEFAULT_CFG_SCALE = 2;
195
+ var DEFAULT_MAX_NEW_TOKENS = 2048;
196
+ var SUPPORTED_LANGUAGES = /* @__PURE__ */ new Set([
197
+ // Germanic
198
+ "de",
199
+ "en",
200
+ "nl",
201
+ "sv",
202
+ "da",
203
+ "no",
204
+ // Romance
205
+ "fr",
206
+ "es",
207
+ "it",
208
+ "pt",
209
+ "ro",
210
+ // Slavic
211
+ "pl",
212
+ "cs",
213
+ "uk",
214
+ "bg",
215
+ "sk",
216
+ "sl",
217
+ "hr",
218
+ "sr",
219
+ // Uralic
220
+ "fi",
221
+ "hu",
222
+ // Other European
223
+ "el",
224
+ "tr",
225
+ "ru",
226
+ // CJK + SEA
227
+ "zh",
228
+ "ja",
229
+ "ko",
230
+ "vi",
231
+ "yue",
232
+ "th",
233
+ "id",
234
+ "ms",
235
+ // Semitic + Indic + Other
236
+ "ar",
237
+ "hi",
238
+ "he",
239
+ "fa",
240
+ "ur",
241
+ "bn",
242
+ "ta"
243
+ ]);
244
+ function validateLanguage(language) {
245
+ if (language === null || language === void 0) return void 0;
246
+ if (!SUPPORTED_LANGUAGES.has(language)) {
247
+ const supported = [...SUPPORTED_LANGUAGES].sort().join(", ");
248
+ throw new Error(
249
+ `language must be a supported ISO 639-1 code (${supported}), got '${language}'. Note: BCP 47 tags like 'de-DE' are not accepted \u2014 use 'de' instead.`
250
+ );
251
+ }
252
+ return language;
253
+ }
254
+
255
+ // package.json
256
+ var package_default = {
257
+ name: "kugelaudio",
258
+ version: "0.9.0",
259
+ description: "Official JavaScript/TypeScript SDK for KugelAudio TTS API",
260
+ main: "dist/index.js",
261
+ module: "dist/index.mjs",
262
+ types: "dist/index.d.ts",
263
+ exports: {
264
+ ".": {
265
+ types: "./dist/index.d.ts",
266
+ import: "./dist/index.mjs",
267
+ require: "./dist/index.js"
268
+ },
269
+ "./livekit": {
270
+ types: "./dist/livekit/index.d.ts",
271
+ import: "./dist/livekit/index.mjs",
272
+ require: "./dist/livekit/index.js"
273
+ }
274
+ },
275
+ files: [
276
+ "dist",
277
+ "src",
278
+ "LICENSE",
279
+ "CHANGELOG.md"
280
+ ],
281
+ scripts: {
282
+ build: "tsup src/index.ts src/livekit/index.ts --format cjs,esm --dts",
283
+ dev: "tsup src/index.ts src/livekit/index.ts --format cjs,esm --dts --watch",
284
+ lint: "eslint src/",
285
+ test: "vitest run",
286
+ "test:watch": "vitest",
287
+ prepublishOnly: "npm run build"
288
+ },
289
+ keywords: [
290
+ "tts",
291
+ "text-to-speech",
292
+ "audio",
293
+ "streaming",
294
+ "websocket",
295
+ "kugelaudio"
296
+ ],
297
+ author: "KugelAudio <hello@kugelaudio.com>",
298
+ license: "MIT",
299
+ repository: {
300
+ type: "git",
301
+ url: "https://github.com/Kugelaudio/KugelAudio",
302
+ directory: "sdks/js"
303
+ },
304
+ homepage: "https://kugelaudio.com",
305
+ bugs: {
306
+ url: "https://github.com/Kugelaudio/KugelAudio/issues"
307
+ },
308
+ devDependencies: {
309
+ "@livekit/agents": "^1.4.11",
310
+ "@livekit/rtc-node": "^0.13.30",
311
+ "@types/node": "^25.3.2",
312
+ "@types/ws": "^8.18.1",
313
+ tsup: "^8.0.0",
314
+ typescript: "^6.0.2",
315
+ vitest: "^4.0.18"
316
+ },
317
+ peerDependencies: {
318
+ "@livekit/agents": ">=1.0.0",
319
+ "@livekit/rtc-node": ">=0.13.0"
320
+ },
321
+ peerDependenciesMeta: {
322
+ "@livekit/agents": {
323
+ optional: true
324
+ },
325
+ "@livekit/rtc-node": {
326
+ optional: true
327
+ }
328
+ },
329
+ engines: {
330
+ node: ">=18.0.0"
331
+ },
332
+ dependencies: {
333
+ tsx: "^4.21.0",
334
+ ws: "^8.18.0"
335
+ }
336
+ };
337
+
338
+ // src/livekit/wire.ts
339
+ var SDK_NAME = "js";
340
+ var SDK_VERSION = package_default.version;
341
+ function appendSdkQuery(url) {
342
+ const separator = url.includes("?") ? "&" : "?";
343
+ return `${url}${separator}sdk=${encodeURIComponent(SDK_NAME)}&sdk_version=${encodeURIComponent(SDK_VERSION)}`;
344
+ }
345
+ function buildMultiWsUrl(baseUrl, apiKey) {
346
+ const wsBase = baseUrl.replace("https://", "wss://").replace("http://", "ws://");
347
+ return appendSdkQuery(`${wsBase}/ws/tts/multi?api_key=${apiKey}`);
348
+ }
349
+ function buildTextPayload(contextId, text, opts, { flush = false, includeConfig = false } = {}) {
350
+ const msg = { text, context_id: contextId };
351
+ if (flush) msg.flush = true;
352
+ if (includeConfig) {
353
+ msg.model_id = opts.model;
354
+ msg.sample_rate = opts.sampleRate;
355
+ msg.word_timestamps = opts.wordTimestamps;
356
+ msg.normalize = opts.normalize;
357
+ if (opts.language !== void 0) msg.language = opts.language;
358
+ const voiceSettings = {};
359
+ if (opts.voiceId !== null && opts.voiceId !== void 0) {
360
+ voiceSettings.voice_id = opts.voiceId;
361
+ }
362
+ if (opts.cfgScale !== 2) voiceSettings.cfg_scale = opts.cfgScale;
363
+ if (opts.maxNewTokens !== 2048) voiceSettings.max_new_tokens = opts.maxNewTokens;
364
+ if (Object.keys(voiceSettings).length > 0) msg.voice_settings = voiceSettings;
365
+ }
366
+ return msg;
367
+ }
368
+ function buildClosePayload(contextId, immediate = false) {
369
+ const msg = {
370
+ close_context: true,
371
+ context_id: contextId
372
+ };
373
+ if (immediate) msg.immediate = true;
374
+ return msg;
375
+ }
376
+ function wordTimestampsToTimed(timestamps) {
377
+ return timestamps.map((ts) => ({
378
+ text: ts.word,
379
+ startTime: ts.start_ms / 1e3,
380
+ endTime: ts.end_ms / 1e3
381
+ }));
382
+ }
383
+
384
+ // src/livekit/tts.ts
385
+ var NUM_CHANNELS = 1;
386
+ var DEFAULT_BASE_URL = "https://api.kugelaudio.com";
387
+ var EU_BASE_URL = "https://api.eu.kugelaudio.com";
388
+ function parseApiKey(apiKey) {
389
+ for (const prefix of ["eu-", "us-", "global-"]) {
390
+ if (apiKey.startsWith(prefix)) {
391
+ return { cleanKey: apiKey.slice(prefix.length), region: prefix.slice(0, -1) };
392
+ }
393
+ }
394
+ return { cleanKey: apiKey };
395
+ }
396
+ function apiStatusErrorFromFrame(data, requestId) {
397
+ const err = classifyWsFrame(data);
398
+ const statusCode = err.statusCode ?? (typeof data.code === "number" ? data.code : 500);
399
+ return new import_agents.APIStatusError({
400
+ message: err.message,
401
+ options: { statusCode, requestId, body: data }
402
+ });
403
+ }
404
+ function toLiveKitConnError(err) {
405
+ if (err instanceof Error && /handshake has timed out/i.test(err.message)) {
406
+ return new import_agents.APITimeoutError({ message: "Timed out connecting to /ws/tts/multi" });
407
+ }
408
+ const typed = classifyWsHandshakeError(err);
409
+ if (typed && typed.statusCode !== void 0) {
410
+ return new import_agents.APIStatusError({
411
+ message: typed.message,
412
+ options: { statusCode: typed.statusCode }
413
+ });
414
+ }
415
+ const message = err instanceof Error ? err.message : String(err);
416
+ return new import_agents.APIConnectionError({ message: `Failed to connect to /ws/tts/multi: ${message}` });
417
+ }
418
+ function bufferToArrayBuffer(buf) {
419
+ return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
420
+ }
421
+ function deferred() {
422
+ const d = { settled: false };
423
+ d.promise = new Promise((resolve, reject) => {
424
+ d.resolve = () => {
425
+ if (d.settled) return;
426
+ d.settled = true;
427
+ resolve();
428
+ };
429
+ d.reject = (err) => {
430
+ if (d.settled) return;
431
+ d.settled = true;
432
+ reject(err);
433
+ };
434
+ });
435
+ d.promise.catch(() => {
436
+ });
437
+ return d;
438
+ }
439
+ function delay(ms) {
440
+ return new Promise((resolve) => setTimeout(resolve, ms));
441
+ }
442
+ var _tail;
443
+ var Mutex = class {
444
+ constructor() {
445
+ __privateAdd(this, _tail, Promise.resolve());
446
+ }
447
+ async lock() {
448
+ let release;
449
+ const next = new Promise((resolve) => {
450
+ release = resolve;
451
+ });
452
+ const prev = __privateGet(this, _tail);
453
+ __privateSet(this, _tail, __privateGet(this, _tail).then(() => next));
454
+ await prev;
455
+ return release;
456
+ }
457
+ };
458
+ _tail = new WeakMap();
459
+ var _bstream, _lastFrame, _pendingTimed, _ended, _AudioSink_instances, emit_fn;
460
+ var AudioSink = class {
461
+ constructor(sampleRate, put, requestId, segmentId) {
462
+ this.put = put;
463
+ this.requestId = requestId;
464
+ this.segmentId = segmentId;
465
+ __privateAdd(this, _AudioSink_instances);
466
+ __privateAdd(this, _bstream);
467
+ __privateAdd(this, _lastFrame);
468
+ __privateAdd(this, _pendingTimed, []);
469
+ __privateAdd(this, _ended, false);
470
+ __privateSet(this, _bstream, new import_agents.AudioByteStream(sampleRate, NUM_CHANNELS));
471
+ }
472
+ pushAudio(bytes) {
473
+ if (__privateGet(this, _ended)) return;
474
+ for (const frame of __privateGet(this, _bstream).write(bufferToArrayBuffer(bytes))) {
475
+ __privateMethod(this, _AudioSink_instances, emit_fn).call(this, false);
476
+ __privateSet(this, _lastFrame, frame);
477
+ }
478
+ }
479
+ pushTimed(words) {
480
+ if (__privateGet(this, _ended)) return;
481
+ __privateGet(this, _pendingTimed).push(...words);
482
+ }
483
+ /** Flush any buffered audio and emit the terminal (`final`) frame. */
484
+ end() {
485
+ if (__privateGet(this, _ended)) return;
486
+ __privateSet(this, _ended, true);
487
+ for (const frame of __privateGet(this, _bstream).flush()) {
488
+ __privateMethod(this, _AudioSink_instances, emit_fn).call(this, false);
489
+ __privateSet(this, _lastFrame, frame);
490
+ }
491
+ __privateMethod(this, _AudioSink_instances, emit_fn).call(this, true);
492
+ }
493
+ };
494
+ _bstream = new WeakMap();
495
+ _lastFrame = new WeakMap();
496
+ _pendingTimed = new WeakMap();
497
+ _ended = new WeakMap();
498
+ _AudioSink_instances = new WeakSet();
499
+ emit_fn = function(final) {
500
+ if (!__privateGet(this, _lastFrame)) return;
501
+ this.put({
502
+ requestId: this.requestId,
503
+ segmentId: this.segmentId,
504
+ frame: __privateGet(this, _lastFrame),
505
+ final,
506
+ timedTranscripts: __privateGet(this, _pendingTimed).length > 0 ? __privateGet(this, _pendingTimed) : void 0
507
+ });
508
+ __privateSet(this, _lastFrame, void 0);
509
+ __privateSet(this, _pendingTimed, []);
510
+ };
511
+ var _opts, _ws, _isCurrent, _closed, _activeContexts, _contexts, _inputQueue, _inputResolver, _sendTask, _recvTask, _logger, _Connection_instances, enqueue_fn, sendLoop_fn, recvLoop_fn, handleMessage_fn;
512
+ var Connection = class {
513
+ constructor(opts) {
514
+ __privateAdd(this, _Connection_instances);
515
+ __privateAdd(this, _opts);
516
+ __privateAdd(this, _ws, null);
517
+ __privateAdd(this, _isCurrent, true);
518
+ __privateAdd(this, _closed, false);
519
+ __privateAdd(this, _activeContexts, /* @__PURE__ */ new Set());
520
+ __privateAdd(this, _contexts, /* @__PURE__ */ new Map());
521
+ __privateAdd(this, _inputQueue, []);
522
+ __privateAdd(this, _inputResolver, null);
523
+ __privateAdd(this, _sendTask, null);
524
+ __privateAdd(this, _recvTask, null);
525
+ __privateAdd(this, _logger, (0, import_agents.log)());
526
+ __privateSet(this, _opts, opts);
527
+ }
528
+ get isCurrent() {
529
+ return __privateGet(this, _isCurrent);
530
+ }
531
+ get closed() {
532
+ return __privateGet(this, _closed);
533
+ }
534
+ markNonCurrent() {
535
+ __privateSet(this, _isCurrent, false);
536
+ }
537
+ async connect(timeoutMs = 1e4) {
538
+ const url = buildMultiWsUrl(__privateGet(this, _opts).baseURL, __privateGet(this, _opts).apiKey);
539
+ await new Promise((resolve, reject) => {
540
+ let opened = false;
541
+ const ws = new import_ws.WebSocket(url, { handshakeTimeout: timeoutMs });
542
+ __privateSet(this, _ws, ws);
543
+ ws.on("open", () => {
544
+ opened = true;
545
+ __privateSet(this, _sendTask, __privateMethod(this, _Connection_instances, sendLoop_fn).call(this));
546
+ __privateSet(this, _recvTask, __privateMethod(this, _Connection_instances, recvLoop_fn).call(this));
547
+ resolve();
548
+ });
549
+ ws.on("unexpected-response", (_req, res) => {
550
+ if (!opened) {
551
+ reject(toLiveKitConnError({ statusCode: res.statusCode, message: res.statusMessage }));
552
+ }
553
+ });
554
+ ws.on("error", (err) => {
555
+ if (!opened) reject(toLiveKitConnError(err));
556
+ });
557
+ });
558
+ }
559
+ registerContext(contextId, sink, waiter, isStreaming) {
560
+ if (__privateGet(this, _closed)) {
561
+ waiter.reject(
562
+ new import_agents.APIConnectionError({ message: "Connection closed before request started" })
563
+ );
564
+ return null;
565
+ }
566
+ const ctx = {
567
+ sink,
568
+ waiter,
569
+ isStreaming,
570
+ lastActivityAt: Date.now()
571
+ };
572
+ __privateGet(this, _contexts).set(contextId, ctx);
573
+ return ctx;
574
+ }
575
+ sendText(contextId, text, flush) {
576
+ const includeConfig = !__privateGet(this, _activeContexts).has(contextId);
577
+ if (includeConfig) __privateGet(this, _activeContexts).add(contextId);
578
+ __privateMethod(this, _Connection_instances, enqueue_fn).call(this, buildTextPayload(contextId, text, __privateGet(this, _opts), { flush, includeConfig }));
579
+ }
580
+ closeContext(contextId, immediate) {
581
+ __privateMethod(this, _Connection_instances, enqueue_fn).call(this, buildClosePayload(contextId, immediate));
582
+ }
583
+ cleanupContext(contextId) {
584
+ __privateGet(this, _contexts).delete(contextId);
585
+ __privateGet(this, _activeContexts).delete(contextId);
586
+ }
587
+ async close() {
588
+ var _a;
589
+ if (__privateGet(this, _closed)) return;
590
+ __privateSet(this, _closed, true);
591
+ (_a = __privateGet(this, _inputResolver)) == null ? void 0 : _a.call(this);
592
+ for (const ctx of __privateGet(this, _contexts).values()) {
593
+ ctx.waiter.reject(new import_agents.APIConnectionError({ message: "Connection closed" }));
594
+ }
595
+ __privateGet(this, _contexts).clear();
596
+ if (__privateGet(this, _ws)) {
597
+ try {
598
+ if (__privateGet(this, _ws).readyState === import_ws.WebSocket.OPEN) {
599
+ __privateGet(this, _ws).send(JSON.stringify({ close_socket: true }));
600
+ }
601
+ __privateGet(this, _ws).close();
602
+ } catch {
603
+ }
604
+ __privateSet(this, _ws, null);
605
+ }
606
+ await __privateGet(this, _sendTask)?.catch(() => {
607
+ });
608
+ await __privateGet(this, _recvTask)?.catch(() => {
609
+ });
610
+ }
611
+ };
612
+ _opts = new WeakMap();
613
+ _ws = new WeakMap();
614
+ _isCurrent = new WeakMap();
615
+ _closed = new WeakMap();
616
+ _activeContexts = new WeakMap();
617
+ _contexts = new WeakMap();
618
+ _inputQueue = new WeakMap();
619
+ _inputResolver = new WeakMap();
620
+ _sendTask = new WeakMap();
621
+ _recvTask = new WeakMap();
622
+ _logger = new WeakMap();
623
+ _Connection_instances = new WeakSet();
624
+ enqueue_fn = function(payload) {
625
+ var _a;
626
+ if (__privateGet(this, _closed)) return;
627
+ __privateGet(this, _inputQueue).push(payload);
628
+ (_a = __privateGet(this, _inputResolver)) == null ? void 0 : _a.call(this);
629
+ };
630
+ sendLoop_fn = async function() {
631
+ try {
632
+ while (!__privateGet(this, _closed)) {
633
+ if (__privateGet(this, _inputQueue).length === 0) {
634
+ await new Promise((resolve) => {
635
+ __privateSet(this, _inputResolver, resolve);
636
+ });
637
+ __privateSet(this, _inputResolver, null);
638
+ }
639
+ if (__privateGet(this, _closed)) break;
640
+ const payload = __privateGet(this, _inputQueue).shift();
641
+ if (!payload) continue;
642
+ if (!__privateGet(this, _ws) || __privateGet(this, _ws).readyState !== import_ws.WebSocket.OPEN) break;
643
+ __privateGet(this, _ws).send(JSON.stringify(payload));
644
+ }
645
+ } catch (err) {
646
+ __privateGet(this, _logger).warn({ error: err }, "kugelaudio livekit send loop error");
647
+ __privateSet(this, _isCurrent, false);
648
+ if (__privateGet(this, _ws) && __privateGet(this, _ws).readyState === import_ws.WebSocket.OPEN) __privateGet(this, _ws).close();
649
+ }
650
+ };
651
+ recvLoop_fn = async function() {
652
+ const ws = __privateGet(this, _ws);
653
+ if (!ws) return;
654
+ try {
655
+ await new Promise((resolve) => {
656
+ const onMessage = (raw) => {
657
+ const text = typeof raw === "string" ? raw : Array.isArray(raw) ? Buffer.concat(raw).toString() : raw instanceof ArrayBuffer ? Buffer.from(raw).toString() : raw.toString();
658
+ try {
659
+ __privateMethod(this, _Connection_instances, handleMessage_fn).call(this, JSON.parse(text));
660
+ } catch (err) {
661
+ __privateGet(this, _logger).warn({ error: err }, "failed to parse /ws/tts/multi message");
662
+ }
663
+ };
664
+ const finish = () => {
665
+ ws.off("message", onMessage);
666
+ ws.off("close", finish);
667
+ ws.off("error", finish);
668
+ resolve();
669
+ };
670
+ ws.on("message", onMessage);
671
+ ws.on("close", finish);
672
+ ws.on("error", finish);
673
+ });
674
+ } finally {
675
+ __privateSet(this, _isCurrent, false);
676
+ for (const ctx of __privateGet(this, _contexts).values()) {
677
+ ctx.waiter.reject(
678
+ new import_agents.APIConnectionError({ message: "WebSocket connection closed unexpectedly" })
679
+ );
680
+ }
681
+ }
682
+ };
683
+ handleMessage_fn = function(data) {
684
+ if (data.session_closed) {
685
+ __privateSet(this, _isCurrent, false);
686
+ return;
687
+ }
688
+ const contextId = typeof data.context_id === "string" ? data.context_id : void 0;
689
+ if (data.error) {
690
+ if (contextId) {
691
+ const ctx2 = __privateGet(this, _contexts).get(contextId);
692
+ ctx2?.waiter.reject(apiStatusErrorFromFrame(data, contextId));
693
+ this.cleanupContext(contextId);
694
+ }
695
+ return;
696
+ }
697
+ const ctx = contextId ? __privateGet(this, _contexts).get(contextId) : void 0;
698
+ if (!ctx) return;
699
+ ctx.lastActivityAt = Date.now();
700
+ if (data.context_timeout) {
701
+ ctx.waiter.reject(new import_agents.APITimeoutError({ message: "context timed out" }));
702
+ this.cleanupContext(contextId);
703
+ return;
704
+ }
705
+ if (typeof data.audio === "string") {
706
+ ctx.sink.pushAudio(Buffer.from(data.audio, "base64"));
707
+ }
708
+ if (Array.isArray(data.word_timestamps)) {
709
+ ctx.sink.pushTimed(
710
+ wordTimestampsToTimed(data.word_timestamps).map((w) => (0, import_agents.createTimedString)(w))
711
+ );
712
+ }
713
+ if (data.chunk_complete && !ctx.isStreaming) {
714
+ this.closeContext(contextId, false);
715
+ }
716
+ if (data.context_closed) {
717
+ ctx.sink.end();
718
+ ctx.waiter.resolve();
719
+ this.cleanupContext(contextId);
720
+ }
721
+ };
722
+ async function waitForContextIdle(ctx, idleThresholdMs) {
723
+ const tick = Math.min(1e3, Math.max(100, idleThresholdMs / 4));
724
+ for (; ; ) {
725
+ const result = await Promise.race([
726
+ ctx.waiter.promise.then(() => "done"),
727
+ delay(tick).then(() => "tick")
728
+ ]);
729
+ if (result === "done") return;
730
+ if (Date.now() - ctx.lastActivityAt >= idleThresholdMs) {
731
+ throw new import_agents.APITimeoutError({ message: "timed out waiting for KugelAudio audio" });
732
+ }
733
+ }
734
+ }
735
+ var _opts2, _streams, _currentConnection, _connectionLock, _logger2;
736
+ var TTS = class extends import_agents.tts.TTS {
737
+ constructor(opts = {}) {
738
+ const sampleRate = opts.sampleRate ?? DEFAULT_SAMPLE_RATE;
739
+ const wordTimestamps = opts.wordTimestamps ?? false;
740
+ super(sampleRate, NUM_CHANNELS, {
741
+ streaming: true,
742
+ alignedTranscript: wordTimestamps
743
+ });
744
+ __privateAdd(this, _opts2);
745
+ __privateAdd(this, _streams, /* @__PURE__ */ new Set());
746
+ __privateAdd(this, _currentConnection, null);
747
+ __privateAdd(this, _connectionLock, new Mutex());
748
+ __privateAdd(this, _logger2, (0, import_agents.log)());
749
+ this.label = "kugelaudio.TTS";
750
+ const apiKey = opts.apiKey ?? process.env.KUGELAUDIO_API_KEY;
751
+ if (!apiKey) {
752
+ throw new Error(
753
+ "KUGELAUDIO_API_KEY must be set or apiKey must be provided to the TTS constructor."
754
+ );
755
+ }
756
+ const { cleanKey } = parseApiKey(apiKey);
757
+ const language = validateLanguage(opts.language);
758
+ let baseURL;
759
+ if (opts.baseURL) {
760
+ baseURL = opts.baseURL.replace(/\/$/, "");
761
+ } else {
762
+ const { region } = parseApiKey(apiKey);
763
+ baseURL = region === "eu" ? EU_BASE_URL : DEFAULT_BASE_URL;
764
+ }
765
+ __privateSet(this, _opts2, {
766
+ apiKey: cleanKey,
767
+ baseURL,
768
+ model: opts.model ?? DEFAULT_MODEL,
769
+ voiceId: opts.voiceId ?? DEFAULT_VOICE_ID,
770
+ sampleRate,
771
+ cfgScale: clampCfgScale(opts.cfgScale ?? DEFAULT_CFG_SCALE) ?? DEFAULT_CFG_SCALE,
772
+ maxNewTokens: opts.maxNewTokens ?? DEFAULT_MAX_NEW_TOKENS,
773
+ wordTimestamps,
774
+ normalize: opts.normalize ?? true,
775
+ language
776
+ });
777
+ }
778
+ get model() {
779
+ return __privateGet(this, _opts2).model;
780
+ }
781
+ get provider() {
782
+ return "KugelAudio";
783
+ }
784
+ /** Get or create the persistent `/ws/tts/multi` connection. */
785
+ async currentConnection(timeoutMs = 1e4) {
786
+ const unlock = await __privateGet(this, _connectionLock).lock();
787
+ try {
788
+ if (__privateGet(this, _currentConnection) && __privateGet(this, _currentConnection).isCurrent && !__privateGet(this, _currentConnection).closed) {
789
+ return __privateGet(this, _currentConnection);
790
+ }
791
+ const conn = new Connection({ ...__privateGet(this, _opts2) });
792
+ await conn.connect(timeoutMs);
793
+ __privateSet(this, _currentConnection, conn);
794
+ return conn;
795
+ } finally {
796
+ unlock();
797
+ }
798
+ }
799
+ /**
800
+ * Eagerly establish the WebSocket connection to remove handshake latency
801
+ * from the first synthesis. Errors are logged and retried on first use.
802
+ */
803
+ prewarm() {
804
+ this.currentConnection().then(() => __privateGet(this, _logger2).info("KugelAudio TTS connection pre-warmed")).catch(
805
+ (err) => __privateGet(this, _logger2).warn(
806
+ { error: err },
807
+ "Failed to prewarm KugelAudio connection; will retry on first synthesis"
808
+ )
809
+ );
810
+ }
811
+ /**
812
+ * Update TTS options at runtime. Changing any option marks the current
813
+ * connection non-current so the next synthesis opens a fresh one.
814
+ */
815
+ updateOptions(opts) {
816
+ let changed = false;
817
+ const set = (key, value) => {
818
+ if (__privateGet(this, _opts2)[key] !== value) {
819
+ __privateGet(this, _opts2)[key] = value;
820
+ changed = true;
821
+ }
822
+ };
823
+ if (opts.model !== void 0) set("model", opts.model);
824
+ if (opts.voiceId !== void 0) set("voiceId", opts.voiceId);
825
+ if (opts.cfgScale !== void 0) {
826
+ set("cfgScale", clampCfgScale(opts.cfgScale) ?? DEFAULT_CFG_SCALE);
827
+ }
828
+ if (opts.maxNewTokens !== void 0) set("maxNewTokens", opts.maxNewTokens);
829
+ if (opts.normalize !== void 0) set("normalize", opts.normalize);
830
+ if (opts.wordTimestamps !== void 0) set("wordTimestamps", opts.wordTimestamps);
831
+ if (opts.language !== void 0) set("language", validateLanguage(opts.language));
832
+ if (changed && __privateGet(this, _currentConnection)) {
833
+ const old = __privateGet(this, _currentConnection);
834
+ old.markNonCurrent();
835
+ __privateSet(this, _currentConnection, null);
836
+ old.close().catch(() => {
837
+ });
838
+ }
839
+ }
840
+ synthesize(text, connOptions, abortSignal) {
841
+ return new ChunkedStream(this, text, { ...__privateGet(this, _opts2) }, connOptions, abortSignal);
842
+ }
843
+ stream(options) {
844
+ const stream = new SynthesizeStream(this, { ...__privateGet(this, _opts2) }, options?.connOptions);
845
+ __privateGet(this, _streams).add(stream);
846
+ stream.abortSignal.addEventListener("abort", () => __privateGet(this, _streams).delete(stream), {
847
+ once: true
848
+ });
849
+ return stream;
850
+ }
851
+ async close() {
852
+ for (const stream of __privateGet(this, _streams)) stream.close();
853
+ __privateGet(this, _streams).clear();
854
+ if (__privateGet(this, _currentConnection)) {
855
+ await __privateGet(this, _currentConnection).close();
856
+ __privateSet(this, _currentConnection, null);
857
+ }
858
+ }
859
+ };
860
+ _opts2 = new WeakMap();
861
+ _streams = new WeakMap();
862
+ _currentConnection = new WeakMap();
863
+ _connectionLock = new WeakMap();
864
+ _logger2 = new WeakMap();
865
+ var _tts, _opts3, _connOptions;
866
+ var ChunkedStream = class extends import_agents.tts.ChunkedStream {
867
+ constructor(ttsInstance, text, opts, connOptions, abortSignal) {
868
+ super(text, ttsInstance, connOptions, abortSignal);
869
+ __privateAdd(this, _tts);
870
+ __privateAdd(this, _opts3);
871
+ __privateAdd(this, _connOptions);
872
+ this.label = "kugelaudio.ChunkedStream";
873
+ __privateSet(this, _tts, ttsInstance);
874
+ __privateSet(this, _opts3, opts);
875
+ __privateSet(this, _connOptions, connOptions ?? { maxRetry: 3, retryIntervalMs: 2e3, timeoutMs: 1e4 });
876
+ }
877
+ async run() {
878
+ const contextId = (0, import_agents.shortuuid)();
879
+ const connection = await __privateGet(this, _tts).currentConnection(__privateGet(this, _connOptions).timeoutMs);
880
+ const sink = new AudioSink(__privateGet(this, _opts3).sampleRate, (a) => this.queue.put(a), contextId, contextId);
881
+ const waiter = deferred();
882
+ const ctx = connection.registerContext(contextId, sink, waiter, false);
883
+ if (!ctx) {
884
+ await waiter.promise;
885
+ return;
886
+ }
887
+ const onAbort = () => waiter.reject(new Error("aborted"));
888
+ this.abortController.signal.addEventListener("abort", onAbort, { once: true });
889
+ try {
890
+ connection.sendText(contextId, this.inputText, true);
891
+ await waitForContextIdle(ctx, __privateGet(this, _connOptions).timeoutMs);
892
+ } catch (err) {
893
+ connection.closeContext(contextId, true);
894
+ connection.cleanupContext(contextId);
895
+ if (this.abortController.signal.aborted) return;
896
+ throw err;
897
+ } finally {
898
+ this.abortController.signal.removeEventListener("abort", onAbort);
899
+ }
900
+ }
901
+ };
902
+ _tts = new WeakMap();
903
+ _opts3 = new WeakMap();
904
+ _connOptions = new WeakMap();
905
+ var _tts2, _opts4, _contextId;
906
+ var _SynthesizeStream = class _SynthesizeStream extends import_agents.tts.SynthesizeStream {
907
+ constructor(ttsInstance, opts, connOptions) {
908
+ super(ttsInstance, connOptions);
909
+ __privateAdd(this, _tts2);
910
+ __privateAdd(this, _opts4);
911
+ __privateAdd(this, _contextId);
912
+ this.label = "kugelaudio.SynthesizeStream";
913
+ __privateSet(this, _tts2, ttsInstance);
914
+ __privateSet(this, _opts4, opts);
915
+ }
916
+ /** The server-side context id of the current (latest) run attempt. */
917
+ get contextId() {
918
+ return __privateGet(this, _contextId);
919
+ }
920
+ async run() {
921
+ const contextId = (0, import_agents.shortuuid)();
922
+ __privateSet(this, _contextId, contextId);
923
+ const timeoutMs = this.connOptions.timeoutMs;
924
+ const connection = await __privateGet(this, _tts2).currentConnection(timeoutMs);
925
+ const sink = new AudioSink(__privateGet(this, _opts4).sampleRate, (a) => this.queue.put(a), contextId, contextId);
926
+ const waiter = deferred();
927
+ const ctx = connection.registerContext(contextId, sink, waiter, true);
928
+ if (!ctx) {
929
+ await waiter.promise;
930
+ return;
931
+ }
932
+ const onAbort = () => waiter.reject(new Error("aborted"));
933
+ this.abortController.signal.addEventListener("abort", onAbort, { once: true });
934
+ const STOP = /* @__PURE__ */ Symbol("stop");
935
+ const stopInput = deferred();
936
+ let failed = false;
937
+ const inputTask = async () => {
938
+ for (; ; ) {
939
+ const result = await Promise.race([
940
+ this.input.next(),
941
+ stopInput.promise.then(() => STOP)
942
+ ]);
943
+ if (typeof result === "symbol") return;
944
+ if (result.done) break;
945
+ if (this.abortController.signal.aborted || failed) return;
946
+ const data = result.value;
947
+ if (data === _SynthesizeStream.FLUSH_SENTINEL) {
948
+ connection.sendText(contextId, "", true);
949
+ continue;
950
+ }
951
+ if (!data) continue;
952
+ connection.sendText(contextId, data, false);
953
+ }
954
+ if (!this.abortController.signal.aborted && !failed) {
955
+ connection.sendText(contextId, "", true);
956
+ connection.closeContext(contextId, false);
957
+ }
958
+ };
959
+ const input = inputTask();
960
+ input.catch((err) => waiter.reject(err instanceof Error ? err : new Error(String(err))));
961
+ try {
962
+ await waitForContextIdle(ctx, timeoutMs);
963
+ } catch (err) {
964
+ failed = true;
965
+ connection.closeContext(contextId, true);
966
+ connection.cleanupContext(contextId);
967
+ if (this.abortController.signal.aborted) return;
968
+ throw err;
969
+ } finally {
970
+ this.abortController.signal.removeEventListener("abort", onAbort);
971
+ stopInput.resolve();
972
+ await input.catch(() => {
973
+ });
974
+ }
975
+ }
976
+ };
977
+ _tts2 = new WeakMap();
978
+ _opts4 = new WeakMap();
979
+ _contextId = new WeakMap();
980
+ var SynthesizeStream = _SynthesizeStream;
981
+ // Annotate the CommonJS export names for ESM import in node:
982
+ 0 && (module.exports = {
983
+ ChunkedStream,
984
+ DEFAULT_CFG_SCALE,
985
+ DEFAULT_MAX_NEW_TOKENS,
986
+ DEFAULT_MODEL,
987
+ DEFAULT_SAMPLE_RATE,
988
+ DEFAULT_VOICE_ID,
989
+ SUPPORTED_LANGUAGES,
990
+ SUPPORTED_SAMPLE_RATES,
991
+ SynthesizeStream,
992
+ TTS,
993
+ validateLanguage
994
+ });