@fonoster/amd 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.
@@ -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,61 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- 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;
7
- /**
8
- * Copyright (C) 2025 by Fonoster Inc (https://fonoster.com)
9
- * http://github.com/fonoster/fonoster
10
- *
11
- * This file is part of Fonoster
12
- *
13
- * Licensed under the MIT License (the "License");
14
- * you may not use this file except in compliance with
15
- * the License. You may obtain a copy of the License at
16
- *
17
- * https://opensource.org/licenses/MIT
18
- *
19
- * Unless required by applicable law or agreed to in writing, software
20
- * distributed under the License is distributed on an "AS IS" BASIS,
21
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22
- * See the License for the specific language governing permissions and
23
- * limitations under the License.
24
- */
25
- const path_1 = require("path");
26
- const dotenv_1 = __importDefault(require("dotenv"));
27
- if (process.env.NODE_ENV === "development") {
28
- dotenv_1.default.config({ path: (0, path_1.join)(__dirname, "..", "..", "..", ".env") });
29
- }
30
- const e = process.env;
31
- // A malformed AMD_* number must not silently break the probe (NaN
32
- // thresholds disable the deadline / confidence gate), so fall back to the
33
- // default.
34
- const positiveInt = (raw, fallback) => {
35
- const n = parseInt(raw !== null && raw !== void 0 ? raw : "", 10);
36
- return Number.isFinite(n) && n > 0 ? n : fallback;
37
- };
38
- const unitFloat = (raw, fallback) => {
39
- const n = parseFloat(raw !== null && raw !== void 0 ? raw : "");
40
- return Number.isFinite(n) && n >= 0 && n <= 1 ? n : fallback;
41
- };
42
- // FastAGI listener. AgiServer binds all interfaces; there is no separate
43
- // bind-address knob.
44
- exports.AGI_PORT = positiveInt(e.AMD_AGI_PORT, 4573);
45
- // AudioSocket listener that receives the probed call audio.
46
- exports.AUDIOSOCKET_PORT = positiveInt(e.AMD_AUDIOSOCKET_PORT, 9092);
47
- exports.AUDIOSOCKET_BIND_ADDR = e.AMD_AUDIOSOCKET_BIND_ADDR || "0.0.0.0";
48
- // The host:port Asterisk is told to connect to for the AudioSocket leg — must
49
- // be reachable FROM Asterisk, which is why this is separate from the bind
50
- // address above (e.g. the compose service name, not 0.0.0.0).
51
- exports.AUDIOSOCKET_ADVERTISE_HOST = e.AMD_AUDIOSOCKET_ADVERTISE_HOST || "amd";
52
- // Leading audio to gather before classifying, in milliseconds.
53
- exports.PROBE_MS = positiveInt(e.AMD_PROBE_MS, 3000);
54
- // Hard deadline for one whole AGI session (buffering + classify + SET
55
- // VARIABLE). On expiry the verdict is NOTSURE/ML-TIMEOUT.
56
- exports.TIMEOUT_MS = positiveInt(e.AMD_TIMEOUT_MS, 4000);
57
- // Verdicts below this confidence are downgraded to NOTSURE.
58
- exports.MIN_CONFIDENCE = unitFloat(e.AMD_MIN_CONFIDENCE, 0.8);
59
- // Override the model directory (model.onnx / mel_filters.bin / meta.json).
60
- // Empty string -> the copy bundled with @fonoster/amd.
61
- 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 };
@@ -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
- }
@@ -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 };