botium-connector-voip 0.0.19 → 0.0.21
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.
|
@@ -5,8 +5,9 @@ Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
5
5
|
var ws = require('ws');
|
|
6
6
|
var lodash = require('lodash');
|
|
7
7
|
var axios = require('axios');
|
|
8
|
+
var http = require('http');
|
|
9
|
+
var https = require('https');
|
|
8
10
|
var debug$4 = require('debug');
|
|
9
|
-
var path = require('path');
|
|
10
11
|
var musicMetadata = require('music-metadata');
|
|
11
12
|
|
|
12
13
|
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
|
|
@@ -14,8 +15,9 @@ function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'defau
|
|
|
14
15
|
var ws__default = /*#__PURE__*/_interopDefaultLegacy(ws);
|
|
15
16
|
var lodash__default = /*#__PURE__*/_interopDefaultLegacy(lodash);
|
|
16
17
|
var axios__default = /*#__PURE__*/_interopDefaultLegacy(axios);
|
|
18
|
+
var http__default = /*#__PURE__*/_interopDefaultLegacy(http);
|
|
19
|
+
var https__default = /*#__PURE__*/_interopDefaultLegacy(https);
|
|
17
20
|
var debug__default = /*#__PURE__*/_interopDefaultLegacy(debug$4);
|
|
18
|
-
var path__default = /*#__PURE__*/_interopDefaultLegacy(path);
|
|
19
21
|
var musicMetadata__default = /*#__PURE__*/_interopDefaultLegacy(musicMetadata);
|
|
20
22
|
|
|
21
23
|
const debug$3 = debug__default["default"]('botium-connector-voip');
|
|
@@ -37,6 +39,9 @@ const Capabilities = {
|
|
|
37
39
|
VOIP_TTS_BODY: 'VOIP_TTS_BODY',
|
|
38
40
|
VOIP_TTS_HEADERS: 'VOIP_TTS_HEADERS',
|
|
39
41
|
VOIP_TTS_TIMEOUT: 'VOIP_TTS_TIMEOUT',
|
|
42
|
+
VOIP_TTS_CACHE_ENABLE: 'VOIP_TTS_CACHE_ENABLE',
|
|
43
|
+
VOIP_TTS_CACHE_SIZE: 'VOIP_TTS_CACHE_SIZE',
|
|
44
|
+
VOIP_TTS_PREFETCH_ENABLE: 'VOIP_TTS_PREFETCH_ENABLE',
|
|
40
45
|
VOIP_WORKER_URL: 'VOIP_WORKER_URL',
|
|
41
46
|
VOIP_WORKER_APIKEY: 'VOIP_WORKER_APIKEY',
|
|
42
47
|
VOIP_SIP_POOL_CALLER_ENABLE: 'VOIP_SIP_POOL_CALLER_ENABLE',
|
|
@@ -70,6 +75,9 @@ const Defaults = {
|
|
|
70
75
|
VOIP_STT_TIMEOUT: 10000,
|
|
71
76
|
VOIP_TTS_METHOD: 'GET',
|
|
72
77
|
VOIP_TTS_TIMEOUT: 10000,
|
|
78
|
+
VOIP_TTS_CACHE_ENABLE: true,
|
|
79
|
+
VOIP_TTS_CACHE_SIZE: 50,
|
|
80
|
+
VOIP_TTS_PREFETCH_ENABLE: true,
|
|
73
81
|
VOIP_STT_MESSAGE_HANDLING: 'ORIGINAL',
|
|
74
82
|
VOIP_STT_MESSAGE_HANDLING_TIMEOUT: 2500,
|
|
75
83
|
VOIP_STT_MESSAGE_HANDLING_DELIMITER: '. ',
|
|
@@ -84,6 +92,12 @@ const Defaults = {
|
|
|
84
92
|
VOIP_USE_GLOBAL_VOIP_WORKER: false,
|
|
85
93
|
VOIP_SIP_PROTOCOL: 'TCP'
|
|
86
94
|
};
|
|
95
|
+
const TTS_HTTP_AGENT = new http__default["default"].Agent({
|
|
96
|
+
keepAlive: true
|
|
97
|
+
});
|
|
98
|
+
const TTS_HTTPS_AGENT = new https__default["default"].Agent({
|
|
99
|
+
keepAlive: true
|
|
100
|
+
});
|
|
87
101
|
class BotiumConnectorVoip {
|
|
88
102
|
constructor({
|
|
89
103
|
queueBotSays,
|
|
@@ -97,9 +111,18 @@ class BotiumConnectorVoip {
|
|
|
97
111
|
this.sentencesBuilding = 0;
|
|
98
112
|
this.sentencesFinal = 0;
|
|
99
113
|
this.sentenceBuilding = false;
|
|
114
|
+
this.ttsCache = new Map();
|
|
115
|
+
this.ttsCacheEnabled = false;
|
|
116
|
+
this.ttsCacheMaxEntries = 0;
|
|
117
|
+
this.ttsPrefetchEnabled = false;
|
|
118
|
+
|
|
119
|
+
// For debugging latency between incoming STT (bot says) and outgoing audio (sendAudio)
|
|
120
|
+
this._lastBotSaysQueuedAt = null;
|
|
121
|
+
this._lastBotSaysText = null;
|
|
100
122
|
}
|
|
101
123
|
async Validate() {
|
|
102
124
|
debug$3('Validate called');
|
|
125
|
+
this.caps = Object.assign({}, Defaults, this.caps);
|
|
103
126
|
debug$3(this.caps.VOIP_STT_MESSAGE_HANDLING);
|
|
104
127
|
if (this.caps.VOIP_TTS_URL) {
|
|
105
128
|
this.axiosTtsParams = {
|
|
@@ -107,7 +130,9 @@ class BotiumConnectorVoip {
|
|
|
107
130
|
params: this._getParams(Capabilities.VOIP_TTS_PARAMS),
|
|
108
131
|
method: this.caps.VOIP_TTS_METHOD,
|
|
109
132
|
timeout: this.caps.VOIP_TTS_TIMEOUT,
|
|
110
|
-
headers: this._getHeaders(Capabilities.VOIP_TTS_HEADERS)
|
|
133
|
+
headers: this._getHeaders(Capabilities.VOIP_TTS_HEADERS),
|
|
134
|
+
httpAgent: TTS_HTTP_AGENT,
|
|
135
|
+
httpsAgent: TTS_HTTPS_AGENT
|
|
111
136
|
};
|
|
112
137
|
try {
|
|
113
138
|
const {
|
|
@@ -125,7 +150,10 @@ class BotiumConnectorVoip {
|
|
|
125
150
|
throw new Error(`Checking TTS Status failed - ${this._getAxiosErrOutput(err)}`);
|
|
126
151
|
}
|
|
127
152
|
}
|
|
128
|
-
|
|
153
|
+
const cacheSize = parseInt(this.caps[Capabilities.VOIP_TTS_CACHE_SIZE], 10);
|
|
154
|
+
this.ttsCacheEnabled = !!this.caps[Capabilities.VOIP_TTS_CACHE_ENABLE];
|
|
155
|
+
this.ttsCacheMaxEntries = lodash__default["default"].isFinite(cacheSize) && cacheSize > 0 ? cacheSize : 0;
|
|
156
|
+
this.ttsPrefetchEnabled = !!this.caps[Capabilities.VOIP_TTS_PREFETCH_ENABLE];
|
|
129
157
|
}
|
|
130
158
|
async Start() {
|
|
131
159
|
debug$3('Start called');
|
|
@@ -135,19 +163,31 @@ class BotiumConnectorVoip {
|
|
|
135
163
|
this.end = false;
|
|
136
164
|
this.connected = false;
|
|
137
165
|
this.convoStep = null;
|
|
166
|
+
this._lastBotSaysQueuedAt = null;
|
|
167
|
+
this._lastBotSaysText = null;
|
|
168
|
+
if (this.ttsCache) {
|
|
169
|
+
this.ttsCache.clear();
|
|
170
|
+
}
|
|
138
171
|
const sendBotMsg = botMsg => {
|
|
172
|
+
this._lastBotSaysQueuedAt = Date.now();
|
|
173
|
+
this._lastBotSaysText = botMsg && botMsg.messageText ? String(botMsg.messageText) : null;
|
|
139
174
|
setTimeout(() => this.queueBotSays(botMsg), 0);
|
|
140
175
|
};
|
|
141
176
|
const joinBotMsg = (botMsgs, joinLastPrevMsg) => {
|
|
142
177
|
const botMsg = {};
|
|
143
178
|
botMsg.messageText = botMsgs.map(m => m.messageText).join(this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING_DELIMITER] || '');
|
|
144
179
|
botMsg.sourceData = botMsgs.map(m => m.sourceData);
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
180
|
+
const firstStart = lodash__default["default"].get(botMsgs, '[0].sourceData.data.start', null);
|
|
181
|
+
const lastEnd = lodash__default["default"].get(botMsgs, `[${botMsgs.length - 1}].sourceData.data.end`, null);
|
|
182
|
+
const prevEnd = lodash__default["default"].get(joinLastPrevMsg, 'sourceData.data.end', null);
|
|
183
|
+
// joinLastPrevMsg can be null (first joined message) OR incomplete (in case JOIN timeout fired before we got any final STT info).
|
|
184
|
+
// In those cases, fall back to "start-of-first-chunk" semantics (same as previous behaviour for nil joinLastPrevMsg).
|
|
185
|
+
if (lodash__default["default"].isFinite(firstStart) && lodash__default["default"].isFinite(lastEnd)) {
|
|
186
|
+
botMsg.sourceData[0].silenceDuration = lodash__default["default"].isFinite(prevEnd) ? firstStart - prevEnd : firstStart;
|
|
187
|
+
botMsg.sourceData[0].voiceDuration = lastEnd - firstStart;
|
|
148
188
|
} else {
|
|
149
|
-
botMsg.sourceData[0].silenceDuration =
|
|
150
|
-
botMsg.sourceData[0].voiceDuration =
|
|
189
|
+
botMsg.sourceData[0].silenceDuration = lodash__default["default"].isFinite(firstStart) ? firstStart : null;
|
|
190
|
+
botMsg.sourceData[0].voiceDuration = lodash__default["default"].isFinite(firstStart) && lodash__default["default"].isFinite(lastEnd) ? lastEnd - firstStart : null;
|
|
151
191
|
}
|
|
152
192
|
return botMsg;
|
|
153
193
|
};
|
|
@@ -176,12 +216,14 @@ class BotiumConnectorVoip {
|
|
|
176
216
|
const connect = async retryIndex => {
|
|
177
217
|
retryIndex = retryIndex || 0;
|
|
178
218
|
try {
|
|
219
|
+
const workerUrl = this.caps[Capabilities.VOIP_USE_GLOBAL_VOIP_WORKER] ? process.env.BOTIUM_VOIP_WORKER_URL : this.caps[Capabilities.VOIP_WORKER_URL].replace('wss', 'https').replace('ws', 'http');
|
|
220
|
+
const baseUrl = workerUrl.endsWith('/') ? workerUrl.slice(0, -1) : workerUrl;
|
|
179
221
|
const res = await axios__default["default"]({
|
|
180
222
|
method: 'post',
|
|
181
223
|
data: {
|
|
182
224
|
API_KEY: this.caps[Capabilities.VOIP_USE_GLOBAL_VOIP_WORKER] ? process.env.BOTIUM_VOIP_WORKER_APIKEY : this.caps[Capabilities.VOIP_WORKER_APIKEY]
|
|
183
225
|
},
|
|
184
|
-
url:
|
|
226
|
+
url: `${baseUrl}/initCall`
|
|
185
227
|
});
|
|
186
228
|
if (res) {
|
|
187
229
|
data = res.data;
|
|
@@ -235,6 +277,35 @@ class BotiumConnectorVoip {
|
|
|
235
277
|
}
|
|
236
278
|
this.eventEmitter.on('CONVO_STEP_NEXT', (container, convoStep) => {
|
|
237
279
|
this.convoStep = convoStep;
|
|
280
|
+
this._maybePrefetchTts(convoStep);
|
|
281
|
+
// For PSST: send join silence duration per step to VOIP worker (controls PSST silence trigger)
|
|
282
|
+
try {
|
|
283
|
+
if (this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING] === 'PSST' && this.ws && this.ws.readyState === ws__default["default"].OPEN) {
|
|
284
|
+
const joinLogicHook = this._getJoinLogicHook(convoStep);
|
|
285
|
+
let silenceMs = null;
|
|
286
|
+
if (joinLogicHook && joinLogicHook.args && joinLogicHook.args.length > 0) {
|
|
287
|
+
silenceMs = parseInt(joinLogicHook.args[0], 10);
|
|
288
|
+
}
|
|
289
|
+
// Fallback to global timeout if no per-step hook is set
|
|
290
|
+
if (!lodash__default["default"].isFinite(silenceMs) || silenceMs <= 0) {
|
|
291
|
+
silenceMs = parseInt(this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING_TIMEOUT], 10);
|
|
292
|
+
}
|
|
293
|
+
// PSST: treat like JOIN, but subtract 500ms from timeout
|
|
294
|
+
if (lodash__default["default"].isFinite(silenceMs) && silenceMs > 0) {
|
|
295
|
+
silenceMs = Math.max(0, silenceMs - 500);
|
|
296
|
+
}
|
|
297
|
+
if (lodash__default["default"].isFinite(silenceMs) && silenceMs > 0 && this.sessionId) {
|
|
298
|
+
debug$3(`PSST: sending silenceDurationMs=${silenceMs} for sessionId=${this.sessionId}`);
|
|
299
|
+
this.ws.send(JSON.stringify({
|
|
300
|
+
METHOD: 'setSttSilenceDuration',
|
|
301
|
+
sessionId: this.sessionId,
|
|
302
|
+
silenceDurationMs: silenceMs
|
|
303
|
+
}));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
} catch (err) {
|
|
307
|
+
debug$3(`Failed sending PSST silence duration to VOIP worker: ${err.message || err}`);
|
|
308
|
+
}
|
|
238
309
|
});
|
|
239
310
|
this.silence = null;
|
|
240
311
|
this.msgCount = 0;
|
|
@@ -243,6 +314,18 @@ class BotiumConnectorVoip {
|
|
|
243
314
|
this.silenceTimeout = null;
|
|
244
315
|
this.wsOpened = true;
|
|
245
316
|
debug$3(`Websocket connection to ${wsEndpoint} opened.`);
|
|
317
|
+
const sttHandling = this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING];
|
|
318
|
+
const isPsst = sttHandling === 'PSST';
|
|
319
|
+
const sttLegacy = true;
|
|
320
|
+
let sttUrl = this.caps[Capabilities.VOIP_STT_URL_STREAM];
|
|
321
|
+
if (isPsst && lodash__default["default"].isString(sttUrl)) {
|
|
322
|
+
const replaced = sttUrl.replace('/api/sttstream/', '/api/stt/');
|
|
323
|
+
if (replaced !== sttUrl) {
|
|
324
|
+
sttUrl = replaced;
|
|
325
|
+
} else {
|
|
326
|
+
debug$3(`PSST: Could not derive /api/stt url from ${sttUrl}, using as-is`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
246
329
|
const request = {
|
|
247
330
|
METHOD: 'initCall',
|
|
248
331
|
SIP_CALLER_AUTO: this.caps[Capabilities.VOIP_SIP_POOL_CALLER_ENABLE],
|
|
@@ -263,8 +346,9 @@ class BotiumConnectorVoip {
|
|
|
263
346
|
ICE_TURN_PASSWORD: this.caps[Capabilities.VOIP_ICE_TURN_PASSWORD],
|
|
264
347
|
ICE_TURN_PROTOCOL: this.caps[Capabilities.VOIP_ICE_TURN_PROTOCOL] || 'TCP',
|
|
265
348
|
MIN_SILENCE_DURATION: this.caps[Capabilities.VOIP_SILENCE_DURATION_TIMEOUT_ENABLE] ? this.caps[Capabilities.VOIP_SILENCE_DURATION_TIMEOUT] : null,
|
|
349
|
+
STT_LEGACY: sttLegacy,
|
|
266
350
|
STT_CONFIG: {
|
|
267
|
-
stt_url:
|
|
351
|
+
stt_url: sttUrl,
|
|
268
352
|
stt_params: this.caps[Capabilities.VOIP_STT_PARAMS_STREAM],
|
|
269
353
|
stt_body: this.caps[Capabilities.VOIP_STT_BODY_STREAM] || null
|
|
270
354
|
},
|
|
@@ -285,9 +369,88 @@ class BotiumConnectorVoip {
|
|
|
285
369
|
});
|
|
286
370
|
this.ws.on('message', async data => {
|
|
287
371
|
const parsedData = JSON.parse(data);
|
|
372
|
+
// Allow fullRecord delivery even if Stop() was already called.
|
|
373
|
+
// Otherwise the recording can be dropped if the worker streams it late (common on hangup).
|
|
374
|
+
if (this.stopCalled) {
|
|
375
|
+
const allowedTypes = ['fullRecord', 'fullRecordStart', 'fullRecordChunk', 'fullRecordEnd', 'error'];
|
|
376
|
+
if (!parsedData || !parsedData.type || !allowedTypes.includes(parsedData.type)) {
|
|
377
|
+
debug$3(`${this.sessionId} - Stop already called, ignoring incoming message`);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
288
381
|
const parsedDataLog = lodash__default["default"].cloneDeep(parsedData);
|
|
289
|
-
|
|
382
|
+
// Sanitize all potential base64 fields for logging (don't dump huge buffers)
|
|
383
|
+
const sanitizeBase64Fields = (obj, prefix = '') => {
|
|
384
|
+
if (!obj || typeof obj !== 'object') return;
|
|
385
|
+
for (const key of Object.keys(obj)) {
|
|
386
|
+
const val = obj[key];
|
|
387
|
+
if (typeof val === 'string' && val.length > 500) {
|
|
388
|
+
obj[key] = `<base64:${val.length}chars>`;
|
|
389
|
+
} else if (val && typeof val === 'object' && !Array.isArray(val)) {
|
|
390
|
+
sanitizeBase64Fields(val, `${prefix}${key}.`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
sanitizeBase64Fields(parsedDataLog);
|
|
290
395
|
debug$3(JSON.stringify(parsedDataLog, null, 2));
|
|
396
|
+
const _extractFullRecordBase64 = pd => {
|
|
397
|
+
if (!pd) return null;
|
|
398
|
+
// Different VOIP workers may put the payload in various fields - search all string fields
|
|
399
|
+
// First try known field names
|
|
400
|
+
const knownFields = [pd.fullRecord, pd.full_record, pd.data?.fullRecord, pd.data?.full_record, pd.data?.b64_buffer, pd.data?.base64, pd.data?.full_record_buffer, pd.data?.buffer, pd.buffer, pd.base64, pd.audio, pd.audioData, pd.data?.audio, pd.data?.audioData];
|
|
401
|
+
for (const val of knownFields) {
|
|
402
|
+
if (typeof val === 'string' && val.length > 100) return val;
|
|
403
|
+
}
|
|
404
|
+
// Fallback: find any large string field (likely base64)
|
|
405
|
+
const findLargeString = (obj, depth = 0) => {
|
|
406
|
+
if (!obj || typeof obj !== 'object' || depth > 3) return null;
|
|
407
|
+
for (const key of Object.keys(obj)) {
|
|
408
|
+
const val = obj[key];
|
|
409
|
+
if (typeof val === 'string' && val.length > 1000) {
|
|
410
|
+
debug$3(`${this.sessionId} - Found base64 in field '${key}' (len=${val.length})`);
|
|
411
|
+
return val;
|
|
412
|
+
}
|
|
413
|
+
if (val && typeof val === 'object' && !Array.isArray(val)) {
|
|
414
|
+
const found = findLargeString(val, depth + 1);
|
|
415
|
+
if (found) return found;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return null;
|
|
419
|
+
};
|
|
420
|
+
return findLargeString(pd);
|
|
421
|
+
};
|
|
422
|
+
const emitFullRecordAttachment = base64 => {
|
|
423
|
+
if (!base64 || typeof base64 !== 'string' || base64.length === 0) {
|
|
424
|
+
// Log available fields (length only) to help diagnose worker payload shape without dumping base64
|
|
425
|
+
try {
|
|
426
|
+
const candidates = {
|
|
427
|
+
fullRecord: typeof parsedData?.fullRecord === 'string' ? parsedData.fullRecord.length : null,
|
|
428
|
+
full_record: typeof parsedData?.full_record === 'string' ? parsedData.full_record.length : null,
|
|
429
|
+
data_fullRecord: typeof parsedData?.data?.fullRecord === 'string' ? parsedData.data.fullRecord.length : null,
|
|
430
|
+
data_full_record: typeof parsedData?.data?.full_record === 'string' ? parsedData.data.full_record.length : null,
|
|
431
|
+
data_b64_buffer: typeof parsedData?.data?.b64_buffer === 'string' ? parsedData.data.b64_buffer.length : null,
|
|
432
|
+
data_base64: typeof parsedData?.data?.base64 === 'string' ? parsedData.data.base64.length : null,
|
|
433
|
+
data_full_record_buffer: typeof parsedData?.data?.full_record_buffer === 'string' ? parsedData.data.full_record_buffer.length : null,
|
|
434
|
+
data_buffer: typeof parsedData?.data?.buffer === 'string' ? parsedData.data.buffer.length : null
|
|
435
|
+
};
|
|
436
|
+
debug$3(`${this.sessionId} - fullRecordEnd received but base64 is empty, skipping attachment emit. CandidateLengths=${JSON.stringify(candidates)}`);
|
|
437
|
+
} catch (err) {
|
|
438
|
+
debug$3(`${this.sessionId} - fullRecordEnd received but base64 is empty, skipping attachment emit (diag failed: ${err.message || err})`);
|
|
439
|
+
}
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
debug$3(`${this.sessionId} - Emitting MESSAGE_ATTACHMENT full_record.wav (base64Len=${base64.length}, stopCalled=${!!this.stopCalled})`);
|
|
443
|
+
this.eventEmitter.emit('MESSAGE_ATTACHMENT', this.container, {
|
|
444
|
+
name: 'full_record.wav',
|
|
445
|
+
mimeType: 'audio/wav',
|
|
446
|
+
base64,
|
|
447
|
+
// Session context from capabilities for correlation
|
|
448
|
+
sessionContext: {
|
|
449
|
+
testSessionId: this.caps.VOIP_TEST_SESSION_ID || null,
|
|
450
|
+
testSessionJobId: this.caps.VOIP_TEST_SESSION_JOB_ID || null
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
};
|
|
291
454
|
if (parsedData && parsedData.type === 'callinfo' && parsedData.status === 'initialized') {
|
|
292
455
|
this.sessionId = parsedData.voipConfig.sessionId;
|
|
293
456
|
}
|
|
@@ -323,6 +486,22 @@ class BotiumConnectorVoip {
|
|
|
323
486
|
reject(new Error(`Error: ${parsedData.message}`));
|
|
324
487
|
sendBotMsg(new Error(`Error: ${parsedData.message}`));
|
|
325
488
|
}
|
|
489
|
+
|
|
490
|
+
// Full record streaming support:
|
|
491
|
+
// - some VOIP workers send the recording in chunks and an end marker
|
|
492
|
+
if (parsedData && parsedData.type === 'fullRecordStart') {
|
|
493
|
+
this.fullRecord = '';
|
|
494
|
+
}
|
|
495
|
+
if (parsedData && parsedData.type === 'fullRecordChunk') {
|
|
496
|
+
const chunk = _extractFullRecordBase64(parsedData);
|
|
497
|
+
if (chunk) this.fullRecord = (this.fullRecord || '') + chunk;
|
|
498
|
+
}
|
|
499
|
+
if (parsedData && parsedData.type === 'fullRecordEnd') {
|
|
500
|
+
const tail = _extractFullRecordBase64(parsedData);
|
|
501
|
+
if (tail) this.fullRecord = (this.fullRecord || '') + tail;
|
|
502
|
+
emitFullRecordAttachment(this.fullRecord || tail);
|
|
503
|
+
this.end = true;
|
|
504
|
+
}
|
|
326
505
|
if (parsedData && parsedData.type === 'silence') {
|
|
327
506
|
if (lodash__default["default"].isNil(this._getIgnoreSilenceDurationAsserterLogicHook(this.convoStep))) {
|
|
328
507
|
if (lodash__default["default"].isNil(this._getJoinLogicHook(this.convoStep)) && parsedData.data.silence.length > 0) {
|
|
@@ -332,11 +511,8 @@ class BotiumConnectorVoip {
|
|
|
332
511
|
}
|
|
333
512
|
}
|
|
334
513
|
if (parsedData && parsedData.type === 'fullRecord') {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
mimeType: 'audio/wav',
|
|
338
|
-
base64: parsedData.fullRecord
|
|
339
|
-
});
|
|
514
|
+
// Non-chunked full record
|
|
515
|
+
emitFullRecordAttachment(_extractFullRecordBase64(parsedData));
|
|
340
516
|
this.end = true;
|
|
341
517
|
}
|
|
342
518
|
if (parsedData && parsedData.data && parsedData.data.final === false) {
|
|
@@ -350,16 +526,29 @@ class BotiumConnectorVoip {
|
|
|
350
526
|
}
|
|
351
527
|
}
|
|
352
528
|
this.firstSttInfoReceived = true;
|
|
353
|
-
if (this.prevData) {
|
|
529
|
+
if (this.prevData && this.prevData.data && !lodash__default["default"].isNil(this.prevData.data.end)) {
|
|
354
530
|
if (!lodash__default["default"].isNil(parsedData.data.start)) {
|
|
355
531
|
const silenceDuration = parsedData.data.start - this.prevData.data.end;
|
|
356
532
|
const joinLogicHook = this._getJoinLogicHook(this.convoStep);
|
|
357
|
-
const
|
|
533
|
+
const sttHandling = this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING];
|
|
534
|
+
const isPsst = sttHandling === 'PSST';
|
|
535
|
+
const isJoinMethod = sttHandling === 'JOIN' || isPsst;
|
|
536
|
+
const toJoinTimeoutMs = ms => {
|
|
537
|
+
const parsed = parseInt(ms, 10);
|
|
538
|
+
if (!lodash__default["default"].isFinite(parsed) || parsed <= 0) return null;
|
|
539
|
+
return isPsst ? Math.max(0, parsed - 500) : parsed;
|
|
540
|
+
};
|
|
358
541
|
let matched = false;
|
|
359
|
-
if (
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
542
|
+
if (!lodash__default["default"].isNil(joinLogicHook) && isJoinMethod) {
|
|
543
|
+
const joinTimeoutMs = toJoinTimeoutMs(joinLogicHook && joinLogicHook.args && joinLogicHook.args[0]);
|
|
544
|
+
if (lodash__default["default"].isFinite(joinTimeoutMs) && joinTimeoutMs > 0 && silenceDuration > joinTimeoutMs / 1000) {
|
|
545
|
+
matched = true;
|
|
546
|
+
}
|
|
547
|
+
} else if (lodash__default["default"].isNil(joinLogicHook) && isJoinMethod) {
|
|
548
|
+
const joinTimeoutMs = toJoinTimeoutMs(this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING_TIMEOUT]);
|
|
549
|
+
if (lodash__default["default"].isFinite(joinTimeoutMs) && joinTimeoutMs > 0 && silenceDuration > joinTimeoutMs / 1000) {
|
|
550
|
+
matched = true;
|
|
551
|
+
}
|
|
363
552
|
}
|
|
364
553
|
if (matched && this.botMsgs.length > 0) {
|
|
365
554
|
sendBotMsg(joinBotMsg(this.botMsgs, this.joinLastPrevMsg));
|
|
@@ -374,6 +563,7 @@ class BotiumConnectorVoip {
|
|
|
374
563
|
const confidenceThreshold = this._getConfidenceScoreLogicHook(this.convoStep) && this._getConfidenceScoreLogicHook(this.convoStep).args[0] || this.caps[Capabilities.VOIP_STT_CONFIDENCE_THRESHOLD];
|
|
375
564
|
const successfulConfidenceScore = this._getConfidenceScore(parsedData) >= confidenceThreshold;
|
|
376
565
|
debug$3(`Message: ${parsedData.data.message} / Confidence Score: ${this._getConfidenceScore(parsedData)} (Threshold: ${confidenceThreshold})`);
|
|
566
|
+
// ORIGINAL: emit final message immediately, unless join hooks are active.
|
|
377
567
|
if (this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING] === 'ORIGINAL' && lodash__default["default"].isNil(this._getJoinLogicHook(this.convoStep))) {
|
|
378
568
|
let botMsg = {
|
|
379
569
|
messageText: parsedData.data.message
|
|
@@ -388,7 +578,9 @@ class BotiumConnectorVoip {
|
|
|
388
578
|
this.firstMsg = false;
|
|
389
579
|
} else {
|
|
390
580
|
const sourceData = parsedData;
|
|
391
|
-
|
|
581
|
+
const start = lodash__default["default"].get(parsedData, 'data.start', null);
|
|
582
|
+
const prevEnd = lodash__default["default"].get(this.prevData, 'data.end', null);
|
|
583
|
+
sourceData.silenceDuration = lodash__default["default"].isFinite(start) && lodash__default["default"].isFinite(prevEnd) ? start - prevEnd : lodash__default["default"].isFinite(start) ? start : null;
|
|
392
584
|
sourceData.voiceDuration = parsedData.data.end - parsedData.data.start;
|
|
393
585
|
botMsg = Object.assign({}, botMsg, {
|
|
394
586
|
sourceData
|
|
@@ -415,7 +607,9 @@ class BotiumConnectorVoip {
|
|
|
415
607
|
this.firstMsg = false;
|
|
416
608
|
} else {
|
|
417
609
|
const sourceData = parsedData;
|
|
418
|
-
|
|
610
|
+
const start = lodash__default["default"].get(parsedData, 'data.start', null);
|
|
611
|
+
const prevEnd = lodash__default["default"].get(this.prevData, 'data.end', null);
|
|
612
|
+
sourceData.silenceDuration = lodash__default["default"].isFinite(start) && lodash__default["default"].isFinite(prevEnd) ? start - prevEnd : lodash__default["default"].isFinite(start) ? start : null;
|
|
419
613
|
sourceData.voiceDuration = parsedData.data.end - parsedData.data.start;
|
|
420
614
|
botMsg = Object.assign({}, botMsg, {
|
|
421
615
|
sourceData
|
|
@@ -429,7 +623,7 @@ class BotiumConnectorVoip {
|
|
|
429
623
|
}
|
|
430
624
|
this.botMsgs = [];
|
|
431
625
|
}
|
|
432
|
-
if (this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING] === 'JOIN' || this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING] === 'CONCAT' || !lodash__default["default"].isNil(this._getJoinLogicHook(this.convoStep))) {
|
|
626
|
+
if (this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING] === 'JOIN' || this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING] === 'PSST' || this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING] === 'CONCAT' || !lodash__default["default"].isNil(this._getJoinLogicHook(this.convoStep))) {
|
|
433
627
|
const botMsg = {
|
|
434
628
|
messageText: parsedData.data.message,
|
|
435
629
|
sourceData: parsedData
|
|
@@ -437,15 +631,27 @@ class BotiumConnectorVoip {
|
|
|
437
631
|
this.prevData = parsedData;
|
|
438
632
|
if (successfulConfidenceScore) {
|
|
439
633
|
this.botMsgs.push(botMsg);
|
|
634
|
+
const sttHandling = this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING];
|
|
635
|
+
const isPsst = sttHandling === 'PSST';
|
|
636
|
+
const toJoinTimeoutMs = ms => {
|
|
637
|
+
const parsed = parseInt(ms, 10);
|
|
638
|
+
if (!lodash__default["default"].isFinite(parsed) || parsed <= 0) return null;
|
|
639
|
+
return isPsst ? Math.max(0, parsed - 500) : parsed;
|
|
640
|
+
};
|
|
641
|
+
const joinLogicHook = this._getJoinLogicHook(this.convoStep);
|
|
642
|
+
let joinTimeoutMs = toJoinTimeoutMs(lodash__default["default"].get(joinLogicHook, 'args[0]'));
|
|
643
|
+
if (!lodash__default["default"].isFinite(joinTimeoutMs) || joinTimeoutMs <= 0) {
|
|
644
|
+
joinTimeoutMs = toJoinTimeoutMs(this.caps[Capabilities.VOIP_STT_MESSAGE_HANDLING_TIMEOUT]);
|
|
645
|
+
}
|
|
440
646
|
this.silenceTimeout = setTimeout(() => {
|
|
441
647
|
if (this.botMsgs.length > 0) {
|
|
442
|
-
debug$3('Silence Duration Timeout (PSST):',
|
|
648
|
+
debug$3('Silence Duration Timeout (JOIN/PSST):', joinTimeoutMs, 'ms');
|
|
443
649
|
sendBotMsg(joinBotMsg(this.botMsgs, this.joinLastPrevMsg));
|
|
444
650
|
this.firstMsg = false;
|
|
445
651
|
this.joinLastPrevMsg = this.botMsgs[this.botMsgs.length - 1];
|
|
446
652
|
this.botMsgs = [];
|
|
447
653
|
}
|
|
448
|
-
},
|
|
654
|
+
}, joinTimeoutMs || 0);
|
|
449
655
|
}
|
|
450
656
|
}
|
|
451
657
|
}
|
|
@@ -457,7 +663,24 @@ class BotiumConnectorVoip {
|
|
|
457
663
|
}
|
|
458
664
|
async UserSays(msg) {
|
|
459
665
|
debug$3('UserSays called');
|
|
460
|
-
|
|
666
|
+
// Avoid logging large buffers/base64 (can break job logs and overwhelm stdout)
|
|
667
|
+
try {
|
|
668
|
+
const safeLog = {
|
|
669
|
+
messageText: msg && msg.messageText ? String(msg.messageText).substring(0, 200) : undefined,
|
|
670
|
+
buttons: msg && Array.isArray(msg.buttons) ? msg.buttons.length : 0,
|
|
671
|
+
hasMedia: !!(msg && msg.media && msg.media.length > 0),
|
|
672
|
+
mediaMimeType: msg && msg.media && msg.media[0] ? msg.media[0].mimeType : undefined,
|
|
673
|
+
mediaSize: msg && msg.media && msg.media[0] && Buffer.isBuffer(msg.media[0].buffer) ? msg.media[0].buffer.length : undefined,
|
|
674
|
+
attachments: msg && Array.isArray(msg.attachments) ? msg.attachments.map(a => ({
|
|
675
|
+
name: a && (a.name || a.title),
|
|
676
|
+
mimeType: a && a.mimeType,
|
|
677
|
+
base64Len: a && a.base64 ? a.base64.length : 0
|
|
678
|
+
})) : []
|
|
679
|
+
};
|
|
680
|
+
debug$3(safeLog);
|
|
681
|
+
} catch (err) {
|
|
682
|
+
debug$3(`UserSays: failed to build safe log: ${err.message || err}`);
|
|
683
|
+
}
|
|
461
684
|
if (!msg.attachments) {
|
|
462
685
|
msg.attachments = [];
|
|
463
686
|
}
|
|
@@ -472,23 +695,32 @@ class BotiumConnectorVoip {
|
|
|
472
695
|
});
|
|
473
696
|
this.ws.send(request);
|
|
474
697
|
} else if (msg && msg.messageText) {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
698
|
+
// Check for DTMF tag in messageText: <DTMF>1234</DTMF>
|
|
699
|
+
const dtmfMatch = msg.messageText.match(/<DTMF>([^<]+)<\/DTMF>/i);
|
|
700
|
+
if (dtmfMatch && dtmfMatch[1]) {
|
|
701
|
+
const digits = dtmfMatch[1];
|
|
702
|
+
debug$3(`Sending DTMF from messageText: ${digits}`);
|
|
703
|
+
const request = JSON.stringify({
|
|
704
|
+
METHOD: 'sendDtmf',
|
|
705
|
+
digits,
|
|
706
|
+
sessionId: this.sessionId
|
|
707
|
+
});
|
|
708
|
+
this.ws.send(request);
|
|
709
|
+
return resolve();
|
|
710
|
+
}
|
|
711
|
+
if (!this.axiosTtsParams) {
|
|
712
|
+
if (!(msg.media && msg.media.length > 0 && msg.media[0].buffer)) {
|
|
713
|
+
return reject(new Error('TTS not configured, only audio input supported'));
|
|
714
|
+
}
|
|
715
|
+
} else {
|
|
716
|
+
const ttsRequest = this._buildTtsRequest(msg.messageText);
|
|
717
|
+
if (!ttsRequest) return reject(new Error('TTS not configured, only audio input supported'));
|
|
486
718
|
msg.sourceData = ttsRequest;
|
|
487
|
-
let
|
|
719
|
+
let ttsResult = null;
|
|
488
720
|
try {
|
|
489
|
-
|
|
721
|
+
ttsResult = await this._getTtsAudio(ttsRequest, msg.messageText);
|
|
490
722
|
} catch (err) {
|
|
491
|
-
reject(new Error(`TTS "${msg.messageText}" failed - ${this._getAxiosErrOutput(err)}`));
|
|
723
|
+
return reject(new Error(`TTS "${msg.messageText}" failed - ${this._getAxiosErrOutput(err)}`));
|
|
492
724
|
}
|
|
493
725
|
if (msg && msg.messageText && msg.messageText.length > 0) {
|
|
494
726
|
const apiKey = this._extractApiKey(this._getBody(Capabilities.VOIP_TTS_BODY));
|
|
@@ -499,22 +731,28 @@ class BotiumConnectorVoip {
|
|
|
499
731
|
apiKey
|
|
500
732
|
});
|
|
501
733
|
}
|
|
502
|
-
if (Buffer.isBuffer(
|
|
503
|
-
duration =
|
|
734
|
+
if (ttsResult && Buffer.isBuffer(ttsResult.buffer)) {
|
|
735
|
+
duration = parseFloat(ttsResult.duration) || 0;
|
|
736
|
+
const b64Buffer = ttsResult.buffer.toString('base64');
|
|
504
737
|
const request = JSON.stringify({
|
|
505
738
|
METHOD: 'sendAudio',
|
|
506
739
|
PESQ: false,
|
|
507
740
|
sessionId: this.sessionId,
|
|
508
|
-
b64_buffer:
|
|
741
|
+
b64_buffer: b64Buffer
|
|
509
742
|
});
|
|
743
|
+
if (this._lastBotSaysQueuedAt) {
|
|
744
|
+
const latencyMs = Date.now() - this._lastBotSaysQueuedAt;
|
|
745
|
+
const botText = this._lastBotSaysText ? this._lastBotSaysText.substring(0, 120) : '<unknown>';
|
|
746
|
+
debug$3(`Latency (bot says -> sendAudio/TTS): ${latencyMs} ms (last bot: ${botText})`);
|
|
747
|
+
}
|
|
510
748
|
msg.attachments.push({
|
|
511
749
|
name: 'tts.wav',
|
|
512
750
|
mimeType: 'audio/wav',
|
|
513
|
-
base64:
|
|
751
|
+
base64: b64Buffer
|
|
514
752
|
});
|
|
515
753
|
this.ws.send(request);
|
|
516
754
|
} else {
|
|
517
|
-
reject(new Error(
|
|
755
|
+
return reject(new Error('TTS failed, response is empty'));
|
|
518
756
|
}
|
|
519
757
|
}
|
|
520
758
|
}
|
|
@@ -524,6 +762,11 @@ class BotiumConnectorVoip {
|
|
|
524
762
|
sessionId: this.sessionId,
|
|
525
763
|
b64_buffer: msg.media[0].buffer.toString('base64')
|
|
526
764
|
});
|
|
765
|
+
if (this._lastBotSaysQueuedAt) {
|
|
766
|
+
const latencyMs = Date.now() - this._lastBotSaysQueuedAt;
|
|
767
|
+
const botText = this._lastBotSaysText ? this._lastBotSaysText.substring(0, 120) : '<unknown>';
|
|
768
|
+
debug$3(`Latency (bot says -> sendAudio/MEDIA): ${latencyMs} ms (last bot: ${botText})`);
|
|
769
|
+
}
|
|
527
770
|
this.ws.send(request);
|
|
528
771
|
msg.attachments.push({
|
|
529
772
|
name: msg.media[0].mediaUri,
|
|
@@ -548,6 +791,11 @@ class BotiumConnectorVoip {
|
|
|
548
791
|
}
|
|
549
792
|
async Stop() {
|
|
550
793
|
debug$3(`${this.sessionId} - Stop called`);
|
|
794
|
+
this.stopCalled = true;
|
|
795
|
+
if (this.silenceTimeout) {
|
|
796
|
+
clearTimeout(this.silenceTimeout);
|
|
797
|
+
this.silenceTimeout = null;
|
|
798
|
+
}
|
|
551
799
|
if (this.ws && this.ws.readyState !== ws__default["default"].CLOSED) {
|
|
552
800
|
const request = JSON.stringify({
|
|
553
801
|
METHOD: 'stopCall',
|
|
@@ -555,9 +803,12 @@ class BotiumConnectorVoip {
|
|
|
555
803
|
});
|
|
556
804
|
this.ws.send(request);
|
|
557
805
|
await new Promise(resolve => {
|
|
558
|
-
setTimeout(resolve, 100000);
|
|
559
|
-
setInterval(() => {
|
|
806
|
+
const stopTimeout = setTimeout(resolve, 100000);
|
|
807
|
+
this._stopWaitInterval = setInterval(() => {
|
|
560
808
|
if (this.end) {
|
|
809
|
+
clearTimeout(stopTimeout);
|
|
810
|
+
clearInterval(this._stopWaitInterval);
|
|
811
|
+
this._stopWaitInterval = null;
|
|
561
812
|
this.wsOpened = false;
|
|
562
813
|
this.ws = null;
|
|
563
814
|
this.end = false;
|
|
@@ -575,6 +826,126 @@ class BotiumConnectorVoip {
|
|
|
575
826
|
this.firstSttInfoReceived = false;
|
|
576
827
|
}
|
|
577
828
|
}
|
|
829
|
+
_isTtsCacheEnabled() {
|
|
830
|
+
return this.ttsCacheEnabled && this.ttsCacheMaxEntries > 0;
|
|
831
|
+
}
|
|
832
|
+
_touchTtsCache(cacheKey, entry) {
|
|
833
|
+
if (!this._isTtsCacheEnabled()) return;
|
|
834
|
+
this.ttsCache.delete(cacheKey);
|
|
835
|
+
this.ttsCache.set(cacheKey, entry);
|
|
836
|
+
}
|
|
837
|
+
_setTtsCacheEntry(cacheKey, entry) {
|
|
838
|
+
if (!this._isTtsCacheEnabled()) return;
|
|
839
|
+
this.ttsCache.set(cacheKey, entry);
|
|
840
|
+
this._trimTtsCache();
|
|
841
|
+
}
|
|
842
|
+
_deleteTtsCacheEntry(cacheKey) {
|
|
843
|
+
if (this.ttsCache) {
|
|
844
|
+
this.ttsCache.delete(cacheKey);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
_trimTtsCache() {
|
|
848
|
+
if (!this._isTtsCacheEnabled()) return;
|
|
849
|
+
while (this.ttsCache.size > this.ttsCacheMaxEntries) {
|
|
850
|
+
const oldestKey = this.ttsCache.keys().next().value;
|
|
851
|
+
this.ttsCache.delete(oldestKey);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
_shouldPrefetchTtsText(text) {
|
|
855
|
+
if (!lodash__default["default"].isString(text)) return false;
|
|
856
|
+
if (!text.trim()) return false;
|
|
857
|
+
return !/\$[A-Za-z]\w*/.test(text);
|
|
858
|
+
}
|
|
859
|
+
_maybePrefetchTts(convoStep) {
|
|
860
|
+
if (!this.ttsPrefetchEnabled || !this._isTtsCacheEnabled()) return;
|
|
861
|
+
if (!convoStep || convoStep.sender !== 'me') return;
|
|
862
|
+
if (!this._shouldPrefetchTtsText(convoStep.messageText)) return;
|
|
863
|
+
const ttsRequest = this._buildTtsRequest(convoStep.messageText);
|
|
864
|
+
if (!ttsRequest) return;
|
|
865
|
+
this._getTtsAudio(ttsRequest, convoStep.messageText).catch(err => {
|
|
866
|
+
debug$3(`TTS prefetch failed - ${this._getAxiosErrOutput(err)}`);
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
_buildTtsRequest(text) {
|
|
870
|
+
if (!this.axiosTtsParams) return null;
|
|
871
|
+
return {
|
|
872
|
+
...this.axiosTtsParams,
|
|
873
|
+
params: {
|
|
874
|
+
...(this.axiosTtsParams.params || {}),
|
|
875
|
+
text
|
|
876
|
+
},
|
|
877
|
+
data: this._getBody(Capabilities.VOIP_TTS_BODY),
|
|
878
|
+
responseType: 'arraybuffer'
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
_getTtsCacheKey(ttsRequest) {
|
|
882
|
+
const cacheKeyObj = {
|
|
883
|
+
url: ttsRequest.url,
|
|
884
|
+
method: ttsRequest.method,
|
|
885
|
+
params: ttsRequest.params,
|
|
886
|
+
data: ttsRequest.data
|
|
887
|
+
};
|
|
888
|
+
try {
|
|
889
|
+
return JSON.stringify(cacheKeyObj);
|
|
890
|
+
} catch (err) {
|
|
891
|
+
return `${ttsRequest.url}|${ttsRequest.method}|${lodash__default["default"].get(ttsRequest, 'params.text', '')}`;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
async _fetchTts(ttsRequest, text) {
|
|
895
|
+
const ttsStart = Date.now();
|
|
896
|
+
const ttsResponse = await axios__default["default"](ttsRequest);
|
|
897
|
+
const ttsMs = Date.now() - ttsStart;
|
|
898
|
+
const textLen = lodash__default["default"].isString(text) ? text.length : 0;
|
|
899
|
+
debug$3(`TTS response ${ttsMs} ms (chars: ${textLen})`);
|
|
900
|
+
if (!ttsResponse || !Buffer.isBuffer(ttsResponse.data)) {
|
|
901
|
+
throw new Error(`TTS failed, response is: ${this._getAxiosShortenedOutput(ttsResponse && ttsResponse.data)}`);
|
|
902
|
+
}
|
|
903
|
+
const durationHeader = ttsResponse.headers && ttsResponse.headers['content-duration'];
|
|
904
|
+
return {
|
|
905
|
+
buffer: ttsResponse.data,
|
|
906
|
+
duration: durationHeader
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
async _getTtsAudio(ttsRequest, text) {
|
|
910
|
+
if (!ttsRequest) throw new Error('TTS request not configured');
|
|
911
|
+
const cacheKey = this._getTtsCacheKey(ttsRequest);
|
|
912
|
+
if (this._isTtsCacheEnabled()) {
|
|
913
|
+
const cached = this.ttsCache.get(cacheKey);
|
|
914
|
+
if (cached) {
|
|
915
|
+
if (cached.state === 'ready') {
|
|
916
|
+
this._touchTtsCache(cacheKey, cached);
|
|
917
|
+
debug$3(`TTS cache hit (chars: ${lodash__default["default"].isString(text) ? text.length : 0})`);
|
|
918
|
+
return {
|
|
919
|
+
buffer: cached.buffer,
|
|
920
|
+
duration: cached.duration
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
if (cached.state === 'pending') {
|
|
924
|
+
return cached.promise;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
const fetchPromise = this._fetchTts(ttsRequest, text).then(result => {
|
|
929
|
+
if (this._isTtsCacheEnabled()) {
|
|
930
|
+
this._setTtsCacheEntry(cacheKey, {
|
|
931
|
+
state: 'ready',
|
|
932
|
+
buffer: result.buffer,
|
|
933
|
+
duration: result.duration
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
return result;
|
|
937
|
+
}).catch(err => {
|
|
938
|
+
this._deleteTtsCacheEntry(cacheKey);
|
|
939
|
+
throw err;
|
|
940
|
+
});
|
|
941
|
+
if (this._isTtsCacheEnabled()) {
|
|
942
|
+
this._setTtsCacheEntry(cacheKey, {
|
|
943
|
+
state: 'pending',
|
|
944
|
+
promise: fetchPromise
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
return fetchPromise;
|
|
948
|
+
}
|
|
578
949
|
_getParams(capParams) {
|
|
579
950
|
if (this.caps[capParams]) {
|
|
580
951
|
if (lodash__default["default"].isString(this.caps[capParams])) return JSON.parse(this.caps[capParams]);else return this.caps[capParams];
|