Match TTS voices to the selected LiteLLM model
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.
This commit is contained in:
parent
82ed46d01b
commit
524ad40d49
21 changed files with 246 additions and 21 deletions
|
|
@ -13,7 +13,7 @@
|
|||
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||
<item name="windowActionBar">false</item>
|
||||
<item name="windowNoTitle">true</item>
|
||||
<item name="android:background">@null</item>
|
||||
<item name="android:background">@color/colorPrimary</item>
|
||||
<item name="android:statusBarColor">@color/colorPrimaryDark</item>
|
||||
<item name="android:navigationBarColor">@color/colorPrimaryDark</item>
|
||||
</style>
|
||||
|
|
@ -22,4 +22,4 @@
|
|||
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
|
||||
<item name="android:background">@drawable/splash</item>
|
||||
</style>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -11,6 +11,9 @@
|
|||
<button class="wv-subtab-btn" data-subtab="milestones">
|
||||
<i class="fas fa-baby"></i> Milestones
|
||||
</button>
|
||||
<button class="wv-subtab-btn" data-subtab="lincoln">
|
||||
<i class="fas fa-clipboard-list"></i> Lincoln
|
||||
</button>
|
||||
<button class="wv-subtab-btn" data-subtab="shadess" style="display:none;">
|
||||
<i class="fas fa-brain"></i> SSHADESS (12+)
|
||||
</button>
|
||||
|
|
@ -119,6 +122,65 @@
|
|||
|
||||
</div>
|
||||
|
||||
<!-- Lincoln quick-reference sub-panel -->
|
||||
<div id="wv-panel-lincoln" class="wv-subpanel hidden">
|
||||
<div class="card" style="margin-bottom:10px;">
|
||||
<div class="card-header output-header">
|
||||
<h3><i class="fas fa-clipboard-list"></i> Lincoln Well-Child Quick Reference</h3>
|
||||
<div class="output-actions">
|
||||
<button class="btn-sm btn-primary" data-action="copy" data-target="wv-lincoln-reference"><i class="fas fa-copy"></i> Copy</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="wv-lincoln-reference" class="wv-lincoln-reference">
|
||||
<div class="wv-lincoln-grid">
|
||||
<section class="wv-lincoln-card">
|
||||
<h4>Infancy</h4>
|
||||
<ul>
|
||||
<li><strong>Newborn:</strong> POC visit; check for jaundice.</li>
|
||||
<li><strong>2 weeks:</strong> weight gain, newborn screen, umbilicus check.</li>
|
||||
<li><strong>1 month:</strong> maternal PHQ-9.</li>
|
||||
<li><strong>2 months:</strong> Vaxelis, rotavirus, Prevnar.</li>
|
||||
<li><strong>4 months:</strong> Vaxelis, rotavirus, Prevnar.</li>
|
||||
<li><strong>6 months:</strong> routine vaccines and Prevnar; confirm rotavirus eligibility by product and age.</li>
|
||||
<li><strong>9 months:</strong> SWYC; no routine vaccines noted.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="wv-lincoln-card">
|
||||
<h4>Toddler / Preschool</h4>
|
||||
<ul>
|
||||
<li><strong>12 months:</strong> MMR, varicella, Hep A; CBC and lead.</li>
|
||||
<li><strong>15 months:</strong> Pentacel, Prevnar, influenza.</li>
|
||||
<li><strong>18 months:</strong> POSI, SWYC; Hep A second dose.</li>
|
||||
<li><strong>2 years:</strong> POSI/SWYC; CBC and lead.</li>
|
||||
<li><strong>3 years:</strong> blood pressure check and vision screening; BP is commonly missed and can be added on diagnosis.</li>
|
||||
<li><strong>4 years:</strong> hearing and vision start; ProQuad and Kinrix.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="wv-lincoln-card">
|
||||
<h4>School Age / Adolescence</h4>
|
||||
<ul>
|
||||
<li><strong>Lipid screening:</strong> AAP screening at 9-11 years and 17-21 years.</li>
|
||||
<li><strong>Depression screening:</strong> begin at 12 years and older.</li>
|
||||
<li><strong>MenB:</strong> discuss Bexsero/MenB at 16-23 years, preferably 16-18 years, when chosen or indicated.</li>
|
||||
<li><strong>Age ≥18 years:</strong> Hep C testing.</li>
|
||||
<li><strong>Cervical cancer screening:</strong> start Pap smear screening at 21 years.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="wv-lincoln-card">
|
||||
<h4>Catch-Up / Screening Reminders</h4>
|
||||
<ul>
|
||||
<li><strong>Influenza:</strong> if a child 6 months through 8 years needs 2 doses, give doses 4 weeks apart.</li>
|
||||
<li><strong>Lead:</strong> continue lead screening reminders through age 6 years; add diagnosis when needed.</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SSHADESS sub-panel (age 12+) -->
|
||||
<div id="wv-panel-shadess" class="wv-subpanel hidden">
|
||||
<div class="card" style="margin-bottom:10px;">
|
||||
|
|
@ -305,4 +367,3 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -397,6 +397,21 @@ textarea.full-input{resize:vertical;}
|
|||
.wv-section-title{font-size:14px;font-weight:700;color:var(--g700);margin-bottom:12px;display:flex;align-items:center;gap:8px;}
|
||||
.wv-section-title i{color:var(--blue);}
|
||||
|
||||
/* Lincoln quick reference */
|
||||
.wv-lincoln-reference{padding:14px 16px;background:var(--g50);}
|
||||
.wv-lincoln-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:14px;}
|
||||
.wv-lincoln-card{border:1px solid var(--g100);border-radius:12px;padding:14px 16px;background:white;box-shadow:0 1px 2px rgba(15,23,42,0.04);}
|
||||
.wv-lincoln-card h4{margin:0 0 10px;font-size:14px;color:var(--g800);}
|
||||
.wv-lincoln-card ul{margin:0;padding-left:18px;color:var(--g700);font-size:13px;line-height:1.65;}
|
||||
.wv-lincoln-card li{margin-bottom:6px;}
|
||||
.wv-lincoln-card li:last-child{margin-bottom:0;}
|
||||
@media(max-width:640px){
|
||||
.wv-lincoln-reference{padding:10px;}
|
||||
.wv-lincoln-grid{grid-template-columns:1fr;gap:10px;}
|
||||
.wv-lincoln-card{padding:12px;}
|
||||
.wv-lincoln-card ul{font-size:12.5px;line-height:1.55;}
|
||||
}
|
||||
|
||||
/* Billing */
|
||||
.wv-billing-grid{display:flex;flex-wrap:wrap;gap:12px;align-items:center;}
|
||||
.wv-billing-cell{display:flex;align-items:center;gap:8px;}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ var { authMiddleware, adminMiddleware } = require('../middleware/auth');
|
|||
var PROMPTS = require('../utils/prompts');
|
||||
var logger = require('../utils/logger');
|
||||
var { gatewayUrl } = require('../utils/errors');
|
||||
var { getTTSEnvProvider, getLiteLLMTTSDiscoveryItems, getTTSProvider, getTTSVoiceLists } = require('../utils/ttsProvider');
|
||||
var { getTTSEnvProvider, getLiteLLMTTSDiscoveryItems, getLiteLLMTTSRequestOptions, getLiteLLMTTSVoicesForModel, isLiteLLMTTSVoiceCompatible, getTTSProvider } = require('../utils/ttsProvider');
|
||||
var { getLiteLLMHeaders, getLiteLLMAdminHeaders } = require('../utils/litellm');
|
||||
var { getSTTDependencies, getLiteLLMSTTModels, getSTTModelLists, getSTTProvider } = require('../utils/sttProvider');
|
||||
var { getLiteLLMEmbeddingModels } = require('../utils/embeddings');
|
||||
|
|
@ -550,12 +550,17 @@ router.get('/config/tts', async function(req, res) {
|
|||
var dbModel = await db.getSetting('tts.model') || '';
|
||||
var envVoice = process.env.LITELLM_TTS_VOICE || '';
|
||||
var envModel = process.env.LITELLM_TTS_MODEL || '';
|
||||
var currentModel = dbModel || envModel;
|
||||
var voices = getLiteLLMTTSVoicesForModel(currentModel, { currentVoice: dbVoice });
|
||||
var currentVoice = [dbVoice, envVoice, voices[0]].find(function(voice) {
|
||||
return isLiteLLMTTSVoiceCompatible(currentModel, voice);
|
||||
}) || '';
|
||||
res.json({
|
||||
success: true,
|
||||
provider: activeProvider,
|
||||
envProvider: envProvider,
|
||||
currentVoice: dbVoice || envVoice,
|
||||
currentModel: dbModel || envModel,
|
||||
currentVoice: currentVoice,
|
||||
currentModel: currentModel,
|
||||
dbVoice: dbVoice,
|
||||
dbModel: dbModel,
|
||||
envVoice: envVoice,
|
||||
|
|
@ -563,7 +568,9 @@ router.get('/config/tts', async function(req, res) {
|
|||
configured: {
|
||||
litellm: !!process.env.LITELLM_API_BASE
|
||||
},
|
||||
voices: getTTSVoiceLists()
|
||||
voices: {
|
||||
litellm: voices
|
||||
}
|
||||
});
|
||||
} catch (e) { res.status(500).json({ error: 'Request failed' }); }
|
||||
});
|
||||
|
|
@ -616,11 +623,15 @@ router.post('/config/tts/test', async function(req, res) {
|
|||
var adminModel = await db.getSetting('tts.model') || '';
|
||||
var adminVoice = await db.getSetting('tts.voice') || '';
|
||||
var ttsModel = adminModel || process.env.LITELLM_TTS_MODEL || '';
|
||||
var usedVoice = voice || adminVoice || process.env.LITELLM_TTS_VOICE || '';
|
||||
var defaultVoices = getLiteLLMTTSVoicesForModel(ttsModel, { currentVoice: adminVoice });
|
||||
var usedVoice = [voice, adminVoice, process.env.LITELLM_TTS_VOICE || '', defaultVoices[0]].find(function(candidate) {
|
||||
return isLiteLLMTTSVoiceCompatible(ttsModel, candidate);
|
||||
}) || '';
|
||||
if (!ttsModel) return res.json({ success: false, error: 'No LiteLLM TTS model configured' });
|
||||
var payload = Object.assign({ model: ttsModel, voice: usedVoice, input: text }, getLiteLLMTTSRequestOptions(ttsModel));
|
||||
|
||||
var ttsResp = await axios.post(gatewayUrl('/audio/speech'),
|
||||
{ model: ttsModel, voice: usedVoice, input: text },
|
||||
payload,
|
||||
{ headers: getLiteLLMHeaders('application/json'), responseType: 'arraybuffer', timeout: 60000 }
|
||||
);
|
||||
var buffer = Buffer.from(ttsResp.data);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ const router = express.Router();
|
|||
const { authMiddleware } = require('../middleware/auth');
|
||||
var logger = require('../utils/logger');
|
||||
var { gatewayUrl } = require('../utils/errors');
|
||||
var { getTTSProvider } = require('../utils/ttsProvider');
|
||||
var { getLiteLLMTTSRequestOptions, getLiteLLMTTSVoicesForModel, isLiteLLMTTSVoiceCompatible, getTTSProvider } = require('../utils/ttsProvider');
|
||||
var { getLiteLLMHeaders } = require('../utils/litellm');
|
||||
|
||||
// TTS is intentionally routed only through LiteLLM. Provider-specific voice
|
||||
|
|
@ -22,19 +22,22 @@ router.post('/text-to-speech', authMiddleware, async (req, res) => {
|
|||
var userVoice = userPrefs?.tts_voice;
|
||||
var adminVoice = await db.getSetting('tts.voice') || '';
|
||||
var adminModel = await db.getSetting('tts.model') || '';
|
||||
var resolvedVoice = userVoice || adminVoice || '';
|
||||
|
||||
if (ttsProvider !== 'litellm' || !process.env.LITELLM_API_BASE) {
|
||||
return res.status(400).json({ error: 'TTS not configured. Set LITELLM_API_BASE.' });
|
||||
}
|
||||
var ttsModel = adminModel || process.env.LITELLM_TTS_MODEL || '';
|
||||
var ttsVoice = resolvedVoice || process.env.LITELLM_TTS_VOICE || '';
|
||||
var defaultVoices = getLiteLLMTTSVoicesForModel(ttsModel, { currentVoice: adminVoice });
|
||||
var ttsVoice = [userVoice, adminVoice, process.env.LITELLM_TTS_VOICE || '', defaultVoices[0]].find(function(voice) {
|
||||
return isLiteLLMTTSVoiceCompatible(ttsModel, voice);
|
||||
}) || '';
|
||||
if (!ttsModel) return res.status(400).json({ error: 'No LiteLLM TTS model configured.' });
|
||||
var payload = Object.assign({ model: ttsModel, input: text, voice: ttsVoice }, getLiteLLMTTSRequestOptions(ttsModel));
|
||||
|
||||
var ttsResp = await fetch(gatewayUrl('/audio/speech'), {
|
||||
method: 'POST',
|
||||
headers: getLiteLLMHeaders('application/json'),
|
||||
body: JSON.stringify({ model: ttsModel, input: text, voice: ttsVoice })
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
if (!ttsResp.ok) {
|
||||
var errBody = await ttsResp.text();
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ var router = express.Router();
|
|||
var db = require('../db/database');
|
||||
var { authMiddleware } = require('../middleware/auth');
|
||||
var { getSTTModelLists, getSTTProvider } = require('../utils/sttProvider');
|
||||
var { getTTSProvider, getTTSVoiceLists } = require('../utils/ttsProvider');
|
||||
var { getLiteLLMTTSVoicesForModel, getTTSProvider } = require('../utils/ttsProvider');
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
||||
|
|
@ -49,14 +49,18 @@ router.get('/preferences/options', async function(req, res) {
|
|||
var provider = getSTTProvider();
|
||||
var ttsProvider = getTTSProvider();
|
||||
|
||||
var dbModel = await db.getSetting('tts.model') || '';
|
||||
var dbVoice = await db.getSetting('tts.voice') || '';
|
||||
var ttsModel = dbModel || process.env.LITELLM_TTS_MODEL || '';
|
||||
var sttModels = getSTTModelLists().litellm.map(function(model) { return { value: model, label: model }; });
|
||||
var ttsVoices = getTTSVoiceLists().litellm.map(function(voice) { return { value: voice, label: voice }; });
|
||||
var ttsVoices = getLiteLLMTTSVoicesForModel(ttsModel, { currentVoice: dbVoice }).map(function(voice) { return { value: voice, label: voice }; });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
sttProvider: provider || 'none',
|
||||
sttModels: sttModels,
|
||||
ttsProvider: ttsProvider || 'browser',
|
||||
ttsModel: ttsModel || null,
|
||||
ttsVoices: ttsVoices
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,20 @@ function parseList(value) {
|
|||
.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';
|
||||
|
|
@ -20,10 +34,58 @@ function getTTSEnvProvider() {
|
|||
|
||||
function getTTSVoiceLists() {
|
||||
return {
|
||||
litellm: parseList(process.env.LITELLM_TTS_VOICES)
|
||||
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';
|
||||
|
|
@ -56,6 +118,26 @@ function getLiteLLMTTSDiscoveryItems(models, opts) {
|
|||
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;
|
||||
}
|
||||
|
||||
|
|
@ -64,7 +146,10 @@ module.exports = {
|
|||
getLiteLLMTTSDiscoveryItems,
|
||||
getLiteLLMHeaders,
|
||||
getLiteLLMTTSModels,
|
||||
getLiteLLMTTSRequestOptions,
|
||||
getLiteLLMTTSVoicesForModel,
|
||||
getTTSProvider,
|
||||
getTTSVoiceLists,
|
||||
isLiteLLMTTSVoiceCompatible,
|
||||
isLiteLLMTTSModel
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const ENV_KEYS = ['TTS_PROVIDER', 'LITELLM_API_BASE', 'LITELLM_API_KEY', 'LITELLM_MASTER_KEY', 'LITELLM_TTS_VOICES'];
|
||||
const ENV_KEYS = ['TTS_PROVIDER', 'LITELLM_API_BASE', 'LITELLM_API_KEY', 'LITELLM_MASTER_KEY', 'LITELLM_TTS_VOICES', 'LITELLM_TTS_VOICE'];
|
||||
|
||||
function withEnv(overrides, fn) {
|
||||
const previous = {};
|
||||
|
|
@ -36,10 +36,11 @@ test('TTS provider ignores non-LiteLLM provider overrides', () => {
|
|||
test('LiteLLM TTS voice list comes from configured voice catalog', () => {
|
||||
const ttsProvider = require('../src/utils/ttsProvider');
|
||||
withEnv({ LITELLM_TTS_VOICES: 'sherpa/kokoro:am_adam, sherpa/kokoro:af_bella' }, () => {
|
||||
assert.deepEqual(ttsProvider.getTTSVoiceLists().litellm, [
|
||||
'sherpa/kokoro:am_adam',
|
||||
'sherpa/kokoro:af_bella'
|
||||
]);
|
||||
const voices = ttsProvider.getTTSVoiceLists().litellm;
|
||||
assert.equal(voices[0], 'sherpa/kokoro:am_adam');
|
||||
assert.equal(voices[1], 'sherpa/kokoro:af_bella');
|
||||
assert.equal(voices.includes('Jasper'), true);
|
||||
assert.equal(voices.includes('F1'), true);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -92,6 +93,19 @@ test('LiteLLM TTS discovery includes metadata models and configured fallbacks',
|
|||
});
|
||||
});
|
||||
|
||||
test('LiteLLM TTS discovery expands Kitten and Supertonic voices', () => {
|
||||
const ttsProvider = require('../src/utils/ttsProvider');
|
||||
withEnv({}, () => {
|
||||
const items = ttsProvider.getLiteLLMTTSDiscoveryItems([
|
||||
{ model_name: 'local-kitten-tts', model_info: { mode: 'audio_speech' } },
|
||||
{ model_name: 'local-supertonic-tts', model_info: { mode: 'audio_speech' } }
|
||||
], {});
|
||||
assert.equal(items.some(function(item) { return item.id === 'local-kitten-tts' && item.kind === 'model'; }), true);
|
||||
assert.equal(items.some(function(item) { return item.id === 'Jasper' && item.kind === 'voice'; }), true);
|
||||
assert.equal(items.some(function(item) { return item.id === 'F1' && item.kind === 'voice'; }), true);
|
||||
});
|
||||
});
|
||||
|
||||
test('LiteLLM TTS discovery still shows configured model if metadata lookup fails', () => {
|
||||
const ttsProvider = require('../src/utils/ttsProvider');
|
||||
assert.deepEqual(ttsProvider.getLiteLLMTTSDiscoveryItems([], {
|
||||
|
|
@ -103,6 +117,38 @@ test('LiteLLM TTS discovery still shows configured model if metadata lookup fail
|
|||
]);
|
||||
});
|
||||
|
||||
test('LiteLLM TTS voices are scoped to the active local model', () => {
|
||||
const ttsProvider = require('../src/utils/ttsProvider');
|
||||
withEnv({ LITELLM_TTS_VOICES: 'sherpa/kokoro:am_adam,sherpa/kokoro:af_bella', LITELLM_TTS_VOICE: 'sherpa/kokoro:am_adam' }, () => {
|
||||
assert.deepEqual(ttsProvider.getLiteLLMTTSVoicesForModel('local-kitten-tts'), ['Bella', 'Jasper', 'Luna', 'Bruno', 'Rosie', 'Hugo', 'Kiki', 'Leo']);
|
||||
assert.deepEqual(ttsProvider.getLiteLLMTTSVoicesForModel('local-supertonic-tts'), ['F1', 'F2', 'F3', 'F4', 'F5', 'M1', 'M2', 'M3', 'M4', 'M5']);
|
||||
assert.deepEqual(ttsProvider.getLiteLLMTTSVoicesForModel('local-kokoro-tts'), ['sherpa/kokoro:am_adam', 'sherpa/kokoro:af_bella']);
|
||||
assert.deepEqual(ttsProvider.getLiteLLMTTSVoicesForModel('groq-orpheus-english'), ['autumn', 'diana', 'hannah', 'austin', 'daniel', 'troy']);
|
||||
assert.deepEqual(ttsProvider.getLiteLLMTTSVoicesForModel('canopylabs/orpheus-arabic-saudi'), ['abdullah', 'fahad', 'sultan', 'lulwa', 'noura', 'aisha']);
|
||||
});
|
||||
});
|
||||
|
||||
test('LiteLLM TTS compatibility rejects cross-model local voices', () => {
|
||||
const ttsProvider = require('../src/utils/ttsProvider');
|
||||
assert.equal(ttsProvider.isLiteLLMTTSVoiceCompatible('local-kokoro-tts', 'Bella'), false);
|
||||
assert.equal(ttsProvider.isLiteLLMTTSVoiceCompatible('local-kokoro-tts', 'M1'), false);
|
||||
assert.equal(ttsProvider.isLiteLLMTTSVoiceCompatible('local-kitten-tts', 'Bella'), true);
|
||||
assert.equal(ttsProvider.isLiteLLMTTSVoiceCompatible('local-kitten-tts', 'M1'), false);
|
||||
assert.equal(ttsProvider.isLiteLLMTTSVoiceCompatible('local-supertonic-tts', 'M1'), true);
|
||||
assert.equal(ttsProvider.isLiteLLMTTSVoiceCompatible('local-supertonic-tts', 'Bella'), false);
|
||||
assert.equal(ttsProvider.isLiteLLMTTSVoiceCompatible('groq-orpheus-english', 'hannah'), true);
|
||||
assert.equal(ttsProvider.isLiteLLMTTSVoiceCompatible('groq-orpheus-english', 'sherpa/kokoro:af_bella'), false);
|
||||
assert.equal(ttsProvider.isLiteLLMTTSVoiceCompatible('groq-orpheus-arabic-saudi', 'aisha'), true);
|
||||
assert.equal(ttsProvider.isLiteLLMTTSVoiceCompatible('groq-orpheus-arabic-saudi', 'troy'), false);
|
||||
});
|
||||
|
||||
test('Groq Orpheus TTS requests force wav response format', () => {
|
||||
const ttsProvider = require('../src/utils/ttsProvider');
|
||||
assert.deepEqual(ttsProvider.getLiteLLMTTSRequestOptions('groq-orpheus-english'), { response_format: 'wav' });
|
||||
assert.deepEqual(ttsProvider.getLiteLLMTTSRequestOptions('canopylabs/orpheus-arabic-saudi'), { response_format: 'wav' });
|
||||
assert.deepEqual(ttsProvider.getLiteLLMTTSRequestOptions('local-kokoro-tts'), {});
|
||||
});
|
||||
|
||||
test('LiteLLM headers include optional content type without exposing key value', () => {
|
||||
const ttsProvider = require('../src/utils/ttsProvider');
|
||||
withEnv({ LITELLM_API_KEY: 'secret-token' }, () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue