introduce PipelineParams audio input/output sample rates
This commit is contained in:
@@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- Added new fields to `PipelineParams` to control audio input and output sample
|
||||||
|
rates for the whole pipeline. This allows controlling sample rates from a
|
||||||
|
single place instead of having to specify sample rates in each
|
||||||
|
service. Setting a sample rate to a service is still possible and will
|
||||||
|
override the value from `PipelineParams`.
|
||||||
|
|
||||||
- Introduce audio resamplers (`BaseAudioResampler`). This is just a base class
|
- Introduce audio resamplers (`BaseAudioResampler`). This is just a base class
|
||||||
to implement audio resamplers. Currently, two implementations are provided
|
to implement audio resamplers. Currently, two implementations are provided
|
||||||
`SOXRAudioResampler` and `ResampyResampler`. A new
|
`SOXRAudioResampler` and `ResampyResampler`. A new
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from runner import configure
|
|||||||
from pipecat.frames.frames import AudioRawFrame, EndFrame, OutputAudioRawFrame, TTSSpeakFrame
|
from pipecat.frames.frames import AudioRawFrame, EndFrame, OutputAudioRawFrame, TTSSpeakFrame
|
||||||
from pipecat.pipeline.pipeline import Pipeline
|
from pipecat.pipeline.pipeline import Pipeline
|
||||||
from pipecat.pipeline.runner import PipelineRunner
|
from pipecat.pipeline.runner import PipelineRunner
|
||||||
from pipecat.pipeline.task import PipelineTask
|
from pipecat.pipeline.task import PipelineParams, PipelineTask
|
||||||
from pipecat.services.cartesia import CartesiaTTSService
|
from pipecat.services.cartesia import CartesiaTTSService
|
||||||
from pipecat.transports.services.daily import DailyParams, DailyTransport
|
from pipecat.transports.services.daily import DailyParams, DailyTransport
|
||||||
|
|
||||||
@@ -31,16 +31,15 @@ logger.add(sys.stderr, level="DEBUG")
|
|||||||
class SilenceFrame(OutputAudioRawFrame):
|
class SilenceFrame(OutputAudioRawFrame):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
audio: bytes = None,
|
*,
|
||||||
sample_rate: int = 16000,
|
sample_rate: int,
|
||||||
num_channels: int = 1,
|
duration: float,
|
||||||
duration: float = 0.1,
|
|
||||||
):
|
):
|
||||||
# Initialize the parent class with the silent frame's data
|
# Initialize the parent class with the silent frame's data
|
||||||
super().__init__(
|
super().__init__(
|
||||||
audio=self.create_silent_audio_frame(sample_rate, num_channels, duration).audio,
|
audio=self.create_silent_audio_frame(sample_rate, 1, duration).audio,
|
||||||
sample_rate=sample_rate,
|
sample_rate=sample_rate,
|
||||||
num_channels=num_channels,
|
num_channels=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -80,7 +79,10 @@ async def main():
|
|||||||
return
|
return
|
||||||
await task.queue_frames(
|
await task.queue_frames(
|
||||||
[
|
[
|
||||||
SilenceFrame(duration=0.5),
|
SilenceFrame(
|
||||||
|
sample_rate=task.params.audio_out_sample_rate,
|
||||||
|
duration=0.5,
|
||||||
|
),
|
||||||
TTSSpeakFrame(f"Hello there, how are you doing today ?"),
|
TTSSpeakFrame(f"Hello there, how are you doing today ?"),
|
||||||
EndFrame(),
|
EndFrame(),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ async def main():
|
|||||||
"Respond bot",
|
"Respond bot",
|
||||||
DailyParams(
|
DailyParams(
|
||||||
audio_out_enabled=True,
|
audio_out_enabled=True,
|
||||||
audio_out_sample_rate=24000,
|
|
||||||
transcription_enabled=True,
|
transcription_enabled=True,
|
||||||
vad_enabled=True,
|
vad_enabled=True,
|
||||||
vad_analyzer=SileroVADAnalyzer(),
|
vad_analyzer=SileroVADAnalyzer(),
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ async def main():
|
|||||||
"Respond bot",
|
"Respond bot",
|
||||||
DailyParams(
|
DailyParams(
|
||||||
audio_out_enabled=True,
|
audio_out_enabled=True,
|
||||||
audio_out_sample_rate=24000,
|
|
||||||
transcription_enabled=True,
|
transcription_enabled=True,
|
||||||
vad_enabled=True,
|
vad_enabled=True,
|
||||||
vad_analyzer=SileroVADAnalyzer(),
|
vad_analyzer=SileroVADAnalyzer(),
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ async def main():
|
|||||||
"Respond bot",
|
"Respond bot",
|
||||||
DailyParams(
|
DailyParams(
|
||||||
audio_out_enabled=True,
|
audio_out_enabled=True,
|
||||||
audio_out_sample_rate=24000,
|
|
||||||
vad_enabled=True,
|
vad_enabled=True,
|
||||||
vad_analyzer=SileroVADAnalyzer(),
|
vad_analyzer=SileroVADAnalyzer(),
|
||||||
vad_audio_passthrough=True,
|
vad_audio_passthrough=True,
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ from pipecat.frames.frames import (
|
|||||||
)
|
)
|
||||||
from pipecat.pipeline.pipeline import Pipeline
|
from pipecat.pipeline.pipeline import Pipeline
|
||||||
from pipecat.pipeline.runner import PipelineRunner
|
from pipecat.pipeline.runner import PipelineRunner
|
||||||
from pipecat.pipeline.task import PipelineTask
|
from pipecat.pipeline.task import PipelineParams, PipelineTask
|
||||||
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
|
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
|
||||||
from pipecat.transports.services.daily import DailyParams, DailyTransport
|
from pipecat.transports.services.daily import DailyParams, DailyTransport
|
||||||
|
|
||||||
@@ -61,7 +61,6 @@ async def main():
|
|||||||
"Test",
|
"Test",
|
||||||
DailyParams(
|
DailyParams(
|
||||||
audio_in_enabled=True,
|
audio_in_enabled=True,
|
||||||
audio_in_sample_rate=24000,
|
|
||||||
audio_out_enabled=True,
|
audio_out_enabled=True,
|
||||||
camera_out_enabled=True,
|
camera_out_enabled=True,
|
||||||
camera_out_is_live=True,
|
camera_out_is_live=True,
|
||||||
@@ -78,7 +77,9 @@ async def main():
|
|||||||
|
|
||||||
runner = PipelineRunner()
|
runner = PipelineRunner()
|
||||||
|
|
||||||
task = PipelineTask(pipeline)
|
task = PipelineTask(
|
||||||
|
pipeline, PipelineParams(audio_in_sample_rate=24000, audio_out_sample_rate=24000)
|
||||||
|
)
|
||||||
|
|
||||||
await runner.run(task)
|
await runner.run(task)
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ from pipecat.frames.frames import (
|
|||||||
)
|
)
|
||||||
from pipecat.pipeline.pipeline import Pipeline
|
from pipecat.pipeline.pipeline import Pipeline
|
||||||
from pipecat.pipeline.runner import PipelineRunner
|
from pipecat.pipeline.runner import PipelineRunner
|
||||||
from pipecat.pipeline.task import PipelineTask
|
from pipecat.pipeline.task import PipelineParams, PipelineTask
|
||||||
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
|
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
|
||||||
from pipecat.transports.base_transport import TransportParams
|
from pipecat.transports.base_transport import TransportParams
|
||||||
from pipecat.transports.local.tk import TkLocalTransport
|
from pipecat.transports.local.tk import TkLocalTransport
|
||||||
@@ -62,7 +62,7 @@ async def main():
|
|||||||
tk_root.title("Local Mirror")
|
tk_root.title("Local Mirror")
|
||||||
|
|
||||||
daily_transport = DailyTransport(
|
daily_transport = DailyTransport(
|
||||||
room_url, token, "Test", DailyParams(audio_in_enabled=True, audio_in_sample_rate=24000)
|
room_url, token, "Test", DailyParams(audio_in_enabled=True)
|
||||||
)
|
)
|
||||||
|
|
||||||
tk_transport = TkLocalTransport(
|
tk_transport = TkLocalTransport(
|
||||||
@@ -82,7 +82,9 @@ async def main():
|
|||||||
|
|
||||||
pipeline = Pipeline([daily_transport.input(), MirrorProcessor(), tk_transport.output()])
|
pipeline = Pipeline([daily_transport.input(), MirrorProcessor(), tk_transport.output()])
|
||||||
|
|
||||||
task = PipelineTask(pipeline)
|
task = PipelineTask(
|
||||||
|
pipeline, PipelineParams(audio_in_sample_rate=24000, audio_out_sample_rate=24000)
|
||||||
|
)
|
||||||
|
|
||||||
async def run_tk():
|
async def run_tk():
|
||||||
while not task.has_finished():
|
while not task.has_finished():
|
||||||
|
|||||||
@@ -51,8 +51,6 @@ async def main():
|
|||||||
out_params=GStreamerPipelineSource.OutputParams(
|
out_params=GStreamerPipelineSource.OutputParams(
|
||||||
video_width=1280,
|
video_width=1280,
|
||||||
video_height=720,
|
video_height=720,
|
||||||
audio_sample_rate=24000,
|
|
||||||
audio_channels=1,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -80,9 +80,7 @@ async def main():
|
|||||||
"Respond bot",
|
"Respond bot",
|
||||||
DailyParams(
|
DailyParams(
|
||||||
audio_in_enabled=True,
|
audio_in_enabled=True,
|
||||||
audio_in_sample_rate=24000,
|
|
||||||
audio_out_enabled=True,
|
audio_out_enabled=True,
|
||||||
audio_out_sample_rate=24000,
|
|
||||||
transcription_enabled=False,
|
transcription_enabled=False,
|
||||||
vad_enabled=True,
|
vad_enabled=True,
|
||||||
vad_analyzer=SileroVADAnalyzer(params=VADParams(stop_secs=0.8)),
|
vad_analyzer=SileroVADAnalyzer(params=VADParams(stop_secs=0.8)),
|
||||||
|
|||||||
@@ -177,9 +177,7 @@ async def main():
|
|||||||
"Respond bot",
|
"Respond bot",
|
||||||
DailyParams(
|
DailyParams(
|
||||||
audio_in_enabled=True,
|
audio_in_enabled=True,
|
||||||
audio_in_sample_rate=24000,
|
|
||||||
audio_out_enabled=True,
|
audio_out_enabled=True,
|
||||||
audio_out_sample_rate=24000,
|
|
||||||
transcription_enabled=False,
|
transcription_enabled=False,
|
||||||
vad_enabled=True,
|
vad_enabled=True,
|
||||||
vad_analyzer=SileroVADAnalyzer(params=VADParams(stop_secs=0.8)),
|
vad_analyzer=SileroVADAnalyzer(params=VADParams(stop_secs=0.8)),
|
||||||
|
|||||||
@@ -88,6 +88,10 @@ async def main():
|
|||||||
task = PipelineTask(
|
task = PipelineTask(
|
||||||
pipeline,
|
pipeline,
|
||||||
PipelineParams(
|
PipelineParams(
|
||||||
|
# We just use 16000 because that's what Tavus is expecting and
|
||||||
|
# we avoid resampling.
|
||||||
|
audio_in_sample_rate=16000,
|
||||||
|
audio_out_sample_rate=16000,
|
||||||
allow_interruptions=True,
|
allow_interruptions=True,
|
||||||
enable_metrics=True,
|
enable_metrics=True,
|
||||||
enable_usage_metrics=True,
|
enable_usage_metrics=True,
|
||||||
|
|||||||
@@ -639,7 +639,6 @@ async def main():
|
|||||||
vad_enabled=True,
|
vad_enabled=True,
|
||||||
vad_analyzer=SileroVADAnalyzer(),
|
vad_analyzer=SileroVADAnalyzer(),
|
||||||
vad_audio_passthrough=True,
|
vad_audio_passthrough=True,
|
||||||
audio_in_sample_rate=16000,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -37,8 +37,6 @@ async def main():
|
|||||||
token,
|
token,
|
||||||
"Respond bot",
|
"Respond bot",
|
||||||
DailyParams(
|
DailyParams(
|
||||||
audio_in_sample_rate=16000,
|
|
||||||
audio_out_sample_rate=24000,
|
|
||||||
audio_out_enabled=True,
|
audio_out_enabled=True,
|
||||||
vad_enabled=True,
|
vad_enabled=True,
|
||||||
vad_audio_passthrough=True,
|
vad_audio_passthrough=True,
|
||||||
|
|||||||
@@ -37,8 +37,6 @@ async def main():
|
|||||||
token,
|
token,
|
||||||
"Respond bot",
|
"Respond bot",
|
||||||
DailyParams(
|
DailyParams(
|
||||||
audio_in_sample_rate=16000,
|
|
||||||
audio_out_sample_rate=24000,
|
|
||||||
audio_out_enabled=True,
|
audio_out_enabled=True,
|
||||||
vad_enabled=True,
|
vad_enabled=True,
|
||||||
vad_audio_passthrough=True,
|
vad_audio_passthrough=True,
|
||||||
|
|||||||
@@ -84,8 +84,6 @@ async def main():
|
|||||||
token,
|
token,
|
||||||
"Respond bot",
|
"Respond bot",
|
||||||
DailyParams(
|
DailyParams(
|
||||||
audio_in_sample_rate=16000,
|
|
||||||
audio_out_sample_rate=24000,
|
|
||||||
audio_out_enabled=True,
|
audio_out_enabled=True,
|
||||||
vad_enabled=True,
|
vad_enabled=True,
|
||||||
vad_audio_passthrough=True,
|
vad_audio_passthrough=True,
|
||||||
|
|||||||
@@ -37,8 +37,6 @@ async def main():
|
|||||||
token,
|
token,
|
||||||
"Respond bot",
|
"Respond bot",
|
||||||
DailyParams(
|
DailyParams(
|
||||||
audio_in_sample_rate=16000,
|
|
||||||
audio_out_sample_rate=24000,
|
|
||||||
audio_out_enabled=True,
|
audio_out_enabled=True,
|
||||||
vad_enabled=True,
|
vad_enabled=True,
|
||||||
vad_audio_passthrough=True,
|
vad_audio_passthrough=True,
|
||||||
@@ -47,8 +45,6 @@ async def main():
|
|||||||
# matter because we can only use the Multimodal Live API's phrase
|
# matter because we can only use the Multimodal Live API's phrase
|
||||||
# endpointing, for now.
|
# endpointing, for now.
|
||||||
vad_analyzer=SileroVADAnalyzer(params=VADParams(stop_secs=0.5)),
|
vad_analyzer=SileroVADAnalyzer(params=VADParams(stop_secs=0.5)),
|
||||||
start_audio_paused=True,
|
|
||||||
start_video_paused=True,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -52,8 +52,6 @@ async def main():
|
|||||||
token,
|
token,
|
||||||
"Respond bot",
|
"Respond bot",
|
||||||
DailyParams(
|
DailyParams(
|
||||||
audio_in_sample_rate=16000,
|
|
||||||
audio_out_sample_rate=24000,
|
|
||||||
audio_out_enabled=True,
|
audio_out_enabled=True,
|
||||||
vad_enabled=True,
|
vad_enabled=True,
|
||||||
vad_audio_passthrough=True,
|
vad_audio_passthrough=True,
|
||||||
|
|||||||
@@ -38,8 +38,6 @@ load_dotenv(override=True)
|
|||||||
logger.remove(0)
|
logger.remove(0)
|
||||||
logger.add(sys.stderr, level="DEBUG")
|
logger.add(sys.stderr, level="DEBUG")
|
||||||
|
|
||||||
DESIRED_SAMPLE_RATE = 16000
|
|
||||||
|
|
||||||
|
|
||||||
def generate_token(room_name: str, participant_name: str, api_key: str, api_secret: str) -> str:
|
def generate_token(room_name: str, participant_name: str, api_key: str, api_secret: str) -> str:
|
||||||
token = api.AccessToken(api_key, api_secret)
|
token = api.AccessToken(api_key, api_secret)
|
||||||
@@ -114,11 +112,8 @@ async def main():
|
|||||||
token=token,
|
token=token,
|
||||||
room_name=room_name,
|
room_name=room_name,
|
||||||
params=LiveKitParams(
|
params=LiveKitParams(
|
||||||
audio_in_channels=1,
|
|
||||||
audio_in_enabled=True,
|
audio_in_enabled=True,
|
||||||
audio_out_enabled=True,
|
audio_out_enabled=True,
|
||||||
audio_in_sample_rate=DESIRED_SAMPLE_RATE,
|
|
||||||
audio_out_sample_rate=DESIRED_SAMPLE_RATE,
|
|
||||||
vad_analyzer=SileroVADAnalyzer(),
|
vad_analyzer=SileroVADAnalyzer(),
|
||||||
vad_enabled=True,
|
vad_enabled=True,
|
||||||
vad_audio_passthrough=True,
|
vad_audio_passthrough=True,
|
||||||
@@ -128,7 +123,6 @@ async def main():
|
|||||||
stt = DeepgramSTTService(
|
stt = DeepgramSTTService(
|
||||||
api_key=os.getenv("DEEPGRAM_API_KEY"),
|
api_key=os.getenv("DEEPGRAM_API_KEY"),
|
||||||
live_options=LiveOptions(
|
live_options=LiveOptions(
|
||||||
sample_rate=DESIRED_SAMPLE_RATE,
|
|
||||||
vad_events=True,
|
vad_events=True,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -138,7 +132,6 @@ async def main():
|
|||||||
tts = CartesiaTTSService(
|
tts = CartesiaTTSService(
|
||||||
api_key=os.getenv("CARTESIA_API_KEY"),
|
api_key=os.getenv("CARTESIA_API_KEY"),
|
||||||
voice_id="79a125e8-cd45-4c13-8a67-188112f4dd22", # British Lady
|
voice_id="79a125e8-cd45-4c13-8a67-188112f4dd22", # British Lady
|
||||||
sample_rate=DESIRED_SAMPLE_RATE,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
messages = [
|
messages = [
|
||||||
|
|||||||
@@ -121,8 +121,6 @@ async def main():
|
|||||||
token,
|
token,
|
||||||
"Chatbot",
|
"Chatbot",
|
||||||
DailyParams(
|
DailyParams(
|
||||||
audio_in_sample_rate=16000,
|
|
||||||
audio_out_sample_rate=24000,
|
|
||||||
audio_out_enabled=True,
|
audio_out_enabled=True,
|
||||||
camera_out_enabled=True,
|
camera_out_enabled=True,
|
||||||
camera_out_width=1024,
|
camera_out_width=1024,
|
||||||
|
|||||||
@@ -112,7 +112,6 @@ async def main():
|
|||||||
token,
|
token,
|
||||||
"studypal",
|
"studypal",
|
||||||
DailyParams(
|
DailyParams(
|
||||||
audio_out_sample_rate=44100,
|
|
||||||
audio_out_enabled=True,
|
audio_out_enabled=True,
|
||||||
transcription_enabled=True,
|
transcription_enabled=True,
|
||||||
vad_enabled=True,
|
vad_enabled=True,
|
||||||
@@ -124,7 +123,6 @@ async def main():
|
|||||||
api_key=os.getenv("CARTESIA_API_KEY"),
|
api_key=os.getenv("CARTESIA_API_KEY"),
|
||||||
voice_id=os.getenv("CARTESIA_VOICE_ID", "4d2fd738-3b3d-4368-957a-bb4805275bd9"),
|
voice_id=os.getenv("CARTESIA_VOICE_ID", "4d2fd738-3b3d-4368-957a-bb4805275bd9"),
|
||||||
# British Narration Lady: 4d2fd738-3b3d-4368-957a-bb4805275bd9
|
# British Narration Lady: 4d2fd738-3b3d-4368-957a-bb4805275bd9
|
||||||
sample_rate=44100,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"), model="gpt-4o-mini")
|
llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"), model="gpt-4o-mini")
|
||||||
@@ -155,7 +153,12 @@ Your task is to help the user understand and learn from this article in 2 senten
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
task = PipelineTask(pipeline, PipelineParams(allow_interruptions=True, enable_metrics=True))
|
task = PipelineTask(
|
||||||
|
pipeline,
|
||||||
|
PipelineParams(
|
||||||
|
audio_out_sample_rate=44100, allow_interruptions=True, enable_metrics=True
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
@transport.event_handler("on_first_participant_joined")
|
@transport.event_handler("on_first_participant_joined")
|
||||||
async def on_first_participant_joined(transport, participant):
|
async def on_first_participant_joined(transport, participant):
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import sys
|
|||||||
import wave
|
import wave
|
||||||
|
|
||||||
import aiofiles
|
import aiofiles
|
||||||
from deepgram import LiveOptions
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from fastapi import WebSocket
|
from fastapi import WebSocket
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@@ -36,8 +35,6 @@ load_dotenv(override=True)
|
|||||||
logger.remove(0)
|
logger.remove(0)
|
||||||
logger.add(sys.stderr, level="DEBUG")
|
logger.add(sys.stderr, level="DEBUG")
|
||||||
|
|
||||||
SAMPLE_RATE = 8000
|
|
||||||
|
|
||||||
|
|
||||||
async def save_audio(server_name: str, audio: bytes, sample_rate: int, num_channels: int):
|
async def save_audio(server_name: str, audio: bytes, sample_rate: int, num_channels: int):
|
||||||
if len(audio) > 0:
|
if len(audio) > 0:
|
||||||
@@ -63,29 +60,21 @@ async def run_bot(websocket_client: WebSocket, stream_sid: str, testing: bool):
|
|||||||
params=FastAPIWebsocketParams(
|
params=FastAPIWebsocketParams(
|
||||||
audio_in_enabled=True,
|
audio_in_enabled=True,
|
||||||
audio_out_enabled=True,
|
audio_out_enabled=True,
|
||||||
audio_out_sample_rate=SAMPLE_RATE,
|
|
||||||
add_wav_header=False,
|
add_wav_header=False,
|
||||||
vad_enabled=True,
|
vad_enabled=True,
|
||||||
vad_analyzer=SileroVADAnalyzer(sample_rate=SAMPLE_RATE),
|
vad_analyzer=SileroVADAnalyzer(),
|
||||||
vad_audio_passthrough=True,
|
vad_audio_passthrough=True,
|
||||||
serializer=TwilioFrameSerializer(
|
serializer=TwilioFrameSerializer(stream_sid),
|
||||||
stream_sid, TwilioFrameSerializer.InputParams(sample_rate=SAMPLE_RATE)
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"), model="gpt-4o")
|
llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"), model="gpt-4o")
|
||||||
|
|
||||||
stt = DeepgramSTTService(
|
stt = DeepgramSTTService(api_key=os.getenv("DEEPGRAM_API_KEY"), audio_passthrough=True)
|
||||||
api_key=os.getenv("DEEPGRAM_API_KEY"),
|
|
||||||
live_options=LiveOptions(sample_rate=SAMPLE_RATE),
|
|
||||||
audio_passthrough=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
tts = CartesiaTTSService(
|
tts = CartesiaTTSService(
|
||||||
api_key=os.getenv("CARTESIA_API_KEY"),
|
api_key=os.getenv("CARTESIA_API_KEY"),
|
||||||
voice_id="79a125e8-cd45-4c13-8a67-188112f4dd22", # British Lady
|
voice_id="79a125e8-cd45-4c13-8a67-188112f4dd22", # British Lady
|
||||||
sample_rate=SAMPLE_RATE,
|
|
||||||
push_silence_after_stop=testing,
|
push_silence_after_stop=testing,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -101,7 +90,7 @@ async def run_bot(websocket_client: WebSocket, stream_sid: str, testing: bool):
|
|||||||
|
|
||||||
# NOTE: Watch out! This will save all the conversation in memory. You can
|
# NOTE: Watch out! This will save all the conversation in memory. You can
|
||||||
# pass `buffer_size` to get periodic callbacks.
|
# pass `buffer_size` to get periodic callbacks.
|
||||||
audiobuffer = AudioBufferProcessor(sample_rate=SAMPLE_RATE)
|
audiobuffer = AudioBufferProcessor()
|
||||||
|
|
||||||
pipeline = Pipeline(
|
pipeline = Pipeline(
|
||||||
[
|
[
|
||||||
@@ -116,7 +105,12 @@ async def run_bot(websocket_client: WebSocket, stream_sid: str, testing: bool):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
task = PipelineTask(pipeline, params=PipelineParams(allow_interruptions=True))
|
task = PipelineTask(
|
||||||
|
pipeline,
|
||||||
|
params=PipelineParams(
|
||||||
|
audio_in_sample_rate=8000, audio_out_sample_rate=8000, allow_interruptions=True
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
@transport.event_handler("on_client_connected")
|
@transport.event_handler("on_client_connected")
|
||||||
async def on_client_connected(transport, client):
|
async def on_client_connected(transport, client):
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ from uuid import uuid4
|
|||||||
|
|
||||||
import aiofiles
|
import aiofiles
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from deepgram import LiveOptions
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
@@ -44,7 +43,6 @@ logger.add(sys.stderr, level="DEBUG")
|
|||||||
|
|
||||||
|
|
||||||
DEFAULT_CLIENT_DURATION = 30
|
DEFAULT_CLIENT_DURATION = 30
|
||||||
SAMPLE_RATE = 8000
|
|
||||||
|
|
||||||
|
|
||||||
async def download_twiml(server_url: str) -> str:
|
async def download_twiml(server_url: str) -> str:
|
||||||
@@ -92,15 +90,10 @@ async def run_client(client_name: str, server_url: str, duration_secs: int):
|
|||||||
params=WebsocketClientParams(
|
params=WebsocketClientParams(
|
||||||
audio_in_enabled=True,
|
audio_in_enabled=True,
|
||||||
audio_out_enabled=True,
|
audio_out_enabled=True,
|
||||||
audio_out_sample_rate=SAMPLE_RATE,
|
|
||||||
add_wav_header=False,
|
add_wav_header=False,
|
||||||
serializer=TwilioFrameSerializer(
|
serializer=TwilioFrameSerializer(stream_sid),
|
||||||
stream_sid, params=TwilioFrameSerializer.InputParams(sample_rate=SAMPLE_RATE)
|
|
||||||
),
|
|
||||||
vad_enabled=True,
|
vad_enabled=True,
|
||||||
vad_analyzer=SileroVADAnalyzer(
|
vad_analyzer=SileroVADAnalyzer(params=VADParams(stop_secs=1.5)),
|
||||||
params=VADParams(stop_secs=1.5), sample_rate=SAMPLE_RATE
|
|
||||||
),
|
|
||||||
vad_audio_passthrough=True,
|
vad_audio_passthrough=True,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -110,14 +103,12 @@ async def run_client(client_name: str, server_url: str, duration_secs: int):
|
|||||||
# We let the audio passthrough so we can record the conversation.
|
# We let the audio passthrough so we can record the conversation.
|
||||||
stt = DeepgramSTTService(
|
stt = DeepgramSTTService(
|
||||||
api_key=os.getenv("DEEPGRAM_API_KEY"),
|
api_key=os.getenv("DEEPGRAM_API_KEY"),
|
||||||
live_options=LiveOptions(sample_rate=SAMPLE_RATE),
|
|
||||||
audio_passthrough=True,
|
audio_passthrough=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
tts = CartesiaTTSService(
|
tts = CartesiaTTSService(
|
||||||
api_key=os.getenv("CARTESIA_API_KEY"),
|
api_key=os.getenv("CARTESIA_API_KEY"),
|
||||||
voice_id="e13cae5c-ec59-4f71-b0a6-266df3c9bb8e", # Madame Mischief
|
voice_id="e13cae5c-ec59-4f71-b0a6-266df3c9bb8e", # Madame Mischief
|
||||||
sample_rate=SAMPLE_RATE,
|
|
||||||
push_silence_after_stop=True,
|
push_silence_after_stop=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -133,7 +124,7 @@ async def run_client(client_name: str, server_url: str, duration_secs: int):
|
|||||||
|
|
||||||
# NOTE: Watch out! This will save all the conversation in memory. You can
|
# NOTE: Watch out! This will save all the conversation in memory. You can
|
||||||
# pass `buffer_size` to get periodic callbacks.
|
# pass `buffer_size` to get periodic callbacks.
|
||||||
audiobuffer = AudioBufferProcessor(sample_rate=SAMPLE_RATE)
|
audiobuffer = AudioBufferProcessor()
|
||||||
|
|
||||||
pipeline = Pipeline(
|
pipeline = Pipeline(
|
||||||
[
|
[
|
||||||
@@ -148,7 +139,12 @@ async def run_client(client_name: str, server_url: str, duration_secs: int):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
task = PipelineTask(pipeline, params=PipelineParams(allow_interruptions=True))
|
task = PipelineTask(
|
||||||
|
pipeline,
|
||||||
|
params=PipelineParams(
|
||||||
|
audio_in_sample_rate=8000, audio_out_sample_rate=8000, allow_interruptions=True
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
@transport.event_handler("on_connected")
|
@transport.event_handler("on_connected")
|
||||||
async def on_connected(transport: WebsocketClientTransport, client):
|
async def on_connected(transport: WebsocketClientTransport, client):
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from pipecat.pipeline.pipeline import Pipeline
|
|||||||
from pipecat.pipeline.runner import PipelineRunner
|
from pipecat.pipeline.runner import PipelineRunner
|
||||||
from pipecat.pipeline.task import PipelineParams, PipelineTask
|
from pipecat.pipeline.task import PipelineParams, PipelineTask
|
||||||
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
|
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
|
||||||
|
from pipecat.serializers.protobuf import ProtobufFrameSerializer
|
||||||
from pipecat.services.cartesia import CartesiaTTSService
|
from pipecat.services.cartesia import CartesiaTTSService
|
||||||
from pipecat.services.deepgram import DeepgramSTTService
|
from pipecat.services.deepgram import DeepgramSTTService
|
||||||
from pipecat.services.openai import OpenAILLMService
|
from pipecat.services.openai import OpenAILLMService
|
||||||
@@ -80,7 +81,7 @@ class SessionTimeoutHandler:
|
|||||||
async def main():
|
async def main():
|
||||||
transport = WebsocketServerTransport(
|
transport = WebsocketServerTransport(
|
||||||
params=WebsocketServerParams(
|
params=WebsocketServerParams(
|
||||||
audio_out_sample_rate=16000,
|
serializer=ProtobufFrameSerializer(),
|
||||||
audio_out_enabled=True,
|
audio_out_enabled=True,
|
||||||
add_wav_header=True,
|
add_wav_header=True,
|
||||||
vad_enabled=True,
|
vad_enabled=True,
|
||||||
@@ -97,7 +98,6 @@ async def main():
|
|||||||
tts = CartesiaTTSService(
|
tts = CartesiaTTSService(
|
||||||
api_key=os.getenv("CARTESIA_API_KEY"),
|
api_key=os.getenv("CARTESIA_API_KEY"),
|
||||||
voice_id="79a125e8-cd45-4c13-8a67-188112f4dd22", # British Lady
|
voice_id="79a125e8-cd45-4c13-8a67-188112f4dd22", # British Lady
|
||||||
sample_rate=16000,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
messages = [
|
messages = [
|
||||||
@@ -122,7 +122,12 @@ async def main():
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
task = PipelineTask(pipeline, params=PipelineParams(allow_interruptions=True))
|
task = PipelineTask(
|
||||||
|
pipeline,
|
||||||
|
params=PipelineParams(
|
||||||
|
audio_in_sample_rate=16000, audio_out_sample_rate=16000, allow_interruptions=True
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
@transport.event_handler("on_client_connected")
|
@transport.event_handler("on_client_connected")
|
||||||
async def on_client_connected(transport, client):
|
async def on_client_connected(transport, client):
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
import time
|
import time
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@@ -104,11 +105,8 @@ class SileroOnnxModel:
|
|||||||
|
|
||||||
|
|
||||||
class SileroVADAnalyzer(VADAnalyzer):
|
class SileroVADAnalyzer(VADAnalyzer):
|
||||||
def __init__(self, *, sample_rate: int = 16000, params: VADParams = VADParams()):
|
def __init__(self, *, sample_rate: Optional[int] = None, params: VADParams = VADParams()):
|
||||||
super().__init__(sample_rate=sample_rate, num_channels=1, params=params)
|
super().__init__(sample_rate=sample_rate, params=params)
|
||||||
|
|
||||||
if sample_rate != 16000 and sample_rate != 8000:
|
|
||||||
raise ValueError("Silero VAD sample rate needs to be 16000 or 8000")
|
|
||||||
|
|
||||||
logger.debug("Loading Silero VAD model...")
|
logger.debug("Loading Silero VAD model...")
|
||||||
|
|
||||||
@@ -138,6 +136,12 @@ class SileroVADAnalyzer(VADAnalyzer):
|
|||||||
# VADAnalyzer
|
# VADAnalyzer
|
||||||
#
|
#
|
||||||
|
|
||||||
|
def set_sample_rate(self, sample_rate: int):
|
||||||
|
if sample_rate != 16000 and sample_rate != 8000:
|
||||||
|
raise ValueError("Silero VAD sample rate needs to be 16000 or 8000")
|
||||||
|
|
||||||
|
super().set_sample_rate(sample_rate)
|
||||||
|
|
||||||
def num_frames_required(self) -> int:
|
def num_frames_required(self) -> int:
|
||||||
return 512 if self.sample_rate == 16000 else 256
|
return 512 if self.sample_rate == 16000 else 256
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
@@ -33,11 +34,11 @@ class VADParams(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class VADAnalyzer:
|
class VADAnalyzer:
|
||||||
def __init__(self, *, sample_rate: int, num_channels: int, params: VADParams):
|
def __init__(self, *, sample_rate: Optional[int] = None, params: VADParams):
|
||||||
self._sample_rate = sample_rate
|
self._init_sample_rate = sample_rate
|
||||||
self._num_channels = num_channels
|
self._sample_rate = 0
|
||||||
|
self._params = params
|
||||||
self.set_params(params)
|
self._num_channels = 1
|
||||||
|
|
||||||
self._vad_buffer = b""
|
self._vad_buffer = b""
|
||||||
|
|
||||||
@@ -65,13 +66,17 @@ class VADAnalyzer:
|
|||||||
def voice_confidence(self, buffer) -> float:
|
def voice_confidence(self, buffer) -> float:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def set_sample_rate(self, sample_rate: int):
|
||||||
|
self._sample_rate = self._init_sample_rate or sample_rate
|
||||||
|
self.set_params(self._params)
|
||||||
|
|
||||||
def set_params(self, params: VADParams):
|
def set_params(self, params: VADParams):
|
||||||
logger.info(f"Setting VAD params to: {params}")
|
logger.info(f"Setting VAD params to: {params}")
|
||||||
self._params = params
|
self._params = params
|
||||||
self._vad_frames = self.num_frames_required()
|
self._vad_frames = self.num_frames_required()
|
||||||
self._vad_frames_num_bytes = self._vad_frames * self._num_channels * 2
|
self._vad_frames_num_bytes = self._vad_frames * self._num_channels * 2
|
||||||
|
|
||||||
vad_frames_per_sec = self._vad_frames / self._sample_rate
|
vad_frames_per_sec = self._vad_frames / self.sample_rate
|
||||||
|
|
||||||
self._vad_start_frames = round(self._params.start_secs / vad_frames_per_sec)
|
self._vad_start_frames = round(self._params.start_secs / vad_frames_per_sec)
|
||||||
self._vad_stop_frames = round(self._params.stop_secs / vad_frames_per_sec)
|
self._vad_stop_frames = round(self._params.stop_secs / vad_frames_per_sec)
|
||||||
@@ -80,7 +85,7 @@ class VADAnalyzer:
|
|||||||
self._vad_state: VADState = VADState.QUIET
|
self._vad_state: VADState = VADState.QUIET
|
||||||
|
|
||||||
def _get_smoothed_volume(self, audio: bytes) -> float:
|
def _get_smoothed_volume(self, audio: bytes) -> float:
|
||||||
volume = calculate_audio_volume(audio, self._sample_rate)
|
volume = calculate_audio_volume(audio, self.sample_rate)
|
||||||
return exp_smoothing(volume, self._prev_volume, self._smoothing_factor)
|
return exp_smoothing(volume, self._prev_volume, self._smoothing_factor)
|
||||||
|
|
||||||
def analyze_audio(self, buffer) -> VADState:
|
def analyze_audio(self, buffer) -> VADState:
|
||||||
|
|||||||
@@ -428,6 +428,8 @@ class StartFrame(SystemFrame):
|
|||||||
|
|
||||||
clock: BaseClock
|
clock: BaseClock
|
||||||
task_manager: TaskManager
|
task_manager: TaskManager
|
||||||
|
audio_in_sample_rate: int = 16000
|
||||||
|
audio_out_sample_rate: int = 24000
|
||||||
allow_interruptions: bool = False
|
allow_interruptions: bool = False
|
||||||
enable_metrics: bool = False
|
enable_metrics: bool = False
|
||||||
enable_usage_metrics: bool = False
|
enable_usage_metrics: bool = False
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ HEARTBEAT_MONITOR_SECONDS = HEARTBEAT_SECONDS * 5
|
|||||||
class PipelineParams(BaseModel):
|
class PipelineParams(BaseModel):
|
||||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||||
|
|
||||||
|
audio_in_sample_rate: int = 16000
|
||||||
|
audio_out_sample_rate: int = 24000
|
||||||
allow_interruptions: bool = False
|
allow_interruptions: bool = False
|
||||||
enable_heartbeats: bool = False
|
enable_heartbeats: bool = False
|
||||||
enable_metrics: bool = False
|
enable_metrics: bool = False
|
||||||
@@ -136,6 +138,11 @@ class PipelineTask(BaseTask):
|
|||||||
"""Returns the name of this task."""
|
"""Returns the name of this task."""
|
||||||
return self._name
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def params(self) -> PipelineParams:
|
||||||
|
"""Returns the pipeline parameters of this task."""
|
||||||
|
return self._params
|
||||||
|
|
||||||
def set_event_loop(self, loop: asyncio.AbstractEventLoop):
|
def set_event_loop(self, loop: asyncio.AbstractEventLoop):
|
||||||
self._task_manager.set_event_loop(loop)
|
self._task_manager.set_event_loop(loop)
|
||||||
|
|
||||||
@@ -275,6 +282,8 @@ class PipelineTask(BaseTask):
|
|||||||
enable_usage_metrics=self._params.enable_usage_metrics,
|
enable_usage_metrics=self._params.enable_usage_metrics,
|
||||||
report_only_initial_ttfb=self._params.report_only_initial_ttfb,
|
report_only_initial_ttfb=self._params.report_only_initial_ttfb,
|
||||||
observer=self._observer,
|
observer=self._observer,
|
||||||
|
audio_in_sample_rate=self._params.audio_in_sample_rate,
|
||||||
|
audio_out_sample_rate=self._params.audio_out_sample_rate,
|
||||||
)
|
)
|
||||||
await self._source.queue_frame(start_frame, FrameDirection.DOWNSTREAM)
|
await self._source.queue_frame(start_frame, FrameDirection.DOWNSTREAM)
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
import time
|
import time
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from pipecat.audio.utils import create_default_resampler, interleave_stereo_audio, mix_audio
|
from pipecat.audio.utils import create_default_resampler, interleave_stereo_audio, mix_audio
|
||||||
from pipecat.frames.frames import (
|
from pipecat.frames.frames import (
|
||||||
@@ -14,6 +15,7 @@ from pipecat.frames.frames import (
|
|||||||
Frame,
|
Frame,
|
||||||
InputAudioRawFrame,
|
InputAudioRawFrame,
|
||||||
OutputAudioRawFrame,
|
OutputAudioRawFrame,
|
||||||
|
StartFrame,
|
||||||
)
|
)
|
||||||
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
|
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
|
||||||
|
|
||||||
@@ -33,10 +35,16 @@ class AudioBufferProcessor(FrameProcessor):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, *, sample_rate: int = 24000, num_channels: int = 1, buffer_size: int = 0, **kwargs
|
self,
|
||||||
|
*,
|
||||||
|
sample_rate: Optional[int] = None,
|
||||||
|
num_channels: int = 1,
|
||||||
|
buffer_size: int = 0,
|
||||||
|
**kwargs,
|
||||||
):
|
):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self._sample_rate = sample_rate
|
self._init_sample_rate = sample_rate
|
||||||
|
self._sample_rate = 0
|
||||||
self._num_channels = num_channels
|
self._num_channels = num_channels
|
||||||
self._buffer_size = buffer_size
|
self._buffer_size = buffer_size
|
||||||
|
|
||||||
@@ -86,6 +94,10 @@ class AudioBufferProcessor(FrameProcessor):
|
|||||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||||
await super().process_frame(frame, direction)
|
await super().process_frame(frame, direction)
|
||||||
|
|
||||||
|
# Update output sample rate if necessary.
|
||||||
|
if isinstance(frame, StartFrame):
|
||||||
|
self._update_sample_rate(frame)
|
||||||
|
|
||||||
if self._recording and isinstance(frame, InputAudioRawFrame):
|
if self._recording and isinstance(frame, InputAudioRawFrame):
|
||||||
# Add silence if we need to.
|
# Add silence if we need to.
|
||||||
silence = self._compute_silence(self._last_user_frame_at)
|
silence = self._compute_silence(self._last_user_frame_at)
|
||||||
@@ -113,6 +125,9 @@ class AudioBufferProcessor(FrameProcessor):
|
|||||||
|
|
||||||
await self.push_frame(frame, direction)
|
await self.push_frame(frame, direction)
|
||||||
|
|
||||||
|
def _update_sample_rate(self, frame: StartFrame):
|
||||||
|
self._sample_rate = self._init_sample_rate or frame.audio_out_sample_rate
|
||||||
|
|
||||||
async def _call_on_audio_data_handler(self):
|
async def _call_on_audio_data_handler(self):
|
||||||
if not self.has_audio() or not self._recording:
|
if not self.has_audio() or not self._recording:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from pipecat.audio.vad.silero import SileroVADAnalyzer
|
from pipecat.audio.vad.silero import SileroVADAnalyzer
|
||||||
@@ -11,6 +13,7 @@ from pipecat.audio.vad.vad_analyzer import VADParams, VADState
|
|||||||
from pipecat.frames.frames import (
|
from pipecat.frames.frames import (
|
||||||
AudioRawFrame,
|
AudioRawFrame,
|
||||||
Frame,
|
Frame,
|
||||||
|
StartFrame,
|
||||||
StartInterruptionFrame,
|
StartInterruptionFrame,
|
||||||
StopInterruptionFrame,
|
StopInterruptionFrame,
|
||||||
UserStartedSpeakingFrame,
|
UserStartedSpeakingFrame,
|
||||||
@@ -23,7 +26,7 @@ class SileroVAD(FrameProcessor):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
sample_rate: int = 16000,
|
sample_rate: Optional[int] = None,
|
||||||
vad_params: VADParams = VADParams(),
|
vad_params: VADParams = VADParams(),
|
||||||
audio_passthrough: bool = False,
|
audio_passthrough: bool = False,
|
||||||
):
|
):
|
||||||
@@ -41,6 +44,9 @@ class SileroVAD(FrameProcessor):
|
|||||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||||
await super().process_frame(frame, direction)
|
await super().process_frame(frame, direction)
|
||||||
|
|
||||||
|
if isinstance(frame, StartFrame):
|
||||||
|
self._vad_analyzer.set_sample_rate(frame.audio_in_sample_rate)
|
||||||
|
|
||||||
if isinstance(frame, AudioRawFrame):
|
if isinstance(frame, AudioRawFrame):
|
||||||
await self._analyze_audio(frame)
|
await self._analyze_audio(frame)
|
||||||
if self._audio_passthrough:
|
if self._audio_passthrough:
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
@@ -38,7 +39,7 @@ class GStreamerPipelineSource(FrameProcessor):
|
|||||||
class OutputParams(BaseModel):
|
class OutputParams(BaseModel):
|
||||||
video_width: int = 1280
|
video_width: int = 1280
|
||||||
video_height: int = 720
|
video_height: int = 720
|
||||||
audio_sample_rate: int = 24000
|
audio_sample_rate: Optional[int] = None
|
||||||
audio_channels: int = 1
|
audio_channels: int = 1
|
||||||
clock_sync: bool = True
|
clock_sync: bool = True
|
||||||
|
|
||||||
@@ -46,6 +47,7 @@ class GStreamerPipelineSource(FrameProcessor):
|
|||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
self._out_params = out_params
|
self._out_params = out_params
|
||||||
|
self._sample_rate = 0
|
||||||
|
|
||||||
Gst.init()
|
Gst.init()
|
||||||
|
|
||||||
@@ -90,6 +92,7 @@ class GStreamerPipelineSource(FrameProcessor):
|
|||||||
await self.push_frame(frame, direction)
|
await self.push_frame(frame, direction)
|
||||||
|
|
||||||
async def _start(self, frame: StartFrame):
|
async def _start(self, frame: StartFrame):
|
||||||
|
self._sample_rate = self._out_params.audio_sample_rate or frame.audio_out_sample_rate
|
||||||
self._player.set_state(Gst.State.PLAYING)
|
self._player.set_state(Gst.State.PLAYING)
|
||||||
|
|
||||||
async def _stop(self, frame: EndFrame):
|
async def _stop(self, frame: EndFrame):
|
||||||
@@ -122,7 +125,7 @@ class GStreamerPipelineSource(FrameProcessor):
|
|||||||
audioresample = Gst.ElementFactory.make("audioresample", None)
|
audioresample = Gst.ElementFactory.make("audioresample", None)
|
||||||
audiocapsfilter = Gst.ElementFactory.make("capsfilter", None)
|
audiocapsfilter = Gst.ElementFactory.make("capsfilter", None)
|
||||||
audiocaps = Gst.Caps.from_string(
|
audiocaps = Gst.Caps.from_string(
|
||||||
f"audio/x-raw,format=S16LE,rate={self._out_params.audio_sample_rate},channels={self._out_params.audio_channels},layout=interleaved"
|
f"audio/x-raw,format=S16LE,rate={self._sample_rate},channels={self._out_params.audio_channels},layout=interleaved"
|
||||||
)
|
)
|
||||||
audiocapsfilter.set_property("caps", audiocaps)
|
audiocapsfilter.set_property("caps", audiocaps)
|
||||||
appsink_audio = Gst.ElementFactory.make("appsink", None)
|
appsink_audio = Gst.ElementFactory.make("appsink", None)
|
||||||
@@ -188,7 +191,7 @@ class GStreamerPipelineSource(FrameProcessor):
|
|||||||
(_, info) = buffer.map(Gst.MapFlags.READ)
|
(_, info) = buffer.map(Gst.MapFlags.READ)
|
||||||
frame = OutputAudioRawFrame(
|
frame = OutputAudioRawFrame(
|
||||||
audio=info.data,
|
audio=info.data,
|
||||||
sample_rate=self._out_params.audio_sample_rate,
|
sample_rate=self._sample_rate,
|
||||||
num_channels=self._out_params.audio_channels,
|
num_channels=self._out_params.audio_channels,
|
||||||
)
|
)
|
||||||
asyncio.run_coroutine_threadsafe(self.push_frame(frame), self.get_event_loop())
|
asyncio.run_coroutine_threadsafe(self.push_frame(frame), self.get_event_loop())
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from pipecat.frames.frames import Frame
|
from pipecat.frames.frames import Frame, StartFrame
|
||||||
|
|
||||||
|
|
||||||
class FrameSerializerType(Enum):
|
class FrameSerializerType(Enum):
|
||||||
@@ -21,6 +21,9 @@ class FrameSerializer(ABC):
|
|||||||
def type(self) -> FrameSerializerType:
|
def type(self) -> FrameSerializerType:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
async def setup(self, frame: StartFrame):
|
||||||
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def serialize(self, frame: Frame) -> str | bytes | None:
|
async def serialize(self, frame: Frame) -> str | bytes | None:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ from pipecat.frames.frames import (
|
|||||||
InputAudioRawFrame,
|
InputAudioRawFrame,
|
||||||
InputDTMFFrame,
|
InputDTMFFrame,
|
||||||
KeypadEntry,
|
KeypadEntry,
|
||||||
|
StartFrame,
|
||||||
StartInterruptionFrame,
|
StartInterruptionFrame,
|
||||||
)
|
)
|
||||||
from pipecat.serializers.base_serializer import FrameSerializer, FrameSerializerType
|
from pipecat.serializers.base_serializer import FrameSerializer, FrameSerializerType
|
||||||
@@ -29,8 +31,8 @@ from pipecat.serializers.base_serializer import FrameSerializer, FrameSerializer
|
|||||||
|
|
||||||
class TelnyxFrameSerializer(FrameSerializer):
|
class TelnyxFrameSerializer(FrameSerializer):
|
||||||
class InputParams(BaseModel):
|
class InputParams(BaseModel):
|
||||||
telnyx_sample_rate: int = 8000
|
telnyx_sample_rate: Optional[int] = None
|
||||||
sample_rate: int = 16000
|
sample_rate: Optional[int] = None
|
||||||
inbound_encoding: str = "PCMU"
|
inbound_encoding: str = "PCMU"
|
||||||
outbound_encoding: str = "PCMU"
|
outbound_encoding: str = "PCMU"
|
||||||
|
|
||||||
@@ -52,17 +54,21 @@ class TelnyxFrameSerializer(FrameSerializer):
|
|||||||
def type(self) -> FrameSerializerType:
|
def type(self) -> FrameSerializerType:
|
||||||
return FrameSerializerType.TEXT
|
return FrameSerializerType.TEXT
|
||||||
|
|
||||||
|
async def setup(self, frame: StartFrame):
|
||||||
|
self._telnyx_sample_rate = self._params.telnyx_sample_rate or frame.audio_in_sample_rate
|
||||||
|
self._sample_rate = self._params.sample_rate or frame.audio_out_sample_rate
|
||||||
|
|
||||||
async def serialize(self, frame: Frame) -> str | bytes | None:
|
async def serialize(self, frame: Frame) -> str | bytes | None:
|
||||||
if isinstance(frame, AudioRawFrame):
|
if isinstance(frame, AudioRawFrame):
|
||||||
data = frame.audio
|
data = frame.audio
|
||||||
|
|
||||||
if self._params.inbound_encoding == "PCMU":
|
if self._params.inbound_encoding == "PCMU":
|
||||||
serialized_data = await pcm_to_ulaw(
|
serialized_data = await pcm_to_ulaw(
|
||||||
data, frame.sample_rate, self._params.telnyx_sample_rate, self._resampler
|
data, frame.sample_rate, self._telnyx_sample_rate, self._resampler
|
||||||
)
|
)
|
||||||
elif self._params.inbound_encoding == "PCMA":
|
elif self._params.inbound_encoding == "PCMA":
|
||||||
serialized_data = await pcm_to_alaw(
|
serialized_data = await pcm_to_alaw(
|
||||||
data, frame.sample_rate, self._params.telnyx_sample_rate, self._resampler
|
data, frame.sample_rate, self._telnyx_sample_rate, self._resampler
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unsupported encoding: {self._params.inbound_encoding}")
|
raise ValueError(f"Unsupported encoding: {self._params.inbound_encoding}")
|
||||||
@@ -89,22 +95,22 @@ class TelnyxFrameSerializer(FrameSerializer):
|
|||||||
if self._params.outbound_encoding == "PCMU":
|
if self._params.outbound_encoding == "PCMU":
|
||||||
deserialized_data = await ulaw_to_pcm(
|
deserialized_data = await ulaw_to_pcm(
|
||||||
payload,
|
payload,
|
||||||
self._params.telnyx_sample_rate,
|
self._telnyx_sample_rate,
|
||||||
self._params.sample_rate,
|
self._sample_rate,
|
||||||
self._resampler,
|
self._resampler,
|
||||||
)
|
)
|
||||||
elif self._params.outbound_encoding == "PCMA":
|
elif self._params.outbound_encoding == "PCMA":
|
||||||
deserialized_data = await alaw_to_pcm(
|
deserialized_data = await alaw_to_pcm(
|
||||||
payload,
|
payload,
|
||||||
self._params.telnyx_sample_rate,
|
self._telnyx_sample_rate,
|
||||||
self._params.sample_rate,
|
self._sample_rate,
|
||||||
self._resampler,
|
self._resampler,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unsupported encoding: {self._params.outbound_encoding}")
|
raise ValueError(f"Unsupported encoding: {self._params.outbound_encoding}")
|
||||||
|
|
||||||
audio_frame = InputAudioRawFrame(
|
audio_frame = InputAudioRawFrame(
|
||||||
audio=deserialized_data, num_channels=1, sample_rate=self._params.sample_rate
|
audio=deserialized_data, num_channels=1, sample_rate=self._sample_rate
|
||||||
)
|
)
|
||||||
return audio_frame
|
return audio_frame
|
||||||
elif message["event"] == "dtmf":
|
elif message["event"] == "dtmf":
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
@@ -16,6 +17,7 @@ from pipecat.frames.frames import (
|
|||||||
InputAudioRawFrame,
|
InputAudioRawFrame,
|
||||||
InputDTMFFrame,
|
InputDTMFFrame,
|
||||||
KeypadEntry,
|
KeypadEntry,
|
||||||
|
StartFrame,
|
||||||
StartInterruptionFrame,
|
StartInterruptionFrame,
|
||||||
TransportMessageFrame,
|
TransportMessageFrame,
|
||||||
TransportMessageUrgentFrame,
|
TransportMessageUrgentFrame,
|
||||||
@@ -25,19 +27,26 @@ from pipecat.serializers.base_serializer import FrameSerializer, FrameSerializer
|
|||||||
|
|
||||||
class TwilioFrameSerializer(FrameSerializer):
|
class TwilioFrameSerializer(FrameSerializer):
|
||||||
class InputParams(BaseModel):
|
class InputParams(BaseModel):
|
||||||
twilio_sample_rate: int = 8000
|
twilio_sample_rate: Optional[int] = None
|
||||||
sample_rate: int = 16000
|
sample_rate: Optional[int] = None
|
||||||
|
|
||||||
def __init__(self, stream_sid: str, params: InputParams = InputParams()):
|
def __init__(self, stream_sid: str, params: InputParams = InputParams()):
|
||||||
self._stream_sid = stream_sid
|
self._stream_sid = stream_sid
|
||||||
self._params = params
|
self._params = params
|
||||||
|
|
||||||
|
self._twilio_sample_rate = 0
|
||||||
|
self._sample_rate = 0
|
||||||
|
|
||||||
self._resampler = create_default_resampler()
|
self._resampler = create_default_resampler()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def type(self) -> FrameSerializerType:
|
def type(self) -> FrameSerializerType:
|
||||||
return FrameSerializerType.TEXT
|
return FrameSerializerType.TEXT
|
||||||
|
|
||||||
|
async def setup(self, frame: StartFrame):
|
||||||
|
self._twilio_sample_rate = self._params.twilio_sample_rate or frame.audio_in_sample_rate
|
||||||
|
self._sample_rate = self._params.sample_rate or frame.audio_out_sample_rate
|
||||||
|
|
||||||
async def serialize(self, frame: Frame) -> str | bytes | None:
|
async def serialize(self, frame: Frame) -> str | bytes | None:
|
||||||
if isinstance(frame, StartInterruptionFrame):
|
if isinstance(frame, StartInterruptionFrame):
|
||||||
answer = {"event": "clear", "streamSid": self._stream_sid}
|
answer = {"event": "clear", "streamSid": self._stream_sid}
|
||||||
@@ -46,7 +55,7 @@ class TwilioFrameSerializer(FrameSerializer):
|
|||||||
data = frame.audio
|
data = frame.audio
|
||||||
|
|
||||||
serialized_data = await pcm_to_ulaw(
|
serialized_data = await pcm_to_ulaw(
|
||||||
data, frame.sample_rate, self._params.twilio_sample_rate, self._resampler
|
data, frame.sample_rate, self._twilio_sample_rate, self._resampler
|
||||||
)
|
)
|
||||||
payload = base64.b64encode(serialized_data).decode("utf-8")
|
payload = base64.b64encode(serialized_data).decode("utf-8")
|
||||||
answer = {
|
answer = {
|
||||||
@@ -67,10 +76,10 @@ class TwilioFrameSerializer(FrameSerializer):
|
|||||||
payload = base64.b64decode(payload_base64)
|
payload = base64.b64decode(payload_base64)
|
||||||
|
|
||||||
deserialized_data = await ulaw_to_pcm(
|
deserialized_data = await ulaw_to_pcm(
|
||||||
payload, self._params.twilio_sample_rate, self._params.sample_rate, self._resampler
|
payload, self._twilio_sample_rate, self._sample_rate, self._resampler
|
||||||
)
|
)
|
||||||
audio_frame = InputAudioRawFrame(
|
audio_frame = InputAudioRawFrame(
|
||||||
audio=deserialized_data, num_channels=1, sample_rate=self._params.sample_rate
|
audio=deserialized_data, num_channels=1, sample_rate=self._sample_rate
|
||||||
)
|
)
|
||||||
return audio_frame
|
return audio_frame
|
||||||
elif message["event"] == "dtmf":
|
elif message["event"] == "dtmf":
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ class TTSService(AIService):
|
|||||||
# if push_silence_after_stop is True, send this amount of audio silence
|
# if push_silence_after_stop is True, send this amount of audio silence
|
||||||
silence_time_s: float = 2.0,
|
silence_time_s: float = 2.0,
|
||||||
# TTS output sample rate
|
# TTS output sample rate
|
||||||
sample_rate: int = 24000,
|
sample_rate: Optional[int] = None,
|
||||||
text_filter: Optional[BaseTextFilter] = None,
|
text_filter: Optional[BaseTextFilter] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
@@ -224,7 +224,8 @@ class TTSService(AIService):
|
|||||||
self._stop_frame_timeout_s: float = stop_frame_timeout_s
|
self._stop_frame_timeout_s: float = stop_frame_timeout_s
|
||||||
self._push_silence_after_stop: bool = push_silence_after_stop
|
self._push_silence_after_stop: bool = push_silence_after_stop
|
||||||
self._silence_time_s: float = silence_time_s
|
self._silence_time_s: float = silence_time_s
|
||||||
self._sample_rate: int = sample_rate
|
self._init_sample_rate = sample_rate
|
||||||
|
self._sample_rate = 0
|
||||||
self._voice_id: str = ""
|
self._voice_id: str = ""
|
||||||
self._settings: Dict[str, Any] = {}
|
self._settings: Dict[str, Any] = {}
|
||||||
self._text_filter: Optional[BaseTextFilter] = text_filter
|
self._text_filter: Optional[BaseTextFilter] = text_filter
|
||||||
@@ -248,16 +249,20 @@ class TTSService(AIService):
|
|||||||
async def flush_audio(self):
|
async def flush_audio(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def language_to_service_language(self, language: Language) -> str | None:
|
|
||||||
return Language(language)
|
|
||||||
|
|
||||||
# Converts the text to audio.
|
# Converts the text to audio.
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def run_tts(self, text: str) -> AsyncGenerator[Frame, None]:
|
async def run_tts(self, text: str) -> AsyncGenerator[Frame, None]:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def language_to_service_language(self, language: Language) -> str | None:
|
||||||
|
return Language(language)
|
||||||
|
|
||||||
|
async def update_setting(self, key: str, value: Any):
|
||||||
|
pass
|
||||||
|
|
||||||
async def start(self, frame: StartFrame):
|
async def start(self, frame: StartFrame):
|
||||||
await super().start(frame)
|
await super().start(frame)
|
||||||
|
self._sample_rate = self._init_sample_rate or frame.audio_out_sample_rate
|
||||||
if self._push_stop_frames:
|
if self._push_stop_frames:
|
||||||
self._stop_frame_task = self.create_task(self._stop_frame_handler())
|
self._stop_frame_task = self.create_task(self._stop_frame_handler())
|
||||||
|
|
||||||
@@ -467,9 +472,17 @@ class WordTTSService(TTSService):
|
|||||||
class STTService(AIService):
|
class STTService(AIService):
|
||||||
"""STTService is a base class for speech-to-text services."""
|
"""STTService is a base class for speech-to-text services."""
|
||||||
|
|
||||||
def __init__(self, audio_passthrough=False, **kwargs):
|
def __init__(
|
||||||
|
self,
|
||||||
|
audio_passthrough=False,
|
||||||
|
# STT input sample rate
|
||||||
|
sample_rate: Optional[int] = None,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self._audio_passthrough = audio_passthrough
|
self._audio_passthrough = audio_passthrough
|
||||||
|
self._init_sample_rate = sample_rate
|
||||||
|
self._sample_rate = 0
|
||||||
self._settings: Dict[str, Any] = {}
|
self._settings: Dict[str, Any] = {}
|
||||||
self._muted: bool = False
|
self._muted: bool = False
|
||||||
|
|
||||||
@@ -478,6 +491,10 @@ class STTService(AIService):
|
|||||||
"""Returns whether the STT service is currently muted."""
|
"""Returns whether the STT service is currently muted."""
|
||||||
return self._muted
|
return self._muted
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sample_rate(self) -> int:
|
||||||
|
return self._sample_rate
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def set_model(self, model: str):
|
async def set_model(self, model: str):
|
||||||
self.set_model_name(model)
|
self.set_model_name(model)
|
||||||
@@ -491,6 +508,10 @@ class STTService(AIService):
|
|||||||
"""Returns transcript as a string"""
|
"""Returns transcript as a string"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
async def start(self, frame: StartFrame):
|
||||||
|
await super().start(frame)
|
||||||
|
self._sample_rate = self._init_sample_rate or frame.audio_in_sample_rate
|
||||||
|
|
||||||
async def _update_settings(self, settings: Mapping[str, Any]):
|
async def _update_settings(self, settings: Mapping[str, Any]):
|
||||||
logger.info(f"Updating STT settings: {self._settings}")
|
logger.info(f"Updating STT settings: {self._settings}")
|
||||||
for key, value in settings.items():
|
for key, value in settings.items():
|
||||||
@@ -540,17 +561,15 @@ class SegmentedSTTService(STTService):
|
|||||||
min_volume: float = 0.6,
|
min_volume: float = 0.6,
|
||||||
max_silence_secs: float = 0.3,
|
max_silence_secs: float = 0.3,
|
||||||
max_buffer_secs: float = 1.5,
|
max_buffer_secs: float = 1.5,
|
||||||
sample_rate: int = 24000,
|
sample_rate: Optional[int] = None,
|
||||||
num_channels: int = 1,
|
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
super().__init__(**kwargs)
|
super().__init__(sample_rate=sample_rate, **kwargs)
|
||||||
self._min_volume = min_volume
|
self._min_volume = min_volume
|
||||||
self._max_silence_secs = max_silence_secs
|
self._max_silence_secs = max_silence_secs
|
||||||
self._max_buffer_secs = max_buffer_secs
|
self._max_buffer_secs = max_buffer_secs
|
||||||
self._sample_rate = sample_rate
|
self._content = None
|
||||||
self._num_channels = num_channels
|
self._wave = None
|
||||||
(self._content, self._wave) = self._new_wave()
|
|
||||||
self._silence_num_frames = 0
|
self._silence_num_frames = 0
|
||||||
# Volume exponential smoothing
|
# Volume exponential smoothing
|
||||||
self._smoothing_factor = 0.2
|
self._smoothing_factor = 0.2
|
||||||
@@ -569,8 +588,8 @@ class SegmentedSTTService(STTService):
|
|||||||
|
|
||||||
# If buffer is not empty and we have enough data or there's been a long
|
# If buffer is not empty and we have enough data or there's been a long
|
||||||
# silence, transcribe the audio gathered so far.
|
# silence, transcribe the audio gathered so far.
|
||||||
silence_secs = self._silence_num_frames / self._sample_rate
|
silence_secs = self._silence_num_frames / self.sample_rate
|
||||||
buffer_secs = self._wave.getnframes() / self._sample_rate
|
buffer_secs = self._wave.getnframes() / self.sample_rate
|
||||||
if self._content.tell() > 0 and (
|
if self._content.tell() > 0 and (
|
||||||
buffer_secs > self._max_buffer_secs or silence_secs > self._max_silence_secs
|
buffer_secs > self._max_buffer_secs or silence_secs > self._max_silence_secs
|
||||||
):
|
):
|
||||||
@@ -580,18 +599,24 @@ class SegmentedSTTService(STTService):
|
|||||||
await self.process_generator(self.run_stt(self._content.read()))
|
await self.process_generator(self.run_stt(self._content.read()))
|
||||||
(self._content, self._wave) = self._new_wave()
|
(self._content, self._wave) = self._new_wave()
|
||||||
|
|
||||||
|
async def start(self, frame: StartFrame):
|
||||||
|
await super().start(frame)
|
||||||
|
(self._content, self._wave) = self._new_wave()
|
||||||
|
|
||||||
async def stop(self, frame: EndFrame):
|
async def stop(self, frame: EndFrame):
|
||||||
|
await super().stop(frame)
|
||||||
self._wave.close()
|
self._wave.close()
|
||||||
|
|
||||||
async def cancel(self, frame: CancelFrame):
|
async def cancel(self, frame: CancelFrame):
|
||||||
|
await super().cancel(frame)
|
||||||
self._wave.close()
|
self._wave.close()
|
||||||
|
|
||||||
def _new_wave(self):
|
def _new_wave(self):
|
||||||
content = io.BytesIO()
|
content = io.BytesIO()
|
||||||
ww = wave.open(content, "wb")
|
ww = wave.open(content, "wb")
|
||||||
ww.setsampwidth(2)
|
ww.setsampwidth(2)
|
||||||
ww.setnchannels(self._num_channels)
|
ww.setnchannels(1)
|
||||||
ww.setframerate(self._sample_rate)
|
ww.setframerate(self.sample_rate)
|
||||||
return (content, ww)
|
return (content, ww)
|
||||||
|
|
||||||
def _get_smoothed_volume(self, frame: AudioRawFrame) -> float:
|
def _get_smoothed_volume(self, frame: AudioRawFrame) -> float:
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from typing import AsyncGenerator
|
from typing import AsyncGenerator, Optional
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
@@ -38,20 +38,17 @@ class AssemblyAISTTService(STTService):
|
|||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
api_key: str,
|
api_key: str,
|
||||||
sample_rate: int = 16000,
|
sample_rate: Optional[int] = None,
|
||||||
encoding: AudioEncoding = AudioEncoding("pcm_s16le"),
|
encoding: AudioEncoding = AudioEncoding("pcm_s16le"),
|
||||||
language=Language.EN, # Only English is supported for Realtime
|
language=Language.EN, # Only English is supported for Realtime
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
super().__init__(**kwargs)
|
super().__init__(sample_rate=sample_rate, **kwargs)
|
||||||
|
|
||||||
aai.settings.api_key = api_key
|
aai.settings.api_key = api_key
|
||||||
self._transcriber: aai.RealtimeTranscriber | None = None
|
self._transcriber: aai.RealtimeTranscriber | None = None
|
||||||
# Store reference to the main event loop for use in callback functions
|
|
||||||
self._loop = asyncio.get_event_loop()
|
|
||||||
|
|
||||||
self._settings = {
|
self._settings = {
|
||||||
"sample_rate": sample_rate,
|
|
||||||
"encoding": encoding,
|
"encoding": encoding,
|
||||||
"language": language,
|
"language": language,
|
||||||
}
|
}
|
||||||
@@ -121,7 +118,7 @@ class AssemblyAISTTService(STTService):
|
|||||||
|
|
||||||
# Schedule the coroutine to run in the main event loop
|
# Schedule the coroutine to run in the main event loop
|
||||||
# This is necessary because this callback runs in a different thread
|
# This is necessary because this callback runs in a different thread
|
||||||
asyncio.run_coroutine_threadsafe(self.push_frame(frame), self._loop)
|
asyncio.run_coroutine_threadsafe(self.push_frame(frame), self.get_event_loop())
|
||||||
|
|
||||||
def on_error(error: aai.RealtimeError):
|
def on_error(error: aai.RealtimeError):
|
||||||
"""Callback for handling errors from AssemblyAI.
|
"""Callback for handling errors from AssemblyAI.
|
||||||
@@ -131,14 +128,16 @@ class AssemblyAISTTService(STTService):
|
|||||||
"""
|
"""
|
||||||
logger.error(f"{self}: An error occurred: {error}")
|
logger.error(f"{self}: An error occurred: {error}")
|
||||||
# Schedule the coroutine to run in the main event loop
|
# Schedule the coroutine to run in the main event loop
|
||||||
asyncio.run_coroutine_threadsafe(self.push_frame(ErrorFrame(str(error))), self._loop)
|
asyncio.run_coroutine_threadsafe(
|
||||||
|
self.push_frame(ErrorFrame(str(error))), self.get_event_loop()
|
||||||
|
)
|
||||||
|
|
||||||
def on_close():
|
def on_close():
|
||||||
"""Callback for when the connection to AssemblyAI is closed."""
|
"""Callback for when the connection to AssemblyAI is closed."""
|
||||||
logger.info(f"{self}: Disconnected from AssemblyAI")
|
logger.info(f"{self}: Disconnected from AssemblyAI")
|
||||||
|
|
||||||
self._transcriber = aai.RealtimeTranscriber(
|
self._transcriber = aai.RealtimeTranscriber(
|
||||||
sample_rate=self._settings["sample_rate"],
|
sample_rate=self.sample_rate,
|
||||||
encoding=self._settings["encoding"],
|
encoding=self._settings["encoding"],
|
||||||
on_data=on_data,
|
on_data=on_data,
|
||||||
on_error=on_error,
|
on_error=on_error,
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ class PollyTTSService(TTSService):
|
|||||||
aws_session_token: Optional[str] = None,
|
aws_session_token: Optional[str] = None,
|
||||||
region: Optional[str] = None,
|
region: Optional[str] = None,
|
||||||
voice_id: str = "Joanna",
|
voice_id: str = "Joanna",
|
||||||
sample_rate: int = 24000,
|
sample_rate: Optional[int] = None,
|
||||||
params: InputParams = InputParams(),
|
params: InputParams = InputParams(),
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
@@ -138,7 +138,6 @@ class PollyTTSService(TTSService):
|
|||||||
region_name=region,
|
region_name=region,
|
||||||
)
|
)
|
||||||
self._settings = {
|
self._settings = {
|
||||||
"sample_rate": sample_rate,
|
|
||||||
"engine": params.engine,
|
"engine": params.engine,
|
||||||
"language": self.language_to_service_language(params.language)
|
"language": self.language_to_service_language(params.language)
|
||||||
if params.language
|
if params.language
|
||||||
@@ -226,9 +225,7 @@ class PollyTTSService(TTSService):
|
|||||||
yield None
|
yield None
|
||||||
return
|
return
|
||||||
|
|
||||||
audio_data = await self._resampler.resample(
|
audio_data = await self._resampler.resample(audio_data, 16000, self.sample_rate)
|
||||||
audio_data, 16000, self._settings["sample_rate"]
|
|
||||||
)
|
|
||||||
|
|
||||||
await self.start_tts_usage_metrics(text)
|
await self.start_tts_usage_metrics(text)
|
||||||
|
|
||||||
@@ -239,7 +236,7 @@ class PollyTTSService(TTSService):
|
|||||||
chunk = audio_data[i : i + chunk_size]
|
chunk = audio_data[i : i + chunk_size]
|
||||||
if len(chunk) > 0:
|
if len(chunk) > 0:
|
||||||
await self.stop_ttfb_metrics()
|
await self.stop_ttfb_metrics()
|
||||||
frame = TTSAudioRawFrame(chunk, self._settings["sample_rate"], 1)
|
frame = TTSAudioRawFrame(chunk, self.sample_rate, 1)
|
||||||
yield frame
|
yield frame
|
||||||
|
|
||||||
yield TTSStoppedFrame()
|
yield TTSStoppedFrame()
|
||||||
|
|||||||
@@ -450,14 +450,13 @@ class AzureBaseTTSService(TTSService):
|
|||||||
api_key: str,
|
api_key: str,
|
||||||
region: str,
|
region: str,
|
||||||
voice="en-US-SaraNeural",
|
voice="en-US-SaraNeural",
|
||||||
sample_rate: int = 24000,
|
sample_rate: Optional[int] = None,
|
||||||
params: InputParams = InputParams(),
|
params: InputParams = InputParams(),
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
super().__init__(sample_rate=sample_rate, **kwargs)
|
super().__init__(sample_rate=sample_rate, **kwargs)
|
||||||
|
|
||||||
self._settings = {
|
self._settings = {
|
||||||
"sample_rate": sample_rate,
|
|
||||||
"emphasis": params.emphasis,
|
"emphasis": params.emphasis,
|
||||||
"language": self.language_to_service_language(params.language)
|
"language": self.language_to_service_language(params.language)
|
||||||
if params.language
|
if params.language
|
||||||
@@ -537,7 +536,7 @@ class AzureTTSService(AzureBaseTTSService):
|
|||||||
speech_recognition_language=self._settings["language"],
|
speech_recognition_language=self._settings["language"],
|
||||||
)
|
)
|
||||||
speech_config.set_speech_synthesis_output_format(
|
speech_config.set_speech_synthesis_output_format(
|
||||||
sample_rate_to_output_format(self._settings["sample_rate"])
|
sample_rate_to_output_format(self.sample_rate)
|
||||||
)
|
)
|
||||||
speech_config.set_service_property(
|
speech_config.set_service_property(
|
||||||
"synthesizer.synthesis.connection.synthesisConnectionImpl",
|
"synthesizer.synthesis.connection.synthesisConnectionImpl",
|
||||||
@@ -591,7 +590,7 @@ class AzureTTSService(AzureBaseTTSService):
|
|||||||
|
|
||||||
yield TTSAudioRawFrame(
|
yield TTSAudioRawFrame(
|
||||||
audio=chunk,
|
audio=chunk,
|
||||||
sample_rate=self._settings["sample_rate"],
|
sample_rate=self.sample_rate,
|
||||||
num_channels=1,
|
num_channels=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -612,7 +611,7 @@ class AzureHttpTTSService(AzureBaseTTSService):
|
|||||||
speech_recognition_language=self._settings["language"],
|
speech_recognition_language=self._settings["language"],
|
||||||
)
|
)
|
||||||
speech_config.set_speech_synthesis_output_format(
|
speech_config.set_speech_synthesis_output_format(
|
||||||
sample_rate_to_output_format(self._settings["sample_rate"])
|
sample_rate_to_output_format(self.sample_rate)
|
||||||
)
|
)
|
||||||
|
|
||||||
self._speech_synthesizer = SpeechSynthesizer(speech_config=speech_config, audio_config=None)
|
self._speech_synthesizer = SpeechSynthesizer(speech_config=speech_config, audio_config=None)
|
||||||
@@ -633,7 +632,7 @@ class AzureHttpTTSService(AzureBaseTTSService):
|
|||||||
# Azure always sends a 44-byte header. Strip it off.
|
# Azure always sends a 44-byte header. Strip it off.
|
||||||
yield TTSAudioRawFrame(
|
yield TTSAudioRawFrame(
|
||||||
audio=result.audio_data[44:],
|
audio=result.audio_data[44:],
|
||||||
sample_rate=self._settings["sample_rate"],
|
sample_rate=self.sample_rate,
|
||||||
num_channels=1,
|
num_channels=1,
|
||||||
)
|
)
|
||||||
yield TTSStoppedFrame()
|
yield TTSStoppedFrame()
|
||||||
@@ -650,24 +649,14 @@ class AzureSTTService(STTService):
|
|||||||
*,
|
*,
|
||||||
api_key: str,
|
api_key: str,
|
||||||
region: str,
|
region: str,
|
||||||
language=Language.EN_US,
|
language: Language = Language.EN_US,
|
||||||
sample_rate=24000,
|
sample_rate: Optional[int] = None,
|
||||||
channels=1,
|
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
super().__init__(**kwargs)
|
super().__init__(sample_rate=sample_rate, **kwargs)
|
||||||
|
|
||||||
speech_config = SpeechConfig(subscription=api_key, region=region)
|
self._speech_config = SpeechConfig(subscription=api_key, region=region)
|
||||||
speech_config.speech_recognition_language = language
|
self._speech_config.speech_recognition_language = language
|
||||||
|
|
||||||
stream_format = AudioStreamFormat(samples_per_second=sample_rate, channels=channels)
|
|
||||||
self._audio_stream = PushAudioInputStream(stream_format)
|
|
||||||
|
|
||||||
audio_config = AudioConfig(stream=self._audio_stream)
|
|
||||||
self._speech_recognizer = SpeechRecognizer(
|
|
||||||
speech_config=speech_config, audio_config=audio_config
|
|
||||||
)
|
|
||||||
self._speech_recognizer.recognized.connect(self._on_handle_recognized)
|
|
||||||
|
|
||||||
async def run_stt(self, audio: bytes) -> AsyncGenerator[Frame, None]:
|
async def run_stt(self, audio: bytes) -> AsyncGenerator[Frame, None]:
|
||||||
await self.start_processing_metrics()
|
await self.start_processing_metrics()
|
||||||
@@ -677,6 +666,16 @@ class AzureSTTService(STTService):
|
|||||||
|
|
||||||
async def start(self, frame: StartFrame):
|
async def start(self, frame: StartFrame):
|
||||||
await super().start(frame)
|
await super().start(frame)
|
||||||
|
|
||||||
|
stream_format = AudioStreamFormat(samples_per_second=self.sample_rate, channels=1)
|
||||||
|
self._audio_stream = PushAudioInputStream(stream_format)
|
||||||
|
|
||||||
|
audio_config = AudioConfig(stream=self._audio_stream)
|
||||||
|
|
||||||
|
self._speech_recognizer = SpeechRecognizer(
|
||||||
|
speech_config=self._speech_config, audio_config=audio_config
|
||||||
|
)
|
||||||
|
self._speech_recognizer.recognized.connect(self._on_handle_recognized)
|
||||||
self._speech_recognizer.start_continuous_recognition_async()
|
self._speech_recognizer.start_continuous_recognition_async()
|
||||||
|
|
||||||
async def stop(self, frame: EndFrame):
|
async def stop(self, frame: EndFrame):
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ class CartesiaTTSService(WordTTSService, WebsocketService):
|
|||||||
cartesia_version: str = "2024-06-10",
|
cartesia_version: str = "2024-06-10",
|
||||||
url: str = "wss://api.cartesia.ai/tts/websocket",
|
url: str = "wss://api.cartesia.ai/tts/websocket",
|
||||||
model: str = "sonic",
|
model: str = "sonic",
|
||||||
sample_rate: int = 24000,
|
sample_rate: Optional[int] = None,
|
||||||
encoding: str = "pcm_s16le",
|
encoding: str = "pcm_s16le",
|
||||||
container: str = "raw",
|
container: str = "raw",
|
||||||
params: InputParams = InputParams(),
|
params: InputParams = InputParams(),
|
||||||
@@ -121,7 +121,7 @@ class CartesiaTTSService(WordTTSService, WebsocketService):
|
|||||||
"output_format": {
|
"output_format": {
|
||||||
"container": container,
|
"container": container,
|
||||||
"encoding": encoding,
|
"encoding": encoding,
|
||||||
"sample_rate": sample_rate,
|
"sample_rate": 0,
|
||||||
},
|
},
|
||||||
"language": self.language_to_service_language(params.language)
|
"language": self.language_to_service_language(params.language)
|
||||||
if params.language
|
if params.language
|
||||||
@@ -174,6 +174,7 @@ class CartesiaTTSService(WordTTSService, WebsocketService):
|
|||||||
|
|
||||||
async def start(self, frame: StartFrame):
|
async def start(self, frame: StartFrame):
|
||||||
await super().start(frame)
|
await super().start(frame)
|
||||||
|
self._settings["output_format"]["sample_rate"] = self.sample_rate
|
||||||
await self._connect()
|
await self._connect()
|
||||||
|
|
||||||
async def stop(self, frame: EndFrame):
|
async def stop(self, frame: EndFrame):
|
||||||
@@ -262,7 +263,7 @@ class CartesiaTTSService(WordTTSService, WebsocketService):
|
|||||||
self.start_word_timestamps()
|
self.start_word_timestamps()
|
||||||
frame = TTSAudioRawFrame(
|
frame = TTSAudioRawFrame(
|
||||||
audio=base64.b64decode(msg["data"]),
|
audio=base64.b64decode(msg["data"]),
|
||||||
sample_rate=self._settings["output_format"]["sample_rate"],
|
sample_rate=self.sample_rate,
|
||||||
num_channels=1,
|
num_channels=1,
|
||||||
)
|
)
|
||||||
await self.push_frame(frame)
|
await self.push_frame(frame)
|
||||||
@@ -328,7 +329,7 @@ class CartesiaHttpTTSService(TTSService):
|
|||||||
voice_id: str,
|
voice_id: str,
|
||||||
model: str = "sonic",
|
model: str = "sonic",
|
||||||
base_url: str = "https://api.cartesia.ai",
|
base_url: str = "https://api.cartesia.ai",
|
||||||
sample_rate: int = 24000,
|
sample_rate: Optional[int] = None,
|
||||||
encoding: str = "pcm_s16le",
|
encoding: str = "pcm_s16le",
|
||||||
container: str = "raw",
|
container: str = "raw",
|
||||||
params: InputParams = InputParams(),
|
params: InputParams = InputParams(),
|
||||||
@@ -341,7 +342,7 @@ class CartesiaHttpTTSService(TTSService):
|
|||||||
"output_format": {
|
"output_format": {
|
||||||
"container": container,
|
"container": container,
|
||||||
"encoding": encoding,
|
"encoding": encoding,
|
||||||
"sample_rate": sample_rate,
|
"sample_rate": 0,
|
||||||
},
|
},
|
||||||
"language": self.language_to_service_language(params.language)
|
"language": self.language_to_service_language(params.language)
|
||||||
if params.language
|
if params.language
|
||||||
@@ -360,6 +361,10 @@ class CartesiaHttpTTSService(TTSService):
|
|||||||
def language_to_service_language(self, language: Language) -> str | None:
|
def language_to_service_language(self, language: Language) -> str | None:
|
||||||
return language_to_cartesia_language(language)
|
return language_to_cartesia_language(language)
|
||||||
|
|
||||||
|
async def start(self, frame: StartFrame):
|
||||||
|
await super().start(frame)
|
||||||
|
self._settings["output_format"]["sample_rate"] = self.sample_rate
|
||||||
|
|
||||||
async def stop(self, frame: EndFrame):
|
async def stop(self, frame: EndFrame):
|
||||||
await super().stop(frame)
|
await super().stop(frame)
|
||||||
await self._client.close()
|
await self._client.close()
|
||||||
@@ -394,9 +399,7 @@ class CartesiaHttpTTSService(TTSService):
|
|||||||
)
|
)
|
||||||
|
|
||||||
frame = TTSAudioRawFrame(
|
frame = TTSAudioRawFrame(
|
||||||
audio=output["audio"],
|
audio=output["audio"], sample_rate=self.sample_rate, num_channels=1
|
||||||
sample_rate=self._settings["output_format"]["sample_rate"],
|
|
||||||
num_channels=1,
|
|
||||||
)
|
)
|
||||||
yield frame
|
yield frame
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from typing import AsyncGenerator
|
from typing import AsyncGenerator, Optional
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
@@ -53,14 +53,13 @@ class DeepgramTTSService(TTSService):
|
|||||||
*,
|
*,
|
||||||
api_key: str,
|
api_key: str,
|
||||||
voice: str = "aura-helios-en",
|
voice: str = "aura-helios-en",
|
||||||
sample_rate: int = 24000,
|
sample_rate: Optional[int] = None,
|
||||||
encoding: str = "linear16",
|
encoding: str = "linear16",
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
super().__init__(sample_rate=sample_rate, **kwargs)
|
super().__init__(sample_rate=sample_rate, **kwargs)
|
||||||
|
|
||||||
self._settings = {
|
self._settings = {
|
||||||
"sample_rate": sample_rate,
|
|
||||||
"encoding": encoding,
|
"encoding": encoding,
|
||||||
}
|
}
|
||||||
self.set_voice(voice)
|
self.set_voice(voice)
|
||||||
@@ -75,7 +74,7 @@ class DeepgramTTSService(TTSService):
|
|||||||
options = SpeakOptions(
|
options = SpeakOptions(
|
||||||
model=self._voice_id,
|
model=self._voice_id,
|
||||||
encoding=self._settings["encoding"],
|
encoding=self._settings["encoding"],
|
||||||
sample_rate=self._settings["sample_rate"],
|
sample_rate=self.sample_rate,
|
||||||
container="none",
|
container="none",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -103,9 +102,7 @@ class DeepgramTTSService(TTSService):
|
|||||||
chunk = audio_buffer.read(chunk_size)
|
chunk = audio_buffer.read(chunk_size)
|
||||||
if not chunk:
|
if not chunk:
|
||||||
break
|
break
|
||||||
frame = TTSAudioRawFrame(
|
frame = TTSAudioRawFrame(audio=chunk, sample_rate=self.sample_rate, num_channels=1)
|
||||||
audio=chunk, sample_rate=self._settings["sample_rate"], num_channels=1
|
|
||||||
)
|
|
||||||
yield frame
|
yield frame
|
||||||
|
|
||||||
yield TTSStoppedFrame()
|
yield TTSStoppedFrame()
|
||||||
@@ -121,15 +118,16 @@ class DeepgramSTTService(STTService):
|
|||||||
*,
|
*,
|
||||||
api_key: str,
|
api_key: str,
|
||||||
url: str = "",
|
url: str = "",
|
||||||
live_options: LiveOptions = None,
|
sample_rate: Optional[int] = None,
|
||||||
|
live_options: Optional[LiveOptions] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
super().__init__(**kwargs)
|
super().__init__(sample_rate=sample_rate, **kwargs)
|
||||||
|
|
||||||
default_options = LiveOptions(
|
default_options = LiveOptions(
|
||||||
encoding="linear16",
|
encoding="linear16",
|
||||||
language=Language.EN,
|
language=Language.EN,
|
||||||
model="nova-2-general",
|
model="nova-2-general",
|
||||||
sample_rate=16000,
|
|
||||||
channels=1,
|
channels=1,
|
||||||
interim_results=True,
|
interim_results=True,
|
||||||
smart_format=True,
|
smart_format=True,
|
||||||
@@ -187,6 +185,7 @@ class DeepgramSTTService(STTService):
|
|||||||
|
|
||||||
async def start(self, frame: StartFrame):
|
async def start(self, frame: StartFrame):
|
||||||
await super().start(frame)
|
await super().start(frame)
|
||||||
|
self._settings["sample_rate"] = self.sample_rate
|
||||||
await self._connect()
|
await self._connect()
|
||||||
|
|
||||||
async def stop(self, frame: EndFrame):
|
async def stop(self, frame: EndFrame):
|
||||||
|
|||||||
@@ -104,17 +104,17 @@ def language_to_elevenlabs_language(language: Language) -> str | None:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def sample_rate_from_output_format(output_format: str) -> int:
|
def output_format_from_sample_rate(sample_rate: int) -> str:
|
||||||
match output_format:
|
match sample_rate:
|
||||||
case "pcm_16000":
|
case 16000:
|
||||||
return 16000
|
return "pcm_16000"
|
||||||
case "pcm_22050":
|
case 22050:
|
||||||
return 22050
|
return "pcm_22050"
|
||||||
case "pcm_24000":
|
case 24000:
|
||||||
return 24000
|
return "pcm_24000"
|
||||||
case "pcm_44100":
|
case 44100:
|
||||||
return 44100
|
return "pcm_44100"
|
||||||
return 16000
|
return "pcm_16000"
|
||||||
|
|
||||||
|
|
||||||
def calculate_word_times(
|
def calculate_word_times(
|
||||||
@@ -165,7 +165,7 @@ class ElevenLabsTTSService(WordTTSService, WebsocketService):
|
|||||||
voice_id: str,
|
voice_id: str,
|
||||||
model: str = "eleven_flash_v2_5",
|
model: str = "eleven_flash_v2_5",
|
||||||
url: str = "wss://api.elevenlabs.io",
|
url: str = "wss://api.elevenlabs.io",
|
||||||
output_format: ElevenLabsOutputFormat = "pcm_24000",
|
sample_rate: Optional[int] = None,
|
||||||
params: InputParams = InputParams(),
|
params: InputParams = InputParams(),
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
@@ -189,7 +189,7 @@ class ElevenLabsTTSService(WordTTSService, WebsocketService):
|
|||||||
push_text_frames=False,
|
push_text_frames=False,
|
||||||
push_stop_frames=True,
|
push_stop_frames=True,
|
||||||
stop_frame_timeout_s=2.0,
|
stop_frame_timeout_s=2.0,
|
||||||
sample_rate=sample_rate_from_output_format(output_format),
|
sample_rate=sample_rate,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
WebsocketService.__init__(self)
|
WebsocketService.__init__(self)
|
||||||
@@ -197,11 +197,9 @@ class ElevenLabsTTSService(WordTTSService, WebsocketService):
|
|||||||
self._api_key = api_key
|
self._api_key = api_key
|
||||||
self._url = url
|
self._url = url
|
||||||
self._settings = {
|
self._settings = {
|
||||||
"sample_rate": sample_rate_from_output_format(output_format),
|
|
||||||
"language": self.language_to_service_language(params.language)
|
"language": self.language_to_service_language(params.language)
|
||||||
if params.language
|
if params.language
|
||||||
else None,
|
else None,
|
||||||
"output_format": output_format,
|
|
||||||
"optimize_streaming_latency": params.optimize_streaming_latency,
|
"optimize_streaming_latency": params.optimize_streaming_latency,
|
||||||
"stability": params.stability,
|
"stability": params.stability,
|
||||||
"similarity_boost": params.similarity_boost,
|
"similarity_boost": params.similarity_boost,
|
||||||
@@ -211,6 +209,7 @@ class ElevenLabsTTSService(WordTTSService, WebsocketService):
|
|||||||
}
|
}
|
||||||
self.set_model_name(model)
|
self.set_model_name(model)
|
||||||
self.set_voice(voice_id)
|
self.set_voice(voice_id)
|
||||||
|
self._output_format = "" # initialized in start()
|
||||||
self._voice_settings = self._set_voice_settings()
|
self._voice_settings = self._set_voice_settings()
|
||||||
|
|
||||||
# Indicates if we have sent TTSStartedFrame. It will reset to False when
|
# Indicates if we have sent TTSStartedFrame. It will reset to False when
|
||||||
@@ -254,7 +253,7 @@ class ElevenLabsTTSService(WordTTSService, WebsocketService):
|
|||||||
await self._disconnect()
|
await self._disconnect()
|
||||||
await self._connect()
|
await self._connect()
|
||||||
|
|
||||||
async def _update_settings(self, settings: Dict[str, Any]):
|
async def _update_settings(self, settings: Mapping[str, Any]):
|
||||||
prev_voice = self._voice_id
|
prev_voice = self._voice_id
|
||||||
await super()._update_settings(settings)
|
await super()._update_settings(settings)
|
||||||
if not prev_voice == self._voice_id:
|
if not prev_voice == self._voice_id:
|
||||||
@@ -264,6 +263,7 @@ class ElevenLabsTTSService(WordTTSService, WebsocketService):
|
|||||||
|
|
||||||
async def start(self, frame: StartFrame):
|
async def start(self, frame: StartFrame):
|
||||||
await super().start(frame)
|
await super().start(frame)
|
||||||
|
self._output_format = output_format_from_sample_rate(self.sample_rate)
|
||||||
await self._connect()
|
await self._connect()
|
||||||
|
|
||||||
async def stop(self, frame: EndFrame):
|
async def stop(self, frame: EndFrame):
|
||||||
@@ -322,7 +322,7 @@ class ElevenLabsTTSService(WordTTSService, WebsocketService):
|
|||||||
|
|
||||||
voice_id = self._voice_id
|
voice_id = self._voice_id
|
||||||
model = self.model_name
|
model = self.model_name
|
||||||
output_format = self._settings["output_format"]
|
output_format = self._output_format
|
||||||
url = f"{self._url}/v1/text-to-speech/{voice_id}/stream-input?model_id={model}&output_format={output_format}&auto_mode={self._settings['auto_mode']}"
|
url = f"{self._url}/v1/text-to-speech/{voice_id}/stream-input?model_id={model}&output_format={output_format}&auto_mode={self._settings['auto_mode']}"
|
||||||
|
|
||||||
if self._settings["optimize_streaming_latency"]:
|
if self._settings["optimize_streaming_latency"]:
|
||||||
@@ -375,7 +375,7 @@ class ElevenLabsTTSService(WordTTSService, WebsocketService):
|
|||||||
self.start_word_timestamps()
|
self.start_word_timestamps()
|
||||||
|
|
||||||
audio = base64.b64decode(msg["audio"])
|
audio = base64.b64decode(msg["audio"])
|
||||||
frame = TTSAudioRawFrame(audio, self._settings["sample_rate"], 1)
|
frame = TTSAudioRawFrame(audio, self.sample_rate, 1)
|
||||||
await self.push_frame(frame)
|
await self.push_frame(frame)
|
||||||
if msg.get("alignment"):
|
if msg.get("alignment"):
|
||||||
word_times = calculate_word_times(msg["alignment"], self._cumulative_time)
|
word_times = calculate_word_times(msg["alignment"], self._cumulative_time)
|
||||||
@@ -428,7 +428,7 @@ class ElevenLabsHttpTTSService(TTSService):
|
|||||||
aiohttp_session: aiohttp ClientSession
|
aiohttp_session: aiohttp ClientSession
|
||||||
model: Model ID (default: "eleven_flash_v2_5" for low latency)
|
model: Model ID (default: "eleven_flash_v2_5" for low latency)
|
||||||
base_url: API base URL
|
base_url: API base URL
|
||||||
output_format: Audio output format (PCM)
|
sample_rate: Output sample rate
|
||||||
params: Additional parameters for voice configuration
|
params: Additional parameters for voice configuration
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -448,24 +448,21 @@ class ElevenLabsHttpTTSService(TTSService):
|
|||||||
aiohttp_session: aiohttp.ClientSession,
|
aiohttp_session: aiohttp.ClientSession,
|
||||||
model: str = "eleven_flash_v2_5",
|
model: str = "eleven_flash_v2_5",
|
||||||
base_url: str = "https://api.elevenlabs.io",
|
base_url: str = "https://api.elevenlabs.io",
|
||||||
output_format: ElevenLabsOutputFormat = "pcm_24000",
|
sample_rate: Optional[int] = None,
|
||||||
params: InputParams = InputParams(),
|
params: InputParams = InputParams(),
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
super().__init__(sample_rate=sample_rate_from_output_format(output_format), **kwargs)
|
super().__init__(sample_rate=sample_rate, **kwargs)
|
||||||
|
|
||||||
self._api_key = api_key
|
self._api_key = api_key
|
||||||
self._base_url = base_url
|
self._base_url = base_url
|
||||||
self._output_format = output_format
|
|
||||||
self._params = params
|
self._params = params
|
||||||
self._session = aiohttp_session
|
self._session = aiohttp_session
|
||||||
|
|
||||||
self._settings = {
|
self._settings = {
|
||||||
"sample_rate": sample_rate_from_output_format(output_format),
|
|
||||||
"language": self.language_to_service_language(params.language)
|
"language": self.language_to_service_language(params.language)
|
||||||
if params.language
|
if params.language
|
||||||
else None,
|
else None,
|
||||||
"output_format": output_format,
|
|
||||||
"optimize_streaming_latency": params.optimize_streaming_latency,
|
"optimize_streaming_latency": params.optimize_streaming_latency,
|
||||||
"stability": params.stability,
|
"stability": params.stability,
|
||||||
"similarity_boost": params.similarity_boost,
|
"similarity_boost": params.similarity_boost,
|
||||||
@@ -474,6 +471,7 @@ class ElevenLabsHttpTTSService(TTSService):
|
|||||||
}
|
}
|
||||||
self.set_model_name(model)
|
self.set_model_name(model)
|
||||||
self.set_voice(voice_id)
|
self.set_voice(voice_id)
|
||||||
|
self._output_format = "" # initialized in start()
|
||||||
self._voice_settings = self._set_voice_settings()
|
self._voice_settings = self._set_voice_settings()
|
||||||
|
|
||||||
def can_generate_metrics(self) -> bool:
|
def can_generate_metrics(self) -> bool:
|
||||||
@@ -508,6 +506,10 @@ class ElevenLabsHttpTTSService(TTSService):
|
|||||||
|
|
||||||
return voice_settings or None
|
return voice_settings or None
|
||||||
|
|
||||||
|
async def start(self, frame: StartFrame):
|
||||||
|
await super().start(frame)
|
||||||
|
self._output_format = output_format_from_sample_rate(self.sample_rate)
|
||||||
|
|
||||||
async def run_tts(self, text: str) -> AsyncGenerator[Frame, None]:
|
async def run_tts(self, text: str) -> AsyncGenerator[Frame, None]:
|
||||||
"""Generate speech from text using ElevenLabs streaming API.
|
"""Generate speech from text using ElevenLabs streaming API.
|
||||||
|
|
||||||
@@ -570,7 +572,7 @@ class ElevenLabsHttpTTSService(TTSService):
|
|||||||
async for chunk in response.content:
|
async for chunk in response.content:
|
||||||
if chunk:
|
if chunk:
|
||||||
await self.stop_ttfb_metrics()
|
await self.stop_ttfb_metrics()
|
||||||
yield TTSAudioRawFrame(chunk, self._settings["sample_rate"], 1)
|
yield TTSAudioRawFrame(chunk, self.sample_rate, 1)
|
||||||
|
|
||||||
yield TTSStoppedFrame()
|
yield TTSStoppedFrame()
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ class FishAudioTTSService(TTSService, WebsocketService):
|
|||||||
api_key: str,
|
api_key: str,
|
||||||
model: str, # This is the reference_id
|
model: str, # This is the reference_id
|
||||||
output_format: FishAudioOutputFormat = "pcm",
|
output_format: FishAudioOutputFormat = "pcm",
|
||||||
sample_rate: int = 24000,
|
sample_rate: Optional[int] = None,
|
||||||
params: InputParams = InputParams(),
|
params: InputParams = InputParams(),
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
@@ -70,7 +70,7 @@ class FishAudioTTSService(TTSService, WebsocketService):
|
|||||||
self._started = False
|
self._started = False
|
||||||
|
|
||||||
self._settings = {
|
self._settings = {
|
||||||
"sample_rate": sample_rate,
|
"sample_rate": 0,
|
||||||
"latency": params.latency,
|
"latency": params.latency,
|
||||||
"format": output_format,
|
"format": output_format,
|
||||||
"prosody": {
|
"prosody": {
|
||||||
@@ -92,6 +92,7 @@ class FishAudioTTSService(TTSService, WebsocketService):
|
|||||||
|
|
||||||
async def start(self, frame: StartFrame):
|
async def start(self, frame: StartFrame):
|
||||||
await super().start(frame)
|
await super().start(frame)
|
||||||
|
self._settings["sample_rate"] = self.sample_rate
|
||||||
await self._connect()
|
await self._connect()
|
||||||
|
|
||||||
async def stop(self, frame: EndFrame):
|
async def stop(self, frame: EndFrame):
|
||||||
@@ -157,9 +158,7 @@ class FishAudioTTSService(TTSService, WebsocketService):
|
|||||||
audio_data = msg.get("audio")
|
audio_data = msg.get("audio")
|
||||||
# Only process larger chunks to remove msgpack overhead
|
# Only process larger chunks to remove msgpack overhead
|
||||||
if audio_data and len(audio_data) > 1024:
|
if audio_data and len(audio_data) > 1024:
|
||||||
frame = TTSAudioRawFrame(
|
frame = TTSAudioRawFrame(audio_data, self.sample_rate, 1)
|
||||||
audio_data, self._settings["sample_rate"], 1
|
|
||||||
)
|
|
||||||
await self.push_frame(frame)
|
await self.push_frame(frame)
|
||||||
await self.stop_ttfb_metrics()
|
await self.stop_ttfb_metrics()
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class AudioInputMessage(BaseModel):
|
|||||||
realtimeInput: RealtimeInput
|
realtimeInput: RealtimeInput
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_raw_audio(cls, raw_audio: bytes, sample_rate=16000) -> "AudioInputMessage":
|
def from_raw_audio(cls, raw_audio: bytes, sample_rate: int) -> "AudioInputMessage":
|
||||||
data = base64.b64encode(raw_audio).decode("utf-8")
|
data = base64.b64encode(raw_audio).decode("utf-8")
|
||||||
return cls(
|
return cls(
|
||||||
realtimeInput=RealtimeInput(
|
realtimeInput=RealtimeInput(
|
||||||
|
|||||||
@@ -203,6 +203,8 @@ class GeminiMultimodalLiveLLMService(LLMService):
|
|||||||
self._bot_audio_buffer = bytearray()
|
self._bot_audio_buffer = bytearray()
|
||||||
self._bot_text_buffer = ""
|
self._bot_text_buffer = ""
|
||||||
|
|
||||||
|
self._sample_rate = 24000
|
||||||
|
|
||||||
self._settings = {
|
self._settings = {
|
||||||
"frequency_penalty": params.frequency_penalty,
|
"frequency_penalty": params.frequency_penalty,
|
||||||
"max_tokens": params.max_tokens,
|
"max_tokens": params.max_tokens,
|
||||||
@@ -521,7 +523,7 @@ class GeminiMultimodalLiveLLMService(LLMService):
|
|||||||
if self._audio_input_paused:
|
if self._audio_input_paused:
|
||||||
return
|
return
|
||||||
# Send all audio to Gemini
|
# Send all audio to Gemini
|
||||||
evt = events.AudioInputMessage.from_raw_audio(frame.audio)
|
evt = events.AudioInputMessage.from_raw_audio(frame.audio, frame.sample_rate)
|
||||||
await self.send_client_event(evt)
|
await self.send_client_event(evt)
|
||||||
# Manage a buffer of audio to use for transcription
|
# Manage a buffer of audio to use for transcription
|
||||||
audio = frame.audio
|
audio = frame.audio
|
||||||
@@ -650,7 +652,7 @@ class GeminiMultimodalLiveLLMService(LLMService):
|
|||||||
inline_data = part.inlineData
|
inline_data = part.inlineData
|
||||||
if not inline_data:
|
if not inline_data:
|
||||||
return
|
return
|
||||||
if inline_data.mimeType != "audio/pcm;rate=24000":
|
if inline_data.mimeType != f"audio/pcm;rate={self._sample_rate}":
|
||||||
logger.warning(f"Unrecognized server_content format {inline_data.mimeType}")
|
logger.warning(f"Unrecognized server_content format {inline_data.mimeType}")
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -665,7 +667,7 @@ class GeminiMultimodalLiveLLMService(LLMService):
|
|||||||
self._bot_audio_buffer.extend(audio)
|
self._bot_audio_buffer.extend(audio)
|
||||||
frame = TTSAudioRawFrame(
|
frame = TTSAudioRawFrame(
|
||||||
audio=audio,
|
audio=audio,
|
||||||
sample_rate=24000,
|
sample_rate=self._sample_rate,
|
||||||
num_channels=1,
|
num_channels=1,
|
||||||
)
|
)
|
||||||
await self.push_frame(frame)
|
await self.push_frame(frame)
|
||||||
|
|||||||
@@ -131,7 +131,6 @@ def language_to_gladia_language(language: Language) -> str | None:
|
|||||||
|
|
||||||
class GladiaSTTService(STTService):
|
class GladiaSTTService(STTService):
|
||||||
class InputParams(BaseModel):
|
class InputParams(BaseModel):
|
||||||
sample_rate: Optional[int] = 16000
|
|
||||||
language: Optional[Language] = Language.EN
|
language: Optional[Language] = Language.EN
|
||||||
endpointing: Optional[float] = 0.2
|
endpointing: Optional[float] = 0.2
|
||||||
maximum_duration_without_endpointing: Optional[int] = 10
|
maximum_duration_without_endpointing: Optional[int] = 10
|
||||||
@@ -144,17 +143,18 @@ class GladiaSTTService(STTService):
|
|||||||
api_key: str,
|
api_key: str,
|
||||||
url: str = "https://api.gladia.io/v2/live",
|
url: str = "https://api.gladia.io/v2/live",
|
||||||
confidence: float = 0.5,
|
confidence: float = 0.5,
|
||||||
|
sample_rate: Optional[int] = None,
|
||||||
params: InputParams = InputParams(),
|
params: InputParams = InputParams(),
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
super().__init__(**kwargs)
|
super().__init__(sample_rate=sample_rate, **kwargs)
|
||||||
|
|
||||||
self._api_key = api_key
|
self._api_key = api_key
|
||||||
self._url = url
|
self._url = url
|
||||||
self._settings = {
|
self._settings = {
|
||||||
"encoding": "wav/pcm",
|
"encoding": "wav/pcm",
|
||||||
"bit_depth": 16,
|
"bit_depth": 16,
|
||||||
"sample_rate": params.sample_rate,
|
"sample_rate": 0,
|
||||||
"channels": 1,
|
"channels": 1,
|
||||||
"language_config": {
|
"language_config": {
|
||||||
"languages": [self.language_to_service_language(params.language)]
|
"languages": [self.language_to_service_language(params.language)]
|
||||||
@@ -178,6 +178,7 @@ class GladiaSTTService(STTService):
|
|||||||
|
|
||||||
async def start(self, frame: StartFrame):
|
async def start(self, frame: StartFrame):
|
||||||
await super().start(frame)
|
await super().start(frame)
|
||||||
|
self._settings["sample_rate"] = self.sample_rate
|
||||||
response = await self._setup_gladia()
|
response = await self._setup_gladia()
|
||||||
self._websocket = await websockets.connect(response["url"])
|
self._websocket = await websockets.connect(response["url"])
|
||||||
self._receive_task = self.create_task(self._receive_task_handler())
|
self._receive_task = self.create_task(self._receive_task_handler())
|
||||||
|
|||||||
@@ -883,14 +883,13 @@ class GoogleTTSService(TTSService):
|
|||||||
credentials: Optional[str] = None,
|
credentials: Optional[str] = None,
|
||||||
credentials_path: Optional[str] = None,
|
credentials_path: Optional[str] = None,
|
||||||
voice_id: str = "en-US-Neural2-A",
|
voice_id: str = "en-US-Neural2-A",
|
||||||
sample_rate: int = 24000,
|
sample_rate: Optional[int] = None,
|
||||||
params: InputParams = InputParams(),
|
params: InputParams = InputParams(),
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
super().__init__(sample_rate=sample_rate, **kwargs)
|
super().__init__(sample_rate=sample_rate, **kwargs)
|
||||||
|
|
||||||
self._settings = {
|
self._settings = {
|
||||||
"sample_rate": sample_rate,
|
|
||||||
"pitch": params.pitch,
|
"pitch": params.pitch,
|
||||||
"rate": params.rate,
|
"rate": params.rate,
|
||||||
"volume": params.volume,
|
"volume": params.volume,
|
||||||
@@ -996,7 +995,7 @@ class GoogleTTSService(TTSService):
|
|||||||
)
|
)
|
||||||
audio_config = texttospeech_v1.AudioConfig(
|
audio_config = texttospeech_v1.AudioConfig(
|
||||||
audio_encoding=texttospeech_v1.AudioEncoding.LINEAR16,
|
audio_encoding=texttospeech_v1.AudioEncoding.LINEAR16,
|
||||||
sample_rate_hertz=self._settings["sample_rate"],
|
sample_rate_hertz=self.sample_rate,
|
||||||
)
|
)
|
||||||
|
|
||||||
request = texttospeech_v1.SynthesizeSpeechRequest(
|
request = texttospeech_v1.SynthesizeSpeechRequest(
|
||||||
@@ -1019,7 +1018,7 @@ class GoogleTTSService(TTSService):
|
|||||||
if not chunk:
|
if not chunk:
|
||||||
break
|
break
|
||||||
await self.stop_ttfb_metrics()
|
await self.stop_ttfb_metrics()
|
||||||
frame = TTSAudioRawFrame(chunk, self._settings["sample_rate"], 1)
|
frame = TTSAudioRawFrame(chunk, self.sample_rate, 1)
|
||||||
yield frame
|
yield frame
|
||||||
await asyncio.sleep(0) # Allow other tasks to run
|
await asyncio.sleep(0) # Allow other tasks to run
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from typing import AsyncGenerator
|
from typing import AsyncGenerator, Optional
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ class LmntTTSService(TTSService, WebsocketService):
|
|||||||
*,
|
*,
|
||||||
api_key: str,
|
api_key: str,
|
||||||
voice_id: str,
|
voice_id: str,
|
||||||
sample_rate: int = 24000,
|
sample_rate: Optional[int] = None,
|
||||||
language: Language = Language.EN,
|
language: Language = Language.EN,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
@@ -81,7 +81,6 @@ class LmntTTSService(TTSService, WebsocketService):
|
|||||||
self._api_key = api_key
|
self._api_key = api_key
|
||||||
self._voice_id = voice_id
|
self._voice_id = voice_id
|
||||||
self._settings = {
|
self._settings = {
|
||||||
"sample_rate": sample_rate,
|
|
||||||
"language": self.language_to_service_language(language),
|
"language": self.language_to_service_language(language),
|
||||||
"format": "raw", # Use raw format for direct PCM data
|
"format": "raw", # Use raw format for direct PCM data
|
||||||
}
|
}
|
||||||
@@ -132,7 +131,7 @@ class LmntTTSService(TTSService, WebsocketService):
|
|||||||
"X-API-Key": self._api_key,
|
"X-API-Key": self._api_key,
|
||||||
"voice": self._voice_id,
|
"voice": self._voice_id,
|
||||||
"format": self._settings["format"],
|
"format": self._settings["format"],
|
||||||
"sample_rate": self._settings["sample_rate"],
|
"sample_rate": self.sample_rate,
|
||||||
"language": self._settings["language"],
|
"language": self._settings["language"],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,7 +174,7 @@ class LmntTTSService(TTSService, WebsocketService):
|
|||||||
await self.stop_ttfb_metrics()
|
await self.stop_ttfb_metrics()
|
||||||
frame = TTSAudioRawFrame(
|
frame = TTSAudioRawFrame(
|
||||||
audio=message,
|
audio=message,
|
||||||
sample_rate=self._settings["sample_rate"],
|
sample_rate=self.sample_rate,
|
||||||
num_channels=1,
|
num_channels=1,
|
||||||
)
|
)
|
||||||
await self.push_frame(frame)
|
await self.push_frame(frame)
|
||||||
|
|||||||
@@ -415,17 +415,14 @@ class OpenAITTSService(TTSService):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
api_key: str | None = None,
|
api_key: Optional[str] = None,
|
||||||
voice: str = "alloy",
|
voice: str = "alloy",
|
||||||
model: Literal["tts-1", "tts-1-hd"] = "tts-1",
|
model: Literal["tts-1", "tts-1-hd"] = "tts-1",
|
||||||
sample_rate: int = 24000,
|
sample_rate: Optional[int] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
super().__init__(sample_rate=sample_rate, **kwargs)
|
super().__init__(sample_rate=sample_rate, **kwargs)
|
||||||
|
|
||||||
self._settings = {
|
|
||||||
"sample_rate": sample_rate,
|
|
||||||
}
|
|
||||||
self.set_model_name(model)
|
self.set_model_name(model)
|
||||||
self.set_voice(voice)
|
self.set_voice(voice)
|
||||||
|
|
||||||
@@ -465,7 +462,7 @@ class OpenAITTSService(TTSService):
|
|||||||
async for chunk in r.iter_bytes(8192):
|
async for chunk in r.iter_bytes(8192):
|
||||||
if len(chunk) > 0:
|
if len(chunk) > 0:
|
||||||
await self.stop_ttfb_metrics()
|
await self.stop_ttfb_metrics()
|
||||||
frame = TTSAudioRawFrame(chunk, self._settings["sample_rate"], 1)
|
frame = TTSAudioRawFrame(chunk, self.sample_rate, 1)
|
||||||
yield frame
|
yield frame
|
||||||
yield TTSStoppedFrame()
|
yield TTSStoppedFrame()
|
||||||
except BadRequestError as e:
|
except BadRequestError as e:
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ class PlayHTTTSService(TTSService, WebsocketService):
|
|||||||
user_id: str,
|
user_id: str,
|
||||||
voice_url: str,
|
voice_url: str,
|
||||||
voice_engine: str = "Play3.0-mini",
|
voice_engine: str = "Play3.0-mini",
|
||||||
sample_rate: int = 24000,
|
sample_rate: Optional[int] = None,
|
||||||
output_format: str = "wav",
|
output_format: str = "wav",
|
||||||
params: InputParams = InputParams(),
|
params: InputParams = InputParams(),
|
||||||
**kwargs,
|
**kwargs,
|
||||||
@@ -132,7 +132,6 @@ class PlayHTTTSService(TTSService, WebsocketService):
|
|||||||
self._request_id = None
|
self._request_id = None
|
||||||
|
|
||||||
self._settings = {
|
self._settings = {
|
||||||
"sample_rate": sample_rate,
|
|
||||||
"language": self.language_to_service_language(params.language)
|
"language": self.language_to_service_language(params.language)
|
||||||
if params.language
|
if params.language
|
||||||
else "english",
|
else "english",
|
||||||
@@ -250,7 +249,7 @@ class PlayHTTTSService(TTSService, WebsocketService):
|
|||||||
if message.startswith(b"RIFF"):
|
if message.startswith(b"RIFF"):
|
||||||
continue
|
continue
|
||||||
await self.stop_ttfb_metrics()
|
await self.stop_ttfb_metrics()
|
||||||
frame = TTSAudioRawFrame(message, self._settings["sample_rate"], 1)
|
frame = TTSAudioRawFrame(message, self.sample_rate, 1)
|
||||||
await self.push_frame(frame)
|
await self.push_frame(frame)
|
||||||
else:
|
else:
|
||||||
logger.debug(f"Received text message: {message}")
|
logger.debug(f"Received text message: {message}")
|
||||||
@@ -301,7 +300,7 @@ class PlayHTTTSService(TTSService, WebsocketService):
|
|||||||
"voice": self._voice_id,
|
"voice": self._voice_id,
|
||||||
"voice_engine": self._settings["voice_engine"],
|
"voice_engine": self._settings["voice_engine"],
|
||||||
"output_format": self._settings["output_format"],
|
"output_format": self._settings["output_format"],
|
||||||
"sample_rate": self._settings["sample_rate"],
|
"sample_rate": self.sample_rate,
|
||||||
"language": self._settings["language"],
|
"language": self._settings["language"],
|
||||||
"speed": self._settings["speed"],
|
"speed": self._settings["speed"],
|
||||||
"seed": self._settings["seed"],
|
"seed": self._settings["seed"],
|
||||||
@@ -339,7 +338,7 @@ class PlayHTHttpTTSService(TTSService):
|
|||||||
user_id: str,
|
user_id: str,
|
||||||
voice_url: str,
|
voice_url: str,
|
||||||
voice_engine: str = "Play3.0-mini-http", # Options: Play3.0-mini-http, Play3.0-mini-ws
|
voice_engine: str = "Play3.0-mini-http", # Options: Play3.0-mini-http, Play3.0-mini-ws
|
||||||
sample_rate: int = 24000,
|
sample_rate: Optional[int] = None,
|
||||||
params: InputParams = InputParams(),
|
params: InputParams = InputParams(),
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
@@ -353,7 +352,6 @@ class PlayHTHttpTTSService(TTSService):
|
|||||||
api_key=self._api_key,
|
api_key=self._api_key,
|
||||||
)
|
)
|
||||||
self._settings = {
|
self._settings = {
|
||||||
"sample_rate": sample_rate,
|
|
||||||
"language": self.language_to_service_language(params.language)
|
"language": self.language_to_service_language(params.language)
|
||||||
if params.language
|
if params.language
|
||||||
else "english",
|
else "english",
|
||||||
@@ -377,7 +375,7 @@ class PlayHTHttpTTSService(TTSService):
|
|||||||
self._options = TTSOptions(
|
self._options = TTSOptions(
|
||||||
voice=self._voice_id,
|
voice=self._voice_id,
|
||||||
language=playht_language,
|
language=playht_language,
|
||||||
sample_rate=self._settings["sample_rate"],
|
sample_rate=self.sample_rate,
|
||||||
format=self._settings["format"],
|
format=self._settings["format"],
|
||||||
speed=self._settings["speed"],
|
speed=self._settings["speed"],
|
||||||
seed=self._settings["seed"],
|
seed=self._settings["seed"],
|
||||||
@@ -422,7 +420,7 @@ class PlayHTHttpTTSService(TTSService):
|
|||||||
else:
|
else:
|
||||||
if len(chunk):
|
if len(chunk):
|
||||||
await self.stop_ttfb_metrics()
|
await self.stop_ttfb_metrics()
|
||||||
frame = TTSAudioRawFrame(chunk, self._settings["sample_rate"], 1)
|
frame = TTSAudioRawFrame(chunk, self.sample_rate, 1)
|
||||||
yield frame
|
yield frame
|
||||||
yield TTSStoppedFrame()
|
yield TTSStoppedFrame()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class RimeHttpTTSService(TTSService):
|
|||||||
api_key: str,
|
api_key: str,
|
||||||
voice_id: str = "eva",
|
voice_id: str = "eva",
|
||||||
model: str = "mist",
|
model: str = "mist",
|
||||||
sample_rate: int = 24000,
|
sample_rate: Optional[int] = None,
|
||||||
params: InputParams = InputParams(),
|
params: InputParams = InputParams(),
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
@@ -43,7 +43,6 @@ class RimeHttpTTSService(TTSService):
|
|||||||
self._api_key = api_key
|
self._api_key = api_key
|
||||||
self._base_url = "https://users.rime.ai/v1/rime-tts"
|
self._base_url = "https://users.rime.ai/v1/rime-tts"
|
||||||
self._settings = {
|
self._settings = {
|
||||||
"samplingRate": sample_rate,
|
|
||||||
"speedAlpha": params.speed_alpha,
|
"speedAlpha": params.speed_alpha,
|
||||||
"reduceLatency": params.reduce_latency,
|
"reduceLatency": params.reduce_latency,
|
||||||
"pauseBetweenBrackets": params.pause_between_brackets,
|
"pauseBetweenBrackets": params.pause_between_brackets,
|
||||||
@@ -71,6 +70,7 @@ class RimeHttpTTSService(TTSService):
|
|||||||
payload["text"] = text
|
payload["text"] = text
|
||||||
payload["speaker"] = self._voice_id
|
payload["speaker"] = self._voice_id
|
||||||
payload["modelId"] = self._model_name
|
payload["modelId"] = self._model_name
|
||||||
|
payload["samplingRate"] = self.sample_rate
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self.start_ttfb_metrics()
|
await self.start_ttfb_metrics()
|
||||||
@@ -96,7 +96,7 @@ class RimeHttpTTSService(TTSService):
|
|||||||
first_chunk = False
|
first_chunk = False
|
||||||
|
|
||||||
if chunk:
|
if chunk:
|
||||||
frame = TTSAudioRawFrame(chunk, self._settings["samplingRate"], 1)
|
frame = TTSAudioRawFrame(chunk, self.sample_rate, 1)
|
||||||
yield frame
|
yield frame
|
||||||
|
|
||||||
yield TTSStoppedFrame()
|
yield TTSStoppedFrame()
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ class FastPitchTTSService(TTSService):
|
|||||||
api_key: str,
|
api_key: str,
|
||||||
server: str = "grpc.nvcf.nvidia.com:443",
|
server: str = "grpc.nvcf.nvidia.com:443",
|
||||||
voice_id: str = "English-US.Female-1",
|
voice_id: str = "English-US.Female-1",
|
||||||
sample_rate: int = 24000,
|
sample_rate: Optional[int] = None,
|
||||||
function_id: str = "0149dedb-2be8-4195-b9a0-e57e0e14f972",
|
function_id: str = "0149dedb-2be8-4195-b9a0-e57e0e14f972",
|
||||||
params: InputParams = InputParams(),
|
params: InputParams = InputParams(),
|
||||||
**kwargs,
|
**kwargs,
|
||||||
@@ -57,7 +57,6 @@ class FastPitchTTSService(TTSService):
|
|||||||
super().__init__(sample_rate=sample_rate, **kwargs)
|
super().__init__(sample_rate=sample_rate, **kwargs)
|
||||||
self._api_key = api_key
|
self._api_key = api_key
|
||||||
self._voice_id = voice_id
|
self._voice_id = voice_id
|
||||||
self._sample_rate = sample_rate
|
|
||||||
self._language_code = params.language
|
self._language_code = params.language
|
||||||
self._quality = params.quality
|
self._quality = params.quality
|
||||||
|
|
||||||
@@ -87,7 +86,7 @@ class FastPitchTTSService(TTSService):
|
|||||||
text,
|
text,
|
||||||
self._voice_id,
|
self._voice_id,
|
||||||
self._language_code,
|
self._language_code,
|
||||||
sample_rate_hz=self._sample_rate,
|
sample_rate_hz=self.sample_rate,
|
||||||
audio_prompt_file=None,
|
audio_prompt_file=None,
|
||||||
quality=self._quality,
|
quality=self._quality,
|
||||||
custom_dictionary={},
|
custom_dictionary={},
|
||||||
@@ -114,7 +113,7 @@ class FastPitchTTSService(TTSService):
|
|||||||
await self.stop_ttfb_metrics()
|
await self.stop_ttfb_metrics()
|
||||||
frame = TTSAudioRawFrame(
|
frame = TTSAudioRawFrame(
|
||||||
audio=resp.audio,
|
audio=resp.audio,
|
||||||
sample_rate=self._sample_rate,
|
sample_rate=self.sample_rate,
|
||||||
num_channels=1,
|
num_channels=1,
|
||||||
)
|
)
|
||||||
yield frame
|
yield frame
|
||||||
@@ -136,10 +135,11 @@ class ParakeetSTTService(STTService):
|
|||||||
api_key: str,
|
api_key: str,
|
||||||
server: str = "grpc.nvcf.nvidia.com:443",
|
server: str = "grpc.nvcf.nvidia.com:443",
|
||||||
function_id: str = "1598d209-5e27-4d3c-8079-4751568b1081",
|
function_id: str = "1598d209-5e27-4d3c-8079-4751568b1081",
|
||||||
|
sample_rate: Optional[int] = None,
|
||||||
params: InputParams = InputParams(),
|
params: InputParams = InputParams(),
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
super().__init__(**kwargs)
|
super().__init__(sample_rate=sample_rate, **kwargs)
|
||||||
self._api_key = api_key
|
self._api_key = api_key
|
||||||
self._profanity_filter = False
|
self._profanity_filter = False
|
||||||
self._automatic_punctuation = False
|
self._automatic_punctuation = False
|
||||||
@@ -154,7 +154,6 @@ class ParakeetSTTService(STTService):
|
|||||||
self._stop_history_eou = -1
|
self._stop_history_eou = -1
|
||||||
self._stop_threshold_eou = -1.0
|
self._stop_threshold_eou = -1.0
|
||||||
self._custom_configuration = ""
|
self._custom_configuration = ""
|
||||||
self._sample_rate: int = 16000
|
|
||||||
|
|
||||||
self.set_model_name("parakeet-ctc-1.1b-asr")
|
self.set_model_name("parakeet-ctc-1.1b-asr")
|
||||||
|
|
||||||
@@ -166,6 +165,14 @@ class ParakeetSTTService(STTService):
|
|||||||
|
|
||||||
self._asr_service = riva.client.ASRService(auth)
|
self._asr_service = riva.client.ASRService(auth)
|
||||||
|
|
||||||
|
self._queue = asyncio.Queue()
|
||||||
|
|
||||||
|
def can_generate_metrics(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def start(self, frame: StartFrame):
|
||||||
|
await super().start(frame)
|
||||||
|
|
||||||
config = riva.client.StreamingRecognitionConfig(
|
config = riva.client.StreamingRecognitionConfig(
|
||||||
config=riva.client.RecognitionConfig(
|
config=riva.client.RecognitionConfig(
|
||||||
encoding=riva.client.AudioEncoding.LINEAR_PCM,
|
encoding=riva.client.AudioEncoding.LINEAR_PCM,
|
||||||
@@ -175,14 +182,16 @@ class ParakeetSTTService(STTService):
|
|||||||
profanity_filter=self._profanity_filter,
|
profanity_filter=self._profanity_filter,
|
||||||
enable_automatic_punctuation=self._automatic_punctuation,
|
enable_automatic_punctuation=self._automatic_punctuation,
|
||||||
verbatim_transcripts=not self._no_verbatim_transcripts,
|
verbatim_transcripts=not self._no_verbatim_transcripts,
|
||||||
sample_rate_hertz=self._sample_rate,
|
sample_rate_hertz=self.sample_rate,
|
||||||
audio_channel_count=1,
|
audio_channel_count=1,
|
||||||
),
|
),
|
||||||
interim_results=True,
|
interim_results=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
riva.client.add_word_boosting_to_config(
|
riva.client.add_word_boosting_to_config(
|
||||||
config, self._boosted_lm_words, self._boosted_lm_score
|
config, self._boosted_lm_words, self._boosted_lm_score
|
||||||
)
|
)
|
||||||
|
|
||||||
riva.client.add_endpoint_parameters_to_config(
|
riva.client.add_endpoint_parameters_to_config(
|
||||||
config,
|
config,
|
||||||
self._start_history,
|
self._start_history,
|
||||||
@@ -193,15 +202,9 @@ class ParakeetSTTService(STTService):
|
|||||||
self._stop_threshold_eou,
|
self._stop_threshold_eou,
|
||||||
)
|
)
|
||||||
riva.client.add_custom_configuration_to_config(config, self._custom_configuration)
|
riva.client.add_custom_configuration_to_config(config, self._custom_configuration)
|
||||||
|
|
||||||
self._config = config
|
self._config = config
|
||||||
|
|
||||||
self._queue = asyncio.Queue()
|
|
||||||
|
|
||||||
def can_generate_metrics(self) -> bool:
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def start(self, frame: StartFrame):
|
|
||||||
await super().start(frame)
|
|
||||||
self._thread_task = self.create_task(self._thread_task_handler())
|
self._thread_task = self.create_task(self._thread_task_handler())
|
||||||
self._response_task = self.create_task(self._response_task_handler())
|
self._response_task = self.create_task(self._response_task_handler())
|
||||||
self._response_queue = asyncio.Queue()
|
self._response_queue = asyncio.Queue()
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
# SPDX-License-Identifier: BSD 2-Clause License
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
#
|
#
|
||||||
|
|
||||||
from typing import Any, AsyncGenerator, Dict
|
from typing import Any, AsyncGenerator, Dict, Optional
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@@ -76,7 +76,7 @@ class XTTSService(TTSService):
|
|||||||
base_url: str,
|
base_url: str,
|
||||||
aiohttp_session: aiohttp.ClientSession,
|
aiohttp_session: aiohttp.ClientSession,
|
||||||
language: Language = Language.EN,
|
language: Language = Language.EN,
|
||||||
sample_rate: int = 24000,
|
sample_rate: Optional[int] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
super().__init__(sample_rate=sample_rate, **kwargs)
|
super().__init__(sample_rate=sample_rate, **kwargs)
|
||||||
@@ -164,18 +164,18 @@ class XTTSService(TTSService):
|
|||||||
|
|
||||||
# XTTS uses 24000 so we need to resample to our desired rate.
|
# XTTS uses 24000 so we need to resample to our desired rate.
|
||||||
resampled_audio = await self._resampler.resample(
|
resampled_audio = await self._resampler.resample(
|
||||||
bytes(process_data), 24000, self._sample_rate
|
bytes(process_data), 24000, self.sample_rate
|
||||||
)
|
)
|
||||||
# Create the frame with the resampled audio
|
# Create the frame with the resampled audio
|
||||||
frame = TTSAudioRawFrame(resampled_audio, self._sample_rate, 1)
|
frame = TTSAudioRawFrame(resampled_audio, self.sample_rate, 1)
|
||||||
yield frame
|
yield frame
|
||||||
|
|
||||||
# Process any remaining data in the buffer.
|
# Process any remaining data in the buffer.
|
||||||
if len(buffer) > 0:
|
if len(buffer) > 0:
|
||||||
resampled_audio = await self._resampler.resample(
|
resampled_audio = await self._resampler.resample(
|
||||||
bytes(buffer), 24000, self._sample_rate
|
bytes(buffer), 24000, self.sample_rate
|
||||||
)
|
)
|
||||||
frame = TTSAudioRawFrame(resampled_audio, self._sample_rate, 1)
|
frame = TTSAudioRawFrame(resampled_audio, self.sample_rate, 1)
|
||||||
yield frame
|
yield frame
|
||||||
|
|
||||||
yield TTSStoppedFrame()
|
yield TTSStoppedFrame()
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ class BaseInputTransport(FrameProcessor):
|
|||||||
|
|
||||||
self._params = params
|
self._params = params
|
||||||
|
|
||||||
|
# Input sample rate. It will be initialized on StartFrame.
|
||||||
|
self._sample_rate = 0
|
||||||
|
|
||||||
# We read audio from a single queue one at a time and we then run VAD in
|
# We read audio from a single queue one at a time and we then run VAD in
|
||||||
# a thread. Therefore, only one thread should be necessary.
|
# a thread. Therefore, only one thread should be necessary.
|
||||||
self._executor = ThreadPoolExecutor(max_workers=1)
|
self._executor = ThreadPoolExecutor(max_workers=1)
|
||||||
@@ -43,10 +46,23 @@ class BaseInputTransport(FrameProcessor):
|
|||||||
# if passthrough is enabled.
|
# if passthrough is enabled.
|
||||||
self._audio_task = None
|
self._audio_task = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sample_rate(self) -> int:
|
||||||
|
return self._sample_rate
|
||||||
|
|
||||||
|
@property
|
||||||
|
def vad_analyzer(self) -> VADAnalyzer | None:
|
||||||
|
return self._params.vad_analyzer
|
||||||
|
|
||||||
async def start(self, frame: StartFrame):
|
async def start(self, frame: StartFrame):
|
||||||
|
self._sample_rate = self._params.audio_in_sample_rate or frame.audio_in_sample_rate
|
||||||
|
|
||||||
|
# Configure VAD analyzer.
|
||||||
|
if self._params.vad_enabled and self._params.vad_analyzer:
|
||||||
|
self._params.vad_analyzer.set_sample_rate(self._sample_rate)
|
||||||
# Start audio filter.
|
# Start audio filter.
|
||||||
if self._params.audio_in_filter:
|
if self._params.audio_in_filter:
|
||||||
await self._params.audio_in_filter.start(self._params.audio_in_sample_rate)
|
await self._params.audio_in_filter.start(self._sample_rate)
|
||||||
# Create audio input queue and task if needed.
|
# Create audio input queue and task if needed.
|
||||||
if self._params.audio_in_enabled or self._params.vad_enabled:
|
if self._params.audio_in_enabled or self._params.vad_enabled:
|
||||||
self._audio_in_queue = asyncio.Queue()
|
self._audio_in_queue = asyncio.Queue()
|
||||||
@@ -67,9 +83,6 @@ class BaseInputTransport(FrameProcessor):
|
|||||||
await self.cancel_task(self._audio_task)
|
await self.cancel_task(self._audio_task)
|
||||||
self._audio_task = None
|
self._audio_task = None
|
||||||
|
|
||||||
def vad_analyzer(self) -> VADAnalyzer | None:
|
|
||||||
return self._params.vad_analyzer
|
|
||||||
|
|
||||||
async def push_audio_frame(self, frame: InputAudioRawFrame):
|
async def push_audio_frame(self, frame: InputAudioRawFrame):
|
||||||
if self._params.audio_in_enabled or self._params.vad_enabled:
|
if self._params.audio_in_enabled or self._params.vad_enabled:
|
||||||
await self._audio_in_queue.put(frame)
|
await self._audio_in_queue.put(frame)
|
||||||
@@ -104,9 +117,8 @@ class BaseInputTransport(FrameProcessor):
|
|||||||
await self.push_frame(frame, direction)
|
await self.push_frame(frame, direction)
|
||||||
await self.stop(frame)
|
await self.stop(frame)
|
||||||
elif isinstance(frame, VADParamsUpdateFrame):
|
elif isinstance(frame, VADParamsUpdateFrame):
|
||||||
vad_analyzer = self.vad_analyzer()
|
if self.vad_analyzer:
|
||||||
if vad_analyzer:
|
self.vad_analyzer.set_params(frame.params)
|
||||||
vad_analyzer.set_params(frame.params)
|
|
||||||
elif isinstance(frame, FilterUpdateSettingsFrame) and self._params.audio_in_filter:
|
elif isinstance(frame, FilterUpdateSettingsFrame) and self._params.audio_in_filter:
|
||||||
await self._params.audio_in_filter.process_frame(frame)
|
await self._params.audio_in_filter.process_frame(frame)
|
||||||
# Other frames
|
# Other frames
|
||||||
@@ -140,11 +152,10 @@ class BaseInputTransport(FrameProcessor):
|
|||||||
|
|
||||||
async def _vad_analyze(self, audio_frame: InputAudioRawFrame) -> VADState:
|
async def _vad_analyze(self, audio_frame: InputAudioRawFrame) -> VADState:
|
||||||
state = VADState.QUIET
|
state = VADState.QUIET
|
||||||
vad_analyzer = self.vad_analyzer()
|
if self.vad_analyzer:
|
||||||
if vad_analyzer:
|
|
||||||
logger.trace(f"{self}: analyzing VAD on {audio_frame}")
|
logger.trace(f"{self}: analyzing VAD on {audio_frame}")
|
||||||
state = await self.get_event_loop().run_in_executor(
|
state = await self.get_event_loop().run_in_executor(
|
||||||
self._executor, vad_analyzer.analyze_audio, audio_frame.audio
|
self._executor, self.vad_analyzer.analyze_audio, audio_frame.audio
|
||||||
)
|
)
|
||||||
logger.trace(f"{self}: done analyzing VAD on {audio_frame}")
|
logger.trace(f"{self}: done analyzing VAD on {audio_frame}")
|
||||||
return state
|
return state
|
||||||
|
|||||||
@@ -57,12 +57,11 @@ class BaseOutputTransport(FrameProcessor):
|
|||||||
# framerate.
|
# framerate.
|
||||||
self._camera_images = None
|
self._camera_images = None
|
||||||
|
|
||||||
# We will write 20ms audio at a time. If we receive long audio frames we
|
# Output sample rate. It will be initialized on StartFrame.
|
||||||
# will chunk them. This will help with interruption handling.
|
self._sample_rate = 0
|
||||||
audio_bytes_10ms = (
|
|
||||||
int(self._params.audio_out_sample_rate / 100) * self._params.audio_out_channels * 2
|
# Chunk size that will be written. It will be computed on StartFrame
|
||||||
)
|
self._audio_chunk_size = 0
|
||||||
self._audio_chunk_size = audio_bytes_10ms * 2
|
|
||||||
self._audio_buffer = bytearray()
|
self._audio_buffer = bytearray()
|
||||||
|
|
||||||
self._stopped_event = asyncio.Event()
|
self._stopped_event = asyncio.Event()
|
||||||
@@ -70,10 +69,21 @@ class BaseOutputTransport(FrameProcessor):
|
|||||||
# Indicates if the bot is currently speaking.
|
# Indicates if the bot is currently speaking.
|
||||||
self._bot_speaking = False
|
self._bot_speaking = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sample_rate(self) -> int:
|
||||||
|
return self._sample_rate
|
||||||
|
|
||||||
async def start(self, frame: StartFrame):
|
async def start(self, frame: StartFrame):
|
||||||
|
self._sample_rate = self._params.audio_out_sample_rate or frame.audio_out_sample_rate
|
||||||
|
|
||||||
|
# We will write 20ms audio at a time. If we receive long audio frames we
|
||||||
|
# will chunk them. This will help with interruption handling.
|
||||||
|
audio_bytes_10ms = int(self._sample_rate / 100) * self._params.audio_out_channels * 2
|
||||||
|
self._audio_chunk_size = audio_bytes_10ms * 2
|
||||||
|
|
||||||
# Start audio mixer.
|
# Start audio mixer.
|
||||||
if self._params.audio_out_mixer:
|
if self._params.audio_out_mixer:
|
||||||
await self._params.audio_out_mixer.start(self._params.audio_out_sample_rate)
|
await self._params.audio_out_mixer.start(self._sample_rate)
|
||||||
self._create_camera_task()
|
self._create_camera_task()
|
||||||
self._create_sink_tasks()
|
self._create_sink_tasks()
|
||||||
|
|
||||||
@@ -298,7 +308,7 @@ class BaseOutputTransport(FrameProcessor):
|
|||||||
# Generate an audio frame with only the mixer's part.
|
# Generate an audio frame with only the mixer's part.
|
||||||
frame = OutputAudioRawFrame(
|
frame = OutputAudioRawFrame(
|
||||||
audio=await self._params.audio_out_mixer.mix(silence),
|
audio=await self._params.audio_out_mixer.mix(silence),
|
||||||
sample_rate=self._params.audio_out_sample_rate,
|
sample_rate=self._sample_rate,
|
||||||
num_channels=self._params.audio_out_channels,
|
num_channels=self._params.audio_out_channels,
|
||||||
)
|
)
|
||||||
yield frame
|
yield frame
|
||||||
|
|||||||
@@ -31,12 +31,12 @@ class TransportParams(BaseModel):
|
|||||||
camera_out_color_format: str = "RGB"
|
camera_out_color_format: str = "RGB"
|
||||||
audio_out_enabled: bool = False
|
audio_out_enabled: bool = False
|
||||||
audio_out_is_live: bool = False
|
audio_out_is_live: bool = False
|
||||||
audio_out_sample_rate: int = 24000
|
audio_out_sample_rate: Optional[int] = None
|
||||||
audio_out_channels: int = 1
|
audio_out_channels: int = 1
|
||||||
audio_out_bitrate: int = 96000
|
audio_out_bitrate: int = 96000
|
||||||
audio_out_mixer: Optional[BaseAudioMixer] = None
|
audio_out_mixer: Optional[BaseAudioMixer] = None
|
||||||
audio_in_enabled: bool = False
|
audio_in_enabled: bool = False
|
||||||
audio_in_sample_rate: int = 16000
|
audio_in_sample_rate: Optional[int] = None
|
||||||
audio_in_channels: int = 1
|
audio_in_channels: int = 1
|
||||||
audio_in_filter: Optional[BaseAudioFilter] = None
|
audio_in_filter: Optional[BaseAudioFilter] = None
|
||||||
vad_enabled: bool = False
|
vad_enabled: bool = False
|
||||||
|
|||||||
@@ -28,35 +28,40 @@ except ModuleNotFoundError as e:
|
|||||||
class LocalAudioInputTransport(BaseInputTransport):
|
class LocalAudioInputTransport(BaseInputTransport):
|
||||||
def __init__(self, py_audio: pyaudio.PyAudio, params: TransportParams):
|
def __init__(self, py_audio: pyaudio.PyAudio, params: TransportParams):
|
||||||
super().__init__(params)
|
super().__init__(params)
|
||||||
|
self._py_audio = py_audio
|
||||||
|
self._in_stream = None
|
||||||
|
self._sample_rate = 0
|
||||||
|
|
||||||
sample_rate = self._params.audio_in_sample_rate
|
async def start(self, frame: StartFrame):
|
||||||
num_frames = int(sample_rate / 100) * 2 # 20ms of audio
|
await super().start(frame)
|
||||||
|
|
||||||
self._in_stream = py_audio.open(
|
self._sample_rate = self._params.audio_in_sample_rate or frame.audio_in_sample_rate
|
||||||
format=py_audio.get_format_from_width(2),
|
num_frames = int(self._sample_rate / 100) * 2 # 20ms of audio
|
||||||
channels=params.audio_in_channels,
|
|
||||||
rate=params.audio_in_sample_rate,
|
self._in_stream = self._py_audio.open(
|
||||||
|
format=self._py_audio.get_format_from_width(2),
|
||||||
|
channels=self._params.audio_in_channels,
|
||||||
|
rate=self._sample_rate,
|
||||||
frames_per_buffer=num_frames,
|
frames_per_buffer=num_frames,
|
||||||
stream_callback=self._audio_in_callback,
|
stream_callback=self._audio_in_callback,
|
||||||
input=True,
|
input=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def start(self, frame: StartFrame):
|
|
||||||
await super().start(frame)
|
|
||||||
self._in_stream.start_stream()
|
self._in_stream.start_stream()
|
||||||
|
|
||||||
async def cleanup(self):
|
async def cleanup(self):
|
||||||
await super().cleanup()
|
await super().cleanup()
|
||||||
self._in_stream.stop_stream()
|
if self._in_stream:
|
||||||
# This is not very pretty (taken from PyAudio docs).
|
self._in_stream.stop_stream()
|
||||||
while self._in_stream.is_active():
|
# This is not very pretty (taken from PyAudio docs).
|
||||||
await asyncio.sleep(0.1)
|
while self._in_stream.is_active():
|
||||||
self._in_stream.close()
|
await asyncio.sleep(0.1)
|
||||||
|
self._in_stream.close()
|
||||||
|
self._in_stream = None
|
||||||
|
|
||||||
def _audio_in_callback(self, in_data, frame_count, time_info, status):
|
def _audio_in_callback(self, in_data, frame_count, time_info, status):
|
||||||
frame = InputAudioRawFrame(
|
frame = InputAudioRawFrame(
|
||||||
audio=in_data,
|
audio=in_data,
|
||||||
sample_rate=self._params.audio_in_sample_rate,
|
sample_rate=self._sample_rate,
|
||||||
num_channels=self._params.audio_in_channels,
|
num_channels=self._params.audio_in_channels,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -68,32 +73,41 @@ class LocalAudioInputTransport(BaseInputTransport):
|
|||||||
class LocalAudioOutputTransport(BaseOutputTransport):
|
class LocalAudioOutputTransport(BaseOutputTransport):
|
||||||
def __init__(self, py_audio: pyaudio.PyAudio, params: TransportParams):
|
def __init__(self, py_audio: pyaudio.PyAudio, params: TransportParams):
|
||||||
super().__init__(params)
|
super().__init__(params)
|
||||||
|
self._py_audio = py_audio
|
||||||
|
self._out_stream = None
|
||||||
|
self._sample_rate = 0
|
||||||
|
|
||||||
# We only write audio frames from a single task, so only one thread
|
# We only write audio frames from a single task, so only one thread
|
||||||
# should be necessary.
|
# should be necessary.
|
||||||
self._executor = ThreadPoolExecutor(max_workers=1)
|
self._executor = ThreadPoolExecutor(max_workers=1)
|
||||||
|
|
||||||
self._out_stream = py_audio.open(
|
|
||||||
format=py_audio.get_format_from_width(2),
|
|
||||||
channels=params.audio_out_channels,
|
|
||||||
rate=params.audio_out_sample_rate,
|
|
||||||
output=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def start(self, frame: StartFrame):
|
async def start(self, frame: StartFrame):
|
||||||
await super().start(frame)
|
await super().start(frame)
|
||||||
|
|
||||||
|
self._sample_rate = self._params.audio_out_sample_rate or frame.audio_out_sample_rate
|
||||||
|
|
||||||
|
self._out_stream = self._py_audio.open(
|
||||||
|
format=self._py_audio.get_format_from_width(2),
|
||||||
|
channels=self._params.audio_out_channels,
|
||||||
|
rate=self._sample_rate,
|
||||||
|
output=True,
|
||||||
|
)
|
||||||
self._out_stream.start_stream()
|
self._out_stream.start_stream()
|
||||||
|
|
||||||
async def cleanup(self):
|
async def cleanup(self):
|
||||||
await super().cleanup()
|
await super().cleanup()
|
||||||
self._out_stream.stop_stream()
|
if self._out_stream:
|
||||||
# This is not very pretty (taken from PyAudio docs).
|
self._out_stream.stop_stream()
|
||||||
while self._out_stream.is_active():
|
# This is not very pretty (taken from PyAudio docs).
|
||||||
await asyncio.sleep(0.1)
|
while self._out_stream.is_active():
|
||||||
self._out_stream.close()
|
await asyncio.sleep(0.1)
|
||||||
|
self._out_stream.close()
|
||||||
|
|
||||||
async def write_raw_audio_frames(self, frames: bytes):
|
async def write_raw_audio_frames(self, frames: bytes):
|
||||||
await self.get_event_loop().run_in_executor(self._executor, self._out_stream.write, frames)
|
if self._out_stream:
|
||||||
|
await self.get_event_loop().run_in_executor(
|
||||||
|
self._executor, self._out_stream.write, frames
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class LocalAudioTransport(BaseTransport):
|
class LocalAudioTransport(BaseTransport):
|
||||||
|
|||||||
@@ -36,35 +36,39 @@ except ModuleNotFoundError as e:
|
|||||||
class TkInputTransport(BaseInputTransport):
|
class TkInputTransport(BaseInputTransport):
|
||||||
def __init__(self, py_audio: pyaudio.PyAudio, params: TransportParams):
|
def __init__(self, py_audio: pyaudio.PyAudio, params: TransportParams):
|
||||||
super().__init__(params)
|
super().__init__(params)
|
||||||
|
self._py_audio = py_audio
|
||||||
|
self._in_stream = None
|
||||||
|
self._sample_rate = 0
|
||||||
|
|
||||||
sample_rate = self._params.audio_in_sample_rate
|
async def start(self, frame: StartFrame):
|
||||||
num_frames = int(sample_rate / 100) * 2 # 20ms of audio
|
await super().start(frame)
|
||||||
|
|
||||||
self._in_stream = py_audio.open(
|
self._sample_rate = self._params.audio_in_sample_rate or frame.audio_in_sample_rate
|
||||||
format=py_audio.get_format_from_width(2),
|
num_frames = int(self._sample_rate / 100) * 2 # 20ms of audio
|
||||||
channels=params.audio_in_channels,
|
|
||||||
rate=params.audio_in_sample_rate,
|
self._in_stream = self._py_audio.open(
|
||||||
|
format=self._py_audio.get_format_from_width(2),
|
||||||
|
channels=self._params.audio_in_channels,
|
||||||
|
rate=self._sample_rate,
|
||||||
frames_per_buffer=num_frames,
|
frames_per_buffer=num_frames,
|
||||||
stream_callback=self._audio_in_callback,
|
stream_callback=self._audio_in_callback,
|
||||||
input=True,
|
input=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def start(self, frame: StartFrame):
|
|
||||||
await super().start(frame)
|
|
||||||
self._in_stream.start_stream()
|
self._in_stream.start_stream()
|
||||||
|
|
||||||
async def cleanup(self):
|
async def cleanup(self):
|
||||||
await super().cleanup()
|
await super().cleanup()
|
||||||
self._in_stream.stop_stream()
|
if self._in_stream:
|
||||||
# This is not very pretty (taken from PyAudio docs).
|
self._in_stream.stop_stream()
|
||||||
while self._in_stream.is_active():
|
# This is not very pretty (taken from PyAudio docs).
|
||||||
await asyncio.sleep(0.1)
|
while self._in_stream.is_active():
|
||||||
self._in_stream.close()
|
await asyncio.sleep(0.1)
|
||||||
|
self._in_stream.close()
|
||||||
|
|
||||||
def _audio_in_callback(self, in_data, frame_count, time_info, status):
|
def _audio_in_callback(self, in_data, frame_count, time_info, status):
|
||||||
frame = InputAudioRawFrame(
|
frame = InputAudioRawFrame(
|
||||||
audio=in_data,
|
audio=in_data,
|
||||||
sample_rate=self._params.audio_in_sample_rate,
|
sample_rate=self._sample_rate,
|
||||||
num_channels=self._params.audio_in_channels,
|
num_channels=self._params.audio_in_channels,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -76,18 +80,14 @@ class TkInputTransport(BaseInputTransport):
|
|||||||
class TkOutputTransport(BaseOutputTransport):
|
class TkOutputTransport(BaseOutputTransport):
|
||||||
def __init__(self, tk_root: tk.Tk, py_audio: pyaudio.PyAudio, params: TransportParams):
|
def __init__(self, tk_root: tk.Tk, py_audio: pyaudio.PyAudio, params: TransportParams):
|
||||||
super().__init__(params)
|
super().__init__(params)
|
||||||
|
self._py_audio = py_audio
|
||||||
|
self._out_stream = None
|
||||||
|
self._sample_rate = 0
|
||||||
|
|
||||||
# We only write audio frames from a single task, so only one thread
|
# We only write audio frames from a single task, so only one thread
|
||||||
# should be necessary.
|
# should be necessary.
|
||||||
self._executor = ThreadPoolExecutor(max_workers=1)
|
self._executor = ThreadPoolExecutor(max_workers=1)
|
||||||
|
|
||||||
self._out_stream = py_audio.open(
|
|
||||||
format=py_audio.get_format_from_width(2),
|
|
||||||
channels=params.audio_out_channels,
|
|
||||||
rate=params.audio_out_sample_rate,
|
|
||||||
output=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Start with a neutral gray background.
|
# Start with a neutral gray background.
|
||||||
array = np.ones((1024, 1024, 3)) * 128
|
array = np.ones((1024, 1024, 3)) * 128
|
||||||
data = f"P5 {1024} {1024} 255 ".encode() + array.astype(np.uint8).tobytes()
|
data = f"P5 {1024} {1024} 255 ".encode() + array.astype(np.uint8).tobytes()
|
||||||
@@ -97,18 +97,31 @@ class TkOutputTransport(BaseOutputTransport):
|
|||||||
|
|
||||||
async def start(self, frame: StartFrame):
|
async def start(self, frame: StartFrame):
|
||||||
await super().start(frame)
|
await super().start(frame)
|
||||||
|
|
||||||
|
self._sample_rate = self._params.audio_out_sample_rate or frame.audio_out_sample_rate
|
||||||
|
|
||||||
|
self._out_stream = self._py_audio.open(
|
||||||
|
format=self._py_audio.get_format_from_width(2),
|
||||||
|
channels=self._params.audio_out_channels,
|
||||||
|
rate=self._sample_rate,
|
||||||
|
output=True,
|
||||||
|
)
|
||||||
self._out_stream.start_stream()
|
self._out_stream.start_stream()
|
||||||
|
|
||||||
async def cleanup(self):
|
async def cleanup(self):
|
||||||
await super().cleanup()
|
await super().cleanup()
|
||||||
self._out_stream.stop_stream()
|
if self._out_stream:
|
||||||
# This is not very pretty (taken from PyAudio docs).
|
self._out_stream.stop_stream()
|
||||||
while self._out_stream.is_active():
|
# This is not very pretty (taken from PyAudio docs).
|
||||||
await asyncio.sleep(0.1)
|
while self._out_stream.is_active():
|
||||||
self._out_stream.close()
|
await asyncio.sleep(0.1)
|
||||||
|
self._out_stream.close()
|
||||||
|
|
||||||
async def write_raw_audio_frames(self, frames: bytes):
|
async def write_raw_audio_frames(self, frames: bytes):
|
||||||
await self.get_event_loop().run_in_executor(self._executor, self._out_stream.write, frames)
|
if self._out_stream:
|
||||||
|
await self.get_event_loop().run_in_executor(
|
||||||
|
self._executor, self._out_stream.write, frames
|
||||||
|
)
|
||||||
|
|
||||||
async def write_frame_to_camera(self, frame: OutputImageRawFrame):
|
async def write_frame_to_camera(self, frame: OutputImageRawFrame):
|
||||||
self.get_event_loop().call_soon(self._write_frame_to_tk, frame)
|
self.get_event_loop().call_soon(self._write_frame_to_tk, frame)
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ class FastAPIWebsocketInputTransport(BaseInputTransport):
|
|||||||
|
|
||||||
async def start(self, frame: StartFrame):
|
async def start(self, frame: StartFrame):
|
||||||
await super().start(frame)
|
await super().start(frame)
|
||||||
|
await self._params.serializer.setup(frame)
|
||||||
if self._params.session_timeout:
|
if self._params.session_timeout:
|
||||||
self._monitor_websocket_task = self.create_task(self._monitor_websocket())
|
self._monitor_websocket_task = self.create_task(self._monitor_websocket())
|
||||||
await self._callbacks.on_client_connected(self._websocket)
|
await self._callbacks.on_client_connected(self._websocket)
|
||||||
@@ -118,9 +119,19 @@ class FastAPIWebsocketOutputTransport(BaseOutputTransport):
|
|||||||
self._websocket = websocket
|
self._websocket = websocket
|
||||||
self._params = params
|
self._params = params
|
||||||
|
|
||||||
self._send_interval = (self._audio_chunk_size / self._params.audio_out_sample_rate) / 2
|
# write_raw_audio_frames() is called quickly, as soon as we get audio
|
||||||
|
# (e.g. from the TTS), and since this is just a network connection we
|
||||||
|
# would be sending it to quickly. Instead, we want to block to emulate
|
||||||
|
# an audio device, this is what the send interval is. It will be
|
||||||
|
# computed on StartFrame.
|
||||||
|
self._send_interval = 0
|
||||||
self._next_send_time = 0
|
self._next_send_time = 0
|
||||||
|
|
||||||
|
async def start(self, frame: StartFrame):
|
||||||
|
await super().start(frame)
|
||||||
|
await self._params.serializer.setup(frame)
|
||||||
|
self._send_interval = (self._audio_chunk_size / self.sample_rate) / 2
|
||||||
|
|
||||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||||
await super().process_frame(frame, direction)
|
await super().process_frame(frame, direction)
|
||||||
|
|
||||||
@@ -136,7 +147,7 @@ class FastAPIWebsocketOutputTransport(BaseOutputTransport):
|
|||||||
|
|
||||||
frame = OutputAudioRawFrame(
|
frame = OutputAudioRawFrame(
|
||||||
audio=frames,
|
audio=frames,
|
||||||
sample_rate=self._params.audio_out_sample_rate,
|
sample_rate=self.sample_rate,
|
||||||
num_channels=self._params.audio_out_channels,
|
num_channels=self._params.audio_out_channels,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ class WebsocketClientInputTransport(BaseInputTransport):
|
|||||||
|
|
||||||
async def start(self, frame: StartFrame):
|
async def start(self, frame: StartFrame):
|
||||||
await super().start(frame)
|
await super().start(frame)
|
||||||
|
await self._params.serializer.setup(frame)
|
||||||
await self._session.setup(frame)
|
await self._session.setup(frame)
|
||||||
await self._session.connect()
|
await self._session.connect()
|
||||||
|
|
||||||
@@ -154,11 +155,18 @@ class WebsocketClientOutputTransport(BaseOutputTransport):
|
|||||||
self._session = session
|
self._session = session
|
||||||
self._params = params
|
self._params = params
|
||||||
|
|
||||||
self._send_interval = (self._audio_chunk_size / self._params.audio_out_sample_rate) / 2
|
# write_raw_audio_frames() is called quickly, as soon as we get audio
|
||||||
|
# (e.g. from the TTS), and since this is just a network connection we
|
||||||
|
# would be sending it to quickly. Instead, we want to block to emulate
|
||||||
|
# an audio device, this is what the send interval is. It will be
|
||||||
|
# computed on StartFrame.
|
||||||
|
self._send_interval = 0
|
||||||
self._next_send_time = 0
|
self._next_send_time = 0
|
||||||
|
|
||||||
async def start(self, frame: StartFrame):
|
async def start(self, frame: StartFrame):
|
||||||
await super().start(frame)
|
await super().start(frame)
|
||||||
|
self._send_interval = (self._audio_chunk_size / self.sample_rate) / 2
|
||||||
|
await self._params.serializer.setup(frame)
|
||||||
await self._session.setup(frame)
|
await self._session.setup(frame)
|
||||||
await self._session.connect()
|
await self._session.connect()
|
||||||
|
|
||||||
@@ -176,7 +184,7 @@ class WebsocketClientOutputTransport(BaseOutputTransport):
|
|||||||
async def write_raw_audio_frames(self, frames: bytes):
|
async def write_raw_audio_frames(self, frames: bytes):
|
||||||
frame = OutputAudioRawFrame(
|
frame = OutputAudioRawFrame(
|
||||||
audio=frames,
|
audio=frames,
|
||||||
sample_rate=self._params.audio_out_sample_rate,
|
sample_rate=self.sample_rate,
|
||||||
num_channels=self._params.audio_out_channels,
|
num_channels=self._params.audio_out_channels,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ from pipecat.frames.frames import (
|
|||||||
)
|
)
|
||||||
from pipecat.processors.frame_processor import FrameDirection
|
from pipecat.processors.frame_processor import FrameDirection
|
||||||
from pipecat.serializers.base_serializer import FrameSerializer
|
from pipecat.serializers.base_serializer import FrameSerializer
|
||||||
from pipecat.serializers.protobuf import ProtobufFrameSerializer
|
|
||||||
from pipecat.transports.base_input import BaseInputTransport
|
from pipecat.transports.base_input import BaseInputTransport
|
||||||
from pipecat.transports.base_output import BaseOutputTransport
|
from pipecat.transports.base_output import BaseOutputTransport
|
||||||
from pipecat.transports.base_transport import BaseTransport, TransportParams
|
from pipecat.transports.base_transport import BaseTransport, TransportParams
|
||||||
@@ -39,7 +38,7 @@ except ModuleNotFoundError as e:
|
|||||||
|
|
||||||
class WebsocketServerParams(TransportParams):
|
class WebsocketServerParams(TransportParams):
|
||||||
add_wav_header: bool = False
|
add_wav_header: bool = False
|
||||||
serializer: FrameSerializer = ProtobufFrameSerializer()
|
serializer: FrameSerializer
|
||||||
session_timeout: int | None = None
|
session_timeout: int | None = None
|
||||||
|
|
||||||
|
|
||||||
@@ -67,20 +66,32 @@ class WebsocketServerInputTransport(BaseInputTransport):
|
|||||||
|
|
||||||
self._websocket: websockets.WebSocketServerProtocol | None = None
|
self._websocket: websockets.WebSocketServerProtocol | None = None
|
||||||
|
|
||||||
|
self._server_task = None
|
||||||
|
|
||||||
|
# This task will monitor the websocket connection periodically.
|
||||||
|
self._monitor_task = None
|
||||||
|
|
||||||
self._stop_server_event = asyncio.Event()
|
self._stop_server_event = asyncio.Event()
|
||||||
|
|
||||||
async def start(self, frame: StartFrame):
|
async def start(self, frame: StartFrame):
|
||||||
await super().start(frame)
|
await super().start(frame)
|
||||||
|
await self._params.serializer.setup(frame)
|
||||||
self._server_task = self.create_task(self._server_task_handler())
|
self._server_task = self.create_task(self._server_task_handler())
|
||||||
|
|
||||||
async def stop(self, frame: EndFrame):
|
async def stop(self, frame: EndFrame):
|
||||||
await super().stop(frame)
|
await super().stop(frame)
|
||||||
self._stop_server_event.set()
|
self._stop_server_event.set()
|
||||||
await self.wait_for_task(self._server_task)
|
if self._monitor_task:
|
||||||
|
await self.cancel_task(self._monitor_task)
|
||||||
|
if self._server_task:
|
||||||
|
await self.wait_for_task(self._server_task)
|
||||||
|
|
||||||
async def cancel(self, frame: CancelFrame):
|
async def cancel(self, frame: CancelFrame):
|
||||||
await super().cancel(frame)
|
await super().cancel(frame)
|
||||||
await self.cancel_task(self._server_task)
|
if self._monitor_task:
|
||||||
|
await self.cancel_task(self._monitor_task)
|
||||||
|
if self._server_task:
|
||||||
|
await self.cancel_task(self._server_task)
|
||||||
|
|
||||||
async def _server_task_handler(self):
|
async def _server_task_handler(self):
|
||||||
logger.info(f"Starting websocket server on {self._host}:{self._port}")
|
logger.info(f"Starting websocket server on {self._host}:{self._port}")
|
||||||
@@ -100,7 +111,9 @@ class WebsocketServerInputTransport(BaseInputTransport):
|
|||||||
|
|
||||||
# Create a task to monitor the websocket connection
|
# Create a task to monitor the websocket connection
|
||||||
if self._params.session_timeout:
|
if self._params.session_timeout:
|
||||||
self.create_task(self._monitor_websocket(websocket))
|
self._monitor_task = self.create_task(
|
||||||
|
self._monitor_websocket(websocket, self._params.session_timeout)
|
||||||
|
)
|
||||||
|
|
||||||
# Handle incoming messages
|
# Handle incoming messages
|
||||||
try:
|
try:
|
||||||
@@ -125,10 +138,13 @@ class WebsocketServerInputTransport(BaseInputTransport):
|
|||||||
|
|
||||||
logger.info(f"Client {websocket.remote_address} disconnected")
|
logger.info(f"Client {websocket.remote_address} disconnected")
|
||||||
|
|
||||||
async def _monitor_websocket(self, websocket: websockets.WebSocketServerProtocol):
|
async def _monitor_websocket(
|
||||||
"""Wait for self._params.session_timeout seconds, if the websocket is still open, trigger timeout event."""
|
self, websocket: websockets.WebSocketServerProtocol, session_timeout: int
|
||||||
|
):
|
||||||
|
"""Wait for session_timeout seconds, if the websocket is still open,
|
||||||
|
trigger timeout event."""
|
||||||
try:
|
try:
|
||||||
await asyncio.sleep(self._params.session_timeout)
|
await asyncio.sleep(session_timeout)
|
||||||
if not websocket.closed:
|
if not websocket.closed:
|
||||||
await self._callbacks.on_session_timeout(websocket)
|
await self._callbacks.on_session_timeout(websocket)
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
@@ -144,7 +160,12 @@ class WebsocketServerOutputTransport(BaseOutputTransport):
|
|||||||
|
|
||||||
self._websocket: websockets.WebSocketServerProtocol | None = None
|
self._websocket: websockets.WebSocketServerProtocol | None = None
|
||||||
|
|
||||||
self._send_interval = (self._audio_chunk_size / self._params.audio_out_sample_rate) / 2
|
# write_raw_audio_frames() is called quickly, as soon as we get audio
|
||||||
|
# (e.g. from the TTS), and since this is just a network connection we
|
||||||
|
# would be sending it to quickly. Instead, we want to block to emulate
|
||||||
|
# an audio device, this is what the send interval is. It will be
|
||||||
|
# computed on StartFrame.
|
||||||
|
self._send_interval = 0
|
||||||
self._next_send_time = 0
|
self._next_send_time = 0
|
||||||
|
|
||||||
async def set_client_connection(self, websocket: websockets.WebSocketServerProtocol | None):
|
async def set_client_connection(self, websocket: websockets.WebSocketServerProtocol | None):
|
||||||
@@ -153,6 +174,11 @@ class WebsocketServerOutputTransport(BaseOutputTransport):
|
|||||||
logger.warning("Only one client allowed, using new connection")
|
logger.warning("Only one client allowed, using new connection")
|
||||||
self._websocket = websocket
|
self._websocket = websocket
|
||||||
|
|
||||||
|
async def start(self, frame: StartFrame):
|
||||||
|
await super().start(frame)
|
||||||
|
await self._params.serializer.setup(frame)
|
||||||
|
self._send_interval = (self._audio_chunk_size / self.sample_rate) / 2
|
||||||
|
|
||||||
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
async def process_frame(self, frame: Frame, direction: FrameDirection):
|
||||||
await super().process_frame(frame, direction)
|
await super().process_frame(frame, direction)
|
||||||
|
|
||||||
@@ -168,7 +194,7 @@ class WebsocketServerOutputTransport(BaseOutputTransport):
|
|||||||
|
|
||||||
frame = OutputAudioRawFrame(
|
frame = OutputAudioRawFrame(
|
||||||
audio=frames,
|
audio=frames,
|
||||||
sample_rate=self._params.audio_out_sample_rate,
|
sample_rate=self.sample_rate,
|
||||||
num_channels=self._params.audio_out_channels,
|
num_channels=self._params.audio_out_channels,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -213,14 +239,13 @@ class WebsocketServerOutputTransport(BaseOutputTransport):
|
|||||||
class WebsocketServerTransport(BaseTransport):
|
class WebsocketServerTransport(BaseTransport):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
params: WebsocketServerParams,
|
||||||
host: str = "localhost",
|
host: str = "localhost",
|
||||||
port: int = 8765,
|
port: int = 8765,
|
||||||
params: WebsocketServerParams = WebsocketServerParams(),
|
|
||||||
input_name: str | None = None,
|
input_name: str | None = None,
|
||||||
output_name: str | None = None,
|
output_name: str | None = None,
|
||||||
loop: asyncio.AbstractEventLoop | None = None,
|
|
||||||
):
|
):
|
||||||
super().__init__(input_name=input_name, output_name=output_name, loop=loop)
|
super().__init__(input_name=input_name, output_name=output_name)
|
||||||
self._host = host
|
self._host = host
|
||||||
self._port = port
|
self._port = port
|
||||||
self._params = params
|
self._params = params
|
||||||
|
|||||||
@@ -71,11 +71,11 @@ class DailyTransportMessageUrgentFrame(TransportMessageUrgentFrame):
|
|||||||
|
|
||||||
|
|
||||||
class WebRTCVADAnalyzer(VADAnalyzer):
|
class WebRTCVADAnalyzer(VADAnalyzer):
|
||||||
def __init__(self, *, sample_rate=16000, num_channels=1, params: VADParams = VADParams()):
|
def __init__(self, *, sample_rate: Optional[int] = None, params: VADParams = VADParams()):
|
||||||
super().__init__(sample_rate=sample_rate, num_channels=num_channels, params=params)
|
super().__init__(sample_rate=sample_rate, params=params)
|
||||||
|
|
||||||
self._webrtc_vad = Daily.create_native_vad(
|
self._webrtc_vad = Daily.create_native_vad(
|
||||||
reset_period_ms=VAD_RESET_PERIOD_MS, sample_rate=sample_rate, channels=num_channels
|
reset_period_ms=VAD_RESET_PERIOD_MS, sample_rate=self.sample_rate, channels=1
|
||||||
)
|
)
|
||||||
logger.debug("Loaded native WebRTC VAD")
|
logger.debug("Loaded native WebRTC VAD")
|
||||||
|
|
||||||
@@ -222,33 +222,13 @@ class DailyTransportClient(EventHandler):
|
|||||||
self._callback_queue = asyncio.Queue()
|
self._callback_queue = asyncio.Queue()
|
||||||
self._callback_task = None
|
self._callback_task = None
|
||||||
|
|
||||||
|
# Input and ouput sample rates. They will be initialize on setup().
|
||||||
|
self._in_sample_rate = 0
|
||||||
|
self._out_sample_rate = 0
|
||||||
|
|
||||||
self._camera: VirtualCameraDevice | None = None
|
self._camera: VirtualCameraDevice | None = None
|
||||||
if self._params.camera_out_enabled:
|
|
||||||
self._camera = Daily.create_camera_device(
|
|
||||||
self._camera_name(),
|
|
||||||
width=self._params.camera_out_width,
|
|
||||||
height=self._params.camera_out_height,
|
|
||||||
color_format=self._params.camera_out_color_format,
|
|
||||||
)
|
|
||||||
|
|
||||||
self._mic: VirtualMicrophoneDevice | None = None
|
self._mic: VirtualMicrophoneDevice | None = None
|
||||||
if self._params.audio_out_enabled:
|
|
||||||
self._mic = Daily.create_microphone_device(
|
|
||||||
self._mic_name(),
|
|
||||||
sample_rate=self._params.audio_out_sample_rate,
|
|
||||||
channels=self._params.audio_out_channels,
|
|
||||||
non_blocking=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
self._speaker: VirtualSpeakerDevice | None = None
|
self._speaker: VirtualSpeakerDevice | None = None
|
||||||
if self._params.audio_in_enabled or self._params.vad_enabled:
|
|
||||||
self._speaker = Daily.create_speaker_device(
|
|
||||||
self._speaker_name(),
|
|
||||||
sample_rate=self._params.audio_in_sample_rate,
|
|
||||||
channels=self._params.audio_in_channels,
|
|
||||||
non_blocking=True,
|
|
||||||
)
|
|
||||||
Daily.select_speaker_device(self._speaker_name())
|
|
||||||
|
|
||||||
def _camera_name(self):
|
def _camera_name(self):
|
||||||
return f"camera-{self}"
|
return f"camera-{self}"
|
||||||
@@ -281,7 +261,7 @@ class DailyTransportClient(EventHandler):
|
|||||||
if not self._speaker:
|
if not self._speaker:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
sample_rate = self._params.audio_in_sample_rate
|
sample_rate = self._in_sample_rate
|
||||||
num_channels = self._params.audio_in_channels
|
num_channels = self._params.audio_in_channels
|
||||||
num_frames = int(sample_rate / 100) * 2 # 20ms of audio
|
num_frames = int(sample_rate / 100) * 2 # 20ms of audio
|
||||||
|
|
||||||
@@ -315,6 +295,34 @@ class DailyTransportClient(EventHandler):
|
|||||||
self._camera.write_frame(frame.image)
|
self._camera.write_frame(frame.image)
|
||||||
|
|
||||||
async def setup(self, frame: StartFrame):
|
async def setup(self, frame: StartFrame):
|
||||||
|
self._in_sample_rate = self._params.audio_in_sample_rate or frame.audio_in_sample_rate
|
||||||
|
self._out_sample_rate = self._params.audio_out_sample_rate or frame.audio_out_sample_rate
|
||||||
|
|
||||||
|
if self._params.camera_out_enabled and not self._camera:
|
||||||
|
self._camera = Daily.create_camera_device(
|
||||||
|
self._camera_name(),
|
||||||
|
width=self._params.camera_out_width,
|
||||||
|
height=self._params.camera_out_height,
|
||||||
|
color_format=self._params.camera_out_color_format,
|
||||||
|
)
|
||||||
|
|
||||||
|
if self._params.audio_out_enabled and not self._mic:
|
||||||
|
self._mic = Daily.create_microphone_device(
|
||||||
|
self._mic_name(),
|
||||||
|
sample_rate=self._out_sample_rate,
|
||||||
|
channels=self._params.audio_out_channels,
|
||||||
|
non_blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (self._params.audio_in_enabled or self._params.vad_enabled) and not self._speaker:
|
||||||
|
self._speaker = Daily.create_speaker_device(
|
||||||
|
self._speaker_name(),
|
||||||
|
sample_rate=self._in_sample_rate,
|
||||||
|
channels=self._params.audio_in_channels,
|
||||||
|
non_blocking=True,
|
||||||
|
)
|
||||||
|
Daily.select_speaker_device(self._speaker_name())
|
||||||
|
|
||||||
if not self._task_manager:
|
if not self._task_manager:
|
||||||
self._task_manager = frame.task_manager
|
self._task_manager = frame.task_manager
|
||||||
self._callback_task = self._task_manager.create_task(
|
self._callback_task = self._task_manager.create_task(
|
||||||
@@ -707,6 +715,7 @@ class DailyInputTransport(BaseInputTransport):
|
|||||||
super().__init__(params, **kwargs)
|
super().__init__(params, **kwargs)
|
||||||
|
|
||||||
self._client = client
|
self._client = client
|
||||||
|
self._params = params
|
||||||
|
|
||||||
self._video_renderers = {}
|
self._video_renderers = {}
|
||||||
|
|
||||||
@@ -715,11 +724,10 @@ class DailyInputTransport(BaseInputTransport):
|
|||||||
self._audio_in_task = None
|
self._audio_in_task = None
|
||||||
|
|
||||||
self._vad_analyzer: VADAnalyzer | None = params.vad_analyzer
|
self._vad_analyzer: VADAnalyzer | None = params.vad_analyzer
|
||||||
if params.vad_enabled and not params.vad_analyzer:
|
|
||||||
self._vad_analyzer = WebRTCVADAnalyzer(
|
@property
|
||||||
sample_rate=self._params.audio_in_sample_rate,
|
def vad_analyzer(self) -> VADAnalyzer | None:
|
||||||
num_channels=self._params.audio_in_channels,
|
return self._vad_analyzer
|
||||||
)
|
|
||||||
|
|
||||||
async def start(self, frame: StartFrame):
|
async def start(self, frame: StartFrame):
|
||||||
# Parent start.
|
# Parent start.
|
||||||
@@ -728,6 +736,9 @@ class DailyInputTransport(BaseInputTransport):
|
|||||||
await self._client.setup(frame)
|
await self._client.setup(frame)
|
||||||
# Join the room.
|
# Join the room.
|
||||||
await self._client.join()
|
await self._client.join()
|
||||||
|
# Inialize WebRTC VAD if needed.
|
||||||
|
if self._params.vad_enabled and not self._params.vad_analyzer:
|
||||||
|
self._vad_analyzer = WebRTCVADAnalyzer(sample_rate=self.sample_rate)
|
||||||
# Create audio task. It reads audio frames from Daily and push them
|
# Create audio task. It reads audio frames from Daily and push them
|
||||||
# internally for VAD processing.
|
# internally for VAD processing.
|
||||||
if self._params.audio_in_enabled or self._params.vad_enabled:
|
if self._params.audio_in_enabled or self._params.vad_enabled:
|
||||||
@@ -757,9 +768,6 @@ class DailyInputTransport(BaseInputTransport):
|
|||||||
await super().cleanup()
|
await super().cleanup()
|
||||||
await self._client.cleanup()
|
await self._client.cleanup()
|
||||||
|
|
||||||
def vad_analyzer(self) -> VADAnalyzer | None:
|
|
||||||
return self._vad_analyzer
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# FrameProcessor
|
# FrameProcessor
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ class LiveKitTransportClient:
|
|||||||
return self._room
|
return self._room
|
||||||
|
|
||||||
async def setup(self, frame: StartFrame):
|
async def setup(self, frame: StartFrame):
|
||||||
|
self._out_sample_rate = self._params.audio_out_sample_rate or frame.audio_out_sample_rate
|
||||||
if not self._task_manager:
|
if not self._task_manager:
|
||||||
self._task_manager = frame.task_manager
|
self._task_manager = frame.task_manager
|
||||||
self._room = rtc.Room(loop=self._task_manager.get_event_loop())
|
self._room = rtc.Room(loop=self._task_manager.get_event_loop())
|
||||||
@@ -138,7 +139,7 @@ class LiveKitTransportClient:
|
|||||||
|
|
||||||
# Set up audio source and track
|
# Set up audio source and track
|
||||||
self._audio_source = rtc.AudioSource(
|
self._audio_source = rtc.AudioSource(
|
||||||
self._params.audio_out_sample_rate, self._params.audio_out_channels
|
self._out_sample_rate, self._params.audio_out_channels
|
||||||
)
|
)
|
||||||
self._audio_track = rtc.LocalAudioTrack.create_audio_track(
|
self._audio_track = rtc.LocalAudioTrack.create_audio_track(
|
||||||
"pipecat-audio", self._audio_source
|
"pipecat-audio", self._audio_source
|
||||||
@@ -351,6 +352,10 @@ class LiveKitInputTransport(BaseInputTransport):
|
|||||||
self._vad_analyzer: VADAnalyzer | None = params.vad_analyzer
|
self._vad_analyzer: VADAnalyzer | None = params.vad_analyzer
|
||||||
self._resampler = create_default_resampler()
|
self._resampler = create_default_resampler()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def vad_analyzer(self) -> VADAnalyzer | None:
|
||||||
|
return self._vad_analyzer
|
||||||
|
|
||||||
async def start(self, frame: StartFrame):
|
async def start(self, frame: StartFrame):
|
||||||
await super().start(frame)
|
await super().start(frame)
|
||||||
await self._client.setup(frame)
|
await self._client.setup(frame)
|
||||||
@@ -372,9 +377,6 @@ class LiveKitInputTransport(BaseInputTransport):
|
|||||||
if self._audio_in_task and (self._params.audio_in_enabled or self._params.vad_enabled):
|
if self._audio_in_task and (self._params.audio_in_enabled or self._params.vad_enabled):
|
||||||
await self.cancel_task(self._audio_in_task)
|
await self.cancel_task(self._audio_in_task)
|
||||||
|
|
||||||
def vad_analyzer(self) -> VADAnalyzer | None:
|
|
||||||
return self._vad_analyzer
|
|
||||||
|
|
||||||
async def push_app_message(self, message: Any, sender: str):
|
async def push_app_message(self, message: Any, sender: str):
|
||||||
frame = LiveKitTransportMessageUrgentFrame(message=message, participant_id=sender)
|
frame = LiveKitTransportMessageUrgentFrame(message=message, participant_id=sender)
|
||||||
await self.push_frame(frame)
|
await self.push_frame(frame)
|
||||||
@@ -401,12 +403,12 @@ class LiveKitInputTransport(BaseInputTransport):
|
|||||||
audio_frame = audio_frame_event.frame
|
audio_frame = audio_frame_event.frame
|
||||||
|
|
||||||
audio_data = await self._resampler.resample(
|
audio_data = await self._resampler.resample(
|
||||||
audio_frame.data.tobytes(), audio_frame.sample_rate, self._params.audio_in_sample_rate
|
audio_frame.data.tobytes(), audio_frame.sample_rate, self.sample_rate
|
||||||
)
|
)
|
||||||
|
|
||||||
return AudioRawFrame(
|
return AudioRawFrame(
|
||||||
audio=audio_data,
|
audio=audio_data,
|
||||||
sample_rate=self._params.audio_in_sample_rate,
|
sample_rate=self.sample_rate,
|
||||||
num_channels=audio_frame.num_channels,
|
num_channels=audio_frame.num_channels,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -448,7 +450,7 @@ class LiveKitOutputTransport(BaseOutputTransport):
|
|||||||
|
|
||||||
return rtc.AudioFrame(
|
return rtc.AudioFrame(
|
||||||
data=pipecat_audio,
|
data=pipecat_audio,
|
||||||
sample_rate=self._params.audio_out_sample_rate,
|
sample_rate=self.sample_rate,
|
||||||
num_channels=self._params.audio_out_channels,
|
num_channels=self._params.audio_out_channels,
|
||||||
samples_per_channel=samples_per_channel,
|
samples_per_channel=samples_per_channel,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user