Pre-recorded message example
This commit is contained in:
@@ -8,6 +8,7 @@ import os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from loguru import logger
|
||||
from prerecorded_message_processor import PrerecordedMessageProcessor
|
||||
|
||||
from pipecat.audio.turn.smart_turn.base_smart_turn import SmartTurnParams
|
||||
from pipecat.audio.turn.smart_turn.local_smart_turn_v3 import LocalSmartTurnAnalyzerV3
|
||||
@@ -77,12 +78,18 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
|
||||
context = LLMContext(messages)
|
||||
context_aggregator = LLMContextAggregatorPair(context)
|
||||
|
||||
# Create the prerecorded message processor
|
||||
prerecorded_processor = PrerecordedMessageProcessor(
|
||||
audio_file_path=os.path.join(os.path.dirname(__file__), "assets", "ding1.wav")
|
||||
)
|
||||
|
||||
pipeline = Pipeline(
|
||||
[
|
||||
transport.input(), # Transport user input
|
||||
stt,
|
||||
context_aggregator.user(), # User responses
|
||||
llm, # LLM
|
||||
prerecorded_processor, # Check for prerecorded message trigger
|
||||
tts, # TTS
|
||||
transport.output(), # Transport bot output
|
||||
context_aggregator.assistant(), # Assistant spoken responses
|
||||
|
||||
114
examples/foundational/prerecorded_message_processor.py
Normal file
114
examples/foundational/prerecorded_message_processor.py
Normal file
@@ -0,0 +1,114 @@
|
||||
#
|
||||
# Copyright (c) 2024–2025, Daily
|
||||
#
|
||||
# SPDX-License-Identifier: BSD 2-Clause License
|
||||
#
|
||||
|
||||
"""Prerecorded message processor for playing audio instead of TTS."""
|
||||
|
||||
import wave
|
||||
|
||||
from pipecat.frames.frames import (
|
||||
Frame,
|
||||
LLMFullResponseEndFrame,
|
||||
LLMFullResponseStartFrame,
|
||||
LLMTextFrame,
|
||||
OutputAudioRawFrame,
|
||||
)
|
||||
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
|
||||
|
||||
|
||||
class PrerecordedMessageProcessor(FrameProcessor):
|
||||
"""Processor that intercepts specific LLM text and plays prerecorded audio.
|
||||
|
||||
This processor checks incoming LLMTextFrame instances for a specific text pattern.
|
||||
When the pattern "Your pre-recorded message" is detected, it replaces the text
|
||||
with a prerecorded audio message by pushing LLMFullResponseStartFrame, the audio
|
||||
data as OutputAudioRawFrame, and LLMFullResponseEndFrame. Other frames pass through
|
||||
unchanged.
|
||||
|
||||
Parameters:
|
||||
audio_file_path: Path to the WAV file containing the prerecorded message.
|
||||
|
||||
Example::
|
||||
|
||||
processor = PrerecordedMessageProcessor(
|
||||
audio_file_path="path/to/message.wav"
|
||||
)
|
||||
|
||||
pipeline = Pipeline([
|
||||
transport.input(),
|
||||
stt,
|
||||
context_aggregator.user(),
|
||||
llm,
|
||||
processor, # Insert before TTS
|
||||
tts,
|
||||
transport.output(),
|
||||
context_aggregator.assistant(),
|
||||
])
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
audio_file_path: str,
|
||||
**kwargs,
|
||||
):
|
||||
"""Initialize the prerecorded message processor.
|
||||
|
||||
Args:
|
||||
audio_file_path: Path to the WAV file containing the prerecorded message.
|
||||
**kwargs: Additional arguments passed to FrameProcessor.
|
||||
"""
|
||||
super().__init__(**kwargs)
|
||||
self._audio_file_path = audio_file_path
|
||||
self._audio_data = None
|
||||
self._sample_rate = None
|
||||
self._num_channels = None
|
||||
self._load_audio()
|
||||
|
||||
def _load_audio(self) -> None:
|
||||
"""Load the prerecorded audio file into memory."""
|
||||
try:
|
||||
with wave.open(self._audio_file_path, "rb") as wav_file:
|
||||
self._sample_rate = wav_file.getframerate()
|
||||
self._num_channels = wav_file.getnchannels()
|
||||
self._audio_data = wav_file.readframes(wav_file.getnframes())
|
||||
except Exception as e:
|
||||
raise ValueError(f"Failed to load audio file {self._audio_file_path}: {e}")
|
||||
|
||||
# Ensure audio was loaded successfully
|
||||
if self._audio_data is None or self._sample_rate is None or self._num_channels is None:
|
||||
raise ValueError(f"Failed to load audio data from {self._audio_file_path}")
|
||||
|
||||
async def process_frame(self, frame: Frame, direction: FrameDirection) -> None:
|
||||
"""Process incoming frames and replace specific text with prerecorded audio.
|
||||
|
||||
Args:
|
||||
frame: The frame to process.
|
||||
direction: Direction of the frame flow.
|
||||
"""
|
||||
await super().process_frame(frame, direction)
|
||||
|
||||
# Check if this is an LLMTextFrame with our trigger text
|
||||
if isinstance(frame, LLMTextFrame) and frame.text == "Your pre-recorded message":
|
||||
# Ensure audio data is loaded (should always be true after __init__)
|
||||
if self._audio_data is None or self._sample_rate is None or self._num_channels is None:
|
||||
raise RuntimeError("Audio data not loaded")
|
||||
|
||||
# Push the prerecorded message sequence
|
||||
await self.push_frame(LLMFullResponseStartFrame(), direction)
|
||||
await self.push_frame(frame, direction) # Keep the text frame for context
|
||||
|
||||
# Push the prerecorded audio
|
||||
audio_frame = OutputAudioRawFrame(
|
||||
audio=self._audio_data,
|
||||
sample_rate=self._sample_rate,
|
||||
num_channels=self._num_channels,
|
||||
)
|
||||
await self.push_frame(audio_frame, direction)
|
||||
|
||||
await self.push_frame(LLMFullResponseEndFrame(), direction)
|
||||
else:
|
||||
# Pass through all other frames unchanged
|
||||
await self.push_frame(frame, direction)
|
||||
Reference in New Issue
Block a user