Saltar al contenido principal

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:

ArgumentoTipoDescripcion
provider_typestringstt, llm, o tts
namestringIdentificador unico usado en la API (ej. my_tts)
clsType[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:

FuncionDescripcion
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

NombreClaseDescripcion
deepgramDeepgramSTTProviderSTT en tiempo real via Deepgram Nova
openaiOpenAISTTProviderAPI de OpenAI Whisper
whisperWhisperSTTProviderOpenAI Whisper (local)
elevenlabsElevenLabsSTTProviderElevenLabs STT
qwenQwenSTTProviderAlibaba Qwen STT
fishFishSTTProviderFish Audio STT

Proveedores LLM

NombreClaseDescripcion
openaiOpenAILLMProviderGPT-4o, GPT-4, GPT-3.5
claudeClaudeLLMProviderModelos Anthropic Claude
googleGoogleLLMProviderModelos Google Gemini
kimiKimiLLMProviderModelos Moonshot Kimi

Proveedores TTS

NombreClaseDescripcion
deepgramDeepgramTTSProviderDeepgram Aura TTS
openaiOpenAITTSProviderOpenAI TTS
elevenlabsElevenLabsTTSProviderElevenLabs TTS
qwenQwenTTSProviderAlibaba Qwen TTS
resembleResembleTTSProviderResemble AI TTS
fishFishTTSProviderFish Audio TTS
inworldInworldTTSProviderInworld TTS
cartesiaCartesiaTTSProviderCartesia Sonic TTS
googleGoogleCloudTTSProviderGoogle 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_geminiGoogleGeminiTTSProviderGoogle 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:

googlegoogle_gemini
ClaseGoogleCloudTTSProviderGoogleGeminiTTSProvider
APICloud Text-to-SpeechGemini API
Endpointtexttospeech.googleapis.com/v1/text:synthesizegenerativelanguage.googleapis.com/v1beta/models/{model}:generateContent
CredencialAPI key de Cloud Text-to-SpeechAPI key de la Gemini API (AI Studio, o un proyecto con la Generative Language API habilitada)
FacturaciónCloud Text-to-SpeechGemini API, facturada por separado
Id de vozcompleto, p. ej. en-US-Neural2-Cnombre preconstruido simple, p. ej. Zephyr
Idiomalocale por vozninguno - sigue al texto de entrada
Instrucción de estilono soportadatts_config.google_style_prompt, máx. 500 caracteres
Audio de salidaLINEAR16 24 kHz, cabecera RIFF eliminada por el proveedorPCM 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 ValueError desde validate() 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.config para leer configuraciones especificas del proveedor pasadas a traves de extra_config al crear o actualizar un proveedor mediante la API.