mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-07-16 11:31:04 +02:00
feat: add gemini live and speaches integration (#220)
* feat: add speaches models * feat: add gemini realtime and speaches integration - Add gemini realtime support - Add speaches support for locally hosted LLMs * chore: bump pipecat * feat: add language option * fix: add skip aggregator types to tts settings * fix: make API key optional for realtime
This commit is contained in:
parent
e0c3d6c3bf
commit
87e72d5f6f
19 changed files with 743 additions and 270 deletions
|
|
@ -46,7 +46,9 @@ class UserConfigurationValidator:
|
|||
ServiceProviders.SPEECHMATICS.value: self._check_speechmatics_api_key,
|
||||
ServiceProviders.CAMB.value: self._check_camb_api_key,
|
||||
ServiceProviders.AWS_BEDROCK.value: self._check_aws_bedrock_api_key,
|
||||
ServiceProviders.SELF_HOSTED.value: self._check_self_hosted_api_key,
|
||||
ServiceProviders.SPEACHES.value: self._check_speaches_api_key,
|
||||
ServiceProviders.OPENAI_REALTIME.value: self._check_openai_api_key,
|
||||
ServiceProviders.GOOGLE_REALTIME.value: self._check_google_api_key,
|
||||
}
|
||||
|
||||
async def validate(
|
||||
|
|
@ -70,6 +72,13 @@ class UserConfigurationValidator:
|
|||
configuration.embeddings, "embeddings", required=False
|
||||
)
|
||||
)
|
||||
# Realtime is optional - only validate if is_realtime is enabled
|
||||
if configuration.is_realtime:
|
||||
status_list.extend(
|
||||
self._validate_service(
|
||||
configuration.realtime, "realtime", required=True
|
||||
)
|
||||
)
|
||||
|
||||
if status_list:
|
||||
raise ValueError(status_list)
|
||||
|
|
@ -90,10 +99,10 @@ class UserConfigurationValidator:
|
|||
|
||||
provider = service_config.provider
|
||||
|
||||
# Self-hosted doesn't require an API key
|
||||
if provider == ServiceProviders.SELF_HOSTED.value:
|
||||
# Speaches doesn't require an API key
|
||||
if provider == ServiceProviders.SPEACHES.value:
|
||||
try:
|
||||
if not self._check_self_hosted_api_key(provider, service_config):
|
||||
if not self._check_speaches_api_key(provider, service_config):
|
||||
return [
|
||||
{
|
||||
"model": service_name,
|
||||
|
|
@ -199,9 +208,9 @@ class UserConfigurationValidator:
|
|||
def _check_camb_api_key(self, model: str, api_key: str) -> bool:
|
||||
return True
|
||||
|
||||
def _check_self_hosted_api_key(self, model: str, service_config) -> bool:
|
||||
def _check_speaches_api_key(self, model: str, service_config) -> bool:
|
||||
if not getattr(service_config, "base_url", None):
|
||||
raise ValueError("base_url is required for self-hosted LLM")
|
||||
raise ValueError("base_url is required for Speaches services")
|
||||
return True
|
||||
|
||||
def _check_aws_bedrock_api_key(self, model: str, service_config) -> bool:
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ def contains_masked_key(api_key: str | list[str] | None) -> bool:
|
|||
|
||||
def check_for_masked_keys(config: "UserConfiguration") -> None:
|
||||
"""Raise ValueError if any service in *config* still has a masked API key."""
|
||||
for field in ("llm", "tts", "stt", "embeddings"):
|
||||
for field in ("llm", "tts", "stt", "embeddings", "realtime"):
|
||||
service = getattr(config, field, None)
|
||||
if service is None:
|
||||
continue
|
||||
|
|
@ -121,6 +121,8 @@ def mask_user_config(config: UserConfiguration) -> Dict[str, Any]:
|
|||
"tts": _mask_service(config.tts),
|
||||
"stt": _mask_service(config.stt),
|
||||
"embeddings": _mask_service(config.embeddings),
|
||||
"realtime": _mask_service(config.realtime),
|
||||
"is_realtime": config.is_realtime,
|
||||
"test_phone_number": config.test_phone_number,
|
||||
"timezone": config.timezone,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from typing import Dict
|
|||
from api.schemas.user_configuration import UserConfiguration
|
||||
from api.services.configuration.masking import resolve_masked_api_keys
|
||||
|
||||
SERVICE_FIELDS = ("llm", "tts", "stt", "embeddings")
|
||||
SERVICE_FIELDS = ("llm", "tts", "stt", "embeddings", "realtime")
|
||||
|
||||
|
||||
def merge_user_configurations(
|
||||
|
|
@ -64,6 +64,9 @@ def merge_user_configurations(
|
|||
_merge_service_block(service)
|
||||
|
||||
# other simple fields
|
||||
if "is_realtime" in incoming_partial:
|
||||
merged["is_realtime"] = incoming_partial["is_realtime"]
|
||||
|
||||
if "test_phone_number" in incoming_partial:
|
||||
merged["test_phone_number"] = incoming_partial["test_phone_number"]
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ class ServiceType(Enum):
|
|||
TTS = auto()
|
||||
STT = auto()
|
||||
EMBEDDINGS = auto()
|
||||
REALTIME = auto()
|
||||
|
||||
|
||||
class ServiceProviders(str, Enum):
|
||||
|
|
@ -27,7 +28,9 @@ class ServiceProviders(str, Enum):
|
|||
SPEECHMATICS = "speechmatics"
|
||||
CAMB = "camb"
|
||||
AWS_BEDROCK = "aws_bedrock"
|
||||
SELF_HOSTED = "self_hosted"
|
||||
SPEACHES = "speaches"
|
||||
OPENAI_REALTIME = "openai_realtime"
|
||||
GOOGLE_REALTIME = "google_realtime"
|
||||
|
||||
|
||||
class BaseServiceConfiguration(BaseModel):
|
||||
|
|
@ -41,7 +44,9 @@ class BaseServiceConfiguration(BaseModel):
|
|||
ServiceProviders.AZURE,
|
||||
ServiceProviders.DOGRAH,
|
||||
ServiceProviders.AWS_BEDROCK,
|
||||
ServiceProviders.SELF_HOSTED,
|
||||
ServiceProviders.SPEACHES,
|
||||
ServiceProviders.OPENAI_REALTIME,
|
||||
ServiceProviders.GOOGLE_REALTIME,
|
||||
# ServiceProviders.SARVAM,
|
||||
]
|
||||
api_key: str | list[str]
|
||||
|
|
@ -97,6 +102,7 @@ REGISTRY: Dict[ServiceType, Dict[str, Type[BaseServiceConfiguration]]] = {
|
|||
ServiceType.TTS: {},
|
||||
ServiceType.STT: {},
|
||||
ServiceType.EMBEDDINGS: {},
|
||||
ServiceType.REALTIME: {},
|
||||
}
|
||||
|
||||
T = TypeVar("T", bound=BaseServiceConfiguration)
|
||||
|
|
@ -191,14 +197,18 @@ AWS_BEDROCK_MODELS = [
|
|||
@register_llm
|
||||
class OpenAILLMService(BaseLLMConfiguration):
|
||||
provider: Literal[ServiceProviders.OPENAI] = ServiceProviders.OPENAI
|
||||
model: str = Field(default="gpt-4.1", json_schema_extra={"examples": OPENAI_MODELS})
|
||||
model: str = Field(
|
||||
default="gpt-4.1",
|
||||
json_schema_extra={"examples": OPENAI_MODELS, "allow_custom_input": True},
|
||||
)
|
||||
|
||||
|
||||
@register_llm
|
||||
class GoogleLLMService(BaseLLMConfiguration):
|
||||
provider: Literal[ServiceProviders.GOOGLE] = ServiceProviders.GOOGLE
|
||||
model: str = Field(
|
||||
default="gemini-2.0-flash", json_schema_extra={"examples": GOOGLE_MODELS}
|
||||
default="gemini-2.0-flash",
|
||||
json_schema_extra={"examples": GOOGLE_MODELS, "allow_custom_input": True},
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -206,7 +216,8 @@ class GoogleLLMService(BaseLLMConfiguration):
|
|||
class GroqLLMService(BaseLLMConfiguration):
|
||||
provider: Literal[ServiceProviders.GROQ] = ServiceProviders.GROQ
|
||||
model: str = Field(
|
||||
default="llama-3.3-70b-versatile", json_schema_extra={"examples": GROQ_MODELS}
|
||||
default="llama-3.3-70b-versatile",
|
||||
json_schema_extra={"examples": GROQ_MODELS, "allow_custom_input": True},
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -214,7 +225,8 @@ class GroqLLMService(BaseLLMConfiguration):
|
|||
class OpenRouterLLMConfiguration(BaseLLMConfiguration):
|
||||
provider: Literal[ServiceProviders.OPENROUTER] = ServiceProviders.OPENROUTER
|
||||
model: str = Field(
|
||||
default="openai/gpt-4.1", json_schema_extra={"examples": OPENROUTER_MODELS}
|
||||
default="openai/gpt-4.1",
|
||||
json_schema_extra={"examples": OPENROUTER_MODELS, "allow_custom_input": True},
|
||||
)
|
||||
|
||||
base_url: str = Field(default="https://openrouter.ai/api/v1")
|
||||
|
|
@ -224,7 +236,8 @@ class OpenRouterLLMConfiguration(BaseLLMConfiguration):
|
|||
class AzureLLMService(BaseLLMConfiguration):
|
||||
provider: Literal[ServiceProviders.AZURE] = ServiceProviders.AZURE
|
||||
model: str = Field(
|
||||
default="gpt-4.1-mini", json_schema_extra={"examples": AZURE_MODELS}
|
||||
default="gpt-4.1-mini",
|
||||
json_schema_extra={"examples": AZURE_MODELS, "allow_custom_input": True},
|
||||
)
|
||||
|
||||
endpoint: str
|
||||
|
|
@ -234,7 +247,8 @@ class AzureLLMService(BaseLLMConfiguration):
|
|||
class DograhLLMService(BaseLLMConfiguration):
|
||||
provider: Literal[ServiceProviders.DOGRAH] = ServiceProviders.DOGRAH
|
||||
model: str = Field(
|
||||
default="default", json_schema_extra={"examples": DOGRAH_LLM_MODELS}
|
||||
default="default",
|
||||
json_schema_extra={"examples": DOGRAH_LLM_MODELS, "allow_custom_input": True},
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -243,7 +257,7 @@ class AWSBedrockLLMConfiguration(BaseLLMConfiguration):
|
|||
provider: Literal[ServiceProviders.AWS_BEDROCK] = ServiceProviders.AWS_BEDROCK
|
||||
model: str = Field(
|
||||
default="us.amazon.nova-pro-v1:0",
|
||||
json_schema_extra={"examples": AWS_BEDROCK_MODELS},
|
||||
json_schema_extra={"examples": AWS_BEDROCK_MODELS, "allow_custom_input": True},
|
||||
)
|
||||
aws_access_key: str = Field(default="")
|
||||
aws_secret_key: str = Field(default="")
|
||||
|
|
@ -251,14 +265,18 @@ class AWSBedrockLLMConfiguration(BaseLLMConfiguration):
|
|||
api_key: str | list[str] | None = Field(default=None)
|
||||
|
||||
|
||||
SELF_HOSTED_LLM_MODELS = ["llama3", "mistral", "phi3", "qwen2", "gemma2", "deepseek-r1"]
|
||||
SPEACHES_LLM_MODELS = ["llama3", "mistral", "phi3", "qwen2", "gemma2", "deepseek-r1"]
|
||||
|
||||
|
||||
@register_llm
|
||||
class SelfHostedLLMConfiguration(BaseLLMConfiguration):
|
||||
provider: Literal[ServiceProviders.SELF_HOSTED] = ServiceProviders.SELF_HOSTED
|
||||
class SpeachesLLMConfiguration(BaseLLMConfiguration):
|
||||
provider: Literal[ServiceProviders.SPEACHES] = ServiceProviders.SPEACHES
|
||||
model: str = Field(
|
||||
default="llama3", json_schema_extra={"examples": SELF_HOSTED_LLM_MODELS}
|
||||
default="llama3",
|
||||
json_schema_extra={
|
||||
"examples": SPEACHES_LLM_MODELS,
|
||||
"allow_custom_input": True,
|
||||
},
|
||||
)
|
||||
base_url: str = Field(
|
||||
default="http://localhost:11434/v1",
|
||||
|
|
@ -267,6 +285,78 @@ class SelfHostedLLMConfiguration(BaseLLMConfiguration):
|
|||
api_key: str | list[str] | None = Field(default=None)
|
||||
|
||||
|
||||
OPENAI_REALTIME_MODELS = ["gpt-4o-realtime-preview", "gpt-4o-mini-realtime-preview"]
|
||||
OPENAI_REALTIME_VOICES = [
|
||||
"alloy",
|
||||
"ash",
|
||||
"ballad",
|
||||
"coral",
|
||||
"echo",
|
||||
"sage",
|
||||
"shimmer",
|
||||
"verse",
|
||||
]
|
||||
|
||||
|
||||
# @register_service(ServiceType.REALTIME)
|
||||
# class OpenAIRealtimeLLMConfiguration(BaseLLMConfiguration):
|
||||
# provider: Literal[ServiceProviders.OPENAI_REALTIME] = (
|
||||
# ServiceProviders.OPENAI_REALTIME
|
||||
# )
|
||||
# model: str = Field(
|
||||
# default="gpt-4o-realtime-preview",
|
||||
# json_schema_extra={
|
||||
# "examples": OPENAI_REALTIME_MODELS,
|
||||
# "allow_custom_input": True,
|
||||
# },
|
||||
# )
|
||||
# voice: str = Field(
|
||||
# default="alloy",
|
||||
# json_schema_extra={"examples": OPENAI_REALTIME_VOICES},
|
||||
# )
|
||||
|
||||
|
||||
GOOGLE_REALTIME_MODELS = ["gemini-3.1-flash-live-preview"]
|
||||
GOOGLE_REALTIME_VOICES = ["Puck", "Charon", "Kore", "Fenrir", "Aoede"]
|
||||
GOOGLE_REALTIME_LANGUAGES = [
|
||||
"en"
|
||||
]
|
||||
|
||||
|
||||
@register_service(ServiceType.REALTIME)
|
||||
class GoogleRealtimeLLMConfiguration(BaseLLMConfiguration):
|
||||
provider: Literal[ServiceProviders.GOOGLE_REALTIME] = (
|
||||
ServiceProviders.GOOGLE_REALTIME
|
||||
)
|
||||
model: str = Field(
|
||||
default="gemini-3.1-flash-live-preview",
|
||||
json_schema_extra={
|
||||
"examples": GOOGLE_REALTIME_MODELS,
|
||||
"allow_custom_input": True,
|
||||
},
|
||||
)
|
||||
voice: str = Field(
|
||||
default="Puck",
|
||||
json_schema_extra={
|
||||
"examples": GOOGLE_REALTIME_VOICES,
|
||||
"allow_custom_input": True,
|
||||
},
|
||||
)
|
||||
language: str = Field(
|
||||
default="en",
|
||||
json_schema_extra={
|
||||
"examples": GOOGLE_REALTIME_LANGUAGES,
|
||||
"allow_custom_input": True,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
REALTIME_PROVIDERS = {
|
||||
ServiceProviders.OPENAI_REALTIME.value,
|
||||
ServiceProviders.GOOGLE_REALTIME.value,
|
||||
}
|
||||
|
||||
|
||||
LLMConfig = Annotated[
|
||||
Union[
|
||||
OpenAILLMService,
|
||||
|
|
@ -276,7 +366,15 @@ LLMConfig = Annotated[
|
|||
AzureLLMService,
|
||||
DograhLLMService,
|
||||
AWSBedrockLLMConfiguration,
|
||||
SelfHostedLLMConfiguration,
|
||||
SpeachesLLMConfiguration,
|
||||
],
|
||||
Field(discriminator="provider"),
|
||||
]
|
||||
|
||||
RealtimeConfig = Annotated[
|
||||
Union[
|
||||
# OpenAIRealtimeLLMConfiguration,
|
||||
GoogleRealtimeLLMConfiguration,
|
||||
],
|
||||
Field(discriminator="provider"),
|
||||
]
|
||||
|
|
@ -462,6 +560,34 @@ class CambTTSConfiguration(BaseTTSConfiguration):
|
|||
language: str = Field(default="en-us", description="BCP-47 language code")
|
||||
|
||||
|
||||
SPEACHES_TTS_MODELS = ["hexgrad/Kokoro-82M"]
|
||||
|
||||
|
||||
@register_tts
|
||||
class SpeachesTTSConfiguration(BaseTTSConfiguration):
|
||||
provider: Literal[ServiceProviders.SPEACHES] = ServiceProviders.SPEACHES
|
||||
model: str = Field(
|
||||
default="kokoro",
|
||||
json_schema_extra={
|
||||
"examples": SPEACHES_TTS_MODELS,
|
||||
"allow_custom_input": True,
|
||||
},
|
||||
)
|
||||
voice: str = Field(
|
||||
default="af_heart",
|
||||
json_schema_extra={"allow_custom_input": True},
|
||||
description="Voice ID for the TTS engine",
|
||||
)
|
||||
base_url: str = Field(
|
||||
default="http://localhost:8000/v1",
|
||||
description="OpenAI-compatible TTS endpoint (Kokoro-FastAPI, etc.)",
|
||||
)
|
||||
speed: float = Field(
|
||||
default=1.0, ge=0.25, le=4.0, description="Speech speed (0.25 to 4.0)"
|
||||
)
|
||||
api_key: str | list[str] | None = Field(default=None)
|
||||
|
||||
|
||||
TTSConfig = Annotated[
|
||||
Union[
|
||||
DeepgramTTSConfiguration,
|
||||
|
|
@ -471,6 +597,7 @@ TTSConfig = Annotated[
|
|||
DograhTTSService,
|
||||
SarvamTTSConfiguration,
|
||||
CambTTSConfiguration,
|
||||
SpeachesTTSConfiguration,
|
||||
],
|
||||
Field(discriminator="provider"),
|
||||
]
|
||||
|
|
@ -674,6 +801,37 @@ class SpeechmaticsSTTConfiguration(BaseSTTConfiguration):
|
|||
)
|
||||
|
||||
|
||||
SPEACHES_STT_MODELS = [
|
||||
"Systran/faster-distil-whisper-small.en",
|
||||
"Systran/faster-whisper-large-v3",
|
||||
]
|
||||
SPEACHES_STT_LANGUAGES = ["en", "ar", "nl", "fr", "de", "hi", "it", "pt", "es"]
|
||||
|
||||
|
||||
@register_stt
|
||||
class SpeachesSTTConfiguration(BaseSTTConfiguration):
|
||||
provider: Literal[ServiceProviders.SPEACHES] = ServiceProviders.SPEACHES
|
||||
model: str = Field(
|
||||
default="Systran/faster-distil-whisper-small.en",
|
||||
json_schema_extra={
|
||||
"examples": SPEACHES_STT_MODELS,
|
||||
"allow_custom_input": True,
|
||||
},
|
||||
)
|
||||
language: str = Field(
|
||||
default="en",
|
||||
json_schema_extra={
|
||||
"examples": SPEACHES_STT_LANGUAGES,
|
||||
"allow_custom_input": True,
|
||||
},
|
||||
)
|
||||
base_url: str = Field(
|
||||
default="http://localhost:8000/v1",
|
||||
description="OpenAI-compatible STT endpoint (Speaches, etc.)",
|
||||
)
|
||||
api_key: str | list[str] | None = Field(default=None)
|
||||
|
||||
|
||||
STTConfig = Annotated[
|
||||
Union[
|
||||
DeepgramSTTConfiguration,
|
||||
|
|
@ -682,6 +840,7 @@ STTConfig = Annotated[
|
|||
DograhSTTService,
|
||||
SpeechmaticsSTTConfiguration,
|
||||
SarvamSTTConfiguration,
|
||||
SpeachesSTTConfiguration,
|
||||
],
|
||||
Field(discriminator="provider"),
|
||||
]
|
||||
|
|
@ -720,6 +879,6 @@ EmbeddingsConfig = Annotated[
|
|||
]
|
||||
|
||||
ServiceConfig = Annotated[
|
||||
Union[LLMConfig, TTSConfig, STTConfig, EmbeddingsConfig],
|
||||
Union[LLMConfig, RealtimeConfig, TTSConfig, STTConfig, EmbeddingsConfig],
|
||||
Field(discriminator="provider"),
|
||||
]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue