I configured most of my Asterisk 21 & FreePBX 17 server with the help of ChatGPT and can connect and dial all of the custom extensions I have created in /etc/asterisk/extensions_custom.conf except this new one I can’t get to work. I’ve been going in circles with ChatGPT and Grok on the solution and nothing has fixed it. I’m trying to setup a continuous listening + DTMF app in Nodejs to change the MoH whenever I say ‘metal’ or ‘jazz’ or press 1 or 2 and the app isn’t registering. I don’t know if I need to figure out how to get the ARI user setup with “apps=speech_dtmf_app” or if that makes no difference if the ARI user has read/write access. This is what I have:
[root@chaos asterisk]# NODE_PATH=/usr/local/lib/node_modules node /usr/local/bin/ari_websocket.js
LOG (VoskAPI:ReadDataFiles():model.cc:213) Decoding params beam=10 max-active=3000 lattice-beam=2
LOG (VoskAPI:ReadDataFiles():model.cc:216) Silence phones 1:2:3:4:5:6:7:8:9:10
LOG (VoskAPI:RemoveOrphanNodes():nnet-nnet.cc:948) Removed 0 orphan nodes.
LOG (VoskAPI:RemoveOrphanComponents():nnet-nnet.cc:847) Removing 0 orphan components.
LOG (VoskAPI:ReadDataFiles():model.cc:248) Loading i-vector extractor from /usr/local/share/vosk/model/ivector/final.ie
LOG (VoskAPI:ComputeDerivedVars():ivector-extractor.cc:183) Computing derived variables for iVector extractor
LOG (VoskAPI:ComputeDerivedVars():ivector-extractor.cc:204) Done.
LOG (VoskAPI:ReadDataFiles():model.cc:282) Loading HCL and G from /usr/local/share/vosk/model/graph/HCLr.fst /usr/local/share/vosk/model/graph/Gr.fst
LOG (VoskAPI:ReadDataFiles():model.cc:303) Loading winfo /usr/local/share/vosk/model/graph/phones/word_boundary.int
Connecting to ARI with app: speech_dtmf_app
Listening for RTP audio on UDP port 5555
[
'This API is using a deprecated version of Swagger! Please see http://github.com/wordnik/swagger-core/wiki for more info'
]
ARI connected and app registered: speech_dtmf_app
Registered ARI apps: []
[root@chaos asterisk]# asterisk -rx "ari show apps"
Application Name
=========================
[root@chaos asterisk]#
[2025-10-09 14:24:39] DEBUG[1606534][C-00000002] pbx.c: Launching 'Stasis'
[2025-10-09 14:24:39] WARNING[1606534][C-00000002] ari/ari_websockets.c: PJSIP/1337-00000001: Failed to find outbound websocket per-call config for app 'speech_dtmf_app'
[2025-10-09 14:24:39] WARNING[1606534][C-00000002] app_stasis.c: PJSIP/1337-00000001: Stasis app 'speech_dtmf_app' doesn't exist
[2025-10-09 14:24:39] DEBUG[1606534][C-00000002] pbx.c: Launching 'Hangup'
[2025-10-09 14:24:39] DEBUG[1606534][C-00000002] channel.c: Soft-Hanging (0x20) up channel 'PJSIP/1337-00000001'
[2025-10-09 14:24:39] DEBUG[1606534][C-00000002] pbx.c: Spawn extension (from-internal,*555,5) exited non-zero on 'PJSIP/1337-00000001'
[root@chaos asterisk]# cat /etc/asterisk/extensions_custom.conf
[from-internal-custom]
exten => *555,1,NoOp(Live Speech + DTMF WebSocket)
same => n,Answer()
same => n,Playback(beep)
same => n,Stasis(speech_dtmf_app)
same => n,Hangup()
[root@chaos ~]# cat /usr/local/bin/ari_websocket.js
const AriClient = require('ari-client');
const vosk = require('vosk');
const dgram = require('dgram');
const fs = require('fs');
const { execSync } = require('child_process');
const ARI_URL = 'http://127.0.0.1:8088/ari';
const ARI_USER = 'speechdtmfuser';
const ARI_PASS = 'SOMEPASSWORD';
const ARI_APP = 'speech_dtmf_app';
const MODEL_PATH = '/usr/local/share/vosk/model';
const SAMPLE_RATE = 16000;
const UDP_PORT = 5555;
// Verify Vosk model exists
if (!fs.existsSync(MODEL_PATH)) {
console.error('Vosk model not found at', MODEL_PATH);
process.exit(1);
}
// Initialize Vosk
const model = new vosk.Model(MODEL_PATH);
const rec = new vosk.Recognizer({ model, sampleRate: SAMPLE_RATE });
// UDP socket for ExternalMedia streaming
const socket = dgram.createSocket('udp4');
socket.on('message', msg => {
const result = rec.acceptWaveform(msg) ? rec.result() : rec.partialResult();
const text = (result.text || '').toLowerCase();
if (text) {
console.log('Speech detected:', text);
handleKeyword(text);
}
});
socket.bind(UDP_PORT, () => console.log('Listening for RTP audio on UDP port', UDP_PORT));
// Keyword actions
async function handleKeyword(text) {
try {
if (text.includes('metal')) await performAction('metal');
if (text.includes('jazz')) await performAction('jazz');
if (text.includes('stop')) await performAction('stop');
} catch (err) {
console.error('handleKeyword error:', err.message);
}
}
// DTMF actions (placeholder, channel passed from StasisStart)
async function performAction(option, channel = null) {
console.log('Performing action:', option);
if (!channel) return;
try {
if (option === 'metal' || option === 'jazz') {
const ttsFile = `/var/lib/asterisk/sounds/tts-${channel.id}.wav`;
execSync(`flite -t "You chose ${option}" -o ${ttsFile}`);
await channel.play({ media: `sound:tts-${channel.id}` });
await channel.startMoh({ mohClass: option });
} else if (option === 'stop') {
await channel.stopMoh();
}
} catch (err) {
console.error('performAction error:', err.message);
}
}
let ariClient = null;
// Connect to ARI with correct app registration
async function connectARI() {
try {
console.log('Connecting to ARI with app:', ARI_APP);
// The fourth argument registers the Stasis app
ariClient = await AriClient.connect(ARI_URL, ARI_USER, ARI_PASS, ARI_APP);
console.log('ARI connected and app registered:', ARI_APP);
// Confirm registration
const apps = await ariClient.applications.list();
console.log('Registered ARI apps:', apps.map(a => a.name));
// Handle new calls entering the app
ariClient.on('StasisStart', async event => {
const channel = event.channel;
console.log('New call:', channel.name);
try {
await channel.answer();
} catch (err) {
console.error('Failed to answer channel:', err.message);
return;
}
// Handle DTMF
channel.on('ChannelDtmfReceived', async dtmf => {
console.log('DTMF received:', dtmf.digit);
if (dtmf.digit === '1') await performAction('metal', channel);
if (dtmf.digit === '2') await performAction('jazz', channel);
if (dtmf.digit === '3') await performAction('stop', channel);
});
// ExternalMedia for continuous streaming to UDP port
try {
await ariClient.channels.externalMedia({
channelId: channel.id,
format: 'slin16',
transport: 'udp',
direction: 'both',
endpoint: `127.0.0.1:${UDP_PORT}`
});
console.log('ExternalMedia established for channel', channel.id);
} catch (err) {
console.error('ExternalMedia error:', err.message);
}
});
ariClient.on('StasisEnd', event => {
console.log('Call ended:', event.channel.name);
});
ariClient.on('error', err => {
console.error('ARI client error:', err.message);
});
} catch (err) {
console.error('ARI connection failed:', err.message, 'Retrying in 5s...');
setTimeout(connectARI, 5000);
}
}
// Start the ARI connection
connectARI();