Merge remote-tracking branch 'origin/main' into feat/provisional-vad

# Conflicts:
#	api/routes/main.py
#	api/tests/test_active_calls.py
#	pipecat
#	scripts/rolling_update.sh
This commit is contained in:
Abhishek Kumar 2026-06-29 13:43:57 +05:30
commit cc658131aa
28 changed files with 1101 additions and 31 deletions

View file

@ -12,6 +12,11 @@ UI_APP_URL="http://localhost:3000"
DATABASE_URL="postgresql+asyncpg://postgres:postgres@localhost:5432/postgres"
REDIS_URL="redis://:redissecret@localhost:6379"
# Internal devops secret for deployment scripts and lifecycle hooks.
# scripts/rolling_update.sh sends this to protected operational endpoints via
# X-Dograh-Devops-Secret. Use a unique random value in production.
DOGRAH_DEVOPS_SECRET="change-me-dograh-devops-secret"
# AWS S3 Configuration
ENABLE_AWS_S3="false"
# AWS_ACCESS_KEY_ID=""

View file

@ -14,4 +14,6 @@ UI_APP_URL=http://localhost:3000
DATABASE_URL="postgresql+asyncpg://postgres:postgres@localhost:5432/test_db"
REDIS_URL="redis://:redissecret@localhost:6379/0"
DOGRAH_DEVOPS_SECRET="test-dograh-devops-secret"
MINIO_PUBLIC_ENDPOINT=http://localhost:9000

View file

@ -54,6 +54,7 @@ STACK_AUTH_PROJECT_ID = os.getenv("STACK_AUTH_PROJECT_ID")
STACK_PUBLISHABLE_CLIENT_KEY = os.getenv("STACK_PUBLISHABLE_CLIENT_KEY")
DOGRAH_MPS_SECRET_KEY = os.getenv("DOGRAH_MPS_SECRET_KEY", None)
MPS_API_URL = os.getenv("MPS_API_URL", "https://services.dograh.com")
DOGRAH_DEVOPS_SECRET = os.getenv("DOGRAH_DEVOPS_SECRET") or None
# Storage Configuration
ENABLE_AWS_S3 = os.getenv("ENABLE_AWS_S3", "false").lower() == "true"

View file

@ -1,4 +1,7 @@
from fastapi import APIRouter
import secrets
from typing import Annotated
from fastapi import APIRouter, Header, HTTPException, status
from loguru import logger
from pydantic import BaseModel
@ -131,8 +134,35 @@ class ActiveCallsResponse(BaseModel):
active_calls: int
DOGRAH_DEVOPS_SECRET_HEADER = "X-Dograh-Devops-Secret"
def _verify_devops_secret(
configured_secret: str | None,
provided_secret: str | None,
) -> None:
if not configured_secret:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Devops secret is not configured",
)
if not provided_secret or not secrets.compare_digest(
provided_secret,
configured_secret,
):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Forbidden",
)
@router.get("/health/active-calls", response_model=ActiveCallsResponse)
async def active_calls() -> ActiveCallsResponse:
async def active_calls(
x_dograh_devops_secret: Annotated[
str | None,
Header(alias=DOGRAH_DEVOPS_SECRET_HEADER),
] = None,
) -> ActiveCallsResponse:
"""In-flight call count for THIS worker — the drain signal for deploys.
A deploy orchestrator polls this per worker and waits for zero before
@ -141,6 +171,8 @@ async def active_calls() -> ActiveCallsResponse:
count is per-process: one uvicorn per VM port (scripts/rolling_update.sh)
or per Kubernetes pod (preStop hook). See api/services/pipecat/active_calls.py.
"""
from api.constants import DOGRAH_DEVOPS_SECRET
from api.services.pipecat.active_calls import active_call_count
_verify_devops_secret(DOGRAH_DEVOPS_SECRET, x_dograh_devops_secret)
return ActiveCallsResponse(active_calls=active_call_count())

View file

