@lokutor/sdk 1.5.1 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -2
- package/dist/index.d.mts +34 -21
- package/dist/index.d.ts +34 -21
- package/dist/index.js +151 -86
- package/dist/index.mjs +151 -86
- package/package.json +1 -1
- package/src/client.ts +161 -92
- package/src/types.ts +36 -17
package/dist/index.js
CHANGED
|
@@ -140,7 +140,12 @@ function isRetryableCode(code) {
|
|
|
140
140
|
"validation.speed_out_of_range",
|
|
141
141
|
"validation.steps_out_of_range",
|
|
142
142
|
"validation.invalid_request_format",
|
|
143
|
-
"internal.cancelled"
|
|
143
|
+
"internal.cancelled",
|
|
144
|
+
"agent.not_found",
|
|
145
|
+
"billing.insufficient_balance",
|
|
146
|
+
"billing.topup_required",
|
|
147
|
+
"billing.limit_reached",
|
|
148
|
+
"billing.account_disputed"
|
|
144
149
|
];
|
|
145
150
|
return !fatal.includes(code);
|
|
146
151
|
}
|
|
@@ -715,6 +720,9 @@ function nowMs() {
|
|
|
715
720
|
function wsToHttp(url) {
|
|
716
721
|
return url.replace(/^wss:/, "https:").replace(/^ws:/, "http:");
|
|
717
722
|
}
|
|
723
|
+
function apiOrigin(base) {
|
|
724
|
+
return new URL(wsToHttp(base || DEFAULT_URLS.VOICE_AGENT)).origin;
|
|
725
|
+
}
|
|
718
726
|
async function fetchJson(url, timeoutMs = 1e4) {
|
|
719
727
|
const controller = new AbortController();
|
|
720
728
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -827,10 +835,16 @@ var VoiceAgentClient = class {
|
|
|
827
835
|
serverPlaybackRate = 44100;
|
|
828
836
|
serverInputRate = 16e3;
|
|
829
837
|
serverUrl;
|
|
838
|
+
authProvider;
|
|
839
|
+
voiceExplicit = false;
|
|
840
|
+
languageExplicit = false;
|
|
830
841
|
constructor(config) {
|
|
831
|
-
this.apiKey = config.apiKey;
|
|
842
|
+
this.apiKey = config.apiKey || "";
|
|
843
|
+
this.authProvider = config.auth;
|
|
832
844
|
this.agentId = config.agentId || "";
|
|
833
|
-
this.prompt = config.prompt;
|
|
845
|
+
this.prompt = config.prompt ?? "";
|
|
846
|
+
this.voiceExplicit = config.voice !== void 0;
|
|
847
|
+
this.languageExplicit = config.language !== void 0;
|
|
834
848
|
this.voice = config.voice || "F1" /* F1 */;
|
|
835
849
|
this.language = config.language || "en" /* ENGLISH */;
|
|
836
850
|
this.serverUrl = config.serverUrl || DEFAULT_URLS.VOICE_AGENT;
|
|
@@ -850,6 +864,15 @@ var VoiceAgentClient = class {
|
|
|
850
864
|
*/
|
|
851
865
|
async connect(customAudioManager) {
|
|
852
866
|
this.isUserDisconnect = false;
|
|
867
|
+
if (this.authProvider) {
|
|
868
|
+
try {
|
|
869
|
+
this.apiKey = await this.authProvider();
|
|
870
|
+
} catch (err) {
|
|
871
|
+
const error = new LokutorError("auth.missing_key", "The auth callback failed to return a credential", { original: err, retryable: true });
|
|
872
|
+
this.emit("error", error);
|
|
873
|
+
throw error;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
853
876
|
if (this.enableAudio || customAudioManager) {
|
|
854
877
|
if (customAudioManager) {
|
|
855
878
|
this.audioManager = customAudioManager;
|
|
@@ -872,11 +895,11 @@ var VoiceAgentClient = class {
|
|
|
872
895
|
let url = this.serverUrl;
|
|
873
896
|
if (this.apiKey) {
|
|
874
897
|
const separator = url.includes("?") ? "&" : "?";
|
|
875
|
-
url += `${separator}api_key=${this.apiKey}`;
|
|
898
|
+
url += `${separator}api_key=${encodeURIComponent(this.apiKey)}`;
|
|
876
899
|
}
|
|
877
900
|
if (this.agentId) {
|
|
878
901
|
const separator = url.includes("?") ? "&" : "?";
|
|
879
|
-
url += `${separator}agent_id=${this.agentId}`;
|
|
902
|
+
url += `${separator}agent_id=${encodeURIComponent(this.agentId)}`;
|
|
880
903
|
}
|
|
881
904
|
const redactedUrl = url.replace(/api_key=[^&]+/, "api_key=***");
|
|
882
905
|
sdkTrace("ws.connect", {
|
|
@@ -942,7 +965,7 @@ var VoiceAgentClient = class {
|
|
|
942
965
|
});
|
|
943
966
|
console.error("\u274C WebSocket error:", error.message);
|
|
944
967
|
sdkTrace("ws.error", { code: error.code, message: error.message });
|
|
945
|
-
|
|
968
|
+
this.emit("error", error);
|
|
946
969
|
if (!this.isConnected) {
|
|
947
970
|
settle(() => reject(error));
|
|
948
971
|
}
|
|
@@ -979,8 +1002,8 @@ var VoiceAgentClient = class {
|
|
|
979
1002
|
});
|
|
980
1003
|
console.log(`Session ended by the server (code ${event.code})`);
|
|
981
1004
|
this.emit("ended", { code: event.code, reason: event.reason || "" });
|
|
982
|
-
|
|
983
|
-
|
|
1005
|
+
this.emit("error", ended);
|
|
1006
|
+
this.emit("status", "disconnected");
|
|
984
1007
|
return;
|
|
985
1008
|
}
|
|
986
1009
|
if (!this.isUserDisconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
@@ -988,7 +1011,7 @@ var VoiceAgentClient = class {
|
|
|
988
1011
|
this.reconnectAttempts++;
|
|
989
1012
|
const backoffDelay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), 1e4);
|
|
990
1013
|
console.warn(`Connection lost. Reconnecting in ${backoffDelay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
|
|
991
|
-
|
|
1014
|
+
this.emit("status", "reconnecting");
|
|
992
1015
|
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
993
1016
|
this.reconnectTimer = setTimeout(() => {
|
|
994
1017
|
this.reconnectTimer = null;
|
|
@@ -998,15 +1021,15 @@ var VoiceAgentClient = class {
|
|
|
998
1021
|
}, backoffDelay);
|
|
999
1022
|
} else if (this.isUserDisconnect) {
|
|
1000
1023
|
console.log("Disconnected");
|
|
1001
|
-
|
|
1024
|
+
this.emit("status", "disconnected");
|
|
1002
1025
|
} else {
|
|
1003
1026
|
console.log("Max reconnection attempts reached");
|
|
1004
|
-
|
|
1027
|
+
this.emit("status", "disconnected");
|
|
1005
1028
|
}
|
|
1006
1029
|
};
|
|
1007
1030
|
} catch (err) {
|
|
1008
1031
|
const error = err instanceof LokutorError ? err : new LokutorError("internal.error", "Failed to create WebSocket connection", { original: err });
|
|
1009
|
-
|
|
1032
|
+
this.emit("error", error);
|
|
1010
1033
|
settle(() => reject(error));
|
|
1011
1034
|
}
|
|
1012
1035
|
});
|
|
@@ -1034,13 +1057,14 @@ var VoiceAgentClient = class {
|
|
|
1034
1057
|
sendConfig() {
|
|
1035
1058
|
if (!this.ws || !this.isConnected) return;
|
|
1036
1059
|
this.configConfirmed = false;
|
|
1037
|
-
|
|
1038
|
-
this.ws.send(JSON.stringify({ type: "
|
|
1060
|
+
const agentSession = Boolean(this.agentId);
|
|
1061
|
+
if (!agentSession || this.voiceExplicit) this.ws.send(JSON.stringify({ type: "voice", data: this.voice }));
|
|
1062
|
+
if (!agentSession || this.languageExplicit) this.ws.send(JSON.stringify({ type: "language", data: this.language }));
|
|
1039
1063
|
if (this.tools && this.tools.length > 0) {
|
|
1040
1064
|
this.ws.send(JSON.stringify({ type: "tools", data: this.tools }));
|
|
1041
1065
|
}
|
|
1042
1066
|
this.ws.send(JSON.stringify({ type: "visemes", data: this.wantVisemes }));
|
|
1043
|
-
this.ws.send(JSON.stringify({ type: "prompt", data: this.prompt }));
|
|
1067
|
+
if (!agentSession || this.prompt) this.ws.send(JSON.stringify({ type: "prompt", data: this.prompt }));
|
|
1044
1068
|
this.ws.send(JSON.stringify({ type: "rates", playback: 44100, input: 16e3 }));
|
|
1045
1069
|
sdkTrace("ws.send.config", {
|
|
1046
1070
|
promptLen: this.prompt?.length || 0,
|
|
@@ -1101,6 +1125,10 @@ var VoiceAgentClient = class {
|
|
|
1101
1125
|
sdkTrace("server_rates", { playback: this.serverPlaybackRate, input: this.serverInputRate });
|
|
1102
1126
|
break;
|
|
1103
1127
|
case "transcript":
|
|
1128
|
+
if (msg.partial) {
|
|
1129
|
+
this.emit("transcription_partial", msg.data);
|
|
1130
|
+
break;
|
|
1131
|
+
}
|
|
1104
1132
|
const role = msg.role === "user" ? "user" : "agent";
|
|
1105
1133
|
this.messages.push({
|
|
1106
1134
|
role,
|
|
@@ -1108,11 +1136,9 @@ var VoiceAgentClient = class {
|
|
|
1108
1136
|
timestamp: nowMs()
|
|
1109
1137
|
});
|
|
1110
1138
|
if (msg.role === "user") {
|
|
1111
|
-
if (this.onTranscription) this.onTranscription(msg.data);
|
|
1112
1139
|
this.emit("transcription", msg.data);
|
|
1113
1140
|
console.log(`\u{1F4AC} You: ${msg.data}`);
|
|
1114
1141
|
} else {
|
|
1115
|
-
if (this.onResponse) this.onResponse(msg.data);
|
|
1116
1142
|
this.emit("response", msg.data);
|
|
1117
1143
|
console.log(`\u{1F916} Agent: ${msg.data}`);
|
|
1118
1144
|
}
|
|
@@ -1134,7 +1160,6 @@ var VoiceAgentClient = class {
|
|
|
1134
1160
|
if (msg.data === "interrupted" && this.audioManager) {
|
|
1135
1161
|
this.audioManager.stopPlayback();
|
|
1136
1162
|
}
|
|
1137
|
-
if (this.onStatus) this.onStatus(msg.data);
|
|
1138
1163
|
this.emit("status", msg.data);
|
|
1139
1164
|
const icons = {
|
|
1140
1165
|
"interrupted": "\u26A1",
|
|
@@ -1184,11 +1209,12 @@ var VoiceAgentClient = class {
|
|
|
1184
1209
|
}
|
|
1185
1210
|
break;
|
|
1186
1211
|
}
|
|
1187
|
-
case "tool_call":
|
|
1188
|
-
|
|
1212
|
+
case "tool_call": {
|
|
1213
|
+
const call = { name: msg.name, arguments: msg.arguments, call_id: msg.call_id };
|
|
1189
1214
|
console.log(`\u{1F6E0}\uFE0F Tool Call: ${msg.name}(${msg.arguments})`);
|
|
1190
|
-
this.emit("tool_call",
|
|
1215
|
+
this.emit("tool_call", call);
|
|
1191
1216
|
break;
|
|
1217
|
+
}
|
|
1192
1218
|
}
|
|
1193
1219
|
} catch (e) {
|
|
1194
1220
|
sdkTrace("ws.recv.parse_error", { preview: text?.slice(0, 120) });
|
|
@@ -1302,7 +1328,7 @@ var VoiceAgentClient = class {
|
|
|
1302
1328
|
* No authentication required.
|
|
1303
1329
|
*/
|
|
1304
1330
|
static async fetchVoices(baseUrl) {
|
|
1305
|
-
const url =
|
|
1331
|
+
const url = apiOrigin(baseUrl);
|
|
1306
1332
|
const data = await fetchJson(`${url}/voices`);
|
|
1307
1333
|
return data.voices || [];
|
|
1308
1334
|
}
|
|
@@ -1311,7 +1337,7 @@ var VoiceAgentClient = class {
|
|
|
1311
1337
|
* No authentication required.
|
|
1312
1338
|
*/
|
|
1313
1339
|
static async fetchLanguages(baseUrl) {
|
|
1314
|
-
const url =
|
|
1340
|
+
const url = apiOrigin(baseUrl);
|
|
1315
1341
|
const data = await fetchJson(`${url}/languages`);
|
|
1316
1342
|
return data.languages || [];
|
|
1317
1343
|
}
|
|
@@ -1320,7 +1346,7 @@ var VoiceAgentClient = class {
|
|
|
1320
1346
|
* No authentication required.
|
|
1321
1347
|
*/
|
|
1322
1348
|
static async fetchModels(baseUrl) {
|
|
1323
|
-
const url =
|
|
1349
|
+
const url = apiOrigin(baseUrl);
|
|
1324
1350
|
const data = await fetchJson(`${url}/models`);
|
|
1325
1351
|
return data.models || [];
|
|
1326
1352
|
}
|
|
@@ -1329,7 +1355,7 @@ var VoiceAgentClient = class {
|
|
|
1329
1355
|
* No authentication required.
|
|
1330
1356
|
*/
|
|
1331
1357
|
static async fetchConfig(baseUrl) {
|
|
1332
|
-
const url =
|
|
1358
|
+
const url = apiOrigin(baseUrl);
|
|
1333
1359
|
return fetchJson(`${url}/config`);
|
|
1334
1360
|
}
|
|
1335
1361
|
/**
|
|
@@ -1337,16 +1363,16 @@ var VoiceAgentClient = class {
|
|
|
1337
1363
|
* No authentication required.
|
|
1338
1364
|
*/
|
|
1339
1365
|
static async fetchStatus(baseUrl) {
|
|
1340
|
-
const url =
|
|
1366
|
+
const url = apiOrigin(baseUrl);
|
|
1341
1367
|
return fetchJson(`${url}/status`);
|
|
1342
1368
|
}
|
|
1343
1369
|
/**
|
|
1344
|
-
* Fetch
|
|
1345
|
-
* No authentication required.
|
|
1370
|
+
* Fetch readiness from the server (GET /ready: 200 once it can serve).
|
|
1371
|
+
* No authentication required. (/health is the operator dashboard's and needs its password.)
|
|
1346
1372
|
*/
|
|
1347
1373
|
static async fetchHealth(baseUrl) {
|
|
1348
|
-
const url =
|
|
1349
|
-
return fetchJson(`${url}/
|
|
1374
|
+
const url = apiOrigin(baseUrl);
|
|
1375
|
+
return fetchJson(`${url}/ready`);
|
|
1350
1376
|
}
|
|
1351
1377
|
/**
|
|
1352
1378
|
* Update the system prompt mid-conversation
|
|
@@ -1371,6 +1397,7 @@ var VoiceAgentClient = class {
|
|
|
1371
1397
|
*/
|
|
1372
1398
|
updateVoice(voice) {
|
|
1373
1399
|
this.voice = voice;
|
|
1400
|
+
this.voiceExplicit = true;
|
|
1374
1401
|
if (this.ws && this.ws.readyState === WebSocket.OPEN && this.isConnected) {
|
|
1375
1402
|
this.ws.send(JSON.stringify({ type: "voice", data: voice }));
|
|
1376
1403
|
console.log(`\u2699\uFE0F Updated voice: ${voice}`);
|
|
@@ -1381,6 +1408,7 @@ var VoiceAgentClient = class {
|
|
|
1381
1408
|
*/
|
|
1382
1409
|
updateLanguage(language) {
|
|
1383
1410
|
this.language = language;
|
|
1411
|
+
this.languageExplicit = true;
|
|
1384
1412
|
if (this.ws && this.ws.readyState === WebSocket.OPEN && this.isConnected) {
|
|
1385
1413
|
this.ws.send(JSON.stringify({ type: "language", data: language }));
|
|
1386
1414
|
console.log(`\u2699\uFE0F Updated language: ${language}`);
|
|
@@ -1440,98 +1468,120 @@ var TTSClient = class {
|
|
|
1440
1468
|
*/
|
|
1441
1469
|
synthesize(options) {
|
|
1442
1470
|
return new Promise((resolve, reject) => {
|
|
1443
|
-
let
|
|
1471
|
+
let timer;
|
|
1444
1472
|
let ws;
|
|
1445
1473
|
let startTime;
|
|
1446
1474
|
let firstByteReceived = false;
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1475
|
+
let finished = false;
|
|
1476
|
+
const finish = (err) => {
|
|
1477
|
+
if (finished) return;
|
|
1478
|
+
finished = true;
|
|
1479
|
+
if (timer) clearTimeout(timer);
|
|
1480
|
+
try {
|
|
1481
|
+
ws?.close();
|
|
1482
|
+
} catch {
|
|
1483
|
+
}
|
|
1484
|
+
if (err) {
|
|
1485
|
+
if (options.onError) options.onError(err);
|
|
1486
|
+
reject(err);
|
|
1487
|
+
} else {
|
|
1452
1488
|
resolve();
|
|
1453
|
-
}
|
|
1489
|
+
}
|
|
1490
|
+
};
|
|
1491
|
+
const arm = () => {
|
|
1492
|
+
if (timer) clearTimeout(timer);
|
|
1493
|
+
timer = setTimeout(
|
|
1494
|
+
() => finish(new LokutorError(
|
|
1495
|
+
"internal.timeout",
|
|
1496
|
+
firstByteReceived ? "TTS stream stalled" : "No audio within 30 s",
|
|
1497
|
+
{ retryable: true }
|
|
1498
|
+
)),
|
|
1499
|
+
firstByteReceived ? 15e3 : 3e4
|
|
1500
|
+
);
|
|
1454
1501
|
};
|
|
1455
1502
|
try {
|
|
1456
1503
|
let url = DEFAULT_URLS.TTS;
|
|
1457
1504
|
if (this.apiKey) {
|
|
1458
1505
|
const separator = url.includes("?") ? "&" : "?";
|
|
1459
|
-
url += `${separator}api_key=${this.apiKey}`;
|
|
1506
|
+
url += `${separator}api_key=${encodeURIComponent(this.apiKey)}`;
|
|
1460
1507
|
}
|
|
1461
1508
|
ws = new WebSocket(url);
|
|
1462
1509
|
ws.binaryType = "arraybuffer";
|
|
1463
1510
|
ws.onopen = () => {
|
|
1464
|
-
|
|
1511
|
+
arm();
|
|
1465
1512
|
const req = {
|
|
1466
1513
|
text: options.text,
|
|
1467
1514
|
voice: options.voice || "F1" /* F1 */,
|
|
1468
1515
|
language: options.language || "en" /* ENGLISH */,
|
|
1469
|
-
speed: options.speed || 1.05,
|
|
1470
|
-
steps: options.steps || 24,
|
|
1471
1516
|
visemes: options.visemes || false
|
|
1472
1517
|
};
|
|
1518
|
+
if (options.speed !== void 0) req.speed = options.speed;
|
|
1519
|
+
if (options.steps !== void 0) req.steps = options.steps;
|
|
1473
1520
|
ws.send(JSON.stringify(req));
|
|
1474
1521
|
startTime = nowMs();
|
|
1475
1522
|
};
|
|
1476
1523
|
ws.onmessage = async (event) => {
|
|
1477
|
-
refreshTimeout();
|
|
1478
1524
|
if (event.data instanceof ArrayBuffer) {
|
|
1479
1525
|
if (!firstByteReceived) {
|
|
1480
1526
|
const ttfb = nowMs() - startTime;
|
|
1481
1527
|
if (options.onTTFB) options.onTTFB(ttfb);
|
|
1482
1528
|
firstByteReceived = true;
|
|
1483
1529
|
}
|
|
1530
|
+
arm();
|
|
1484
1531
|
if (options.onAudio) options.onAudio(new Uint8Array(event.data));
|
|
1485
1532
|
return;
|
|
1486
1533
|
}
|
|
1487
1534
|
const text = event.data.toString();
|
|
1488
1535
|
if (text === "EOS") {
|
|
1489
|
-
|
|
1490
|
-
ws.close();
|
|
1491
|
-
resolve();
|
|
1536
|
+
finish();
|
|
1492
1537
|
return;
|
|
1493
1538
|
}
|
|
1539
|
+
let msg;
|
|
1494
1540
|
try {
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
options.
|
|
1512
|
-
|
|
1513
|
-
}
|
|
1514
|
-
if (msg.type === "eos") {
|
|
1515
|
-
if (activityTimeout) clearTimeout(activityTimeout);
|
|
1516
|
-
ws.close();
|
|
1517
|
-
resolve();
|
|
1541
|
+
msg = JSON.parse(text);
|
|
1542
|
+
} catch {
|
|
1543
|
+
return;
|
|
1544
|
+
}
|
|
1545
|
+
if (msg.type === "error") {
|
|
1546
|
+
const data = typeof msg.data === "object" && msg.data ? msg.data : { message: msg.data };
|
|
1547
|
+
finish(new LokutorError(data.code ?? "internal.error", data.message ?? "TTS failed", {
|
|
1548
|
+
detail: data.detail,
|
|
1549
|
+
retryable: data.retryable ?? false
|
|
1550
|
+
}));
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
if (msg.type === "audio" && msg.data) {
|
|
1554
|
+
const audioBuffer = base64ToUint8Array(msg.data);
|
|
1555
|
+
if (!firstByteReceived) {
|
|
1556
|
+
const ttfb = nowMs() - startTime;
|
|
1557
|
+
if (options.onTTFB) options.onTTFB(ttfb);
|
|
1558
|
+
firstByteReceived = true;
|
|
1518
1559
|
}
|
|
1519
|
-
|
|
1560
|
+
arm();
|
|
1561
|
+
if (options.onAudio) options.onAudio(audioBuffer);
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
if (msg.type === "visemes" && Array.isArray(msg.data) && options.onVisemes) {
|
|
1565
|
+
options.onVisemes(normalizeVisemes(msg.data));
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1568
|
+
if (Array.isArray(msg) && options.onVisemes) {
|
|
1569
|
+
options.onVisemes(normalizeVisemes(msg));
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
if (msg.type === "eos") {
|
|
1573
|
+
finish();
|
|
1520
1574
|
}
|
|
1521
1575
|
};
|
|
1522
|
-
ws.onerror = (err) =>
|
|
1523
|
-
|
|
1524
|
-
if (
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
if (activityTimeout) clearTimeout(activityTimeout);
|
|
1529
|
-
resolve();
|
|
1576
|
+
ws.onerror = (err) => finish(err);
|
|
1577
|
+
ws.onclose = (ev) => {
|
|
1578
|
+
if (!finished) {
|
|
1579
|
+
if (firstByteReceived && ev.code === 1e3) finish();
|
|
1580
|
+
else finish(new LokutorError("internal.error", `TTS socket closed (${ev.code}${ev.reason ? `: ${ev.reason}` : ""})`, { retryable: true }));
|
|
1581
|
+
}
|
|
1530
1582
|
};
|
|
1531
1583
|
} catch (err) {
|
|
1532
|
-
|
|
1533
|
-
if (options.onError) options.onError(err);
|
|
1534
|
-
reject(err);
|
|
1584
|
+
finish(err);
|
|
1535
1585
|
}
|
|
1536
1586
|
});
|
|
1537
1587
|
}
|
|
@@ -1637,17 +1687,28 @@ var SpeechToTextClient = class {
|
|
|
1637
1687
|
try {
|
|
1638
1688
|
let url = this.serverUrl;
|
|
1639
1689
|
const separator = url.includes("?") ? "&" : "?";
|
|
1640
|
-
url += `${separator}api_key=${this.apiKey}`;
|
|
1690
|
+
url += `${separator}api_key=${encodeURIComponent(this.apiKey)}`;
|
|
1641
1691
|
this.ws = new WebSocket(url);
|
|
1642
1692
|
this.ws.onopen = async () => {
|
|
1643
1693
|
this.isConnected = true;
|
|
1644
1694
|
this.onStatusChange?.("connected");
|
|
1645
1695
|
this.ws.send(JSON.stringify({ language: this.language || "en" /* ENGLISH */, vad: this.vad }));
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
this.ws.
|
|
1696
|
+
try {
|
|
1697
|
+
await this.audioManager.startMicrophone((data) => {
|
|
1698
|
+
if (this.isConnected && this.ws?.readyState === WebSocket.OPEN) {
|
|
1699
|
+
this.ws.send(data);
|
|
1700
|
+
}
|
|
1701
|
+
});
|
|
1702
|
+
} catch (err) {
|
|
1703
|
+
const error = err instanceof LokutorError ? err : new LokutorError("audio.microphone_unavailable", "Could not start the microphone", { original: err });
|
|
1704
|
+
this.onError?.(error);
|
|
1705
|
+
try {
|
|
1706
|
+
this.ws?.close();
|
|
1707
|
+
} catch {
|
|
1649
1708
|
}
|
|
1650
|
-
|
|
1709
|
+
settle(() => reject(error));
|
|
1710
|
+
return;
|
|
1711
|
+
}
|
|
1651
1712
|
settle(() => resolve(true));
|
|
1652
1713
|
};
|
|
1653
1714
|
this.ws.onmessage = (event) => {
|
|
@@ -1661,7 +1722,11 @@ var SpeechToTextClient = class {
|
|
|
1661
1722
|
this.onPartialTranscript?.(msg.data ?? "");
|
|
1662
1723
|
}
|
|
1663
1724
|
} else if (msg.type === "error") {
|
|
1664
|
-
|
|
1725
|
+
const data = typeof msg.data === "object" && msg.data ? msg.data : { message: msg.data };
|
|
1726
|
+
this.onError?.(new LokutorError(data.code ?? "internal.error", data.message ?? "STT error", {
|
|
1727
|
+
detail: data.detail,
|
|
1728
|
+
retryable: data.retryable ?? false
|
|
1729
|
+
}));
|
|
1665
1730
|
}
|
|
1666
1731
|
} catch {
|
|
1667
1732
|
}
|