Pepguy commited on
Commit
d988ffd
·
verified ·
1 Parent(s): f87d762

Update app.js

Browse files
Files changed (1) hide show
  1. app.js +283 -245
app.js CHANGED
@@ -1,263 +1,301 @@
1
- import express from "express"; import { createServer } from "http";
 
2
  import { WebSocketServer } from "ws";
3
 
4
- const app = express(); const server = createServer(app); const wss = new
5
- WebSocketServer({ server, path: "/audio-stream" });
6
-
7
- app.use(express.static("public", { index: false })); app.use(express.json());
8
-
9
- const PORT = process.env.PORT || 7860; const TELNYX_API_KEY =
10
- process.env.TELNYX_API_KEY; const TELNYX_CONNECTION_ID =
11
- process.env.TELNYX_CONNECTION_ID; const TELNYX_FROM_NUMBER =
12
- process.env.TELNYX_FROM_NUMBER; const AI_SERVER_URL = process.env.AI_SERVER_URL;
13
- const SIP_USERNAME = process.env.TELNYX_SIP_USERNAME; // Add the Alpha Sender variable (falls back to your phone number if not set in ENV) const
14
- TELNYX_ALPHA_SENDER = process.env.TELNYX_ALPHA_SENDER || TELNYX_FROM_NUMBER; //Add this near the top where your other ENV variables are: const
15
- TELNYX_MESSAGING_PROFILE_ID = process.env.TELNYX_MESSAGING_PROFILE_ID;
16
-
17
- const ACTIVE_VOICE = "Enceladus_3"; const log = (tag, ...args) =>
18
- console.log([${new Date().toISOString()}] [${tag}], ...args);
19
-
20
- let pendingCalls = new Map(); let activeLeadIds = new Map();
21
-
22
- // 10-Second Debounce Buffer for Telnyx Incoming Webhook Spam const
23
- incomingCallDebounce = new Map();
24
-
25
- // ── MU-LAW VAD ──
26
- const MU_LAW_TABLE = new Int16Array(256); for (let i = 0; i
27
- < 256; i++) { const sign = (i & 0x80) ? -1 : 1; const exponent = (~i >> 4)
28
- & 0x07; const mantissa = ~i & 0x0f; const sample = ((mantissa << 3) + 132) <<
29
- exponent; MU_LAW_TABLE[i] = sign * (sample - 132); } function
30
- decodeMuLaw(buffer) { const out = new Int16Array(buffer.length); for (let i = 0;
31
- i < buffer.length; i++) out[i] = MU_LAW_TABLE[buffer[i]]; return out; } function
32
- calculateRMS(pcm) { let sum = 0; for (let i = 0; i < pcm.length; i++) sum +=
33
- pcm[i] * pcm[i]; return Math.sqrt(sum / pcm.length); }
34
-
35
- const sendTelnyxCmd = async (callId, action, body = {}) => { try {
36
- const url = callId ? https://api.telnyx.com/v2/calls/${callId}/actions/${action} : https://api.telnyx.com/v2/calls; const response = await fetch(url, { method:
37
- "POST", headers: { "Content-Type": "application/json", Authorization: Bearer
38
- ${TELNYX_API_KEY} }, body: JSON.stringify(body) }); if (!response.ok)
39
- log("TELNYX CMD ERROR", [${action}] ${await response.text()}); return response;
40
- } catch (e) { log("TELNYX CMD NETWORK ERROR", e.message); } };
41
-
42
- app.post("/api/call-action", async (req, res) => { const { call_control_id,
43
- action, lead_id } = req.body; if (!pendingCalls.has(call_control_id)) return
44
- res.status(404).json({ error: "Call not pending" });
45
-
46
- if (lead_id) activeLeadIds.set(call_control_id, lead_id);
47
- pendingCalls.set(call_control_id, action);
48
-
49
- if (action === "reject") { await sendTelnyxCmd(call_control_id, "hangup");
50
- pendingCalls.delete(call_control_id); } else { await
51
- sendTelnyxCmd(call_control_id, "answer"); } res.json({ ok: true }); });
52
-
53
- app.post("/api/hangup", async (req, res) => { await
54
- sendTelnyxCmd(req.body.call_control_id, "hangup"); res.json({ ok: true }); });
55
-
56
- app.post("/api/send-sms", async (req, res) => { log("SMS", Attempting outbound
57
- SMS to ${req.body.to}...); try { const sendRequest = async (fromNumber) => {
58
- const payload = { from: fromNumber, to: req.body.to, text: req.body.text }; if
59
- (TELNYX_MESSAGING_PROFILE_ID) payload.messaging_profile_id =
60
- TELNYX_MESSAGING_PROFILE_ID;
61
-
62
- return await fetch("https://api.telnyx.com/v2/messages", {
63
- method: "POST",
64
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${TELNYX_API_KEY}` },
65
- body: JSON.stringify(payload)
66
- });
67
- };
68
-
69
- // 1. Try sending with the Alpha Sender (ColorizeWeb) first
70
- let response = await sendRequest(TELNYX_ALPHA_SENDER);
71
- let data = await response.json();
72
-
73
- // 2. If rejected due to strict Alpha Sender rules (40305), fallback to the real US phone number instantly
74
- if (!response.ok && data.errors && data.errors.some(e => e.code === '40305' || e.code === '40012')) {
75
- log("SMS WARNING", `Alpha Sender rejected. Falling back to standard US number ${TELNYX_FROM_NUMBER}...`);
76
- response = await sendRequest(TELNYX_FROM_NUMBER);
77
- data = await response.json();
78
- }
79
-
80
- if (!response.ok) {
81
- log("SMS ERROR", JSON.stringify(data));
82
- } else {
83
- log("SMS SUCCESS", `Message accepted by Telnyx for delivery to ${req.body.to}`);
84
- }
85
-
86
- res.json(data);
87
-
88
- } catch(e) { log("SMS NETWORK ERROR", e.message); res.status(500).json({ error:
89
- e.message }); } });
90
-
91
- app.post("/dial", async (req, res) => { const { to, lead_id, action = "manual" }
92
- = req.body; log("DIAL", Outbound to ${to} (Mode: ${action})); try { const
93
- response = await fetch("https://api.telnyx.com/v2/calls", { method: "POST",
94
- headers: { "Content-Type": "application/json", Authorization: Bearer
95
- ${TELNYX_API_KEY} }, body: JSON.stringify({ connection_id: TELNYX_CONNECTION_ID,
96
- to, from: TELNYX_FROM_NUMBER }) }); const data = await response.json(); if
97
- (!response.ok) return res.status(400).json({ ok: false, error:
98
- data?.errors?.[0]?.detail });
99
-
100
- const ccid = data.data.call_control_id;
101
- pendingCalls.set(ccid, action);
102
- if (lead_id) activeLeadIds.set(ccid, lead_id);
103
-
104
- await fetch(`${AI_SERVER_URL}/api/log-outbound`, {
105
- method: "POST", headers: { "Content-Type": "application/json" },
106
- body: JSON.stringify({ call_control_id: ccid, action, lead_id })
107
- }).catch(() => {});
108
-
109
- res.json({ ok: true, call_control_id: ccid });
110
-
111
- } catch (err) { res.status(500).json({ ok: false, error: err.message }); } });
112
-
113
- app.post("/webhook/call", async (req, res) => { res.sendStatus(200); const type
114
- = req.body?.data?.event_type; const payload = req.body?.data?.payload; const
115
- callId = payload?.call_control_id; if (!type) return;
116
-
117
- switch (type) { case "call.initiated": if (payload?.direction === "incoming") {
118
- const fromNumber = payload.from; const now = Date.now();
119
-
120
- if (incomingCallDebounce.has(fromNumber)) {
121
- if (now - incomingCallDebounce.get(fromNumber) < 10000) {
122
- log("INCOMING DEBOUNCED", `Ignoring duplicate payload from ${fromNumber}`);
123
- return;
124
- }
125
- }
126
- incomingCallDebounce.set(fromNumber, now);
127
-
128
- log("INCOMING", `From: ${fromNumber}`);
129
- pendingCalls.set(callId, "waiting");
130
-
131
- await fetch(`${AI_SERVER_URL}/api/incoming-call`, {
132
- method: "POST", headers: { "Content-Type": "application/json" },
133
- body: JSON.stringify({ call_control_id: callId, from: fromNumber })
134
- }).catch(()=>{});
135
- }
136
- break;
137
-
138
- case "call.answered":
139
- if (payload?.client_state) {
140
- const legACallId = Buffer.from(payload.client_state, "base64").toString("ascii");
141
- log("BRIDGING", `Bridging Leg A (${legACallId}) ↔ Leg B (${callId})`);
142
- await sendTelnyxCmd(callId, "bridge", { call_control_id: legACallId });
143
- break;
144
- }
145
-
146
- const actionType = pendingCalls.get(callId) || "ai";
147
-
148
- await sendTelnyxCmd(callId, "record_start", { format: "mp3", channels: "dual", play_beep: false });
149
-
150
- /* if (actionType === "manual") {
151
- log("BRIDGING", `Dialing SIP for manual takeover...`);
152
- await sendTelnyxCmd(null, "dial", {
153
- to: `sip:${SIP_USERNAME}@sip.telnyx.com`, from: TELNYX_FROM_NUMBER,
154
- connection_id: TELNYX_CONNECTION_ID, client_state: Buffer.from(callId).toString("base64")
155
- });
156
- } */
157
- if (actionType === "manual") {
158
-
159
- log("BRIDGING", Dialing Agent Cell for manual takeover...); // Hardcode your
160
- actual personal cell phone number here for the launch const MY_CELL_NUMBER =
161
- "+2347066923490"; // <--- PUT YOUR REAL PHONE NUMBER HERE
162
-
163
- await sendTelnyxCmd(null, "dial", { to: MY_CELL_NUMBER, from:
164
- TELNYX_FROM_NUMBER, connection_id: TELNYX_CONNECTION_ID, client_state:
165
- Buffer.from(callId).toString("base64") }); } else { log("AI CONNECTING",
166
- Starting stream for ${callId}); await sendTelnyxCmd(callId, "playback_start", {
167
- audio_url: http://${req.headers.host}/ambience.mp3, target_legs: "caller", loop:
168
- "infinity" }); await sendTelnyxCmd(callId, "streaming_start", { stream_url:
169
- wss://${req.headers.host}/audio-stream, stream_track: "inbound_track" }); await
170
- sendTelnyxCmd(callId, "transcription_start", { transcription_engine: "Telnyx",
171
- transcription_tracks: "inbound" }); setTimeout(async () => { log("HANGUP",
172
- Enforcing 90s limit on ${callId}); await sendTelnyxCmd(callId, "hangup");
173
- }, 90000); } break;
174
-
175
- case "call.transcription":
176
- if (!payload?.transcription_data?.is_final) break;
177
  try {
178
- const aiRes = await fetch(`${AI_SERVER_URL}/api/process-text-turn`, {
179
- method: "POST", headers: { "Content-Type": "application/json" },
180
- body: JSON.stringify({ text: payload.transcription_data.transcript.trim(), call_id: callId, lead_id: activeLeadIds.get(callId) || null })
 
181
  });
182
- if (aiRes.ok) {
183
- const aiData = await aiRes.json();
184
- if (aiData.speak_url) await sendTelnyxCmd(callId, "playback_start", { audio_url: aiData.speak_url, target_legs: "self" });
185
- }
186
- } catch (e) { log("AI_SERVER_ERROR", e.message); }
187
- break;
 
 
188
 
189
- case "call.recording.saved":
190
- if (payload.recording_urls?.mp3) {
191
-
192
- await fetch(`${AI_SERVER_URL}/api/save-recording`, {
193
-
194
- method: "POST", headers: { "Content-Type": "application/json" }, body:
195
- JSON.stringify({ call_control_id: callId, recording_url:
196
- payload.recording_urls.mp3 }) }) .then(async (r) => { if (!r.ok)
197
- console.log([RECORDING ERROR] AI Server returned status: ${r.status} - ${await
198
- r.text()});
199
-
200
- console.log(Recording URL: ${payload.recording_urls.mp3}); }) .catch((err) => {
201
- console.log([RECORDING NET ERROR] Failed to hit AI Server:, err.message); });
202
-
203
  }
204
- break;
 
205
 
206
- case "call.hangup":
207
- pendingCalls.delete(callId);
208
- activeLeadIds.delete(callId);
209
- await fetch(`${AI_SERVER_URL}/api/call-ended`, {
210
- method: "POST", headers: { "Content-Type": "application/json" },
211
- body: JSON.stringify({ call_control_id: callId })
212
- }).catch(() => {});
213
- break;
214
 
215
- } });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
 
217
- wss.on("connection", (ws) => { let isSpeaking = false; let silenceFrames = 0;
218
- let callControlId = null; let hasPlayedFiller = false; let hasGreeted = false;
219
- let totalNoiseFrames = 0;
 
 
 
 
 
 
 
220
 
221
- ws.on("message", async (raw) => { try { const msg = JSON.parse(raw); if
222
- (msg.event === "start") { callControlId = msg.start?.call_control_id;
223
- setTimeout(async () => { if (hasGreeted || !callControlId) return; hasGreeted =
224
- true; const greetingPhrase = "Hi, this is Nathan."; await
225
- sendTelnyxCmd(callControlId, "playback_start", { audio_url:
226
- ${AI_SERVER_URL}/play-audio?voice=${ACTIVE_VOICE}&phrase=${encodeURIComponent(greetingPhrase)},
227
- target_legs: "self" }); fetch(${AI_SERVER_URL}/api/register-greeting, { method:
228
- "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({
229
- call_id: callControlId, phrase: greetingPhrase, lead_id:
230
- activeLeadIds.get(callControlId) || null }) }).catch(() => {}); }, 1500); }
231
 
232
- if (msg.event === "media") {
233
- const pcm16 = decodeMuLaw(Buffer.from(msg.media.payload, "base64"));
234
- const rms = calculateRMS(pcm16);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
 
236
- if (rms > 500) {
237
- hasGreeted = true;
238
- silenceFrames = 0;
239
- totalNoiseFrames++;
240
- if (!isSpeaking) {
241
- isSpeaking = true;
242
- hasPlayedFiller = false;
243
- await sendTelnyxCmd(callControlId, "playback_stop");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
  }
245
- } else if (isSpeaking) {
246
- silenceFrames++;
247
- if (silenceFrames > 35 && !hasPlayedFiller) {
248
- hasPlayedFiller = true;
249
- isSpeaking = false;
250
- if (totalNoiseFrames > 40) {
251
- const fillers = ["Right.", "Got it.", "Okay.", "Yeah.", "Ah, okay."];
252
- sendTelnyxCmd(callControlId, "playback_start", { audio_url: `${AI_SERVER_URL}/play-audio?voice=${ACTIVE_VOICE}&phrase=${encodeURIComponent(fillers[Math.floor(Math.random() * fillers.length)])}`, target_legs: "self" }).catch(() => {});
 
 
 
 
253
  }
254
- totalNoiseFrames = 0;
 
 
 
 
 
 
 
 
 
 
255
  }
256
- }
 
 
 
 
 
 
 
 
 
 
257
  }
258
- } catch {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
 
260
- }); });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
261
 
262
- server.listen(PORT, "0.0.0.0", () => { log("STARTUP", Telecom routing server
263
- running on port ${PORT}); });
 
 
1
+ import express from "express";
2
+ import { createServer } from "http";
3
  import { WebSocketServer } from "ws";
4
 
5
+ const app = express();
6
+ const server = createServer(app);
7
+ const wss = new WebSocketServer({ server, path: "/audio-stream" });
8
+
9
+ app.use(express.static("public", { index: false }));
10
+ app.use(express.json());
11
+
12
+ const PORT = process.env.PORT || 7860;
13
+ const TELNYX_API_KEY = process.env.TELNYX_API_KEY;
14
+ const TELNYX_CONNECTION_ID = process.env.TELNYX_CONNECTION_ID;
15
+ const TELNYX_FROM_NUMBER = process.env.TELNYX_FROM_NUMBER;
16
+ const AI_SERVER_URL = process.env.AI_SERVER_URL;
17
+ const TELNYX_ALPHA_SENDER = process.env.TELNYX_ALPHA_SENDER || TELNYX_FROM_NUMBER;
18
+ const TELNYX_MESSAGING_PROFILE_ID = process.env.TELNYX_MESSAGING_PROFILE_ID;
19
+
20
+ const ACTIVE_VOICE = "Enceladus_3";
21
+ const log = (tag, ...args) => console.log(`[${new Date().toISOString()}] [${tag}]`, ...args);
22
+
23
+ let pendingCalls = new Map();
24
+ let activeLeadIds = new Map();
25
+ const incomingCallDebounce = new Map();
26
+
27
+ // ── MU-LAW VAD ──
28
+ const MU_LAW_TABLE = new Int16Array(256);
29
+ for (let i = 0; i < 256; i++) {
30
+ const sign = (i & 0x80) ? -1 : 1;
31
+ const exponent = (~i >> 4) & 0x07;
32
+ const mantissa = ~i & 0x0f;
33
+ const sample = ((mantissa << 3) + 132) << exponent;
34
+ MU_LAW_TABLE[i] = sign * (sample - 132);
35
+ }
36
+ function decodeMuLaw(buffer) {
37
+ const out = new Int16Array(buffer.length);
38
+ for (let i = 0; i < buffer.length; i++) out[i] = MU_LAW_TABLE[buffer[i]];
39
+ return out;
40
+ }
41
+ function calculateRMS(pcm) {
42
+ let sum = 0;
43
+ for (let i = 0; i < pcm.length; i++) sum += pcm[i] * pcm[i];
44
+ return Math.sqrt(sum / pcm.length);
45
+ }
46
+
47
+ const sendTelnyxCmd = async (callId, action, body = {}) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  try {
49
+ const url = callId ? `https://api.telnyx.com/v2/calls/${callId}/actions/${action}` : `https://api.telnyx.com/v2/calls`;
50
+ const response = await fetch(url, {
51
+ method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${TELNYX_API_KEY}` },
52
+ body: JSON.stringify(body)
53
  });
54
+ if (!response.ok) log("TELNYX CMD ERROR", `[${action}] ${await response.text()}`);
55
+ return response;
56
+ } catch (e) { log("TELNYX CMD NETWORK ERROR", e.message); }
57
+ };
58
+
59
+ app.post("/api/call-action", async (req, res) => {
60
+ const { call_control_id, action, lead_id } = req.body;
61
+ if (!pendingCalls.has(call_control_id)) return res.status(404).json({ error: "Call not pending" });
62
 
63
+ if (lead_id) activeLeadIds.set(call_control_id, lead_id);
64
+ pendingCalls.set(call_control_id, action);
65
+
66
+ if (action === "reject") {
67
+ await sendTelnyxCmd(call_control_id, "hangup");
68
+ pendingCalls.delete(call_control_id);
69
+ } else {
70
+ await sendTelnyxCmd(call_control_id, "answer");
 
 
 
 
 
 
71
  }
72
+ res.json({ ok: true });
73
+ });
74
 
75
+ app.post("/api/hangup", async (req, res) => {
76
+ await sendTelnyxCmd(req.body.call_control_id, "hangup");
77
+ res.json({ ok: true });
78
+ });
 
 
 
 
79
 
80
+ app.post("/api/send-sms", async (req, res) => {
81
+ log("SMS", `Attempting outbound SMS to ${req.body.to}...`);
82
+ try {
83
+ const sendRequest = async (fromNumber) => {
84
+ const payload = { from: fromNumber, to: req.body.to, text: req.body.text };
85
+ if (TELNYX_MESSAGING_PROFILE_ID) payload.messaging_profile_id = TELNYX_MESSAGING_PROFILE_ID;
86
+
87
+ return await fetch("https://api.telnyx.com/v2/messages", {
88
+ method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${TELNYX_API_KEY}` },
89
+ body: JSON.stringify(payload)
90
+ });
91
+ };
92
+
93
+ let response = await sendRequest(TELNYX_ALPHA_SENDER);
94
+ let data = await response.json();
95
+
96
+ if (!response.ok && data.errors && data.errors.some(e => e.code === '40305' || e.code === '40012')) {
97
+ log("SMS WARNING", `Alpha Sender rejected. Falling back to standard US number ${TELNYX_FROM_NUMBER}...`);
98
+ response = await sendRequest(TELNYX_FROM_NUMBER);
99
+ data = await response.json();
100
+ }
101
+
102
+ if (!response.ok) log("SMS ERROR", JSON.stringify(data));
103
+ else log("SMS SUCCESS", `Message accepted by Telnyx for delivery to ${req.body.to}`);
104
+
105
+ res.json(data);
106
+ } catch(e) {
107
+ log("SMS NETWORK ERROR", e.message);
108
+ res.status(500).json({ error: e.message });
109
+ }
110
+ });
111
 
112
+ app.post("/dial", async (req, res) => {
113
+ const { to, lead_id, action = "manual" } = req.body;
114
+ log("DIAL", `Outbound to ${to} (Mode: ${action})`);
115
+ try {
116
+ const response = await fetch("https://api.telnyx.com/v2/calls", {
117
+ method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${TELNYX_API_KEY}` },
118
+ body: JSON.stringify({ connection_id: TELNYX_CONNECTION_ID, to, from: TELNYX_FROM_NUMBER })
119
+ });
120
+ const data = await response.json();
121
+ if (!response.ok) return res.status(400).json({ ok: false, error: data?.errors?.[0]?.detail });
122
 
123
+ const ccid = data.data.call_control_id;
124
+ pendingCalls.set(ccid, action);
125
+ if (lead_id) activeLeadIds.set(ccid, lead_id);
 
 
 
 
 
 
 
126
 
127
+ await fetch(`${AI_SERVER_URL}/api/log-outbound`, {
128
+ method: "POST", headers: { "Content-Type": "application/json" },
129
+ body: JSON.stringify({ call_control_id: ccid, action, lead_id })
130
+ }).catch(() => {});
131
+
132
+ res.json({ ok: true, call_control_id: ccid });
133
+ } catch (err) { res.status(500).json({ ok: false, error: err.message }); }
134
+ });
135
+
136
+ app.post("/webhook/call", async (req, res) => {
137
+ res.sendStatus(200);
138
+ const type = req.body?.data?.event_type;
139
+ const payload = req.body?.data?.payload;
140
+ const callId = payload?.call_control_id;
141
+ if (!type) return;
142
+
143
+ switch (type) {
144
+ case "call.initiated":
145
+ if (payload?.direction === "incoming") {
146
+ const fromNumber = payload.from;
147
+ const now = Date.now();
148
+
149
+ if (incomingCallDebounce.has(fromNumber)) {
150
+ if (now - incomingCallDebounce.get(fromNumber) < 10000) {
151
+ log("INCOMING DEBOUNCED", `Ignoring duplicate payload from ${fromNumber}`);
152
+ return;
153
+ }
154
+ }
155
+ incomingCallDebounce.set(fromNumber, now);
156
+
157
+ log("INCOMING", `From: ${fromNumber}`);
158
+ pendingCalls.set(callId, "waiting");
159
+
160
+ await fetch(`${AI_SERVER_URL}/api/incoming-call`, {
161
+ method: "POST", headers: { "Content-Type": "application/json" },
162
+ body: JSON.stringify({ call_control_id: callId, from: fromNumber })
163
+ }).catch(()=>{});
164
+ }
165
+ break;
166
+
167
+ case "call.answered":
168
+ if (payload?.client_state) {
169
+ const legACallId = Buffer.from(payload.client_state, "base64").toString("ascii");
170
+ log("BRIDGING", `Agent picked up! Stopping ringback and Bridging Leg A (${legACallId}) ↔ Leg B (${callId})`);
171
+
172
+ // Stop the ringing sound for the customer
173
+ await sendTelnyxCmd(legACallId, "playback_stop");
174
+
175
+ // Bridge them
176
+ await sendTelnyxCmd(callId, "bridge", { call_control_id: legACallId });
177
+ break;
178
+ }
179
 
180
+ const actionType = pendingCalls.get(callId) || "ai";
181
+
182
+ await sendTelnyxCmd(callId, "record_start", { format: "mp3", channels: "dual", play_beep: false });
183
+
184
+ if (actionType === "manual") {
185
+ log("BRIDGING", `Dialing Agent Cell (+2348069234734) for manual takeover...`);
186
+
187
+ // 1. Play ringing to the customer
188
+ await sendTelnyxCmd(callId, "playback_start", {
189
+ audio_url: "https://actions.telnyx.com/sample-assets/ringback.wav",
190
+ target_legs: "self"
191
+ });
192
+
193
+ // 2. Dial your cell phone
194
+ const MY_CELL_NUMBER = "+2347066923490";
195
+ await sendTelnyxCmd(null, "dial", {
196
+ to: MY_CELL_NUMBER,
197
+ from: TELNYX_FROM_NUMBER,
198
+ connection_id: TELNYX_CONNECTION_ID,
199
+ client_state: Buffer.from(callId).toString("base64")
200
+ });
201
+ } else {
202
+ log("AI CONNECTING", `Starting stream for ${callId}`);
203
+ await sendTelnyxCmd(callId, "playback_start", { audio_url: `http://${req.headers.host}/ambience.mp3`, target_legs: "caller", loop: "infinity" });
204
+ await sendTelnyxCmd(callId, "streaming_start", { stream_url: `wss://${req.headers.host}/audio-stream`, stream_track: "inbound_track" });
205
+ await sendTelnyxCmd(callId, "transcription_start", { transcription_engine: "Telnyx", transcription_tracks: "inbound" });
206
+ setTimeout(async () => { log("HANGUP", `Enforcing 90s limit on ${callId}`); await sendTelnyxCmd(callId, "hangup"); }, 90000);
207
  }
208
+ break;
209
+
210
+ case "call.transcription":
211
+ if (!payload?.transcription_data?.is_final) break;
212
+ try {
213
+ const aiRes = await fetch(`${AI_SERVER_URL}/api/process-text-turn`, {
214
+ method: "POST", headers: { "Content-Type": "application/json" },
215
+ body: JSON.stringify({ text: payload.transcription_data.transcript.trim(), call_id: callId, lead_id: activeLeadIds.get(callId) || null })
216
+ });
217
+ if (aiRes.ok) {
218
+ const aiData = await aiRes.json();
219
+ if (aiData.speak_url) await sendTelnyxCmd(callId, "playback_start", { audio_url: aiData.speak_url, target_legs: "self" });
220
  }
221
+ } catch (e) { log("AI_SERVER_ERROR", e.message); }
222
+ break;
223
+
224
+ case "call.recording.saved":
225
+ if (payload.recording_urls?.mp3) {
226
+ await fetch(`${AI_SERVER_URL}/api/save-recording`, {
227
+ method: "POST", headers: { "Content-Type": "application/json" },
228
+ body: JSON.stringify({ call_control_id: callId, recording_url: payload.recording_urls.mp3 })
229
+ })
230
+ .then(async (r) => { if (!r.ok) log("RECORDING ERROR", `AI Server status: ${r.status}`); })
231
+ .catch((err) => log("RECORDING NET ERROR", err.message));
232
  }
233
+ break;
234
+
235
+ case "call.hangup":
236
+ log("HANGUP", `Call ended. Reason: ${payload.hangup_cause}`);
237
+ pendingCalls.delete(callId);
238
+ activeLeadIds.delete(callId);
239
+ await fetch(`${AI_SERVER_URL}/api/call-ended`, {
240
+ method: "POST", headers: { "Content-Type": "application/json" },
241
+ body: JSON.stringify({ call_control_id: callId })
242
+ }).catch(() => {});
243
+ break;
244
  }
245
+ });
246
+
247
+ wss.on("connection", (ws) => {
248
+ let isSpeaking = false;
249
+ let silenceFrames = 0;
250
+ let callControlId = null;
251
+ let hasPlayedFiller = false;
252
+ let hasGreeted = false;
253
+ let totalNoiseFrames = 0;
254
+
255
+ ws.on("message", async (raw) => {
256
+ try {
257
+ const msg = JSON.parse(raw);
258
+ if (msg.event === "start") {
259
+ callControlId = msg.start?.call_control_id;
260
+ setTimeout(async () => {
261
+ if (hasGreeted || !callControlId) return;
262
+ hasGreeted = true;
263
+ const greetingPhrase = "Hi, this is Nathan.";
264
+ await sendTelnyxCmd(callControlId, "playback_start", { audio_url: `${AI_SERVER_URL}/play-audio?voice=${ACTIVE_VOICE}&phrase=${encodeURIComponent(greetingPhrase)}`, target_legs: "self" });
265
+ fetch(`${AI_SERVER_URL}/api/register-greeting`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ call_id: callControlId, phrase: greetingPhrase, lead_id: activeLeadIds.get(callControlId) || null }) }).catch(() => {});
266
+ }, 1500);
267
+ }
268
 
269
+ if (msg.event === "media") {
270
+ const pcm16 = decodeMuLaw(Buffer.from(msg.media.payload, "base64"));
271
+ const rms = calculateRMS(pcm16);
272
+
273
+ if (rms > 500) {
274
+ hasGreeted = true;
275
+ silenceFrames = 0;
276
+ totalNoiseFrames++;
277
+ if (!isSpeaking) {
278
+ isSpeaking = true;
279
+ hasPlayedFiller = false;
280
+ await sendTelnyxCmd(callControlId, "playback_stop");
281
+ }
282
+ } else if (isSpeaking) {
283
+ silenceFrames++;
284
+ if (silenceFrames > 35 && !hasPlayedFiller) {
285
+ hasPlayedFiller = true;
286
+ isSpeaking = false;
287
+ if (totalNoiseFrames > 40) {
288
+ const fillers = ["Right.", "Got it.", "Okay.", "Yeah.", "Ah, okay."];
289
+ sendTelnyxCmd(callControlId, "playback_start", { audio_url: `${AI_SERVER_URL}/play-audio?voice=${ACTIVE_VOICE}&phrase=${encodeURIComponent(fillers[Math.floor(Math.random() * fillers.length)])}`, target_legs: "self" }).catch(() => {});
290
+ }
291
+ totalNoiseFrames = 0;
292
+ }
293
+ }
294
+ }
295
+ } catch {}
296
+ });
297
+ });
298
 
299
+ server.listen(PORT, "0.0.0.0", () => {
300
+ log("STARTUP", `Telecom routing server running on port ${PORT}`);
301
+ });