@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.js
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __create = Object.create;
|
|
3
2
|
var __defProp = Object.defineProperty;
|
|
4
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
7
5
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __esm = (fn, res) => function __init() {
|
|
9
|
-
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
10
|
-
};
|
|
11
6
|
var __export = (target, all) => {
|
|
12
7
|
for (var name in all)
|
|
13
8
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -20,159 +15,8 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
20
15
|
}
|
|
21
16
|
return to;
|
|
22
17
|
};
|
|
23
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
24
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
25
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
26
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
27
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
28
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
29
|
-
mod
|
|
30
|
-
));
|
|
31
18
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
19
|
|
|
33
|
-
// src/types.ts
|
|
34
|
-
var VoiceStyle, Language, AUDIO_CONFIG, DEFAULT_URLS;
|
|
35
|
-
var init_types = __esm({
|
|
36
|
-
"src/types.ts"() {
|
|
37
|
-
"use strict";
|
|
38
|
-
VoiceStyle = /* @__PURE__ */ ((VoiceStyle2) => {
|
|
39
|
-
VoiceStyle2["F1"] = "F1";
|
|
40
|
-
VoiceStyle2["F2"] = "F2";
|
|
41
|
-
VoiceStyle2["F3"] = "F3";
|
|
42
|
-
VoiceStyle2["F4"] = "F4";
|
|
43
|
-
VoiceStyle2["F5"] = "F5";
|
|
44
|
-
VoiceStyle2["M1"] = "M1";
|
|
45
|
-
VoiceStyle2["M2"] = "M2";
|
|
46
|
-
VoiceStyle2["M3"] = "M3";
|
|
47
|
-
VoiceStyle2["M4"] = "M4";
|
|
48
|
-
VoiceStyle2["M5"] = "M5";
|
|
49
|
-
return VoiceStyle2;
|
|
50
|
-
})(VoiceStyle || {});
|
|
51
|
-
Language = /* @__PURE__ */ ((Language2) => {
|
|
52
|
-
Language2["ENGLISH"] = "en";
|
|
53
|
-
Language2["SPANISH"] = "es";
|
|
54
|
-
Language2["FRENCH"] = "fr";
|
|
55
|
-
Language2["PORTUGUESE"] = "pt";
|
|
56
|
-
Language2["KOREAN"] = "ko";
|
|
57
|
-
return Language2;
|
|
58
|
-
})(Language || {});
|
|
59
|
-
AUDIO_CONFIG = {
|
|
60
|
-
SAMPLE_RATE: 16e3,
|
|
61
|
-
SAMPLE_RATE_INPUT: 16e3,
|
|
62
|
-
SPEAKER_SAMPLE_RATE: 44100,
|
|
63
|
-
SAMPLE_RATE_OUTPUT: 44100,
|
|
64
|
-
CHANNELS: 1,
|
|
65
|
-
CHUNK_DURATION_MS: 20,
|
|
66
|
-
get CHUNK_SIZE() {
|
|
67
|
-
return Math.floor(this.SAMPLE_RATE * this.CHUNK_DURATION_MS / 1e3);
|
|
68
|
-
}
|
|
69
|
-
};
|
|
70
|
-
DEFAULT_URLS = {
|
|
71
|
-
VOICE_AGENT: "wss://api.lokutor.com/ws/agent",
|
|
72
|
-
TTS: "wss://api.lokutor.com/ws/tts"
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
// src/node-audio.ts
|
|
78
|
-
var node_audio_exports = {};
|
|
79
|
-
__export(node_audio_exports, {
|
|
80
|
-
NodeAudioManager: () => NodeAudioManager
|
|
81
|
-
});
|
|
82
|
-
var NodeAudioManager;
|
|
83
|
-
var init_node_audio = __esm({
|
|
84
|
-
"src/node-audio.ts"() {
|
|
85
|
-
"use strict";
|
|
86
|
-
init_types();
|
|
87
|
-
NodeAudioManager = class {
|
|
88
|
-
speaker = null;
|
|
89
|
-
recorder = null;
|
|
90
|
-
recordingStream = null;
|
|
91
|
-
isMuted = false;
|
|
92
|
-
isListening = false;
|
|
93
|
-
constructor() {
|
|
94
|
-
}
|
|
95
|
-
async init() {
|
|
96
|
-
try {
|
|
97
|
-
const Speaker = await import("speaker").catch(() => null);
|
|
98
|
-
if (!Speaker) {
|
|
99
|
-
console.warn('\u26A0\uFE0F Package "speaker" is missing. Hardware output will be disabled.');
|
|
100
|
-
console.warn("\u{1F449} Run: npm install speaker");
|
|
101
|
-
}
|
|
102
|
-
} catch (e) {
|
|
103
|
-
console.error("Error initializing Node audio:", e);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
async startMicrophone(onAudioInput) {
|
|
107
|
-
if (this.isListening) return;
|
|
108
|
-
try {
|
|
109
|
-
const recorder = await import("node-record-lpcm16").catch(() => null);
|
|
110
|
-
if (!recorder) {
|
|
111
|
-
throw new Error('Package "node-record-lpcm16" is missing. Microphone input failed.\n\u{1F449} Run: npm install node-record-lpcm16');
|
|
112
|
-
}
|
|
113
|
-
console.log("\u{1F3A4} Starting microphone (Node.js)...");
|
|
114
|
-
this.recordingStream = recorder.record({
|
|
115
|
-
sampleRate: AUDIO_CONFIG.SAMPLE_RATE,
|
|
116
|
-
threshold: 0,
|
|
117
|
-
verbose: false,
|
|
118
|
-
recordProgram: "sox"
|
|
119
|
-
// default
|
|
120
|
-
});
|
|
121
|
-
this.recordingStream.stream().on("data", (chunk) => {
|
|
122
|
-
if (!this.isMuted && onAudioInput) {
|
|
123
|
-
onAudioInput(new Uint8Array(chunk));
|
|
124
|
-
}
|
|
125
|
-
});
|
|
126
|
-
this.isListening = true;
|
|
127
|
-
} catch (e) {
|
|
128
|
-
console.error("Failed to start microphone:", e.message);
|
|
129
|
-
throw e;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
stopMicrophone() {
|
|
133
|
-
if (this.recordingStream) {
|
|
134
|
-
this.recordingStream.stop();
|
|
135
|
-
this.recordingStream = null;
|
|
136
|
-
}
|
|
137
|
-
this.isListening = false;
|
|
138
|
-
}
|
|
139
|
-
async playAudio(pcm16Data) {
|
|
140
|
-
try {
|
|
141
|
-
if (!this.speaker) {
|
|
142
|
-
const Speaker = (await import("speaker")).default;
|
|
143
|
-
this.speaker = new Speaker({
|
|
144
|
-
channels: AUDIO_CONFIG.CHANNELS,
|
|
145
|
-
bitDepth: 16,
|
|
146
|
-
sampleRate: AUDIO_CONFIG.SPEAKER_SAMPLE_RATE
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
this.speaker.write(Buffer.from(pcm16Data));
|
|
150
|
-
} catch (e) {
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
stopPlayback() {
|
|
154
|
-
if (this.speaker) {
|
|
155
|
-
this.speaker.end();
|
|
156
|
-
this.speaker = null;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
cleanup() {
|
|
160
|
-
this.stopMicrophone();
|
|
161
|
-
this.stopPlayback();
|
|
162
|
-
}
|
|
163
|
-
isMicMuted() {
|
|
164
|
-
return this.isMuted;
|
|
165
|
-
}
|
|
166
|
-
setMuted(muted) {
|
|
167
|
-
this.isMuted = muted;
|
|
168
|
-
}
|
|
169
|
-
getAmplitude() {
|
|
170
|
-
return 0;
|
|
171
|
-
}
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
});
|
|
175
|
-
|
|
176
20
|
// src/index.ts
|
|
177
21
|
var index_exports = {};
|
|
178
22
|
__export(index_exports, {
|
|
@@ -180,6 +24,7 @@ __export(index_exports, {
|
|
|
180
24
|
BrowserAudioManager: () => BrowserAudioManager,
|
|
181
25
|
DEFAULT_URLS: () => DEFAULT_URLS,
|
|
182
26
|
Language: () => Language,
|
|
27
|
+
LokutorError: () => LokutorError,
|
|
183
28
|
StreamResampler: () => StreamResampler,
|
|
184
29
|
TTSClient: () => TTSClient,
|
|
185
30
|
VoiceAgentClient: () => VoiceAgentClient,
|
|
@@ -188,6 +33,7 @@ __export(index_exports, {
|
|
|
188
33
|
bytesToPcm16: () => bytesToPcm16,
|
|
189
34
|
calculateRMS: () => calculateRMS,
|
|
190
35
|
float32ToPcm16: () => float32ToPcm16,
|
|
36
|
+
isRetryable: () => isRetryable,
|
|
191
37
|
normalizeAudio: () => normalizeAudio,
|
|
192
38
|
pcm16ToBytes: () => pcm16ToBytes,
|
|
193
39
|
pcm16ToFloat32: () => pcm16ToFloat32,
|
|
@@ -197,13 +43,79 @@ __export(index_exports, {
|
|
|
197
43
|
simpleTTS: () => simpleTTS
|
|
198
44
|
});
|
|
199
45
|
module.exports = __toCommonJS(index_exports);
|
|
200
|
-
init_types();
|
|
201
|
-
|
|
202
|
-
// src/client.ts
|
|
203
|
-
init_types();
|
|
204
46
|
|
|
205
|
-
// src/
|
|
206
|
-
|
|
47
|
+
// src/types.ts
|
|
48
|
+
var VoiceStyle = /* @__PURE__ */ ((VoiceStyle2) => {
|
|
49
|
+
VoiceStyle2["F1"] = "F1";
|
|
50
|
+
VoiceStyle2["F2"] = "F2";
|
|
51
|
+
VoiceStyle2["F3"] = "F3";
|
|
52
|
+
VoiceStyle2["F4"] = "F4";
|
|
53
|
+
VoiceStyle2["F5"] = "F5";
|
|
54
|
+
VoiceStyle2["M1"] = "M1";
|
|
55
|
+
VoiceStyle2["M2"] = "M2";
|
|
56
|
+
VoiceStyle2["M3"] = "M3";
|
|
57
|
+
VoiceStyle2["M4"] = "M4";
|
|
58
|
+
VoiceStyle2["M5"] = "M5";
|
|
59
|
+
return VoiceStyle2;
|
|
60
|
+
})(VoiceStyle || {});
|
|
61
|
+
var Language = /* @__PURE__ */ ((Language2) => {
|
|
62
|
+
Language2["ENGLISH"] = "en";
|
|
63
|
+
Language2["SPANISH"] = "es";
|
|
64
|
+
Language2["FRENCH"] = "fr";
|
|
65
|
+
Language2["PORTUGUESE"] = "pt";
|
|
66
|
+
Language2["KOREAN"] = "ko";
|
|
67
|
+
return Language2;
|
|
68
|
+
})(Language || {});
|
|
69
|
+
var AUDIO_CONFIG = {
|
|
70
|
+
SAMPLE_RATE: 16e3,
|
|
71
|
+
SAMPLE_RATE_INPUT: 16e3,
|
|
72
|
+
SPEAKER_SAMPLE_RATE: 44100,
|
|
73
|
+
SAMPLE_RATE_OUTPUT: 44100,
|
|
74
|
+
CHANNELS: 1,
|
|
75
|
+
CHUNK_DURATION_MS: 20,
|
|
76
|
+
get CHUNK_SIZE() {
|
|
77
|
+
return Math.floor(this.SAMPLE_RATE * this.CHUNK_DURATION_MS / 1e3);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
var DEFAULT_URLS = {
|
|
81
|
+
VOICE_AGENT: "wss://api.lokutor.com/ws/agent",
|
|
82
|
+
TTS: "wss://api.lokutor.com/ws/tts"
|
|
83
|
+
};
|
|
84
|
+
var LokutorError = class extends Error {
|
|
85
|
+
code;
|
|
86
|
+
detail;
|
|
87
|
+
retryable;
|
|
88
|
+
original;
|
|
89
|
+
constructor(code, message, opts) {
|
|
90
|
+
super(message);
|
|
91
|
+
this.name = "LokutorError";
|
|
92
|
+
this.code = code;
|
|
93
|
+
this.detail = opts?.detail;
|
|
94
|
+
this.retryable = opts?.retryable ?? isRetryableCode(code);
|
|
95
|
+
this.original = opts?.original;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
function isRetryableCode(code) {
|
|
99
|
+
const fatal = [
|
|
100
|
+
"auth.missing_key",
|
|
101
|
+
"auth.invalid_key",
|
|
102
|
+
"auth.time_limited",
|
|
103
|
+
"validation.invalid_voice",
|
|
104
|
+
"validation.invalid_language",
|
|
105
|
+
"validation.text_too_long",
|
|
106
|
+
"validation.speed_out_of_range",
|
|
107
|
+
"validation.steps_out_of_range",
|
|
108
|
+
"validation.invalid_request_format",
|
|
109
|
+
"internal.cancelled"
|
|
110
|
+
];
|
|
111
|
+
return !fatal.includes(code);
|
|
112
|
+
}
|
|
113
|
+
function isRetryable(error) {
|
|
114
|
+
if (error instanceof LokutorError) {
|
|
115
|
+
return error.retryable;
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
207
119
|
|
|
208
120
|
// src/audio-utils.ts
|
|
209
121
|
function pcm16ToFloat32(int16Data) {
|
|
@@ -264,6 +176,9 @@ function pcm16ToBytes(data) {
|
|
|
264
176
|
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
265
177
|
}
|
|
266
178
|
function bytesToPcm16(bytes) {
|
|
179
|
+
if (bytes.length % 2 !== 0) {
|
|
180
|
+
bytes = bytes.slice(0, bytes.length - 1);
|
|
181
|
+
}
|
|
267
182
|
return new Int16Array(bytes.buffer, bytes.byteOffset, bytes.length / 2);
|
|
268
183
|
}
|
|
269
184
|
function normalizeAudio(data, targetPeak = 0.95) {
|
|
@@ -313,11 +228,14 @@ var StreamResampler = class {
|
|
|
313
228
|
combined.set(this.inputBuffer);
|
|
314
229
|
combined.set(inputChunk, this.inputBuffer.length);
|
|
315
230
|
const ratio = this.inputRate / this.outputRate;
|
|
316
|
-
|
|
231
|
+
let outputLength = Math.floor(combined.length / ratio);
|
|
317
232
|
if (outputLength === 0 && !flush) {
|
|
318
233
|
this.inputBuffer = combined;
|
|
319
234
|
return new Float32Array(0);
|
|
320
235
|
}
|
|
236
|
+
if (flush && outputLength === 0 && combined.length > 0) {
|
|
237
|
+
outputLength = 1;
|
|
238
|
+
}
|
|
321
239
|
const output = new Float32Array(outputLength);
|
|
322
240
|
for (let i = 0; i < outputLength; i++) {
|
|
323
241
|
const pos = i * ratio;
|
|
@@ -326,10 +244,8 @@ var StreamResampler = class {
|
|
|
326
244
|
const weight = pos - left;
|
|
327
245
|
output[i] = combined[left] * (1 - weight) + combined[right] * weight;
|
|
328
246
|
}
|
|
329
|
-
const
|
|
330
|
-
this.inputBuffer = combined.slice(
|
|
331
|
-
combined.length - remainingSamples
|
|
332
|
-
);
|
|
247
|
+
const consumed = Math.floor(outputLength * ratio);
|
|
248
|
+
this.inputBuffer = combined.slice(consumed);
|
|
333
249
|
return output;
|
|
334
250
|
}
|
|
335
251
|
reset() {
|
|
@@ -491,6 +407,10 @@ var BrowserAudioManager = class {
|
|
|
491
407
|
console.warn("AudioContext not initialized");
|
|
492
408
|
return;
|
|
493
409
|
}
|
|
410
|
+
if (pcm16Data.length % 2 !== 0) {
|
|
411
|
+
console.warn(`Discarding odd-length PCM buffer (${pcm16Data.length} bytes)`);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
494
414
|
const int16Array = new Int16Array(
|
|
495
415
|
pcm16Data.buffer,
|
|
496
416
|
pcm16Data.byteOffset,
|
|
@@ -619,6 +539,54 @@ var BrowserAudioManager = class {
|
|
|
619
539
|
};
|
|
620
540
|
|
|
621
541
|
// src/client.ts
|
|
542
|
+
function sdkTraceEnabled() {
|
|
543
|
+
try {
|
|
544
|
+
if (typeof window === "undefined") return false;
|
|
545
|
+
const w = window;
|
|
546
|
+
return Boolean(w.LOKUTOR_TRACE) || window.localStorage?.getItem("lokutorTrace") === "1";
|
|
547
|
+
} catch {
|
|
548
|
+
return false;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
function sdkTrace(...args) {
|
|
552
|
+
if (sdkTraceEnabled()) {
|
|
553
|
+
console.log("[SDK TRACE]", ...args);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
function nowMs() {
|
|
557
|
+
if (typeof performance !== "undefined" && performance.now) {
|
|
558
|
+
return performance.now();
|
|
559
|
+
}
|
|
560
|
+
return Date.now();
|
|
561
|
+
}
|
|
562
|
+
function wsToHttp(url) {
|
|
563
|
+
return url.replace(/^wss:/, "https:").replace(/^ws:/, "http:");
|
|
564
|
+
}
|
|
565
|
+
async function fetchJson(url, timeoutMs = 1e4) {
|
|
566
|
+
const controller = new AbortController();
|
|
567
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
568
|
+
try {
|
|
569
|
+
const res = await fetch(url, {
|
|
570
|
+
signal: controller.signal,
|
|
571
|
+
headers: { Accept: "application/json" }
|
|
572
|
+
});
|
|
573
|
+
clearTimeout(timer);
|
|
574
|
+
if (!res.ok) {
|
|
575
|
+
throw new LokutorError("internal.error", `HTTP ${res.status} from ${url}`, {
|
|
576
|
+
detail: await res.text().catch(() => ""),
|
|
577
|
+
retryable: res.status >= 500
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
return await res.json();
|
|
581
|
+
} catch (err) {
|
|
582
|
+
clearTimeout(timer);
|
|
583
|
+
if (err instanceof LokutorError) throw err;
|
|
584
|
+
throw new LokutorError("internal.error", `Failed to fetch ${url}`, {
|
|
585
|
+
original: err,
|
|
586
|
+
retryable: true
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
}
|
|
622
590
|
function base64ToUint8Array(base64) {
|
|
623
591
|
const binaryString = atob(base64);
|
|
624
592
|
const bytes = new Uint8Array(binaryString.length);
|
|
@@ -627,6 +595,39 @@ function base64ToUint8Array(base64) {
|
|
|
627
595
|
}
|
|
628
596
|
return bytes;
|
|
629
597
|
}
|
|
598
|
+
function normalizeVisemes(payload) {
|
|
599
|
+
if (!Array.isArray(payload)) return [];
|
|
600
|
+
const normalized = [];
|
|
601
|
+
for (const item of payload) {
|
|
602
|
+
if (!item || typeof item !== "object") continue;
|
|
603
|
+
const c = String(item.c ?? item.char ?? "sil").toLowerCase();
|
|
604
|
+
const t = Number(item.t ?? item.timestamp ?? 0);
|
|
605
|
+
const v = Number(item.v ?? item.id ?? 0);
|
|
606
|
+
normalized.push({
|
|
607
|
+
v: Number.isFinite(v) ? v : 0,
|
|
608
|
+
c,
|
|
609
|
+
t: Number.isFinite(t) ? t : 0
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
return normalized;
|
|
613
|
+
}
|
|
614
|
+
function extractVisemePayload(msg) {
|
|
615
|
+
if (Array.isArray(msg?.data)) {
|
|
616
|
+
return normalizeVisemes(msg.data);
|
|
617
|
+
}
|
|
618
|
+
if (Array.isArray(msg?.data?.visemes)) {
|
|
619
|
+
return normalizeVisemes(msg.data.visemes);
|
|
620
|
+
}
|
|
621
|
+
if (msg?.data && !Array.isArray(msg.data) && typeof msg.data === "object") {
|
|
622
|
+
const singularInData = normalizeVisemes([msg.data]);
|
|
623
|
+
if (singularInData.length > 0) return singularInData;
|
|
624
|
+
}
|
|
625
|
+
if (msg && !Array.isArray(msg) && typeof msg === "object") {
|
|
626
|
+
const singularAtRoot = normalizeVisemes([msg]);
|
|
627
|
+
if (singularAtRoot.length > 0) return singularAtRoot;
|
|
628
|
+
}
|
|
629
|
+
return [];
|
|
630
|
+
}
|
|
630
631
|
var VoiceAgentClient = class {
|
|
631
632
|
ws = null;
|
|
632
633
|
apiKey;
|
|
@@ -654,11 +655,13 @@ var VoiceAgentClient = class {
|
|
|
654
655
|
reconnecting = false;
|
|
655
656
|
reconnectAttempts = 0;
|
|
656
657
|
maxReconnectAttempts = 5;
|
|
658
|
+
serverUrl;
|
|
657
659
|
constructor(config) {
|
|
658
660
|
this.apiKey = config.apiKey;
|
|
659
661
|
this.prompt = config.prompt;
|
|
660
662
|
this.voice = config.voice || "F1" /* F1 */;
|
|
661
663
|
this.language = config.language || "en" /* ENGLISH */;
|
|
664
|
+
this.serverUrl = config.serverUrl || DEFAULT_URLS.VOICE_AGENT;
|
|
662
665
|
this.onTranscription = config.onTranscription;
|
|
663
666
|
this.onResponse = config.onResponse;
|
|
664
667
|
this.onAudioCallback = config.onAudio;
|
|
@@ -686,13 +689,28 @@ var VoiceAgentClient = class {
|
|
|
686
689
|
}
|
|
687
690
|
}
|
|
688
691
|
return new Promise((resolve, reject) => {
|
|
692
|
+
let settled = false;
|
|
693
|
+
const settle = (fn) => {
|
|
694
|
+
if (!settled) {
|
|
695
|
+
settled = true;
|
|
696
|
+
fn();
|
|
697
|
+
}
|
|
698
|
+
};
|
|
689
699
|
try {
|
|
690
|
-
let url =
|
|
700
|
+
let url = this.serverUrl;
|
|
691
701
|
if (this.apiKey) {
|
|
692
702
|
const separator = url.includes("?") ? "&" : "?";
|
|
693
703
|
url += `${separator}api_key=${this.apiKey}`;
|
|
694
704
|
}
|
|
695
|
-
|
|
705
|
+
const redactedUrl = url.replace(/api_key=[^&]+/, "api_key=***");
|
|
706
|
+
sdkTrace("ws.connect", {
|
|
707
|
+
endpoint: this.serverUrl,
|
|
708
|
+
url: redactedUrl,
|
|
709
|
+
enableAudio: this.enableAudio,
|
|
710
|
+
wantVisemes: this.wantVisemes,
|
|
711
|
+
hasAudioManager: Boolean(this.audioManager)
|
|
712
|
+
});
|
|
713
|
+
console.log(`\u{1F517} Connecting to ${this.serverUrl}...`);
|
|
696
714
|
this.ws = new WebSocket(url);
|
|
697
715
|
this.ws.binaryType = "arraybuffer";
|
|
698
716
|
this.ws.onopen = async () => {
|
|
@@ -700,6 +718,7 @@ var VoiceAgentClient = class {
|
|
|
700
718
|
this.reconnectAttempts = 0;
|
|
701
719
|
this.reconnecting = false;
|
|
702
720
|
console.log("\u2705 Connected to voice agent!");
|
|
721
|
+
sdkTrace("ws.open");
|
|
703
722
|
this.sendConfig();
|
|
704
723
|
if (this.audioManager) {
|
|
705
724
|
await this.audioManager.startMicrophone((data) => {
|
|
@@ -708,22 +727,54 @@ var VoiceAgentClient = class {
|
|
|
708
727
|
}
|
|
709
728
|
});
|
|
710
729
|
}
|
|
711
|
-
resolve(true);
|
|
730
|
+
settle(() => resolve(true));
|
|
712
731
|
};
|
|
713
732
|
this.ws.onmessage = async (event) => {
|
|
714
733
|
if (event.data instanceof ArrayBuffer) {
|
|
734
|
+
sdkTrace("ws.message.binary", { bytes: event.data.byteLength });
|
|
715
735
|
this.handleBinaryMessage(new Uint8Array(event.data));
|
|
716
736
|
} else {
|
|
737
|
+
sdkTrace("ws.message.text", { length: String(event.data).length });
|
|
717
738
|
this.handleTextMessage(event.data.toString());
|
|
718
739
|
}
|
|
719
740
|
};
|
|
720
741
|
this.ws.onerror = (err) => {
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
742
|
+
const error = new LokutorError("ws.close", "WebSocket connection error", {
|
|
743
|
+
detail: `readyState=${this.ws?.readyState}, bufferedAmount=${this.ws?.bufferedAmount}`,
|
|
744
|
+
original: err,
|
|
745
|
+
retryable: true
|
|
746
|
+
});
|
|
747
|
+
console.error("\u274C WebSocket error:", error.message);
|
|
748
|
+
sdkTrace("ws.error", { code: error.code, message: error.message });
|
|
749
|
+
if (this.onError) this.onError(error);
|
|
750
|
+
if (!this.isConnected) {
|
|
751
|
+
settle(() => reject(error));
|
|
752
|
+
}
|
|
724
753
|
};
|
|
725
|
-
this.ws.onclose = () => {
|
|
754
|
+
this.ws.onclose = (event) => {
|
|
726
755
|
this.isConnected = false;
|
|
756
|
+
const diagnostic = {
|
|
757
|
+
code: event.code,
|
|
758
|
+
reason: event.reason,
|
|
759
|
+
wasClean: event.wasClean,
|
|
760
|
+
url: this.serverUrl,
|
|
761
|
+
isUserDisconnect: this.isUserDisconnect,
|
|
762
|
+
reconnectAttempts: this.reconnectAttempts
|
|
763
|
+
};
|
|
764
|
+
sdkTrace("ws.close", diagnostic);
|
|
765
|
+
if (!settled && !this.isUserDisconnect) {
|
|
766
|
+
const error = new LokutorError("ws.close", `WebSocket closed unexpectedly (code ${event.code})`, {
|
|
767
|
+
detail: event.reason || "No reason provided",
|
|
768
|
+
retryable: event.code !== 1008
|
|
769
|
+
});
|
|
770
|
+
settle(() => reject(error));
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
if (!event.wasClean && event.code === 1006 && this.reconnectAttempts === 0 && !this.isUserDisconnect) {
|
|
774
|
+
console.error("\u274C Connection rejected (code 1006). Likely causes: invalid API key, endpoint unavailable, or CORS blocked.");
|
|
775
|
+
console.error(" URL:", this.serverUrl.replace(/api_key=[^&]+/, "api_key=***"));
|
|
776
|
+
}
|
|
777
|
+
console.log(`\u{1F50C} WebSocket closed \u2014 code: ${event.code}, reason: "${event.reason || "none"}", clean: ${event.wasClean}`);
|
|
727
778
|
if (!this.isUserDisconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
728
779
|
this.reconnecting = true;
|
|
729
780
|
this.reconnectAttempts++;
|
|
@@ -739,37 +790,27 @@ var VoiceAgentClient = class {
|
|
|
739
790
|
}
|
|
740
791
|
};
|
|
741
792
|
} catch (err) {
|
|
742
|
-
|
|
743
|
-
|
|
793
|
+
const error = err instanceof LokutorError ? err : new LokutorError("internal.error", "Failed to create WebSocket connection", { original: err });
|
|
794
|
+
if (this.onError) this.onError(error);
|
|
795
|
+
settle(() => reject(error));
|
|
744
796
|
}
|
|
745
797
|
});
|
|
746
798
|
}
|
|
747
799
|
/**
|
|
748
800
|
* The "Golden Path" - Starts a managed session with hardware handled automatically.
|
|
749
|
-
* This is the recommended way to start a conversation in
|
|
801
|
+
* This is the recommended way to start a conversation in browser environments.
|
|
750
802
|
*/
|
|
751
803
|
async startManaged(config) {
|
|
752
804
|
this.enableAudio = true;
|
|
753
805
|
if (config?.audioManager) {
|
|
754
806
|
this.audioManager = config.audioManager;
|
|
755
807
|
} else if (!this.audioManager) {
|
|
756
|
-
if (typeof window
|
|
757
|
-
|
|
758
|
-
} else {
|
|
759
|
-
try {
|
|
760
|
-
const { NodeAudioManager: NodeAudioManager2 } = await Promise.resolve().then(() => (init_node_audio(), node_audio_exports));
|
|
761
|
-
this.audioManager = new NodeAudioManager2();
|
|
762
|
-
} catch (e) {
|
|
763
|
-
console.error('\u274C Failed to load NodeAudioManager. Please ensure "speaker" and "node-record-lpcm16" are installed.');
|
|
764
|
-
}
|
|
808
|
+
if (typeof window === "undefined") {
|
|
809
|
+
throw new LokutorError("internal.error", "startManaged() requires a browser environment. Pass a custom audioManager for non-browser runtimes.", { retryable: false });
|
|
765
810
|
}
|
|
811
|
+
this.audioManager = new BrowserAudioManager();
|
|
766
812
|
}
|
|
767
813
|
await this.connect();
|
|
768
|
-
if (this.audioManager && this.isConnected) {
|
|
769
|
-
await this.audioManager.startMicrophone((data) => {
|
|
770
|
-
this.sendAudio(data);
|
|
771
|
-
});
|
|
772
|
-
}
|
|
773
814
|
return this;
|
|
774
815
|
}
|
|
775
816
|
/**
|
|
@@ -777,10 +818,18 @@ var VoiceAgentClient = class {
|
|
|
777
818
|
*/
|
|
778
819
|
sendConfig() {
|
|
779
820
|
if (!this.ws || !this.isConnected) return;
|
|
780
|
-
this.ws.send(JSON.stringify({ type: "
|
|
821
|
+
this.ws.send(JSON.stringify({ type: "visemes", data: this.wantVisemes }));
|
|
781
822
|
this.ws.send(JSON.stringify({ type: "voice", data: this.voice }));
|
|
782
823
|
this.ws.send(JSON.stringify({ type: "language", data: this.language }));
|
|
783
|
-
this.ws.send(JSON.stringify({ type: "
|
|
824
|
+
this.ws.send(JSON.stringify({ type: "prompt", data: this.prompt }));
|
|
825
|
+
this.ws.send(JSON.stringify({ type: "rates", playback: 44100, input: 16e3 }));
|
|
826
|
+
sdkTrace("ws.send.config", {
|
|
827
|
+
promptLen: this.prompt?.length || 0,
|
|
828
|
+
voice: this.voice,
|
|
829
|
+
language: this.language,
|
|
830
|
+
visemes: this.wantVisemes,
|
|
831
|
+
tools: this.tools?.length || 0
|
|
832
|
+
});
|
|
784
833
|
if (this.tools && this.tools.length > 0) {
|
|
785
834
|
this.ws.send(JSON.stringify({ type: "tools", data: this.tools }));
|
|
786
835
|
}
|
|
@@ -814,6 +863,15 @@ var VoiceAgentClient = class {
|
|
|
814
863
|
handleTextMessage(text) {
|
|
815
864
|
try {
|
|
816
865
|
const msg = JSON.parse(text);
|
|
866
|
+
if (!msg || typeof msg !== "object") {
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
sdkTrace("ws.recv.type", {
|
|
870
|
+
type: msg.type,
|
|
871
|
+
hasData: Object.prototype.hasOwnProperty.call(msg, "data"),
|
|
872
|
+
dataKind: Array.isArray(msg.data) ? "array" : typeof msg.data,
|
|
873
|
+
generation: msg.generation ?? null
|
|
874
|
+
});
|
|
817
875
|
switch (msg.type) {
|
|
818
876
|
case "audio":
|
|
819
877
|
if (msg.data) {
|
|
@@ -826,7 +884,7 @@ var VoiceAgentClient = class {
|
|
|
826
884
|
this.messages.push({
|
|
827
885
|
role,
|
|
828
886
|
text: msg.data,
|
|
829
|
-
timestamp:
|
|
887
|
+
timestamp: nowMs()
|
|
830
888
|
});
|
|
831
889
|
if (msg.role === "user") {
|
|
832
890
|
if (this.onTranscription) this.onTranscription(msg.data);
|
|
@@ -858,19 +916,44 @@ var VoiceAgentClient = class {
|
|
|
858
916
|
console.log(`${icons[msg.data] || ""} Status: ${msg.data}`);
|
|
859
917
|
break;
|
|
860
918
|
case "visemes":
|
|
861
|
-
|
|
862
|
-
|
|
919
|
+
case "viseme": {
|
|
920
|
+
const msgGen = msg.generation ?? this.currentGeneration;
|
|
921
|
+
if (msgGen < this.currentGeneration) {
|
|
922
|
+
sdkTrace("visemes.discard", { msgGen, currentGen: this.currentGeneration });
|
|
923
|
+
break;
|
|
924
|
+
}
|
|
925
|
+
const normalized = extractVisemePayload(msg);
|
|
926
|
+
const explicitlyEmptyArray = Array.isArray(msg?.data) || Array.isArray(msg?.data?.visemes);
|
|
927
|
+
sdkTrace("visemes.recv", {
|
|
928
|
+
rawType: msg.type,
|
|
929
|
+
normalizedCount: normalized.length,
|
|
930
|
+
first: normalized[0] ?? null
|
|
931
|
+
});
|
|
932
|
+
if (normalized.length > 0 || explicitlyEmptyArray) {
|
|
933
|
+
this.emit("visemes", normalized);
|
|
863
934
|
}
|
|
864
935
|
break;
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
936
|
+
}
|
|
937
|
+
case "error": {
|
|
938
|
+
const backendCode = msg.data?.code ?? "internal.error";
|
|
939
|
+
const backendMessage = msg.data?.message ?? msg.data ?? "Unknown server error";
|
|
940
|
+
const backendDetail = msg.data?.detail;
|
|
941
|
+
const backendRetryable = msg.data?.retryable ?? true;
|
|
942
|
+
const error = new LokutorError(backendCode, backendMessage, {
|
|
943
|
+
detail: backendDetail,
|
|
944
|
+
retryable: backendRetryable
|
|
945
|
+
});
|
|
946
|
+
if (this.onError) this.onError(error);
|
|
947
|
+
console.error(`\u274C Server error: [${error.code}] ${error.message}`);
|
|
868
948
|
break;
|
|
949
|
+
}
|
|
869
950
|
case "tool_call":
|
|
870
951
|
console.log(`\u{1F6E0}\uFE0F Tool Call: ${msg.name}(${msg.arguments})`);
|
|
871
952
|
break;
|
|
872
953
|
}
|
|
873
954
|
} catch (e) {
|
|
955
|
+
sdkTrace("ws.recv.parse_error", { preview: text?.slice(0, 120) });
|
|
956
|
+
console.debug("Failed to parse message:", e);
|
|
874
957
|
}
|
|
875
958
|
}
|
|
876
959
|
/**
|
|
@@ -932,6 +1015,21 @@ var VoiceAgentClient = class {
|
|
|
932
1015
|
this.audioManager.cleanup();
|
|
933
1016
|
}
|
|
934
1017
|
this.isConnected = false;
|
|
1018
|
+
this.reconnecting = false;
|
|
1019
|
+
this.reconnectAttempts = 0;
|
|
1020
|
+
}
|
|
1021
|
+
/**
|
|
1022
|
+
* Returns true if the client is currently connected.
|
|
1023
|
+
*/
|
|
1024
|
+
get connected() {
|
|
1025
|
+
return this.isConnected;
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* Returns the current generation counter.
|
|
1029
|
+
* Useful for correlating audio/viseme chunks with utterances.
|
|
1030
|
+
*/
|
|
1031
|
+
get generation() {
|
|
1032
|
+
return this.currentGeneration;
|
|
935
1033
|
}
|
|
936
1034
|
/**
|
|
937
1035
|
* Toggles the microphone mute state (if managed by client)
|
|
@@ -954,17 +1052,70 @@ var VoiceAgentClient = class {
|
|
|
954
1052
|
}
|
|
955
1053
|
return 0;
|
|
956
1054
|
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Fetch available voice styles from the server.
|
|
1057
|
+
* No authentication required.
|
|
1058
|
+
*/
|
|
1059
|
+
static async fetchVoices(baseUrl) {
|
|
1060
|
+
const url = wsToHttp(baseUrl || DEFAULT_URLS.VOICE_AGENT);
|
|
1061
|
+
const data = await fetchJson(`${url}/voices`);
|
|
1062
|
+
return data.voices || [];
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* Fetch supported languages from the server.
|
|
1066
|
+
* No authentication required.
|
|
1067
|
+
*/
|
|
1068
|
+
static async fetchLanguages(baseUrl) {
|
|
1069
|
+
const url = wsToHttp(baseUrl || DEFAULT_URLS.VOICE_AGENT);
|
|
1070
|
+
const data = await fetchJson(`${url}/languages`);
|
|
1071
|
+
return data.languages || [];
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Fetch loaded TTS model versions from the server.
|
|
1075
|
+
* No authentication required.
|
|
1076
|
+
*/
|
|
1077
|
+
static async fetchModels(baseUrl) {
|
|
1078
|
+
const url = wsToHttp(baseUrl || DEFAULT_URLS.VOICE_AGENT);
|
|
1079
|
+
const data = await fetchJson(`${url}/models`);
|
|
1080
|
+
return data.models || [];
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Fetch server configuration (limits and defaults).
|
|
1084
|
+
* No authentication required.
|
|
1085
|
+
*/
|
|
1086
|
+
static async fetchConfig(baseUrl) {
|
|
1087
|
+
const url = wsToHttp(baseUrl || DEFAULT_URLS.VOICE_AGENT);
|
|
1088
|
+
return fetchJson(`${url}/config`);
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* Fetch rich runtime status from the server.
|
|
1092
|
+
* No authentication required.
|
|
1093
|
+
*/
|
|
1094
|
+
static async fetchStatus(baseUrl) {
|
|
1095
|
+
const url = wsToHttp(baseUrl || DEFAULT_URLS.VOICE_AGENT);
|
|
1096
|
+
return fetchJson(`${url}/status`);
|
|
1097
|
+
}
|
|
1098
|
+
/**
|
|
1099
|
+
* Fetch health/liveness status from the server.
|
|
1100
|
+
* No authentication required.
|
|
1101
|
+
*/
|
|
1102
|
+
static async fetchHealth(baseUrl) {
|
|
1103
|
+
const url = wsToHttp(baseUrl || DEFAULT_URLS.VOICE_AGENT);
|
|
1104
|
+
return fetchJson(`${url}/health`);
|
|
1105
|
+
}
|
|
957
1106
|
/**
|
|
958
1107
|
* Update the system prompt mid-conversation
|
|
959
1108
|
*/
|
|
960
1109
|
updatePrompt(newPrompt) {
|
|
961
1110
|
this.prompt = newPrompt;
|
|
962
|
-
if (this.ws && this.isConnected) {
|
|
1111
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN && this.isConnected) {
|
|
963
1112
|
try {
|
|
964
1113
|
this.ws.send(JSON.stringify({ type: "prompt", data: newPrompt }));
|
|
965
1114
|
console.log(`\u2699\uFE0F Updated prompt: ${newPrompt.substring(0, 50)}...`);
|
|
966
1115
|
} catch (error) {
|
|
967
|
-
|
|
1116
|
+
const err = new LokutorError("internal.error", "Failed to update prompt", { original: error });
|
|
1117
|
+
if (this.onError) this.onError(err);
|
|
1118
|
+
console.error("Error updating prompt:", err.message);
|
|
968
1119
|
}
|
|
969
1120
|
} else {
|
|
970
1121
|
console.warn("Not connected - prompt will be updated on next connection");
|
|
@@ -1027,37 +1178,52 @@ var TTSClient = class {
|
|
|
1027
1178
|
visemes: options.visemes || false
|
|
1028
1179
|
};
|
|
1029
1180
|
ws.send(JSON.stringify(req));
|
|
1030
|
-
startTime =
|
|
1181
|
+
startTime = nowMs();
|
|
1031
1182
|
};
|
|
1032
1183
|
ws.onmessage = async (event) => {
|
|
1033
1184
|
refreshTimeout();
|
|
1034
1185
|
if (event.data instanceof ArrayBuffer) {
|
|
1035
1186
|
if (!firstByteReceived) {
|
|
1036
|
-
const ttfb =
|
|
1187
|
+
const ttfb = nowMs() - startTime;
|
|
1037
1188
|
if (options.onTTFB) options.onTTFB(ttfb);
|
|
1038
1189
|
firstByteReceived = true;
|
|
1039
1190
|
}
|
|
1040
1191
|
if (options.onAudio) options.onAudio(new Uint8Array(event.data));
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
const text = event.data.toString();
|
|
1195
|
+
if (text === "EOS") {
|
|
1196
|
+
if (activityTimeout) clearTimeout(activityTimeout);
|
|
1197
|
+
ws.close();
|
|
1198
|
+
resolve();
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
try {
|
|
1202
|
+
const msg = JSON.parse(text);
|
|
1203
|
+
if (msg.type === "audio" && msg.data) {
|
|
1204
|
+
const audioBuffer = base64ToUint8Array(msg.data);
|
|
1205
|
+
if (!firstByteReceived) {
|
|
1206
|
+
const ttfb = nowMs() - startTime;
|
|
1207
|
+
if (options.onTTFB) options.onTTFB(ttfb);
|
|
1208
|
+
firstByteReceived = true;
|
|
1209
|
+
}
|
|
1210
|
+
if (options.onAudio) options.onAudio(audioBuffer);
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
if (msg.type === "visemes" && Array.isArray(msg.data) && options.onVisemes) {
|
|
1214
|
+
options.onVisemes(normalizeVisemes(msg.data));
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
if (Array.isArray(msg) && options.onVisemes) {
|
|
1218
|
+
options.onVisemes(normalizeVisemes(msg));
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
if (msg.type === "eos") {
|
|
1044
1222
|
if (activityTimeout) clearTimeout(activityTimeout);
|
|
1045
1223
|
ws.close();
|
|
1046
1224
|
resolve();
|
|
1047
|
-
return;
|
|
1048
|
-
}
|
|
1049
|
-
try {
|
|
1050
|
-
const msg = JSON.parse(text);
|
|
1051
|
-
if (Array.isArray(msg) && options.onVisemes) {
|
|
1052
|
-
options.onVisemes(msg);
|
|
1053
|
-
}
|
|
1054
|
-
if (msg.type === "eos") {
|
|
1055
|
-
if (activityTimeout) clearTimeout(activityTimeout);
|
|
1056
|
-
ws.close();
|
|
1057
|
-
resolve();
|
|
1058
|
-
}
|
|
1059
|
-
} catch (e) {
|
|
1060
1225
|
}
|
|
1226
|
+
} catch (e) {
|
|
1061
1227
|
}
|
|
1062
1228
|
};
|
|
1063
1229
|
ws.onerror = (err) => {
|
|
@@ -1092,6 +1258,7 @@ async function simpleTTS(options) {
|
|
|
1092
1258
|
BrowserAudioManager,
|
|
1093
1259
|
DEFAULT_URLS,
|
|
1094
1260
|
Language,
|
|
1261
|
+
LokutorError,
|
|
1095
1262
|
StreamResampler,
|
|
1096
1263
|
TTSClient,
|
|
1097
1264
|
VoiceAgentClient,
|
|
@@ -1100,6 +1267,7 @@ async function simpleTTS(options) {
|
|
|
1100
1267
|
bytesToPcm16,
|
|
1101
1268
|
calculateRMS,
|
|
1102
1269
|
float32ToPcm16,
|
|
1270
|
+
isRetryable,
|
|
1103
1271
|
normalizeAudio,
|
|
1104
1272
|
pcm16ToBytes,
|
|
1105
1273
|
pcm16ToFloat32,
|