support houndify wss (#551)

This commit is contained in:
Hoan Luu Huu
2026-04-23 18:20:04 +07:00
committed by GitHub
parent fb54f562f7
commit dd755f8746
3 changed files with 103 additions and 379 deletions
+90 -61
View File
@@ -5,7 +5,8 @@ const sdk = require('microsoft-cognitiveservices-speech-sdk');
const { SpeechClient } = require('@soniox/soniox-node');
const fs = require('fs');
const { AssemblyAI } = require('assemblyai');
const Houndify = require('houndify');
const crypto = require('crypto');
const WebSocket = require('ws');
const { GladiaClient } = require('@gladiaio/sdk');
const {decrypt, obscureKey} = require('./encrypt-decrypt');
const { RealtimeSession } = require('speechmatics');
@@ -660,76 +661,104 @@ const testAssemblyStt = async(logger, credentials) => {
});
};
const _houndifySign = (nonce, accessKey) => {
const b64 = accessKey.replace(/-/g, '+').replace(/_/g, '/');
const keyBin = Buffer.from(b64, 'base64');
const digest = crypto.createHmac('sha256', keyBin).update(nonce).digest();
return digest.toString('base64').replace(/\+/g, '-').replace(/\//g, '_');
};
const testHoundifyStt = async(logger, credentials) => {
const {client_id, client_key, user_id, houndify_server_uri} = credentials;
const {client_id, client_key, houndify_server_uri} = credentials;
const wsUrl = houndify_server_uri || 'wss://apiws-polaris.houndify.com/v2/transcription';
return new Promise((resolve, reject) => {
// api-server use houndify js sdk, which connect to wss server
// freeswitch modules use houndify c/c++ sdk, which connect to https server
// we cannot test credentials on https server here.
if (houndify_server_uri) {
return true;
}
const timeout = setTimeout(() => {
ws.close();
reject(new Error('Houndify WS credential test timed out after 15s'));
}, 15000);
try {
// Read the test audio file
const audioBuffer = fs.readFileSync(`${__dirname}/../../data/test_audio.wav`);
let gotTranscript = false;
const ws = new WebSocket(wsUrl);
// Create VoiceRequest for speech-to-text testing
const voiceRequest = new Houndify.VoiceRequest({
// Your Houndify Client ID and Key
clientId: client_id,
clientKey: client_key,
ws.on('error', (err) => {
clearTimeout(timeout);
reject(new Error(`WebSocket error: ${err.message}`));
});
// Request info
requestInfo: {
UserID: user_id || 'test_user',
Latitude: 37.388309,
Longitude: -121.973968,
},
ws.on('close', () => {
clearTimeout(timeout);
if (!gotTranscript) {
reject(new Error('Connection closed before receiving transcription'));
}
});
// Audio format configuration
sampleRate: 16000,
enableVAD: true,
// Response and error handlers
onResponse: function(response, info) {
logger.debug({response, info}, 'Houndify STT response received');
if (response && response.AllResults && response.AllResults.length > 0) {
resolve(response);
} else {
reject(new Error('No transcription results received'));
}
},
onError: function(err, info) {
logger.error({err, info}, 'Houndify STT error');
reject(err);
},
onRecordingStarted: function() {
logger.debug('Houndify recording started');
},
onRecordingStopped: function() {
logger.debug('Houndify recording stopped');
}
});
// Send audio in chunks (VoiceRequest automatically starts when you write data)
const chunkSize = 1024;
for (let i = 0; i < audioBuffer.length; i += chunkSize) {
const chunk = audioBuffer.slice(i, i + chunkSize);
voiceRequest.write(chunk);
ws.on('message', (data) => {
const msg = data.toString();
let parsed;
try {
parsed = JSON.parse(msg);
} catch (e) {
return;
}
// End the request
voiceRequest.end();
if (parsed.error) {
clearTimeout(timeout);
ws.close();
return reject(new Error(`Houndify error: ${parsed.error}`));
}
} catch (error) {
logger.error({error}, 'Failed to create Houndify VoiceRequest');
reject(error);
}
if (parsed.status === 'ok' && parsed.nonce) {
const signature = _houndifySign(parsed.nonce, client_key);
const authMsg = JSON.stringify({
version: '1.1',
access_id: client_id,
signature
});
ws.send(authMsg);
const requestInfo = JSON.stringify({
client_id,
sdk: 'jambonz-api-server',
audiofmt: {type: 'S16LE', sample_rate: 8000},
model: 'base_8k',
segmentation: {mode: 'none'},
voice_activity_detection: false
});
ws.send(requestInfo);
return;
}
if (parsed.type === 'QueryIDMessage') {
logger.debug({query_id: parsed.query_id}, 'Houndify WS test: received QueryIDMessage');
try {
const audioBuffer = fs.readFileSync(`${__dirname}/../../data/test_audio.wav`);
const pcmData = audioBuffer.subarray(44);
const chunkSize = 3200;
for (let i = 0; i < pcmData.length; i += chunkSize) {
ws.send(pcmData.subarray(i, Math.min(i + chunkSize, pcmData.length)));
}
ws.send('Done');
} catch (err) {
clearTimeout(timeout);
ws.close();
reject(new Error(`Failed to send test audio: ${err.message}`));
}
return;
}
if (parsed.type === 'PartialTranscript' ||
parsed.type === 'FinalSegmentTranscript' ||
parsed.type === 'FinalTranscript') {
if (!gotTranscript) {
gotTranscript = true;
logger.debug({type: parsed.type, text: parsed.text}, 'Houndify WS test: received transcript');
clearTimeout(timeout);
ws.close();
resolve({status: 'ok', text: parsed.text || ''});
}
}
});
});
};