Pre-recorded message example
This commit is contained in:
@@ -8,6 +8,7 @@ import os
|
|||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from loguru import logger
|
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.base_smart_turn import SmartTurnParams
|
||||||
from pipecat.audio.turn.smart_turn.local_smart_turn_v3 import LocalSmartTurnAnalyzerV3
|
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 = LLMContext(messages)
|
||||||
context_aggregator = LLMContextAggregatorPair(context)
|
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(
|
pipeline = Pipeline(
|
||||||
[
|
[
|
||||||
transport.input(), # Transport user input
|
transport.input(), # Transport user input
|
||||||
stt,
|
stt,
|
||||||
context_aggregator.user(), # User responses
|
context_aggregator.user(), # User responses
|
||||||
llm, # LLM
|
llm, # LLM
|
||||||
|
prerecorded_processor, # Check for prerecorded message trigger
|
||||||
tts, # TTS
|
tts, # TTS
|
||||||
transport.output(), # Transport bot output
|
transport.output(), # Transport bot output
|
||||||
context_aggregator.assistant(), # Assistant spoken responses
|
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