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

Update app.js

Browse files
Files changed (1) hide show
  1. app.js +19 -30
app.js CHANGED
@@ -51,7 +51,7 @@ const updateLog = (callId, statusUpdates) => {
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 ───
@@ -216,7 +216,16 @@ app.get("/", (req, res) => {
216
  `);
217
  });
218
 
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 ───────────────────────────
@@ -266,7 +275,7 @@ app.post("/dial", async (req, res) => {
266
  recording_url: null,
267
  raw_telnyx_url: null,
268
  is_playing: false,
269
- timers: {},
270
  created_at: new Date().toISOString()
271
  });
272
 
@@ -289,7 +298,7 @@ app.post("/call/hangup", async (req, res) => {
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;
@@ -299,7 +308,6 @@ app.post("/webhook/call", async (req, res) => {
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,21 +335,13 @@ app.post("/webhook/call", async (req, res) => {
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
 
346
  if (entry) {
347
  entry.timers.hangup = setTimeout(async () => {
@@ -358,7 +358,6 @@ app.post("/webhook/call", async (req, res) => {
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
 
@@ -388,8 +387,8 @@ wss.on("connection", (ws, req) => {
388
  let isProcessingTurn = false;
389
  let hasGreeted = false;
390
 
391
- const SILENCE_THRESHOLD_FRAMES = 25; // 25 frames * 20ms = 500ms of silence
392
- const VOLUME_THRESHOLD = 400; // RMS Volume Threshold
393
 
394
  ws.on("message", async (raw) => {
395
  try {
@@ -399,7 +398,6 @@ wss.on("connection", (ws, req) => {
399
  callControlId = msg.start?.call_control_id;
400
  log("WS_STREAM", `Receiving audio for call ${callControlId}`);
401
 
402
- // Smart Initial Greeting
403
  setTimeout(async () => {
404
  if (!hasGreeted && callControlId) {
405
  const entry = callLogs.find(l => l.call_control_id === callControlId);
@@ -422,7 +420,6 @@ wss.on("connection", (ws, req) => {
422
  const pcm16 = decodeMuLaw(mulawBuffer);
423
  const rms = calculateRMS(pcm16);
424
 
425
- // ── 1. NOISE DETECTED (Human is speaking) ──
426
  if (rms > VOLUME_THRESHOLD) {
427
  hasGreeted = true;
428
  silenceFrames = 0;
@@ -433,7 +430,6 @@ wss.on("connection", (ws, req) => {
433
  isProcessingTurn = false;
434
  log("VAD", "Human started speaking.");
435
 
436
- // ── BARGE IN ──
437
  const entry = callLogs.find(l => l.call_control_id === callControlId);
438
  if (entry && entry.is_playing) {
439
  log("BARGE-IN", "Interrupting AI playback.");
@@ -442,13 +438,10 @@ wss.on("connection", (ws, req) => {
442
  }
443
  }
444
  }
445
-
446
- // ── 2. SILENCE DETECTED ──
447
  else if (isSpeaking) {
448
  pcmChunks.push(pcm16);
449
  silenceFrames++;
450
 
451
- // ── 3. SOFT TURN TRIGGER (500ms silence limit reached) ──
452
  if (silenceFrames > SILENCE_THRESHOLD_FRAMES && !isProcessingTurn) {
453
  log("VAD", "500ms silence detected. Turn ended.");
454
  isSpeaking = false;
@@ -462,9 +455,8 @@ wss.on("connection", (ws, req) => {
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;
@@ -472,7 +464,6 @@ wss.on("connection", (ws, req) => {
472
 
473
  const wavBuffer = pcm16ToWav(finalPcm, 8000);
474
 
475
- // ── IMMEDIATE BACKCHANNEL HACK (0ms perceived latency) ──
476
  const fillers = ["Right.", "Got it.", "Okay."];
477
  const filler = fillers[Math.floor(Math.random() * fillers.length)];
478
 
@@ -482,7 +473,6 @@ wss.on("connection", (ws, req) => {
482
  target_legs: "self"
483
  }).catch(()=>{});
484
 
485
- // ── BACKGROUND AI PROCESSING ──
486
  try {
487
  const aiRes = await fetch(`${AI_SERVER_URL}/api/process-audio-turn?voice=${voice}&call_id=${callControlId}`, {
488
  method: "POST", headers: { "Content-Type": "audio/wav" }, body: wavBuffer
@@ -492,7 +482,6 @@ wss.on("connection", (ws, req) => {
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;
 
51
  const clearCallTimers = (entry) => {
52
  if (!entry || !entry.timers) return;
53
  Object.values(entry.timers).forEach(clearTimeout);
54
+ entry.timers = {}; // clear out the objects
55
  };
56
 
57
  // ─── VAD MATH UTILITIES ───
 
216
  `);
217
  });
218
 
