TTSServices: for now just specify a single text aggregator

This commit is contained in:
Aleix Conchillo Flaqué
2025-03-19 11:02:29 -07:00
parent fc0f404d26
commit 336e2f1579
5 changed files with 15 additions and 32 deletions

View File

@@ -21,8 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added new `BaseTextAggregator`. Text aggregators are used by the TTS service - Added new `BaseTextAggregator`. Text aggregators are used by the TTS service
to aggregate LLM tokens and decide when the aggregated text should be pushed to aggregate LLM tokens and decide when the aggregated text should be pushed
to the TTS service. They also allow for the text to be manipulated while it's to the TTS service. They also allow for the text to be manipulated while it's
being aggregated. Multiple text aggregators can be passed with being aggregated. A text aggregator can be passed via `text_aggregator` to the
`text_aggregators` to the TTS service. TTS service.
- Added new `UltravoxSTTService`. - Added new `UltravoxSTTService`.
(see https://github.com/fixie-ai/ultravox) (see https://github.com/fixie-ai/ultravox)

View File

@@ -119,7 +119,7 @@ async def main():
tts = CartesiaTTSService( tts = CartesiaTTSService(
api_key=os.getenv("CARTESIA_API_KEY"), api_key=os.getenv("CARTESIA_API_KEY"),
voice_id=VOICE_IDS["narrator"], voice_id=VOICE_IDS["narrator"],
text_aggregators=[pattern_aggregator], text_aggregator=pattern_aggregator,
) )
# Initialize LLM # Initialize LLM

View File

@@ -239,7 +239,7 @@ class TTSService(AIService):
# TTS output sample rate # TTS output sample rate
sample_rate: Optional[int] = None, sample_rate: Optional[int] = None,
# Text aggregator to aggregate incoming tokens and decide when to push to the TTS. # Text aggregator to aggregate incoming tokens and decide when to push to the TTS.
text_aggregators: Sequence[BaseTextAggregator] = [], text_aggregator: Optional[BaseTextAggregator] = None,
# Text filter executed after text has been aggregated. # Text filter executed after text has been aggregated.
text_filters: Sequence[BaseTextFilter] = [], text_filters: Sequence[BaseTextFilter] = [],
text_filter: Optional[BaseTextFilter] = None, text_filter: Optional[BaseTextFilter] = None,
@@ -257,10 +257,7 @@ class TTSService(AIService):
self._sample_rate = 0 self._sample_rate = 0
self._voice_id: str = "" self._voice_id: str = ""
self._settings: Dict[str, Any] = {} self._settings: Dict[str, Any] = {}
# Ensure there's at least one text aggregator. self._text_aggregator: BaseTextAggregator = text_aggregator or SimpleTextAggregator()
self._text_aggregators: Sequence[BaseTextAggregator] = text_aggregators or [
SimpleTextAggregator()
]
self._text_filters: Sequence[BaseTextFilter] = text_filters self._text_filters: Sequence[BaseTextFilter] = text_filters
if text_filter: if text_filter:
import warnings import warnings
@@ -358,8 +355,8 @@ class TTSService(AIService):
# pause to avoid audio overlapping. # pause to avoid audio overlapping.
await self._maybe_pause_frame_processing() await self._maybe_pause_frame_processing()
sentence = self._text_aggregators[-1].text sentence = self._text_aggregator.text
self._reset_aggregators() self._text_aggregator.reset()
self._processing_text = False self._processing_text = False
await self._push_tts_frames(sentence) await self._push_tts_frames(sentence)
if isinstance(frame, LLMFullResponseEndFrame): if isinstance(frame, LLMFullResponseEndFrame):
@@ -405,8 +402,7 @@ class TTSService(AIService):
async def _handle_interruption(self, frame: StartInterruptionFrame, direction: FrameDirection): async def _handle_interruption(self, frame: StartInterruptionFrame, direction: FrameDirection):
self._processing_text = False self._processing_text = False
for aggregator in self._text_aggregators: self._text_aggregator.handle_interruption()
aggregator.handle_interruption()
for filter in self._text_filters: for filter in self._text_filters:
filter.handle_interruption() filter.handle_interruption()
@@ -418,25 +414,12 @@ class TTSService(AIService):
if self._pause_frame_processing: if self._pause_frame_processing:
await self.resume_processing_frames() await self.resume_processing_frames()
def _reset_aggregators(self):
for aggregator in self._text_aggregators:
aggregator.reset()
async def _process_text_frame(self, frame: TextFrame): async def _process_text_frame(self, frame: TextFrame):
text: Optional[str] = None text: Optional[str] = None
if not self._aggregate_sentences: if not self._aggregate_sentences:
text = frame.text text = frame.text
else: else:
current_text = frame.text text = self._text_aggregator.aggregate(frame.text)
# Process all aggregators except the last one.
for aggregator in self._text_aggregators[:-1]:
aggregator.aggregate(current_text)
current_text = aggregator.text
# The last aggregator decides whether we are sending text to the
# TTS or not.
text = self._text_aggregators[-1].aggregate(current_text)
if text: if text:
await self._push_tts_frames(text) await self._push_tts_frames(text)

View File

@@ -7,7 +7,7 @@
import base64 import base64
import json import json
import uuid import uuid
from typing import AsyncGenerator, List, Optional, Sequence, Union from typing import AsyncGenerator, List, Optional, Union
from loguru import logger from loguru import logger
from pydantic import BaseModel from pydantic import BaseModel
@@ -91,7 +91,7 @@ class CartesiaTTSService(AudioContextWordTTSService):
encoding: str = "pcm_s16le", encoding: str = "pcm_s16le",
container: str = "raw", container: str = "raw",
params: InputParams = InputParams(), params: InputParams = InputParams(),
text_aggregators: Sequence[BaseTextAggregator] = [], text_aggregator: Optional[BaseTextAggregator] = None,
**kwargs, **kwargs,
): ):
# Aggregating sentences still gives cleaner-sounding results and fewer # Aggregating sentences still gives cleaner-sounding results and fewer
@@ -109,7 +109,7 @@ class CartesiaTTSService(AudioContextWordTTSService):
push_text_frames=False, push_text_frames=False,
pause_frame_processing=True, pause_frame_processing=True,
sample_rate=sample_rate, sample_rate=sample_rate,
text_aggregators=text_aggregators or [SkipTagsAggregator([("<spell>", "</spell>")])], text_aggregator=text_aggregator or SkipTagsAggregator([("<spell>", "</spell>")]),
**kwargs, **kwargs,
) )

