Merge branch 'main' into feat/telnyx-telephony

This commit is contained in:
Abhishek 2026-03-25 16:15:29 +05:30 committed by GitHub
commit 9dc64456d8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 1071 additions and 313 deletions

View file

@ -165,49 +165,39 @@ class RealtimeFeedbackObserver(BaseObserver):
frame = data.frame
frame_direction = data.direction
logger.trace(f"{self} Received Frame: {frame} Direction: {frame_direction}")
# Handle pipeline termination - stop clock task
if isinstance(frame, (EndFrame, CancelFrame, StopFrame)):
await self._cancel_clock_task()
return
# Handle interruptions - clear any queued bot text
if isinstance(frame, InterruptionFrame):
await self._handle_interruption()
return
# Bot speaking state - WS only (ephemeral state signals, not persisted)
if isinstance(frame, BotStartedSpeakingFrame):
await self._send_ws(
{"type": RealtimeFeedbackType.BOT_STARTED_SPEAKING.value, "payload": {}}
)
return
if isinstance(frame, BotStoppedSpeakingFrame):
await self._send_ws(
{"type": RealtimeFeedbackType.BOT_STOPPED_SPEAKING.value, "payload": {}}
)
return
# User mute state - WS only (ephemeral state signals, not persisted)
if isinstance(frame, UserMuteStartedFrame):
await self._send_ws(
{"type": RealtimeFeedbackType.USER_MUTE_STARTED.value, "payload": {}}
)
return
if isinstance(frame, UserMuteStoppedFrame):
await self._send_ws(
{"type": RealtimeFeedbackType.USER_MUTE_STOPPED.value, "payload": {}}
)
return
# Skip already processed frames (frames can be observed multiple times)
if frame.id in self._frames_seen:
return
self._frames_seen.add(frame.id)
logger.trace(f"{self} Received Frame: {frame} Direction: {frame_direction}")
# Handle pipeline termination - stop clock task
if isinstance(frame, (EndFrame, CancelFrame, StopFrame)):
await self._cancel_clock_task()
# Handle interruptions - clear any queued bot text
elif isinstance(frame, InterruptionFrame):
await self._handle_interruption()
# Bot speaking state - WS only (ephemeral state signals, not persisted)
elif isinstance(frame, BotStartedSpeakingFrame):
await self._send_ws(
{"type": RealtimeFeedbackType.BOT_STARTED_SPEAKING.value, "payload": {}}
)
elif isinstance(frame, BotStoppedSpeakingFrame):
await self._send_ws(
{"type": RealtimeFeedbackType.BOT_STOPPED_SPEAKING.value, "payload": {}}
)
# User mute state - WS only (ephemeral state signals, not persisted)
elif isinstance(frame, UserMuteStartedFrame):
await self._send_ws(
{"type": RealtimeFeedbackType.USER_MUTE_STARTED.value, "payload": {}}
)
elif isinstance(frame, UserMuteStoppedFrame):
await self._send_ws(
{"type": RealtimeFeedbackType.USER_MUTE_STOPPED.value, "payload": {}}
)
# Handle user transcriptions (interim) - WebSocket only
if isinstance(frame, InterimTranscriptionFrame):
elif isinstance(frame, InterimTranscriptionFrame):
await self._send_ws(
{
"type": RealtimeFeedbackType.USER_TRANSCRIPTION.value,

View file

@ -66,6 +66,7 @@ class RecordingRouterProcessor(FrameProcessor):
self._frame_buffer: list[tuple[LLMTextFrame, FrameDirection]] = []
self._mode: Optional[str] = None # None = detecting, "tts", "recording"
self._recording_id_buffer = ""
self._recording_playback_started = False
# ------------------------------------------------------------------
# Frame dispatch
@ -99,9 +100,15 @@ class RecordingRouterProcessor(FrameProcessor):
await self.push_frame(frame, direction)
return
# --- Recording mode: accumulate recording_id silently ---
# --- Recording mode: accumulate text and start playback ASAP ---
if self._mode == "recording":
self._recording_id_buffer += frame.text
if not self._recording_playback_started:
buf = self._recording_id_buffer.lstrip()
if " " in buf:
recording_id = buf.split()[0]
self._recording_playback_started = True
await self._play_recording(recording_id)
return
# --- Detection mode: buffer until marker found ---
@ -178,16 +185,21 @@ class RecordingRouterProcessor(FrameProcessor):
self, frame: LLMFullResponseEndFrame, direction: FrameDirection
):
if self._mode == "recording":
recording_id = self._recording_id_buffer.strip()
if recording_id:
# Push accumulated text as TTSTextFrame for UI feedback via observer
full_text = self._recording_id_buffer.strip()
if full_text:
recording_id = full_text.split()[0]
# Push full text (marker + id + transcript) for assistant context
await self.push_frame(
TTSTextFrame(
text=RECORDING_MARKER + self._recording_id_buffer,
aggregated_by="recording_router",
)
)
await self._play_recording(recording_id)
# Fallback: if response ended before a space arrived (no transcript)
if not self._recording_playback_started:
await self._play_recording(recording_id)
else:
logger.warning(
"RecordingRouterProcessor: recording mode but empty recording_id"
@ -256,3 +268,4 @@ class RecordingRouterProcessor(FrameProcessor):
self._frame_buffer = []
self._mode = None
self._recording_id_buffer = ""
self._recording_playback_started = False

View file

@ -8,7 +8,11 @@ from api.services.configuration.registry import ServiceProviders
from pipecat.services.aws.llm import AWSBedrockLLMService, AWSBedrockLLMSettings
from pipecat.services.azure.llm import AzureLLMService, AzureLLMSettings
from pipecat.services.cartesia.stt import CartesiaSTTService
from pipecat.services.cartesia.tts import CartesiaTTSService, CartesiaTTSSettings, GenerationConfig
from pipecat.services.cartesia.tts import (
CartesiaTTSService,
CartesiaTTSSettings,
GenerationConfig,
)
from pipecat.services.deepgram.flux.stt import (
DeepgramFluxSTTService,
DeepgramFluxSTTSettings,
@ -212,13 +216,19 @@ def create_tts_service(user_config, audio_config: "AudioConfig"):
)
elif user_config.tts.provider == ServiceProviders.CARTESIA.value:
speed = getattr(user_config.tts, "speed", None)
generation_config = GenerationConfig(speed=speed) if speed and speed != 1.0 else None
generation_config = (
GenerationConfig(speed=speed) if speed and speed != 1.0 else None
)
return CartesiaTTSService(
api_key=user_config.tts.api_key,
settings=CartesiaTTSSettings(
voice=user_config.tts.voice,
model=user_config.tts.model,
**({"generation_config": generation_config} if generation_config else {}),
**(
{"generation_config": generation_config}
if generation_config
else {}
),
),
text_filters=[xml_function_tag_filter],
silence_time_s=1.0,
@ -353,6 +363,12 @@ def create_llm_service_from_provider(
aws_region=aws_region,
settings=AWSBedrockLLMSettings(model=model),
)
elif provider == ServiceProviders.SELF_HOSTED.value:
return OpenAILLMService(
base_url=base_url or "http://localhost:11434/v1",
api_key=api_key or "none",
settings=OpenAILLMSettings(model=model),
)
else:
raise HTTPException(status_code=400, detail=f"Invalid LLM provider {provider}")
@ -368,6 +384,8 @@ def create_llm_service(user_config):
kwargs["base_url"] = user_config.llm.base_url
elif provider == ServiceProviders.AZURE.value:
kwargs["endpoint"] = user_config.llm.endpoint
elif provider == ServiceProviders.SELF_HOSTED.value:
kwargs["base_url"] = user_config.llm.base_url
elif provider == ServiceProviders.AWS_BEDROCK.value:
kwargs["aws_access_key"] = user_config.llm.aws_access_key
kwargs["aws_secret_key"] = user_config.llm.aws_secret_key