219
+ // ── FIX: Strip Circular Timer Objects Before Sending to Dashboard ──
220
+ app.get("/api/logs", (req, res) => {
221
+ const safeLogs = callLogs.map(entry => {
222
+ // Destructure to remove 'timers' from the object we send to the frontend
223
+ const { timers, ...safeEntry } = entry;
224
+ return safeEntry;
225
+ });
226
+ res.json(safeLogs.reverse());
227
+ });
228
+
229
  app.get("/health", (req, res) => res.json({ status: "ok", uptime: process.uptime() }));
230
 
231
  // ─── PROXY FOR TELNYX RECORDINGS ───────────────────────────
 
275
  recording_url: null,
276
  raw_telnyx_url: null,
277
  is_playing: false,
278
+ timers: {}, // Timers object correctly initialized
279
  created_at: new Date().toISOString()
280
  });
281
 
 
298
  }
299
  });
300
 
301
+ // ─── TELNYX WEBHOOK ────────────────────────────────────────
302
  app.post("/webhook/call", async (req, res) => {
303
  const type = req.body?.data?.event_type;
304
  const payload = req.body?.data?.payload;
 
308
 
309
  const entry = callLogs.find(l => l.call_control_id === callId);
310
 
 
311
  if (entry && entry.status === "completed" && type !== "call.recording.saved") {
312
  return res.sendStatus(200);
313
  }
 
335
  log("ANSWERED", `Call ${callId} connected.`);
336
  updateLog(callId, { status: "in-progress" });
337
 
 
338
  await sendTelnyxCmd(callId, "record_start", { format: "mp3", channels: "dual", play_beep: false });
339
 
 
 
 
 
 
 
 
 
 
340
  // Start WebSocket Media Stream to listen to the caller natively via VAD
341
  const wsUrl = `wss://${req.headers.host}/audio-stream`;
342
+
343
+ // Changed to `fork_start` as it's the more stable command for Telnyx V2 Media Streaming
344
+ await sendTelnyxCmd(callId, "fork_start", { stream_url: wsUrl, track: "inbound_track" });
345
 
346
  if (entry) {
347
  entry.timers.hangup = setTimeout(async () => {
 
358
  break;
359
 
360
  case "call.recording.saved":
 
361
  const rawUrl = payload.recording_urls.mp3;
362
  log("RECORDING URL", `\n\n⬇️ DOWNLOAD RAW AUDIO HERE ⬇️\n${rawUrl}\n\n`);
363
 
 
387
  let isProcessingTurn = false;
388
  let hasGreeted = false;
389
 
390
+ const SILENCE_THRESHOLD_FRAMES = 25;
391
+ const VOLUME_THRESHOLD = 400;
392
 
393
  ws.on("message", async (raw) => {
394
  try {
 
398
  callControlId = msg.start?.call_control_id;
399
  log("WS_STREAM", `Receiving audio for call ${callControlId}`);
400
 
 
401
  setTimeout(async () => {
402
  if (!hasGreeted && callControlId) {
403
  const entry = callLogs.find(l => l.call_control_id === callControlId);
 
420
  const pcm16 = decodeMuLaw(mulawBuffer);
421
  const rms = calculateRMS(pcm16);
422
 
 
423
  if (rms > VOLUME_THRESHOLD) {
424
  hasGreeted = true;
425
  silenceFrames = 0;
 
430
  isProcessingTurn = false;
431
  log("VAD", "Human started speaking.");
432
 
 
433
  const entry = callLogs.find(l => l.call_control_id === callControlId);
434
  if (entry && entry.is_playing) {
435
  log("BARGE-IN", "Interrupting AI playback.");
 
438
  }
439
  }
440
  }
 
 
441
  else if (isSpeaking) {
442
  pcmChunks.push(pcm16);
443
  silenceFrames++;
444
 
 
445
  if (silenceFrames > SILENCE_THRESHOLD_FRAMES && !isProcessingTurn) {
446
  log("VAD", "500ms silence detected. Turn ended.");
447
  isSpeaking = false;
 
455
  const finalPcm = new Int16Array(totalLength);
456
  let offset = 0;
457
  for (let chunk of pcmChunks) { finalPcm.set(chunk, offset); offset += chunk.length; }
458
+ pcmChunks = [];
459
 
 
460
  if (totalLength < 8000 * 0.2) {
461
  isProcessingTurn = false;
462
  return;
 
464
 
465
  const wavBuffer = pcm16ToWav(finalPcm, 8000);
466
 
 
467
  const fillers = ["Right.", "Got it.", "Okay."];
468
  const filler = fillers[Math.floor(Math.random() * fillers.length)];
469
 
 
473
  target_legs: "self"
474
  }).catch(()=>{});
475
 
 
476
  try {
477
  const aiRes = await fetch(`${AI_SERVER_URL}/api/process-audio-turn?voice=${voice}&call_id=${callControlId}`, {
478
  method: "POST", headers: { "Content-Type": "audio/wav" }, body: wavBuffer
 
482
  const aiData = await aiRes.json();
483
  if (aiData.transcript) updateLog(callControlId, { last_transcript: aiData.transcript });
484
 
 
485
  if (!isSpeaking && aiData.speak_url) {
486
  log("AI_REPLY", `Playing full reply.`);
487
  if (entry) entry.is_playing = true;