Voice lists were a single flat set from LITELLM_TTS_VOICES, so picking a model could leave an incompatible voice selected and the request would fail at the gateway. Voices are now resolved per model family (Kokoro, Kitten, Supertonic, Groq Orpheus EN/AR), with a compatibility check that falls back through user → admin → env → first valid voice. Groq Orpheus requests also pin response_format to wav. Also refreshes the cardiac/respiratory auscultation samples, extends the well-visit component, and fixes the Android launch theme background (@null → colorPrimary) so the splash does not flash through. NOTE: this is in-progress work that was already sitting uncommitted in the working tree; it is committed here as-is so the tree was clean for the release bump.
155 lines
6.3 KiB
JavaScript
155 lines
6.3 KiB
JavaScript
const { getLiteLLMHeaders } = require('./litellm');
|
|
|
|
function parseList(value) {
|
|
return String(value || '')
|
|
.split(',')
|
|
.map(function(item) { return item.trim(); })
|
|
.filter(Boolean);
|
|
}
|
|
|
|
var KITTEN_TTS_VOICES = ['Bella', 'Jasper', 'Luna', 'Bruno', 'Rosie', 'Hugo', 'Kiki', 'Leo'];
|
|
var SUPERTONIC_TTS_VOICES = ['F1', 'F2', 'F3', 'F4', 'F5', 'M1', 'M2', 'M3', 'M4', 'M5'];
|
|
var GROQ_ORPHEUS_ENGLISH_VOICES = ['autumn', 'diana', 'hannah', 'austin', 'daniel', 'troy'];
|
|
var GROQ_ORPHEUS_ARABIC_VOICES = ['abdullah', 'fahad', 'sultan', 'lulwa', 'noura', 'aisha'];
|
|
|
|
function uniqueList(values) {
|
|
var seen = new Set();
|
|
return (values || []).filter(function(value) {
|
|
if (!value || seen.has(value)) return false;
|
|
seen.add(value);
|
|
return true;
|
|
});
|
|
}
|
|
|
|
function getTTSProvider() {
|
|
var env = process.env.TTS_PROVIDER;
|
|
if (env === 'litellm') return 'litellm';
|
|
if (process.env.LITELLM_API_BASE) return 'litellm';
|
|
return 'none';
|
|
}
|
|
|
|
function getTTSEnvProvider() {
|
|
return process.env.TTS_PROVIDER || 'auto';
|
|
}
|
|
|
|
function getTTSVoiceLists() {
|
|
return {
|
|
litellm: uniqueList(parseList(process.env.LITELLM_TTS_VOICES).concat(KITTEN_TTS_VOICES, SUPERTONIC_TTS_VOICES, GROQ_ORPHEUS_ENGLISH_VOICES, GROQ_ORPHEUS_ARABIC_VOICES))
|
|
};
|
|
}
|
|
|
|
function getLiteLLMTTSModelFamily(model) {
|
|
var id = String(model || '').toLowerCase();
|
|
if (id === 'local-kitten-tts') return 'kitten';
|
|
if (id === 'local-supertonic-tts') return 'supertonic';
|
|
if (id === 'local-kokoro-tts') return 'kokoro';
|
|
if (id === 'groq-orpheus-english' || id === 'canopylabs/orpheus-v1-english') return 'groq-orpheus-english';
|
|
if (id === 'groq-orpheus-arabic-saudi' || id === 'canopylabs/orpheus-arabic-saudi') return 'groq-orpheus-arabic';
|
|
return 'unknown';
|
|
}
|
|
|
|
function getLiteLLMTTSRequestOptions(model) {
|
|
var family = getLiteLLMTTSModelFamily(model);
|
|
if (family === 'groq-orpheus-english' || family === 'groq-orpheus-arabic') {
|
|
return { response_format: 'wav' };
|
|
}
|
|
return {};
|
|
}
|
|
|
|
function isLiteLLMTTSVoiceCompatible(model, voice) {
|
|
if (!voice) return true;
|
|
var family = getLiteLLMTTSModelFamily(model);
|
|
if (family === 'kitten') return KITTEN_TTS_VOICES.indexOf(voice) !== -1;
|
|
if (family === 'supertonic') return SUPERTONIC_TTS_VOICES.indexOf(voice) !== -1;
|
|
if (family === 'groq-orpheus-english') return GROQ_ORPHEUS_ENGLISH_VOICES.indexOf(String(voice).toLowerCase()) !== -1;
|
|
if (family === 'groq-orpheus-arabic') return GROQ_ORPHEUS_ARABIC_VOICES.indexOf(String(voice).toLowerCase()) !== -1;
|
|
if (family === 'kokoro') {
|
|
return KITTEN_TTS_VOICES.indexOf(voice) === -1 && SUPERTONIC_TTS_VOICES.indexOf(voice) === -1 &&
|
|
GROQ_ORPHEUS_ENGLISH_VOICES.indexOf(String(voice).toLowerCase()) === -1 && GROQ_ORPHEUS_ARABIC_VOICES.indexOf(String(voice).toLowerCase()) === -1;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function getLiteLLMTTSVoicesForModel(model, opts) {
|
|
opts = opts || {};
|
|
var family = getLiteLLMTTSModelFamily(model);
|
|
var voices = [];
|
|
if (family === 'kitten') voices = KITTEN_TTS_VOICES.slice();
|
|
else if (family === 'supertonic') voices = SUPERTONIC_TTS_VOICES.slice();
|
|
else if (family === 'groq-orpheus-english') voices = GROQ_ORPHEUS_ENGLISH_VOICES.slice();
|
|
else if (family === 'groq-orpheus-arabic') voices = GROQ_ORPHEUS_ARABIC_VOICES.slice();
|
|
else voices = parseList(process.env.LITELLM_TTS_VOICES);
|
|
|
|
[opts.currentVoice, process.env.LITELLM_TTS_VOICE].forEach(function(voice) {
|
|
if (isLiteLLMTTSVoiceCompatible(model, voice)) voices.push(voice);
|
|
});
|
|
return uniqueList(voices.filter(function(voice) { return isLiteLLMTTSVoiceCompatible(model, voice); }));
|
|
}
|
|
|
|
function isLiteLLMTTSModel(model) {
|
|
var mode = model && model.model_info && model.model_info.mode ? String(model.model_info.mode) : '';
|
|
return mode === 'audio_speech';
|
|
}
|
|
|
|
function getLiteLLMTTSModels(models) {
|
|
return (models || [])
|
|
.filter(isLiteLLMTTSModel)
|
|
.map(function(model) { return model && (model.id || model.model_name) ? (model.id || model.model_name) : String(model || ''); });
|
|
}
|
|
|
|
function pushUniqueTTSItem(items, item) {
|
|
if (!item || !item.id) return;
|
|
if (items.some(function(existing) { return existing.id === item.id && existing.kind === item.kind; })) return;
|
|
items.push(item);
|
|
}
|
|
|
|
function getLiteLLMTTSDiscoveryItems(models, opts) {
|
|
opts = opts || {};
|
|
var items = [];
|
|
getLiteLLMTTSModels(models).forEach(function(id) {
|
|
pushUniqueTTSItem(items, { id: id, name: id, source: 'gateway-api', kind: 'model' });
|
|
});
|
|
if (opts.currentModel) {
|
|
pushUniqueTTSItem(items, { id: opts.currentModel, name: opts.currentModel, source: 'configured-model', kind: 'model' });
|
|
}
|
|
if (opts.currentVoice) {
|
|
pushUniqueTTSItem(items, { id: opts.currentVoice, name: opts.currentVoice, source: 'configured-voice', kind: 'voice' });
|
|
}
|
|
getTTSVoiceLists().litellm.forEach(function(voice) {
|
|
pushUniqueTTSItem(items, { id: voice, name: voice, source: 'configured-voice-list', kind: 'voice' });
|
|
});
|
|
if (getLiteLLMTTSModels(models).indexOf('local-kitten-tts') !== -1 || opts.currentModel === 'local-kitten-tts') {
|
|
KITTEN_TTS_VOICES.forEach(function(voice) {
|
|
pushUniqueTTSItem(items, { id: voice, name: 'Kitten ' + voice, source: 'local-kitten-tts', kind: 'voice' });
|
|
});
|
|
}
|
|
if (getLiteLLMTTSModels(models).indexOf('local-supertonic-tts') !== -1 || opts.currentModel === 'local-supertonic-tts') {
|
|
SUPERTONIC_TTS_VOICES.forEach(function(voice) {
|
|
pushUniqueTTSItem(items, { id: voice, name: 'Supertonic ' + voice, source: 'local-supertonic-tts', kind: 'voice' });
|
|
});
|
|
}
|
|
if (getLiteLLMTTSModels(models).indexOf('groq-orpheus-english') !== -1 || opts.currentModel === 'groq-orpheus-english') {
|
|
GROQ_ORPHEUS_ENGLISH_VOICES.forEach(function(voice) {
|
|
pushUniqueTTSItem(items, { id: voice, name: 'Groq Orpheus ' + voice, source: 'groq-orpheus-english', kind: 'voice' });
|
|
});
|
|
}
|
|
if (getLiteLLMTTSModels(models).indexOf('groq-orpheus-arabic-saudi') !== -1 || opts.currentModel === 'groq-orpheus-arabic-saudi') {
|
|
GROQ_ORPHEUS_ARABIC_VOICES.forEach(function(voice) {
|
|
pushUniqueTTSItem(items, { id: voice, name: 'Groq Orpheus Arabic ' + voice, source: 'groq-orpheus-arabic-saudi', kind: 'voice' });
|
|
});
|
|
}
|
|
return items;
|
|
}
|
|
|
|
module.exports = {
|
|
getTTSEnvProvider,
|
|
getLiteLLMTTSDiscoveryItems,
|
|
getLiteLLMHeaders,
|
|
getLiteLLMTTSModels,
|
|
getLiteLLMTTSRequestOptions,
|
|
getLiteLLMTTSVoicesForModel,
|
|
getTTSProvider,
|
|
getTTSVoiceLists,
|
|
isLiteLLMTTSVoiceCompatible,
|
|
isLiteLLMTTSModel
|
|
};
|