View File

@@ -7,7 +7,7 @@
import base64 import base64
import json import json
import uuid import uuid
from typing import AsyncGenerator, Optional, Sequence from typing import AsyncGenerator, Optional
import aiohttp import aiohttp
from loguru import logger from loguru import logger
@@ -80,7 +80,7 @@ class RimeTTSService(AudioContextWordTTSService):
model: str = "mistv2", model: str = "mistv2",
sample_rate: Optional[int] = None, sample_rate: Optional[int] = None,
params: InputParams = InputParams(), params: InputParams = InputParams(),
text_aggregators: Sequence[BaseTextAggregator] = [], text_aggregator: Optional[BaseTextAggregator] = None,
**kwargs, **kwargs,
): ):
"""Initialize Rime TTS service. """Initialize Rime TTS service.
@@ -100,7 +100,7 @@ class RimeTTSService(AudioContextWordTTSService):
push_stop_frames=True, push_stop_frames=True,
pause_frame_processing=True, pause_frame_processing=True,
sample_rate=sample_rate, sample_rate=sample_rate,
text_aggregators=text_aggregators or [SkipTagsAggregator([("spell(", ")")])], text_aggregator=text_aggregator or SkipTagsAggregator([("spell(", ")")]),
**kwargs, **kwargs,
) )