Pepguy commited on
Commit
233fa4e
·
verified ·
1 Parent(s): d644408

Update app.js

Browse files
Files changed (1) hide show
  1. app.js +20 -35
app.js CHANGED
@@ -24,7 +24,6 @@ const log = (tag, ...args) => {
24
 
25
  let callLogs = [];
26
 
27
- // ─── UTILITY: GLOBAL TELNYX COMMAND DISPATCHER ───
28
  const sendTelnyxCmd = async (callId, action, body = {}) => {
29
  try {
30
  const response = await fetch(`https://api.telnyx.com/v2/calls/${callId}/actions/${action}`, {
@@ -32,7 +31,6 @@ const sendTelnyxCmd = async (callId, action, body = {}) => {
32
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${TELNYX_API_KEY}` },
33
  body: JSON.stringify(body)
34
  });
35
-
36
  if (!response.ok) {
37
  log("TELNYX CMD ERROR", `[${action}] failed (${response.status}): ${await response.text()}`);
38
  } else {
@@ -56,7 +54,6 @@ const clearCallTimers = (entry) => {
56
  entry.timers = {};
57
  };
58
 
59
- // ─── VAD MATH UTILITIES ───
60
  const MU_LAW_TABLE = new Int16Array(256);
61
  for (let i = 0; i < 256; i++) {
62
  let sign = (i & 0x80) ? -1 : 1;
@@ -78,9 +75,6 @@ function calculateRMS(pcm16Array) {
78
  return Math.sqrt(sum / pcm16Array.length);
79
  }
80
 
81
- // ─────────────────────────────────────────────
82
- // 1. EMBEDDED DASHBOARD
83
- // ─────────────────────────────────────────────
84
  app.get("/", (req, res) => {
85
  res.send(`
86
  <!DOCTYPE html>
@@ -209,11 +203,9 @@ app.get("/api/logs", (req, res) => {
209
 
210
  app.get("/health", (req, res) => res.json({ status: "ok", uptime: process.uptime() }));
211
 
212
- // ─── PROXY FOR TELNYX RECORDINGS ───────────────────────────
213
  app.get("/play-recording/:call_control_id", async (req, res) => {
214
  const logEntry = callLogs.find(l => l.call_control_id === req.params.call_control_id);
215
  if (!logEntry || !logEntry.raw_telnyx_url) return res.sendStatus(404);
216
-
217
  try {
218
  const s3Res = await fetch(logEntry.raw_telnyx_url);
219
  if (!s3Res.ok) return res.sendStatus(s3Res.status);
@@ -226,11 +218,9 @@ app.get("/play-recording/:call_control_id", async (req, res) => {
226
  }
227
  });
228
 
229
- // ─── DIAL OUT ROUTE ────────────────────────────────────────
230
  app.post("/dial", async (req, res) => {
231
  const { to } = req.body;
232
- // SETTING ENCELADUS AS THE DEFAULT VOICE
233
- const voice = "Enceladus";
234
  log("DIAL", `Dialing ${to}...`);
235
 
236
  try {
@@ -268,7 +258,6 @@ app.post("/dial", async (req, res) => {
268
  }
269
  });
270
 
271
- // ─── MANUAL HANG UP ────────────────────────────────────────
272
  app.post("/call/hangup", async (req, res) => {
273
  const { call_control_id } = req.body;
274
  if (!call_control_id) return res.status(400).json({ error: "Missing call_control_id" });
@@ -281,7 +270,6 @@ app.post("/call/hangup", async (req, res) => {
281
  }
282
  });
283
 
284
- // ─── HYBRID WEBHOOK (Telnyx STT Engine + VAD) ──────────────
285
  app.post("/webhook/call", async (req, res) => {
286
  const type = req.body?.data?.event_type;
287
  const payload = req.body?.data?.payload;
@@ -307,7 +295,7 @@ app.post("/webhook/call", async (req, res) => {
307
 
308
  callLogs.push({
309
  id: randomUUID(), call_control_id: callId, target_number: payload?.from,
310
- voice_used: "Enceladus", status: "in-progress", last_transcript: "", recording_url: null,
311
  raw_telnyx_url: null, is_playing: false, timers: {}, created_at: new Date().toISOString()
312
  });
313
  await sendTelnyxCmd(callId, "answer");
@@ -316,7 +304,7 @@ app.post("/webhook/call", async (req, res) => {
316
 
317
  case "call.answered":
318
  log("ANSWERED", `Call ${callId} connected.`);
319
- updateLog(callId, { status: "in-progress" });
320
 
321
  await sendTelnyxCmd(callId, "record_start", { format: "mp3", channels: "dual", play_beep: false });
322
 
@@ -351,9 +339,8 @@ app.post("/webhook/call", async (req, res) => {
351
  const data = payload?.transcription_data;
352
  if (!data) break;
353
 
354
- const voice = entry ? entry.voice_used : "Enceladus";
355
 
356
- // 1. BARGE-IN (User is currently speaking)
357
  if (!data.is_final) {
358
  if (entry && entry.is_playing) {
359
  log("BARGE-IN", "User is speaking. Shutting up AI.");
@@ -362,7 +349,6 @@ app.post("/webhook/call", async (req, res) => {
362
  }
363
  }
364
 
365
- // 2. TURN ENDED (User finished speaking)
366
  if (data.is_final) {
367
  const transcript = data.transcript.trim();
368
  if (!transcript) break;
@@ -370,7 +356,6 @@ app.post("/webhook/call", async (req, res) => {
370
  log("TRANSCRIPT", `User said: "${transcript}"`);
371
  updateLog(callId, { last_transcript: transcript });
372
 
373
- // FETCH FULL AI RESPONSE
374
  try {
375
  const aiRes = await fetch(`${AI_SERVER_URL}/api/process-text-turn`, {
376
  method: "POST",
@@ -381,7 +366,7 @@ app.post("/webhook/call", async (req, res) => {
381
  if (aiRes.ok) {
382
  const aiData = await aiRes.json();
383
  if (aiData.speak_url && entry && entry.status !== "completed") {
384
- log("AI_REPLY", `Playing full reply: ${aiData.raw_text}`);
385
  updateLog(callId, { is_playing: true });
386
  await sendTelnyxCmd(callId, "playback_start", {
387
  audio_url: aiData.speak_url,
@@ -413,7 +398,6 @@ app.post("/webhook/call", async (req, res) => {
413
  res.sendStatus(200);
414
  });
415
 
416
- // ─── WEBSOCKET (VAD & Early Staller) ───
417
  wss.on("connection", (ws, req) => {
418
  log("WS", `VAD WebSocket connected`);
419
 
@@ -424,9 +408,8 @@ wss.on("connection", (ws, req) => {
424
  let hasGreeted = false;
425
  let totalNoiseFrames = 0;
426
 
427
- // INCREASED THRESHOLDS FOR A MORE PATIENT AI
428
- const SILENCE_THRESHOLD_FRAMES = 37; // ~750ms of silence needed before declaring a turn end
429
- const VOLUME_THRESHOLD = 500; // Require louder noise to prevent false barge-ins
430
 
431
  ws.on("message", async (raw) => {
432
  try {
@@ -435,12 +418,11 @@ wss.on("connection", (ws, req) => {
435
  if (msg.event === "start") {
436
  callControlId = msg.start?.call_control_id;
437
 
438
- // Smart Initial Greeting
439
  setTimeout(async () => {
440
  if (!hasGreeted && callControlId) {
441
  const entry = callLogs.find(l => l.call_control_id === callControlId);
442
  if (entry && entry.status === "in-progress") {
443
- const voice = entry.voice_used || "Enceladus";
444
  log("SMART_GREETING", "No immediate human audio detected. Saying Hello.");
445
  entry.is_playing = true;
446
  hasGreeted = true;
@@ -482,23 +464,26 @@ wss.on("connection", (ws, req) => {
482
  hasPlayedFiller = true;
483
  isSpeaking = false;
484
 
485
- // LAZY BACKCHANNELING:
486
- // Only play a filler word if they spoke continuously for ~800ms
487
- // This prevents it from saying "Okay" to an IVR breath or a short "Yeah".
488
- if (totalNoiseFrames > 40) {
489
- log("VAD_EARLY_TURN", "Long sentence detected. Playing early staller...");
490
- const entry = callLogs.find(l => l.call_control_id === callControlId);
491
- const voice = entry ? entry.voice_used : "Enceladus";
492
 
 
 
493
  const fillers = ["Right.", "Got it.", "Okay.", "Yeah.", "Ah, okay."];
494
  const filler = fillers[Math.floor(Math.random() * fillers.length)];
495
-
496
  if (entry) entry.is_playing = true;
497
  sendTelnyxCmd(callControlId, "playback_start", {
498
  audio_url: `${AI_SERVER_URL}/play-audio?voice=${voice}&phrase=${encodeURIComponent(filler)}`,
499
  target_legs: "self"
500
- }).catch(()=>{});
 
 
501
  }
 
502
  totalNoiseFrames = 0;
503
  }
504
  }
 
24
 
25
  let callLogs = [];
26
 
 
27
  const sendTelnyxCmd = async (callId, action, body = {}) => {
28
  try {
29
  const response = await fetch(`https://api.telnyx.com/v2/calls/${callId}/actions/${action}`, {
 
31
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${TELNYX_API_KEY}` },
32
  body: JSON.stringify(body)
33
  });
 
34
  if (!response.ok) {
35
  log("TELNYX CMD ERROR", `[${action}] failed (${response.status}): ${await response.text()}`);
36
  } else {
 
54
  entry.timers = {};
55
  };
56
 
 
57
  const MU_LAW_TABLE = new Int16Array(256);
58
  for (let i = 0; i < 256; i++) {
59
  let sign = (i & 0x80) ? -1 : 1;
 
75
  return Math.sqrt(sum / pcm16Array.length);
76
  }
77
 
 
 
 
78
  app.get("/", (req, res) => {
79
  res.send(`
80
  <!DOCTYPE html>
 
203
 
204
  app.get("/health", (req, res) => res.json({ status: "ok", uptime: process.uptime() }));
205
 
 
206
  app.get("/play-recording/:call_control_id", async (req, res) => {
207
  const logEntry = callLogs.find(l => l.call_control_id === req.params.call_control_id);
208
  if (!logEntry || !logEntry.raw_telnyx_url) return res.sendStatus(404);
 
209
  try {
210
  const s3Res = await fetch(logEntry.raw_telnyx_url);
211
  if (!s3Res.ok) return res.sendStatus(s3Res.status);
 
218
  }
219
  });
220
 
 
221
  app.post("/dial", async (req, res) => {
222
  const { to } = req.body;
223
+ const voice = "Enceladus_2";
 
224
  log("DIAL", `Dialing ${to}...`);
225
 
226
  try {
 
258
  }
259
  });
260
 
 
261
  app.post("/call/hangup", async (req, res) => {
262
  const { call_control_id } = req.body;
263
  if (!call_control_id) return res.status(400).json({ error: "Missing call_control_id" });
 
270
  }
271
  });
272
 
 
273
  app.post("/webhook/call", async (req, res) => {
274
  const type = req.body?.data?.event_type;
275
  const payload = req.body?.data?.payload;
 
295
 
296
  callLogs.push({
297
  id: randomUUID(), call_control_id: callId, target_number: payload?.from,
298
+ voice_used: "Enceladus_2", status: "in-progress", last_transcript: "", recording_url: null,
299
  raw_telnyx_url: null, is_playing: false, timers: {}, created_at: new Date().toISOString()
300
  });
301
  await sendTelnyxCmd(callId, "answer");
 
304
 
305
  case "call.answered":
306
  log("ANSWERED", `Call ${callId} connected.`);
307
+ updateLog(callId, { status: "in-progress", answered_at: new Date().toISOString() });
308
 
309
  await sendTelnyxCmd(callId, "record_start", { format: "mp3", channels: "dual", play_beep: false });
310
 
 
339
  const data = payload?.transcription_data;
340
  if (!data) break;
341
 
342
+ const voice = entry ? entry.voice_used : "Enceladus_2";
343
 
 
344
  if (!data.is_final) {
345
  if (entry && entry.is_playing) {
346
  log("BARGE-IN", "User is speaking. Shutting up AI.");
 
349
  }
350
  }
351
 
 
352
  if (data.is_final) {
353
  const transcript = data.transcript.trim();
354
  if (!transcript) break;
 
356
  log("TRANSCRIPT", `User said: "${transcript}"`);
357
  updateLog(callId, { last_transcript: transcript });
358
 
 
359
  try {
360
  const aiRes = await fetch(`${AI_SERVER_URL}/api/process-text-turn`, {
361
  method: "POST",
 
366
  if (aiRes.ok) {
367
  const aiData = await aiRes.json();
368
  if (aiData.speak_url && entry && entry.status !== "completed") {
369
+ log("AI_REPLY", `Playing full reply.`);
370
  updateLog(callId, { is_playing: true });
371
  await sendTelnyxCmd(callId, "playback_start", {
372
  audio_url: aiData.speak_url,
 
398
  res.sendStatus(200);
399
  });
400
 
 
401
  wss.on("connection", (ws, req) => {
402
  log("WS", `VAD WebSocket connected`);
403
 
 
408
  let hasGreeted = false;
409
  let totalNoiseFrames = 0;
410
 
411
+ const SILENCE_THRESHOLD_FRAMES = 37;
412
+ const VOLUME_THRESHOLD = 500;
 
413
 
414
  ws.on("message", async (raw) => {
415
  try {
 
418
  if (msg.event === "start") {
419
  callControlId = msg.start?.call_control_id;
420
 
 
421
  setTimeout(async () => {
422
  if (!hasGreeted && callControlId) {
423
  const entry = callLogs.find(l => l.call_control_id === callControlId);
424
  if (entry && entry.status === "in-progress") {
425
+ const voice = entry.voice_used || "Enceladus_2";
426
  log("SMART_GREETING", "No immediate human audio detected. Saying Hello.");
427
  entry.is_playing = true;
428
  hasGreeted = true;
 
464
  hasPlayedFiller = true;
465
  isSpeaking = false;
466
 
467
+ const entry = callLogs.find(l => l.call_control_id === callControlId);
468
+ const voice = entry ? entry.voice_used : "Enceladus_2";
469
+
470
+ const callAge = entry?.answered_at
471
+ ? Date.now() - new Date(entry.answered_at).getTime()
472
+ : 0;
 
473
 
474
+ if (totalNoiseFrames > 40 && callAge > 8000) {
475
+ log("VAD_EARLY_TURN", "Long sentence detected. Playing early staller...");
476
  const fillers = ["Right.", "Got it.", "Okay.", "Yeah.", "Ah, okay."];
477
  const filler = fillers[Math.floor(Math.random() * fillers.length)];
 
478
  if (entry) entry.is_playing = true;
479
  sendTelnyxCmd(callControlId, "playback_start", {
480
  audio_url: `${AI_SERVER_URL}/play-audio?voice=${voice}&phrase=${encodeURIComponent(filler)}`,
481
  target_legs: "self"
482
+ }).catch(() => {});
483
+ } else {
484
+ log("VAD_BLACKOUT", `Suppressed filler (call age: ${callAge}ms)`);
485
  }
486
+
487
  totalNoiseFrames = 0;
488
  }
489
  }