@fonoster/voice 0.27.0 → 0.31.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 +32 -419
- package/dist/Session.d.ts +161 -0
- package/dist/Session.js +576 -0
- package/dist/VoiceServer.d.ts +70 -5
- package/dist/VoiceServer.js +149 -43
- package/dist/{verbs/Dial.d.ts → errors.d.ts} +9 -13
- package/dist/{verbs/Answer.js → errors.js} +18 -7
- package/dist/index.d.ts +4 -6
- package/dist/index.js +4 -15
- package/dist/signature.d.ts +31 -0
- package/dist/signature.js +93 -0
- package/dist/types.d.ts +88 -10
- package/dist/types.js +0 -21
- package/package.json +13 -19
- package/session.d.ts +1 -0
- package/session.js +2 -0
- package/dist/VoiceResponse.d.ts +0 -304
- package/dist/VoiceResponse.js +0 -461
- package/dist/createSession.d.ts +0 -22
- package/dist/createSession.js +0 -66
- package/dist/defaultServerConfig.d.ts +0 -21
- package/dist/defaultServerConfig.js +0 -10
- package/dist/demo.d.ts +0 -1
- package/dist/demo.js +0 -83
- package/dist/filters.d.ts +0 -44
- package/dist/filters.js +0 -17
- package/dist/serviceDefinition.d.ts +0 -2
- package/dist/serviceDefinition.js +0 -29
- package/dist/verbs/Answer.d.ts +0 -24
- package/dist/verbs/Dial.js +0 -60
- package/dist/verbs/Gather.d.ts +0 -25
- package/dist/verbs/Gather.js +0 -57
- package/dist/verbs/Hangup.d.ts +0 -24
- package/dist/verbs/Hangup.js +0 -29
- package/dist/verbs/Mute.d.ts +0 -25
- package/dist/verbs/Mute.js +0 -36
- package/dist/verbs/Play.d.ts +0 -25
- package/dist/verbs/Play.js +0 -33
- package/dist/verbs/PlayDtmf.d.ts +0 -25
- package/dist/verbs/PlayDtmf.js +0 -32
- package/dist/verbs/PlaybackControl.d.ts +0 -25
- package/dist/verbs/PlaybackControl.js +0 -35
- package/dist/verbs/Record.d.ts +0 -25
- package/dist/verbs/Record.js +0 -49
- package/dist/verbs/Say.d.ts +0 -25
- package/dist/verbs/Say.js +0 -33
- package/dist/verbs/SetAudioFilters.d.ts +0 -25
- package/dist/verbs/SetAudioFilters.js +0 -16
- package/dist/verbs/StopSay.d.ts +0 -25
- package/dist/verbs/StopSay.js +0 -11
- package/dist/verbs/Stream.d.ts +0 -39
- package/dist/verbs/Stream.js +0 -80
- package/dist/verbs/StreamGather.d.ts +0 -37
- package/dist/verbs/StreamGather.js +0 -66
- package/dist/verbs/Unmute.d.ts +0 -22
- package/dist/verbs/Unmute.js +0 -26
- package/dist/verbs/Verb.d.ts +0 -28
- package/dist/verbs/Verb.js +0 -103
- package/dist/verbs/getExpectedContent.d.ts +0 -2
- package/dist/verbs/getExpectedContent.js +0 -32
- package/dist/verbs/index.d.ts +0 -35
- package/dist/verbs/index.js +0 -51
- package/dist/verbs/validateRequest.d.ts +0 -20
- package/dist/verbs/validateRequest.js +0 -13
package/dist/Session.js
ADDED
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __asyncValues = (this && this.__asyncValues) || function (o) {
|
|
12
|
+
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
|
|
13
|
+
var m = o[Symbol.asyncIterator], i;
|
|
14
|
+
return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
|
|
15
|
+
function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
|
|
16
|
+
function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
|
|
17
|
+
};
|
|
18
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
19
|
+
var t = {};
|
|
20
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
21
|
+
t[p] = s[p];
|
|
22
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
23
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
24
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
25
|
+
t[p[i]] = s[p[i]];
|
|
26
|
+
}
|
|
27
|
+
return t;
|
|
28
|
+
};
|
|
29
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
30
|
+
exports.Session = void 0;
|
|
31
|
+
const errors_1 = require("./errors");
|
|
32
|
+
const DEFAULT_GATHER_TIMEOUT_MS = 4000;
|
|
33
|
+
/** Commands that make AppBridge start listening (speech recognition and turn events). */
|
|
34
|
+
const SPEECH_COMMANDS = new Set([
|
|
35
|
+
"speak",
|
|
36
|
+
"play",
|
|
37
|
+
"subscribe",
|
|
38
|
+
"listen.update"
|
|
39
|
+
]);
|
|
40
|
+
const MAX_BUFFERED_INPUTS = 32;
|
|
41
|
+
/**
|
|
42
|
+
* One call's session, built on the fonoster.session.v1 messages (voice-sdk S8). Verbs are
|
|
43
|
+
* conveniences: each sends one command and resolves on the event that answers it.
|
|
44
|
+
*/
|
|
45
|
+
class Session {
|
|
46
|
+
constructor(start, transport, signedFor) {
|
|
47
|
+
var _a, _b;
|
|
48
|
+
this.transport = transport;
|
|
49
|
+
this.seq = 0;
|
|
50
|
+
this.outputs = new Map();
|
|
51
|
+
this.commands = new Map();
|
|
52
|
+
this.marks = new Map();
|
|
53
|
+
this.endWaiters = [];
|
|
54
|
+
this.listeners = new Map();
|
|
55
|
+
this.frameListeners = new Set();
|
|
56
|
+
this.buffered = [];
|
|
57
|
+
this.listening = false;
|
|
58
|
+
this.signedFor = signedFor;
|
|
59
|
+
this.sessionRef = start.sessionRef;
|
|
60
|
+
this.callRef = start.callRef;
|
|
61
|
+
this.appRef = start.appRef;
|
|
62
|
+
this.direction = start.direction;
|
|
63
|
+
this.from = start.from;
|
|
64
|
+
this.to = start.to;
|
|
65
|
+
this.metadata = ((_a = start.metadata) !== null && _a !== void 0 ? _a : {});
|
|
66
|
+
this.traceparent = start.traceparent;
|
|
67
|
+
this.handoffPayload = ((_b = start.handoff) !== null && _b !== void 0 ? _b : null);
|
|
68
|
+
this.speech = start.speech;
|
|
69
|
+
this.audioFormat = start.audio;
|
|
70
|
+
this.tracks = start.tracks;
|
|
71
|
+
this.audio = {
|
|
72
|
+
onFrame: (cb) => void this.frameListeners.add(cb),
|
|
73
|
+
send: (frame) => this.transport.sendBinary(frame),
|
|
74
|
+
mark: (name) => new Promise((resolve, reject) => {
|
|
75
|
+
var _a;
|
|
76
|
+
if (this.endedReason)
|
|
77
|
+
return reject(new errors_1.SessionEndedError(this.endedReason));
|
|
78
|
+
const list = (_a = this.marks.get(name)) !== null && _a !== void 0 ? _a : [];
|
|
79
|
+
list.push(resolve);
|
|
80
|
+
this.marks.set(name, list);
|
|
81
|
+
this.send({ type: "mark", name });
|
|
82
|
+
}),
|
|
83
|
+
clear: () => this.send({ type: "clear" })
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
get ended() {
|
|
87
|
+
return this.endedReason !== undefined;
|
|
88
|
+
}
|
|
89
|
+
// ---- speech -----------------------------------------------------------------------
|
|
90
|
+
/** Speaks text, or a stream of text pieces as one utterance, and reports what was heard. */
|
|
91
|
+
say(input_1) {
|
|
92
|
+
return __awaiter(this, arguments, void 0, function* (input, options = {}) {
|
|
93
|
+
var _a, input_2, input_2_1;
|
|
94
|
+
var _b, e_1, _c, _d;
|
|
95
|
+
var _e, _f;
|
|
96
|
+
const id = this.nextId("say");
|
|
97
|
+
const fields = this.compact({
|
|
98
|
+
voice: options.voice,
|
|
99
|
+
language: options.language,
|
|
100
|
+
interruptible: options.interruptible,
|
|
101
|
+
preempt: options.preempt
|
|
102
|
+
});
|
|
103
|
+
if (typeof input === "string") {
|
|
104
|
+
const done = this.awaitOutput(id, input, (_e = options.interruptible) !== null && _e !== void 0 ? _e : true);
|
|
105
|
+
this.send(Object.assign({ type: "speak", id, text: input, last: true }, fields));
|
|
106
|
+
return done;
|
|
107
|
+
}
|
|
108
|
+
let text = "";
|
|
109
|
+
const done = this.awaitOutput(id, "", (_f = options.interruptible) !== null && _f !== void 0 ? _f : true);
|
|
110
|
+
try {
|
|
111
|
+
try {
|
|
112
|
+
for (_a = true, input_2 = __asyncValues(input); input_2_1 = yield input_2.next(), _b = input_2_1.done, !_b; _a = true) {
|
|
113
|
+
_d = input_2_1.value;
|
|
114
|
+
_a = false;
|
|
115
|
+
const piece = _d;
|
|
116
|
+
text += piece;
|
|
117
|
+
this.send(Object.assign({ type: "speak", id, text: piece, last: false }, fields));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
121
|
+
finally {
|
|
122
|
+
try {
|
|
123
|
+
if (!_a && !_b && (_c = input_2.return)) yield _c.call(input_2);
|
|
124
|
+
}
|
|
125
|
+
finally { if (e_1) throw e_1.error; }
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch (e) {
|
|
129
|
+
// End the utterance so AppBridge does not wait for more pieces.
|
|
130
|
+
this.send(Object.assign({ type: "speak", id, text: "", last: true }, fields));
|
|
131
|
+
this.outputs.delete(id);
|
|
132
|
+
done.catch(() => undefined);
|
|
133
|
+
throw e;
|
|
134
|
+
}
|
|
135
|
+
const output = this.outputs.get(id);
|
|
136
|
+
if (output)
|
|
137
|
+
output.text = text;
|
|
138
|
+
this.send(Object.assign({ type: "speak", id, text: "", last: true }, fields));
|
|
139
|
+
return done;
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
/** Cuts the agent off: every pending say and play resolves as interrupted. */
|
|
143
|
+
stopSay() {
|
|
144
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
145
|
+
this.send({ type: "clear" });
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
play(url_1) {
|
|
149
|
+
return __awaiter(this, arguments, void 0, function* (url, options = {}) {
|
|
150
|
+
var _a;
|
|
151
|
+
const id = this.nextId("play");
|
|
152
|
+
const done = this.awaitOutput(id, "", (_a = options.interruptible) !== null && _a !== void 0 ? _a : true);
|
|
153
|
+
this.send(Object.assign({ type: "play", id, url }, this.compact(options)));
|
|
154
|
+
const r = yield done;
|
|
155
|
+
return { interrupted: r.interrupted, heardMs: r.heardMs };
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
/** Waits for speech (turn.ended) or digits (dtmf), whichever comes first. */
|
|
159
|
+
gather(options = {}) {
|
|
160
|
+
var _a;
|
|
161
|
+
if (this.endedReason)
|
|
162
|
+
return Promise.reject(new errors_1.SessionEndedError(this.endedReason));
|
|
163
|
+
if (this.gatherState)
|
|
164
|
+
return Promise.reject(new Error("a gather is already pending"));
|
|
165
|
+
const source = (_a = options.source) !== null && _a !== void 0 ? _a : "speech_and_dtmf";
|
|
166
|
+
// AppBridge listens only after a Speech-layer command; a gather may be the first one.
|
|
167
|
+
if (source !== "dtmf" && !this.listening)
|
|
168
|
+
this.send({ type: "listen.update" });
|
|
169
|
+
return new Promise((resolve, reject) => {
|
|
170
|
+
var _a;
|
|
171
|
+
const g = {
|
|
172
|
+
speech: source !== "dtmf",
|
|
173
|
+
dtmf: source !== "speech",
|
|
174
|
+
maxDigits: options.maxDigits,
|
|
175
|
+
finishOnKey: options.finishOnKey,
|
|
176
|
+
timeoutMs: (_a = options.timeout) !== null && _a !== void 0 ? _a : DEFAULT_GATHER_TIMEOUT_MS,
|
|
177
|
+
digits: "",
|
|
178
|
+
resolve,
|
|
179
|
+
reject
|
|
180
|
+
};
|
|
181
|
+
this.gatherState = g;
|
|
182
|
+
this.armGatherTimer(g);
|
|
183
|
+
// Inputs that arrived before the gather (type-ahead) count, in order.
|
|
184
|
+
const backlog = this.buffered;
|
|
185
|
+
this.buffered = [];
|
|
186
|
+
for (const input of backlog) {
|
|
187
|
+
if (this.gatherState !== g)
|
|
188
|
+
break;
|
|
189
|
+
this.feedGather(input);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Starts listening or changes how AppBridge listens (`listen.update`): recognition language,
|
|
195
|
+
* hints, and the turn-taking patience knob (`low`, `normal` or `high`).
|
|
196
|
+
*/
|
|
197
|
+
listen() {
|
|
198
|
+
return __awaiter(this, arguments, void 0, function* (options = {}) {
|
|
199
|
+
this.send(Object.assign({ type: "listen.update" }, this.compact(options)));
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
/** Requests opt-in events: "turn.estimate", "transcript.partial". */
|
|
203
|
+
subscribe(events) {
|
|
204
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
205
|
+
this.send({ type: "subscribe", events });
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
on(event, cb) {
|
|
209
|
+
var _a;
|
|
210
|
+
const set = (_a = this.listeners.get(event)) !== null && _a !== void 0 ? _a : new Set();
|
|
211
|
+
set.add(cb);
|
|
212
|
+
this.listeners.set(event, set);
|
|
213
|
+
return this;
|
|
214
|
+
}
|
|
215
|
+
off(event, cb) {
|
|
216
|
+
var _a;
|
|
217
|
+
(_a = this.listeners.get(event)) === null || _a === void 0 ? void 0 : _a.delete(cb);
|
|
218
|
+
return this;
|
|
219
|
+
}
|
|
220
|
+
// ---- control ----------------------------------------------------------------------
|
|
221
|
+
playDtmf(digits) {
|
|
222
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
223
|
+
this.command({ type: "dtmf.send", digits });
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
/** Transfers the call; resolves when AppBridge ends the session with reason transfer. */
|
|
227
|
+
transfer(to, options = {}) {
|
|
228
|
+
const id = this.nextId("transfer");
|
|
229
|
+
if (options.mode === "warm")
|
|
230
|
+
this.lastWarmTransfer = id;
|
|
231
|
+
return this.untilEnd(["transfer"], id, () => this.send(Object.assign({ type: "transfer", id,
|
|
232
|
+
to }, this.compact({ mode: options.mode, headers: options.headers }))));
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Completes the pending warm transfer: the agent leaves, caller and new party stay. Resolves
|
|
236
|
+
* when the session ends with reason transfer; rejects with a SessionError
|
|
237
|
+
* (`transfer_not_answered`) when the new party has not answered yet, which leaves the transfer
|
|
238
|
+
* pending: wait for the `transfer.answered` event and call it again.
|
|
239
|
+
*/
|
|
240
|
+
completeTransfer() {
|
|
241
|
+
const id = this.lastWarmTransfer;
|
|
242
|
+
if (!id)
|
|
243
|
+
return Promise.reject(new Error("no warm transfer is pending"));
|
|
244
|
+
return new Promise((resolve, reject) => {
|
|
245
|
+
if (this.endedReason)
|
|
246
|
+
return reject(new errors_1.SessionEndedError(this.endedReason));
|
|
247
|
+
const waiter = { reasons: ["transfer"], resolve, reject };
|
|
248
|
+
this.endWaiters.push(waiter);
|
|
249
|
+
this.completing = (e) => {
|
|
250
|
+
this.endWaiters = this.endWaiters.filter((w) => w !== waiter);
|
|
251
|
+
reject(e);
|
|
252
|
+
};
|
|
253
|
+
try {
|
|
254
|
+
this.send({ type: "transfer.complete", id });
|
|
255
|
+
}
|
|
256
|
+
catch (e) {
|
|
257
|
+
this.completing = undefined;
|
|
258
|
+
reject(e);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
hold() {
|
|
263
|
+
return __awaiter(this, arguments, void 0, function* (options = {}) {
|
|
264
|
+
this.command(Object.assign({ type: "hold" }, this.compact(options)));
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
resume() {
|
|
268
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
269
|
+
this.command({ type: "resume" });
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
/** Hands the call to another app with a payload; resolves when this session ends. */
|
|
273
|
+
handoff(app, payload = {}) {
|
|
274
|
+
const id = this.nextId("handoff");
|
|
275
|
+
return this.untilEnd(["handoff"], id, () => this.send({ type: "handoff", id, app, payload }));
|
|
276
|
+
}
|
|
277
|
+
/** Hangs up; resolves when the session ends. */
|
|
278
|
+
hangup() {
|
|
279
|
+
if (this.endedReason)
|
|
280
|
+
return Promise.resolve();
|
|
281
|
+
return this.untilEnd(["app_hangup", "caller_hangup", "closed"], undefined, () => this.send({ type: "hangup" }));
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Adds a note to the call record (record.note).
|
|
285
|
+
*
|
|
286
|
+
* @internal Reserved for Fonoster's own apps (Autopilot); not part of the public SDK and may change.
|
|
287
|
+
*/
|
|
288
|
+
note(data) {
|
|
289
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
290
|
+
this.command({ type: "record.note", data });
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Adds model token usage to the call (usage.report, add-usage-metering).
|
|
295
|
+
*
|
|
296
|
+
* @internal Reserved for Fonoster's own Autopilot; AppBridge refuses it from other apps.
|
|
297
|
+
*/
|
|
298
|
+
reportUsage(lines) {
|
|
299
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
300
|
+
this.command({ type: "usage.report", lines });
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
/** Resolves at once: AppBridge answers the call on connect (voice-sdk S2). */
|
|
304
|
+
answer() {
|
|
305
|
+
return __awaiter(this, void 0, void 0, function* () { });
|
|
306
|
+
}
|
|
307
|
+
// ---- incoming ---------------------------------------------------------------------
|
|
308
|
+
/** Feeds one frame from the socket. */
|
|
309
|
+
receive(data, isBinary) {
|
|
310
|
+
if (isBinary) {
|
|
311
|
+
const trackId = data[0];
|
|
312
|
+
const frame = data.subarray(1);
|
|
313
|
+
for (const cb of this.frameListeners)
|
|
314
|
+
cb(frame, trackId);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
let msg;
|
|
318
|
+
try {
|
|
319
|
+
msg = JSON.parse(data.toString());
|
|
320
|
+
}
|
|
321
|
+
catch (_a) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
this.onMessage(msg);
|
|
325
|
+
}
|
|
326
|
+
/** The socket closed. */
|
|
327
|
+
closed() {
|
|
328
|
+
this.finish("closed");
|
|
329
|
+
}
|
|
330
|
+
onMessage(msg) {
|
|
331
|
+
var _a, _b, _c, _d;
|
|
332
|
+
const type = msg.type;
|
|
333
|
+
switch (type) {
|
|
334
|
+
case "output.started": {
|
|
335
|
+
const o = this.outputs.get(msg.id);
|
|
336
|
+
if (o)
|
|
337
|
+
o.startedAt = Date.now();
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
case "output.done": {
|
|
341
|
+
const o = this.take(msg.id);
|
|
342
|
+
o === null || o === void 0 ? void 0 : o.resolve({
|
|
343
|
+
interrupted: false,
|
|
344
|
+
heardText: o.text,
|
|
345
|
+
heardMs: o.startedAt ? Date.now() - o.startedAt : 0
|
|
346
|
+
});
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
case "output.interrupted": {
|
|
350
|
+
const o = this.take(msg.id);
|
|
351
|
+
o === null || o === void 0 ? void 0 : o.resolve({
|
|
352
|
+
interrupted: true,
|
|
353
|
+
heardText: String((_a = msg.heardText) !== null && _a !== void 0 ? _a : ""),
|
|
354
|
+
heardMs: Number((_b = msg.heardMs) !== null && _b !== void 0 ? _b : 0)
|
|
355
|
+
});
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
case "mark": {
|
|
359
|
+
const list = this.marks.get(msg.name);
|
|
360
|
+
const resolve = list === null || list === void 0 ? void 0 : list.shift();
|
|
361
|
+
if (list && !list.length)
|
|
362
|
+
this.marks.delete(msg.name);
|
|
363
|
+
resolve === null || resolve === void 0 ? void 0 : resolve({ cleared: msg.cleared === true });
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
case "error":
|
|
367
|
+
return this.onError(msg);
|
|
368
|
+
case "session.end":
|
|
369
|
+
return this.finish(String((_c = msg.reason) !== null && _c !== void 0 ? _c : "closed"));
|
|
370
|
+
case "dtmf":
|
|
371
|
+
this.emit("dtmf", { digit: msg.digit });
|
|
372
|
+
return this.input({ type: "dtmf", digit: String(msg.digit) });
|
|
373
|
+
case "turn.ended":
|
|
374
|
+
this.emit(type, this.without(msg, "type"));
|
|
375
|
+
return this.input({ type: "turn.ended", text: String((_d = msg.text) !== null && _d !== void 0 ? _d : "") });
|
|
376
|
+
case "turn.user_started":
|
|
377
|
+
this.bargeIn();
|
|
378
|
+
this.emit(type, {});
|
|
379
|
+
return this.input({ type: "turn.user_started" });
|
|
380
|
+
case "turn.likely_ended":
|
|
381
|
+
case "turn.resumed":
|
|
382
|
+
case "turn.backchannel":
|
|
383
|
+
case "transcript.partial":
|
|
384
|
+
case "turn.estimate":
|
|
385
|
+
case "amd.result":
|
|
386
|
+
case "transfer.answered":
|
|
387
|
+
return this.emit(type, this.without(msg, "type"));
|
|
388
|
+
default:
|
|
389
|
+
// Unknown types are ignored (additive-only protocol).
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
onError(msg) {
|
|
393
|
+
var _a;
|
|
394
|
+
const error = new errors_1.SessionError(String(msg.code), String((_a = msg.message) !== null && _a !== void 0 ? _a : msg.code), msg.fatal === true);
|
|
395
|
+
const id = msg.relatesTo;
|
|
396
|
+
// A refused completeTransfer() fails that call, not the pending transfer.
|
|
397
|
+
if (error.code === "transfer_not_answered" && this.completing) {
|
|
398
|
+
const completing = this.completing;
|
|
399
|
+
this.completing = undefined;
|
|
400
|
+
completing(error);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
const output = id ? this.take(id) : undefined;
|
|
404
|
+
const command = id ? this.commands.get(id) : undefined;
|
|
405
|
+
if (output)
|
|
406
|
+
output.reject(error);
|
|
407
|
+
else if (command) {
|
|
408
|
+
this.commands.delete(id);
|
|
409
|
+
command(error);
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
this.emit("error", {
|
|
413
|
+
code: error.code,
|
|
414
|
+
message: error.message,
|
|
415
|
+
fatal: error.fatal
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
if (error.fatal)
|
|
419
|
+
this.finish("error");
|
|
420
|
+
}
|
|
421
|
+
// ---- gather -----------------------------------------------------------------------
|
|
422
|
+
input(input) {
|
|
423
|
+
if (this.gatherState)
|
|
424
|
+
return this.feedGather(input);
|
|
425
|
+
if (input.type === "turn.user_started")
|
|
426
|
+
return;
|
|
427
|
+
this.buffered.push(input);
|
|
428
|
+
if (this.buffered.length > MAX_BUFFERED_INPUTS)
|
|
429
|
+
this.buffered.shift();
|
|
430
|
+
}
|
|
431
|
+
feedGather(input) {
|
|
432
|
+
const g = this.gatherState;
|
|
433
|
+
if (input.type === "turn.user_started") {
|
|
434
|
+
// The caller is talking: wait for the end of the turn instead of timing out mid-sentence.
|
|
435
|
+
if (g.speech && !g.digits && g.timer)
|
|
436
|
+
clearTimeout(g.timer);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
if (input.type === "turn.ended") {
|
|
440
|
+
// First input wins: once digits started, speech is ignored.
|
|
441
|
+
if (g.speech && !g.digits)
|
|
442
|
+
this.endGather({ speech: input.text });
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if (!g.dtmf)
|
|
446
|
+
return;
|
|
447
|
+
if (g.finishOnKey && input.digit === g.finishOnKey)
|
|
448
|
+
return this.endGather({ digits: g.digits });
|
|
449
|
+
g.digits += input.digit;
|
|
450
|
+
if (g.maxDigits && g.digits.length >= g.maxDigits)
|
|
451
|
+
return this.endGather({ digits: g.digits });
|
|
452
|
+
this.armGatherTimer(g);
|
|
453
|
+
}
|
|
454
|
+
armGatherTimer(g) {
|
|
455
|
+
if (g.timer)
|
|
456
|
+
clearTimeout(g.timer);
|
|
457
|
+
g.timer = setTimeout(() => this.endGather(g.digits ? { digits: g.digits } : {}), g.timeoutMs);
|
|
458
|
+
}
|
|
459
|
+
endGather(result) {
|
|
460
|
+
const g = this.gatherState;
|
|
461
|
+
if (!g)
|
|
462
|
+
return;
|
|
463
|
+
if (g.timer)
|
|
464
|
+
clearTimeout(g.timer);
|
|
465
|
+
this.gatherState = undefined;
|
|
466
|
+
g.resolve(result);
|
|
467
|
+
}
|
|
468
|
+
// ---- internals --------------------------------------------------------------------
|
|
469
|
+
nextId(kind) {
|
|
470
|
+
return `${kind}-${++this.seq}`;
|
|
471
|
+
}
|
|
472
|
+
send(message) {
|
|
473
|
+
if (this.endedReason)
|
|
474
|
+
throw new errors_1.SessionEndedError(this.endedReason);
|
|
475
|
+
if (SPEECH_COMMANDS.has(message.type))
|
|
476
|
+
this.listening = true;
|
|
477
|
+
this.transport.sendJson(message);
|
|
478
|
+
}
|
|
479
|
+
/** Sends a command whose only answer is a possible error (logged as a session error). */
|
|
480
|
+
command(message) {
|
|
481
|
+
const id = this.nextId(String(message.type).replace(".", "-"));
|
|
482
|
+
this.send(Object.assign(Object.assign({}, message), { id }));
|
|
483
|
+
}
|
|
484
|
+
awaitOutput(id, text, interruptible = true) {
|
|
485
|
+
return new Promise((resolve, reject) => {
|
|
486
|
+
if (this.endedReason)
|
|
487
|
+
return reject(new errors_1.SessionEndedError(this.endedReason));
|
|
488
|
+
this.outputs.set(id, { text, resolve, reject, interruptible });
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* The app owns interruption (appbridge B22): when the caller starts talking over an interruptible
|
|
493
|
+
* say or play, the SDK cuts it off with clear; a non-interruptible one keeps playing.
|
|
494
|
+
*/
|
|
495
|
+
bargeIn() {
|
|
496
|
+
const playing = [...this.outputs.values()].filter((o) => o.startedAt !== undefined);
|
|
497
|
+
if (playing.length && playing.every((o) => o.interruptible))
|
|
498
|
+
this.send({ type: "clear" });
|
|
499
|
+
}
|
|
500
|
+
take(id) {
|
|
501
|
+
const o = this.outputs.get(id);
|
|
502
|
+
this.outputs.delete(id);
|
|
503
|
+
return o;
|
|
504
|
+
}
|
|
505
|
+
untilEnd(reasons, id, sendIt) {
|
|
506
|
+
return new Promise((resolve, reject) => {
|
|
507
|
+
if (this.endedReason)
|
|
508
|
+
return reject(new errors_1.SessionEndedError(this.endedReason));
|
|
509
|
+
const waiter = { reasons, resolve, reject };
|
|
510
|
+
this.endWaiters.push(waiter);
|
|
511
|
+
if (id) {
|
|
512
|
+
this.commands.set(id, (e) => {
|
|
513
|
+
this.endWaiters = this.endWaiters.filter((w) => w !== waiter);
|
|
514
|
+
reject(e);
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
try {
|
|
518
|
+
sendIt();
|
|
519
|
+
}
|
|
520
|
+
catch (e) {
|
|
521
|
+
reject(e);
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
finish(reason) {
|
|
526
|
+
if (this.endedReason)
|
|
527
|
+
return;
|
|
528
|
+
this.endedReason = reason;
|
|
529
|
+
this.completing = undefined;
|
|
530
|
+
const ended = new errors_1.SessionEndedError(reason);
|
|
531
|
+
for (const w of this.endWaiters) {
|
|
532
|
+
if (w.reasons.includes(reason))
|
|
533
|
+
w.resolve();
|
|
534
|
+
else
|
|
535
|
+
w.reject(ended);
|
|
536
|
+
}
|
|
537
|
+
this.endWaiters = [];
|
|
538
|
+
for (const o of this.outputs.values())
|
|
539
|
+
o.reject(ended);
|
|
540
|
+
this.outputs.clear();
|
|
541
|
+
this.commands.clear();
|
|
542
|
+
for (const list of this.marks.values())
|
|
543
|
+
for (const r of list)
|
|
544
|
+
r({ cleared: true });
|
|
545
|
+
this.marks.clear();
|
|
546
|
+
if (this.gatherState) {
|
|
547
|
+
const g = this.gatherState;
|
|
548
|
+
if (g.timer)
|
|
549
|
+
clearTimeout(g.timer);
|
|
550
|
+
this.gatherState = undefined;
|
|
551
|
+
g.reject(ended);
|
|
552
|
+
}
|
|
553
|
+
this.emit("end", { reason });
|
|
554
|
+
this.transport.close();
|
|
555
|
+
}
|
|
556
|
+
emit(event, data) {
|
|
557
|
+
var _a;
|
|
558
|
+
for (const cb of (_a = this.listeners.get(event)) !== null && _a !== void 0 ? _a : []) {
|
|
559
|
+
try {
|
|
560
|
+
cb(data);
|
|
561
|
+
}
|
|
562
|
+
catch (_b) {
|
|
563
|
+
// A listener's failure must not break the session.
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
compact(o) {
|
|
568
|
+
return Object.fromEntries(Object.entries(o).filter(([, v]) => v !== undefined));
|
|
569
|
+
}
|
|
570
|
+
without(msg, key) {
|
|
571
|
+
const _a = msg, _b = key, _ = _a[_b], rest = __rest(_a, [typeof _b === "symbol" ? _b : _b + ""]);
|
|
572
|
+
void _;
|
|
573
|
+
return rest;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
exports.Session = Session;
|
package/dist/VoiceServer.d.ts
CHANGED
|
@@ -1,6 +1,71 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
|
|
3
|
+
* http://github.com/fonoster/fonoster
|
|
4
|
+
*
|
|
5
|
+
* This file is part of Fonoster
|
|
6
|
+
*
|
|
7
|
+
* Licensed under the MIT License (the "License");
|
|
8
|
+
* you may not use this file except in compliance with
|
|
9
|
+
* the License. You may obtain a copy of the License at
|
|
10
|
+
*
|
|
11
|
+
* https://opensource.org/licenses/MIT
|
|
12
|
+
*
|
|
13
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
* See the License for the specific language governing permissions and
|
|
17
|
+
* limitations under the License.
|
|
18
|
+
*/
|
|
19
|
+
import * as http from "http";
|
|
20
|
+
import { Duplex } from "stream";
|
|
21
|
+
import { Session } from "./Session";
|
|
22
|
+
export type SessionHandler = (session: Session) => Promise<void> | void;
|
|
23
|
+
export type Handler = SessionHandler;
|
|
24
|
+
export type VoiceServerOptions = {
|
|
25
|
+
port?: number;
|
|
26
|
+
host?: string;
|
|
27
|
+
/** Path to accept sessions on (default: any). */
|
|
28
|
+
path?: string;
|
|
29
|
+
/** The app's signing secret (default: APP_SIGNING_SECRET). */
|
|
30
|
+
signingSecret?: string;
|
|
31
|
+
/** Turn verification off for local development only; every connection then logs a warning. */
|
|
32
|
+
verifySignature?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* The app's URL as configured in Fonoster (one or more). When set, signatures are checked
|
|
35
|
+
* against it only, never against a URL built from the request's Host header. Set it whenever
|
|
36
|
+
* a proxy changes the host or scheme, and always for a server several apps share.
|
|
37
|
+
*/
|
|
38
|
+
publicUrl?: string | string[];
|
|
39
|
+
/**
|
|
40
|
+
* Require the app-bound signature (X-Fonoster-App-Signature): a connection without it is
|
|
41
|
+
* refused, and session.start must be for the app it was signed for. For a server several apps
|
|
42
|
+
* share one secret with, such as the hosted Autopilot.
|
|
43
|
+
*/
|
|
44
|
+
requireAppBinding?: boolean;
|
|
45
|
+
/** How long to wait for session.start after the socket opens (default 5 s). */
|
|
46
|
+
startTimeoutMs?: number;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Accepts AppBridge sessions over WebSocket and runs the handler once per session
|
|
50
|
+
* (voice-sdk, "VoiceServer accepts AppBridge sessions over WebSocket").
|
|
51
|
+
*/
|
|
52
|
+
export declare class VoiceServer {
|
|
53
|
+
private readonly options;
|
|
54
|
+
private wss;
|
|
55
|
+
private server?;
|
|
56
|
+
private handler?;
|
|
57
|
+
private readonly secret?;
|
|
58
|
+
private readonly verify;
|
|
59
|
+
constructor(options?: VoiceServerOptions);
|
|
60
|
+
/** Starts a standalone server. Resolves once it is listening. */
|
|
61
|
+
listen(handler: Handler): Promise<this>;
|
|
62
|
+
/** Mounts on an existing HTTP server (Express included): sessions arrive on `path`. */
|
|
63
|
+
attach(server: http.Server, handler: Handler): this;
|
|
64
|
+
get port(): number | undefined;
|
|
65
|
+
close(): Promise<void>;
|
|
66
|
+
/** Handles one upgrade request: verifies the signature, then runs a session. */
|
|
67
|
+
handleUpgrade(req: http.IncomingMessage, socket: Duplex, head: Buffer): void;
|
|
68
|
+
private candidateUrls;
|
|
69
|
+
private run;
|
|
70
|
+
private invoke;
|
|
6
71
|
}
|