| import express from "express"; |
| import { createServer } from "http"; |
| import { WebSocketServer } from "ws"; |
| import { randomUUID } from "crypto"; |
|
|
| const app = express(); |
| const server = createServer(app); |
| const wss = new WebSocketServer({ server, path: "/audio-stream" }); |
|
|
| app.use(express.static("public", { index: false })); |
| app.use(express.json()); |
| app.use(express.urlencoded({ extended: true })); |
|
|
| const PORT = process.env.PORT || 7860; |
| const TELNYX_API_KEY = process.env.TELNYX_API_KEY; |
| const TELNYX_CONNECTION_ID = process.env.TELNYX_CONNECTION_ID; |
| const TELNYX_FROM_NUMBER = process.env.TELNYX_FROM_NUMBER; |
| const AI_SERVER_URL = process.env.AI_SERVER_URL; |
|
|
| const ACTIVE_VOICE = "Enceladus_3"; |
|
|
| const log = (tag, ...args) => console.log(`[${new Date().toISOString()}] [${tag}]`, ...args); |
| let callLogs = []; |
|
|
| const sendTelnyxCmd = async (callId, action, body = {}) => { |
| if (action === "playback_start") { |
| updateLog(callId, { is_playing: true, playback_started_at: Date.now() }); |
| } |
| try { |
| const response = await fetch(`https://api.telnyx.com/v2/calls/${callId}/actions/${action}`, { |
| method: "POST", |
| headers: { "Content-Type": "application/json", Authorization: `Bearer ${TELNYX_API_KEY}` }, |
| body: JSON.stringify(body) |
| }); |
| if (!response.ok) log("TELNYX CMD ERROR", `[${action}] ${await response.text()}`); |
| else log("TELNYX CMD SUCCESS", `[${action}] accepted.`); |
| return response; |
| } catch (e) { log("TELNYX CMD NETWORK ERROR", e.message); } |
| }; |
|
|
| const updateLog = (callId, updates) => { |
| const entry = callLogs.find(l => l.call_control_id === callId); |
| if (entry) Object.assign(entry, updates); |
| return entry; |
| }; |
|
|
| |
| const MU_LAW_TABLE = new Int16Array(256); |
| for (let i = 0; i < 256; i++) { |
| const sign = (i & 0x80) ? -1 : 1; |
| const exponent = (~i >> 4) & 0x07; |
| const mantissa = ~i & 0x0f; |
| const sample = ((mantissa << 3) + 132) << exponent; |
| MU_LAW_TABLE[i] = sign * (sample - 132); |
| } |
| function decodeMuLaw(buffer) { |
| const out = new Int16Array(buffer.length); |
| for (let i = 0; i < buffer.length; i++) out[i] = MU_LAW_TABLE[buffer[i]]; |
| return out; |
| } |
| function calculateRMS(pcm) { |
| let sum = 0; |
| for (let i = 0; i < pcm.length; i++) sum += pcm[i] * pcm[i]; |
| return Math.sqrt(sum / pcm.length); |
| } |
|
|
| |
| app.get("/", (req, res) => { |
| res.send(`<!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Elite Telecom Control Panel</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <script src="https://unpkg.com/react@18.2.0/umd/react.production.min.js"></script> |
| <script src="https://unpkg.com/react-dom@18.2.0/umd/react-dom.production.min.js"></script> |
| <script src="https://unpkg.com/@babel/standalone@7.23.10/babel.min.js"></script> |
| </head> |
| <body class="bg-gray-900 text-gray-100 p-8 font-sans"> |
| <div id="root" class="max-w-4xl mx-auto"></div> |
| <script type="text/babel"> |
| const { useState, useEffect } = React; |
| function App() { |
| const [logs, setLogs] = useState([]); |
| const [targetNumber, setTargetNumber] = useState("+1"); |
| const [status, setStatus] = useState(""); |
| const fetchLogs = () => fetch("/api/logs").then(r => r.json()).then(setLogs); |
| useEffect(() => { fetchLogs(); const t = setInterval(fetchLogs, 2000); return () => clearInterval(t); }, []); |
| const dial = async () => { |
| setStatus("Dialing..."); |
| const res = await fetch("/dial", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ to: targetNumber }) }); |
| const data = await res.json(); |
| setStatus(data.ok ? "Call initiated!" : "Error: " + data.error); |
| fetchLogs(); |
| }; |
| const hangup = async (id) => { |
| await fetch("/call/hangup", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ call_control_id: id }) }); |
| fetchLogs(); |
| }; |
| return ( |
| <div className="space-y-8"> |
| <h1 className="text-3xl font-bold border-b border-gray-700 pb-4">Web Agency Telecom Dashboard</h1> |
| <div className="bg-gray-800 p-6 rounded-lg flex gap-4 items-end"> |
| <div className="flex-1"> |
| <label className="block text-sm text-gray-400 mb-1">Target Phone Number</label> |
| <input type="text" value={targetNumber} onChange={e => setTargetNumber(e.target.value)} className="w-full bg-gray-900 border border-gray-700 rounded p-2 text-white outline-none focus:border-blue-500" /> |
| </div> |
| <button onClick={dial} className="bg-blue-600 hover:bg-blue-500 px-8 py-2 rounded font-bold transition">Trigger Outbound Call</button> |
| </div> |
| {status && <div className="text-sm text-yellow-400">{status}</div>} |
| <div className="bg-gray-800 rounded-lg overflow-hidden"> |
| <table className="w-full text-left"> |
| <thead className="bg-gray-900 text-gray-400 text-sm"> |
| <tr><th className="p-4">Call ID</th><th className="p-4">Number</th><th className="p-4">Last Transcript</th><th className="p-4">Status</th><th className="p-4">Action</th><th className="p-4">Recording</th></tr> |
| </thead> |
| <tbody className="divide-y divide-gray-700"> |
| {logs.map(l => ( |
| <tr key={l.id}> |
| <td className="p-4 text-xs font-mono text-gray-500">{l.call_control_id.slice(0,8)}...</td> |
| <td className="p-4 font-mono">{l.target_number}</td> |
| <td className="p-4 text-xs text-gray-300 italic truncate max-w-[200px]" title={l.last_transcript}>{l.last_transcript || "..."}</td> |
| <td className="p-4"> |
| <span className={\`inline-block px-2 py-1 rounded text-xs \${l.status === 'completed' ? 'bg-green-900 text-green-300' : 'bg-yellow-900 text-yellow-300'}\`}>{l.status}</span> |
| {l.is_playing && <span className="block text-xs text-purple-400 animate-pulse mt-1">AI speaking...</span>} |
| </td> |
| <td className="p-4"> |
| {l.status !== 'completed' ? <button onClick={() => hangup(l.call_control_id)} className="bg-red-600 hover:bg-red-500 text-xs px-3 py-1 rounded font-bold">Force Hang Up</button> : <span className="text-gray-500 text-xs">Ended</span>} |
| </td> |
| <td className="p-4">{l.recording_url ? <audio controls src={l.recording_url} className="h-8 w-48"></audio> : <span className="text-gray-500 text-sm">Recording...</span>}</td> |
| </tr> |
| ))} |
| {logs.length === 0 && <tr><td colSpan="6" className="p-4 text-center text-gray-500">No calls yet</td></tr>} |
| </tbody> |
| </table> |
| </div> |
| </div> |
| ); |
| } |
| ReactDOM.createRoot(document.getElementById('root')).render(<App />); |
| </script> |
| </body> |
| </html>`); |
| }); |
|
|
| app.get("/api/logs", (req, res) => { |
| res.json(callLogs.map(({ timers, ...e }) => e).reverse()); |
| }); |
|
|
| app.get("/health", (req, res) => res.json({ status: "ok" })); |
|
|
| app.get("/play-recording/:ccid", async (req, res) => { |
| const entry = callLogs.find(l => l.call_control_id === req.params.ccid); |
| if (!entry?.raw_telnyx_url) return res.sendStatus(404); |
| try { |
| const s3 = await fetch(entry.raw_telnyx_url); |
| if (!s3.ok) return res.sendStatus(s3.status); |
| const buf = Buffer.from(await s3.arrayBuffer()); |
| res.setHeader("Content-Type", "audio/mpeg"); |
| res.setHeader("Content-Length", buf.length); |
| res.send(buf); |
| } catch { res.sendStatus(500); } |
| }); |
|
|
| app.post("/dial", async (req, res) => { |
| const { to } = req.body; |
| log("DIAL", `Dialing ${to}...`); |
| try { |
| const response = await fetch("https://api.telnyx.com/v2/calls", { |
| method: "POST", |
| headers: { "Content-Type": "application/json", Authorization: `Bearer ${TELNYX_API_KEY}` }, |
| body: JSON.stringify({ |
| connection_id: TELNYX_CONNECTION_ID, |
| to, from: TELNYX_FROM_NUMBER, |
| custom_headers: [{ name: "X-Voice-Selection", value: ACTIVE_VOICE }] |
| }) |
| }); |
| const data = await response.json(); |
| if (!response.ok) return res.status(400).json({ ok: false, error: data?.errors?.[0]?.detail }); |
|
|
| callLogs.push({ |
| id: randomUUID(), call_control_id: data.data.call_control_id, |
| target_number: to, voice_used: ACTIVE_VOICE, status: "dialing", |
| last_transcript: "", recording_url: null, raw_telnyx_url: null, |
| is_playing: false, playback_started_at: null, |
| answered_at: null, timers: {}, created_at: new Date().toISOString() |
| }); |
| res.json({ ok: true }); |
| } catch (err) { res.status(500).json({ ok: false, error: err.message }); } |
| }); |
|
|
| app.post("/call/hangup", async (req, res) => { |
| const { call_control_id } = req.body; |
| if (!call_control_id) return res.status(400).json({ error: "Missing call_control_id" }); |
| await sendTelnyxCmd(call_control_id, "hangup"); |
| res.json({ ok: true }); |
| }); |
|
|
| |
| app.post("/webhook/call", async (req, res) => { |
| const type = req.body?.data?.event_type; |
| const payload = req.body?.data?.payload; |
| const callId = payload?.call_control_id; |
| if (!type) return res.sendStatus(200); |
|
|
| const entry = callLogs.find(l => l.call_control_id === callId); |
| if (entry?.status === "completed" && type !== "call.recording.saved") return res.sendStatus(200); |
|
|
| switch (type) { |
| case "call.initiated": |
| log("INITIATED", `dir=${payload?.direction}`); |
| if (payload?.direction === "incoming") { |
| callLogs.push({ |
| id: randomUUID(), call_control_id: callId, target_number: payload?.from, |
| voice_used: ACTIVE_VOICE, status: "in-progress", last_transcript: "", |
| recording_url: null, raw_telnyx_url: null, is_playing: false, |
| playback_started_at: null, answered_at: null, timers: {}, created_at: new Date().toISOString() |
| }); |
| await sendTelnyxCmd(callId, "answer"); |
| } |
| break; |
|
|
| case "call.answered": |
| log("ANSWERED", `Call ${callId} connected.`); |
| updateLog(callId, { status: "in-progress", answered_at: new Date().toISOString() }); |
| await sendTelnyxCmd(callId, "record_start", { format: "mp3", channels: "dual", play_beep: false }); |
| await sendTelnyxCmd(callId, "streaming_start", { |
| stream_url: `wss://${req.headers.host}/audio-stream`, |
| stream_track: "inbound_track" |
| }); |
| await sendTelnyxCmd(callId, "transcription_start", { |
| transcription_engine: "Telnyx", |
| transcription_tracks: "inbound" |
| }); |
| if (entry) { |
| entry.timers.hangup = setTimeout(async () => { |
| if (entry.status === "completed") return; |
| log("HANGUP", "Enforcing 90-second limit."); |
| await sendTelnyxCmd(callId, "hangup"); |
| }, 90000); |
| } |
| break; |
|
|
| case "call.playback.ended": |
| case "call.playback.stopped": |
| updateLog(callId, { is_playing: false }); |
| break; |
|
|
| case "call.transcription": |
| const tData = payload?.transcription_data; |
| if (!tData) break; |
| const voice = entry?.voice_used || ACTIVE_VOICE; |
|
|
| if (!tData.is_final && entry?.is_playing) { |
| log("BARGE-IN", "User speaking β stopping AI."); |
| updateLog(callId, { is_playing: false }); |
| await sendTelnyxCmd(callId, "playback_stop"); |
| } |
|
|
| if (tData.is_final) { |
| const transcript = tData.transcript.trim(); |
| if (!transcript) break; |
| log("TRANSCRIPT", `"${transcript}"`); |
| updateLog(callId, { last_transcript: transcript }); |
|
|
| try { |
| const aiRes = await fetch(`${AI_SERVER_URL}/api/process-text-turn`, { |
| method: "POST", |
| headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify({ text: transcript, voice, call_id: callId }) |
| }); |
| if (aiRes.ok) { |
| const aiData = await aiRes.json(); |
| if (aiData.speak_url && entry?.status !== "completed") { |
| log("AI_REPLY", `"${aiData.raw_text}"`); |
| await sendTelnyxCmd(callId, "playback_start", { |
| audio_url: aiData.speak_url, target_legs: "self" |
| }); |
| } |
| } |
| } catch (e) { log("AI_SERVER_ERROR", e.message); } |
| } |
| break; |
|
|
| case "call.recording.saved": |
| const rawUrl = payload.recording_urls.mp3; |
| log("RECORDING", rawUrl); |
| updateLog(callId, { raw_telnyx_url: rawUrl, recording_url: `/play-recording/${callId}` }); |
| break; |
|
|
| case "call.hangup": |
| updateLog(callId, { status: "completed", is_playing: false }); |
| if (entry?.timers) Object.values(entry.timers).forEach(clearTimeout); |
| log("CALL ENDED", "Terminated."); |
| break; |
| } |
| res.sendStatus(200); |
| }); |
|
|
| |
| wss.on("connection", (ws) => { |
| log("WS", "VAD WebSocket connected"); |
|
|
| let isSpeaking = false; |
| let silenceFrames = 0; |
| let callControlId = null; |
| let hasPlayedFiller = false; |
| let hasGreeted = false; |
| let totalNoiseFrames = 0; |
| let wasAiPlaying = false; |
|
|
| const SILENCE_THRESHOLD_FRAMES = 35; |
| const VOLUME_THRESHOLD = 500; |
| const ECHO_GRACE_PERIOD = 2000; |
|
|
| ws.on("message", async (raw) => { |
| try { |
| const msg = JSON.parse(raw); |
|
|
| if (msg.event === "start") { |
| callControlId = msg.start?.call_control_id; |
|
|
| |
| |
| |
| setTimeout(async () => { |
| if (hasGreeted || !callControlId) return; |
| const e = callLogs.find(l => l.call_control_id === callControlId); |
| if (e?.status !== "in-progress") return; |
|
|
| hasGreeted = true; |
| const greetingPhrase = "Hi, this is Nathan."; |
| log("SMART_GREETING", `Playing: "${greetingPhrase}"`); |
|
|
| await sendTelnyxCmd(callControlId, "playback_start", { |
| audio_url: `${AI_SERVER_URL}/play-audio?voice=${e.voice_used}&phrase=${encodeURIComponent(greetingPhrase)}`, |
| target_legs: "self" |
| }); |
|
|
| |
| fetch(`${AI_SERVER_URL}/api/register-greeting`, { |
| method: "POST", |
| headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify({ call_id: callControlId, phrase: greetingPhrase }) |
| }).catch(() => {}); |
| }, 1500); |
| } |
|
|
| if (msg.event === "media") { |
| const pcm16 = decodeMuLaw(Buffer.from(msg.media.payload, "base64")); |
| const rms = calculateRMS(pcm16); |
|
|
| const entry = callLogs.find(l => l.call_control_id === callControlId); |
| const nowAiPlaying = entry?.is_playing || false; |
| const timeSincePlayback = entry?.playback_started_at |
| ? (Date.now() - entry.playback_started_at) : 9999; |
| const isEchoWindow = timeSincePlayback < ECHO_GRACE_PERIOD; |
|
|
| |
| |
| if (!wasAiPlaying && nowAiPlaying) { |
| isSpeaking = false; |
| silenceFrames = 0; |
| totalNoiseFrames = 0; |
| hasPlayedFiller = false; |
| } |
| wasAiPlaying = nowAiPlaying; |
|
|
| if (rms > VOLUME_THRESHOLD) { |
| |
| if (isEchoWindow) return; |
|
|
| hasGreeted = true; |
| silenceFrames = 0; |
| totalNoiseFrames++; |
|
|
| if (!isSpeaking) { |
| isSpeaking = true; |
| hasPlayedFiller = false; |
|
|
| if (entry?.is_playing) { |
| log("BARGE-IN (VAD)", "User speaking β stopping AI."); |
| entry.is_playing = false; |
| await sendTelnyxCmd(callControlId, "playback_stop"); |
| } |
| } |
| } else if (isSpeaking) { |
| silenceFrames++; |
|
|
| if (silenceFrames > SILENCE_THRESHOLD_FRAMES && !hasPlayedFiller) { |
| hasPlayedFiller = true; |
| isSpeaking = false; |
|
|
| |
| const callAge = entry?.answered_at |
| ? Date.now() - new Date(entry.answered_at).getTime() : 0; |
|
|
| if (totalNoiseFrames > 40 && callAge > 8000) { |
| log("VAD_EARLY_TURN", "Long pause detected β playing filler."); |
| const fillers = ["Right.", "Got it.", "Okay.", "Yeah.", "Ah, okay."]; |
| const filler = fillers[Math.floor(Math.random() * fillers.length)]; |
| sendTelnyxCmd(callControlId, "playback_start", { |
| audio_url: `${AI_SERVER_URL}/play-audio?voice=${entry?.voice_used || ACTIVE_VOICE}&phrase=${encodeURIComponent(filler)}`, |
| target_legs: "self" |
| }).catch(() => {}); |
| } |
| totalNoiseFrames = 0; |
| } |
| } |
| } |
| } catch {} |
| }); |
| }); |
|
|
| server.listen(PORT, "0.0.0.0", () => { |
| log("STARTUP", `Telecom Server running on port ${PORT}`); |
| }); |