@ -9,6 +9,12 @@ from .azure import (
AZURE_SPEECH_TTS_LANGUAGES,
AZURE_SPEECH_TTS_VOICES,
)
from .cartesia import (
CARTESIA_INK_2_STT_LANGUAGES,
CARTESIA_INK_WHISPER_STT_LANGUAGES,
CARTESIA_STT_LANGUAGES,
CARTESIA_STT_MODELS,
)
from .deepgram import (
DEEPGRAM_FLUX_MODELS,
DEEPGRAM_FLUX_MULTILINGUAL_LANGUAGE_OPTIONS,
@ -59,6 +65,10 @@ __all__ = [
"AZURE_SPEECH_STT_LANGUAGES",
"AZURE_SPEECH_TTS_LANGUAGES",
"AZURE_SPEECH_TTS_VOICES",
"CARTESIA_INK_2_STT_LANGUAGES",
"CARTESIA_INK_WHISPER_STT_LANGUAGES",
"CARTESIA_STT_LANGUAGES",
"CARTESIA_STT_MODELS",
"DEEPGRAM_FLUX_MODELS",
"DEEPGRAM_FLUX_MULTILINGUAL_LANGUAGES",
"DEEPGRAM_FLUX_MULTILINGUAL_LANGUAGE_OPTIONS",

View file

@ -0,0 +1,105 @@
CARTESIA_STT_MODELS = ["ink-2", "ink-whisper"]
CARTESIA_INK_2_STT_LANGUAGES = ("en",)
CARTESIA_INK_WHISPER_STT_LANGUAGES = (
"en",
"zh",
"de",
"es",
"ru",
"ko",
"fr",
"ja",
"pt",
"tr",
"pl",
"ca",
"nl",
"ar",
"sv",
"it",
"id",
"hi",
"fi",
"vi",
"he",
"uk",
"el",
"ms",
"cs",
"ro",
"da",
"hu",
"ta",
"no",
"th",
"ur",
"hr",
"bg",
"lt",
"la",
"mi",
"ml",
"cy",
"sk",
"te",
"fa",
"lv",
"bn",
"sr",
"az",
"sl",
"kn",
"et",
"mk",
"br",
"eu",
"is",
"hy",
"ne",
"mn",
"bs",
"kk",
"sq",
"sw",
"gl",
"mr",
"pa",
"si",
"km",
"sn",
"yo",
"so",
"af",
"oc",
"ka",
"be",
"tg",
"sd",
"gu",
"am",
"yi",
"lo",
"uz",
"fo",
"ht",
"ps",
"tk",
"nn",
"mt",
"sa",
"lb",
"my",
"bo",
"tl",
"mg",
"as",
"tt",
"haw",
"ln",
"ha",
"ba",
"jw",
"su",
"yue",
)
CARTESIA_STT_LANGUAGES = CARTESIA_INK_WHISPER_STT_LANGUAGES

View file

@ -14,6 +14,10 @@ from api.services.configuration.options import (
AZURE_SPEECH_STT_LANGUAGES,
AZURE_SPEECH_TTS_LANGUAGES,
AZURE_SPEECH_TTS_VOICES,
CARTESIA_INK_2_STT_LANGUAGES,
CARTESIA_INK_WHISPER_STT_LANGUAGES,
CARTESIA_STT_LANGUAGES,
CARTESIA_STT_MODELS,
DEEPGRAM_FLUX_MULTILINGUAL_LANGUAGE_OPTIONS,
DEEPGRAM_FLUX_MULTILINGUAL_LANGUAGES,
DEEPGRAM_LANGUAGES,
@ -1323,9 +1327,6 @@ class DeepgramSTTConfiguration(BaseSTTConfiguration):
)
CARTESIA_STT_MODELS = ["ink-whisper"]
@register_stt
class CartesiaSTTConfiguration(BaseSTTConfiguration):
model_config = CARTESIA_PROVIDER_MODEL_CONFIG
@ -1335,6 +1336,17 @@ class CartesiaSTTConfiguration(BaseSTTConfiguration):
description="Cartesia STT model.",
json_schema_extra={"examples": CARTESIA_STT_MODELS},
)
language: str = Field(
default="en",
description="ISO 639-1 language code. ink-2 currently supports English only.",
json_schema_extra={
"examples": CARTESIA_STT_LANGUAGES,
"model_options": {
"ink-2": CARTESIA_INK_2_STT_LANGUAGES,
"ink-whisper": CARTESIA_INK_WHISPER_STT_LANGUAGES,
},
},
)
OPENAI_STT_MODELS = ["gpt-4o-transcribe"]

View file

@ -50,7 +50,7 @@ from api.services.pipecat.service_factory import (
create_realtime_llm_service,
create_stt_service,
create_tts_service,
stt_uses_flux_turns,
stt_uses_external_turns,
)
from api.services.pipecat.tracing_config import (
ensure_tracing,
@ -97,6 +97,19 @@ from pipecat.utils.run_context import set_current_org_id, set_current_run_id
# Setup tracing if enabled
ensure_tracing()
DEFAULT_USER_TURN_STOP_TIMEOUT = 5.0
EXTERNAL_TURN_USER_STOP_TIMEOUT = 30.0
def _resolve_user_turn_stop_timeout(
run_configs: dict, *, uses_external_turns: bool
) -> float:
if "user_turn_stop_timeout" in run_configs:
return float(run_configs["user_turn_stop_timeout"])
if uses_external_turns:
return EXTERNAL_TURN_USER_STOP_TIMEOUT
return DEFAULT_USER_TURN_STOP_TIMEOUT
def _create_realtime_user_turn_config(provider: str):
"""Return user turn strategies and optional local VAD for realtime providers."""
@ -154,6 +167,34 @@ async def run_pipeline_telephony(
user_id: int,
call_id: str,
transport_kwargs: dict,
) -> None:
"""Run a pipeline for any telephony provider."""
# Register before any async setup so deploy drains see calls that are still
# resolving DB/config/transport state.
register_active_call(workflow_run_id)
try:
await _run_pipeline_telephony_impl(
websocket,
provider_name=provider_name,
workflow_id=workflow_id,
workflow_run_id=workflow_run_id,
user_id=user_id,
call_id=call_id,
transport_kwargs=transport_kwargs,
)
finally:
unregister_active_call(workflow_run_id)
async def _run_pipeline_telephony_impl(
websocket,
*,
provider_name: str,
workflow_id: int,
workflow_run_id: int,
user_id: int,
call_id: str,
transport_kwargs: dict,
) -> None:
"""Run a pipeline for any telephony provider.
@ -227,7 +268,7 @@ async def run_pipeline_telephony(
)
try:
await _run_pipeline(
await _run_pipeline_impl(
transport,
workflow_id,
workflow_run_id,
@ -251,6 +292,31 @@ async def run_pipeline_smallwebrtc(
user_id: int,
call_context_vars: dict = {},
user_provider_id: str | None = None,
) -> None:
"""Run pipeline for WebRTC connections."""
# Register before any async setup so deploy drains see calls that are still
# resolving DB/config/transport state.
register_active_call(workflow_run_id)
try:
await _run_pipeline_smallwebrtc_impl(
webrtc_connection,
workflow_id,
workflow_run_id,
user_id,
call_context_vars=call_context_vars,
user_provider_id=user_provider_id,
)
finally:
unregister_active_call(workflow_run_id)
async def _run_pipeline_smallwebrtc_impl(
webrtc_connection: SmallWebRTCConnection,
workflow_id: int,
workflow_run_id: int,
user_id: int,
call_context_vars: dict = {},
user_provider_id: str | None = None,
) -> None:
"""Run pipeline for WebRTC connections"""
logger.debug(
@ -300,7 +366,7 @@ async def run_pipeline_smallwebrtc(
ambient_noise_config,
is_realtime=is_realtime,
)
await _run_pipeline(
await _run_pipeline_impl(
transport,
workflow_id,
workflow_run_id,
@ -323,6 +389,35 @@ async def _run_pipeline(
user_provider_id: str | None = None,
workflow_run=None,
resolved_user_config=None,
) -> None:
"""Run the pipeline with active-call drain accounting."""
register_active_call(workflow_run_id)
try:
await _run_pipeline_impl(
transport,
workflow_id,
workflow_run_id,
user_id,
call_context_vars=call_context_vars,
audio_config=audio_config,
user_provider_id=user_provider_id,
workflow_run=workflow_run,
resolved_user_config=resolved_user_config,
)
finally:
unregister_active_call(workflow_run_id)
async def _run_pipeline_impl(
transport,
workflow_id: int,
workflow_run_id: int,
user_id: int,
call_context_vars: dict = {},
audio_config: AudioConfig = None,
user_provider_id: str | None = None,
workflow_run=None,
resolved_user_config=None,
) -> None:
"""
Run the pipeline with the given transport and configuration
@ -624,16 +719,18 @@ async def _run_pipeline(
# Configure turn strategies based on STT provider, model, and workflow configuration
if is_realtime:
uses_external_turns = False
# Realtime services still need user-turn tracking even when the model
# itself owns speech generation and interruption behavior.
user_turn_strategies, user_vad_analyzer = _create_realtime_user_turn_config(
user_config.realtime.provider
)
else:
# Deepgram Flux and supported Dograh managed Flux languages emit their
# own turn boundaries, so the aggregator follows those external signals.
# Other models use configurable turn detection.
if stt_uses_flux_turns(user_config):
# Some STT services emit their own turn boundaries, so the aggregator
# follows those external signals. Other models use configurable turn
# detection.
uses_external_turns = stt_uses_external_turns(user_config)
if uses_external_turns:
user_turn_strategies = UserTurnStrategies(
start=[
VADUserTurnStartStrategy(),
@ -663,9 +760,15 @@ async def _run_pipeline(
stop=[SpeechTimeoutUserTurnStopStrategy()],
)
user_turn_stop_timeout = _resolve_user_turn_stop_timeout(
run_configs,
uses_external_turns=uses_external_turns,
)
user_params = LLMUserAggregatorParams(
user_turn_strategies=user_turn_strategies,
user_mute_strategies=user_mute_strategies,
user_turn_stop_timeout=user_turn_stop_timeout,
user_idle_timeout=max_user_idle_timeout,
vad_analyzer=user_vad_analyzer,
)

View file

@ -21,12 +21,13 @@ from pipecat.services.aws.llm import AWSBedrockLLMService, AWSBedrockLLMSettings
from pipecat.services.azure.llm import AzureLLMService, AzureLLMSettings
from pipecat.services.azure.stt import AzureSTTService, AzureSTTSettings
from pipecat.services.azure.tts import AzureTTSService, AzureTTSSettings
from pipecat.services.cartesia.stt import CartesiaSTTService
from pipecat.services.cartesia.stt import CartesiaSTTService, CartesiaSTTSettings
from pipecat.services.cartesia.tts import (
CartesiaTTSService,
CartesiaTTSSettings,
GenerationConfig,
)
from pipecat.services.cartesia.turns.stt import CartesiaTurnsSTTService
from pipecat.services.deepgram.flux.stt import (
DeepgramFluxSTTService,
DeepgramFluxSTTSettings,
@ -106,11 +107,13 @@ def dograh_stt_uses_flux_language(language: str | None) -> bool:
return language in DEEPGRAM_FLUX_MULTILINGUAL_LANGUAGE_OPTIONS
def stt_uses_flux_turns(user_config) -> bool:
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
if user_config.stt.provider == ServiceProviders.DOGRAH.value:
return dograh_stt_uses_flux_language(getattr(user_config.stt, "language", None))
if user_config.stt.provider == ServiceProviders.CARTESIA.value:
return user_config.stt.model == "ink-2"
return False
@ -215,8 +218,20 @@ def create_stt_service(
sample_rate=audio_config.transport_in_sample_rate,
)
elif user_config.stt.provider == ServiceProviders.CARTESIA.value:
if user_config.stt.model == "ink-2":
return CartesiaTurnsSTTService(
api_key=user_config.stt.api_key,
should_interrupt=False, # Let UserAggregator emit interruption frames.
sample_rate=audio_config.transport_in_sample_rate,
)
language = getattr(user_config.stt, "language", None) or "en"
return CartesiaSTTService(
api_key=user_config.stt.api_key,
settings=CartesiaSTTSettings(
model=user_config.stt.model,
language=language,
),
sample_rate=audio_config.transport_in_sample_rate,
)
elif user_config.stt.provider == ServiceProviders.DOGRAH.value:

View file

@ -6,7 +6,15 @@ worker. The guarantees that matter for draining: register/unregister are
idempotent, and the count only reaches zero when every registered run is gone.
"""
import asyncio
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from api.routes import main as main_routes
from api.services.pipecat import active_calls
from api.services.pipecat import run_pipeline as run_pipeline_module
def setup_function():
@ -14,6 +22,21 @@ def setup_function():
active_calls._active_run_ids.clear()
def _make_active_calls_client(
monkeypatch,
configured_secret: str | None = "test-dograh-devops-secret",
) -> TestClient:
monkeypatch.setattr("api.constants.DOGRAH_DEVOPS_SECRET", configured_secret)
app = FastAPI()
app.add_api_route(
"/api/v1/health/active-calls",
main_routes.active_calls,
methods=["GET"],
response_model=main_routes.ActiveCallsResponse,
)
return TestClient(app)
def test_starts_empty():
assert active_calls.active_call_count() == 0
@ -51,3 +74,147 @@ def test_full_lifecycle_drains_to_zero():
assert active_calls.active_call_count() == 1
active_calls.unregister_active_call(42)
assert active_calls.active_call_count() == 0
@pytest.mark.asyncio
async def test_run_pipeline_counts_call_during_setup(monkeypatch):
entered_setup = asyncio.Event()
release_setup = asyncio.Event()
async def fake_get_workflow_run(*args, **kwargs):
entered_setup.set()
await release_setup.wait()
raise RuntimeError("setup failed")
monkeypatch.setattr(
run_pipeline_module.db_client,
"get_workflow_run",
fake_get_workflow_run,
)
task = asyncio.create_task(
run_pipeline_module._run_pipeline(
transport=object(),
workflow_id=1,
workflow_run_id=42,
user_id=7,
)
)
await asyncio.wait_for(entered_setup.wait(), timeout=1.0)
assert active_calls.active_call_count() == 1
release_setup.set()
with pytest.raises(RuntimeError, match="setup failed"):
await asyncio.wait_for(task, timeout=1.0)
assert active_calls.active_call_count() == 0
@pytest.mark.asyncio
async def test_webrtc_entrypoint_counts_call_during_setup(monkeypatch):
entered_setup = asyncio.Event()
release_setup = asyncio.Event()
async def fake_get_workflow(*args, **kwargs):
entered_setup.set()
await release_setup.wait()
raise RuntimeError("setup failed")
monkeypatch.setattr(
run_pipeline_module.db_client, "get_workflow", fake_get_workflow
)
task = asyncio.create_task(
run_pipeline_module.run_pipeline_smallwebrtc(
webrtc_connection=object(),
workflow_id=1,
workflow_run_id=43,
user_id=7,
)
)
await asyncio.wait_for(entered_setup.wait(), timeout=1.0)
assert active_calls.active_call_count() == 1
release_setup.set()
with pytest.raises(RuntimeError, match="setup failed"):
await asyncio.wait_for(task, timeout=1.0)
assert active_calls.active_call_count() == 0
@pytest.mark.asyncio
async def test_telephony_entrypoint_counts_call_during_setup(monkeypatch):
entered_setup = asyncio.Event()
release_setup = asyncio.Event()
async def fake_get_workflow(*args, **kwargs):
entered_setup.set()
await release_setup.wait()
raise RuntimeError("setup failed")
monkeypatch.setattr(
run_pipeline_module.db_client, "get_workflow", fake_get_workflow
)
task = asyncio.create_task(
run_pipeline_module.run_pipeline_telephony(
websocket=object(),
provider_name="twilio",
workflow_id=1,
workflow_run_id=44,
user_id=7,
call_id="call-1",
transport_kwargs={},
)
)
await asyncio.wait_for(entered_setup.wait(), timeout=1.0)
assert active_calls.active_call_count() == 1
release_setup.set()
with pytest.raises(RuntimeError, match="setup failed"):
await asyncio.wait_for(task, timeout=1.0)
assert active_calls.active_call_count() == 0
def test_active_calls_route_requires_configured_secret(monkeypatch):
client = _make_active_calls_client(monkeypatch, configured_secret=None)
response = client.get(
"/api/v1/health/active-calls",
headers={"X-Dograh-Devops-Secret": "test-dograh-devops-secret"},
)
assert response.status_code == 503
def test_active_calls_route_rejects_missing_secret_header(monkeypatch):
client = _make_active_calls_client(monkeypatch)
response = client.get("/api/v1/health/active-calls")
assert response.status_code == 403
def test_active_calls_route_rejects_wrong_secret(monkeypatch):
client = _make_active_calls_client(monkeypatch)
response = client.get(
"/api/v1/health/active-calls",
headers={"X-Dograh-Devops-Secret": "wrong"},
)
assert response.status_code == 403
def test_active_calls_route_returns_count_with_secret(monkeypatch):
active_calls.register_active_call(42)
client = _make_active_calls_client(monkeypatch)
response = client.get(
"/api/v1/health/active-calls",
headers={"X-Dograh-Devops-Secret": "test-dograh-devops-secret"},
)
assert response.status_code == 200
assert response.json() == {"active_calls": 1}

View file

@ -0,0 +1,94 @@
from types import SimpleNamespace
from unittest.mock import patch
from api.services.configuration.options import (
CARTESIA_INK_2_STT_LANGUAGES,
CARTESIA_INK_WHISPER_STT_LANGUAGES,
CARTESIA_STT_MODELS,
)
from api.services.configuration.registry import (
CartesiaSTTConfiguration,
ServiceProviders,
)
from api.services.pipecat.audio_config import AudioConfig
from api.services.pipecat.service_factory import (
create_stt_service,
stt_uses_external_turns,
)
def _audio_config() -> AudioConfig:
return AudioConfig(
transport_in_sample_rate=16000,
transport_out_sample_rate=16000,
)
def _cartesia_config(model: str, language: str = "en") -> SimpleNamespace:
return SimpleNamespace(
stt=SimpleNamespace(
provider=ServiceProviders.CARTESIA.value,
api_key="test-key",
model=model,
language=language,
)
)
def test_cartesia_stt_configuration_exposes_ink_2_and_ink_whisper_languages():
config = CartesiaSTTConfiguration(api_key="test-key")
language_schema = CartesiaSTTConfiguration.model_json_schema()["properties"][
"language"
]
assert config.provider == ServiceProviders.CARTESIA
assert config.model == "ink-whisper"
assert config.language == "en"
assert CARTESIA_STT_MODELS == ["ink-2", "ink-whisper"]
assert CARTESIA_INK_2_STT_LANGUAGES == ("en",)
assert "es" in CARTESIA_INK_WHISPER_STT_LANGUAGES
assert language_schema["model_options"]["ink-2"] == ["en"]
assert "es" in language_schema["model_options"]["ink-whisper"]
def test_cartesia_ink_2_uses_external_turns_and_turns_service():
user_config = _cartesia_config("ink-2")
assert stt_uses_external_turns(user_config)
with (
patch(
"api.services.pipecat.service_factory.CartesiaTurnsSTTService"
) as turns_service,
patch("api.services.pipecat.service_factory.CartesiaSTTService") as stt_service,
):
create_stt_service(user_config, _audio_config())
turns_service.assert_called_once()
stt_service.assert_not_called()
kwargs = turns_service.call_args.kwargs
assert kwargs["api_key"] == "test-key"
assert kwargs["sample_rate"] == 16000
assert kwargs["should_interrupt"] is False
def test_cartesia_ink_whisper_uses_manual_stt_service_with_model_and_language():
user_config = _cartesia_config("ink-whisper", language="es")
assert not stt_uses_external_turns(user_config)
with (
patch(
"api.services.pipecat.service_factory.CartesiaTurnsSTTService"
) as turns_service,
patch("api.services.pipecat.service_factory.CartesiaSTTService") as stt_service,
):
create_stt_service(user_config, _audio_config())
turns_service.assert_not_called()
stt_service.assert_called_once()
kwargs = stt_service.call_args.kwargs
assert kwargs["api_key"] == "test-key"
assert kwargs["sample_rate"] == 16000
assert kwargs["settings"].model == "ink-whisper"
assert kwargs["settings"].language == "es"

View file

@ -9,7 +9,7 @@ from api.services.pipecat.audio_config import AudioConfig
from api.services.pipecat.service_factory import (
create_stt_service,
dograh_stt_uses_flux_language,
stt_uses_flux_turns,
stt_uses_external_turns,
)
@ -38,10 +38,10 @@ def test_dograh_flux_language_predicate_matches_multilingual_support():
assert not dograh_stt_uses_flux_language("ar")
def test_stt_uses_flux_turns_only_for_dograh_flux_supported_languages():
assert stt_uses_flux_turns(_dograh_config("multi"))
assert stt_uses_flux_turns(_dograh_config("es"))
assert not stt_uses_flux_turns(_dograh_config("ar"))
def test_stt_uses_external_turns_only_for_dograh_flux_supported_languages():
assert stt_uses_external_turns(_dograh_config("multi"))
assert stt_uses_external_turns(_dograh_config("es"))
assert not stt_uses_external_turns(_dograh_config("ar"))
def test_create_dograh_multi_uses_flux_service_without_language_hint():

View file

@ -11,7 +11,12 @@ from pipecat.turns.user_stop import (
)
from api.services.configuration.registry import ServiceProviders
from api.services.pipecat.run_pipeline import _create_realtime_user_turn_config
from api.services.pipecat.run_pipeline import (
DEFAULT_USER_TURN_STOP_TIMEOUT,
EXTERNAL_TURN_USER_STOP_TIMEOUT,
_create_realtime_user_turn_config,
_resolve_user_turn_stop_timeout,
)
def test_gemini_realtime_uses_local_vad_without_local_interruptions():
@ -72,3 +77,27 @@ def test_unknown_realtime_providers_keep_local_vad():
assert isinstance(strategies.start[0], VADUserTurnStartStrategy)
assert len(strategies.stop) == 1
assert isinstance(strategies.stop[0], SpeechTimeoutUserTurnStopStrategy)
def test_external_turn_stt_uses_longer_stop_timeout():
assert (
_resolve_user_turn_stop_timeout({}, uses_external_turns=True)
== EXTERNAL_TURN_USER_STOP_TIMEOUT
)
def test_standard_stt_keeps_default_stop_timeout():
assert (
_resolve_user_turn_stop_timeout({}, uses_external_turns=False)
== DEFAULT_USER_TURN_STOP_TIMEOUT
)
def test_workflow_config_can_override_user_turn_stop_timeout():
assert (
_resolve_user_turn_stop_timeout(
{"user_turn_stop_timeout": "12.5"},
uses_external_turns=True,
)
== 12.5
)