mirror of
https://github.com/jambonz/jambonz-api-server.git
synced 2026-07-04 19:21:53 +00:00
support houndify wss (#551)
This commit is contained in:
+90
-61
@@ -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 || ''});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user