From 7c919264f97b2d31591fce52d10b75403603f859 Mon Sep 17 00:00:00 2001 From: Sabiha Khan Date: Wed, 17 Jun 2026 17:18:41 +0530 Subject: [PATCH] feat: add custom sarvam tts voice --- api/services/configuration/registry.py | 1 + api/services/pipecat/service_factory.py | 2 +- api/tests/test_sarvam_service_factory.py | 32 ++++++++++++++ .../components/ServiceConfigurationForm.tsx | 44 ++++++++++++------- 4 files changed, 61 insertions(+), 18 deletions(-) diff --git a/api/services/configuration/registry.py b/api/services/configuration/registry.py index 9ac9b7d2..337b1a12 100644 --- a/api/services/configuration/registry.py +++ b/api/services/configuration/registry.py @@ -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, diff --git a/api/services/pipecat/service_factory.py b/api/services/pipecat/service_factory.py index 68f7c303..a5777b26 100644 --- a/api/services/pipecat/service_factory.py +++ b/api/services/pipecat/service_factory.py @@ -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, diff --git a/api/tests/test_sarvam_service_factory.py b/api/tests/test_sarvam_service_factory.py index 7abd0d0c..f08d0483 100644 --- a/api/tests/test_sarvam_service_factory.py +++ b/api/tests/test_sarvam_service_factory.py @@ -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" diff --git a/ui/src/components/ServiceConfigurationForm.tsx b/ui/src/components/ServiceConfigurationForm.tsx index f5075398..fb8e72a4 100644 --- a/ui/src/components/ServiceConfigurationForm.tsx +++ b/ui/src/components/ServiceConfigurationForm.tsx @@ -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 ( 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") {