- Introduce voice utils (model detection, voice parsing/weights, limits) - Enable Kokoro multi-voice strings across OpenAI/Deepinfra/custom providers - Normalize non-Kokoro voices to single token for SDK calls - Expose full Kokoro voice list for custom-openai Kokoro models - Update TTS API to return audio as ArrayBuffer and improve logging - Add multi-select UI for Kokoro voices with provider-based clamping - Preserve Kokoro voice strings in TTSContext and coalesce restarts - Merge multi-sentence quoted dialogue in NLP sentence splitter - Update tests for single and multi-voice selection flows
74 lines
2.2 KiB
TypeScript
74 lines
2.2 KiB
TypeScript
/**
|
|
* Voice Utilities
|
|
*
|
|
* This module provides utilities for handling voice selection and management,
|
|
* particularly for Kokoro multi-voice syntax.
|
|
*/
|
|
|
|
/**
|
|
* Parses a Kokoro voice string into individual voice names
|
|
* Strips weights like "af_heart(0.5)" -> "af_heart"
|
|
*
|
|
* @param voiceString - Voice string to parse (e.g., "af_heart(0.5)+bf_emma(0.5)")
|
|
* @returns Array of voice names without weights
|
|
*/
|
|
export const parseKokoroVoiceNames = (voiceString: string): string[] =>
|
|
voiceString
|
|
.split('+')
|
|
.map(s => s.trim())
|
|
.filter(Boolean)
|
|
.map(s => s.replace(/\([^)]*\)/g, '').trim());
|
|
|
|
/**
|
|
* Builds a Kokoro voice string from an array of voice names
|
|
* Automatically calculates equal weights for multiple voices
|
|
*
|
|
* @param names - Array of voice names
|
|
* @returns Formatted voice string with weights or single voice name
|
|
*/
|
|
export const buildKokoroVoiceString = (names: string[]): string => {
|
|
const n = names.length;
|
|
if (n === 0) return '';
|
|
if (n === 1) return names[0];
|
|
|
|
const weight = 1 / n;
|
|
const weightString = weight.toFixed(2).replace(/0+$/, '').replace(/\.$/, '');
|
|
return names.map(name => `${name}(${weightString})`).join('+');
|
|
};
|
|
|
|
/**
|
|
* Checks if a model name is a Kokoro model
|
|
*
|
|
* @param modelName - TTS model name
|
|
* @returns True if the model is a Kokoro model
|
|
*/
|
|
export const isKokoroModel = (modelName: string | undefined): boolean => {
|
|
return (modelName || '').toLowerCase().includes('kokoro');
|
|
};
|
|
|
|
/**
|
|
* Strips weight annotations from a voice string
|
|
*
|
|
* @param voiceString - Voice string with or without weights
|
|
* @returns Voice string without weights
|
|
*/
|
|
export const stripVoiceWeights = (voiceString: string): string => {
|
|
return voiceString.replace(/\([^)]*\)/g, '').trim();
|
|
};
|
|
|
|
/**
|
|
* Determines the maximum number of voices allowed for a provider/model combination
|
|
*
|
|
* @param provider - TTS provider name
|
|
* @param model - TTS model name
|
|
* @returns Maximum number of voices (Infinity for unlimited)
|
|
*/
|
|
export const getMaxVoicesForProvider = (provider: string, model: string): number => {
|
|
if (!isKokoroModel(model)) return 1;
|
|
|
|
// Deepinfra Kokoro supports up to 2 voices
|
|
if (provider === 'deepinfra') return 2;
|
|
|
|
// Other providers with Kokoro support unlimited voices
|
|
return Infinity;
|
|
};
|