Pepguy commited on
Commit
a1abbca
Β·
verified Β·
1 Parent(s): 22cfb93

Update app.js

Browse files
Files changed (1) hide show
  1. app.js +64 -81
app.js CHANGED
@@ -32,7 +32,10 @@ const sendTelnyxCmd = async (callId, action, body = {}) => {
32
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${TELNYX_API_KEY}` },
33
  body: JSON.stringify(body)
34
  });
35
- if (!response.ok) log("TELNYX CMD ERROR", `[${action}] failed (${response.status}): ${await response.text()}`);
 
 
 
36
  return response;
37
  } catch (e) {
38
  log("TELNYX CMD NETWORK ERROR", e.message);
@@ -45,6 +48,12 @@ const updateLog = (callId, statusUpdates) => {
45
  return entry;
46
  };
47
 
 
 
 
 
 
 
48
  // ─── VAD MATH UTILITIES ───
49
  const MU_LAW_TABLE = new Int16Array(256);
50
  for (let i = 0; i < 256; i++) {
@@ -181,7 +190,9 @@ app.get("/", (req, res) => {
181
  <button onClick={() => hangup(log.call_control_id)} className="bg-red-600 hover:bg-red-500 text-xs px-3 py-1 rounded font-bold transition">
182
  Force Hang Up
183
  </button>
184
- ) : <span className="text-gray-500 text-xs">Ended</span>}
 
 
185
  </td>
186
  <td className="p-4">
187
  {log.recording_url ?
@@ -208,7 +219,7 @@ app.get("/", (req, res) => {
208
  app.get("/api/logs", (req, res) => res.json([...callLogs].reverse()));
209
  app.get("/health", (req, res) => res.json({ status: "ok", uptime: process.uptime() }));
210
 
211
- // ─── PROXY FOR TELNYX RECORDINGS ─────────────
212
  app.get("/play-recording/:call_control_id", async (req, res) => {
213
  const logEntry = callLogs.find(l => l.call_control_id === req.params.call_control_id);
214
  if (!logEntry || !logEntry.raw_telnyx_url) return res.sendStatus(404);
@@ -219,22 +230,28 @@ app.get("/play-recording/:call_control_id", async (req, res) => {
219
  res.setHeader("Content-Type", "audio/mpeg");
220
  res.setHeader("Content-Length", buffer.length);
221
  res.send(buffer);
222
- } catch (e) { res.sendStatus(500); }
 
 
223
  });
224
 
225
- // ─── DIAL OUT ROUTE ──────────────────────────
226
  app.post("/dial", async (req, res) => {
227
  const { to } = req.body;
228
  const voice = "Algieba";
229
  log("DIAL", `Dialing ${to}...`);
230
 
231
  try {
232
- const response = await sendTelnyxCmd("", "v2/calls", {
233
- connection_id: TELNYX_CONNECTION_ID,
234
- to,
235
- from: TELNYX_FROM_NUMBER,
236
- custom_headers: [{ name: "X-Voice-Selection", value: voice }]
237
- }, true);
 
 
 
 
238
 
239
  const data = await response.json();
240
  if (!response.ok) return res.status(400).json({ ok: false, error: data?.errors?.[0]?.detail });
@@ -252,19 +269,27 @@ app.post("/dial", async (req, res) => {
252
  timers: {},
253
  created_at: new Date().toISOString()
254
  });
 
255
  res.json({ ok: true });
256
- } catch (err) { res.status(500).json({ ok: false, error: err.message }); }
 
 
257
  });
258
 
259
- // ─── MANUAL HANG UP ──────────────────────────
260
  app.post("/call/hangup", async (req, res) => {
261
  const { call_control_id } = req.body;
262
  if (!call_control_id) return res.status(400).json({ error: "Missing call_control_id" });
263
- await sendTelnyxCmd(call_control_id, "hangup");
264
- res.json({ ok: true });
 
 
 
 
 
265
  });
266
 
267
- // ─── TELNYX WEBHOOK (State Only, No STT) ──────
268
  app.post("/webhook/call", async (req, res) => {
269
  const type = req.body?.data?.event_type;
270
  const payload = req.body?.data?.payload;
@@ -273,6 +298,8 @@ app.post("/webhook/call", async (req, res) => {
273
  if (!type) return res.sendStatus(200);
274
 
275
  const entry = callLogs.find(l => l.call_control_id === callId);
 
 
276
  if (entry && entry.status === "completed" && type !== "call.recording.saved") {
277
  return res.sendStatus(200);
278
  }
@@ -300,9 +327,19 @@ app.post("/webhook/call", async (req, res) => {
300
  log("ANSWERED", `Call ${callId} connected.`);
301
  updateLog(callId, { status: "in-progress" });
302
 
 
303
  await sendTelnyxCmd(callId, "record_start", { format: "mp3", channels: "dual", play_beep: false });
304
 
305
- // Start WebSocket Media Stream to listen to the caller natively
 
 
 
 
 
 
 
 
 
306
  const wsUrl = `wss://${req.headers.host}/audio-stream`;
307
  await sendTelnyxCmd(callId, "stream_start", { stream_url: wsUrl, track: "inbound_track" });
308
 
@@ -321,19 +358,19 @@ app.post("/webhook/call", async (req, res) => {
321
  break;
322
 
323
  case "call.recording.saved":
324
- log("RECORDING URL", `\n\n⬇️ DOWNLOAD RAW AUDIO HERE ⬇️\n${payload.recording_urls.mp3}\n`);
 
 
 
325
  updateLog(callId, {
326
- raw_telnyx_url: payload.recording_urls.mp3,
327
  recording_url: `/play-recording/${callId}`
328
  });
329
  break;
330
 
331
  case "call.hangup":
332
  updateLog(callId, { status: "completed", is_playing: false });
333
- if (entry && entry.timers) {
334
- Object.values(entry.timers).forEach(clearTimeout);
335
- entry.timers = {};
336
- }
337
  log("CALL ENDED", `Terminated.`);
338
  break;
339
  }
@@ -387,7 +424,7 @@ wss.on("connection", (ws, req) => {
387
 
388
  // ── 1. NOISE DETECTED (Human is speaking) ──
389
  if (rms > VOLUME_THRESHOLD) {
390
- hasGreeted = true; // They spoke, disable smart greeting timer
391
  silenceFrames = 0;
392
  pcmChunks.push(pcm16);
393
 
@@ -421,14 +458,13 @@ wss.on("connection", (ws, req) => {
421
  const entry = callLogs.find(l => l.call_control_id === callControlId);
422
  const voice = entry ? entry.voice_used : "Algieba";
423
 
424
- // Compile WAV buffer for Groq
425
  const totalLength = pcmChunks.reduce((acc, val) => acc + val.length, 0);
426
  const finalPcm = new Int16Array(totalLength);
427
  let offset = 0;
428
  for (let chunk of pcmChunks) { finalPcm.set(chunk, offset); offset += chunk.length; }
429
  pcmChunks = []; // Clear for next turn
430
 
431
- // If the noise burst was less than ~200ms total, ignore it (mic bump/cough)
432
  if (totalLength < 8000 * 0.2) {
433
  isProcessingTurn = false;
434
  return;
@@ -456,9 +492,9 @@ wss.on("connection", (ws, req) => {
456
  const aiData = await aiRes.json();
457
  if (aiData.transcript) updateLog(callControlId, { last_transcript: aiData.transcript });
458
 
459
- // Check race condition: Did human interrupt the AI thinking?
460
  if (!isSpeaking && aiData.speak_url) {
461
- log("AI_REPLY", `Playing reply: ${aiData.raw_text}`);
462
  if (entry) entry.is_playing = true;
463
  await sendTelnyxCmd(callControlId, "playback_start", {
464
  audio_url: aiData.speak_url, target_legs: "self"
@@ -475,59 +511,6 @@ wss.on("connection", (ws, req) => {
475
  });
476
  });
477
 
478
- // Custom fetch wrapper for internal call initiation without full endpoint
479
- const originalFetch = global.fetch;
480
- global.fetch = async (url, options) => {
481
- if (url === "https://api.telnyx.com/v2/calls" && options?.method === "POST" && options?.isCustomDial) {
482
- const rawUrl = url;
483
- delete options.isCustomDial;
484
- return originalFetch(rawUrl, options);
485
- }
486
- return originalFetch(url, options);
487
- };
488
-
489
- // Replace Dial API implementation to use standard fetch safely
490
- app.post("/dial", async (req, res) => {
491
- const { to } = req.body;
492
- const voice = "Algieba";
493
- log("DIAL", `Dialing ${to}...`);
494
-
495
- try {
496
- const response = await fetch("https://api.telnyx.com/v2/calls", {
497
- method: "POST",
498
- isCustomDial: true,
499
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${TELNYX_API_KEY}` },
500
- body: JSON.stringify({
501
- connection_id: TELNYX_CONNECTION_ID,
502
- to,
503
- from: TELNYX_FROM_NUMBER,
504
- custom_headers: [{ name: "X-Voice-Selection", value: voice }]
505
- }),
506
- });
507
-
508
- const data = await response.json();
509
- if (!response.ok) return res.status(400).json({ ok: false, error: data?.errors?.[0]?.detail });
510
-
511
- callLogs.push({
512
- id: randomUUID(),
513
- call_control_id: data.data.call_control_id,
514
- target_number: to,
515
- voice_used: voice,
516
- status: "dialing",
517
- last_transcript: "",
518
- recording_url: null,
519
- raw_telnyx_url: null,
520
- is_playing: false,
521
- timers: {},
522
- created_at: new Date().toISOString()
523
- });
524
-
525
- res.json({ ok: true });
526
- } catch (err) {
527
- res.status(500).json({ ok: false, error: err.message });
528
- }
529
- });
530
-
531
  const requiredEnv = ["TELNYX_API_KEY", "TELNYX_CONNECTION_ID", "TELNYX_FROM_NUMBER", "AI_SERVER_URL"];
532
  requiredEnv.forEach((key) => {
533
  if (!process.env[key]) log("STARTUP WARNING", `Missing env var: ${key}`);
 
32
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${TELNYX_API_KEY}` },
33
  body: JSON.stringify(body)
34
  });
35
+ if (!response.ok) {
36
+ const errText = await response.text();
37
+ log("TELNYX CMD ERROR", `[${action}] failed (${response.status}): ${errText}`);
38
+ }
39
  return response;
40
  } catch (e) {
41
  log("TELNYX CMD NETWORK ERROR", e.message);
 
48
  return entry;
49
  };
50
 
51
+ const clearCallTimers = (entry) => {
52
+ if (!entry || !entry.timers) return;
53
+ Object.values(entry.timers).forEach(clearTimeout);
54
+ entry.timers = {};
55
+ };
56
+
57
  // ─── VAD MATH UTILITIES ───
58
  const MU_LAW_TABLE = new Int16Array(256);
59
  for (let i = 0; i < 256; i++) {
 
190
  <button onClick={() => hangup(log.call_control_id)} className="bg-red-600 hover:bg-red-500 text-xs px-3 py-1 rounded font-bold transition">
191
  Force Hang Up
192
  </button>
193
+ ) : (
194
+ <span className="text-gray-500 text-xs">Ended</span>
195
+ )}
196
  </td>
197
  <td className="p-4">
198
  {log.recording_url ?
 
219
  app.get("/api/logs", (req, res) => res.json([...callLogs].reverse()));
220
  app.get("/health", (req, res) => res.json({ status: "ok", uptime: process.uptime() }));
221
 
222
+ // ─── PROXY FOR TELNYX RECORDINGS ───────────────────────────
223
  app.get("/play-recording/:call_control_id", async (req, res) => {
224
  const logEntry = callLogs.find(l => l.call_control_id === req.params.call_control_id);
225
  if (!logEntry || !logEntry.raw_telnyx_url) return res.sendStatus(404);
 
230
  res.setHeader("Content-Type", "audio/mpeg");
231
  res.setHeader("Content-Length", buffer.length);
232
  res.send(buffer);
233
+ } catch (e) {
234
+ res.sendStatus(500);
235
+ }
236
  });
237
 
238
+ // ─── DIAL OUT ROUTE ────────────────────────────────────────
239
  app.post("/dial", async (req, res) => {
240
  const { to } = req.body;
241
  const voice = "Algieba";
242
  log("DIAL", `Dialing ${to}...`);
243
 
244
  try {
245
+ const response = await fetch("https://api.telnyx.com/v2/calls", {
246
+ method: "POST",
247
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${TELNYX_API_KEY}` },
248
+ body: JSON.stringify({
249
+ connection_id: TELNYX_CONNECTION_ID,
250
+ to,
251
+ from: TELNYX_FROM_NUMBER,
252
+ custom_headers: [{ name: "X-Voice-Selection", value: voice }]
253
+ }),
254
+ });
255
 
256
  const data = await response.json();
257
  if (!response.ok) return res.status(400).json({ ok: false, error: data?.errors?.[0]?.detail });
 
269
  timers: {},
270
  created_at: new Date().toISOString()
271
  });
272
+
273
  res.json({ ok: true });
274
+ } catch (err) {
275
+ res.status(500).json({ ok: false, error: err.message });
276
+ }
277
  });
278
 
279
+ // ─── MANUAL HANG UP ────────────────────────────────────────
280
  app.post("/call/hangup", async (req, res) => {
281
  const { call_control_id } = req.body;
282
  if (!call_control_id) return res.status(400).json({ error: "Missing call_control_id" });
283
+
284
+ try {
285
+ await sendTelnyxCmd(call_control_id, "hangup");
286
+ res.json({ ok: true });
287
+ } catch (err) {
288
+ res.status(500).json({ ok: false, error: err.message });
289
+ }
290
  });
291
 
292
+ // ─── TELNYX WEBHOOK (State Only, No STT) ───────────────────
293
  app.post("/webhook/call", async (req, res) => {
294
  const type = req.body?.data?.event_type;
295
  const payload = req.body?.data?.payload;
 
298
  if (!type) return res.sendStatus(200);
299
 
300
  const entry = callLogs.find(l => l.call_control_id === callId);
301
+
302
+ // Local Lock: Ignore events for dead calls to prevent 422 errors
303
  if (entry && entry.status === "completed" && type !== "call.recording.saved") {
304
  return res.sendStatus(200);
305
  }
 
327
  log("ANSWERED", `Call ${callId} connected.`);
328
  updateLog(callId, { status: "in-progress" });
329
 
330
+ // Start Dual-Channel Recording
331
  await sendTelnyxCmd(callId, "record_start", { format: "mp3", channels: "dual", play_beep: false });
332
 
333
+ // DEACTIVATED UNTIL YOU FIX AUDIO LEVELS:
334
+ /*
335
+ await sendTelnyxCmd(callId, "playback_start", {
336
+ audio_url: `https://${req.headers.host}/ambience.mp3`,
337
+ loop: "infinity",
338
+ target_legs: "self"
339
+ });
340
+ */
341
+
342
+ // Start WebSocket Media Stream to listen to the caller natively via VAD
343
  const wsUrl = `wss://${req.headers.host}/audio-stream`;
344
  await sendTelnyxCmd(callId, "stream_start", { stream_url: wsUrl, track: "inbound_track" });
345
 
 
358
  break;
359
 
360
  case "call.recording.saved":
361
+ // THE PROMINENT RECORDING LOG YOU REQUESTED
362
+ const rawUrl = payload.recording_urls.mp3;
363
+ log("RECORDING URL", `\n\n⬇️ DOWNLOAD RAW AUDIO HERE ⬇️\n${rawUrl}\n\n`);
364
+
365
  updateLog(callId, {
366
+ raw_telnyx_url: rawUrl,
367
  recording_url: `/play-recording/${callId}`
368
  });
369
  break;
370
 
371
  case "call.hangup":
372
  updateLog(callId, { status: "completed", is_playing: false });
373
+ clearCallTimers(entry);
 
 
 
374
  log("CALL ENDED", `Terminated.`);
375
  break;
376
  }
 
424
 
425
  // ── 1. NOISE DETECTED (Human is speaking) ──
426
  if (rms > VOLUME_THRESHOLD) {
427
+ hasGreeted = true;
428
  silenceFrames = 0;
429
  pcmChunks.push(pcm16);
430
 
 
458
  const entry = callLogs.find(l => l.call_control_id === callControlId);
459
  const voice = entry ? entry.voice_used : "Algieba";
460
 
 
461
  const totalLength = pcmChunks.reduce((acc, val) => acc + val.length, 0);
462
  const finalPcm = new Int16Array(totalLength);
463
  let offset = 0;
464
  for (let chunk of pcmChunks) { finalPcm.set(chunk, offset); offset += chunk.length; }
465
  pcmChunks = []; // Clear for next turn
466
 
467
+ // Ignore tiny noise bursts (coughs, mic bumps)
468
  if (totalLength < 8000 * 0.2) {
469
  isProcessingTurn = false;
470
  return;
 
492
  const aiData = await aiRes.json();
493
  if (aiData.transcript) updateLog(callControlId, { last_transcript: aiData.transcript });
494
 
495
+ // Check race condition: Did human interrupt the AI while it was thinking?
496
  if (!isSpeaking && aiData.speak_url) {
497
+ log("AI_REPLY", `Playing full reply.`);
498
  if (entry) entry.is_playing = true;
499
  await sendTelnyxCmd(callControlId, "playback_start", {
500
  audio_url: aiData.speak_url, target_legs: "self"
 
511
  });
512
  });
513
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
514
  const requiredEnv = ["TELNYX_API_KEY", "TELNYX_CONNECTION_ID", "TELNYX_FROM_NUMBER", "AI_SERVER_URL"];
515
  requiredEnv.forEach((key) => {
516
  if (!process.env[key]) log("STARTUP WARNING", `Missing env var: ${key}`);