mirror of
https://github.com/jambonz/jambonz-api-server.git
synced 2026-08-19 09:30:23 +00:00
fix: speechmatics credential test ignored the configured region (#563)
The speechmatics branch of the /test route destructured only api_key out of the decrypted credential, so testSpeechmaticsStt() passed realtimeUrl: undefined to the sdk. ConnectionConfigFull only applies an override when the value is truthy, so every credential was silently tested against wss://eu2.rt.speechmatics.com/v2 no matter which region was stored - a neu or wus key was judged by whatever eu2 happened to answer. - pass speechmatics_stt_uri through from the route - normalize it before handing it to the sdk. The field holds a bare hostname, which the sdk cannot use as-is: it appends the language, yielding "eu2.rt.speechmatics.com/en" and ERR_INVALID_URL. At call time the same value goes to mod_speechmatics_transcribe as SPEECHMATICS_HOST, which always connects over wss on port 443 with a path of /v2, so build exactly that url. Anything else - a full ws url, a port - now fails the test with an actionable message rather than passing against an endpoint only the test can reach, and a credential that has lost the field fails too: the feature server omits SPEECHMATICS_HOST when it is unset and the module refuses the session, so defaulting to a hosted region here would show a green check on a dead credential. - record the speechmatics STT result with sttTestResult(), not ttsTestResult(), which is what every other STT vendor in this file does; the tts column was being stamped for an STT-only vendor Adds coverage for the url building to the main test suite - it is the part of this that can be exercised without an api key. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
53279a59c0
commit
baf80cb84c
@@ -924,15 +924,15 @@ router.get('/:sid/test', async(req, res) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (cred.vendor === 'speechmatics') {
|
} else if (cred.vendor === 'speechmatics') {
|
||||||
const {api_key} = credential;
|
const {api_key, speechmatics_stt_uri} = credential;
|
||||||
if (cred.use_for_stt) {
|
if (cred.use_for_stt) {
|
||||||
try {
|
try {
|
||||||
await testSpeechmaticsStt(logger, {api_key});
|
await testSpeechmaticsStt(logger, {api_key, speechmatics_stt_uri});
|
||||||
results.stt.status = 'ok';
|
results.stt.status = 'ok';
|
||||||
SpeechCredential.ttsTestResult(sid, true);
|
SpeechCredential.sttTestResult(sid, true);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
results.stt = {status: 'fail', reason: err.message};
|
results.stt = {status: 'fail', reason: err.message};
|
||||||
SpeechCredential.ttsTestResult(sid, false);
|
SpeechCredential.sttTestResult(sid, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (cred.vendor === 'playht') {
|
} else if (cred.vendor === 'playht') {
|
||||||
|
|||||||
@@ -83,11 +83,33 @@ const testSonioxStt = async(logger, credentials) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* speechmatics_stt_uri is stored as a bare hostname - eu2.rt.speechmatics.com and friends for
|
||||||
|
* the hosted service, or the hostname of an on-prem container - while the sdk wants a full url
|
||||||
|
* that it appends the language to. At call time the stored value is handed to
|
||||||
|
* mod_speechmatics_transcribe as SPEECHMATICS_HOST, which always dials wss on port 443 with a
|
||||||
|
* path of /v2, so test exactly that. Accepting a shape the runtime cannot dial (a full url, a
|
||||||
|
* port) would report a credential as good that then fails on every call, and quietly falling
|
||||||
|
* back to a default host would do the same for a credential that has lost the field - the
|
||||||
|
* feature server omits SPEECHMATICS_HOST entirely when it is unset, and the module then
|
||||||
|
* refuses to start the session.
|
||||||
|
*/
|
||||||
|
const speechmaticsRealtimeUrl = (speechmatics_stt_uri) => {
|
||||||
|
const host = (speechmatics_stt_uri || '').trim();
|
||||||
|
if (!host) throw new Error('speechmatics_stt_uri is not set on this speech credential');
|
||||||
|
if (!/^[a-z0-9.-]+$/i.test(host)) {
|
||||||
|
throw new Error(
|
||||||
|
`invalid speechmatics_stt_uri '${host}': expected a bare hostname, e.g. eu2.rt.speechmatics.com`);
|
||||||
|
}
|
||||||
|
return `wss://${host}/v2`;
|
||||||
|
};
|
||||||
|
|
||||||
const testSpeechmaticsStt = async(logger, credentials) => {
|
const testSpeechmaticsStt = async(logger, credentials) => {
|
||||||
const {api_key, speechmatics_stt_uri} = credentials;
|
const {api_key, speechmatics_stt_uri} = credentials;
|
||||||
|
const realtimeUrl = speechmaticsRealtimeUrl(speechmatics_stt_uri);
|
||||||
return new Promise(async(resolve, reject) => {
|
return new Promise(async(resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
const session = new RealtimeSession({ apiKey: api_key, realtimeUrl: speechmatics_stt_uri });
|
const session = new RealtimeSession({ apiKey: api_key, realtimeUrl });
|
||||||
let transcription = '';
|
let transcription = '';
|
||||||
session.addListener('Error', (error) => {
|
session.addListener('Error', (error) => {
|
||||||
reject(error);
|
reject(error);
|
||||||
@@ -1845,6 +1867,7 @@ module.exports = {
|
|||||||
testVerbioStt,
|
testVerbioStt,
|
||||||
getLanguagesAndVoicesForVendor,
|
getLanguagesAndVoicesForVendor,
|
||||||
testSpeechmaticsStt,
|
testSpeechmaticsStt,
|
||||||
|
speechmaticsRealtimeUrl,
|
||||||
testCartesia,
|
testCartesia,
|
||||||
testVoxistStt,
|
testVoxistStt,
|
||||||
testOpenAiStt,
|
testOpenAiStt,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ require('./auth');
|
|||||||
require('./sbcs');
|
require('./sbcs');
|
||||||
require('./ms-teams');
|
require('./ms-teams');
|
||||||
require('./speech-credentials');
|
require('./speech-credentials');
|
||||||
|
require('./speechmatics-url');
|
||||||
require('./recent-calls');
|
require('./recent-calls');
|
||||||
require('./users');
|
require('./users');
|
||||||
require('./users-view-only');
|
require('./users-view-only');
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
const test = require('tape');
|
||||||
|
const {speechmaticsRealtimeUrl} = require('../lib/utils/speech-utils');
|
||||||
|
|
||||||
|
/* the runtime hands speechmatics_stt_uri to mod_speechmatics_transcribe as a bare hostname and
|
||||||
|
always dials wss on 443 with a path of /v2, so anything else must fail the credential test
|
||||||
|
rather than pass against a url only the test can reach
|
||||||
|
*/
|
||||||
|
test('speechmatics-url', (t) => {
|
||||||
|
t.equal(speechmaticsRealtimeUrl('eu2.rt.speechmatics.com'),
|
||||||
|
'wss://eu2.rt.speechmatics.com/v2', 'builds the url the runtime dials');
|
||||||
|
t.equal(speechmaticsRealtimeUrl('wus.rt.speechmatics.com'),
|
||||||
|
'wss://wus.rt.speechmatics.com/v2', 'honors the configured region');
|
||||||
|
t.equal(speechmaticsRealtimeUrl('sm-container.internal'),
|
||||||
|
'wss://sm-container.internal/v2', 'accepts an on-prem hostname');
|
||||||
|
t.equal(speechmaticsRealtimeUrl(' neu.rt.speechmatics.com '),
|
||||||
|
'wss://neu.rt.speechmatics.com/v2', 'trims surrounding whitespace');
|
||||||
|
|
||||||
|
for (const uri of [undefined, null, '', ' ']) {
|
||||||
|
t.throws(() => speechmaticsRealtimeUrl(uri), /is not set on this speech credential/,
|
||||||
|
`rejects a credential with no host: ${JSON.stringify(uri)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const uri of [
|
||||||
|
'wss://eu2.rt.speechmatics.com/v2',
|
||||||
|
'https://eu2.rt.speechmatics.com',
|
||||||
|
'sm-container.internal:9000',
|
||||||
|
'eu2.rt.speechmatics.com/v2',
|
||||||
|
'my host'
|
||||||
|
]) {
|
||||||
|
t.throws(() => speechmaticsRealtimeUrl(uri), /expected a bare hostname/,
|
||||||
|
`rejects a shape the runtime cannot dial: ${uri}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user