@lokutor/sdk 1.1.17 → 1.1.19
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.d.mts +119 -4
- package/dist/index.d.ts +119 -4
- package/dist/index.js +387 -219
- package/dist/index.mjs +385 -63
- package/package.json +13 -4
- package/src/audio-utils.ts +253 -0
- package/src/browser-audio.ts +395 -0
- package/src/client.ts +886 -0
- package/src/index.ts +25 -0
- package/src/node-audio.ts +115 -0
- package/src/types.ts +270 -0
- package/dist/chunk-UI24THO7.mjs +0 -44
- package/dist/node-audio-5HOWE6MC.mjs +0 -94
package/dist/index.mjs
CHANGED
|
@@ -1,9 +1,75 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
// src/types.ts
|
|
2
|
+
var VoiceStyle = /* @__PURE__ */ ((VoiceStyle2) => {
|
|
3
|
+
VoiceStyle2["F1"] = "F1";
|
|
4
|
+
VoiceStyle2["F2"] = "F2";
|
|
5
|
+
VoiceStyle2["F3"] = "F3";
|
|
6
|
+
VoiceStyle2["F4"] = "F4";
|
|
7
|
+
VoiceStyle2["F5"] = "F5";
|
|
8
|
+
VoiceStyle2["M1"] = "M1";
|
|
9
|
+
VoiceStyle2["M2"] = "M2";
|
|
10
|
+
VoiceStyle2["M3"] = "M3";
|
|
11
|
+
VoiceStyle2["M4"] = "M4";
|
|
12
|
+
VoiceStyle2["M5"] = "M5";
|
|
13
|
+
return VoiceStyle2;
|
|
14
|
+
})(VoiceStyle || {});
|
|
15
|
+
var Language = /* @__PURE__ */ ((Language2) => {
|
|
16
|
+
Language2["ENGLISH"] = "en";
|
|
17
|
+
Language2["SPANISH"] = "es";
|
|
18
|
+
Language2["FRENCH"] = "fr";
|
|
19
|
+
Language2["PORTUGUESE"] = "pt";
|
|
20
|
+
Language2["KOREAN"] = "ko";
|
|
21
|
+
return Language2;
|
|
22
|
+
})(Language || {});
|
|
23
|
+
var AUDIO_CONFIG = {
|
|
24
|
+
SAMPLE_RATE: 16e3,
|
|
25
|
+
SAMPLE_RATE_INPUT: 16e3,
|
|
26
|
+
SPEAKER_SAMPLE_RATE: 44100,
|
|
27
|
+
SAMPLE_RATE_OUTPUT: 44100,
|
|
28
|
+
CHANNELS: 1,
|
|
29
|
+
CHUNK_DURATION_MS: 20,
|
|
30
|
+
get CHUNK_SIZE() {
|
|
31
|
+
return Math.floor(this.SAMPLE_RATE * this.CHUNK_DURATION_MS / 1e3);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
var DEFAULT_URLS = {
|
|
35
|
+
VOICE_AGENT: "wss://api.lokutor.com/ws/agent",
|
|
36
|
+
TTS: "wss://api.lokutor.com/ws/tts"
|
|
37
|
+
};
|
|
38
|
+
var LokutorError = class extends Error {
|
|
39
|
+
code;
|
|
40
|
+
detail;
|
|
41
|
+
retryable;
|
|
42
|
+
original;
|
|
43
|
+
constructor(code, message, opts) {
|
|
44
|
+
super(message);
|
|
45
|
+
this.name = "LokutorError";
|
|
46
|
+
this.code = code;
|
|
47
|
+
this.detail = opts?.detail;
|
|
48
|
+
this.retryable = opts?.retryable ?? isRetryableCode(code);
|
|
49
|
+
this.original = opts?.original;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
function isRetryableCode(code) {
|
|
53
|
+
const fatal = [
|
|
54
|
+
"auth.missing_key",
|
|
55
|
+
"auth.invalid_key",
|
|
56
|
+
"auth.time_limited",
|
|
57
|
+
"validation.invalid_voice",
|
|
58
|
+
"validation.invalid_language",
|
|
59
|
+
"validation.text_too_long",
|
|
60
|
+
"validation.speed_out_of_range",
|
|
61
|
+
"validation.steps_out_of_range",
|
|
62
|
+
"validation.invalid_request_format",
|
|
63
|
+
"internal.cancelled"
|
|
64
|
+
];
|
|
65
|
+
return !fatal.includes(code);
|
|
66
|
+
}
|
|
67
|
+
function isRetryable(error) {
|
|
68
|
+
if (error instanceof LokutorError) {
|
|
69
|
+
return error.retryable;
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
7
73
|
|
|
8
74
|
// src/audio-utils.ts
|
|
9
75
|
function pcm16ToFloat32(int16Data) {
|
|
@@ -64,6 +130,9 @@ function pcm16ToBytes(data) {
|
|
|
64
130
|
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
65
131
|
}
|
|
66
132
|
function bytesToPcm16(bytes) {
|
|
133
|
+
if (bytes.length % 2 !== 0) {
|
|
134
|
+
bytes = bytes.slice(0, bytes.length - 1);
|
|
135
|
+
}
|
|
67
136
|
return new Int16Array(bytes.buffer, bytes.byteOffset, bytes.length / 2);
|
|
68
137
|
}
|
|
69
138
|
function normalizeAudio(data, targetPeak = 0.95) {
|
|
@@ -113,11 +182,14 @@ var StreamResampler = class {
|
|
|
113
182
|
combined.set(this.inputBuffer);
|
|
114
183
|
combined.set(inputChunk, this.inputBuffer.length);
|
|
115
184
|
const ratio = this.inputRate / this.outputRate;
|
|
116
|
-
|
|
185
|
+
let outputLength = Math.floor(combined.length / ratio);
|
|
117
186
|
if (outputLength === 0 && !flush) {
|
|
118
187
|
this.inputBuffer = combined;
|
|
119
188
|
return new Float32Array(0);
|
|
120
189
|
}
|
|
190
|
+
if (flush && outputLength === 0 && combined.length > 0) {
|
|
191
|
+
outputLength = 1;
|
|
192
|
+
}
|
|
121
193
|
const output = new Float32Array(outputLength);
|
|
122
194
|
for (let i = 0; i < outputLength; i++) {
|
|
123
195
|
const pos = i * ratio;
|
|
@@ -126,10 +198,8 @@ var StreamResampler = class {
|
|
|
126
198
|
const weight = pos - left;
|
|
127
199
|
output[i] = combined[left] * (1 - weight) + combined[right] * weight;
|
|
128
200
|
}
|
|
129
|
-
const
|
|
130
|
-
this.inputBuffer = combined.slice(
|
|
131
|
-
combined.length - remainingSamples
|
|
132
|
-
);
|
|
201
|
+
const consumed = Math.floor(outputLength * ratio);
|
|
202
|
+
this.inputBuffer = combined.slice(consumed);
|
|
133
203
|
return output;
|
|
134
204
|
}
|
|
135
205
|
reset() {
|
|
@@ -291,6 +361,10 @@ var BrowserAudioManager = class {
|
|
|
291
361
|
console.warn("AudioContext not initialized");
|
|
292
362
|
return;
|
|
293
363
|
}
|
|
364
|
+
if (pcm16Data.length % 2 !== 0) {
|
|
365
|
+
console.warn(`Discarding odd-length PCM buffer (${pcm16Data.length} bytes)`);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
294
368
|
const int16Array = new Int16Array(
|
|
295
369
|
pcm16Data.buffer,
|
|
296
370
|
pcm16Data.byteOffset,
|
|
@@ -419,6 +493,54 @@ var BrowserAudioManager = class {
|
|
|
419
493
|
};
|
|
420
494
|
|
|
421
495
|
// src/client.ts
|
|
496
|
+
function sdkTraceEnabled() {
|
|
497
|
+
try {
|
|
498
|
+
if (typeof window === "undefined") return false;
|
|
499
|
+
const w = window;
|
|
500
|
+
return Boolean(w.LOKUTOR_TRACE) || window.localStorage?.getItem("lokutorTrace") === "1";
|
|
501
|
+
} catch {
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
function sdkTrace(...args) {
|
|
506
|
+
if (sdkTraceEnabled()) {
|
|
507
|
+
console.log("[SDK TRACE]", ...args);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
function nowMs() {
|
|
511
|
+
if (typeof performance !== "undefined" && performance.now) {
|
|
512
|
+
return performance.now();
|
|
513
|
+
}
|
|
514
|
+
return Date.now();
|
|
515
|
+
}
|
|
516
|
+
function wsToHttp(url) {
|
|
517
|
+
return url.replace(/^wss:/, "https:").replace(/^ws:/, "http:");
|
|
518
|
+
}
|
|
519
|
+
async function fetchJson(url, timeoutMs = 1e4) {
|
|
520
|
+
const controller = new AbortController();
|
|
521
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
522
|
+
try {
|
|
523
|
+
const res = await fetch(url, {
|
|
524
|
+
signal: controller.signal,
|
|
525
|
+
headers: { Accept: "application/json" }
|
|
526
|
+
});
|
|
527
|
+
clearTimeout(timer);
|
|
528
|
+
if (!res.ok) {
|
|
529
|
+
throw new LokutorError("internal.error", `HTTP ${res.status} from ${url}`, {
|
|
530
|
+
detail: await res.text().catch(() => ""),
|
|
531
|
+
retryable: res.status >= 500
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
return await res.json();
|
|
535
|
+
} catch (err) {
|
|
536
|
+
clearTimeout(timer);
|
|
537
|
+
if (err instanceof LokutorError) throw err;
|
|
538
|
+
throw new LokutorError("internal.error", `Failed to fetch ${url}`, {
|
|
539
|
+
original: err,
|
|
540
|
+
retryable: true
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
}
|
|
422
544
|
function base64ToUint8Array(base64) {
|
|
423
545
|
const binaryString = atob(base64);
|
|
424
546
|
const bytes = new Uint8Array(binaryString.length);
|
|
@@ -427,6 +549,39 @@ function base64ToUint8Array(base64) {
|
|
|
427
549
|
}
|
|
428
550
|
return bytes;
|
|
429
551
|
}
|
|
552
|
+
function normalizeVisemes(payload) {
|
|
553
|
+
if (!Array.isArray(payload)) return [];
|
|
554
|
+
const normalized = [];
|
|
555
|
+
for (const item of payload) {
|
|
556
|
+
if (!item || typeof item !== "object") continue;
|
|
557
|
+
const c = String(item.c ?? item.char ?? "sil").toLowerCase();
|
|
558
|
+
const t = Number(item.t ?? item.timestamp ?? 0);
|
|
559
|
+
const v = Number(item.v ?? item.id ?? 0);
|
|
560
|
+
normalized.push({
|
|
561
|
+
v: Number.isFinite(v) ? v : 0,
|
|
562
|
+
c,
|
|
563
|
+
t: Number.isFinite(t) ? t : 0
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
return normalized;
|
|
567
|
+
}
|
|
568
|
+
function extractVisemePayload(msg) {
|
|
569
|
+
if (Array.isArray(msg?.data)) {
|
|
570
|
+
return normalizeVisemes(msg.data);
|
|
571
|
+
}
|
|
572
|
+
if (Array.isArray(msg?.data?.visemes)) {
|
|
573
|
+
return normalizeVisemes(msg.data.visemes);
|
|
574
|
+
}
|
|
575
|
+
if (msg?.data && !Array.isArray(msg.data) && typeof msg.data === "object") {
|
|
576
|
+
const singularInData = normalizeVisemes([msg.data]);
|
|
577
|
+
if (singularInData.length > 0) return singularInData;
|
|
578
|
+
}
|
|
579
|
+
if (msg && !Array.isArray(msg) && typeof msg === "object") {
|
|
580
|
+
const singularAtRoot = normalizeVisemes([msg]);
|
|
581
|
+
if (singularAtRoot.length > 0) return singularAtRoot;
|
|
582
|
+
}
|
|
583
|
+
return [];
|
|
584
|
+
}
|
|
430
585
|
var VoiceAgentClient = class {
|
|
431
586
|
ws = null;
|
|
432
587
|
apiKey;
|
|
@@ -454,11 +609,13 @@ var VoiceAgentClient = class {
|
|
|
454
609
|
reconnecting = false;
|
|
455
610
|
reconnectAttempts = 0;
|
|
456
611
|
maxReconnectAttempts = 5;
|
|
612
|
+
serverUrl;
|
|
457
613
|
constructor(config) {
|
|
458
614
|
this.apiKey = config.apiKey;
|
|
459
615
|
this.prompt = config.prompt;
|
|
460
616
|
this.voice = config.voice || "F1" /* F1 */;
|
|
461
617
|
this.language = config.language || "en" /* ENGLISH */;
|
|
618
|
+
this.serverUrl = config.serverUrl || DEFAULT_URLS.VOICE_AGENT;
|
|
462
619
|
this.onTranscription = config.onTranscription;
|
|
463
620
|
this.onResponse = config.onResponse;
|
|
464
621
|
this.onAudioCallback = config.onAudio;
|
|
@@ -486,13 +643,28 @@ var VoiceAgentClient = class {
|
|
|
486
643
|
}
|
|
487
644
|
}
|
|
488
645
|
return new Promise((resolve, reject) => {
|
|
646
|
+
let settled = false;
|
|
647
|
+
const settle = (fn) => {
|
|
648
|
+
if (!settled) {
|
|
649
|
+
settled = true;
|
|
650
|
+
fn();
|
|
651
|
+
}
|
|
652
|
+
};
|
|
489
653
|
try {
|
|
490
|
-
let url =
|
|
654
|
+
let url = this.serverUrl;
|
|
491
655
|
if (this.apiKey) {
|
|
492
656
|
const separator = url.includes("?") ? "&" : "?";
|
|
493
657
|
url += `${separator}api_key=${this.apiKey}`;
|
|
494
658
|
}
|
|
495
|
-
|
|
659
|
+
const redactedUrl = url.replace(/api_key=[^&]+/, "api_key=***");
|
|
660
|
+
sdkTrace("ws.connect", {
|
|
661
|
+
endpoint: this.serverUrl,
|
|
662
|
+
url: redactedUrl,
|
|
663
|
+
enableAudio: this.enableAudio,
|
|
664
|
+
wantVisemes: this.wantVisemes,
|
|
665
|
+
hasAudioManager: Boolean(this.audioManager)
|
|
666
|
+
});
|
|
667
|
+
console.log(`\u{1F517} Connecting to ${this.serverUrl}...`);
|
|
496
668
|
this.ws = new WebSocket(url);
|
|
497
669
|
this.ws.binaryType = "arraybuffer";
|
|
498
670
|
this.ws.onopen = async () => {
|
|
@@ -500,6 +672,7 @@ var VoiceAgentClient = class {
|
|
|
500
672
|
this.reconnectAttempts = 0;
|
|
501
673
|
this.reconnecting = false;
|
|
502
674
|
console.log("\u2705 Connected to voice agent!");
|
|
675
|
+
sdkTrace("ws.open");
|
|
503
676
|
this.sendConfig();
|
|
504
677
|
if (this.audioManager) {
|
|
505
678
|
await this.audioManager.startMicrophone((data) => {
|
|
@@ -508,22 +681,54 @@ var VoiceAgentClient = class {
|
|
|
508
681
|
}
|
|
509
682
|
});
|
|
510
683
|
}
|
|
511
|
-
resolve(true);
|
|
684
|
+
settle(() => resolve(true));
|
|
512
685
|
};
|
|
513
686
|
this.ws.onmessage = async (event) => {
|
|
514
687
|
if (event.data instanceof ArrayBuffer) {
|
|
688
|
+
sdkTrace("ws.message.binary", { bytes: event.data.byteLength });
|
|
515
689
|
this.handleBinaryMessage(new Uint8Array(event.data));
|
|
516
690
|
} else {
|
|
691
|
+
sdkTrace("ws.message.text", { length: String(event.data).length });
|
|
517
692
|
this.handleTextMessage(event.data.toString());
|
|
518
693
|
}
|
|
519
694
|
};
|
|
520
695
|
this.ws.onerror = (err) => {
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
696
|
+
const error = new LokutorError("ws.close", "WebSocket connection error", {
|
|
697
|
+
detail: `readyState=${this.ws?.readyState}, bufferedAmount=${this.ws?.bufferedAmount}`,
|
|
698
|
+
original: err,
|
|
699
|
+
retryable: true
|
|
700
|
+
});
|
|
701
|
+
console.error("\u274C WebSocket error:", error.message);
|
|
702
|
+
sdkTrace("ws.error", { code: error.code, message: error.message });
|
|
703
|
+
if (this.onError) this.onError(error);
|
|
704
|
+
if (!this.isConnected) {
|
|
705
|
+
settle(() => reject(error));
|
|
706
|
+
}
|
|
524
707
|
};
|
|
525
|
-
this.ws.onclose = () => {
|
|
708
|
+
this.ws.onclose = (event) => {
|
|
526
709
|
this.isConnected = false;
|
|
710
|
+
const diagnostic = {
|
|
711
|
+
code: event.code,
|
|
712
|
+
reason: event.reason,
|
|
713
|
+
wasClean: event.wasClean,
|
|
714
|
+
url: this.serverUrl,
|
|
715
|
+
isUserDisconnect: this.isUserDisconnect,
|
|
716
|
+
reconnectAttempts: this.reconnectAttempts
|
|
717
|
+
};
|
|
718
|
+
sdkTrace("ws.close", diagnostic);
|
|
719
|
+
if (!settled && !this.isUserDisconnect) {
|
|
720
|
+
const error = new LokutorError("ws.close", `WebSocket closed unexpectedly (code ${event.code})`, {
|
|
721
|
+
detail: event.reason || "No reason provided",
|
|
722
|
+
retryable: event.code !== 1008
|
|
723
|
+
});
|
|
724
|
+
settle(() => reject(error));
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
if (!event.wasClean && event.code === 1006 && this.reconnectAttempts === 0 && !this.isUserDisconnect) {
|
|
728
|
+
console.error("\u274C Connection rejected (code 1006). Likely causes: invalid API key, endpoint unavailable, or CORS blocked.");
|
|
729
|
+
console.error(" URL:", this.serverUrl.replace(/api_key=[^&]+/, "api_key=***"));
|
|
730
|
+
}
|
|
731
|
+
console.log(`\u{1F50C} WebSocket closed \u2014 code: ${event.code}, reason: "${event.reason || "none"}", clean: ${event.wasClean}`);
|
|
527
732
|
if (!this.isUserDisconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
528
733
|
this.reconnecting = true;
|
|
529
734
|
this.reconnectAttempts++;
|
|
@@ -539,37 +744,27 @@ var VoiceAgentClient = class {
|
|
|
539
744
|
}
|
|
540
745
|
};
|
|
541
746
|
} catch (err) {
|
|
542
|
-
|
|
543
|
-
|
|
747
|
+
const error = err instanceof LokutorError ? err : new LokutorError("internal.error", "Failed to create WebSocket connection", { original: err });
|
|
748
|
+
if (this.onError) this.onError(error);
|
|
749
|
+
settle(() => reject(error));
|
|
544
750
|
}
|
|
545
751
|
});
|
|
546
752
|
}
|
|
547
753
|
/**
|
|
548
754
|
* The "Golden Path" - Starts a managed session with hardware handled automatically.
|
|
549
|
-
* This is the recommended way to start a conversation in
|
|
755
|
+
* This is the recommended way to start a conversation in browser environments.
|
|
550
756
|
*/
|
|
551
757
|
async startManaged(config) {
|
|
552
758
|
this.enableAudio = true;
|
|
553
759
|
if (config?.audioManager) {
|
|
554
760
|
this.audioManager = config.audioManager;
|
|
555
761
|
} else if (!this.audioManager) {
|
|
556
|
-
if (typeof window
|
|
557
|
-
|
|
558
|
-
} else {
|
|
559
|
-
try {
|
|
560
|
-
const { NodeAudioManager } = await import("./node-audio-5HOWE6MC.mjs");
|
|
561
|
-
this.audioManager = new NodeAudioManager();
|
|
562
|
-
} catch (e) {
|
|
563
|
-
console.error('\u274C Failed to load NodeAudioManager. Please ensure "speaker" and "node-record-lpcm16" are installed.');
|
|
564
|
-
}
|
|
762
|
+
if (typeof window === "undefined") {
|
|
763
|
+
throw new LokutorError("internal.error", "startManaged() requires a browser environment. Pass a custom audioManager for non-browser runtimes.", { retryable: false });
|
|
565
764
|
}
|
|
765
|
+
this.audioManager = new BrowserAudioManager();
|
|
566
766
|
}
|
|
567
767
|
await this.connect();
|
|
568
|
-
if (this.audioManager && this.isConnected) {
|
|
569
|
-
await this.audioManager.startMicrophone((data) => {
|
|
570
|
-
this.sendAudio(data);
|
|
571
|
-
});
|
|
572
|
-
}
|
|
573
768
|
return this;
|
|
574
769
|
}
|
|
575
770
|
/**
|
|
@@ -577,10 +772,18 @@ var VoiceAgentClient = class {
|
|
|
577
772
|
*/
|
|
578
773
|
sendConfig() {
|
|
579
774
|
if (!this.ws || !this.isConnected) return;
|
|
580
|
-
this.ws.send(JSON.stringify({ type: "
|
|
775
|
+
this.ws.send(JSON.stringify({ type: "visemes", data: this.wantVisemes }));
|
|
581
776
|
this.ws.send(JSON.stringify({ type: "voice", data: this.voice }));
|
|
582
777
|
this.ws.send(JSON.stringify({ type: "language", data: this.language }));
|
|
583
|
-
this.ws.send(JSON.stringify({ type: "
|
|
778
|
+
this.ws.send(JSON.stringify({ type: "prompt", data: this.prompt }));
|
|
779
|
+
this.ws.send(JSON.stringify({ type: "rates", playback: 44100, input: 16e3 }));
|
|
780
|
+
sdkTrace("ws.send.config", {
|
|
781
|
+
promptLen: this.prompt?.length || 0,
|
|
782
|
+
voice: this.voice,
|
|
783
|
+
language: this.language,
|
|
784
|
+
visemes: this.wantVisemes,
|
|
785
|
+
tools: this.tools?.length || 0
|
|
786
|
+
});
|
|
584
787
|
if (this.tools && this.tools.length > 0) {
|
|
585
788
|
this.ws.send(JSON.stringify({ type: "tools", data: this.tools }));
|
|
586
789
|
}
|
|
@@ -614,6 +817,15 @@ var VoiceAgentClient = class {
|
|
|
614
817
|
handleTextMessage(text) {
|
|
615
818
|
try {
|
|
616
819
|
const msg = JSON.parse(text);
|
|
820
|
+
if (!msg || typeof msg !== "object") {
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
sdkTrace("ws.recv.type", {
|
|
824
|
+
type: msg.type,
|
|
825
|
+
hasData: Object.prototype.hasOwnProperty.call(msg, "data"),
|
|
826
|
+
dataKind: Array.isArray(msg.data) ? "array" : typeof msg.data,
|
|
827
|
+
generation: msg.generation ?? null
|
|
828
|
+
});
|
|
617
829
|
switch (msg.type) {
|
|
618
830
|
case "audio":
|
|
619
831
|
if (msg.data) {
|
|
@@ -626,7 +838,7 @@ var VoiceAgentClient = class {
|
|
|
626
838
|
this.messages.push({
|
|
627
839
|
role,
|
|
628
840
|
text: msg.data,
|
|
629
|
-
timestamp:
|
|
841
|
+
timestamp: nowMs()
|
|
630
842
|
});
|
|
631
843
|
if (msg.role === "user") {
|
|
632
844
|
if (this.onTranscription) this.onTranscription(msg.data);
|
|
@@ -658,19 +870,44 @@ var VoiceAgentClient = class {
|
|
|
658
870
|
console.log(`${icons[msg.data] || ""} Status: ${msg.data}`);
|
|
659
871
|
break;
|
|
660
872
|
case "visemes":
|
|
661
|
-
|
|
662
|
-
|
|
873
|
+
case "viseme": {
|
|
874
|
+
const msgGen = msg.generation ?? this.currentGeneration;
|
|
875
|
+
if (msgGen < this.currentGeneration) {
|
|
876
|
+
sdkTrace("visemes.discard", { msgGen, currentGen: this.currentGeneration });
|
|
877
|
+
break;
|
|
878
|
+
}
|
|
879
|
+
const normalized = extractVisemePayload(msg);
|
|
880
|
+
const explicitlyEmptyArray = Array.isArray(msg?.data) || Array.isArray(msg?.data?.visemes);
|
|
881
|
+
sdkTrace("visemes.recv", {
|
|
882
|
+
rawType: msg.type,
|
|
883
|
+
normalizedCount: normalized.length,
|
|
884
|
+
first: normalized[0] ?? null
|
|
885
|
+
});
|
|
886
|
+
if (normalized.length > 0 || explicitlyEmptyArray) {
|
|
887
|
+
this.emit("visemes", normalized);
|
|
663
888
|
}
|
|
664
889
|
break;
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
890
|
+
}
|
|
891
|
+
case "error": {
|
|
892
|
+
const backendCode = msg.data?.code ?? "internal.error";
|
|
893
|
+
const backendMessage = msg.data?.message ?? msg.data ?? "Unknown server error";
|
|
894
|
+
const backendDetail = msg.data?.detail;
|
|
895
|
+
const backendRetryable = msg.data?.retryable ?? true;
|
|
896
|
+
const error = new LokutorError(backendCode, backendMessage, {
|
|
897
|
+
detail: backendDetail,
|
|
898
|
+
retryable: backendRetryable
|
|
899
|
+
});
|
|
900
|
+
if (this.onError) this.onError(error);
|
|
901
|
+
console.error(`\u274C Server error: [${error.code}] ${error.message}`);
|
|
668
902
|
break;
|
|
903
|
+
}
|
|
669
904
|
case "tool_call":
|
|
670
905
|
console.log(`\u{1F6E0}\uFE0F Tool Call: ${msg.name}(${msg.arguments})`);
|
|
671
906
|
break;
|
|
672
907
|
}
|
|
673
908
|
} catch (e) {
|
|
909
|
+
sdkTrace("ws.recv.parse_error", { preview: text?.slice(0, 120) });
|
|
910
|
+
console.debug("Failed to parse message:", e);
|
|
674
911
|
}
|
|
675
912
|
}
|
|
676
913
|
/**
|
|
@@ -732,6 +969,21 @@ var VoiceAgentClient = class {
|
|
|
732
969
|
this.audioManager.cleanup();
|
|
733
970
|
}
|
|
734
971
|
this.isConnected = false;
|
|
972
|
+
this.reconnecting = false;
|
|
973
|
+
this.reconnectAttempts = 0;
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* Returns true if the client is currently connected.
|
|
977
|
+
*/
|
|
978
|
+
get connected() {
|
|
979
|
+
return this.isConnected;
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* Returns the current generation counter.
|
|
983
|
+
* Useful for correlating audio/viseme chunks with utterances.
|
|
984
|
+
*/
|
|
985
|
+
get generation() {
|
|
986
|
+
return this.currentGeneration;
|
|
735
987
|
}
|
|
736
988
|
/**
|
|
737
989
|
* Toggles the microphone mute state (if managed by client)
|
|
@@ -754,17 +1006,70 @@ var VoiceAgentClient = class {
|
|
|
754
1006
|
}
|
|
755
1007
|
return 0;
|
|
756
1008
|
}
|
|
1009
|
+
/**
|
|
1010
|
+
* Fetch available voice styles from the server.
|
|
1011
|
+
* No authentication required.
|
|
1012
|
+
*/
|
|
1013
|
+
static async fetchVoices(baseUrl) {
|
|
1014
|
+
const url = wsToHttp(baseUrl || DEFAULT_URLS.VOICE_AGENT);
|
|
1015
|
+
const data = await fetchJson(`${url}/voices`);
|
|
1016
|
+
return data.voices || [];
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* Fetch supported languages from the server.
|
|
1020
|
+
* No authentication required.
|
|
1021
|
+
*/
|
|
1022
|
+
static async fetchLanguages(baseUrl) {
|
|
1023
|
+
const url = wsToHttp(baseUrl || DEFAULT_URLS.VOICE_AGENT);
|
|
1024
|
+
const data = await fetchJson(`${url}/languages`);
|
|
1025
|
+
return data.languages || [];
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* Fetch loaded TTS model versions from the server.
|
|
1029
|
+
* No authentication required.
|
|
1030
|
+
*/
|
|
1031
|
+
static async fetchModels(baseUrl) {
|
|
1032
|
+
const url = wsToHttp(baseUrl || DEFAULT_URLS.VOICE_AGENT);
|
|
1033
|
+
const data = await fetchJson(`${url}/models`);
|
|
1034
|
+
return data.models || [];
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Fetch server configuration (limits and defaults).
|
|
1038
|
+
* No authentication required.
|
|
1039
|
+
*/
|
|
1040
|
+
static async fetchConfig(baseUrl) {
|
|
1041
|
+
const url = wsToHttp(baseUrl || DEFAULT_URLS.VOICE_AGENT);
|
|
1042
|
+
return fetchJson(`${url}/config`);
|
|
1043
|
+
}
|
|
1044
|
+
/**
|
|
1045
|
+
* Fetch rich runtime status from the server.
|
|
1046
|
+
* No authentication required.
|
|
1047
|
+
*/
|
|
1048
|
+
static async fetchStatus(baseUrl) {
|
|
1049
|
+
const url = wsToHttp(baseUrl || DEFAULT_URLS.VOICE_AGENT);
|
|
1050
|
+
return fetchJson(`${url}/status`);
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Fetch health/liveness status from the server.
|
|
1054
|
+
* No authentication required.
|
|
1055
|
+
*/
|
|
1056
|
+
static async fetchHealth(baseUrl) {
|
|
1057
|
+
const url = wsToHttp(baseUrl || DEFAULT_URLS.VOICE_AGENT);
|
|
1058
|
+
return fetchJson(`${url}/health`);
|
|
1059
|
+
}
|
|
757
1060
|
/**
|
|
758
1061
|
* Update the system prompt mid-conversation
|
|
759
1062
|
*/
|
|
760
1063
|
updatePrompt(newPrompt) {
|
|
761
1064
|
this.prompt = newPrompt;
|
|
762
|
-
if (this.ws && this.isConnected) {
|
|
1065
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN && this.isConnected) {
|
|
763
1066
|
try {
|
|
764
1067
|
this.ws.send(JSON.stringify({ type: "prompt", data: newPrompt }));
|
|
765
1068
|
console.log(`\u2699\uFE0F Updated prompt: ${newPrompt.substring(0, 50)}...`);
|
|
766
1069
|
} catch (error) {
|
|
767
|
-
|
|
1070
|
+
const err = new LokutorError("internal.error", "Failed to update prompt", { original: error });
|
|
1071
|
+
if (this.onError) this.onError(err);
|
|
1072
|
+
console.error("Error updating prompt:", err.message);
|
|
768
1073
|
}
|
|
769
1074
|
} else {
|
|
770
1075
|
console.warn("Not connected - prompt will be updated on next connection");
|
|
@@ -827,37 +1132,52 @@ var TTSClient = class {
|
|
|
827
1132
|
visemes: options.visemes || false
|
|
828
1133
|
};
|
|
829
1134
|
ws.send(JSON.stringify(req));
|
|
830
|
-
startTime =
|
|
1135
|
+
startTime = nowMs();
|
|
831
1136
|
};
|
|
832
1137
|
ws.onmessage = async (event) => {
|
|
833
1138
|
refreshTimeout();
|
|
834
1139
|
if (event.data instanceof ArrayBuffer) {
|
|
835
1140
|
if (!firstByteReceived) {
|
|
836
|
-
const ttfb =
|
|
1141
|
+
const ttfb = nowMs() - startTime;
|
|
837
1142
|
if (options.onTTFB) options.onTTFB(ttfb);
|
|
838
1143
|
firstByteReceived = true;
|
|
839
1144
|
}
|
|
840
1145
|
if (options.onAudio) options.onAudio(new Uint8Array(event.data));
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
const text = event.data.toString();
|
|
1149
|
+
if (text === "EOS") {
|
|
1150
|
+
if (activityTimeout) clearTimeout(activityTimeout);
|
|
1151
|
+
ws.close();
|
|
1152
|
+
resolve();
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
try {
|
|
1156
|
+
const msg = JSON.parse(text);
|
|
1157
|
+
if (msg.type === "audio" && msg.data) {
|
|
1158
|
+
const audioBuffer = base64ToUint8Array(msg.data);
|
|
1159
|
+
if (!firstByteReceived) {
|
|
1160
|
+
const ttfb = nowMs() - startTime;
|
|
1161
|
+
if (options.onTTFB) options.onTTFB(ttfb);
|
|
1162
|
+
firstByteReceived = true;
|
|
1163
|
+
}
|
|
1164
|
+
if (options.onAudio) options.onAudio(audioBuffer);
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
if (msg.type === "visemes" && Array.isArray(msg.data) && options.onVisemes) {
|
|
1168
|
+
options.onVisemes(normalizeVisemes(msg.data));
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
if (Array.isArray(msg) && options.onVisemes) {
|
|
1172
|
+
options.onVisemes(normalizeVisemes(msg));
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
if (msg.type === "eos") {
|
|
844
1176
|
if (activityTimeout) clearTimeout(activityTimeout);
|
|
845
1177
|
ws.close();
|
|
846
1178
|
resolve();
|
|
847
|
-
return;
|
|
848
|
-
}
|
|
849
|
-
try {
|
|
850
|
-
const msg = JSON.parse(text);
|
|
851
|
-
if (Array.isArray(msg) && options.onVisemes) {
|
|
852
|
-
options.onVisemes(msg);
|
|
853
|
-
}
|
|
854
|
-
if (msg.type === "eos") {
|
|
855
|
-
if (activityTimeout) clearTimeout(activityTimeout);
|
|
856
|
-
ws.close();
|
|
857
|
-
resolve();
|
|
858
|
-
}
|
|
859
|
-
} catch (e) {
|
|
860
1179
|
}
|
|
1180
|
+
} catch (e) {
|
|
861
1181
|
}
|
|
862
1182
|
};
|
|
863
1183
|
ws.onerror = (err) => {
|
|
@@ -891,6 +1211,7 @@ export {
|
|
|
891
1211
|
BrowserAudioManager,
|
|
892
1212
|
DEFAULT_URLS,
|
|
893
1213
|
Language,
|
|
1214
|
+
LokutorError,
|
|
894
1215
|
StreamResampler,
|
|
895
1216
|
TTSClient,
|
|
896
1217
|
VoiceAgentClient,
|
|
@@ -899,6 +1220,7 @@ export {
|
|
|
899
1220
|
bytesToPcm16,
|
|
900
1221
|
calculateRMS,
|
|
901
1222
|
float32ToPcm16,
|
|
1223
|
+
isRetryable,
|
|
902
1224
|
normalizeAudio,
|
|
903
1225
|
pcm16ToBytes,
|
|
904
1226
|
pcm16ToFloat32,
|
package/package.json
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lokutor/sdk",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.19",
|
|
4
4
|
"description": "JavaScript/TypeScript SDK for Lokutor Real-time Voice AI",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
8
15
|
"files": [
|
|
9
|
-
"dist"
|
|
16
|
+
"dist",
|
|
17
|
+
"src"
|
|
10
18
|
],
|
|
11
19
|
"scripts": {
|
|
12
20
|
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
@@ -32,6 +40,7 @@
|
|
|
32
40
|
"@types/node": "^20.10.0",
|
|
33
41
|
"tsup": "^8.0.1",
|
|
34
42
|
"typescript": "^5.3.2",
|
|
35
|
-
"vitest": "^1.0.1"
|
|
43
|
+
"vitest": "^1.0.1",
|
|
44
|
+
"wavefile": "^11.0.0"
|
|
36
45
|
}
|
|
37
|
-
}
|
|
46
|
+
}
|