mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-07-13 11:22:14 +02:00
feat: refactor telephony to support multiple telephony configurations (#251)
Co-authored-by: Sabiha Khan <sabihak89@gmail.com>
This commit is contained in:
parent
2f860e7f6d
commit
e16f6438bd
101 changed files with 10906 additions and 5420 deletions
|
|
@ -1,5 +1,4 @@
|
|||
"""
|
||||
Audio configuration for pipeline components.
|
||||
"""Audio configuration for pipeline components.
|
||||
|
||||
This module provides centralized audio configuration to ensure consistent
|
||||
sample rates across all pipeline components and proper coordination between
|
||||
|
|
@ -11,8 +10,6 @@ from typing import Optional
|
|||
|
||||
from loguru import logger
|
||||
|
||||
from api.enums import WorkflowRunMode
|
||||
|
||||
|
||||
@dataclass
|
||||
class AudioConfig:
|
||||
|
|
@ -84,61 +81,35 @@ class AudioConfig:
|
|||
|
||||
|
||||
def create_audio_config(transport_type: str) -> AudioConfig:
|
||||
"""Create audio configuration based on transport type.
|
||||
"""Create audio configuration for a given transport.
|
||||
|
||||
Args:
|
||||
transport_type: Type of transport ("webrtc", "twilio", "plivo", "vonage", "vobiz", "cloudonix")
|
||||
|
||||
Returns:
|
||||
AudioConfig instance with appropriate settings
|
||||
Telephony providers contribute their wire-format sample rate through the
|
||||
provider registry (``ProviderSpec.transport_sample_rate``); WebRTC modes
|
||||
use 16 kHz (transports handle resampling from/to 24 kHz). The remaining
|
||||
AudioConfig fields are derived from the chosen rate.
|
||||
"""
|
||||
if transport_type in (
|
||||
WorkflowRunMode.TWILIO.value,
|
||||
WorkflowRunMode.PLIVO.value,
|
||||
WorkflowRunMode.VOBIZ.value,
|
||||
WorkflowRunMode.CLOUDONIX.value,
|
||||
WorkflowRunMode.ARI.value,
|
||||
WorkflowRunMode.TELNYX.value,
|
||||
):
|
||||
# Twilio, Plivo, Cloudonix, Vobiz, Telnyx, and ARI use MULAW at 8kHz
|
||||
return AudioConfig(
|
||||
transport_in_sample_rate=8000,
|
||||
transport_out_sample_rate=8000,
|
||||
vad_sample_rate=8000, # Use matching VAD rate
|
||||
pipeline_sample_rate=8000, # Keep at 8kHz to avoid resampling
|
||||
buffer_size_seconds=5.0,
|
||||
)
|
||||
elif transport_type == WorkflowRunMode.VONAGE.value:
|
||||
# Vonage uses 16kHz Linear PCM
|
||||
return AudioConfig(
|
||||
transport_in_sample_rate=16000,
|
||||
transport_out_sample_rate=16000,
|
||||
vad_sample_rate=16000, # Use matching VAD rate
|
||||
pipeline_sample_rate=16000, # Keep at 16kHz to avoid resampling
|
||||
buffer_size_seconds=5.0,
|
||||
)
|
||||
elif transport_type in [
|
||||
# Defer registry import to avoid an import cycle: the registry is imported
|
||||
# by every telephony provider package at startup.
|
||||
from api.enums import WorkflowRunMode
|
||||
from api.services.telephony import registry
|
||||
|
||||
telephony_spec = registry.get_optional(transport_type)
|
||||
if telephony_spec is not None:
|
||||
rate = telephony_spec.transport_sample_rate
|
||||
elif transport_type in (
|
||||
WorkflowRunMode.WEBRTC.value,
|
||||
WorkflowRunMode.SMALLWEBRTC.value,
|
||||
]:
|
||||
# WebRTC typically uses 24kHz or 48kHz, but we limit pipeline to 16kHz
|
||||
# The transport will handle resampling between 24kHz and 16kHz
|
||||
return AudioConfig(
|
||||
transport_in_sample_rate=16000, # Transport will resample from 24kHz
|
||||
transport_out_sample_rate=16000, # Transport will resample to 24kHz
|
||||
vad_sample_rate=16000, # VAD native rate
|
||||
pipeline_sample_rate=16000, # Keep pipeline at 16kHz
|
||||
buffer_size_seconds=5.0,
|
||||
)
|
||||
):
|
||||
rate = 16000
|
||||
else:
|
||||
# Default configuration
|
||||
logger.warning(
|
||||
f"Unknown transport type: {transport_type}, using default config"
|
||||
)
|
||||
return AudioConfig(
|
||||
transport_in_sample_rate=16000,
|
||||
transport_out_sample_rate=16000,
|
||||
vad_sample_rate=16000,
|
||||
pipeline_sample_rate=16000,
|
||||
buffer_size_seconds=5.0,
|
||||
)
|
||||
rate = 16000
|
||||
|
||||
return AudioConfig(
|
||||
transport_in_sample_rate=rate,
|
||||
transport_out_sample_rate=rate,
|
||||
vad_sample_rate=rate,
|
||||
pipeline_sample_rate=rate,
|
||||
)
|
||||
|
|
|
|||
55
api/services/pipecat/audio_mixer.py
Normal file
55
api/services/pipecat/audio_mixer.py
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
"""Shared helper for building audio output mixers used by telephony transports."""
|
||||
|
||||
import os
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from api.constants import APP_ROOT_DIR
|
||||
from api.services.pipecat.audio_file_cache import get_cached_ambient_noise_path
|
||||
from pipecat.audio.mixers.silence_mixer import SilenceAudioMixer
|
||||
from pipecat.audio.mixers.soundfile_mixer import SoundfileMixer
|
||||
|
||||
librnnoise_path = os.path.normpath(
|
||||
str(APP_ROOT_DIR / "native" / "rnnoise" / "librnnoise.so")
|
||||
)
|
||||
|
||||
|
||||
async def build_audio_out_mixer(
|
||||
audio_out_sample_rate: int,
|
||||
ambient_noise_config: dict | None,
|
||||
):
|
||||
"""Build the audio output mixer based on the ambient noise configuration.
|
||||
|
||||
Returns a ``SoundfileMixer`` when ambient noise is enabled, or a
|
||||
``SilenceAudioMixer`` otherwise. Supports custom user-uploaded audio
|
||||
files via the ``storage_key`` / ``storage_backend`` fields in the config.
|
||||
"""
|
||||
if not ambient_noise_config or not ambient_noise_config.get("enabled", False):
|
||||
return SilenceAudioMixer()
|
||||
|
||||
volume = ambient_noise_config.get("volume", 0.3)
|
||||
|
||||
storage_key = ambient_noise_config.get("storage_key")
|
||||
storage_backend = ambient_noise_config.get("storage_backend")
|
||||
|
||||
if storage_key and storage_backend:
|
||||
cached_path = await get_cached_ambient_noise_path(
|
||||
storage_key, storage_backend, audio_out_sample_rate
|
||||
)
|
||||
if cached_path:
|
||||
return SoundfileMixer(
|
||||
sound_files={"custom": cached_path},
|
||||
default_sound="custom",
|
||||
volume=volume,
|
||||
)
|
||||
logger.warning("Custom ambient noise file unavailable, falling back to default")
|
||||
|
||||
return SoundfileMixer(
|
||||
sound_files={
|
||||
"office": APP_ROOT_DIR
|
||||
/ "assets"
|
||||
/ f"office-ambience-{audio_out_sample_rate}-mono.wav"
|
||||
},
|
||||
default_sound="office",
|
||||
volume=volume,
|
||||
)
|
||||
|
|
@ -1,11 +1,10 @@
|
|||
import asyncio
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import HTTPException, WebSocket
|
||||
from fastapi import HTTPException
|
||||
from loguru import logger
|
||||
|
||||
from api.db import db_client
|
||||
from api.db.models import WorkflowModel
|
||||
from api.enums import WorkflowRunMode
|
||||
from api.services.configuration.registry import ServiceProviders
|
||||
from api.services.pipecat.audio_config import AudioConfig, create_audio_config
|
||||
|
|
@ -44,17 +43,9 @@ from api.services.pipecat.service_factory import (
|
|||
from api.services.pipecat.tracing_config import (
|
||||
ensure_tracing,
|
||||
)
|
||||
from api.services.pipecat.transport_setup import (
|
||||
create_ari_transport,
|
||||
create_cloudonix_transport,
|
||||
create_plivo_transport,
|
||||
create_telnyx_transport,
|
||||
create_twilio_transport,
|
||||
create_vobiz_transport,
|
||||
create_vonage_transport,
|
||||
create_webrtc_transport,
|
||||
)
|
||||
from api.services.pipecat.transport_setup import create_webrtc_transport
|
||||
from api.services.pipecat.ws_sender_registry import get_ws_sender
|
||||
from api.services.telephony import registry as telephony_registry
|
||||
from api.services.workflow.dto import ReactFlowDTO
|
||||
from api.services.workflow.pipecat_engine import PipecatEngine
|
||||
from api.services.workflow.workflow import WorkflowGraph
|
||||
|
|
@ -95,110 +86,75 @@ from pipecat.utils.run_context import set_current_org_id, set_current_run_id
|
|||
ensure_tracing()
|
||||
|
||||
|
||||
async def run_pipeline_twilio(
|
||||
websocket_client: WebSocket,
|
||||
stream_sid: str,
|
||||
call_sid: str,
|
||||
async def run_pipeline_telephony(
|
||||
websocket,
|
||||
*,
|
||||
provider_name: str,
|
||||
workflow_id: int,
|
||||
workflow_run_id: int,
|
||||
user_id: int,
|
||||
call_id: str,
|
||||
transport_kwargs: dict,
|
||||
) -> None:
|
||||
"""Run pipeline for Twilio connections"""
|
||||
logger.debug(
|
||||
f"Running pipeline for Twilio connection with workflow_id: {workflow_id} and workflow_run_id: {workflow_run_id}"
|
||||
)
|
||||
"""Run a pipeline for any telephony provider.
|
||||
|
||||
Replaces the previous per-provider run_pipeline_<x> functions. The
|
||||
provider's transport factory and audio config are looked up from the
|
||||
registry, so adding a new provider requires no changes here.
|
||||
|
||||
Args:
|
||||
websocket: The accepted WebSocket from the provider.
|
||||
provider_name: Stable identifier of the provider (registry key).
|
||||
workflow_id: Workflow being executed.
|
||||
workflow_run_id: Workflow run row.
|
||||
user_id: Owner of the workflow.
|
||||
call_id: Provider call identifier (stored in cost_info for billing).
|
||||
transport_kwargs: Provider-specific kwargs forwarded to the transport
|
||||
factory (e.g. stream_sid + call_sid for Twilio).
|
||||
"""
|
||||
logger.debug(f"Running {provider_name} pipeline for workflow_run {workflow_run_id}")
|
||||
set_current_run_id(workflow_run_id)
|
||||
|
||||
# Store call ID in cost_info for later cost calculation (provider-agnostic)
|
||||
cost_info = {"call_id": call_sid}
|
||||
await db_client.update_workflow_run(workflow_run_id, cost_info=cost_info)
|
||||
await db_client.update_workflow_run(workflow_run_id, cost_info={"call_id": call_id})
|
||||
|
||||
# Get workflow to extract all pipeline configurations
|
||||
workflow = await db_client.get_workflow(workflow_id, user_id)
|
||||
|
||||
# Set org context early so tasks created by the transport inherit it
|
||||
if workflow:
|
||||
set_current_org_id(workflow.organization_id)
|
||||
|
||||
vad_config = None
|
||||
ambient_noise_config = None
|
||||
if workflow and workflow.workflow_configurations:
|
||||
if "vad_configuration" in workflow.workflow_configurations:
|
||||
vad_config = workflow.workflow_configurations["vad_configuration"]
|
||||
if "ambient_noise_configuration" in workflow.workflow_configurations:
|
||||
ambient_noise_config = workflow.workflow_configurations[
|
||||
"ambient_noise_configuration"
|
||||
]
|
||||
vad_config = workflow.workflow_configurations.get("vad_configuration")
|
||||
ambient_noise_config = workflow.workflow_configurations.get(
|
||||
"ambient_noise_configuration"
|
||||
)
|
||||
|
||||
# Create audio configuration for Twilio
|
||||
audio_config = create_audio_config(WorkflowRunMode.TWILIO.value)
|
||||
# The telephony config id is stamped on the workflow run when it's created
|
||||
# (test call, campaign dispatch, inbound). Transports use it to load creds
|
||||
# from the right config row. Falls back to None for legacy runs (transports
|
||||
# then resolve the org's default config).
|
||||
workflow_run = await db_client.get_workflow_run(workflow_run_id)
|
||||
telephony_configuration_id = None
|
||||
if workflow_run and workflow_run.initial_context:
|
||||
telephony_configuration_id = workflow_run.initial_context.get(
|
||||
"telephony_configuration_id"
|
||||
)
|
||||
|
||||
transport = await create_twilio_transport(
|
||||
websocket_client,
|
||||
stream_sid,
|
||||
call_sid,
|
||||
spec = telephony_registry.get(provider_name)
|
||||
audio_config = create_audio_config(provider_name)
|
||||
|
||||
transport = await spec.transport_factory(
|
||||
websocket,
|
||||
workflow_run_id,
|
||||
audio_config,
|
||||
workflow.organization_id,
|
||||
vad_config,
|
||||
ambient_noise_config,
|
||||
vad_config=vad_config,
|
||||
ambient_noise_config=ambient_noise_config,
|
||||
telephony_configuration_id=telephony_configuration_id,
|
||||
**transport_kwargs,
|
||||
)
|
||||
await _run_pipeline(
|
||||
transport,
|
||||
workflow_id,
|
||||
workflow_run_id,
|
||||
user_id,
|
||||
audio_config=audio_config,
|
||||
)
|
||||
|
||||
|
||||
async def run_pipeline_plivo(
|
||||
websocket_client: WebSocket,
|
||||
stream_id: str,
|
||||
call_id: str,
|
||||
workflow_id: int,
|
||||
workflow_run_id: int,
|
||||
user_id: int,
|
||||
) -> None:
|
||||
"""Run pipeline for Plivo WebSocket connections."""
|
||||
logger.info(
|
||||
f"[run {workflow_run_id}] Starting Plivo pipeline - "
|
||||
f"stream_id={stream_id}, call_id={call_id}, workflow_id={workflow_id}"
|
||||
)
|
||||
set_current_run_id(workflow_run_id)
|
||||
|
||||
cost_info = {"call_id": call_id}
|
||||
await db_client.update_workflow_run(workflow_run_id, cost_info=cost_info)
|
||||
|
||||
workflow = await db_client.get_workflow(workflow_id, user_id)
|
||||
|
||||
if workflow:
|
||||
set_current_org_id(workflow.organization_id)
|
||||
|
||||
vad_config = None
|
||||
ambient_noise_config = None
|
||||
if workflow and workflow.workflow_configurations:
|
||||
if "vad_configuration" in workflow.workflow_configurations:
|
||||
vad_config = workflow.workflow_configurations["vad_configuration"]
|
||||
if "ambient_noise_configuration" in workflow.workflow_configurations:
|
||||
ambient_noise_config = workflow.workflow_configurations[
|
||||
"ambient_noise_configuration"
|
||||
]
|
||||
|
||||
try:
|
||||
audio_config = create_audio_config(WorkflowRunMode.PLIVO.value)
|
||||
|
||||
transport = await create_plivo_transport(
|
||||
websocket_client,
|
||||
stream_id,
|
||||
call_id,
|
||||
workflow_run_id,
|
||||
audio_config,
|
||||
workflow.organization_id,
|
||||
vad_config,
|
||||
ambient_noise_config,
|
||||
)
|
||||
|
||||
await _run_pipeline(
|
||||
transport,
|
||||
workflow_id,
|
||||
|
|
@ -206,341 +162,14 @@ async def run_pipeline_plivo(
|
|||
user_id,
|
||||
audio_config=audio_config,
|
||||
)
|
||||
logger.info(f"[run {workflow_run_id}] Plivo pipeline completed successfully")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"[run {workflow_run_id}] Error in Plivo pipeline: {e}", exc_info=True
|
||||
f"[run {workflow_run_id}] Error in {provider_name} pipeline: {e}",
|
||||
exc_info=True,
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
async def run_pipeline_vonage(
|
||||
websocket_client,
|
||||
call_uuid: str,
|
||||
workflow: WorkflowModel,
|
||||
organization_id: int,
|
||||
workflow_id: int,
|
||||
workflow_run_id: int,
|
||||
user_id: int,
|
||||
):
|
||||
"""Run pipeline for Vonage WebSocket connections.
|
||||
|
||||
Vonage uses raw PCM audio over WebSocket instead of base64-encoded μ-law.
|
||||
The audio is transmitted as binary frames at 16kHz by default.
|
||||
"""
|
||||
logger.info(f"Starting Vonage pipeline for workflow run {workflow_run_id}")
|
||||
set_current_run_id(workflow_run_id)
|
||||
set_current_org_id(organization_id)
|
||||
|
||||
# Store call ID in cost_info for later cost calculation (provider-agnostic)
|
||||
cost_info = {"call_id": call_uuid}
|
||||
await db_client.update_workflow_run(workflow_run_id, cost_info=cost_info)
|
||||
|
||||
# Extract VAD and ambient noise config from workflow
|
||||
vad_config = None
|
||||
ambient_noise_config = None
|
||||
if workflow and workflow.workflow_configurations:
|
||||
if "vad_configuration" in workflow.workflow_configurations:
|
||||
vad_config = workflow.workflow_configurations["vad_configuration"]
|
||||
if "ambient_noise_configuration" in workflow.workflow_configurations:
|
||||
ambient_noise_config = workflow.workflow_configurations[
|
||||
"ambient_noise_configuration"
|
||||
]
|
||||
|
||||
try:
|
||||
# Setup audio config for Vonage using the centralized config
|
||||
audio_config = create_audio_config(WorkflowRunMode.VONAGE.value)
|
||||
|
||||
# Create Vonage transport
|
||||
transport = await create_vonage_transport(
|
||||
websocket_client,
|
||||
call_uuid,
|
||||
workflow_run_id,
|
||||
audio_config,
|
||||
organization_id,
|
||||
vad_config,
|
||||
ambient_noise_config,
|
||||
)
|
||||
|
||||
# No special handshake needed for Vonage
|
||||
# Audio streaming starts immediately
|
||||
|
||||
# Run the pipeline (same as Twilio/WebRTC)
|
||||
await _run_pipeline(
|
||||
transport,
|
||||
workflow_id,
|
||||
workflow_run_id,
|
||||
user_id,
|
||||
call_context_vars={},
|
||||
audio_config=audio_config,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in Vonage pipeline: {e}")
|
||||
raise
|
||||
|
||||
|
||||
async def run_pipeline_ari(
|
||||
websocket_client: WebSocket,
|
||||
channel_id: str,
|
||||
workflow_id: int,
|
||||
workflow_run_id: int,
|
||||
user_id: int,
|
||||
) -> None:
|
||||
"""Run pipeline for Asterisk ARI WebSocket connections.
|
||||
|
||||
ARI uses raw 16-bit signed linear PCM (SLIN16) at 16kHz
|
||||
transmitted as binary WebSocket frames via chan_websocket.
|
||||
"""
|
||||
logger.info(f"Starting ARI pipeline for workflow run {workflow_run_id}")
|
||||
set_current_run_id(workflow_run_id)
|
||||
|
||||
# Store call ID (channel_id) in cost_info
|
||||
cost_info = {"call_id": channel_id}
|
||||
await db_client.update_workflow_run(workflow_run_id, cost_info=cost_info)
|
||||
|
||||
# Get workflow to extract configurations
|
||||
workflow = await db_client.get_workflow(workflow_id, user_id)
|
||||
|
||||
# Set org context early so tasks created by the transport inherit it
|
||||
if workflow:
|
||||
set_current_org_id(workflow.organization_id)
|
||||
|
||||
vad_config = None
|
||||
ambient_noise_config = None
|
||||
if workflow and workflow.workflow_configurations:
|
||||
if "vad_configuration" in workflow.workflow_configurations:
|
||||
vad_config = workflow.workflow_configurations["vad_configuration"]
|
||||
if "ambient_noise_configuration" in workflow.workflow_configurations:
|
||||
ambient_noise_config = workflow.workflow_configurations[
|
||||
"ambient_noise_configuration"
|
||||
]
|
||||
|
||||
try:
|
||||
audio_config = create_audio_config(WorkflowRunMode.ARI.value)
|
||||
|
||||
transport = await create_ari_transport(
|
||||
websocket_client,
|
||||
channel_id,
|
||||
workflow_run_id,
|
||||
audio_config,
|
||||
workflow.organization_id,
|
||||
vad_config,
|
||||
ambient_noise_config,
|
||||
)
|
||||
|
||||
await _run_pipeline(
|
||||
transport,
|
||||
workflow_id,
|
||||
workflow_run_id,
|
||||
user_id,
|
||||
audio_config=audio_config,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in ARI pipeline: {e}")
|
||||
raise
|
||||
|
||||
|
||||
async def run_pipeline_vobiz(
|
||||
websocket_client: WebSocket,
|
||||
stream_id: str,
|
||||
call_id: str,
|
||||
workflow_id: int,
|
||||
workflow_run_id: int,
|
||||
user_id: int,
|
||||
) -> None:
|
||||
"""Run pipeline for Vobiz using Plivo-compatible WebSocket protocol."""
|
||||
logger.info(
|
||||
f"[run {workflow_run_id}] Starting Vobiz pipeline - "
|
||||
f"stream_id={stream_id}, call_id={call_id}, workflow_id={workflow_id}"
|
||||
)
|
||||
set_current_run_id(workflow_run_id)
|
||||
|
||||
cost_info = {"call_id": call_id}
|
||||
await db_client.update_workflow_run(workflow_run_id, cost_info=cost_info)
|
||||
|
||||
workflow = await db_client.get_workflow(workflow_id, user_id)
|
||||
|
||||
# Set org context early so tasks created by the transport inherit it
|
||||
if workflow:
|
||||
set_current_org_id(workflow.organization_id)
|
||||
|
||||
vad_config = None
|
||||
ambient_noise_config = None
|
||||
if workflow and workflow.workflow_configurations:
|
||||
if "vad_configuration" in workflow.workflow_configurations:
|
||||
vad_config = workflow.workflow_configurations["vad_configuration"]
|
||||
if "ambient_noise_configuration" in workflow.workflow_configurations:
|
||||
ambient_noise_config = workflow.workflow_configurations[
|
||||
"ambient_noise_configuration"
|
||||
]
|
||||
|
||||
try:
|
||||
audio_config = create_audio_config(WorkflowRunMode.VOBIZ.value)
|
||||
logger.info(
|
||||
f"[run {workflow_run_id}] Vobiz audio config: "
|
||||
f"sample_rate={audio_config.transport_in_sample_rate}Hz, format=MULAW"
|
||||
)
|
||||
|
||||
transport = await create_vobiz_transport(
|
||||
websocket_client,
|
||||
stream_id,
|
||||
call_id,
|
||||
workflow_run_id,
|
||||
audio_config,
|
||||
workflow.organization_id,
|
||||
vad_config,
|
||||
ambient_noise_config,
|
||||
)
|
||||
|
||||
logger.info(f"[run {workflow_run_id}] Starting Vobiz pipeline execution")
|
||||
await _run_pipeline(
|
||||
transport,
|
||||
workflow_id,
|
||||
workflow_run_id,
|
||||
user_id,
|
||||
audio_config=audio_config,
|
||||
)
|
||||
logger.info(f"[run {workflow_run_id}] Vobiz pipeline completed successfully")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"[run {workflow_run_id}] Error in Vobiz pipeline: {e}", exc_info=True
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
async def run_pipeline_telnyx(
|
||||
websocket_client: WebSocket,
|
||||
stream_id: str,
|
||||
call_control_id: str,
|
||||
workflow_id: int,
|
||||
workflow_run_id: int,
|
||||
user_id: int,
|
||||
) -> None:
|
||||
"""Run pipeline for Telnyx Call Control WebSocket connections.
|
||||
|
||||
Telnyx uses PCMU at 8kHz over WebSocket with base64-encoded media events,
|
||||
similar to Twilio's protocol.
|
||||
"""
|
||||
logger.info(
|
||||
f"[run {workflow_run_id}] Starting Telnyx pipeline - "
|
||||
f"stream_id={stream_id}, call_control_id={call_control_id}, "
|
||||
f"workflow_id={workflow_id}"
|
||||
)
|
||||
set_current_run_id(workflow_run_id)
|
||||
|
||||
cost_info = {"call_id": call_control_id}
|
||||
await db_client.update_workflow_run(workflow_run_id, cost_info=cost_info)
|
||||
|
||||
workflow = await db_client.get_workflow(workflow_id, user_id)
|
||||
|
||||
if workflow:
|
||||
set_current_org_id(workflow.organization_id)
|
||||
|
||||
vad_config = None
|
||||
ambient_noise_config = None
|
||||
if workflow and workflow.workflow_configurations:
|
||||
if "vad_configuration" in workflow.workflow_configurations:
|
||||
vad_config = workflow.workflow_configurations["vad_configuration"]
|
||||
if "ambient_noise_configuration" in workflow.workflow_configurations:
|
||||
ambient_noise_config = workflow.workflow_configurations[
|
||||
"ambient_noise_configuration"
|
||||
]
|
||||
|
||||
try:
|
||||
audio_config = create_audio_config(WorkflowRunMode.TELNYX.value)
|
||||
|
||||
transport = await create_telnyx_transport(
|
||||
websocket_client,
|
||||
stream_id,
|
||||
call_control_id,
|
||||
workflow_run_id,
|
||||
audio_config,
|
||||
workflow.organization_id,
|
||||
vad_config,
|
||||
ambient_noise_config,
|
||||
)
|
||||
|
||||
await _run_pipeline(
|
||||
transport,
|
||||
workflow_id,
|
||||
workflow_run_id,
|
||||
user_id,
|
||||
audio_config=audio_config,
|
||||
)
|
||||
logger.info(f"[run {workflow_run_id}] Telnyx pipeline completed successfully")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"[run {workflow_run_id}] Error in Telnyx pipeline: {e}", exc_info=True
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
async def run_pipeline_cloudonix(
|
||||
websocket_client: WebSocket,
|
||||
stream_sid: str,
|
||||
workflow_id: int,
|
||||
workflow_run_id: int,
|
||||
user_id: int,
|
||||
) -> None:
|
||||
"""Run pipeline for Cloudonix connections"""
|
||||
logger.debug(
|
||||
f"Running pipeline for Cloudonix connection with workflow_id: {workflow_id} and workflow_run_id: {workflow_run_id}"
|
||||
)
|
||||
|
||||
workflow_run = await db_client.get_workflow_run_by_id(workflow_run_id)
|
||||
call_id = workflow_run.gathered_context.get("call_id")
|
||||
if not call_id:
|
||||
logger.warning("call_id not found in gathered_context")
|
||||
raise Exception()
|
||||
|
||||
# Store call ID in cost_info for later cost calculation (provider-agnostic)
|
||||
cost_info = {"call_id": call_id}
|
||||
await db_client.update_workflow_run(workflow_run_id, cost_info=cost_info)
|
||||
|
||||
# Get workflow to extract all pipeline configurations
|
||||
workflow = await db_client.get_workflow(workflow_id, user_id)
|
||||
|
||||
# Set org context early so tasks created by the transport inherit it
|
||||
if workflow:
|
||||
set_current_org_id(workflow.organization_id)
|
||||
|
||||
vad_config = None
|
||||
ambient_noise_config = None
|
||||
if workflow and workflow.workflow_configurations:
|
||||
if "vad_configuration" in workflow.workflow_configurations:
|
||||
vad_config = workflow.workflow_configurations["vad_configuration"]
|
||||
if "ambient_noise_configuration" in workflow.workflow_configurations:
|
||||
ambient_noise_config = workflow.workflow_configurations[
|
||||
"ambient_noise_configuration"
|
||||
]
|
||||
|
||||
# Create audio configuration for Cloudonix
|
||||
audio_config = create_audio_config(WorkflowRunMode.CLOUDONIX.value)
|
||||
|
||||
transport = await create_cloudonix_transport(
|
||||
websocket_client,
|
||||
call_id,
|
||||
stream_sid,
|
||||
workflow_run_id,
|
||||
audio_config,
|
||||
workflow.organization_id,
|
||||
vad_config,
|
||||
ambient_noise_config,
|
||||
)
|
||||
await _run_pipeline(
|
||||
transport,
|
||||
workflow_id,
|
||||
workflow_run_id,
|
||||
user_id,
|
||||
audio_config=audio_config,
|
||||
)
|
||||
|
||||
|
||||
async def run_pipeline_smallwebrtc(
|
||||
webrtc_connection: SmallWebRTCConnection,
|
||||
workflow_id: int,
|
||||
|
|
|
|||
|
|
@ -1,514 +1,14 @@
|
|||
import os
|
||||
"""Transport factories for non-telephony pipelines.
|
||||
|
||||
from fastapi import WebSocket
|
||||
from loguru import logger
|
||||
Telephony transports live in their respective ``api.services.telephony.providers/<name>/transport.py``.
|
||||
This module hosts only the shared, non-telephony transports (WebRTC, internal/LoopTalk).
|
||||
"""
|
||||
|
||||
from api.constants import APP_ROOT_DIR
|
||||
from api.db import db_client
|
||||
from api.enums import OrganizationConfigurationKey
|
||||
from api.services.pipecat.audio_config import AudioConfig
|
||||
from api.services.pipecat.audio_file_cache import get_cached_ambient_noise_path
|
||||
from api.services.telephony.providers.ari_call_strategies import (
|
||||
ARIBridgeSwapStrategy,
|
||||
ARIHangupStrategy,
|
||||
)
|
||||
from api.services.telephony.providers.cloudonix_call_strategies import (
|
||||
CloudonixHangupStrategy,
|
||||
)
|
||||
from api.services.telephony.providers.twilio_call_strategies import (
|
||||
TwilioConferenceStrategy,
|
||||
TwilioHangupStrategy,
|
||||
)
|
||||
from pipecat.serializers.plivo import PlivoFrameSerializer
|
||||
from pipecat.audio.mixers.silence_mixer import SilenceAudioMixer
|
||||
from pipecat.audio.mixers.soundfile_mixer import SoundfileMixer
|
||||
from pipecat.serializers.asterisk import AsteriskFrameSerializer
|
||||
from pipecat.serializers.telnyx import TelnyxFrameSerializer
|
||||
from pipecat.serializers.twilio import TwilioFrameSerializer
|
||||
from pipecat.serializers.vobiz import VobizFrameSerializer
|
||||
from pipecat.serializers.vonage import VonageFrameSerializer
|
||||
from api.services.pipecat.audio_mixer import build_audio_out_mixer
|
||||
from pipecat.transports.base_transport import TransportParams
|
||||
from pipecat.transports.smallwebrtc.connection import SmallWebRTCConnection
|
||||
from pipecat.transports.smallwebrtc.transport import SmallWebRTCTransport
|
||||
from pipecat.transports.websocket.fastapi import (
|
||||
FastAPIWebsocketParams,
|
||||
FastAPIWebsocketTransport,
|
||||
)
|
||||
|
||||
librnnoise_path = os.path.normpath(
|
||||
str(APP_ROOT_DIR / "native" / "rnnoise" / "librnnoise.so")
|
||||
)
|
||||
|
||||
|
||||
async def _build_audio_out_mixer(
|
||||
audio_out_sample_rate: int,
|
||||
ambient_noise_config: dict | None,
|
||||
):
|
||||
"""Build the audio output mixer based on the ambient noise configuration.
|
||||
|
||||
Returns a ``SoundfileMixer`` when ambient noise is enabled, or a
|
||||
``SilenceAudioMixer`` otherwise. Supports custom user-uploaded audio
|
||||
files via the ``storage_key`` / ``storage_backend`` fields in the config.
|
||||
"""
|
||||
if not ambient_noise_config or not ambient_noise_config.get("enabled", False):
|
||||
return SilenceAudioMixer()
|
||||
|
||||
volume = ambient_noise_config.get("volume", 0.3)
|
||||
|
||||
# Check for a custom uploaded ambient noise file
|
||||
storage_key = ambient_noise_config.get("storage_key")
|
||||
storage_backend = ambient_noise_config.get("storage_backend")
|
||||
|
||||
if storage_key and storage_backend:
|
||||
cached_path = await get_cached_ambient_noise_path(
|
||||
storage_key, storage_backend, audio_out_sample_rate
|
||||
)
|
||||
if cached_path:
|
||||
return SoundfileMixer(
|
||||
sound_files={"custom": cached_path},
|
||||
default_sound="custom",
|
||||
volume=volume,
|
||||
)
|
||||
logger.warning("Custom ambient noise file unavailable, falling back to default")
|
||||
|
||||
# Default built-in office ambience
|
||||
return SoundfileMixer(
|
||||
sound_files={
|
||||
"office": APP_ROOT_DIR
|
||||
/ "assets"
|
||||
/ f"office-ambience-{audio_out_sample_rate}-mono.wav"
|
||||
},
|
||||
default_sound="office",
|
||||
volume=volume,
|
||||
)
|
||||
|
||||
|
||||
async def create_twilio_transport(
|
||||
websocket_client: WebSocket,
|
||||
stream_sid: str,
|
||||
call_sid: str,
|
||||
workflow_run_id: int,
|
||||
audio_config: AudioConfig,
|
||||
organization_id: int,
|
||||
vad_config: dict | None = None,
|
||||
ambient_noise_config: dict | None = None,
|
||||
):
|
||||
"""Create a transport for Twilio connections"""
|
||||
|
||||
# Fetch Twilio credentials from organization config
|
||||
config = await db_client.get_configuration(
|
||||
organization_id, OrganizationConfigurationKey.TELEPHONY_CONFIGURATION.value
|
||||
)
|
||||
|
||||
if not config or not config.value:
|
||||
raise ValueError(
|
||||
f"Twilio credentials not configured for organization {organization_id}"
|
||||
)
|
||||
|
||||
account_sid = config.value.get("account_sid")
|
||||
auth_token = config.value.get("auth_token")
|
||||
|
||||
if not account_sid or not auth_token:
|
||||
raise ValueError(
|
||||
f"Incomplete Twilio configuration for organization {organization_id}"
|
||||
)
|
||||
# Create strategy instances
|
||||
transfer_strategy = TwilioConferenceStrategy()
|
||||
hangup_strategy = TwilioHangupStrategy()
|
||||
|
||||
serializer = TwilioFrameSerializer(
|
||||
stream_sid=stream_sid,
|
||||
call_sid=call_sid,
|
||||
account_sid=account_sid,
|
||||
auth_token=auth_token,
|
||||
transfer_strategy=transfer_strategy,
|
||||
hangup_strategy=hangup_strategy,
|
||||
)
|
||||
|
||||
mixer = await _build_audio_out_mixer(
|
||||
audio_config.transport_out_sample_rate, ambient_noise_config
|
||||
)
|
||||
|
||||
return FastAPIWebsocketTransport(
|
||||
websocket=websocket_client,
|
||||
params=FastAPIWebsocketParams(
|
||||
audio_in_enabled=True,
|
||||
audio_out_enabled=True,
|
||||
audio_in_sample_rate=audio_config.transport_in_sample_rate,
|
||||
audio_out_sample_rate=audio_config.transport_out_sample_rate,
|
||||
audio_out_mixer=mixer,
|
||||
serializer=serializer,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def create_plivo_transport(
|
||||
websocket_client: WebSocket,
|
||||
stream_id: str,
|
||||
call_id: str,
|
||||
workflow_run_id: int,
|
||||
audio_config: AudioConfig,
|
||||
organization_id: int,
|
||||
vad_config: dict | None = None,
|
||||
ambient_noise_config: dict | None = None,
|
||||
):
|
||||
"""Create a transport for Plivo connections."""
|
||||
from api.services.telephony.factory import load_telephony_config
|
||||
|
||||
config = await load_telephony_config(organization_id)
|
||||
|
||||
if config.get("provider") != "plivo":
|
||||
raise ValueError(f"Expected Plivo provider, got {config.get('provider')}")
|
||||
|
||||
auth_id = config.get("auth_id")
|
||||
auth_token = config.get("auth_token")
|
||||
|
||||
if not auth_id or not auth_token:
|
||||
raise ValueError(
|
||||
f"Incomplete Plivo configuration for organization {organization_id}"
|
||||
)
|
||||
|
||||
serializer = PlivoFrameSerializer(
|
||||
stream_id=stream_id,
|
||||
call_id=call_id,
|
||||
auth_id=auth_id,
|
||||
auth_token=auth_token,
|
||||
params=PlivoFrameSerializer.InputParams(
|
||||
plivo_sample_rate=8000,
|
||||
sample_rate=audio_config.pipeline_sample_rate,
|
||||
),
|
||||
)
|
||||
|
||||
mixer = await _build_audio_out_mixer(
|
||||
audio_config.transport_out_sample_rate, ambient_noise_config
|
||||
)
|
||||
|
||||
return FastAPIWebsocketTransport(
|
||||
websocket=websocket_client,
|
||||
params=FastAPIWebsocketParams(
|
||||
audio_in_enabled=True,
|
||||
audio_out_enabled=True,
|
||||
audio_in_sample_rate=audio_config.transport_in_sample_rate,
|
||||
audio_out_sample_rate=audio_config.transport_out_sample_rate,
|
||||
audio_out_mixer=mixer,
|
||||
serializer=serializer,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def create_cloudonix_transport(
|
||||
websocket_client: WebSocket,
|
||||
call_id: str,
|
||||
stream_sid: str,
|
||||
workflow_run_id: int,
|
||||
audio_config: AudioConfig,
|
||||
organization_id: int,
|
||||
vad_config: dict | None = None,
|
||||
ambient_noise_config: dict | None = None,
|
||||
):
|
||||
"""Create a transport for Cloudonix connections"""
|
||||
|
||||
# Load Cloudonix configuration from database
|
||||
from api.services.telephony.factory import load_telephony_config
|
||||
|
||||
config = await load_telephony_config(organization_id)
|
||||
|
||||
if config.get("provider") != "cloudonix":
|
||||
raise ValueError(f"Expected Cloudonix provider, got {config.get('provider')}")
|
||||
|
||||
bearer_token = config.get("bearer_token")
|
||||
domain_id = config.get("domain_id")
|
||||
|
||||
if not bearer_token or not domain_id:
|
||||
raise ValueError(
|
||||
f"Incomplete Cloudonix configuration for organization {organization_id}. "
|
||||
f"Required: bearer_token, domain_id"
|
||||
)
|
||||
|
||||
from pipecat.serializers.cloudonix import CloudonixFrameSerializer
|
||||
|
||||
hangup_strategy = CloudonixHangupStrategy()
|
||||
serializer = CloudonixFrameSerializer(
|
||||
call_id=call_id,
|
||||
stream_sid=stream_sid,
|
||||
domain_id=domain_id,
|
||||
bearer_token=bearer_token,
|
||||
hangup_strategy=hangup_strategy,
|
||||
)
|
||||
|
||||
mixer = await _build_audio_out_mixer(
|
||||
audio_config.transport_out_sample_rate, ambient_noise_config
|
||||
)
|
||||
|
||||
return FastAPIWebsocketTransport(
|
||||
websocket=websocket_client,
|
||||
params=FastAPIWebsocketParams(
|
||||
audio_in_enabled=True,
|
||||
audio_out_enabled=True,
|
||||
audio_in_sample_rate=audio_config.transport_in_sample_rate,
|
||||
audio_out_sample_rate=audio_config.transport_out_sample_rate,
|
||||
audio_out_mixer=mixer,
|
||||
serializer=serializer,
|
||||
audio_out_10ms_chunks=2,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def create_telnyx_transport(
|
||||
websocket_client: WebSocket,
|
||||
stream_id: str,
|
||||
call_control_id: str,
|
||||
workflow_run_id: int,
|
||||
audio_config: AudioConfig,
|
||||
organization_id: int,
|
||||
vad_config: dict | None = None,
|
||||
ambient_noise_config: dict | None = None,
|
||||
):
|
||||
"""Create a transport for Telnyx connections."""
|
||||
config = await db_client.get_configuration(
|
||||
organization_id, OrganizationConfigurationKey.TELEPHONY_CONFIGURATION.value
|
||||
)
|
||||
|
||||
if not config or not config.value:
|
||||
raise ValueError(
|
||||
f"Telnyx credentials not configured for organization {organization_id}"
|
||||
)
|
||||
|
||||
if config.value.get("provider") != "telnyx":
|
||||
raise ValueError(
|
||||
f"Expected Telnyx provider, got {config.value.get('provider')}"
|
||||
)
|
||||
|
||||
api_key = config.value.get("api_key")
|
||||
if not api_key:
|
||||
raise ValueError(
|
||||
f"Incomplete Telnyx configuration for organization {organization_id}"
|
||||
)
|
||||
|
||||
serializer = TelnyxFrameSerializer(
|
||||
stream_id=stream_id,
|
||||
call_control_id=call_control_id,
|
||||
api_key=api_key,
|
||||
outbound_encoding="PCMU",
|
||||
inbound_encoding="PCMU",
|
||||
)
|
||||
|
||||
mixer = await _build_audio_out_mixer(
|
||||
audio_config.transport_out_sample_rate, ambient_noise_config
|
||||
)
|
||||
|
||||
return FastAPIWebsocketTransport(
|
||||
websocket=websocket_client,
|
||||
params=FastAPIWebsocketParams(
|
||||
audio_in_enabled=True,
|
||||
audio_out_enabled=True,
|
||||
audio_in_sample_rate=audio_config.transport_in_sample_rate,
|
||||
audio_out_sample_rate=audio_config.transport_out_sample_rate,
|
||||
audio_out_mixer=mixer,
|
||||
serializer=serializer,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def create_ari_transport(
|
||||
websocket_client: WebSocket,
|
||||
channel_id: str,
|
||||
workflow_run_id: int,
|
||||
audio_config: AudioConfig,
|
||||
organization_id: int,
|
||||
vad_config: dict | None = None,
|
||||
ambient_noise_config: dict | None = None,
|
||||
):
|
||||
"""Create a transport for Asterisk ARI connections"""
|
||||
|
||||
from api.services.telephony.factory import load_telephony_config
|
||||
|
||||
config = await load_telephony_config(organization_id)
|
||||
|
||||
if config.get("provider") != "ari":
|
||||
raise ValueError(f"Expected ARI provider, got {config.get('provider')}")
|
||||
|
||||
ari_endpoint = config.get("ari_endpoint")
|
||||
app_name = config.get("app_name")
|
||||
app_password = config.get("app_password")
|
||||
|
||||
if not ari_endpoint or not app_name or not app_password:
|
||||
raise ValueError(
|
||||
f"Incomplete ARI configuration for organization {organization_id}. "
|
||||
f"Required: ari_endpoint, app_name, app_password"
|
||||
)
|
||||
# Create strategy instances
|
||||
transfer_strategy = ARIBridgeSwapStrategy()
|
||||
hangup_strategy = ARIHangupStrategy()
|
||||
|
||||
serializer = AsteriskFrameSerializer(
|
||||
channel_id=channel_id,
|
||||
ari_endpoint=ari_endpoint,
|
||||
app_name=app_name,
|
||||
app_password=app_password,
|
||||
transfer_strategy=transfer_strategy,
|
||||
hangup_strategy=hangup_strategy,
|
||||
params=AsteriskFrameSerializer.InputParams(
|
||||
asterisk_sample_rate=audio_config.transport_in_sample_rate,
|
||||
sample_rate=audio_config.pipeline_sample_rate,
|
||||
),
|
||||
)
|
||||
|
||||
mixer = await _build_audio_out_mixer(
|
||||
audio_config.transport_out_sample_rate, ambient_noise_config
|
||||
)
|
||||
|
||||
return FastAPIWebsocketTransport(
|
||||
websocket=websocket_client,
|
||||
params=FastAPIWebsocketParams(
|
||||
audio_in_enabled=True,
|
||||
audio_out_enabled=True,
|
||||
audio_in_sample_rate=audio_config.transport_in_sample_rate,
|
||||
audio_out_sample_rate=audio_config.transport_out_sample_rate,
|
||||
audio_out_mixer=mixer,
|
||||
serializer=serializer,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def create_vonage_transport(
|
||||
websocket_client,
|
||||
call_uuid: str,
|
||||
workflow_run_id: int,
|
||||
audio_config: AudioConfig,
|
||||
organization_id: int,
|
||||
vad_config: dict | None = None,
|
||||
ambient_noise_config: dict | None = None,
|
||||
):
|
||||
"""Create a transport for Vonage connections"""
|
||||
|
||||
# Use the factory to load config from database
|
||||
from api.services.telephony.factory import load_telephony_config
|
||||
|
||||
config = await load_telephony_config(organization_id)
|
||||
|
||||
if config.get("provider") != "vonage":
|
||||
raise ValueError(f"Expected Vonage provider, got {config.get('provider')}")
|
||||
|
||||
application_id = config.get("application_id")
|
||||
private_key = config.get("private_key")
|
||||
|
||||
if not application_id or not private_key:
|
||||
raise ValueError(
|
||||
f"Incomplete Vonage configuration for organization {organization_id}"
|
||||
)
|
||||
|
||||
serializer = VonageFrameSerializer(
|
||||
call_uuid=call_uuid,
|
||||
application_id=application_id,
|
||||
private_key=private_key,
|
||||
params=VonageFrameSerializer.InputParams(
|
||||
vonage_sample_rate=audio_config.transport_in_sample_rate,
|
||||
sample_rate=audio_config.pipeline_sample_rate,
|
||||
),
|
||||
)
|
||||
|
||||
mixer = await _build_audio_out_mixer(
|
||||
audio_config.transport_out_sample_rate, ambient_noise_config
|
||||
)
|
||||
|
||||
# Important: Vonage uses binary WebSocket mode, not text
|
||||
return FastAPIWebsocketTransport(
|
||||
websocket=websocket_client,
|
||||
params=FastAPIWebsocketParams(
|
||||
audio_in_enabled=True,
|
||||
audio_out_enabled=True,
|
||||
audio_in_sample_rate=audio_config.transport_in_sample_rate,
|
||||
audio_out_sample_rate=audio_config.transport_out_sample_rate,
|
||||
audio_out_mixer=mixer,
|
||||
serializer=serializer,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def create_vobiz_transport(
|
||||
websocket_client: WebSocket,
|
||||
stream_id: str,
|
||||
call_id: str,
|
||||
workflow_run_id: int,
|
||||
audio_config: AudioConfig,
|
||||
organization_id: int,
|
||||
vad_config: dict | None = None,
|
||||
ambient_noise_config: dict | None = None,
|
||||
):
|
||||
"""Create a transport for Vobiz connections.
|
||||
|
||||
Vobiz uses Plivo-compatible WebSocket protocol:
|
||||
- MULAW audio at 8kHz (same as Twilio)
|
||||
- Base64-encoded audio in JSON messages
|
||||
- PlivoFrameSerializer handles the protocol
|
||||
"""
|
||||
from loguru import logger
|
||||
|
||||
logger.info(
|
||||
f"[run {workflow_run_id}] Creating Vobiz transport - "
|
||||
f"stream_id={stream_id}, call_id={call_id}"
|
||||
)
|
||||
|
||||
# Load Vobiz configuration from database
|
||||
from api.services.telephony.factory import load_telephony_config
|
||||
|
||||
config = await load_telephony_config(organization_id)
|
||||
|
||||
if config.get("provider") != "vobiz":
|
||||
raise ValueError(f"Expected Vobiz provider, got {config.get('provider')}")
|
||||
|
||||
auth_id = config.get("auth_id")
|
||||
auth_token = config.get("auth_token")
|
||||
|
||||
if not auth_id or not auth_token:
|
||||
raise ValueError(
|
||||
f"Incomplete Vobiz configuration for organization {organization_id}"
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f"[run {workflow_run_id}] Vobiz config loaded - auth_id={auth_id}, "
|
||||
f"from_numbers={len(config.get('from_numbers', []))} numbers"
|
||||
)
|
||||
|
||||
# Use VobizFrameSerializer for Vobiz WebSocket protocol
|
||||
serializer = VobizFrameSerializer(
|
||||
stream_id=stream_id,
|
||||
call_id=call_id,
|
||||
auth_id=auth_id,
|
||||
auth_token=auth_token,
|
||||
params=VobizFrameSerializer.InputParams(
|
||||
vobiz_sample_rate=8000, # Vobiz uses MULAW at 8kHz
|
||||
sample_rate=audio_config.pipeline_sample_rate,
|
||||
),
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f"[run {workflow_run_id}] VobizFrameSerializer created for Vobiz - "
|
||||
f"transport_rate=8000Hz, pipeline_rate={audio_config.pipeline_sample_rate}Hz"
|
||||
)
|
||||
|
||||
mixer = await _build_audio_out_mixer(
|
||||
audio_config.transport_out_sample_rate, ambient_noise_config
|
||||
)
|
||||
|
||||
# Create WebSocket transport (same structure as Twilio/Vonage)
|
||||
transport = FastAPIWebsocketTransport(
|
||||
websocket=websocket_client,
|
||||
params=FastAPIWebsocketParams(
|
||||
audio_in_enabled=True,
|
||||
audio_out_enabled=True,
|
||||
audio_in_sample_rate=audio_config.transport_in_sample_rate,
|
||||
audio_out_sample_rate=audio_config.transport_out_sample_rate,
|
||||
audio_out_mixer=mixer,
|
||||
serializer=serializer,
|
||||
),
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"[run {workflow_run_id}] Vobiz transport created successfully (VAD enabled)"
|
||||
)
|
||||
return transport
|
||||
|
||||
|
||||
async def create_webrtc_transport(
|
||||
|
|
@ -518,9 +18,8 @@ async def create_webrtc_transport(
|
|||
vad_config: dict | None = None,
|
||||
ambient_noise_config: dict | None = None,
|
||||
):
|
||||
"""Create a transport for WebRTC connections"""
|
||||
|
||||
mixer = await _build_audio_out_mixer(
|
||||
"""Create a transport for WebRTC connections."""
|
||||
mixer = await build_audio_out_mixer(
|
||||
audio_config.transport_out_sample_rate, ambient_noise_config
|
||||
)
|
||||
|
||||
|
|
@ -556,29 +55,3 @@ def create_internal_transport(
|
|||
pass
|
||||
# Commented out because looptalk coming in the regular import flow
|
||||
# was causing issue. May be move this to looptalk/orchestrator.py
|
||||
|
||||
# Create and return the internal transport with latency
|
||||
# return InternalTransport(
|
||||
# params=TransportParams(
|
||||
# audio_out_enabled=True,
|
||||
# audio_out_sample_rate=audio_config.transport_out_sample_rate,
|
||||
# audio_out_channels=1,
|
||||
# audio_in_enabled=True,
|
||||
# audio_in_sample_rate=audio_config.transport_in_sample_rate,
|
||||
# audio_in_channels=1,
|
||||
# audio_out_mixer=(
|
||||
# SoundfileMixer(
|
||||
# sound_files={
|
||||
# "office": APP_ROOT_DIR
|
||||
# / "assets"
|
||||
# / f"office-ambience-{audio_config.transport_out_sample_rate}-mono.wav"
|
||||
# },
|
||||
# default_sound="office",
|
||||
# volume=ambient_noise_config.get("volume", 0.3),
|
||||
# )
|
||||
# if ambient_noise_config and ambient_noise_config.get("enabled", False)
|
||||
# else SilenceAudioMixer()
|
||||
# ),
|
||||
# ),
|
||||
# latency_seconds=latency_seconds,
|
||||
# )
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue