@fonoster/apiserver 0.15.21 → 0.16.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.
Files changed (38) hide show
  1. package/dist/applications/createGetFnUtil.d.ts +3 -3
  2. package/dist/core/httpBridge.js +0 -26
  3. package/dist/voice/VoiceDispatcher.js +1 -0
  4. package/dist/voice/client/AudioSocketHandler.d.ts +35 -0
  5. package/dist/voice/client/AudioSocketHandler.js +68 -0
  6. package/dist/voice/client/AuthorizationHandler.d.ts +13 -0
  7. package/dist/voice/client/AuthorizationHandler.js +78 -0
  8. package/dist/voice/client/ExternalMediaHandler.d.ts +32 -0
  9. package/dist/voice/client/ExternalMediaHandler.js +59 -0
  10. package/dist/voice/client/GrpcClientHandler.d.ts +36 -0
  11. package/dist/voice/client/GrpcClientHandler.js +101 -0
  12. package/dist/voice/client/SpeechHandler.d.ts +60 -0
  13. package/dist/voice/client/SpeechHandler.js +116 -0
  14. package/dist/voice/{VoiceClientImpl.d.ts → client/VoiceClientImpl.d.ts} +17 -17
  15. package/dist/voice/client/VoiceClientImpl.js +158 -0
  16. package/dist/voice/client/index.d.ts +24 -0
  17. package/dist/voice/client/index.js +40 -0
  18. package/dist/voice/connectToAri.js +1 -1
  19. package/dist/voice/createCreateVoiceClient.d.ts +1 -1
  20. package/dist/voice/createCreateVoiceClient.js +3 -3
  21. package/dist/voice/handlers/createSayHandler.js +2 -15
  22. package/dist/voice/handlers/createStopSayHandler.d.ts +3 -0
  23. package/dist/voice/handlers/createStopSayHandler.js +34 -0
  24. package/dist/voice/handlers/index.d.ts +1 -0
  25. package/dist/voice/handlers/index.js +1 -0
  26. package/dist/voice/tts/Azure.js +1 -1
  27. package/dist/voice/tts/Deepgram.d.ts +1 -1
  28. package/dist/voice/tts/Deepgram.js +1 -1
  29. package/dist/voice/tts/ElevenLabs.js +45 -13
  30. package/dist/voice/tts/Google.d.ts +1 -1
  31. package/dist/voice/tts/Google.js +1 -1
  32. package/dist/voice/tts/utils/convertUlawToPCM16.d.ts +26 -0
  33. package/dist/voice/tts/utils/convertUlawToPCM16.js +40 -0
  34. package/dist/voice/tts/utils/index.d.ts +23 -0
  35. package/dist/voice/tts/utils/index.js +39 -0
  36. package/dist/voice/types/voice.d.ts +1 -0
  37. package/package.json +11 -10
  38. package/dist/voice/VoiceClientImpl.js +0 -281
@@ -5,22 +5,22 @@ declare function createGetFnUtil(prisma: Prisma): (ref: string) => Promise<{
5
5
  };
6
6
  textToSpeech: {
7
7
  ref: string;
8
- credentials: string | null;
9
8
  config: import("@prisma/client/runtime/library").JsonValue;
9
+ credentials: string | null;
10
10
  productRef: string;
11
11
  applicationRef: string;
12
12
  };
13
13
  speechToText: {
14
14
  ref: string;
15
- credentials: string | null;
16
15
  config: import("@prisma/client/runtime/library").JsonValue;
16
+ credentials: string | null;
17
17
  productRef: string;
18
18
  applicationRef: string;
19
19
  };
20
20
  intelligence: {
21
21
  ref: string;
22
- credentials: string | null;
23
22
  config: import("@prisma/client/runtime/library").JsonValue;
23
+ credentials: string | null;
24
24
  productRef: string;
25
25
  applicationRef: string;
26
26
  };
@@ -38,36 +38,10 @@ const logger_1 = require("@fonoster/logger");
38
38
  const express_1 = __importDefault(require("express"));
39
39
  const envs_1 = require("../envs");
