feat: add ElevenLabs realtime STT provider support (#512) (#522)

* feat: add ElevenLabs realtime STT provider support (#512)

Wire ElevenLabs scribe_v2_realtime into the STT registry and pipeline factory so BYOK transcribers can use the same provider already supported for TTS.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: address ElevenLabs STT review feedback for language, commits, and host

Pass custom language codes through instead of defaulting to English, use ElevenLabs VAD commit strategy because Dograh VAD runs downstream of STT, and document hostname-only realtime base_url handling.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: preserve ElevenLabs STT endpoint port in realtime host parsing

Use urlparse netloc instead of hostname so validated BYOK/proxy base URLs keep non-default ports when Pipecat builds the websocket endpoint.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: preserve ElevenLabs STT proxy path prefix and remove duplicate tests

Include URL path segments in realtime host normalization for BYOK proxies and delete shadowed pytest definitions.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: allow custom ElevenLabs model input

* fix: normalize ElevenLabs websocket URLs

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Abhishek Kumar <abhishek@a6k.me>
This commit is contained in:
Muhammad Qasim 2026-07-13 14:17:07 +05:00 committed by GitHub
parent c76076fb93
commit cfe1d3709a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 447 additions and 11 deletions

View file

@ -22,6 +22,7 @@ from .deepgram import (
DEEPGRAM_LANGUAGES,
DEEPGRAM_STT_MODELS,
)
from .elevenlabs import ELEVENLABS_STT_LANGUAGES, ELEVENLABS_STT_MODELS
from .gladia import GLADIA_STT_LANGUAGES, GLADIA_STT_MODELS
from .google import (
GOOGLE_MODELS,
@ -69,6 +70,8 @@ __all__ = [
"CARTESIA_INK_WHISPER_STT_LANGUAGES",
"CARTESIA_STT_LANGUAGES",
"CARTESIA_STT_MODELS",
"ELEVENLABS_STT_LANGUAGES",
"ELEVENLABS_STT_MODELS",
"DEEPGRAM_FLUX_MODELS",
"DEEPGRAM_FLUX_MULTILINGUAL_LANGUAGES",
"DEEPGRAM_FLUX_MULTILINGUAL_LANGUAGE_OPTIONS",

View file

@ -0,0 +1,95 @@
ELEVENLABS_STT_MODELS = ("scribe_v2_realtime",)
ELEVENLABS_STT_LANGUAGES = (
"auto",
"af",
"am",
"ar",
"as",
"az",
"be",
"bg",
"bn",
"bs",
"ca",
"ceb",
"cs",
"cy",
"da",
"de",
"el",
"en",
"es",
"et",
"fa",
"fi",
"fil",
"fr",
"ga",
"gl",
"gu",
"ha",
"he",
"hi",
"hr",
"hu",
"hy",
"id",
"ig",
"is",
"it",
"ja",
"jv",
"ka",
"kk",
"km",
"kn",
"ko",
"ku",
"ky",
"lo",
"lt",
"lv",
"mi",
"mk",
"ml",
"mn",
"mr",
"ms",
"mt",
"my",
"ne",
"nl",
"no",
"ny",
"oc",
"or",
"pa",
"pl",
"ps",
"pt",
"ro",
"ru",
"sd",
"sk",
"sl",
"sn",
"so",
"sr",
"sv",
"sw",
"ta",
"te",
"tg",
"th",
"tr",
"uk",
"ur",
"uz",
"vi",
"wo",
"xh",
"yue",
"zh",
"zu",
)

View file

@ -22,6 +22,8 @@ from api.services.configuration.options import (
DEEPGRAM_FLUX_MULTILINGUAL_LANGUAGES,
DEEPGRAM_LANGUAGES,
DEEPGRAM_STT_MODELS,
ELEVENLABS_STT_LANGUAGES,
ELEVENLABS_STT_MODELS,
GLADIA_STT_LANGUAGES,
GLADIA_STT_MODELS,
GOOGLE_MODELS,
@ -862,7 +864,10 @@ class ElevenlabsTTSConfiguration(BaseServiceConfiguration):
model: str = Field(
default="eleven_flash_v2_5",
description="ElevenLabs TTS model.",
json_schema_extra={"examples": ELEVENLABS_TTS_MODELS},
json_schema_extra={
"examples": ELEVENLABS_TTS_MODELS,
"allow_custom_input": True,
},
)
base_url: str = Field(
default="https://api.elevenlabs.io",
@ -1672,6 +1677,39 @@ SMALLEST_STT_LANGUAGES = [
]
@register_stt
class ElevenlabsSTTConfiguration(BaseSTTConfiguration):
model_config = ELEVENLABS_PROVIDER_MODEL_CONFIG
provider: Literal[ServiceProviders.ELEVENLABS] = ServiceProviders.ELEVENLABS
model: str = Field(
default="scribe_v2_realtime",
description="ElevenLabs realtime STT model.",
json_schema_extra={
"examples": ELEVENLABS_STT_MODELS,
"allow_custom_input": True,
},
)
language: str = Field(
default="en",
description=(
"ISO 639-1 language code for transcription. "
"Use 'auto' to let ElevenLabs detect the language."
),
json_schema_extra={
"examples": ELEVENLABS_STT_LANGUAGES,
"allow_custom_input": True,
},
)
base_url: str = Field(
default="https://api.elevenlabs.io",
description=(
"ElevenLabs API base URL. Override to use a Data Residency endpoint "
"(e.g. https://api.eu.residency.elevenlabs.io) for GDPR / HIPAA / "
"regional compliance."
),
)
@register_stt
class SmallestAISTTConfiguration(BaseSTTConfiguration):
model_config = SMALLEST_PROVIDER_MODEL_CONFIG
@ -1706,6 +1744,7 @@ STTConfig = Annotated[
GladiaSTTConfiguration,
AzureSpeechSTTConfiguration,
SmallestAISTTConfiguration,
ElevenlabsSTTConfiguration,
],
Field(discriminator="provider"),
]

View file

@ -38,6 +38,11 @@ from pipecat.services.dograh.flux.stt import DograhFluxSTTService
from pipecat.services.dograh.llm import DograhLLMService
from pipecat.services.dograh.stt import DograhSTTService, DograhSTTSettings
from pipecat.services.dograh.tts import DograhTTSService, DograhTTSSettings
from pipecat.services.elevenlabs.stt import (
CommitStrategy,
ElevenLabsRealtimeSTTService,
ElevenLabsRealtimeSTTSettings,
)
from pipecat.services.elevenlabs.tts import ElevenLabsTTSService, ElevenLabsTTSSettings
from pipecat.services.gladia.stt import GladiaSTTService, GladiaSTTSettings
from pipecat.services.google.llm import GoogleLLMService, GoogleLLMSettings
@ -108,6 +113,52 @@ def dograh_stt_uses_flux_language(language: str | None) -> bool:
return language in DEEPGRAM_FLUX_MULTILINGUAL_LANGUAGE_OPTIONS
def _resolve_elevenlabs_stt_language(
language_code: str | None,
) -> Language | str | None:
if not language_code or language_code == "auto":
return None
try:
return Language(language_code)
except ValueError:
return language_code
def _elevenlabs_websocket_url(base_url: str) -> str:
"""Normalize an ElevenLabs API base URL for WebSocket clients."""
base_url = base_url.strip()
parsed = urlparse(base_url)
if not parsed.netloc:
return base_url.rstrip("/")
websocket_scheme = {
"http": "ws",
"https": "wss",
}.get(parsed.scheme, parsed.scheme)
return urlunparse(
parsed._replace(
scheme=websocket_scheme,
path=parsed.path.rstrip("/"),
)
)
def _elevenlabs_realtime_stt_host(base_url: str) -> str:
"""Return the host/path prefix Pipecat's ElevenLabs realtime STT expects.
Pipecat's realtime STT service builds
``wss://{host}/v1/speech-to-text/realtime`` internally, so remove the scheme
from the same normalized WebSocket URL used by ElevenLabs TTS. Preserve
netloc (including optional ports) and any path prefix used by BYOK proxies.
"""
websocket_url = _elevenlabs_websocket_url(base_url)
parsed = urlparse(websocket_url)
if parsed.netloc:
path = parsed.path
return f"{parsed.netloc}{path}" if path else parsed.netloc
return websocket_url
def stt_uses_external_turns(user_config) -> bool:
if user_config.stt.provider == ServiceProviders.DEEPGRAM.value:
return user_config.stt.model in DEEPGRAM_FLUX_MODELS
@ -415,6 +466,24 @@ def create_stt_service(
),
sample_rate=audio_config.transport_in_sample_rate,
)
elif user_config.stt.provider == ServiceProviders.ELEVENLABS.value:
language_code = getattr(user_config.stt, "language", None)
pipecat_language = _resolve_elevenlabs_stt_language(language_code)
_validate_runtime_service_url(user_config.stt.base_url, "base_url")
elevenlabs_host = _elevenlabs_realtime_stt_host(user_config.stt.base_url)
return ElevenLabsRealtimeSTTService(
api_key=user_config.stt.api_key,
base_url=elevenlabs_host,
commit_strategy=CommitStrategy.VAD,
settings=ElevenLabsRealtimeSTTSettings(
model=user_config.stt.model,
language=pipecat_language,
),
should_interrupt=False,
sample_rate=audio_config.transport_in_sample_rate,
)
else:
raise HTTPException(
status_code=400, detail=f"Invalid STT provider {user_config.stt.provider}"
@ -488,13 +557,11 @@ def create_tts_service(
voice_id = user_config.tts.voice.split(" - ")[1]
except IndexError:
voice_id = user_config.tts.voice
# ElevenLabs TTS uses WebSocket. Users configure base_url with an HTTP
# scheme (matching ElevenLabs documentation, e.g.
# https://api.eu.residency.elevenlabs.io); rewrite it to the WS scheme.
# ElevenLabs TTS consumes the full normalized WebSocket URL. Realtime
# STT uses the same normalization before adapting it to Pipecat's
# scheme-less base_url contract.
_validate_runtime_service_url(user_config.tts.base_url, "base_url")
elevenlabs_url = user_config.tts.base_url.replace("https://", "wss://").replace(
"http://", "ws://"
)
elevenlabs_url = _elevenlabs_websocket_url(user_config.tts.base_url)
return ElevenLabsTTSService(
reconnect_on_error=False,
api_key=user_config.tts.api_key,