Merge pull request #3061 from pipecat-ai/pk/greatly-simplify-inter-frame-spaces-logic
D'oh! My TTS "inter-frame-spaces" logic was *way* overcomplicated (an…
This commit is contained in:
@@ -18,10 +18,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Added `ElevenLabsRealtimeSTTService` which implements the Realtime STT
|
- Added `ElevenLabsRealtimeSTTService` which implements the Realtime STT
|
||||||
service from ElevenLabs.
|
service from ElevenLabs.
|
||||||
|
|
||||||
- Added a `TTSService.includes_inter_frame_spaces` property getter, so that TTS
|
|
||||||
services that subclass `TTSService` can indicate whether the text in the
|
|
||||||
`TTSTextFrame`s they push already contain any necessary inter-frame spaces.
|
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Extracted the logic for retrying connections, and create a new `send_with_retry`
|
- Extracted the logic for retrying connections, and create a new `send_with_retry`
|
||||||
@@ -68,8 +64,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Added ai-coustics integrated VAD (`AICVADAnalyzer`) with `AICFilter` factory and
|
- Added ai-coustics integrated VAD (`AICVADAnalyzer`) with `AICFilter` factory and
|
||||||
example wiring; leverages the enhancement model for robust detection with no
|
example wiring; leverages the enhancement model for robust detection with no
|
||||||
ONNX dependency or added processing complexity.
|
ONNX dependency or added processing complexity.
|
||||||
|
|
||||||
## [0.0.94] - 2025-11-10
|
## [0.0.94] - 2025-11-10
|
||||||
|
|||||||
@@ -352,7 +352,10 @@ class TextFrame(DataFrame):
|
|||||||
class LLMTextFrame(TextFrame):
|
class LLMTextFrame(TextFrame):
|
||||||
"""Text frame generated by LLM services."""
|
"""Text frame generated by LLM services."""
|
||||||
|
|
||||||
pass
|
def __post_init__(self):
|
||||||
|
super().__post_init__()
|
||||||
|
# LLM services send text frames with all necessary spaces included
|
||||||
|
self.includes_inter_frame_spaces = True
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
@@ -373,9 +373,7 @@ class AnthropicLLMService(LLMService):
|
|||||||
|
|
||||||
if event.type == "content_block_delta":
|
if event.type == "content_block_delta":
|
||||||
if hasattr(event.delta, "text"):
|
if hasattr(event.delta, "text"):
|
||||||
frame = LLMTextFrame(event.delta.text)
|
await self.push_frame(LLMTextFrame(event.delta.text))
|
||||||
frame.includes_inter_frame_spaces = True
|
|
||||||
await self.push_frame(frame)
|
|
||||||
completion_tokens_estimate += self._estimate_tokens(event.delta.text)
|
completion_tokens_estimate += self._estimate_tokens(event.delta.text)
|
||||||
elif hasattr(event.delta, "partial_json") and tool_use_block:
|
elif hasattr(event.delta, "partial_json") and tool_use_block:
|
||||||
json_accumulator += event.delta.partial_json
|
json_accumulator += event.delta.partial_json
|
||||||
|
|||||||
@@ -146,15 +146,6 @@ class AsyncAITTSService(InterruptibleTTSService):
|
|||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@property
|
|
||||||
def includes_inter_frame_spaces(self) -> bool:
|
|
||||||
"""Indicates that AsyncAI TTSTextFrames include necessary inter-frame spaces.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True, indicating that AsyncAI's text frames include necessary inter-frame spaces.
|
|
||||||
"""
|
|
||||||
return True
|
|
||||||
|
|
||||||
def language_to_service_language(self, language: Language) -> Optional[str]:
|
def language_to_service_language(self, language: Language) -> Optional[str]:
|
||||||
"""Convert a Language enum to Async language format.
|
"""Convert a Language enum to Async language format.
|
||||||
|
|
||||||
@@ -433,15 +424,6 @@ class AsyncAIHttpTTSService(TTSService):
|
|||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@property
|
|
||||||
def includes_inter_frame_spaces(self) -> bool:
|
|
||||||
"""Indicates that AsyncAI TTSTextFrames include necessary inter-frame spaces.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True, indicating that AsyncAI's text frames include necessary inter-frame spaces.
|
|
||||||
"""
|
|
||||||
return True
|
|
||||||
|
|
||||||
def language_to_service_language(self, language: Language) -> Optional[str]:
|
def language_to_service_language(self, language: Language) -> Optional[str]:
|
||||||
"""Convert a Language enum to Async language format.
|
"""Convert a Language enum to Async language format.
|
||||||
|
|
||||||
|
|||||||
@@ -1078,9 +1078,7 @@ class AWSBedrockLLMService(LLMService):
|
|||||||
if "contentBlockDelta" in event:
|
if "contentBlockDelta" in event:
|
||||||
delta = event["contentBlockDelta"]["delta"]
|
delta = event["contentBlockDelta"]["delta"]
|
||||||
if "text" in delta:
|
if "text" in delta:
|
||||||
frame = LLMTextFrame(delta["text"])
|
await self.push_frame(LLMTextFrame(delta["text"]))
|
||||||
frame.includes_inter_frame_spaces = True
|
|
||||||
await self.push_frame(frame)
|
|
||||||
completion_tokens_estimate += self._estimate_tokens(delta["text"])
|
completion_tokens_estimate += self._estimate_tokens(delta["text"])
|
||||||
elif "toolUse" in delta and "input" in delta["toolUse"]:
|
elif "toolUse" in delta and "input" in delta["toolUse"]:
|
||||||
# Handle partial JSON for tool use
|
# Handle partial JSON for tool use
|
||||||
|
|||||||
@@ -209,15 +209,6 @@ class AWSPollyTTSService(TTSService):
|
|||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@property
|
|
||||||
def includes_inter_frame_spaces(self) -> bool:
|
|
||||||
"""Indicates that AWS TTSTextFrames include necessary inter-frame spaces.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True, indicating that AWS's text frames include necessary inter-frame spaces.
|
|
||||||
"""
|
|
||||||
return True
|
|
||||||
|
|
||||||
def language_to_service_language(self, language: Language) -> Optional[str]:
|
def language_to_service_language(self, language: Language) -> Optional[str]:
|
||||||
"""Convert a Language enum to AWS Polly language format.
|
"""Convert a Language enum to AWS Polly language format.
|
||||||
|
|
||||||
|
|||||||
@@ -151,15 +151,6 @@ class AzureBaseTTSService(TTSService):
|
|||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@property
|
|
||||||
def includes_inter_frame_spaces(self) -> bool:
|
|
||||||
"""Indicates that Azure TTSTextFrames include necessary inter-frame spaces.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True, indicating that Azure's text frames include necessary inter-frame spaces.
|
|
||||||
"""
|
|
||||||
return True
|
|
||||||
|
|
||||||
def language_to_service_language(self, language: Language) -> Optional[str]:
|
def language_to_service_language(self, language: Language) -> Optional[str]:
|
||||||
"""Convert a Language enum to Azure language format.
|
"""Convert a Language enum to Azure language format.
|
||||||
|
|
||||||
|
|||||||
@@ -79,15 +79,6 @@ class DeepgramTTSService(TTSService):
|
|||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@property
|
|
||||||
def includes_inter_frame_spaces(self) -> bool:
|
|
||||||
"""Indicates that Deepgram TTSTextFrames include necessary inter-frame spaces.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True, indicating that Deepgram's text frames include necessary inter-frame spaces.
|
|
||||||
"""
|
|
||||||
return True
|
|
||||||
|
|
||||||
@traced_tts
|
@traced_tts
|
||||||
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 Deepgram's TTS API.
|
"""Generate speech from text using Deepgram's TTS API.
|
||||||
@@ -177,15 +168,6 @@ class DeepgramHttpTTSService(TTSService):
|
|||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@property
|
|
||||||
def includes_inter_frame_spaces(self) -> bool:
|
|
||||||
"""Indicates that Deepgram TTSTextFrames include necessary inter-frame spaces.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True, indicating that Deepgram's text frames include necessary inter-frame spaces.
|
|
||||||
"""
|
|
||||||
return True
|
|
||||||
|
|
||||||
@traced_tts
|
@traced_tts
|
||||||
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 Deepgram's TTS API.
|
"""Generate speech from text using Deepgram's TTS API.
|
||||||
|
|||||||
@@ -159,15 +159,6 @@ class FishAudioTTSService(InterruptibleTTSService):
|
|||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@property
|
|
||||||
def includes_inter_frame_spaces(self) -> bool:
|
|
||||||
"""Indicates that Fish Audio TTSTextFrames include necessary inter-frame spaces.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True, indicating that Fish Audio's text frames include necessary inter-frame spaces.
|
|
||||||
"""
|
|
||||||
return True
|
|
||||||
|
|
||||||
async def set_model(self, model: str):
|
async def set_model(self, model: str):
|
||||||
"""Set the TTS model and reconnect.
|
"""Set the TTS model and reconnect.
|
||||||
|
|
||||||
|
|||||||
@@ -1452,8 +1452,6 @@ class GeminiLiveLLMService(LLMService):
|
|||||||
self._bot_text_buffer += text
|
self._bot_text_buffer += text
|
||||||
self._search_result_buffer += text # Also accumulate for grounding
|
self._search_result_buffer += text # Also accumulate for grounding
|
||||||
frame = LLMTextFrame(text=text)
|
frame = LLMTextFrame(text=text)
|
||||||
# Gemini Live text already includes any necessary inter-chunk spaces
|
|
||||||
frame.includes_inter_frame_spaces = True
|
|
||||||
await self.push_frame(frame)
|
await self.push_frame(frame)
|
||||||
|
|
||||||
# Check for grounding metadata in server content
|
# Check for grounding metadata in server content
|
||||||
|
|||||||
@@ -920,9 +920,7 @@ class GoogleLLMService(LLMService):
|
|||||||
for part in candidate.content.parts:
|
for part in candidate.content.parts:
|
||||||
if not part.thought and part.text:
|
if not part.thought and part.text:
|
||||||
search_result += part.text
|
search_result += part.text
|
||||||
frame = LLMTextFrame(part.text)
|
await self.push_frame(LLMTextFrame(part.text))
|
||||||
frame.includes_inter_frame_spaces = True
|
|
||||||
await self.push_frame(frame)
|
|
||||||
elif part.function_call:
|
elif part.function_call:
|
||||||
function_call = part.function_call
|
function_call = part.function_call
|
||||||
id = function_call.id or str(uuid.uuid4())
|
id = function_call.id or str(uuid.uuid4())
|
||||||
|
|||||||
@@ -596,15 +596,6 @@ class GoogleHttpTTSService(TTSService):
|
|||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@property
|
|
||||||
def includes_inter_frame_spaces(self) -> bool:
|
|
||||||
"""Indicates that Google TTSTextFrames include necessary inter-frame spaces.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True, indicating that Google's text frames include necessary inter-frame spaces.
|
|
||||||
"""
|
|
||||||
return True
|
|
||||||
|
|
||||||
def language_to_service_language(self, language: Language) -> Optional[str]:
|
def language_to_service_language(self, language: Language) -> Optional[str]:
|
||||||
"""Convert a Language enum to Google TTS language format.
|
"""Convert a Language enum to Google TTS language format.
|
||||||
|
|
||||||
@@ -803,15 +794,6 @@ class GoogleBaseTTSService(TTSService):
|
|||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@property
|
|
||||||
def includes_inter_frame_spaces(self) -> bool:
|
|
||||||
"""Indicates that Google and Gemini TTSTextFrames include necessary inter-frame spaces.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True, indicating that Google's text frames include necessary inter-frame spaces.
|
|
||||||
"""
|
|
||||||
return True
|
|
||||||
|
|
||||||
def language_to_service_language(self, language: Language) -> Optional[str]:
|
def language_to_service_language(self, language: Language) -> Optional[str]:
|
||||||
"""Convert a Language enum to Google TTS language format.
|
"""Convert a Language enum to Google TTS language format.
|
||||||
|
|
||||||
|
|||||||
@@ -111,15 +111,6 @@ class GroqTTSService(TTSService):
|
|||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@property
|
|
||||||
def includes_inter_frame_spaces(self) -> bool:
|
|
||||||
"""Indicates that Groq TTSTextFrames include necessary inter-frame spaces.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True, indicating that Groq's text frames include necessary inter-frame spaces.
|
|
||||||
"""
|
|
||||||
return True
|
|
||||||
|
|
||||||
@traced_tts
|
@traced_tts
|
||||||
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 Groq's TTS API.
|
"""Generate speech from text using Groq's TTS API.
|
||||||
|
|||||||
@@ -110,15 +110,6 @@ class HumeTTSService(TTSService):
|
|||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@property
|
|
||||||
def includes_inter_frame_spaces(self) -> bool:
|
|
||||||
"""Indicates that Hume TTSTextFrames include necessary inter-frame spaces.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True, indicating that Hume's text frames include necessary inter-frame spaces.
|
|
||||||
"""
|
|
||||||
return True
|
|
||||||
|
|
||||||
async def start(self, frame: StartFrame) -> None:
|
async def start(self, frame: StartFrame) -> None:
|
||||||
"""Start the service.
|
"""Start the service.
|
||||||
|
|
||||||
|
|||||||
@@ -250,15 +250,6 @@ class InworldTTSService(TTSService):
|
|||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@property
|
|
||||||
def includes_inter_frame_spaces(self) -> bool:
|
|
||||||
"""Indicates that Inworld TTSTextFrames include necessary inter-frame spaces.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True, indicating that Inworld's text frames include necessary inter-frame spaces.
|
|
||||||
"""
|
|
||||||
return True
|
|
||||||
|
|
||||||
async def start(self, frame: StartFrame):
|
async def start(self, frame: StartFrame):
|
||||||
"""Start the Inworld TTS service.
|
"""Start the Inworld TTS service.
|
||||||
|
|
||||||
|
|||||||
@@ -124,15 +124,6 @@ class LmntTTSService(InterruptibleTTSService):
|
|||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@property
|
|
||||||
def includes_inter_frame_spaces(self) -> bool:
|
|
||||||
"""Indicates that LMNT TTSTextFrames include necessary inter-frame spaces.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True, indicating that LMNT's text frames include necessary inter-frame spaces.
|
|
||||||
"""
|
|
||||||
return True
|
|
||||||
|
|
||||||
def language_to_service_language(self, language: Language) -> Optional[str]:
|
def language_to_service_language(self, language: Language) -> Optional[str]:
|
||||||
"""Convert a Language enum to LMNT service language format.
|
"""Convert a Language enum to LMNT service language format.
|
||||||
|
|
||||||
|
|||||||
@@ -194,15 +194,6 @@ class MiniMaxHttpTTSService(TTSService):
|
|||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@property
|
|
||||||
def includes_inter_frame_spaces(self) -> bool:
|
|
||||||
"""Indicates that MiniMax TTSTextFrames include necessary inter-frame spaces.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True, indicating that MiniMax's text frames include necessary inter-frame spaces.
|
|
||||||
"""
|
|
||||||
return True
|
|
||||||
|
|
||||||
def language_to_service_language(self, language: Language) -> Optional[str]:
|
def language_to_service_language(self, language: Language) -> Optional[str]:
|
||||||
"""Convert a Language enum to MiniMax service language format.
|
"""Convert a Language enum to MiniMax service language format.
|
||||||
|
|
||||||
|
|||||||
@@ -151,15 +151,6 @@ class NeuphonicTTSService(InterruptibleTTSService):
|
|||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@property
|
|
||||||
def includes_inter_frame_spaces(self) -> bool:
|
|
||||||
"""Indicates that Neuphonic TTSTextFrames include necessary inter-frame spaces.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True, indicating that Neuphonic's text frames include necessary inter-frame spaces.
|
|
||||||
"""
|
|
||||||
return True
|
|
||||||
|
|
||||||
def language_to_service_language(self, language: Language) -> Optional[str]:
|
def language_to_service_language(self, language: Language) -> Optional[str]:
|
||||||
"""Convert a Language enum to Neuphonic service language format.
|
"""Convert a Language enum to Neuphonic service language format.
|
||||||
|
|
||||||
@@ -449,15 +440,6 @@ class NeuphonicHttpTTSService(TTSService):
|
|||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@property
|
|
||||||
def includes_inter_frame_spaces(self) -> bool:
|
|
||||||
"""Indicates that Neuphonic TTSTextFrames include necessary inter-frame spaces.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True, indicating that Neuphonic's text frames include necessary inter-frame spaces.
|
|
||||||
"""
|
|
||||||
return True
|
|
||||||
|
|
||||||
def language_to_service_language(self, language: Language) -> Optional[str]:
|
def language_to_service_language(self, language: Language) -> Optional[str]:
|
||||||
"""Convert a Language enum to Neuphonic service language format.
|
"""Convert a Language enum to Neuphonic service language format.
|
||||||
|
|
||||||
|
|||||||
@@ -390,9 +390,7 @@ class BaseOpenAILLMService(LLMService):
|
|||||||
# Keep iterating through the response to collect all the argument fragments
|
# Keep iterating through the response to collect all the argument fragments
|
||||||
arguments += tool_call.function.arguments
|
arguments += tool_call.function.arguments
|
||||||
elif chunk.choices[0].delta.content:
|
elif chunk.choices[0].delta.content:
|
||||||
frame = LLMTextFrame(chunk.choices[0].delta.content)
|
await self.push_frame(LLMTextFrame(chunk.choices[0].delta.content))
|
||||||
frame.includes_inter_frame_spaces = True
|
|
||||||
await self.push_frame(frame)
|
|
||||||
|
|
||||||
# When gpt-4o-audio / gpt-4o-mini-audio is used for llm or stt+llm
|
# When gpt-4o-audio / gpt-4o-mini-audio is used for llm or stt+llm
|
||||||
# we need to get LLMTextFrame for the transcript
|
# we need to get LLMTextFrame for the transcript
|
||||||
|
|||||||
@@ -678,8 +678,6 @@ class OpenAIRealtimeLLMService(LLMService):
|
|||||||
# the output modality is "text"
|
# the output modality is "text"
|
||||||
if evt.delta:
|
if evt.delta:
|
||||||
frame = LLMTextFrame(evt.delta)
|
frame = LLMTextFrame(evt.delta)
|
||||||
# OpenAI Realtime text already includes any necessary inter-chunk spaces
|
|
||||||
frame.includes_inter_frame_spaces = True
|
|
||||||
await self.push_frame(frame)
|
await self.push_frame(frame)
|
||||||
|
|
||||||
async def _handle_evt_audio_transcript_delta(self, evt):
|
async def _handle_evt_audio_transcript_delta(self, evt):
|
||||||
|
|||||||
@@ -131,15 +131,6 @@ class OpenAITTSService(TTSService):
|
|||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@property
|
|
||||||
def includes_inter_frame_spaces(self) -> bool:
|
|
||||||
"""Indicates that OpenAI TTSTextFrames include necessary inter-frame spaces.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True, indicating that OpenAI's text frames include necessary inter-frame spaces.
|
|
||||||
"""
|
|
||||||
return True
|
|
||||||
|
|
||||||
async def set_model(self, model: str):
|
async def set_model(self, model: str):
|
||||||
"""Set the TTS model to use.
|
"""Set the TTS model to use.
|
||||||
|
|
||||||
|
|||||||
@@ -66,15 +66,6 @@ class PiperTTSService(TTSService):
|
|||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@property
|
|
||||||
def includes_inter_frame_spaces(self) -> bool:
|
|
||||||
"""Indicates that Piper TTSTextFrames include necessary inter-frame spaces.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True, indicating that Piper's text frames include necessary inter-frame spaces.
|
|
||||||
"""
|
|
||||||
return True
|
|
||||||
|
|
||||||
@traced_tts
|
@traced_tts
|
||||||
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 Piper's HTTP API.
|
"""Generate speech from text using Piper's HTTP API.
|
||||||
|
|||||||
@@ -501,15 +501,6 @@ class RimeHttpTTSService(TTSService):
|
|||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@property
|
|
||||||
def includes_inter_frame_spaces(self) -> bool:
|
|
||||||
"""Indicates that Rime TTSTextFrames include necessary inter-frame spaces.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True, indicating that Rime's text frames include necessary inter-frame spaces.
|
|
||||||
"""
|
|
||||||
return True
|
|
||||||
|
|
||||||
def language_to_service_language(self, language: Language) -> str | None:
|
def language_to_service_language(self, language: Language) -> str | None:
|
||||||
"""Convert pipecat language to Rime language code.
|
"""Convert pipecat language to Rime language code.
|
||||||
|
|
||||||
|
|||||||
@@ -113,15 +113,6 @@ class RivaTTSService(TTSService):
|
|||||||
riva.client.proto.riva_tts_pb2.RivaSynthesisConfigRequest()
|
riva.client.proto.riva_tts_pb2.RivaSynthesisConfigRequest()
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
|
||||||
def includes_inter_frame_spaces(self) -> bool:
|
|
||||||
"""Indicates that Riva TTSTextFrames include necessary inter-frame spaces.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True, indicating that Riva's text frames include necessary inter-frame spaces.
|
|
||||||
"""
|
|
||||||
return True
|
|
||||||
|
|
||||||
async def set_model(self, model: str):
|
async def set_model(self, model: str):
|
||||||
"""Attempt to set the TTS model.
|
"""Attempt to set the TTS model.
|
||||||
|
|
||||||
|
|||||||
@@ -176,9 +176,7 @@ class SambaNovaLLMService(OpenAILLMService): # type: ignore
|
|||||||
# Keep iterating through the response to collect all the argument fragments
|
# Keep iterating through the response to collect all the argument fragments
|
||||||
arguments += tool_call.function.arguments
|
arguments += tool_call.function.arguments
|
||||||
elif chunk.choices[0].delta.content:
|
elif chunk.choices[0].delta.content:
|
||||||
frame = LLMTextFrame(chunk.choices[0].delta.content)
|
await self.push_frame(LLMTextFrame(chunk.choices[0].delta.content))
|
||||||
frame.includes_inter_frame_spaces = True
|
|
||||||
await self.push_frame(frame)
|
|
||||||
|
|
||||||
# When gpt-4o-audio / gpt-4o-mini-audio is used for llm or stt+llm
|
# When gpt-4o-audio / gpt-4o-mini-audio is used for llm or stt+llm
|
||||||
# we need to get LLMTextFrame for the transcript
|
# we need to get LLMTextFrame for the transcript
|
||||||
|
|||||||
@@ -195,15 +195,6 @@ class SarvamHttpTTSService(TTSService):
|
|||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@property
|
|
||||||
def includes_inter_frame_spaces(self) -> bool:
|
|
||||||
"""Indicates that Sarvam TTSTextFrames include necessary inter-frame spaces.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True, indicating that Sarvam's text frames include necessary inter-frame spaces.
|
|
||||||
"""
|
|
||||||
return True
|
|
||||||
|
|
||||||
def language_to_service_language(self, language: Language) -> Optional[str]:
|
def language_to_service_language(self, language: Language) -> Optional[str]:
|
||||||
"""Convert a Language enum to Sarvam AI language format.
|
"""Convert a Language enum to Sarvam AI language format.
|
||||||
|
|
||||||
@@ -467,15 +458,6 @@ class SarvamTTSService(InterruptibleTTSService):
|
|||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@property
|
|
||||||
def includes_inter_frame_spaces(self) -> bool:
|
|
||||||
"""Indicates that Sarvam TTSTextFrames include necessary inter-frame spaces.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True, indicating that Sarvam's text frames include necessary inter-frame spaces.
|
|
||||||
"""
|
|
||||||
return True
|
|
||||||
|
|
||||||
def language_to_service_language(self, language: Language) -> Optional[str]:
|
def language_to_service_language(self, language: Language) -> Optional[str]:
|
||||||
"""Convert a Language enum to Sarvam AI language format.
|
"""Convert a Language enum to Sarvam AI language format.
|
||||||
|
|
||||||
|
|||||||
@@ -105,15 +105,6 @@ class SpeechmaticsTTSService(TTSService):
|
|||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@property
|
|
||||||
def includes_inter_frame_spaces(self) -> bool:
|
|
||||||
"""Indicates that Speechmatics TTSTextFrames include necessary inter-frame spaces.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True, indicating that Speechmatics's text frames include necessary inter-frame spaces.
|
|
||||||
"""
|
|
||||||
return True
|
|
||||||
|
|
||||||
@traced_tts
|
@traced_tts
|
||||||
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 Speechmatics' HTTP API.
|
"""Generate speech from text using Speechmatics' HTTP API.
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ class TTSService(AIService):
|
|||||||
self._voice_id: str = ""
|
self._voice_id: str = ""
|
||||||
self._settings: Dict[str, Any] = {}
|
self._settings: Dict[str, Any] = {}
|
||||||
self._text_aggregator: BaseTextAggregator = text_aggregator or SimpleTextAggregator()
|
self._text_aggregator: BaseTextAggregator = text_aggregator or SimpleTextAggregator()
|
||||||
|
self._aggregated_text_includes_inter_frame_spaces: bool = False
|
||||||
self._text_filters: Sequence[BaseTextFilter] = text_filters or []
|
self._text_filters: Sequence[BaseTextFilter] = text_filters or []
|
||||||
self._transport_destination: Optional[str] = transport_destination
|
self._transport_destination: Optional[str] = transport_destination
|
||||||
self._tracing_enabled: bool = False
|
self._tracing_enabled: bool = False
|
||||||
@@ -192,23 +193,6 @@ class TTSService(AIService):
|
|||||||
CHUNK_SECONDS = 0.5
|
CHUNK_SECONDS = 0.5
|
||||||
return int(self.sample_rate * CHUNK_SECONDS * 2) # 2 bytes/sample
|
return int(self.sample_rate * CHUNK_SECONDS * 2) # 2 bytes/sample
|
||||||
|
|
||||||
@property
|
|
||||||
def includes_inter_frame_spaces(self) -> bool:
|
|
||||||
"""Indicates whether TTSTextFrames include necesary inter-frame spaces.
|
|
||||||
|
|
||||||
When True, the TTSTextFrame objects pushed by this service already
|
|
||||||
include all necessary spaces between subsequent frames. When False,
|
|
||||||
downstream processors (like the assistant context aggregator) may need
|
|
||||||
to add spacing.
|
|
||||||
|
|
||||||
Subclasses should override this property to return True if their text
|
|
||||||
generation process already includes necessary inter-frame spaces.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
False by default. Subclasses can override to return True.
|
|
||||||
"""
|
|
||||||
return False
|
|
||||||
|
|
||||||
async def set_model(self, model: str):
|
async def set_model(self, model: str):
|
||||||
"""Set the TTS model to use.
|
"""Set the TTS model to use.
|
||||||
|
|
||||||
@@ -369,9 +353,16 @@ class TTSService(AIService):
|
|||||||
await self._maybe_pause_frame_processing()
|
await self._maybe_pause_frame_processing()
|
||||||
|
|
||||||
sentence = self._text_aggregator.text
|
sentence = self._text_aggregator.text
|
||||||
|
includes_inter_frame_spaces = self._aggregated_text_includes_inter_frame_spaces
|
||||||
|
|
||||||
|
# Reset aggregator state
|
||||||
await self._text_aggregator.reset()
|
await self._text_aggregator.reset()
|
||||||
self._processing_text = False
|
self._processing_text = False
|
||||||
await self._push_tts_frames(sentence)
|
self._aggregated_text_includes_inter_frame_spaces = False
|
||||||
|
|
||||||
|
await self._push_tts_frames(
|
||||||
|
sentence, includes_inter_frame_spaces=includes_inter_frame_spaces
|
||||||
|
)
|
||||||
if isinstance(frame, LLMFullResponseEndFrame):
|
if isinstance(frame, LLMFullResponseEndFrame):
|
||||||
if self._push_text_frames:
|
if self._push_text_frames:
|
||||||
await self.push_frame(frame, direction)
|
await self.push_frame(frame, direction)
|
||||||
@@ -380,7 +371,8 @@ class TTSService(AIService):
|
|||||||
elif isinstance(frame, TTSSpeakFrame):
|
elif isinstance(frame, TTSSpeakFrame):
|
||||||
# Store if we were processing text or not so we can set it back.
|
# Store if we were processing text or not so we can set it back.
|
||||||
processing_text = self._processing_text
|
processing_text = self._processing_text
|
||||||
await self._push_tts_frames(frame.text)
|
# Assumption: text in TTSSpeakFrame does not include inter-frame spaces
|
||||||
|
await self._push_tts_frames(frame.text, includes_inter_frame_spaces=False)
|
||||||
# We pause processing incoming frames because we are sending data to
|
# We pause processing incoming frames because we are sending data to
|
||||||
# the TTS. We pause to avoid audio overlapping.
|
# the TTS. We pause to avoid audio overlapping.
|
||||||
await self._maybe_pause_frame_processing()
|
await self._maybe_pause_frame_processing()
|
||||||
@@ -474,11 +466,17 @@ class TTSService(AIService):
|
|||||||
text = frame.text
|
text = frame.text
|
||||||
else:
|
else:
|
||||||
text = await self._text_aggregator.aggregate(frame.text)
|
text = await self._text_aggregator.aggregate(frame.text)
|
||||||
|
# Assumption: whether inter-frame spaces are included shouldn't
|
||||||
|
# change during aggregation, so we can just use the latest frame's
|
||||||
|
# value
|
||||||
|
self._aggregated_text_includes_inter_frame_spaces = frame.includes_inter_frame_spaces
|
||||||
|
|
||||||
if text:
|
if text:
|
||||||
await self._push_tts_frames(text)
|
await self._push_tts_frames(
|
||||||
|
text, includes_inter_frame_spaces=frame.includes_inter_frame_spaces
|
||||||
|
)
|
||||||
|
|
||||||
async def _push_tts_frames(self, text: str):
|
async def _push_tts_frames(self, text: str, includes_inter_frame_spaces: bool):
|
||||||
# Remove leading newlines only
|
# Remove leading newlines only
|
||||||
text = text.lstrip("\n")
|
text = text.lstrip("\n")
|
||||||
|
|
||||||
@@ -508,7 +506,7 @@ class TTSService(AIService):
|
|||||||
# We send the original text after the audio. This way, if we are
|
# We send the original text after the audio. This way, if we are
|
||||||
# interrupted, the text is not added to the assistant context.
|
# interrupted, the text is not added to the assistant context.
|
||||||
frame = TTSTextFrame(text)
|
frame = TTSTextFrame(text)
|
||||||
frame.includes_inter_frame_spaces = self.includes_inter_frame_spaces
|
frame.includes_inter_frame_spaces = includes_inter_frame_spaces
|
||||||
await self.push_frame(frame)
|
await self.push_frame(frame)
|
||||||
|
|
||||||
async def _stop_frame_handler(self):
|
async def _stop_frame_handler(self):
|
||||||
@@ -635,6 +633,8 @@ class WordTTSService(TTSService):
|
|||||||
frame = TTSStoppedFrame()
|
frame = TTSStoppedFrame()
|
||||||
frame.pts = last_pts
|
frame.pts = last_pts
|
||||||
else:
|
else:
|
||||||
|
# Assumption: word-by-word text frames don't include spaces, so
|
||||||
|
# we can rely on the default includes_inter_frame_spaces=False
|
||||||
frame = TTSTextFrame(word)
|
frame = TTSTextFrame(word)
|
||||||
frame.pts = self._initial_word_timestamp + timestamp
|
frame.pts = self._initial_word_timestamp + timestamp
|
||||||
if frame:
|
if frame:
|
||||||
|
|||||||
Reference in New Issue
Block a user