40
40
  const logger = (0, logger_1.getLogger)({ service: "apiserver", filePath: __filename });
41
- const CONTENT_TYPE = "audio/L16;rate=16000;channels=1";
42
41
  function httpBridge(identityConfig, params) {
43
42
  const { port } = params;
44
43
  const app = (0, express_1.default)();
45
44
  const streamMap = new Map();
46
- app.get("/api/sounds/:id", (req, res) => {
47
- const idWithoutExtension = req.params.id.split(".")[0];
48
- const stream = streamMap.get(idWithoutExtension);
49
- if (!stream) {
50
- res.status(404).send(`Stream not found for id: ${req.params.id}`);
51
- return;
52
- }
53
- res.setHeader("content-type", CONTENT_TYPE);
54
- stream.on("error", (error) => {
55
- logger.error(`error reading file: ${error.message}`);
56
- if (!res.headersSent) {
57
- res.status(500).send("Error reading file!");
58
- }
59
- else {
60
- res.end();
61
- }
62
- });
63
- stream.on("end", () => {
64
- res.end();
65
- });
66
- stream.on("close", () => {
67
- res.end();
68
- });
69
- stream.pipe(res);
70
- });
71
45
  app.get("/api/identity/accept-invite", (req, res) => __awaiter(this, void 0, void 0, function* () {
72
46
  try {
73
47
  yield (0, identity_1.createUpdateMembershipStatus)(identityConfig)(req.query.token);
@@ -78,6 +78,7 @@ class VoiceDispatcher {
78
78
  vc.on(common_1.StreamContent.SAY_REQUEST, (0, handlers_1.createSayHandler)(ari, vc).bind(this));
79
79
  vc.on(common_1.StreamContent.GATHER_REQUEST, (0, handlers_1.createGatherHandler)(vc).bind(this));
80
80
  vc.on(common_1.StreamContent.DIAL_REQUEST, (0, handlers_1.createDialHandler)(ari, vc).bind(this));
81
+ vc.on(common_1.StreamContent.STOP_SAY_REQUEST, (0, handlers_1.createStopSayHandler)(vc).bind(this));
81
82
  vc.on(common_1.StreamContent.PLAYBACK_CONTROL_REQUEST, (0, handlers_1.createPlaybackControlHandler)(ari, vc).bind(this));
82
83
  vc.on(common_1.StreamContent.START_STREAM_GATHER_REQUEST, (0, handlers_1.createStreamGatherHandler)(vc).bind(this));
83
84
  vc.on(common_1.StreamContent.STOP_STREAM_GATHER_REQUEST, () => {
@@ -0,0 +1,35 @@
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 { Stream } from "stream";
20
+ import { VoiceClientConfig } from "@fonoster/common";
21
+ import { AudioStream } from "@fonoster/streams";
22
+ declare class AudioSocketHandler {
23
+ private audioSocket;
24
+ private audioStream;
25
+ private transcriptionsStream;
26
+ private config;
27
+ constructor(params: {
28
+ transcriptionsStream: Stream;
29
+ config: VoiceClientConfig;
30
+ });
31
+ setupAudioSocket(port: number): Promise<void>;
32
+ getAudioStream(): AudioStream;
33
+ close(): void;
34
+ }
35
+ export { AudioSocketHandler };
@@ -0,0 +1,68 @@
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.AudioSocketHandler = void 0;
13
+ const logger_1 = require("@fonoster/logger");
14
+ const streams_1 = require("@fonoster/streams");
15
+ const transcribeOnConnection_1 = require("../transcribeOnConnection");
16
+ const logger = (0, logger_1.getLogger)({ service: "apiserver", filePath: __filename });
17
+ class AudioSocketHandler {
18
+ constructor(params) {
19
+ this.transcriptionsStream = params.transcriptionsStream;
20
+ this.config = params.config;
21
+ }
22
+ setupAudioSocket(port) {
23
+ return __awaiter(this, void 0, void 0, function* () {
24
+ return new Promise((resolve) => {
25
+ logger.verbose("creating audio socket", { port });
26
+ this.audioSocket = new streams_1.AudioSocket();
27
+ this.audioSocket.onConnection((req, res) => __awaiter(this, void 0, void 0, function* () {
28
+ logger.verbose("audio socket connection received", {
29
+ ref: req.ref,
30
+ sessionRef: this.config.sessionRef
31
+ });
32
+ (0, transcribeOnConnection_1.transcribeOnConnection)(this.transcriptionsStream)(req, res);
33
+ res.onClose(() => {
34
+ logger.verbose("session audio stream closed", {
35
+ sessionRef: this.config.sessionRef
36
+ });
37
+ });
38
+ res.onError((err) => {
39
+ logger.error("session audio stream error", {
40
+ error: err,
41
+ sessionRef: this.config.sessionRef
42
+ });
43
+ });
44
+ this.audioStream = res;
45
+ resolve();
46
+ }));
47
+ this.audioSocket.listen(port, () => {
48
+ logger.verbose("audio socket listening", {
49
+ port,
50
+ appRef: this.config.appRef
51
+ });
52
+ });
53
+ });
54
+ });
55
+ }
56
+ getAudioStream() {
57
+ return this.audioStream;
58
+ }
59
+ close() {
60
+ try {
61
+ this.audioSocket.close();
62
+ }
63
+ catch (e) {
64
+ // Ignore errors on close
65
+ }
66
+ }
67
+ }
68
+ exports.AudioSocketHandler = AudioSocketHandler;
@@ -0,0 +1,13 @@
1
+ import { VoiceClientConfig } from "@fonoster/common";
2
+ import { Client } from "ari-client";
3
+ declare class AuthorizationHandler {
4
+ private config;
5
+ private ari;
6
+ constructor(params: {
7
+ config: VoiceClientConfig;
8
+ ari: Client;
9
+ });
10
+ checkAuthorization(): Promise<boolean>;
11
+ private handleUnauthorizedSession;
12
+ }
13
+ export { AuthorizationHandler };
@@ -0,0 +1,78 @@
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.AuthorizationHandler = void 0;
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 authz_1 = require("@fonoster/authz");
32
+ const logger_1 = require("@fonoster/logger");
33
+ const envs_1 = require("../../envs");
34
+ const logger = (0, logger_1.getLogger)({ service: "apiserver", filePath: __filename });
35
+ class AuthorizationHandler {
36
+ constructor(params) {
37
+ this.config = params.config;
38
+ this.ari = params.ari;
39
+ }
40
+ checkAuthorization() {
41
+ return __awaiter(this, void 0, void 0, function* () {
42
+ if (!envs_1.AUTHZ_SERVICE_ENABLED) {
43
+ return true;
44
+ }
45
+ const { sessionRef: channelId, accessKeyId } = this.config;
46
+ try {
47
+ const authz = new authz_1.AuthzClient(`${envs_1.AUTHZ_SERVICE_HOST}:${envs_1.AUTHZ_SERVICE_PORT}`);
48
+ const authorized = yield authz.checkSessionAuthorized({ accessKeyId });
49
+ if (!authorized) {
50
+ logger.verbose("rejected unauthorized session", { channelId });
51
+ yield this.handleUnauthorizedSession();
52
+ return false;
53
+ }
54
+ return true;
55
+ }
56
+ catch (e) {
57
+ logger.error("authz service error", e);
58
+ yield this.handleUnauthorizedSession();
59
+ return false;
60
+ }
61
+ });
62
+ }
63
+ handleUnauthorizedSession() {
64
+ return __awaiter(this, void 0, void 0, function* () {
65
+ const { sessionRef: channelId } = this.config;
66
+ try {
67
+ yield this.ari.channels.answer({ channelId });
68
+ yield this.ari.channels.play({ channelId, media: "sound:unavailable" });
69
+ yield new Promise((resolve) => setTimeout(resolve, 2000));
70
+ yield this.ari.channels.hangup({ channelId });
71
+ }
72
+ catch (e) {
73
+ logger.error("error handling unauthorized session", e);
74
+ }
75
+ });
76
+ }
77
+ }
78
+ exports.AuthorizationHandler = AuthorizationHandler;
@@ -0,0 +1,32 @@
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 { VoiceClientConfig } from "@fonoster/common";
20
+ import { Bridge, Client } from "ari-client";
21
+ declare class ExternalMediaHandler {
22
+ private ari;
23
+ private bridge;
24
+ private config;
25
+ constructor(params: {
26
+ ari: Client;
27
+ config: VoiceClientConfig;
28
+ });
29
+ setupExternalMedia(port: number): Promise<void>;
30
+ getBridge(): Bridge;
31
+ }
32
+ export { ExternalMediaHandler };
@@ -0,0 +1,59 @@
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.ExternalMediaHandler = void 0;
13
+ const logger_1 = require("@fonoster/logger");
14
+ const types_1 = require("../types");
15
+ const createExternalMediaConfig_1 = require("../utils/createExternalMediaConfig");
16
+ const logger = (0, logger_1.getLogger)({ service: "apiserver", filePath: __filename });
17
+ class ExternalMediaHandler {
18
+ constructor(params) {
19
+ this.ari = params.ari;
20
+ this.config = params.config;
21
+ }
22
+ setupExternalMedia(port) {
23
+ return __awaiter(this, void 0, void 0, function* () {
24
+ const bridge = this.ari.Bridge();
25
+ const channel = this.ari.Channel();
26
+ yield bridge.create({ type: "mixing" });
27
+ logger.verbose("creating external media config", {
28
+ port,
29
+ sessionRef: this.config.sessionRef,
30
+ bridgeId: bridge.id
31
+ });
32
+ channel.externalMedia((0, createExternalMediaConfig_1.createExternalMediaConfig)(port));
33
+ channel.once(types_1.AriEvent.STASIS_START, (_, channel) => __awaiter(this, void 0, void 0, function* () {
34
+ bridge.addChannel({ channel: [this.config.sessionRef, channel.id] });
35
+ logger.verbose("added channel to bridge", {
36
+ sessionRef: this.config.sessionRef,
37
+ channelId: channel.id
38
+ });
39
+ }));
40
+ channel.once("ChannelLeftBridge", () => __awaiter(this, void 0, void 0, function* () {
41
+ logger.verbose("channel left bridge", {
42
+ sessionRef: this.config.sessionRef,
43
+ bridgeId: bridge.id
44
+ });
45
+ try {
46
+ yield bridge.destroy();
47
+ }
48
+ catch (e) {
49
+ // We can only try
50
+ }
51
+ }));
52
+ this.bridge = bridge;
53
+ });
54
+ }
55
+ getBridge() {
56
+ return this.bridge;
57
+ }
58
+ }
59
+ exports.ExternalMediaHandler = ExternalMediaHandler;
@@ -0,0 +1,36 @@
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 { Stream } from "stream";
20
+ import { VoiceClientConfig, VoiceIn, VoiceSessionStreamClient } from "@fonoster/common";
21
+ declare class GrpcClientHandler {
22
+ private config;
23
+ private verbsStream;
24
+ private grpcClient;
25
+ private voice;
26
+ constructor(params: {
27
+ config: VoiceClientConfig;
28
+ verbsStream: Stream;
29
+ });
30
+ setupGrpcClient(): Promise<void>;
31
+ private setupEventHandlers;
32
+ getVoiceStream(): VoiceSessionStreamClient;
33
+ sendResponse(response: VoiceIn): void;
34
+ close(): void;
35
+ }
36
+ export { GrpcClientHandler };
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
36
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
37
+ return new (P || (P = Promise))(function (resolve, reject) {
38
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
39
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
40
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
41
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
42
+ });
43
+ };
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.GrpcClientHandler = void 0;
46
+ const common_1 = require("@fonoster/common");
47
+ const logger_1 = require("@fonoster/logger");
48
+ const grpc = __importStar(require("@grpc/grpc-js"));
49
+ const VoiceServiceClientConstructor_1 = require("../utils/VoiceServiceClientConstructor");
50
+ const logger = (0, logger_1.getLogger)({ service: "apiserver", filePath: __filename });
51
+ class GrpcClientHandler {
52
+ constructor(params) {
53
+ this.config = params.config;
54
+ this.verbsStream = params.verbsStream;
55
+ }
56
+ setupGrpcClient() {
57
+ return __awaiter(this, void 0, void 0, function* () {
58
+ this.grpcClient = new VoiceServiceClientConstructor_1.VoiceServiceClientConstructor(this.config.endpoint, grpc.credentials.createInsecure());
59
+ const metadata = new grpc.Metadata();
60
+ metadata.add("accessKeyId", this.config.accessKeyId);
61
+ metadata.add("token", this.config.sessionToken);
62
+ this.voice = this.grpcClient.createSession(metadata);
63
+ this.setupEventHandlers();
64
+ // Initialize the session
65
+ this.voice.write({ request: this.config });
66
+ });
67
+ }
68
+ setupEventHandlers() {
69
+ this.voice.on(common_1.StreamEvent.DATA, (data) => {
70
+ this.verbsStream.emit(data.content, data);
71
+ });
72
+ this.voice.on(common_1.StreamEvent.ERROR, (error) => {
73
+ if (error.code === grpc.status.UNAVAILABLE) {
74
+ logger.error(`voice server not available at "${this.config.endpoint}"`);
75
+ return;
76
+ }
77
+ logger.error(`grpc stream error: ${error.message}`);
78
+ });
79
+ }
80
+ getVoiceStream() {
81
+ return this.voice;
82
+ }
83
+ sendResponse(response) {
84
+ try {
85
+ this.voice.write(response);
86
+ }
87
+ catch (error) {
88
+ logger.error(`error sending response: ${error.message}`);
89
+ }
90
+ }
91
+ close() {
92
+ try {
93
+ this.voice.end();
94
+ this.grpcClient.close();
95
+ }
96
+ catch (e) {
97
+ // Ignore errors on close
98
+ }
99
+ }
100
+ }
101
+ exports.GrpcClientHandler = GrpcClientHandler;
@@ -0,0 +1,60 @@
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 { Stream } from "stream";
20
+ import { SayOptions } from "@fonoster/common";
21
+ import { AudioStream } from "@fonoster/streams";
22
+ import { Client } from "ari-client";
23
+ import { SpeechResult } from "../stt/types";
24
+ import { SpeechToText, TextToSpeech } from "../types";
25
+ declare class SpeechHandler {
26
+ private tts;
27
+ private stt;
28
+ private ari;
29
+ private transcriptionsStream;
30
+ private audioStream;
31
+ private sessionRef;
32
+ constructor(params: {
33
+ tts: TextToSpeech;
34
+ stt: SpeechToText;
35
+ ari: Client;
36
+ transcriptionsStream: Stream;
37
+ audioStream: AudioStream;
38
+ sessionRef: string;
39
+ });
40
+ synthesize(text: string, options: SayOptions): Promise<string>;
41
+ stopSynthesis(): Promise<void>;
42
+ transcribe(): Promise<SpeechResult>;
43
+ startSpeechGather(callback: (stream: {
44
+ speech: string;
45
+ responseTime: number;
46
+ }) => void): void;
47
+ startDtmfGather(callback: (event: {
48
+ digit: string;
49
+ }) => void): Promise<void>;
50
+ waitForDtmf(params: {
51
+ finishOnKey: string;
52
+ maxDigits: number;
53
+ timeout: number;
54
+ onDigitReceived: () => void;
55
+ }): Promise<{
56
+ digits: string;
57
+ }>;
58
+ stopStreamGather(): void;
59
+ }
60
+ export { SpeechHandler };
@@ -0,0 +1,116 @@
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.SpeechHandler = void 0;
13
+ const logger_1 = require("@fonoster/logger");
14
+ const types_1 = require("../types");
15
+ const logger = (0, logger_1.getLogger)({ service: "apiserver", filePath: __filename });
16
+ class SpeechHandler {
17
+ constructor(params) {
18
+ this.tts = params.tts;
19
+ this.stt = params.stt;
20
+ this.ari = params.ari;
21
+ this.transcriptionsStream = params.transcriptionsStream;
22
+ this.audioStream = params.audioStream;
23
+ this.sessionRef = params.sessionRef;
24
+ }
25
+ synthesize(text, options) {
26
+ return __awaiter(this, void 0, void 0, function* () {
27
+ const { ref, stream } = this.tts.synthesize(text, options);
28
+ logger.verbose("starting audio synthesis", { ref });
29
+ try {
30
+ // Stop any active stream
31
+ this.audioStream.stop();
32
+ yield this.audioStream.playStream(stream);
33
+ }
34
+ catch (error) {
35
+ logger.error(`stream error for ref ${ref}: ${error.message}`, {
36
+ errorDetails: error.stack || "No stack trace"
37
+ });
38
+ }
39
+ return ref;
40
+ });
41
+ }
42
+ stopSynthesis() {
43
+ return __awaiter(this, void 0, void 0, function* () {
44
+ this.audioStream.stop();
45
+ });
46
+ }
47
+ transcribe() {
48
+ return __awaiter(this, void 0, void 0, function* () {
49
+ try {
50
+ return yield this.stt.transcribe(this.transcriptionsStream);
51
+ }
52
+ catch (e) {
53
+ logger.warn("transcription error", e);
54
+ return {};
55
+ }
56
+ });
57
+ }
58
+ startSpeechGather(callback) {
59
+ const out = this.stt.streamTranscribe(this.transcriptionsStream);
60
+ out.on("data", callback);
61
+ out.on("error", (error) => __awaiter(this, void 0, void 0, function* () {
62
+ logger.error("speech recognition error", { error });
63
+ yield this.ari.channels.hangup({ channelId: this.sessionRef });
64
+ }));
65
+ }
66
+ startDtmfGather(callback) {
67
+ return __awaiter(this, void 0, void 0, function* () {
68
+ const channel = yield this.ari.channels.get({ channelId: this.sessionRef });
69
+ channel.on(types_1.AriEvent.CHANNEL_DTMF_RECEIVED, (event) => {
70
+ const { digit } = event;
71
+ callback({ digit });
72
+ });
73
+ });
74
+ }
75
+ // Returns a promise that resolves when DTMF collection is complete
76
+ waitForDtmf(params) {
77
+ return __awaiter(this, void 0, void 0, function* () {
78
+ const { onDigitReceived, finishOnKey, maxDigits, timeout } = params;
79
+ let result = "";
80
+ let timeoutId = null;
81
+ const channel = yield this.ari.channels.get({ channelId: this.sessionRef });
82
+ return new Promise((resolve) => {
83
+ const resetTimer = () => {
84
+ if (timeoutId) {
85
+ clearTimeout(timeoutId);
86
+ }
87
+ timeoutId = setTimeout(() => {
88
+ channel.removeListener(types_1.AriEvent.CHANNEL_DTMF_RECEIVED, dtmfListener);
89
+ resolve({ digits: result });
90
+ }, timeout);
91
+ };
92
+ const dtmfListener = (event) => {
93
+ const { digit } = event;
94
+ // Stops the global timeout
95
+ onDigitReceived();
96
+ resetTimer();
97
+ if (digit !== finishOnKey) {
98
+ result += digit;
99
+ }
100
+ if (result.length >= maxDigits || digit === finishOnKey) {
101
+ clearTimeout(timeoutId);
102
+ channel.removeListener(types_1.AriEvent.CHANNEL_DTMF_RECEIVED, dtmfListener);
103
+ resolve({ digits: result });
104
+ }
105
+ };
106
+ channel.on(types_1.AriEvent.CHANNEL_DTMF_RECEIVED, dtmfListener);
107
+ resetTimer(); // Start the initial timeout
108
+ });
109
+ });
110
+ }
111
+ // Placeholder for future implementation
112
+ stopStreamGather() {
113
+ throw new Error("Method 'stopStreamGather' not implemented.");
114
+ }
115
+ }
116
+ exports.SpeechHandler = SpeechHandler;