Integracion de Proveedores
VOCALS utiliza un patron de abstraccion de proveedores que te permite agregar implementaciones personalizadas de STT, LLM y TTS sin modificar el pipeline principal. Esta guia recorre las interfaces base, los metodos requeridos y el proceso de registro.
Interfaces Base
Todos los proveedores heredan de BaseProvider, que maneja el almacenamiento de API keys y requiere un metodo validate(). Cada etapa del pipeline tiene su propia clase abstracta.
BaseProvider
# backend/app/providers/base.py
class BaseProvider(ABC):
def __init__(self, api_key: str, config: Optional[Dict[str, Any]] = None):
self.api_key = api_key
self.config = config or {}
@abstractmethod
async def validate(self) -> bool:
"""Validate the provider configuration and API key.
Returns True if valid, raises an exception with details otherwise."""
...
async def list_models(self) -> List[str]:
"""Return available model IDs. Override in subclasses.
Returns an empty list by default."""
return []
STTProvider
class STTProvider(BaseProvider):
@abstractmethod
async def transcribe(self, audio_stream: AsyncIterator[bytes]) -> AsyncIterator[str]:
"""Transcribe streaming audio to text.
Args:
audio_stream: Async iterator of PCM audio chunks (16kHz, 16-bit mono).
Yields:
Transcribed text fragments (partial or final results).
"""
...
LLMProvider
class LLMProvider(BaseProvider):
@abstractmethod
async def generate(
self,
messages: list,
system_prompt: Optional[str] = None,
) -> AsyncIterator[str]:
"""Generate a streaming response from the LLM.
Args:
messages: Conversation history as list of dicts with 'role' and 'content'.
system_prompt: Optional system prompt to prepend.
Yields:
Text tokens/chunks as they are generated.
"""
...
TTSProvider
class TTSProvider(BaseProvider):
@abstractmethod
async def synthesize(self, text: str) -> AsyncIterator[bytes]:
"""Synthesize text to streaming audio.
Args:
text: Text to convert to speech.
Yields:
Audio data chunks (PCM 16kHz, 16-bit mono).
"""
...
Implementacion de un Proveedor Personalizado
Aqui tienes un ejemplo completo de como agregar un proveedor TTS personalizado.
Paso 1: Crear la Clase del Proveedor
Crea un nuevo archivo en el subdirectorio correspondiente:
# backend/app/providers/tts/my_tts.py
from typing import Any, AsyncIterator, Dict, List, Optional
import httpx
from app.providers.base import TTSProvider
class MyTTSProvider(TTSProvider):
"""Custom TTS provider implementation."""
# Hardcoded model list (or fetch from API in list_models)
MODELS = ["model-standard", "model-hd"]
def __init__(self, api_key: str, config: Optional[Dict[str, Any]] = None):
super().__init__(api_key, config)
self.model = config.get("model", "model-standard")
self.voice = config.get("voice_id", "default")
self.base_url = "https://api.my-tts-service.com/v1"
async def validate(self) -> bool:
"""Test the API key by making a lightweight API call."""
async with httpx.AsyncClient() as client:
resp = await client.get(
f"{self.base_url}/voices",
headers={"Authorization": f"Bearer {self.api_key}"},
timeout=10.0,
)
if resp.status_code == 401:
raise ValueError("Invalid API key")
resp.raise_for_status()
return True
async def list_models(self) -> List[str]:
"""Return available models. Can query the API or return a static list."""
return self.MODELS
async def synthesize(self, text: str) -> AsyncIterator[bytes]:
"""Stream synthesized audio as PCM 16kHz 16-bit mono chunks."""
async with httpx.AsyncClient() as client:
async with client.stream(
"POST",
f"{self.base_url}/synthesize",
headers={"Authorization": f"Bearer {self.api_key}"},
json={
"text": text,
"model": self.model,
"voice": self.voice,
"output_format": "pcm_16000",
},
timeout=30.0,
) as resp:
resp.raise_for_status()
async for chunk in resp.aiter_bytes(chunk_size=4096):
yield chunk
Paso 2: Registrar el Proveedor
Agrega la importacion y la llamada de registro al registro de proveedores:
# backend/app/providers/registry.py
def _register_all() -> None:
# ... existing registrations ...
from app.providers.tts.my_tts import MyTTSProvider
register_provider("tts", "my_tts", MyTTSProvider)
La funcion register_provider recibe tres argumentos:
| Argumento | Tipo | Descripcion |
|---|---|---|
provider_type | string | stt, llm, o tts |
name | string | Identificador unico usado en la API (ej. my_tts) |
cls | Type[BaseProvider] | La clase del proveedor |
Paso 3: Usar el Proveedor
Despues del registro, el proveedor esta disponible inmediatamente a traves de la API:
# Crear una configuracion de proveedor
curl -X POST \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{
"type": "tts",
"name": "my_tts",
"api_key": "your-api-key-here",
"model_id": "model-hd",
"extra_config": { "voice_id": "custom-voice" }
}' \
https://api.usevocals.com/api/v1/providers
# Probar el proveedor
curl -X POST \
-H "Authorization: Bearer $JWT" \
https://api.usevocals.com/api/v1/providers/{provider_id}/test
# Listar modelos disponibles
curl -H "Authorization: Bearer $JWT" \
https://api.usevocals.com/api/v1/providers/{provider_id}/models
Luego asigna el proveedor a un agente:
curl -X PUT \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{"active_tts_provider_id": "provider-uuid-here"}' \
https://api.usevocals.com/api/v1/agents/{agent_id}
Registro de Proveedores
El registro (backend/app/providers/registry.py) mantiene tres diccionarios -- uno por tipo de proveedor -- que mapean nombres a clases:
_stt_providers: Dict[str, Type[STTProvider]] = {}
_llm_providers: Dict[str, Type[LLMProvider]] = {}
_tts_providers: Dict[str, Type[TTSProvider]] = {}
Funciones principales:
| Funcion | Descripcion |
|---|---|
register_provider(type, name, cls) | Registrar una clase de proveedor |
get_provider(type, name, api_key, config) | Instanciar un proveedor registrado |
list_providers(type) | Listar nombres de proveedores registrados para un tipo |
La funcion _register_all() se ejecuta en tiempo de importacion y registra todos los proveedores incluidos.
Proveedores Registrados Actualmente
Proveedores STT
| Nombre | Clase | Descripcion |
|---|---|---|
deepgram | DeepgramSTTProvider | STT en tiempo real via Deepgram Nova |
openai | OpenAISTTProvider | API de OpenAI Whisper |
whisper | WhisperSTTProvider | OpenAI Whisper (local) |
elevenlabs | ElevenLabsSTTProvider | ElevenLabs STT |
qwen | QwenSTTProvider | Alibaba Qwen STT |
fish | FishSTTProvider | Fish Audio STT |
Proveedores LLM
| Nombre | Clase | Descripcion |
|---|---|---|
openai | OpenAILLMProvider | GPT-4o, GPT-4, GPT-3.5 |
claude | ClaudeLLMProvider | Modelos Anthropic Claude |
google | GoogleLLMProvider | Modelos Google Gemini |
kimi | KimiLLMProvider | Modelos Moonshot Kimi |
Proveedores TTS
| Nombre | Clase | Descripcion |
|---|---|---|
deepgram | DeepgramTTSProvider | Deepgram Aura TTS |
openai | OpenAITTSProvider | OpenAI TTS |
elevenlabs | ElevenLabsTTSProvider | ElevenLabs TTS |
qwen | QwenTTSProvider | Alibaba Qwen TTS |
resemble | ResembleTTSProvider | Resemble AI TTS |
fish | FishTTSProvider | Fish Audio TTS |
inworld | InworldTTSProvider | Inworld TTS |
cartesia | CartesiaTTSProvider | Cartesia Sonic TTS |
google | GoogleCloudTTSProvider | Google Cloud TTS (Neural2/WaveNet, Chirp3-HD, Studio). Lista el catálogo de voces en vivo de un tenant mediante GET /providers/{id}/google-voices. Las voces Gemini no están disponibles aquí: en la superficie de Cloud Text-to-Speech se autorizan mediante el modelo workspace/IAM de Google y no con una simple API key. |
google_gemini | GoogleGeminiTTSProvider | Google Gemini TTS (Gemini API). 30 voces preconstruidas, sin parámetro de idioma, instrucción de estilo opcional dentro del prompt. Distinto de google - ver más abajo. |
Las dos superficies TTS de Google
Google publica texto a voz en dos APIs distintas, y VOCALS las registra como dos proveedores independientes en lugar de niveles de uno solo. No comparten nada salvo el proveedor comercial:
google | google_gemini | |
|---|---|---|
| Clase | GoogleCloudTTSProvider | GoogleGeminiTTSProvider |
| API | Cloud Text-to-Speech | Gemini API |
| Endpoint | texttospeech.googleapis.com/v1/text:synthesize | generativelanguage.googleapis.com/v1beta/models/{model}:generateContent |
| Credencial | API key de Cloud Text-to-Speech | API key de la Gemini API (AI Studio, o un proyecto con la Generative Language API habilitada) |
| Facturación | Cloud Text-to-Speech | Gemini API, facturada por separado |
| Id de voz | completo, p. ej. en-US-Neural2-C | nombre preconstruido simple, p. ej. Zephyr |
| Idioma | locale por voz | ninguno - sigue al texto de entrada |
| Instrucción de estilo | no soportada | tts_config.google_style_prompt, máx. 500 caracteres |
| Audio de salida | LINEAR16 24 kHz, cabecera RIFF eliminada por el proveedor | PCM en bruto 24 kHz, sin cabecera |
Una clave emitida para una superficie no funciona en la otra, y una restricción de API key en la Consola de Google Cloud no puede cubrir ambas. Cada una se configura de forma independiente con su propia clave; no hay compartición ni enlazado automático de credenciales entre ellas.
GoogleGeminiTTSProvider envía la API key en la cabecera x-goog-api-key en lugar de en un parámetro de consulta ?key= - httpx registra las URLs de las peticiones y raise_for_status() incrusta la URL en su mensaje de excepción, por lo que una clave en la cadena de consulta se filtra a los logs en texto plano. Sigue el patrón de cabecera en cualquier proveedor nuevo cuyo servicio lo acepte.
Ambos proveedores TTS de Google ejecutan una síntesis de prueba real de la voz seleccionada al guardar un agente (ver Agentes), de modo que una clave con el alcance equivocado o una voz inalcanzable se detecta antes de que pueda fallar en una llamada en vivo.
Requisitos de Formato de Audio
Todos los proveedores STT reciben audio como un AsyncIterator[bytes] de fragmentos PCM 16kHz, 16-bit mono. El orquestador se encarga de la conversion desde el formato mulaw/8kHz de Twilio antes de pasar el audio al proveedor STT.
Todos los proveedores TTS deben producir audio como fragmentos PCM 16kHz, 16-bit mono. El orquestador convierte la salida de vuelta a mulaw/8kHz para Twilio.
Si la API de tu proveedor utiliza un formato diferente, realiza la conversion dentro de tu implementacion del proveedor.
Buenas Practicas
- Streaming: Usa APIs de streaming cuando esten disponibles. El pipeline procesa audio en tiempo real, por lo que las APIs por lotes agregan una latencia significativa.
- Manejo de timeouts: Establece timeouts razonables en las llamadas HTTP. El orquestador reintentara o degradara elegantemente ante fallos del proveedor.
- Mensajes de error: Lanza
ValueErrordesdevalidate()con un mensaje claro (ej. "Invalid API key", "Model not found"). Estos mensajes se devuelven al usuario a traves de la API. - Listado de modelos: Implementa
list_models()para consultar la API del proveedor cuando sea posible. Recurre a una lista predefinida si la API no soporta la enumeracion de modelos. Devuelve una lista vacia solo como ultimo recurso. Si el proveedor pagina su catalogo, sigue el token de pagina hasta el final: el resultado se cruza con la lista curada, asi que cualquier modelo recomendado que quede mas alla de la primera pagina desapareceria del desplegable sin aviso. - Acceso a configuracion: Usa
self.configpara leer configuraciones especificas del proveedor pasadas a traves deextra_configal crear o actualizar un proveedor mediante la API.