@clawdbot/voice-call 0.1.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/CHANGELOG.md +20 -0
- package/README.md +107 -0
- package/index.ts +477 -0
- package/package.json +14 -0
- package/src/cli.ts +297 -0
- package/src/config.ts +355 -0
- package/src/core-bridge.ts +190 -0
- package/src/manager.ts +846 -0
- package/src/media-stream.ts +279 -0
- package/src/providers/base.ts +67 -0
- package/src/providers/index.ts +9 -0
- package/src/providers/mock.ts +168 -0
- package/src/providers/stt-openai-realtime.ts +303 -0
- package/src/providers/telnyx.ts +364 -0
- package/src/providers/tts-openai.ts +264 -0
- package/src/providers/twilio.ts +537 -0
- package/src/response-generator.ts +171 -0
- package/src/runtime.ts +194 -0
- package/src/tunnel.ts +330 -0
- package/src/types.ts +272 -0
- package/src/utils.ts +12 -0
- package/src/voice-mapping.ts +65 -0
- package/src/webhook-security.ts +197 -0
- package/src/webhook.ts +480 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Media Stream Handler
|
|
3
|
+
*
|
|
4
|
+
* Handles bidirectional audio streaming between Twilio and the AI services.
|
|
5
|
+
* - Receives mu-law audio from Twilio via WebSocket
|
|
6
|
+
* - Forwards to OpenAI Realtime STT for transcription
|
|
7
|
+
* - Sends TTS audio back to Twilio
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { IncomingMessage } from "node:http";
|
|
11
|
+
import type { Duplex } from "node:stream";
|
|
12
|
+
|
|
13
|
+
import { WebSocket, WebSocketServer } from "ws";
|
|
14
|
+
|
|
15
|
+
import type {
|
|
16
|
+
OpenAIRealtimeSTTProvider,
|
|
17
|
+
RealtimeSTTSession,
|
|
18
|
+
} from "./providers/stt-openai-realtime.js";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Configuration for the media stream handler.
|
|
22
|
+
*/
|
|
23
|
+
export interface MediaStreamConfig {
|
|
24
|
+
/** STT provider for transcription */
|
|
25
|
+
sttProvider: OpenAIRealtimeSTTProvider;
|
|
26
|
+
/** Callback when transcript is received */
|
|
27
|
+
onTranscript?: (callId: string, transcript: string) => void;
|
|
28
|
+
/** Callback for partial transcripts (streaming UI) */
|
|
29
|
+
onPartialTranscript?: (callId: string, partial: string) => void;
|
|
30
|
+
/** Callback when stream connects */
|
|
31
|
+
onConnect?: (callId: string, streamSid: string) => void;
|
|
32
|
+
/** Callback when stream disconnects */
|
|
33
|
+
onDisconnect?: (callId: string) => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Active media stream session.
|
|
38
|
+
*/
|
|
39
|
+
interface StreamSession {
|
|
40
|
+
callId: string;
|
|
41
|
+
streamSid: string;
|
|
42
|
+
ws: WebSocket;
|
|
43
|
+
sttSession: RealtimeSTTSession;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Manages WebSocket connections for Twilio media streams.
|
|
48
|
+
*/
|
|
49
|
+
export class MediaStreamHandler {
|
|
50
|
+
private wss: WebSocketServer | null = null;
|
|
51
|
+
private sessions = new Map<string, StreamSession>();
|
|
52
|
+
private config: MediaStreamConfig;
|
|
53
|
+
|
|
54
|
+
constructor(config: MediaStreamConfig) {
|
|
55
|
+
this.config = config;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Handle WebSocket upgrade for media stream connections.
|
|
60
|
+
*/
|
|
61
|
+
handleUpgrade(request: IncomingMessage, socket: Duplex, head: Buffer): void {
|
|
62
|
+
if (!this.wss) {
|
|
63
|
+
this.wss = new WebSocketServer({ noServer: true });
|
|
64
|
+
this.wss.on("connection", (ws, req) => this.handleConnection(ws, req));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
this.wss.handleUpgrade(request, socket, head, (ws) => {
|
|
68
|
+
this.wss?.emit("connection", ws, request);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Handle new WebSocket connection from Twilio.
|
|
74
|
+
*/
|
|
75
|
+
private async handleConnection(
|
|
76
|
+
ws: WebSocket,
|
|
77
|
+
_request: IncomingMessage,
|
|
78
|
+
): Promise<void> {
|
|
79
|
+
let session: StreamSession | null = null;
|
|
80
|
+
|
|
81
|
+
ws.on("message", async (data: Buffer) => {
|
|
82
|
+
try {
|
|
83
|
+
const message = JSON.parse(data.toString()) as TwilioMediaMessage;
|
|
84
|
+
|
|
85
|
+
switch (message.event) {
|
|
86
|
+
case "connected":
|
|
87
|
+
console.log("[MediaStream] Twilio connected");
|
|
88
|
+
break;
|
|
89
|
+
|
|
90
|
+
case "start":
|
|
91
|
+
session = await this.handleStart(ws, message);
|
|
92
|
+
break;
|
|
93
|
+
|
|
94
|
+
case "media":
|
|
95
|
+
if (session && message.media?.payload) {
|
|
96
|
+
// Forward audio to STT
|
|
97
|
+
const audioBuffer = Buffer.from(message.media.payload, "base64");
|
|
98
|
+
session.sttSession.sendAudio(audioBuffer);
|
|
99
|
+
}
|
|
100
|
+
break;
|
|
101
|
+
|
|
102
|
+
case "stop":
|
|
103
|
+
if (session) {
|
|
104
|
+
this.handleStop(session);
|
|
105
|
+
session = null;
|
|
106
|
+
}
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error("[MediaStream] Error processing message:", error);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
ws.on("close", () => {
|
|
115
|
+
if (session) {
|
|
116
|
+
this.handleStop(session);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
ws.on("error", (error) => {
|
|
121
|
+
console.error("[MediaStream] WebSocket error:", error);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Handle stream start event.
|
|
127
|
+
*/
|
|
128
|
+
private async handleStart(
|
|
129
|
+
ws: WebSocket,
|
|
130
|
+
message: TwilioMediaMessage,
|
|
131
|
+
): Promise<StreamSession> {
|
|
132
|
+
const streamSid = message.streamSid || "";
|
|
133
|
+
const callSid = message.start?.callSid || "";
|
|
134
|
+
|
|
135
|
+
console.log(
|
|
136
|
+
`[MediaStream] Stream started: ${streamSid} (call: ${callSid})`,
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// Create STT session
|
|
140
|
+
const sttSession = this.config.sttProvider.createSession();
|
|
141
|
+
|
|
142
|
+
// Set up transcript callbacks
|
|
143
|
+
sttSession.onPartial((partial) => {
|
|
144
|
+
this.config.onPartialTranscript?.(callSid, partial);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
sttSession.onTranscript((transcript) => {
|
|
148
|
+
this.config.onTranscript?.(callSid, transcript);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const session: StreamSession = {
|
|
152
|
+
callId: callSid,
|
|
153
|
+
streamSid,
|
|
154
|
+
ws,
|
|
155
|
+
sttSession,
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
this.sessions.set(streamSid, session);
|
|
159
|
+
|
|
160
|
+
// Notify connection BEFORE STT connect so TTS can work even if STT fails
|
|
161
|
+
this.config.onConnect?.(callSid, streamSid);
|
|
162
|
+
|
|
163
|
+
// Connect to OpenAI STT (non-blocking, log errors but don't fail the call)
|
|
164
|
+
sttSession.connect().catch((err) => {
|
|
165
|
+
console.warn(
|
|
166
|
+
`[MediaStream] STT connection failed (TTS still works):`,
|
|
167
|
+
err.message,
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
return session;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Handle stream stop event.
|
|
176
|
+
*/
|
|
177
|
+
private handleStop(session: StreamSession): void {
|
|
178
|
+
console.log(`[MediaStream] Stream stopped: ${session.streamSid}`);
|
|
179
|
+
|
|
180
|
+
session.sttSession.close();
|
|
181
|
+
this.sessions.delete(session.streamSid);
|
|
182
|
+
this.config.onDisconnect?.(session.callId);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Get an active session with an open WebSocket, or undefined if unavailable.
|
|
187
|
+
*/
|
|
188
|
+
private getOpenSession(streamSid: string): StreamSession | undefined {
|
|
189
|
+
const session = this.sessions.get(streamSid);
|
|
190
|
+
return session?.ws.readyState === WebSocket.OPEN ? session : undefined;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Send a message to a stream's WebSocket if available.
|
|
195
|
+
*/
|
|
196
|
+
private sendToStream(streamSid: string, message: unknown): void {
|
|
197
|
+
const session = this.getOpenSession(streamSid);
|
|
198
|
+
session?.ws.send(JSON.stringify(message));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Send audio to a specific stream (for TTS playback).
|
|
203
|
+
* Audio should be mu-law encoded at 8kHz mono.
|
|
204
|
+
*/
|
|
205
|
+
sendAudio(streamSid: string, muLawAudio: Buffer): void {
|
|
206
|
+
this.sendToStream(streamSid, {
|
|
207
|
+
event: "media",
|
|
208
|
+
streamSid,
|
|
209
|
+
media: { payload: muLawAudio.toString("base64") },
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Send a mark event to track audio playback position.
|
|
215
|
+
*/
|
|
216
|
+
sendMark(streamSid: string, name: string): void {
|
|
217
|
+
this.sendToStream(streamSid, {
|
|
218
|
+
event: "mark",
|
|
219
|
+
streamSid,
|
|
220
|
+
mark: { name },
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Clear audio buffer (interrupt playback).
|
|
226
|
+
*/
|
|
227
|
+
clearAudio(streamSid: string): void {
|
|
228
|
+
this.sendToStream(streamSid, { event: "clear", streamSid });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Get active session by call ID.
|
|
233
|
+
*/
|
|
234
|
+
getSessionByCallId(callId: string): StreamSession | undefined {
|
|
235
|
+
return [...this.sessions.values()].find(
|
|
236
|
+
(session) => session.callId === callId,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Close all sessions.
|
|
242
|
+
*/
|
|
243
|
+
closeAll(): void {
|
|
244
|
+
for (const session of this.sessions.values()) {
|
|
245
|
+
session.sttSession.close();
|
|
246
|
+
session.ws.close();
|
|
247
|
+
}
|
|
248
|
+
this.sessions.clear();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Twilio Media Stream message format.
|
|
254
|
+
*/
|
|
255
|
+
interface TwilioMediaMessage {
|
|
256
|
+
event: "connected" | "start" | "media" | "stop" | "mark" | "clear";
|
|
257
|
+
sequenceNumber?: string;
|
|
258
|
+
streamSid?: string;
|
|
259
|
+
start?: {
|
|
260
|
+
streamSid: string;
|
|
261
|
+
accountSid: string;
|
|
262
|
+
callSid: string;
|
|
263
|
+
tracks: string[];
|
|
264
|
+
mediaFormat: {
|
|
265
|
+
encoding: string;
|
|
266
|
+
sampleRate: number;
|
|
267
|
+
channels: number;
|
|
268
|
+
};
|
|
269
|
+
};
|
|
270
|
+
media?: {
|
|
271
|
+
track?: string;
|
|
272
|
+
chunk?: string;
|
|
273
|
+
timestamp?: string;
|
|
274
|
+
payload?: string;
|
|
275
|
+
};
|
|
276
|
+
mark?: {
|
|
277
|
+
name: string;
|
|
278
|
+
};
|
|
279
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
HangupCallInput,
|
|
3
|
+
InitiateCallInput,
|
|
4
|
+
InitiateCallResult,
|
|
5
|
+
PlayTtsInput,
|
|
6
|
+
ProviderName,
|
|
7
|
+
ProviderWebhookParseResult,
|
|
8
|
+
StartListeningInput,
|
|
9
|
+
StopListeningInput,
|
|
10
|
+
WebhookContext,
|
|
11
|
+
WebhookVerificationResult,
|
|
12
|
+
} from "../types.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Abstract base interface for voice call providers.
|
|
16
|
+
*
|
|
17
|
+
* Each provider (Telnyx, Twilio, etc.) implements this interface to provide
|
|
18
|
+
* a consistent API for the call manager.
|
|
19
|
+
*
|
|
20
|
+
* Responsibilities:
|
|
21
|
+
* - Webhook verification and event parsing
|
|
22
|
+
* - Outbound call initiation and hangup
|
|
23
|
+
* - Media control (TTS playback, STT listening)
|
|
24
|
+
*/
|
|
25
|
+
export interface VoiceCallProvider {
|
|
26
|
+
/** Provider identifier */
|
|
27
|
+
readonly name: ProviderName;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Verify webhook signature/HMAC before processing.
|
|
31
|
+
* Must be called before parseWebhookEvent.
|
|
32
|
+
*/
|
|
33
|
+
verifyWebhook(ctx: WebhookContext): WebhookVerificationResult;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Parse provider-specific webhook payload into normalized events.
|
|
37
|
+
* Returns events and optional response to send back to provider.
|
|
38
|
+
*/
|
|
39
|
+
parseWebhookEvent(ctx: WebhookContext): ProviderWebhookParseResult;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Initiate an outbound call.
|
|
43
|
+
* @returns Provider call ID and status
|
|
44
|
+
*/
|
|
45
|
+
initiateCall(input: InitiateCallInput): Promise<InitiateCallResult>;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Hang up an active call.
|
|
49
|
+
*/
|
|
50
|
+
hangupCall(input: HangupCallInput): Promise<void>;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Play TTS audio to the caller.
|
|
54
|
+
* The provider should handle streaming if supported.
|
|
55
|
+
*/
|
|
56
|
+
playTts(input: PlayTtsInput): Promise<void>;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Start listening for user speech (activate STT).
|
|
60
|
+
*/
|
|
61
|
+
startListening(input: StartListeningInput): Promise<void>;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Stop listening for user speech (deactivate STT).
|
|
65
|
+
*/
|
|
66
|
+
stopListening(input: StopListeningInput): Promise<void>;
|
|
67
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type { VoiceCallProvider } from "./base.js";
|
|
2
|
+
export { MockProvider } from "./mock.js";
|
|
3
|
+
export {
|
|
4
|
+
OpenAIRealtimeSTTProvider,
|
|
5
|
+
type RealtimeSTTConfig,
|
|
6
|
+
type RealtimeSTTSession,
|
|
7
|
+
} from "./stt-openai-realtime.js";
|
|
8
|
+
export { TelnyxProvider } from "./telnyx.js";
|
|
9
|
+
export { TwilioProvider } from "./twilio.js";
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
EndReason,
|
|
5
|
+
HangupCallInput,
|
|
6
|
+
InitiateCallInput,
|
|
7
|
+
InitiateCallResult,
|
|
8
|
+
NormalizedEvent,
|
|
9
|
+
PlayTtsInput,
|
|
10
|
+
ProviderWebhookParseResult,
|
|
11
|
+
StartListeningInput,
|
|
12
|
+
StopListeningInput,
|
|
13
|
+
WebhookContext,
|
|
14
|
+
WebhookVerificationResult,
|
|
15
|
+
} from "../types.js";
|
|
16
|
+
import type { VoiceCallProvider } from "./base.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Mock voice call provider for local testing.
|
|
20
|
+
*
|
|
21
|
+
* Events are driven via webhook POST with JSON body:
|
|
22
|
+
* - { events: NormalizedEvent[] } for bulk events
|
|
23
|
+
* - { event: NormalizedEvent } for single event
|
|
24
|
+
*/
|
|
25
|
+
export class MockProvider implements VoiceCallProvider {
|
|
26
|
+
readonly name = "mock" as const;
|
|
27
|
+
|
|
28
|
+
verifyWebhook(_ctx: WebhookContext): WebhookVerificationResult {
|
|
29
|
+
return { ok: true };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
parseWebhookEvent(ctx: WebhookContext): ProviderWebhookParseResult {
|
|
33
|
+
try {
|
|
34
|
+
const payload = JSON.parse(ctx.rawBody);
|
|
35
|
+
const events: NormalizedEvent[] = [];
|
|
36
|
+
|
|
37
|
+
if (Array.isArray(payload.events)) {
|
|
38
|
+
for (const evt of payload.events) {
|
|
39
|
+
const normalized = this.normalizeEvent(evt);
|
|
40
|
+
if (normalized) events.push(normalized);
|
|
41
|
+
}
|
|
42
|
+
} else if (payload.event) {
|
|
43
|
+
const normalized = this.normalizeEvent(payload.event);
|
|
44
|
+
if (normalized) events.push(normalized);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { events, statusCode: 200 };
|
|
48
|
+
} catch {
|
|
49
|
+
return { events: [], statusCode: 400 };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private normalizeEvent(
|
|
54
|
+
evt: Partial<NormalizedEvent>,
|
|
55
|
+
): NormalizedEvent | null {
|
|
56
|
+
if (!evt.type || !evt.callId) return null;
|
|
57
|
+
|
|
58
|
+
const base = {
|
|
59
|
+
id: evt.id || crypto.randomUUID(),
|
|
60
|
+
callId: evt.callId,
|
|
61
|
+
providerCallId: evt.providerCallId,
|
|
62
|
+
timestamp: evt.timestamp || Date.now(),
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
switch (evt.type) {
|
|
66
|
+
case "call.initiated":
|
|
67
|
+
case "call.ringing":
|
|
68
|
+
case "call.answered":
|
|
69
|
+
case "call.active":
|
|
70
|
+
return { ...base, type: evt.type };
|
|
71
|
+
|
|
72
|
+
case "call.speaking": {
|
|
73
|
+
const payload = evt as Partial<NormalizedEvent & { text?: string }>;
|
|
74
|
+
return {
|
|
75
|
+
...base,
|
|
76
|
+
type: evt.type,
|
|
77
|
+
text: payload.text || "",
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
case "call.speech": {
|
|
82
|
+
const payload = evt as Partial<
|
|
83
|
+
NormalizedEvent & {
|
|
84
|
+
transcript?: string;
|
|
85
|
+
isFinal?: boolean;
|
|
86
|
+
confidence?: number;
|
|
87
|
+
}
|
|
88
|
+
>;
|
|
89
|
+
return {
|
|
90
|
+
...base,
|
|
91
|
+
type: evt.type,
|
|
92
|
+
transcript: payload.transcript || "",
|
|
93
|
+
isFinal: payload.isFinal ?? true,
|
|
94
|
+
confidence: payload.confidence,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
case "call.silence": {
|
|
99
|
+
const payload = evt as Partial<
|
|
100
|
+
NormalizedEvent & { durationMs?: number }
|
|
101
|
+
>;
|
|
102
|
+
return {
|
|
103
|
+
...base,
|
|
104
|
+
type: evt.type,
|
|
105
|
+
durationMs: payload.durationMs || 0,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
case "call.dtmf": {
|
|
110
|
+
const payload = evt as Partial<NormalizedEvent & { digits?: string }>;
|
|
111
|
+
return {
|
|
112
|
+
...base,
|
|
113
|
+
type: evt.type,
|
|
114
|
+
digits: payload.digits || "",
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
case "call.ended": {
|
|
119
|
+
const payload = evt as Partial<
|
|
120
|
+
NormalizedEvent & { reason?: EndReason }
|
|
121
|
+
>;
|
|
122
|
+
return {
|
|
123
|
+
...base,
|
|
124
|
+
type: evt.type,
|
|
125
|
+
reason: payload.reason || "completed",
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
case "call.error": {
|
|
130
|
+
const payload = evt as Partial<
|
|
131
|
+
NormalizedEvent & { error?: string; retryable?: boolean }
|
|
132
|
+
>;
|
|
133
|
+
return {
|
|
134
|
+
...base,
|
|
135
|
+
type: evt.type,
|
|
136
|
+
error: payload.error || "unknown error",
|
|
137
|
+
retryable: payload.retryable,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
default:
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async initiateCall(input: InitiateCallInput): Promise<InitiateCallResult> {
|
|
147
|
+
return {
|
|
148
|
+
providerCallId: `mock-${input.callId}`,
|
|
149
|
+
status: "initiated",
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async hangupCall(_input: HangupCallInput): Promise<void> {
|
|
154
|
+
// No-op for mock
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async playTts(_input: PlayTtsInput): Promise<void> {
|
|
158
|
+
// No-op for mock
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async startListening(_input: StartListeningInput): Promise<void> {
|
|
162
|
+
// No-op for mock
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async stopListening(_input: StopListeningInput): Promise<void> {
|
|
166
|
+
// No-op for mock
|
|
167
|
+
}
|
|
168
|
+
}
|