feat(call, voicemail, ivr): add voicemail and IVR call flows with DTMF handling, prompt playback, recording, and dashboard management

This commit is contained in:
2026-04-10 08:54:46 +00:00
parent 6ecd3f434c
commit e6bd64a534
25 changed files with 3892 additions and 10 deletions

View File

@@ -44,6 +44,8 @@ import {
import { initCodecBridge } from './opusbridge.ts';
import { initAnnouncement } from './announcement.ts';
import { CallManager } from './call/index.ts';
import { PromptCache } from './call/prompt-cache.ts';
import { VoiceboxManager } from './voicebox.ts';
// ---------------------------------------------------------------------------
// Config
@@ -92,6 +94,11 @@ const providerStates = initProviderStates(appConfig.providers, proxy.publicIpSee
initRegistrar(appConfig.devices, log);
// Initialize voicemail and IVR subsystems.
const promptCache = new PromptCache(log);
const voiceboxManager = new VoiceboxManager(log);
voiceboxManager.init(appConfig.voiceboxes ?? []);
const callManager = new CallManager({
appConfig,
sendSip: (buf, dest) => sock.send(buf, dest.port, dest.address),
@@ -101,6 +108,8 @@ const callManager = new CallManager({
getAllBrowserDeviceIds,
sendToBrowserDevice,
getBrowserDeviceWs,
promptCache,
voiceboxManager,
});
// Initialize WebRTC signaling (browser device registration only).
@@ -130,6 +139,7 @@ function getStatus() {
calls: callManager.getStatus(),
callHistory: callManager.getHistory(),
contacts: appConfig.contacts || [],
voicemailCounts: voiceboxManager.getAllUnheardCounts(),
};
}
@@ -252,6 +262,33 @@ sock.bind(LAN_PORT, '0.0.0.0', () => {
// Initialize audio codec bridge (Rust binary via smartrust).
initCodecBridge(log)
.then(() => initAnnouncement(log))
.then(async () => {
// Pre-generate voicemail beep tone.
await promptCache.generateBeep('voicemail-beep', 1000, 500, 8000);
// Pre-generate voicemail greetings for all configured voiceboxes.
for (const vb of appConfig.voiceboxes ?? []) {
if (!vb.enabled) continue;
const promptId = `voicemail-greeting-${vb.id}`;
const wavPath = vb.greetingWavPath;
if (wavPath) {
await promptCache.loadWavPrompt(promptId, wavPath);
} else {
const text = vb.greetingText || 'The person you are trying to reach is not available. Please leave a message after the tone.';
await promptCache.generatePrompt(promptId, text, vb.greetingVoice || 'af_bella');
}
}
// Pre-generate IVR menu prompts.
if (appConfig.ivr?.enabled) {
for (const menu of appConfig.ivr.menus) {
const promptId = `ivr-menu-${menu.id}`;
await promptCache.generatePrompt(promptId, menu.promptText, menu.promptVoice || 'af_bella');
}
}
log(`[startup] prompts cached: ${promptCache.listIds().join(', ') || 'none'}`);
})
.catch((e) => log(`[codec] init failed: ${e}`));
});
@@ -288,6 +325,7 @@ initWebUi(
}
},
callManager,
voiceboxManager,
);
process.on('SIGINT', () => { log('SIGINT, exiting'); process.exit(0); });