feat: add custom sarvam tts voice

This commit is contained in:
Sabiha Khan 2026-06-17 17:18:41 +05:30
parent a2d9ed24ed
commit 7c919264f9
4 changed files with 61 additions and 18 deletions

View file

@ -990,6 +990,7 @@ class SarvamTTSConfiguration(BaseTTSConfiguration):
description="Sarvam voice name; must match the selected model's voice list.",
json_schema_extra={
"examples": SARVAM_V2_VOICES,
"allow_custom_input": True,
"model_options": {
"bulbul:v2": SARVAM_V2_VOICES,
"bulbul:v3": SARVAM_V3_VOICES,

View file

@ -534,7 +534,7 @@ def create_tts_service(
language = getattr(user_config.tts, "language", None)
pipecat_language = language_mapping.get(language, Language.HI)
voice = getattr(user_config.tts, "voice", None) or "anushka"
voice = (getattr(user_config.tts, "voice", None) or "anushka").strip().lower()
speed = getattr(user_config.tts, "speed", None)
settings_kwargs = {
"model": user_config.tts.model,

View file

@ -126,6 +126,15 @@ class TestSarvamTTSServiceFactory:
assert config.language == "hi-IN"
assert config.speed == 1.0
def test_sarvam_tts_voice_schema_allows_custom_model_specific_options(self):
voice_schema = SarvamTTSConfiguration.model_json_schema()["properties"][
"voice"
]
assert voice_schema["allow_custom_input"] is True
assert "bulbul:v2" in voice_schema["model_options"]
assert "bulbul:v3" in voice_schema["model_options"]
def test_create_sarvam_tts_service_maps_speed_to_pace(self):
user_config = SimpleNamespace(
tts=SimpleNamespace(
@ -152,3 +161,26 @@ class TestSarvamTTSServiceFactory:
assert kwargs["settings"].voice == "anushka"
assert kwargs["settings"].language == Language.HI
assert kwargs["settings"].pace == 1.25
def test_create_sarvam_tts_service_normalizes_custom_voice_id(self):
user_config = SimpleNamespace(
tts=SimpleNamespace(
provider=ServiceProviders.SARVAM.value,
api_key="test-key",
model="bulbul:v2",
voice=" Rehan ",
language="hi-IN",
speed=1.0,
)
)
audio_config = AudioConfig(
transport_in_sample_rate=16000, transport_out_sample_rate=16000
)
with patch(
"api.services.pipecat.service_factory.SarvamTTSService"
) as mock_service:
create_tts_service(user_config, audio_config)
kwargs = mock_service.call_args.kwargs
assert kwargs["settings"].voice == "rehan"

View file

@ -130,6 +130,19 @@ function getGlobalSummary(
return model ? `${providerLabel} / ${model}` : providerLabel || provider;
}
function getSchemaDropdownOptions(
schema: SchemaProperty | undefined,
modelValue?: string,
): string[] | undefined {
let dropdownOptions = schema?.enum || schema?.examples;
if (schema?.model_options && modelValue && schema.model_options[modelValue]) {
dropdownOptions = schema.model_options[modelValue];
}
return dropdownOptions;
}
export function ServiceConfigurationForm({
mode,
currentOverrides,
@ -344,10 +357,12 @@ export function ServiceConfigurationForm({
? providerSchema.$defs[(schema as SchemaProperty).$ref!.split('/').pop() || '']
: schema as SchemaProperty;
if (!actualSchema?.allow_custom_input || !actualSchema?.examples) return;
if (!actualSchema?.allow_custom_input) return;
const savedValue = src?.[field] as string | undefined;
if (savedValue && !actualSchema.examples.includes(savedValue)) {
const modelValue = src?.model as string | undefined;
const dropdownOptions = getSchemaDropdownOptions(actualSchema, modelValue);
if (savedValue && dropdownOptions && !dropdownOptions.includes(savedValue)) {
detectedCustomInput[`${service}_${field}`] = true;
}
});
@ -381,10 +396,11 @@ export function ServiceConfigurationForm({
const validVoices = modelOptions[ttsModel as string];
const currentVoice = getValues("tts_voice") as string;
if (validVoices && currentVoice && !validVoices.includes(currentVoice)) {
const isCustomVoice = !!isCustomInput.tts_voice;
if (validVoices && currentVoice && !validVoices.includes(currentVoice) && !isCustomVoice) {
setValue("tts_voice", validVoices[0], { shouldDirty: true });
}
}, [ttsModel, serviceProviders.tts, setValue, getValues, schemas]);
}, [ttsModel, serviceProviders.tts, setValue, getValues, schemas, isCustomInput.tts_voice]);
// Reset language when STT model changes if the provider has model-dependent language options
const sttModel = watch("stt_model");
@ -676,10 +692,13 @@ export function ServiceConfigurationForm({
const actualSchema = schema.$ref && providerSchema.$defs
? providerSchema.$defs[schema.$ref.split('/').pop() || '']
: schema;
const dropdownOptions = getSchemaDropdownOptions(
actualSchema,
watch(`${service}_model`) as string | undefined,
);
if (service === "tts" && field === "voice" && !actualSchema?.allow_custom_input) {
const hasVoiceOptions = actualSchema?.enum || actualSchema?.examples;
if (!hasVoiceOptions) {
if (!dropdownOptions) {
return (
<VoiceSelector
provider={serviceProviders.tts}
@ -693,10 +712,10 @@ export function ServiceConfigurationForm({
}
}
if (actualSchema?.allow_custom_input && actualSchema?.examples) {
if (actualSchema?.allow_custom_input && dropdownOptions && dropdownOptions.length > 0) {
const fieldKey = `${service}_${field}`;
const currentValue = watch(fieldKey) as string || "";
const options = actualSchema.examples;
const options = dropdownOptions;
if (isCustomInput[fieldKey]) {
return (
@ -764,15 +783,6 @@ export function ServiceConfigurationForm({
);
}
let dropdownOptions = actualSchema?.enum || actualSchema?.examples;
if (actualSchema?.model_options) {
const modelValue = watch(`${service}_model`) as string;
if (modelValue && actualSchema.model_options[modelValue]) {
dropdownOptions = actualSchema.model_options[modelValue];
}
}
if (dropdownOptions && dropdownOptions.length > 0) {
const getDisplayName = (value: string) => {
if (field === "language") {