Update app.js
Browse files
app.js
CHANGED
|
@@ -25,7 +25,6 @@ const log = (tag, ...args) => {
|
|
| 25 |
let callLogs = [];
|
| 26 |
|
| 27 |
// βββ UTILITY: GLOBAL TELNYX COMMAND DISPATCHER βββ
|
| 28 |
-
// STRICTLY FIXED: https://api.telnyx.com/v2/calls/...
|
| 29 |
const sendTelnyxCmd = async (callId, action, body = {}) => {
|
| 30 |
try {
|
| 31 |
const response = await fetch(`https://api.telnyx.com/v2/calls/${callId}/actions/${action}`, {
|
|
@@ -214,6 +213,7 @@ app.get("/health", (req, res) => res.json({ status: "ok", uptime: process.uptime
|
|
| 214 |
app.get("/play-recording/:call_control_id", async (req, res) => {
|
| 215 |
const logEntry = callLogs.find(l => l.call_control_id === req.params.call_control_id);
|
| 216 |
if (!logEntry || !logEntry.raw_telnyx_url) return res.sendStatus(404);
|
|
|
|
| 217 |
try {
|
| 218 |
const s3Res = await fetch(logEntry.raw_telnyx_url);
|
| 219 |
if (!s3Res.ok) return res.sendStatus(s3Res.status);
|
|
@@ -229,7 +229,8 @@ app.get("/play-recording/:call_control_id", async (req, res) => {
|
|
| 229 |
// βββ DIAL OUT ROUTE ββββββββββββββββββββββββββββββββββββββββ
|
| 230 |
app.post("/dial", async (req, res) => {
|
| 231 |
const { to } = req.body;
|
| 232 |
-
|
|
|
|
| 233 |
log("DIAL", `Dialing ${to}...`);
|
| 234 |
|
| 235 |
try {
|
|
@@ -306,7 +307,7 @@ app.post("/webhook/call", async (req, res) => {
|
|
| 306 |
|
| 307 |
callLogs.push({
|
| 308 |
id: randomUUID(), call_control_id: callId, target_number: payload?.from,
|
| 309 |
-
voice_used: "
|
| 310 |
raw_telnyx_url: null, is_playing: false, timers: {}, created_at: new Date().toISOString()
|
| 311 |
});
|
| 312 |
await sendTelnyxCmd(callId, "answer");
|
|
@@ -319,7 +320,6 @@ app.post("/webhook/call", async (req, res) => {
|
|
| 319 |
|
| 320 |
await sendTelnyxCmd(callId, "record_start", { format: "mp3", channels: "dual", play_beep: false });
|
| 321 |
|
| 322 |
-
// Open WebSocket purely for the VAD staller
|
| 323 |
const wsUrl = `wss://${req.headers.host}/audio-stream`;
|
| 324 |
log("STREAM", "Connecting WebSocket for early turn detection...");
|
| 325 |
await sendTelnyxCmd(callId, "streaming_start", {
|
|
@@ -327,7 +327,6 @@ app.post("/webhook/call", async (req, res) => {
|
|
| 327 |
stream_track: "inbound_track"
|
| 328 |
});
|
| 329 |
|
| 330 |
-
// Start Native STT to handle the actual text
|
| 331 |
log("STT", "Activating native Telnyx real-time transcription engine...");
|
| 332 |
await sendTelnyxCmd(callId, "transcription_start", {
|
| 333 |
transcription_engine: "Telnyx",
|
|
@@ -352,7 +351,7 @@ app.post("/webhook/call", async (req, res) => {
|
|
| 352 |
const data = payload?.transcription_data;
|
| 353 |
if (!data) break;
|
| 354 |
|
| 355 |
-
const voice = entry ? entry.voice_used : "
|
| 356 |
|
| 357 |
// 1. BARGE-IN (User is currently speaking)
|
| 358 |
if (!data.is_final) {
|
|
@@ -371,7 +370,7 @@ app.post("/webhook/call", async (req, res) => {
|
|
| 371 |
log("TRANSCRIPT", `User said: "${transcript}"`);
|
| 372 |
updateLog(callId, { last_transcript: transcript });
|
| 373 |
|
| 374 |
-
//
|
| 375 |
try {
|
| 376 |
const aiRes = await fetch(`${AI_SERVER_URL}/api/process-text-turn`, {
|
| 377 |
method: "POST",
|
|
@@ -382,7 +381,7 @@ app.post("/webhook/call", async (req, res) => {
|
|
| 382 |
if (aiRes.ok) {
|
| 383 |
const aiData = await aiRes.json();
|
| 384 |
if (aiData.speak_url && entry && entry.status !== "completed") {
|
| 385 |
-
log("AI_REPLY", `Playing full reply.`);
|
| 386 |
updateLog(callId, { is_playing: true });
|
| 387 |
await sendTelnyxCmd(callId, "playback_start", {
|
| 388 |
audio_url: aiData.speak_url,
|
|
@@ -414,9 +413,9 @@ app.post("/webhook/call", async (req, res) => {
|
|
| 414 |
res.sendStatus(200);
|
| 415 |
});
|
| 416 |
|
| 417 |
-
// βββ WEBSOCKET (
|
| 418 |
wss.on("connection", (ws, req) => {
|
| 419 |
-
log("WS", `VAD WebSocket connected
|
| 420 |
|
| 421 |
let isSpeaking = false;
|
| 422 |
let silenceFrames = 0;
|
|
@@ -425,8 +424,9 @@ wss.on("connection", (ws, req) => {
|
|
| 425 |
let hasGreeted = false;
|
| 426 |
let totalNoiseFrames = 0;
|
| 427 |
|
| 428 |
-
|
| 429 |
-
const
|
|
|
|
| 430 |
|
| 431 |
ws.on("message", async (raw) => {
|
| 432 |
try {
|
|
@@ -434,14 +434,13 @@ wss.on("connection", (ws, req) => {
|
|
| 434 |
|
| 435 |
if (msg.event === "start") {
|
| 436 |
callControlId = msg.start?.call_control_id;
|
| 437 |
-
log("WS_STREAM", `Receiving audio for call ${callControlId}`);
|
| 438 |
|
| 439 |
// Smart Initial Greeting
|
| 440 |
setTimeout(async () => {
|
| 441 |
if (!hasGreeted && callControlId) {
|
| 442 |
const entry = callLogs.find(l => l.call_control_id === callControlId);
|
| 443 |
if (entry && entry.status === "in-progress") {
|
| 444 |
-
const voice = entry.voice_used || "
|
| 445 |
log("SMART_GREETING", "No immediate human audio detected. Saying Hello.");
|
| 446 |
entry.is_playing = true;
|
| 447 |
hasGreeted = true;
|
|
@@ -459,7 +458,6 @@ wss.on("connection", (ws, req) => {
|
|
| 459 |
const pcm16 = decodeMuLaw(mulawBuffer);
|
| 460 |
const rms = calculateRMS(pcm16);
|
| 461 |
|
| 462 |
-
// ββ 1. NOISE DETECTED (Human is speaking) ββ
|
| 463 |
if (rms > VOLUME_THRESHOLD) {
|
| 464 |
hasGreeted = true;
|
| 465 |
silenceFrames = 0;
|
|
@@ -469,7 +467,6 @@ wss.on("connection", (ws, req) => {
|
|
| 469 |
isSpeaking = true;
|
| 470 |
hasPlayedFiller = false;
|
| 471 |
|
| 472 |
-
// BARGE IN: If AI is talking, tell it to shut up
|
| 473 |
const entry = callLogs.find(l => l.call_control_id === callControlId);
|
| 474 |
if (entry && entry.is_playing) {
|
| 475 |
log("BARGE-IN", "User is speaking. Interrupting AI playback.");
|
|
@@ -478,21 +475,20 @@ wss.on("connection", (ws, req) => {
|
|
| 478 |
}
|
| 479 |
}
|
| 480 |
}
|
| 481 |
-
|
| 482 |
-
// ββ 2. SILENCE DETECTED ββ
|
| 483 |
else if (isSpeaking) {
|
| 484 |
silenceFrames++;
|
| 485 |
|
| 486 |
-
// ββ 3. EARLY STALL TRIGGER (500ms silence) ββ
|
| 487 |
if (silenceFrames > SILENCE_THRESHOLD_FRAMES && !hasPlayedFiller) {
|
| 488 |
hasPlayedFiller = true;
|
| 489 |
isSpeaking = false;
|
| 490 |
|
| 491 |
-
//
|
| 492 |
-
if
|
| 493 |
-
|
|
|
|
|
|
|
| 494 |
const entry = callLogs.find(l => l.call_control_id === callControlId);
|
| 495 |
-
const voice = entry ? entry.voice_used : "
|
| 496 |
|
| 497 |
const fillers = ["Right.", "Got it.", "Okay.", "Yeah.", "Ah, okay."];
|
| 498 |
const filler = fillers[Math.floor(Math.random() * fillers.length)];
|
|
@@ -517,5 +513,5 @@ requiredEnv.forEach((key) => {
|
|
| 517 |
});
|
| 518 |
|
| 519 |
server.listen(PORT, "0.0.0.0", () => {
|
| 520 |
-
log("STARTUP", `Telecom Server running on port ${PORT}`);
|
| 521 |
});
|
|
|
|
| 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}`, {
|
|
|
|
| 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);
|
|
|
|
| 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 {
|
|
|
|
| 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");
|
|
|
|
| 320 |
|
| 321 |
await sendTelnyxCmd(callId, "record_start", { format: "mp3", channels: "dual", play_beep: false });
|
| 322 |
|
|
|
|
| 323 |
const wsUrl = `wss://${req.headers.host}/audio-stream`;
|
| 324 |
log("STREAM", "Connecting WebSocket for early turn detection...");
|
| 325 |
await sendTelnyxCmd(callId, "streaming_start", {
|
|
|
|
| 327 |
stream_track: "inbound_track"
|
| 328 |
});
|
| 329 |
|
|
|
|
| 330 |
log("STT", "Activating native Telnyx real-time transcription engine...");
|
| 331 |
await sendTelnyxCmd(callId, "transcription_start", {
|
| 332 |
transcription_engine: "Telnyx",
|
|
|
|
| 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) {
|
|
|
|
| 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 |
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 |
res.sendStatus(200);
|
| 414 |
});
|
| 415 |
|
| 416 |
+
// βββ WEBSOCKET (VAD & Early Staller) βββ
|
| 417 |
wss.on("connection", (ws, req) => {
|
| 418 |
+
log("WS", `VAD WebSocket connected`);
|
| 419 |
|
| 420 |
let isSpeaking = false;
|
| 421 |
let silenceFrames = 0;
|
|
|
|
| 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 {
|
|
|
|
| 434 |
|
| 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;
|
|
|
|
| 458 |
const pcm16 = decodeMuLaw(mulawBuffer);
|
| 459 |
const rms = calculateRMS(pcm16);
|
| 460 |
|
|
|
|
| 461 |
if (rms > VOLUME_THRESHOLD) {
|
| 462 |
hasGreeted = true;
|
| 463 |
silenceFrames = 0;
|
|
|
|
| 467 |
isSpeaking = true;
|
| 468 |
hasPlayedFiller = false;
|
| 469 |
|
|
|
|
| 470 |
const entry = callLogs.find(l => l.call_control_id === callControlId);
|
| 471 |
if (entry && entry.is_playing) {
|
| 472 |
log("BARGE-IN", "User is speaking. Interrupting AI playback.");
|
|
|
|
| 475 |
}
|
| 476 |
}
|
| 477 |
}
|
|
|
|
|
|
|
| 478 |
else if (isSpeaking) {
|
| 479 |
silenceFrames++;
|
| 480 |
|
|
|
|
| 481 |
if (silenceFrames > SILENCE_THRESHOLD_FRAMES && !hasPlayedFiller) {
|
| 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)];
|
|
|
|
| 513 |
});
|
| 514 |
|
| 515 |
server.listen(PORT, "0.0.0.0", () => {
|
| 516 |
+
log("STARTUP", `Elite Telecom Server running on port ${PORT}`);
|
| 517 |
});
|