Merge pull request #1979 from pipecat-ai/aleix/buffer-tts-before-playback
buffer audio from TTS service before pushing frames
This commit is contained in:
@@ -20,6 +20,9 @@ asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
|||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed an issue with various TTS services that would cause audio glitches at
|
||||||
|
the start of every bot turn.
|
||||||
|
|
||||||
- Fixed an `AssemblyAISTTService` issue that could cause unexpected behavior
|
- Fixed an `AssemblyAISTTService` issue that could cause unexpected behavior
|
||||||
when yielding empty `Frame()`s.
|
when yielding empty `Frame()`s.
|
||||||
|
|
||||||
|
|||||||
@@ -253,7 +253,8 @@ class AWSPollyTTSService(TTSService):
|
|||||||
|
|
||||||
yield TTSStartedFrame()
|
yield TTSStartedFrame()
|
||||||
|
|
||||||
CHUNK_SIZE = 1024
|
CHUNK_SIZE = self.chunk_size
|
||||||
|
|
||||||
for i in range(0, len(audio_data), CHUNK_SIZE):
|
for i in range(0, len(audio_data), CHUNK_SIZE):
|
||||||
chunk = audio_data[i : i + CHUNK_SIZE]
|
chunk = audio_data[i : i + CHUNK_SIZE]
|
||||||
if len(chunk) > 0:
|
if len(chunk) > 0:
|
||||||
|
|||||||
@@ -362,8 +362,8 @@ class GoogleHttpTTSService(TTSService):
|
|||||||
# Skip the first 44 bytes to remove the WAV header
|
# Skip the first 44 bytes to remove the WAV header
|
||||||
audio_content = response.audio_content[44:]
|
audio_content = response.audio_content[44:]
|
||||||
|
|
||||||
# Read and yield audio data in chunks
|
CHUNK_SIZE = self.chunk_size
|
||||||
CHUNK_SIZE = 1024
|
|
||||||
for i in range(0, len(audio_content), CHUNK_SIZE):
|
for i in range(0, len(audio_content), CHUNK_SIZE):
|
||||||
chunk = audio_content[i : i + CHUNK_SIZE]
|
chunk = audio_content[i : i + CHUNK_SIZE]
|
||||||
if not chunk:
|
if not chunk:
|
||||||
@@ -505,9 +505,10 @@ class GoogleTTSService(TTSService):
|
|||||||
yield TTSStartedFrame()
|
yield TTSStartedFrame()
|
||||||
|
|
||||||
audio_buffer = b""
|
audio_buffer = b""
|
||||||
CHUNK_SIZE = 1024
|
|
||||||
first_chunk_for_ttfb = False
|
first_chunk_for_ttfb = False
|
||||||
|
|
||||||
|
CHUNK_SIZE = self.chunk_size
|
||||||
|
|
||||||
async for response in streaming_responses:
|
async for response in streaming_responses:
|
||||||
chunk = response.audio_content
|
chunk = response.audio_content
|
||||||
if not chunk:
|
if not chunk:
|
||||||
|
|||||||
@@ -227,7 +227,8 @@ class MiniMaxHttpTTSService(TTSService):
|
|||||||
|
|
||||||
# Process the streaming response
|
# Process the streaming response
|
||||||
buffer = bytearray()
|
buffer = bytearray()
|
||||||
CHUNK_SIZE = 1024
|
|
||||||
|
CHUNK_SIZE = self.chunk_size
|
||||||
|
|
||||||
async for chunk in response.content.iter_chunked(CHUNK_SIZE):
|
async for chunk in response.content.iter_chunked(CHUNK_SIZE):
|
||||||
if not chunk:
|
if not chunk:
|
||||||
@@ -279,10 +280,8 @@ class MiniMaxHttpTTSService(TTSService):
|
|||||||
await self.stop_ttfb_metrics()
|
await self.stop_ttfb_metrics()
|
||||||
yield TTSAudioRawFrame(
|
yield TTSAudioRawFrame(
|
||||||
audio=audio_chunk,
|
audio=audio_chunk,
|
||||||
sample_rate=self._settings["audio_setting"][
|
sample_rate=self.sample_rate,
|
||||||
"sample_rate"
|
num_channels=1,
|
||||||
],
|
|
||||||
num_channels=self._settings["audio_setting"]["channel"],
|
|
||||||
)
|
)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
logger.error(f"Error converting hex to binary: {e}")
|
logger.error(f"Error converting hex to binary: {e}")
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ class OpenAITTSService(TTSService):
|
|||||||
|
|
||||||
await self.start_tts_usage_metrics(text)
|
await self.start_tts_usage_metrics(text)
|
||||||
|
|
||||||
CHUNK_SIZE = 1024
|
CHUNK_SIZE = self.chunk_size
|
||||||
|
|
||||||
yield TTSStartedFrame()
|
yield TTSStartedFrame()
|
||||||
async for chunk in r.iter_bytes(CHUNK_SIZE):
|
async for chunk in r.iter_bytes(CHUNK_SIZE):
|
||||||
|
|||||||
@@ -85,8 +85,7 @@ class PiperTTSService(TTSService):
|
|||||||
|
|
||||||
await self.start_tts_usage_metrics(text)
|
await self.start_tts_usage_metrics(text)
|
||||||
|
|
||||||
# Process the streaming response
|
CHUNK_SIZE = self.chunk_size
|
||||||
CHUNK_SIZE = 1024
|
|
||||||
|
|
||||||
yield TTSStartedFrame()
|
yield TTSStartedFrame()
|
||||||
async for chunk in response.content.iter_chunked(CHUNK_SIZE):
|
async for chunk in response.content.iter_chunked(CHUNK_SIZE):
|
||||||
|
|||||||
@@ -430,8 +430,7 @@ class RimeHttpTTSService(TTSService):
|
|||||||
|
|
||||||
yield TTSStartedFrame()
|
yield TTSStartedFrame()
|
||||||
|
|
||||||
# Process the streaming response
|
CHUNK_SIZE = self.chunk_size
|
||||||
CHUNK_SIZE = 1024
|
|
||||||
|
|
||||||
async for chunk in response.content.iter_chunked(CHUNK_SIZE):
|
async for chunk in response.content.iter_chunked(CHUNK_SIZE):
|
||||||
if need_to_strip_wav_header and chunk.startswith(b"RIFF"):
|
if need_to_strip_wav_header and chunk.startswith(b"RIFF"):
|
||||||
|
|||||||
@@ -106,6 +106,19 @@ class TTSService(AIService):
|
|||||||
def sample_rate(self) -> int:
|
def sample_rate(self) -> int:
|
||||||
return self._sample_rate
|
return self._sample_rate
|
||||||
|
|
||||||
|
@property
|
||||||
|
def chunk_size(self) -> int:
|
||||||
|
"""This property indicates how much audio we download (from TTS services
|
||||||
|
that require chunking) before we start pushing the first audio
|
||||||
|
frame. This will make sure we download the rest of the audio while audio
|
||||||
|
is being played without causing audio glitches (specially at the
|
||||||
|
beginning). Of course, this will also depend on how fast the TTS service
|
||||||
|
generates bytes.
|
||||||
|
|
||||||
|
"""
|
||||||
|
CHUNK_SECONDS = 0.5
|
||||||
|
return int(self.sample_rate * CHUNK_SECONDS * 2) # 2 bytes/sample
|
||||||
|
|
||||||
async def set_model(self, model: str):
|
async def set_model(self, model: str):
|
||||||
self.set_model_name(model)
|
self.set_model_name(model)
|
||||||
|
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ class XTTSService(TTSService):
|
|||||||
|
|
||||||
yield TTSStartedFrame()
|
yield TTSStartedFrame()
|
||||||
|
|
||||||
CHUNK_SIZE = 1024
|
CHUNK_SIZE = self.chunk_size
|
||||||
|
|
||||||
buffer = bytearray()
|
buffer = bytearray()
|
||||||
async for chunk in r.content.iter_chunked(CHUNK_SIZE):
|
async for chunk in r.content.iter_chunked(CHUNK_SIZE):
|
||||||
|
|||||||
@@ -47,8 +47,9 @@ async def test_run_piper_tts_success(aiohttp_client):
|
|||||||
|
|
||||||
# Write out some chunked byte data
|
# Write out some chunked byte data
|
||||||
# In reality, you’d return WAV data or similar
|
# In reality, you’d return WAV data or similar
|
||||||
data_chunk_1 = b"\x00\x01\x02\x03" * 1024 # 4096 bytes, 04 TTSAudioRawFrame
|
CHUNK_SIZE = 24000
|
||||||
data_chunk_2 = b"\x04\x05\x06\x07" * 1024 # another chunk
|
data_chunk_1 = b"\x00\x01\x02\x03" * CHUNK_SIZE # 4xTTSAudioRawFrame
|
||||||
|
data_chunk_2 = b"\x04\x05\x06\x07" * CHUNK_SIZE # another chunk
|
||||||
await resp.write(data_chunk_1)
|
await resp.write(data_chunk_1)
|
||||||
await asyncio.sleep(0.01) # simulate async chunk delay
|
await asyncio.sleep(0.01) # simulate async chunk delay
|
||||||
await resp.write(data_chunk_2)
|
await resp.write(data_chunk_2)
|
||||||
|
|||||||
Reference in New Issue
Block a user