dograh/api/tests/test_elevenlabs_stt_service_factory.py
Muhammad Qasim cfe1d3709a
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>
2026-07-13 14:47:07 +05:30

198 lines
6.3 KiB
Python

from types import SimpleNamespace
from unittest.mock import patch
import pytest
from pipecat.services.elevenlabs.stt import CommitStrategy
from pipecat.transcriptions.language import Language
from api.services.configuration.options import (
ELEVENLABS_STT_LANGUAGES,
ELEVENLABS_STT_MODELS,
)
from api.services.configuration.registry import (
ElevenlabsSTTConfiguration,
ServiceProviders,
)
from api.services.pipecat.audio_config import AudioConfig
from api.services.pipecat.service_factory import (
create_stt_service,
create_tts_service,
stt_uses_external_turns,
)
def _audio_config() -> AudioConfig:
return AudioConfig(
transport_in_sample_rate=16000,
transport_out_sample_rate=16000,
)
def _elevenlabs_config(
language: str = "en",
base_url: str = "https://api.elevenlabs.io",
) -> SimpleNamespace:
return SimpleNamespace(
stt=SimpleNamespace(
provider=ServiceProviders.ELEVENLABS.value,
api_key="test-key",
model="scribe_v2_realtime",
language=language,
base_url=base_url,
)
)
def _elevenlabs_tts_config(base_url: str) -> SimpleNamespace:
return SimpleNamespace(
tts=SimpleNamespace(
provider=ServiceProviders.ELEVENLABS.value,
api_key="test-key",
model="eleven_flash_v2_5",
voice="test-voice",
speed=1.0,
base_url=base_url,
)
)
def test_elevenlabs_stt_configuration_exposes_defaults_and_languages():
config = ElevenlabsSTTConfiguration(api_key="test-key")
language_schema = ElevenlabsSTTConfiguration.model_json_schema()["properties"][
"language"
]
assert config.provider == ServiceProviders.ELEVENLABS
assert config.model == "scribe_v2_realtime"
assert config.language == "en"
assert config.base_url == "https://api.elevenlabs.io"
assert ELEVENLABS_STT_MODELS == ("scribe_v2_realtime",)
assert "auto" in ELEVENLABS_STT_LANGUAGES
assert "es" in ELEVENLABS_STT_LANGUAGES
assert language_schema["examples"] == list(ELEVENLABS_STT_LANGUAGES)
def test_elevenlabs_stt_uses_realtime_service_with_language_mapping():
user_config = _elevenlabs_config(language="es")
assert not stt_uses_external_turns(user_config)
with patch(
"api.services.pipecat.service_factory.ElevenLabsRealtimeSTTService"
) as stt_service:
create_stt_service(user_config, _audio_config())
stt_service.assert_called_once()
kwargs = stt_service.call_args.kwargs
assert kwargs["api_key"] == "test-key"
assert kwargs["base_url"] == "api.elevenlabs.io"
assert kwargs["commit_strategy"] == CommitStrategy.VAD
assert kwargs["sample_rate"] == 16000
assert kwargs["should_interrupt"] is False
assert kwargs["settings"].model == "scribe_v2_realtime"
assert kwargs["settings"].language == Language.ES
def test_elevenlabs_stt_auto_language_passes_none():
user_config = _elevenlabs_config(language="auto")
with patch(
"api.services.pipecat.service_factory.ElevenLabsRealtimeSTTService"
) as stt_service:
create_stt_service(user_config, _audio_config())
kwargs = stt_service.call_args.kwargs
assert kwargs["settings"].language is None
def test_elevenlabs_stt_extracts_hostname_from_residency_base_url():
user_config = _elevenlabs_config(base_url="https://api.eu.residency.elevenlabs.io")
with patch(
"api.services.pipecat.service_factory.ElevenLabsRealtimeSTTService"
) as stt_service:
create_stt_service(user_config, _audio_config())
kwargs = stt_service.call_args.kwargs
assert kwargs["base_url"] == "api.eu.residency.elevenlabs.io"
def test_elevenlabs_stt_custom_language_passes_through():
user_config = _elevenlabs_config(language="custom-lang")
with patch(
"api.services.pipecat.service_factory.ElevenLabsRealtimeSTTService"
) as stt_service:
create_stt_service(user_config, _audio_config())
kwargs = stt_service.call_args.kwargs
assert kwargs["settings"].language == "custom-lang"
def test_elevenlabs_stt_bare_hostname_base_url_is_preserved():
user_config = _elevenlabs_config(base_url="api.elevenlabs.io")
with patch(
"api.services.pipecat.service_factory.ElevenLabsRealtimeSTTService"
) as stt_service:
create_stt_service(user_config, _audio_config())
kwargs = stt_service.call_args.kwargs
assert kwargs["base_url"] == "api.elevenlabs.io"
def test_elevenlabs_stt_preserves_non_default_port_in_base_url():
user_config = _elevenlabs_config(base_url="https://localhost:8443")
with patch(
"api.services.pipecat.service_factory.ElevenLabsRealtimeSTTService"
) as stt_service:
create_stt_service(user_config, _audio_config())
kwargs = stt_service.call_args.kwargs
assert kwargs["base_url"] == "localhost:8443"
def test_elevenlabs_stt_preserves_proxy_path_prefix_in_base_url():
user_config = _elevenlabs_config(base_url="https://proxy.example.com/elevenlabs")
with patch(
"api.services.pipecat.service_factory.ElevenLabsRealtimeSTTService"
) as stt_service:
create_stt_service(user_config, _audio_config())
kwargs = stt_service.call_args.kwargs
assert kwargs["base_url"] == "proxy.example.com/elevenlabs"
@pytest.mark.parametrize(
("base_url", "expected_url"),
[
(
"https://api.eu.residency.elevenlabs.io/elevenlabs/",
"wss://api.eu.residency.elevenlabs.io/elevenlabs",
),
("http://localhost:8000/", "ws://localhost:8000"),
],
)
def test_elevenlabs_tts_uses_normalized_websocket_url(base_url, expected_url):
user_config = _elevenlabs_tts_config(base_url)
with patch(
"api.services.pipecat.service_factory.ElevenLabsTTSService"
) as tts_service:
create_tts_service(user_config, _audio_config())
assert tts_service.call_args.kwargs["url"] == expected_url
def test_elevenlabs_stt_listed_custom_language_maps_to_pipecat_enum():
user_config = _elevenlabs_config(language="yue")
with patch(
"api.services.pipecat.service_factory.ElevenLabsRealtimeSTTService"
) as stt_service:
create_stt_service(user_config, _audio_config())
kwargs = stt_service.call_args.kwargs
assert kwargs["settings"].language == Language.YUE