@fonoster/amd 0.30.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 +10 -110
- package/dist/index.d.ts +0 -4
- package/dist/index.js +0 -4
- package/package.json +7 -15
- package/dist/agi/AgiChannel.d.ts +0 -77
- package/dist/agi/AgiChannel.js +0 -171
- package/dist/agi/AgiServer.d.ts +0 -34
- package/dist/agi/AgiServer.js +0 -109
- package/dist/agi/server.d.ts +0 -16
- package/dist/agi/server.js +0 -154
- package/dist/agi/sessionRegistry.d.ts +0 -26
- package/dist/agi/sessionRegistry.js +0 -34
- package/dist/amd/buildAmdVariables.d.ts +0 -13
- package/dist/amd/buildAmdVariables.js +0 -98
- package/dist/audiosocket/server.d.ts +0 -9
- package/dist/audiosocket/server.js +0 -65
- package/dist/envs.d.ts +0 -8
- package/dist/envs.js +0 -56
- package/dist/probe/collectPcm.d.ts +0 -37
- package/dist/probe/collectPcm.js +0 -47
- package/dist/probe/runProbe.d.ts +0 -37
- package/dist/probe/runProbe.js +0 -110
- package/dist/probe/upsample.d.ts +0 -3
- package/dist/probe/upsample.js +0 -53
- package/dist/server.d.ts +0 -1
- package/dist/server.js +0 -27
package/dist/agi/server.js
DELETED
|
@@ -1,154 +0,0 @@
|
|
|
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
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.startAgiServer = startAgiServer;
|
|
13
|
-
/**
|
|
14
|
-
* Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
|
|
15
|
-
* http://github.com/fonoster/fonoster
|
|
16
|
-
*
|
|
17
|
-
* This file is part of Fonoster
|
|
18
|
-
*
|
|
19
|
-
* Licensed under the MIT License (the "License");
|
|
20
|
-
* you may not use this file except in compliance with
|
|
21
|
-
* the License. You may obtain a copy of the License at
|
|
22
|
-
*
|
|
23
|
-
* https://opensource.org/licenses/MIT
|
|
24
|
-
*
|
|
25
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
26
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
27
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
28
|
-
* See the License for the specific language governing permissions and
|
|
29
|
-
* limitations under the License.
|
|
30
|
-
*/
|
|
31
|
-
const logger_1 = require("@fonoster/logger");
|
|
32
|
-
const uuid_1 = require("uuid");
|
|
33
|
-
const buildAmdVariables_1 = require("../amd/buildAmdVariables");
|
|
34
|
-
const envs_1 = require("../envs");
|
|
35
|
-
const AgiServer_1 = require("./AgiServer");
|
|
36
|
-
const sessionRegistry_1 = require("./sessionRegistry");
|
|
37
|
-
const logger = (0, logger_1.getLogger)({ service: "amd", filePath: __filename });
|
|
38
|
-
const TIMED_OUT_RESULT = () => ({
|
|
39
|
-
kind: "unknown",
|
|
40
|
-
cause: "ML-TIMEOUT",
|
|
41
|
-
latencyMs: 0
|
|
42
|
-
});
|
|
43
|
-
// How long to wait, once the verdict is in, for AudioSocket() to return and
|
|
44
|
-
// free the AGI socket. The audio leg hangs up right after classifying and
|
|
45
|
-
// runProbe shares TIMEOUT_MS, so this only runs out on a genuinely stuck leg.
|
|
46
|
-
const AUDIOSOCKET_RELEASE_MS = 2000;
|
|
47
|
-
const delay = (ms, value) => new Promise((resolve) => setTimeout(() => resolve(value), ms));
|
|
48
|
-
/**
|
|
49
|
-
* Starts the FastAGI listener. Asterisk's `AGI(agi://amd:4573,${AMD_MODE})`
|
|
50
|
-
* blocks the channel here. Per session: read the requested mode from the
|
|
51
|
-
* dialplan's first AGI argument (`compact`, the default, or `full`), mint a
|
|
52
|
-
* UUID, EXEC AudioSocket to pull the leading audio (blocks until the
|
|
53
|
-
* AudioSocket side hangs up), await the correlated raw result, map it to
|
|
54
|
-
* however many channel variables that mode produces, SET each, then return
|
|
55
|
-
* so the dialplan resumes at Stasis(mediacontroller).
|
|
56
|
-
*
|
|
57
|
-
* Always fail-open: deadlines bound both the wait for a verdict and the wait
|
|
58
|
-
* for AudioSocket() to return, and the AGI connection is always closed so the
|
|
59
|
-
* dialplan resumes even when no variables could be set.
|
|
60
|
-
*/
|
|
61
|
-
function startAgiServer() {
|
|
62
|
-
const agi = new AgiServer_1.AgiServer({ port: envs_1.AGI_PORT });
|
|
63
|
-
agi.on("ready", (port) => {
|
|
64
|
-
logger.info("agi server listening", { port });
|
|
65
|
-
});
|
|
66
|
-
agi.on("error", (err) => {
|
|
67
|
-
logger.error("agi server error", err);
|
|
68
|
-
});
|
|
69
|
-
agi.on("call", (call) => __awaiter(this, void 0, void 0, function* () {
|
|
70
|
-
const sessionId = (0, uuid_1.v4)();
|
|
71
|
-
const mode = (0, buildAmdVariables_1.parseMode)(call.args[0]);
|
|
72
|
-
let hungUp = false;
|
|
73
|
-
call.once("hangup", () => {
|
|
74
|
-
hungUp = true;
|
|
75
|
-
(0, sessionRegistry_1.unregisterPendingClassification)(sessionId);
|
|
76
|
-
});
|
|
77
|
-
logger.verbose("agi call", { sessionId, mode, channel: call.channel });
|
|
78
|
-
const classification = (0, sessionRegistry_1.registerPendingClassification)(sessionId);
|
|
79
|
-
// AudioSocket()'s result says nothing about whether AMD worked: Asterisk
|
|
80
|
-
// before 20.14 returns -1 ("200 result=-1") whenever the remote ends the
|
|
81
|
-
// stream, including the HANGUP this server sends after every
|
|
82
|
-
// classification. It only tells us when the leg is over and the AGI
|
|
83
|
-
// socket is free again.
|
|
84
|
-
const legEnded = call
|
|
85
|
-
.exec("AudioSocket", `${sessionId},${envs_1.AUDIOSOCKET_ADVERTISE_HOST}:${envs_1.AUDIOSOCKET_PORT}`)
|
|
86
|
-
.catch((err) => {
|
|
87
|
-
logger.verbose("AudioSocket() returned", {
|
|
88
|
-
sessionId,
|
|
89
|
-
result: err.message
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
// The verdict is resolved in-process before the audio leg hangs up, so a
|
|
93
|
-
// leg that ends first (e.g. Asterisk couldn't connect) has no verdict.
|
|
94
|
-
const noVerdict = {
|
|
95
|
-
kind: "unknown",
|
|
96
|
-
cause: "ML-ERROR",
|
|
97
|
-
latencyMs: 0
|
|
98
|
-
};
|
|
99
|
-
const result = yield Promise.race([
|
|
100
|
-
classification,
|
|
101
|
-
legEnded.then(() => noVerdict),
|
|
102
|
-
delay(envs_1.TIMEOUT_MS, TIMED_OUT_RESULT())
|
|
103
|
-
]);
|
|
104
|
-
(0, sessionRegistry_1.unregisterPendingClassification)(sessionId);
|
|
105
|
-
if (result === noVerdict) {
|
|
106
|
-
logger.warn("audiosocket leg ended without a verdict; reporting unknown", {
|
|
107
|
-
sessionId
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
// FastAGI is one command at a time: SET VARIABLE can only be sent once
|
|
111
|
-
// AudioSocket() has returned.
|
|
112
|
-
const released = yield Promise.race([
|
|
113
|
-
legEnded.then(() => true),
|
|
114
|
-
delay(AUDIOSOCKET_RELEASE_MS, false)
|
|
115
|
-
]);
|
|
116
|
-
if (hungUp || call.hungup) {
|
|
117
|
-
logger.verbose("agi call hung up mid-analysis; skipping SET VARIABLE", {
|
|
118
|
-
sessionId
|
|
119
|
-
});
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
if (!released) {
|
|
123
|
-
logger.warn("audiosocket leg still open; skipping SET VARIABLE", {
|
|
124
|
-
sessionId
|
|
125
|
-
});
|
|
126
|
-
call.close();
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
const variables = (0, buildAmdVariables_1.buildAmdVariables)(result, mode, envs_1.MIN_CONFIDENCE);
|
|
130
|
-
logger.verbose("agi call variables", { sessionId, mode, variables });
|
|
131
|
-
try {
|
|
132
|
-
// Sequential, not Promise.all: AgiChannel allows only one command in
|
|
133
|
-
// flight on the AGI socket at a time.
|
|
134
|
-
for (const [name, value] of Object.entries(variables)) {
|
|
135
|
-
yield call.setVariable(name, value);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
catch (err) {
|
|
139
|
-
logger.warn("failed to set AMD channel variables", {
|
|
140
|
-
sessionId,
|
|
141
|
-
error: err.message
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
finally {
|
|
145
|
-
// Asterisk's AGI() app returns control to the dialplan when the AGI
|
|
146
|
-
// script closes the connection — not on any special "done" command.
|
|
147
|
-
// Always close, success or failure, or the channel would sit in
|
|
148
|
-
// AGI() forever.
|
|
149
|
-
if (!call.hungup)
|
|
150
|
-
call.close();
|
|
151
|
-
}
|
|
152
|
-
}));
|
|
153
|
-
return agi;
|
|
154
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
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 { ProbeResult } from "../probe/runProbe";
|
|
20
|
-
/** Called by the AGI handler right before it EXECs AudioSocket(). */
|
|
21
|
-
declare function registerPendingClassification(sessionId: string): Promise<ProbeResult>;
|
|
22
|
-
/** Called by the AudioSocket connection handler once it has a result. */
|
|
23
|
-
declare function resolvePendingClassification(sessionId: string, result: ProbeResult): void;
|
|
24
|
-
/** Called by the AGI handler in its `finally`, in case nothing ever resolved it. */
|
|
25
|
-
declare function unregisterPendingClassification(sessionId: string): void;
|
|
26
|
-
export { registerPendingClassification, resolvePendingClassification, unregisterPendingClassification };
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.registerPendingClassification = registerPendingClassification;
|
|
4
|
-
exports.resolvePendingClassification = resolvePendingClassification;
|
|
5
|
-
exports.unregisterPendingClassification = unregisterPendingClassification;
|
|
6
|
-
/**
|
|
7
|
-
* In-memory correlation between an AGI session and the AudioSocket connection
|
|
8
|
-
* it spawns, keyed by the UUID the AGI handler mints and passes as the
|
|
9
|
-
* AudioSocket() argument. Both listeners live in the same process, so this is
|
|
10
|
-
* a plain Map rather than anything cross-process.
|
|
11
|
-
*
|
|
12
|
-
* Carries the raw, mode-agnostic ProbeResult — not yet mapped to channel
|
|
13
|
-
* variables, since only the AGI side knows which mode (compact/full) this
|
|
14
|
-
* session asked for.
|
|
15
|
-
*/
|
|
16
|
-
const pending = new Map();
|
|
17
|
-
/** Called by the AGI handler right before it EXECs AudioSocket(). */
|
|
18
|
-
function registerPendingClassification(sessionId) {
|
|
19
|
-
return new Promise((resolve) => {
|
|
20
|
-
pending.set(sessionId, { resolve });
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
/** Called by the AudioSocket connection handler once it has a result. */
|
|
24
|
-
function resolvePendingClassification(sessionId, result) {
|
|
25
|
-
const entry = pending.get(sessionId);
|
|
26
|
-
if (!entry)
|
|
27
|
-
return;
|
|
28
|
-
pending.delete(sessionId);
|
|
29
|
-
entry.resolve(result);
|
|
30
|
-
}
|
|
31
|
-
/** Called by the AGI handler in its `finally`, in case nothing ever resolved it. */
|
|
32
|
-
function unregisterPendingClassification(sessionId) {
|
|
33
|
-
pending.delete(sessionId);
|
|
34
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { ProbeResult } from "../probe/runProbe";
|
|
2
|
-
type AmdMode = "compact" | "full";
|
|
3
|
-
type AmdVariables = Record<string, string>;
|
|
4
|
-
/**
|
|
5
|
-
* Parses the mode passed as the AGI session's first dialplan argument (e.g.
|
|
6
|
-
* `AGI(agi://amd:4573,${AMD_MODE})` -> `agi_arg_1` -> here).
|
|
7
|
-
* Anything but the literal "full" is treated as "compact" — the safe,
|
|
8
|
-
* native-AMD-compatible default when the argument is absent, empty, or
|
|
9
|
-
* misspelled.
|
|
10
|
-
*/
|
|
11
|
-
declare function parseMode(raw: string | undefined): AmdMode;
|
|
12
|
-
declare function buildAmdVariables(result: ProbeResult, mode: AmdMode, minConfidence: number): AmdVariables;
|
|
13
|
-
export { AmdMode, AmdVariables, buildAmdVariables, parseMode };
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.buildAmdVariables = buildAmdVariables;
|
|
4
|
-
exports.parseMode = parseMode;
|
|
5
|
-
/**
|
|
6
|
-
* Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
|
|
7
|
-
* http://github.com/fonoster/fonoster
|
|
8
|
-
*
|
|
9
|
-
* This file is part of Fonoster
|
|
10
|
-
*
|
|
11
|
-
* Licensed under the MIT License (the "License");
|
|
12
|
-
* you may not use this file except in compliance with
|
|
13
|
-
* the License. You may obtain a copy of the License at
|
|
14
|
-
*
|
|
15
|
-
* https://opensource.org/licenses/MIT
|
|
16
|
-
*
|
|
17
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
18
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
19
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
20
|
-
* See the License for the specific language governing permissions and
|
|
21
|
-
* limitations under the License.
|
|
22
|
-
*/
|
|
23
|
-
const common_1 = require("@fonoster/common");
|
|
24
|
-
/**
|
|
25
|
-
* Parses the mode passed as the AGI session's first dialplan argument (e.g.
|
|
26
|
-
* `AGI(agi://amd:4573,${AMD_MODE})` -> `agi_arg_1` -> here).
|
|
27
|
-
* Anything but the literal "full" is treated as "compact" — the safe,
|
|
28
|
-
* native-AMD-compatible default when the argument is absent, empty, or
|
|
29
|
-
* misspelled.
|
|
30
|
-
*/
|
|
31
|
-
function parseMode(raw) {
|
|
32
|
-
return raw === "full" ? "full" : "compact";
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Collapses the classifier's 5-way AmdStatus onto the 4-way AMDSTATUS
|
|
36
|
-
* vocabulary Asterisk's own AMD() app uses (HUMAN/MACHINE/NOTSURE/HANGUP —
|
|
37
|
-
* this module never reports HANGUP, that's apiserver's read of a channel
|
|
38
|
-
* that is already gone). This is the seam that lets apiserver's
|
|
39
|
-
* `mapAsteriskAmd.ts` and `createCreateVoiceClient.ts` stay unmodified: in
|
|
40
|
-
* compact mode they only ever see the same four values native AMD() already
|
|
41
|
-
* produces. Confidence, detector, latency, and the VOICEMAIL/IVR distinction
|
|
42
|
-
* are discarded here — that's exactly what "full" mode below is for.
|
|
43
|
-
*/
|
|
44
|
-
function buildCompactVariables(result, minConfidence) {
|
|
45
|
-
if (result.kind === "unknown") {
|
|
46
|
-
return { AMDSTATUS: "NOTSURE", AMDCAUSE: result.cause };
|
|
47
|
-
}
|
|
48
|
-
if (result.confidence < minConfidence) {
|
|
49
|
-
return { AMDSTATUS: "NOTSURE", AMDCAUSE: "ML-LOW-CONFIDENCE" };
|
|
50
|
-
}
|
|
51
|
-
switch (result.status) {
|
|
52
|
-
case common_1.AmdStatus.HUMAN:
|
|
53
|
-
return { AMDSTATUS: "HUMAN", AMDCAUSE: "HUMAN" };
|
|
54
|
-
case common_1.AmdStatus.MACHINE:
|
|
55
|
-
return { AMDSTATUS: "MACHINE", AMDCAUSE: "MACHINE-MACHINE" };
|
|
56
|
-
case common_1.AmdStatus.VOICEMAIL:
|
|
57
|
-
return { AMDSTATUS: "MACHINE", AMDCAUSE: "MACHINE-VOICEMAIL" };
|
|
58
|
-
case common_1.AmdStatus.IVR:
|
|
59
|
-
return { AMDSTATUS: "MACHINE", AMDCAUSE: "MACHINE-IVR" };
|
|
60
|
-
default:
|
|
61
|
-
// Reached only when the classifier itself reports UNKNOWN/UNSPECIFIED
|
|
62
|
-
// — a label AmdModel's mapLabelToStatus didn't recognize — which is
|
|
63
|
-
// independent of (and must not be confused with) the confidence-
|
|
64
|
-
// threshold downgrade above: that one already returned by this point.
|
|
65
|
-
return { AMDSTATUS: "NOTSURE", AMDCAUSE: "ML-UNKNOWN-LABEL" };
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Reports the classifier's own vocabulary and every measurement it
|
|
70
|
-
* produced, untouched (no confidence thresholding) — a consumer that opted
|
|
71
|
-
* into "full" mode is expected to apply its own policy over AMDCONFIDENCE
|
|
72
|
-
* rather than have this module decide NOTSURE for it. Not native-AMD
|
|
73
|
-
* compatible: a consumer reading AMDSTATUS here must expect VOICEMAIL/IVR/
|
|
74
|
-
* UNKNOWN in addition to HUMAN/MACHINE.
|
|
75
|
-
*/
|
|
76
|
-
function buildFullVariables(result) {
|
|
77
|
-
if (result.kind === "unknown") {
|
|
78
|
-
return {
|
|
79
|
-
AMDSTATUS: "UNKNOWN",
|
|
80
|
-
AMDCAUSE: result.cause,
|
|
81
|
-
AMDCONFIDENCE: "0",
|
|
82
|
-
AMDDETECTOR: "",
|
|
83
|
-
AMDLATENCYMS: String(result.latencyMs)
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
return {
|
|
87
|
-
AMDSTATUS: result.status,
|
|
88
|
-
AMDCAUSE: result.status,
|
|
89
|
-
AMDCONFIDENCE: result.confidence.toFixed(4),
|
|
90
|
-
AMDDETECTOR: result.detector,
|
|
91
|
-
AMDLATENCYMS: String(result.latencyMs)
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
function buildAmdVariables(result, mode, minConfidence) {
|
|
95
|
-
return mode === "full"
|
|
96
|
-
? buildFullVariables(result)
|
|
97
|
-
: buildCompactVariables(result, minConfidence);
|
|
98
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { AudioSocket } from "@fonoster/streams";
|
|
2
|
-
/**
|
|
3
|
-
* Starts the AudioSocket listener that receives the audio leg of each AGI
|
|
4
|
-
* session. Asterisk's `EXEC AudioSocket <uuid>,<host>:<port>` connects here;
|
|
5
|
-
* `req.ref` carries the same UUID the AGI handler minted, correlating this
|
|
6
|
-
* connection with the pending classification it's waiting on.
|
|
7
|
-
*/
|
|
8
|
-
declare function startAudioSocketServer(): AudioSocket;
|
|
9
|
-
export { startAudioSocketServer };
|
|
@@ -1,65 +0,0 @@
|
|
|
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
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.startAudioSocketServer = startAudioSocketServer;
|
|
13
|
-
/**
|
|
14
|
-
* Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
|
|
15
|
-
* http://github.com/fonoster/fonoster
|
|
16
|
-
*
|
|
17
|
-
* This file is part of Fonoster
|
|
18
|
-
*
|
|
19
|
-
* Licensed under the MIT License (the "License");
|
|
20
|
-
* you may not use this file except in compliance with
|
|
21
|
-
* the License. You may obtain a copy of the License at
|
|
22
|
-
*
|
|
23
|
-
* https://opensource.org/licenses/MIT
|
|
24
|
-
*
|
|
25
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
26
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
27
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
28
|
-
* See the License for the specific language governing permissions and
|
|
29
|
-
* limitations under the License.
|
|
30
|
-
*/
|
|
31
|
-
const logger_1 = require("@fonoster/logger");
|
|
32
|
-
const streams_1 = require("@fonoster/streams");
|
|
33
|
-
const sessionRegistry_1 = require("../agi/sessionRegistry");
|
|
34
|
-
const envs_1 = require("../envs");
|
|
35
|
-
const runProbe_1 = require("../probe/runProbe");
|
|
36
|
-
const logger = (0, logger_1.getLogger)({ service: "amd", filePath: __filename });
|
|
37
|
-
/**
|
|
38
|
-
* Starts the AudioSocket listener that receives the audio leg of each AGI
|
|
39
|
-
* session. Asterisk's `EXEC AudioSocket <uuid>,<host>:<port>` connects here;
|
|
40
|
-
* `req.ref` carries the same UUID the AGI handler minted, correlating this
|
|
41
|
-
* connection with the pending classification it's waiting on.
|
|
42
|
-
*/
|
|
43
|
-
function startAudioSocketServer() {
|
|
44
|
-
const audioSocket = new streams_1.AudioSocket();
|
|
45
|
-
audioSocket.onConnection((req, stream) => __awaiter(this, void 0, void 0, function* () {
|
|
46
|
-
const sessionId = req.ref;
|
|
47
|
-
logger.verbose("audiosocket connection", { sessionId });
|
|
48
|
-
const result = yield (0, runProbe_1.runProbe)({
|
|
49
|
-
stream,
|
|
50
|
-
probeMs: envs_1.PROBE_MS,
|
|
51
|
-
timeoutMs: envs_1.TIMEOUT_MS,
|
|
52
|
-
modelDir: envs_1.MODEL_PATH || undefined
|
|
53
|
-
});
|
|
54
|
-
logger.verbose("audiosocket result", Object.assign({ sessionId }, result));
|
|
55
|
-
(0, sessionRegistry_1.resolvePendingClassification)(sessionId, result);
|
|
56
|
-
stream.hangup();
|
|
57
|
-
}));
|
|
58
|
-
audioSocket.listen(envs_1.AUDIOSOCKET_PORT, envs_1.AUDIOSOCKET_BIND_ADDR, () => {
|
|
59
|
-
logger.info("audiosocket server listening", {
|
|
60
|
-
port: envs_1.AUDIOSOCKET_PORT,
|
|
61
|
-
bind: envs_1.AUDIOSOCKET_BIND_ADDR
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
return audioSocket;
|
|
65
|
-
}
|
package/dist/envs.d.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
export declare const AGI_PORT: number;
|
|
2
|
-
export declare const AUDIOSOCKET_PORT: number;
|
|
3
|
-
export declare const AUDIOSOCKET_BIND_ADDR: string;
|
|
4
|
-
export declare const AUDIOSOCKET_ADVERTISE_HOST: string;
|
|
5
|
-
export declare const PROBE_MS: number;
|
|
6
|
-
export declare const TIMEOUT_MS: number;
|
|
7
|
-
export declare const MIN_CONFIDENCE: number;
|
|
8
|
-
export declare const MODEL_PATH: string;
|
package/dist/envs.js
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.MODEL_PATH = exports.MIN_CONFIDENCE = exports.TIMEOUT_MS = exports.PROBE_MS = exports.AUDIOSOCKET_ADVERTISE_HOST = exports.AUDIOSOCKET_BIND_ADDR = exports.AUDIOSOCKET_PORT = exports.AGI_PORT = void 0;
|
|
4
|
-
/**
|
|
5
|
-
* Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
|
|
6
|
-
* http://github.com/fonoster/fonoster
|
|
7
|
-
*
|
|
8
|
-
* This file is part of Fonoster
|
|
9
|
-
*
|
|
10
|
-
* Licensed under the MIT License (the "License");
|
|
11
|
-
* you may not use this file except in compliance with
|
|
12
|
-
* the License. You may obtain a copy of the License at
|
|
13
|
-
*
|
|
14
|
-
* https://opensource.org/licenses/MIT
|
|
15
|
-
*
|
|
16
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
17
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
18
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
19
|
-
* See the License for the specific language governing permissions and
|
|
20
|
-
* limitations under the License.
|
|
21
|
-
*/
|
|
22
|
-
// The sidecar's settings come from its container environment (AMD_*), which
|
|
23
|
-
// the deployment sets. It never loads a .env file, not even in development
|
|
24
|
-
// (fix-config-and-placement-gaps).
|
|
25
|
-
const e = process.env;
|
|
26
|
-
// A malformed AMD_* number must not silently break the probe (NaN
|
|
27
|
-
// thresholds disable the deadline / confidence gate), so fall back to the
|
|
28
|
-
// default.
|
|
29
|
-
const positiveInt = (raw, fallback) => {
|
|
30
|
-
const n = parseInt(raw !== null && raw !== void 0 ? raw : "", 10);
|
|
31
|
-
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
32
|
-
};
|
|
33
|
-
const unitFloat = (raw, fallback) => {
|
|
34
|
-
const n = parseFloat(raw !== null && raw !== void 0 ? raw : "");
|
|
35
|
-
return Number.isFinite(n) && n >= 0 && n <= 1 ? n : fallback;
|
|
36
|
-
};
|
|
37
|
-
// FastAGI listener. AgiServer binds all interfaces; there is no separate
|
|
38
|
-
// bind-address knob.
|
|
39
|
-
exports.AGI_PORT = positiveInt(e.AMD_AGI_PORT, 4573);
|
|
40
|
-
// AudioSocket listener that receives the probed call audio.
|
|
41
|
-
exports.AUDIOSOCKET_PORT = positiveInt(e.AMD_AUDIOSOCKET_PORT, 9092);
|
|
42
|
-
exports.AUDIOSOCKET_BIND_ADDR = e.AMD_AUDIOSOCKET_BIND_ADDR || "0.0.0.0";
|
|
43
|
-
// The host:port Asterisk is told to connect to for the AudioSocket leg — must
|
|
44
|
-
// be reachable FROM Asterisk, which is why this is separate from the bind
|
|
45
|
-
// address above (e.g. the compose service name, not 0.0.0.0).
|
|
46
|
-
exports.AUDIOSOCKET_ADVERTISE_HOST = e.AMD_AUDIOSOCKET_ADVERTISE_HOST || "amd";
|
|
47
|
-
// Leading audio to gather before classifying, in milliseconds.
|
|
48
|
-
exports.PROBE_MS = positiveInt(e.AMD_PROBE_MS, 3000);
|
|
49
|
-
// Hard deadline for one whole AGI session (buffering + classify + SET
|
|
50
|
-
// VARIABLE). On expiry the verdict is NOTSURE/ML-TIMEOUT.
|
|
51
|
-
exports.TIMEOUT_MS = positiveInt(e.AMD_TIMEOUT_MS, 4000);
|
|
52
|
-
// Verdicts below this confidence are downgraded to NOTSURE.
|
|
53
|
-
exports.MIN_CONFIDENCE = unitFloat(e.AMD_MIN_CONFIDENCE, 0.8);
|
|
54
|
-
// Override the model directory (model.onnx / mel_filters.bin / meta.json).
|
|
55
|
-
// Empty string -> the copy bundled with @fonoster/amd.
|
|
56
|
-
exports.MODEL_PATH = e.AMD_MODEL_PATH || "";
|
|
@@ -1,37 +0,0 @@
|
|
|
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 { AudioStream } from "@fonoster/streams";
|
|
20
|
-
type CollectPcmParams = {
|
|
21
|
-
stream: AudioStream;
|
|
22
|
-
probeMs: number;
|
|
23
|
-
signal: AbortSignal;
|
|
24
|
-
};
|
|
25
|
-
/**
|
|
26
|
-
* Buffers the first `probeMs` of a call's audio (or whatever arrives before
|
|
27
|
-
* the shared deadline) off an AudioSocket `AudioStream` and returns it as one
|
|
28
|
-
* Buffer. Resolves early once enough audio has accumulated, on the stream
|
|
29
|
-
* closing/erroring, or when `signal` aborts. Never rejects.
|
|
30
|
-
*
|
|
31
|
-
* `AudioStream` exposes no way to remove a single listener, so this settles
|
|
32
|
-
* once via a guard flag rather than detaching — safe here because each
|
|
33
|
-
* AudioStream is scoped to one AGI/AudioSocket session and is torn down right
|
|
34
|
-
* after classification (see `audiosocket/server.ts`).
|
|
35
|
-
*/
|
|
36
|
-
declare function collectPcm(params: CollectPcmParams): Promise<Buffer>;
|
|
37
|
-
export { collectPcm };
|
package/dist/probe/collectPcm.js
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.collectPcm = collectPcm;
|
|
4
|
-
// Asterisk's AudioSocket() app always streams slin (8 kHz), whatever the
|
|
5
|
-
// channel's native rate.
|
|
6
|
-
const SAMPLE_RATE = 8000;
|
|
7
|
-
const BYTES_PER_SAMPLE = 2; // mono, 16-bit little-endian
|
|
8
|
-
/**
|
|
9
|
-
* Buffers the first `probeMs` of a call's audio (or whatever arrives before
|
|
10
|
-
* the shared deadline) off an AudioSocket `AudioStream` and returns it as one
|
|
11
|
-
* Buffer. Resolves early once enough audio has accumulated, on the stream
|
|
12
|
-
* closing/erroring, or when `signal` aborts. Never rejects.
|
|
13
|
-
*
|
|
14
|
-
* `AudioStream` exposes no way to remove a single listener, so this settles
|
|
15
|
-
* once via a guard flag rather than detaching — safe here because each
|
|
16
|
-
* AudioStream is scoped to one AGI/AudioSocket session and is torn down right
|
|
17
|
-
* after classification (see `audiosocket/server.ts`).
|
|
18
|
-
*/
|
|
19
|
-
function collectPcm(params) {
|
|
20
|
-
const { stream, probeMs, signal } = params;
|
|
21
|
-
const bytesNeeded = Math.ceil((probeMs / 1000) * SAMPLE_RATE * BYTES_PER_SAMPLE);
|
|
22
|
-
return new Promise((resolve) => {
|
|
23
|
-
const chunks = [];
|
|
24
|
-
let total = 0;
|
|
25
|
-
let settled = false;
|
|
26
|
-
const finish = () => {
|
|
27
|
-
if (settled)
|
|
28
|
-
return;
|
|
29
|
-
settled = true;
|
|
30
|
-
clearTimeout(windowTimer);
|
|
31
|
-
signal.removeEventListener("abort", finish);
|
|
32
|
-
resolve(Buffer.concat(chunks, total));
|
|
33
|
-
};
|
|
34
|
-
stream.onData((chunk) => {
|
|
35
|
-
if (settled)
|
|
36
|
-
return;
|
|
37
|
-
chunks.push(chunk);
|
|
38
|
-
total += chunk.length;
|
|
39
|
-
if (total >= bytesNeeded)
|
|
40
|
-
finish();
|
|
41
|
-
});
|
|
42
|
-
stream.onClose(finish);
|
|
43
|
-
stream.onError(finish);
|
|
44
|
-
const windowTimer = setTimeout(finish, probeMs);
|
|
45
|
-
signal.addEventListener("abort", finish, { once: true });
|
|
46
|
-
});
|
|
47
|
-
}
|
package/dist/probe/runProbe.d.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { AudioStream } from "@fonoster/streams";
|
|
2
|
-
import { AmdClassification } from "../amd/AmdModel";
|
|
3
|
-
type Classify = (pcm: Buffer) => Promise<AmdClassification>;
|
|
4
|
-
type RunProbeParams = {
|
|
5
|
-
stream: AudioStream;
|
|
6
|
-
probeMs: number;
|
|
7
|
-
timeoutMs: number;
|
|
8
|
-
modelDir?: string;
|
|
9
|
-
classify?: Classify;
|
|
10
|
-
};
|
|
11
|
-
/**
|
|
12
|
-
* Raw, mode-agnostic outcome of one probe — the classifier's own status
|
|
13
|
-
* vocabulary (HUMAN/MACHINE/VOICEMAIL/IVR/UNKNOWN) with confidence, detector,
|
|
14
|
-
* and latency untouched. What this becomes on the wire (compact, native-
|
|
15
|
-
* vocabulary channel vars vs. the full range) is a policy decision made by
|
|
16
|
-
* `buildAmdVariables`, not here — this function only measures.
|
|
17
|
-
*/
|
|
18
|
-
type ProbeResult = {
|
|
19
|
-
kind: "classified";
|
|
20
|
-
status: AmdClassification["status"];
|
|
21
|
-
confidence: number;
|
|
22
|
-
detector: string;
|
|
23
|
-
latencyMs: number;
|
|
24
|
-
} | {
|
|
25
|
-
kind: "unknown";
|
|
26
|
-
cause: string;
|
|
27
|
-
latencyMs: number;
|
|
28
|
-
};
|
|
29
|
-
/**
|
|
30
|
-
* Runs Answering Machine Detection on the leading audio of one AGI/AudioSocket
|
|
31
|
-
* session. Always resolves and never throws: any model error, timeout, or
|
|
32
|
-
* empty capture yields `{ kind: "unknown" }` so the caller can always report
|
|
33
|
-
* *something* back over AGI. Confidence thresholding is deliberately not
|
|
34
|
-
* applied here — see `buildAmdVariables`.
|
|
35
|
-
*/
|
|
36
|
-
declare function runProbe(params: RunProbeParams): Promise<ProbeResult>;
|
|
37
|
-
export { ProbeResult, runProbe, RunProbeParams };
|