🎙️ fix: Align Speech Engine Configuration With Runtime (#14736)

* fix: align speech engine configuration with runtime

* fix: guard speech recording shortcuts

* fix: reconcile speech engine availability

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
Marco Beretta 2026-08-12 19:22:28 +02:00 committed by GitHub
parent 92a8058f02
commit 5ff282f900
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 858 additions and 84 deletions

View file

@ -1,6 +1,21 @@
const { logger } = require('@librechat/data-schemas');
const { getAppConfig } = require('~/server/services/Config');
const LEGACY_EXTERNAL_STT_ENGINES = new Set(['openai', 'azureOpenAI']);
const LEGACY_EXTERNAL_TTS_ENGINES = new Set(['openai', 'azureOpenAI', 'elevenlabs', 'localai']);
function normalizeSpeechEngine(key, value) {
if (key === 'engineSTT' && LEGACY_EXTERNAL_STT_ENGINES.has(value)) {
return 'external';
}
if (key === 'engineTTS' && LEGACY_EXTERNAL_TTS_ENGINES.has(value)) {
return 'external';
}
return value;
}
/**
* This function retrieves the speechTab settings from the custom configuration
* It first fetches the custom configuration
@ -52,7 +67,8 @@ async function getCustomConfigSpeech(req, res) {
} else {
for (const key in speechTab.speechToText) {
if (speechTab.speechToText[key] !== undefined) {
settings[key] = speechTab.speechToText[key];
const value = speechTab.speechToText[key];
settings[key] = normalizeSpeechEngine(key, value);
}
}
}
@ -64,7 +80,8 @@ async function getCustomConfigSpeech(req, res) {
} else {
for (const key in speechTab.textToSpeech) {
if (speechTab.textToSpeech[key] !== undefined) {
settings[key] = speechTab.textToSpeech[key];
const value = speechTab.textToSpeech[key];
settings[key] = normalizeSpeechEngine(key, value);
}
}
}

View file

@ -0,0 +1,95 @@
jest.mock('@librechat/data-schemas', () => ({ logger: { error: jest.fn() } }));
jest.mock('~/server/services/Config', () => ({ getAppConfig: jest.fn() }));
const getCustomConfigSpeech = require('./getCustomConfigSpeech');
const createResponse = () => {
const res = {
send: jest.fn(),
status: jest.fn(),
};
res.status.mockReturnValue(res);
return res;
};
describe('getCustomConfigSpeech', () => {
it.each(['openai', 'azureOpenAI'])(
'normalizes the legacy STT provider "%s" to the external engine',
async (engineSTT) => {
const req = {
config: {
speech: {
stt: { openai: {} },
speechTab: { speechToText: { engineSTT } },
},
},
};
const res = createResponse();
await getCustomConfigSpeech(req, res);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.send).toHaveBeenCalledWith({
sttExternal: true,
ttsExternal: false,
engineSTT: 'external',
});
},
);
it.each(['browser', 'external'])('preserves the runtime STT engine "%s"', async (engineSTT) => {
const req = {
config: {
speech: {
stt: { openai: {} },
speechTab: { speechToText: { engineSTT } },
},
},
};
const res = createResponse();
await getCustomConfigSpeech(req, res);
expect(res.send).toHaveBeenCalledWith(expect.objectContaining({ engineSTT }));
});
it.each(['openai', 'azureOpenAI', 'elevenlabs', 'localai'])(
'normalizes the legacy TTS provider "%s" to the external engine',
async (engineTTS) => {
const req = {
config: {
speech: {
tts: { openai: {} },
speechTab: { textToSpeech: { engineTTS } },
},
},
};
const res = createResponse();
await getCustomConfigSpeech(req, res);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.send).toHaveBeenCalledWith({
sttExternal: false,
ttsExternal: true,
engineTTS: 'external',
});
},
);
it.each(['browser', 'external'])('preserves the runtime TTS engine "%s"', async (engineTTS) => {
const req = {
config: {
speech: {
tts: { openai: {} },
speechTab: { textToSpeech: { engineTTS } },
},
},
};
const res = createResponse();
await getCustomConfigSpeech(req, res);
expect(res.send).toHaveBeenCalledWith(expect.objectContaining({ engineTTS }));
});
});