From 93c40b87dcd12fc5a47d3db3b5246e957bdcacc9 Mon Sep 17 00:00:00 2001 From: Kwindla Hultman Kramer Date: Thu, 1 May 2025 17:07:00 -0700 Subject: [PATCH 01/13] small groq updates --- examples/foundational/07l-interruptible-groq.py | 9 +++++++-- examples/foundational/14f-function-calling-groq.py | 9 +++++++-- pyproject.toml | 2 +- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/examples/foundational/07l-interruptible-groq.py b/examples/foundational/07l-interruptible-groq.py index b6826d49c..869548274 100644 --- a/examples/foundational/07l-interruptible-groq.py +++ b/examples/foundational/07l-interruptible-groq.py @@ -14,6 +14,7 @@ from pipecat.audio.vad.silero import SileroVADAnalyzer from pipecat.pipeline.pipeline import Pipeline from pipecat.pipeline.runner import PipelineRunner from pipecat.pipeline.task import PipelineParams, PipelineTask +from pipecat.processors.aggregators.llm_response import LLMUserAggregatorParams from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext from pipecat.services.groq.llm import GroqLLMService from pipecat.services.groq.stt import GroqSTTService @@ -39,7 +40,9 @@ async def run_bot(webrtc_connection: SmallWebRTCConnection, _: argparse.Namespac stt = GroqSTTService(api_key=os.getenv("GROQ_API_KEY")) - llm = GroqLLMService(api_key=os.getenv("GROQ_API_KEY"), model="llama-3.3-70b-versatile") + llm = GroqLLMService( + api_key=os.getenv("GROQ_API_KEY"), model="meta-llama/llama-4-maverick-17b-128e-instruct" + ) tts = GroqTTSService(api_key=os.getenv("GROQ_API_KEY")) @@ -51,7 +54,9 @@ async def run_bot(webrtc_connection: SmallWebRTCConnection, _: argparse.Namespac ] context = OpenAILLMContext(messages) - context_aggregator = llm.create_context_aggregator(context) + context_aggregator = llm.create_context_aggregator( + context, user_params=LLMUserAggregatorParams(aggregation_timeout=0.05) + ) pipeline = Pipeline( [ diff --git a/examples/foundational/14f-function-calling-groq.py b/examples/foundational/14f-function-calling-groq.py index f71a43fa2..ee6a9a855 100644 --- a/examples/foundational/14f-function-calling-groq.py +++ b/examples/foundational/14f-function-calling-groq.py @@ -17,6 +17,7 @@ from pipecat.frames.frames import TTSSpeakFrame from pipecat.pipeline.pipeline import Pipeline from pipecat.pipeline.runner import PipelineRunner from pipecat.pipeline.task import PipelineParams, PipelineTask +from pipecat.processors.aggregators.llm_response import LLMUserAggregatorParams from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext from pipecat.services.cartesia.tts import CartesiaTTSService from pipecat.services.groq.llm import GroqLLMService @@ -53,7 +54,9 @@ async def run_bot(webrtc_connection: SmallWebRTCConnection, _: argparse.Namespac voice_id="71a7ad14-091c-4e8e-a314-022ece01c121", # British Reading Lady ) - llm = GroqLLMService(api_key=os.getenv("GROQ_API_KEY"), model="llama-3.3-70b-versatile") + llm = GroqLLMService( + api_key=os.getenv("GROQ_API_KEY"), model="meta-llama/llama-4-maverick-17b-128e-instruct" + ) # You can also register a function_name of None to get all functions # sent to the same callback with an additional function_name parameter. llm.register_function("get_current_weather", fetch_weather_from_api) @@ -83,7 +86,9 @@ async def run_bot(webrtc_connection: SmallWebRTCConnection, _: argparse.Namespac ] context = OpenAILLMContext(messages, tools) - context_aggregator = llm.create_context_aggregator(context) + context_aggregator = llm.create_context_aggregator( + context, user_params=LLMUserAggregatorParams(aggregation_timeout=0.05) + ) pipeline = Pipeline( [ diff --git a/pyproject.toml b/pyproject.toml index f7db97ea0..6d1cf840c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,7 +56,7 @@ fish = [ "ormsgpack~=1.7.0", "websockets~=13.1" ] gladia = [ "websockets~=13.1" ] google = [ "google-cloud-speech~=2.31.1", "google-cloud-texttospeech~=2.25.1", "google-genai~=1.7.0", "google-generativeai~=0.8.4", "websockets~=13.1" ] grok = [] -groq = [ "groq~=0.20.0" ] +groq = [ "groq~=0.23.0" ] gstreamer = [ "pygobject~=3.50.0" ] krisp = [ "pipecat-ai-krisp~=0.3.0" ] koala = [ "pvkoala~=2.0.3" ] From 15cbd18acce55771967d642bd6d4bf48e53a9b0e Mon Sep 17 00:00:00 2001 From: Matt Kim Date: Wed, 30 Apr 2025 16:44:42 -0700 Subject: [PATCH 02/13] [Rime] Add phonemizeBetweenBrackets and pauseBetweenBrackets to RimeTTSService (ws) There is a fix incoming in --- src/pipecat/services/rime/tts.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/pipecat/services/rime/tts.py b/src/pipecat/services/rime/tts.py index e6d1d400a..57ade7484 100644 --- a/src/pipecat/services/rime/tts.py +++ b/src/pipecat/services/rime/tts.py @@ -68,6 +68,8 @@ class RimeTTSService(AudioContextWordTTSService): language: Optional[Language] = Language.EN speed_alpha: Optional[float] = 1.0 reduce_latency: Optional[bool] = False + pause_between_brackets: Optional[bool] = False + phonemize_between_brackets: Optional[bool] = False def __init__( self, @@ -117,6 +119,8 @@ class RimeTTSService(AudioContextWordTTSService): else "eng", "speedAlpha": params.speed_alpha, "reduceLatency": params.reduce_latency, + "pauseBetweenBrackets": json.dumps(params.pause_between_brackets), + "phonemizeBetweenBrackets": json.dumps(params.phonemize_between_brackets), } # State tracking From 02c07755b0c5bbb922a03ae057d621bea4e732df Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Fri, 2 May 2025 16:09:50 -0400 Subject: [PATCH 03/13] Add Changelog entry for PR 1707 --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 079142b9a..5954e6baf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Added support for cross-platform local smart turn detection. You can use +- Added two new input parameters to `RimeTTSService`: `pause_between_brackets` + and `phonemize_between_brackets`. + +- Added support for cross-platform local smart turn detection. You can use `LocalSmartTurnAnalyzer` for on-device inference using Torch. - `BaseOutputTransport` now allows multiple destinations if the transport From 63a65627a29ad29dfd554ea3139977b4eb10f511 Mon Sep 17 00:00:00 2001 From: vipyne Date: Wed, 30 Apr 2025 11:25:10 -0500 Subject: [PATCH 04/13] Riva Service: add magpie-tts-multilingual model --- .../07r-interruptible-riva-nim.py | 8 +- src/pipecat/services/riva/stt.py | 204 +++++++++++++++++- src/pipecat/services/riva/tts.py | 54 ++++- 3 files changed, 251 insertions(+), 15 deletions(-) diff --git a/examples/foundational/07r-interruptible-riva-nim.py b/examples/foundational/07r-interruptible-riva-nim.py index 915beda51..ca74dfd7a 100644 --- a/examples/foundational/07r-interruptible-riva-nim.py +++ b/examples/foundational/07r-interruptible-riva-nim.py @@ -16,8 +16,8 @@ from pipecat.pipeline.runner import PipelineRunner from pipecat.pipeline.task import PipelineParams, PipelineTask from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext from pipecat.services.nim.llm import NimLLMService -from pipecat.services.riva.stt import ParakeetSTTService -from pipecat.services.riva.tts import FastPitchTTSService +from pipecat.services.riva.stt import ParakeetSTTService, RivaOfflineSTTService +from pipecat.services.riva.tts import RivaTTSService, FastPitchTTSService from pipecat.transports.base_transport import TransportParams from pipecat.transports.network.small_webrtc import SmallWebRTCTransport from pipecat.transports.network.webrtc_connection import SmallWebRTCConnection @@ -37,11 +37,13 @@ async def run_bot(webrtc_connection: SmallWebRTCConnection, _: argparse.Namespac ), ) - stt = ParakeetSTTService(api_key=os.getenv("NVIDIA_API_KEY")) + stt = RivaOfflineSTTService(api_key=os.getenv("NVIDIA_API_KEY")) + # stt = ParakeetSTTService(api_key=os.getenv("NVIDIA_API_KEY")) llm = NimLLMService(api_key=os.getenv("NVIDIA_API_KEY"), model="meta/llama-3.1-405b-instruct") tts = FastPitchTTSService(api_key=os.getenv("NVIDIA_API_KEY")) + # tts = RivaTTSService(api_key=os.getenv("NVIDIA_API_KEY")) messages = [ { diff --git a/src/pipecat/services/riva/stt.py b/src/pipecat/services/riva/stt.py index 6328bcb65..3676befa3 100644 --- a/src/pipecat/services/riva/stt.py +++ b/src/pipecat/services/riva/stt.py @@ -13,11 +13,13 @@ from pydantic import BaseModel from pipecat.frames.frames import ( CancelFrame, EndFrame, + ErrorFrame, Frame, InterimTranscriptionFrame, StartFrame, TranscriptionFrame, ) +from pipecat.services.stt_service import SegmentedSTTService from pipecat.services.stt_service import STTService from pipecat.transcriptions.language import Language from pipecat.utils.time import time_now_iso8601 @@ -27,20 +29,21 @@ try: except ModuleNotFoundError as e: logger.error(f"Exception: {e}") - logger.error("In order to use NVIDIA Riva STT, you need to `pip install pipecat-ai[riva]`.") + logger.error("In order to use NVIDIA Riva STT, you need to `pip install pipecat-ai[riva]`. Also set NVIDIA_API_KEY env var.") raise Exception(f"Missing module: {e}") -class ParakeetSTTService(STTService): +class RivaSTTService(STTService): class InputParams(BaseModel): language: Optional[Language] = Language.EN_US def __init__( self, *, - api_key: str, + api_key: str = None, server: str = "grpc.nvcf.nvidia.com:443", function_id: str = "1598d209-5e27-4d3c-8079-4751568b1081", + model_name: str = "parakeet-ctc-1.1b-asr", sample_rate: Optional[int] = None, params: InputParams = InputParams(), **kwargs, @@ -61,7 +64,7 @@ class ParakeetSTTService(STTService): self._stop_threshold_eou = -1.0 self._custom_configuration = "" - self.set_model_name("parakeet-ctc-1.1b-asr") + self.set_model_name(model_name) metadata = [ ["function-id", function_id], @@ -196,3 +199,196 @@ class ParakeetSTTService(STTService): def __iter__(self): return self + +class RivaOfflineSTTService(SegmentedSTTService): + """Speech-to-text service using Fal's Wizper API. + + This service uses Fal's Wizper API to perform speech-to-text transcription on audio + segments. It inherits from SegmentedSTTService to handle audio buffering and speech detection. + + Args: + api_key: NVIDIA_API_KEY. + sample_rate: Audio sample rate in Hz. If not provided, uses the pipeline's rate. + params: Configuration parameters for Riva. + **kwargs: Additional arguments passed to SegmentedSTTService. + """ + + class InputParams(BaseModel): + """Configuration parameters for Fal's Wizper API. + + Attributes: + language: Language of the audio input. Defaults to English. + task: Task to perform ('transcribe' or 'translate'). Defaults to 'transcribe'. + chunk_level: Level of chunking ('segment'). Defaults to 'segment'. + version: Version of Wizper model to use. Defaults to '3'. + """ + + language: Optional[Language] = Language.EN + task: str = "transcribe" + chunk_level: str = "segment" + version: str = "3" + + def __init__( + self, + *, + api_key: str = None, + server: str = "grpc.nvcf.nvidia.com:443", + function_id: str = "ee8dc628-76de-4acc-8595-1836e7e857bd", + model_name: str = "canary-1b-asr", + sample_rate: Optional[int] = None, + params: InputParams = InputParams(), + **kwargs, + ): + super().__init__(sample_rate=sample_rate, **kwargs) + self._api_key = api_key + self._profanity_filter = False + self._automatic_punctuation = False + self._no_verbatim_transcripts = False + self._language_code = params.language + self._boosted_lm_words = None + self._boosted_lm_score = 4.0 + self._start_history = -1 + self._start_threshold = -1.0 + self._stop_history = -1 + self._stop_threshold = -1.0 + self._stop_history_eou = -1 + self._stop_threshold_eou = -1.0 + self._custom_configuration = "" + + self.set_model_name(model_name) + + metadata = [ + ["function-id", function_id], + ["authorization", f"Bearer {api_key}"], + ] + auth = riva.client.Auth(None, True, server, metadata) + + self._asr_service = riva.client.ASRService(auth) + + self._queue = asyncio.Queue() + self._config = None + self._thread_task = None + self._response_task = None + + def can_generate_metrics(self) -> bool: + return False + + async def start(self, frame: StartFrame): + await super().start(frame) + + if self._config: + return + + # config = riva.client.StreamingRecognitionConfig( + config=riva.client.RecognitionConfig( + # encoding=riva.client.AudioEncoding.LINEAR_PCM, + language_code=self._language_code, + # model="", + max_alternatives=1, + profanity_filter=self._profanity_filter, + enable_automatic_punctuation=self._automatic_punctuation, + verbatim_transcripts=not self._no_verbatim_transcripts, + # sample_rate_hertz=self.sample_rate, + # audio_channel_count=1, + # enable_word_time_offsets=args.word_time_offsets or args.speaker_diarization,?? + # ), + # interim_results=True, + ) + + riva.client.add_word_boosting_to_config( + config, self._boosted_lm_words, self._boosted_lm_score + ) + + riva.client.add_endpoint_parameters_to_config( + config, + self._start_history, + self._start_threshold, + self._stop_history, + self._stop_history_eou, + self._stop_threshold, + self._stop_threshold_eou, + ) + riva.client.add_custom_configuration_to_config(config, self._custom_configuration) + + self._config = config + + + + async def run_stt(self, audio: bytes) -> AsyncGenerator[Frame, None]: + """Transcribe an audio segment + + Args: + audio: Raw audio bytes in WAV format (already converted by base class). + + Yields: + Frame: TranscriptionFrame containing the transcribed text. + + Note: + The audio is already in WAV format from the SegmentedSTTService. + Only non-empty transcriptions are yielded. + """ + try: + response = self._asr_service.offline_recognize(audio, self._config) + # response = riva.client.print_offline(response=self._asr_service.offline_recognize(audio, self._config)) + print(f"_____stt.py * response: {response}") + # # Send to Fal directly (audio is already in WAV format from base class) + # data_uri = fal_client.encode(audio, "audio/x-wav") + # response = await self._fal_client.run( + # "fal-ai/wizper", + # arguments={"audio_url": data_uri, **self._settings}, + # ) + + if response and "text" in response: + text = response["text"].strip() + if text: # Only yield non-empty text + logger.debug(f"Transcription: [{text}]") + yield TranscriptionFrame( + text, "", time_now_iso8601(), Language(self._settings["language"]) + ) + + except Exception as e: + logger.error(f"Riva Offline STT error: {e}") + yield ErrorFrame(f"Riva Offline STT error: {str(e)}") + + def __next__(self) -> bytes: + if not self._thread_running: + raise StopIteration + future = asyncio.run_coroutine_threadsafe(self._queue.get(), self.get_event_loop()) + return future.result() + + def __iter__(self): + return self + +class ParakeetSTTService(RivaSTTService): + class InputParams(BaseModel): + language: Optional[Language] = Language.EN_US + + def __init__( + self, + *, + api_key: str = None, + server: str = "grpc.nvcf.nvidia.com:443", + function_id: str = "1598d209-5e27-4d3c-8079-4751568b1081", + model_name: str = "parakeet-ctc-1.1b-asr", + sample_rate: Optional[int] = None, + params: InputParams = InputParams(), + **kwargs, + ): + super().__init__( + api_key=api_key, + server=server, + function_id=function_id, + model_name=model_name, + sample_rate=sample_rate, + params=params, + **kwargs, + ) + import warnings + + with warnings.catch_warnings(): + warnings.simplefilter("always") + warnings.warn( + "`ParakeetSTTService` is deprecated, use `RivaSTTService` instead.", + DeprecationWarning, + ) + diff --git a/src/pipecat/services/riva/tts.py b/src/pipecat/services/riva/tts.py index 0fd0c3ce0..f37064acb 100644 --- a/src/pipecat/services/riva/tts.py +++ b/src/pipecat/services/riva/tts.py @@ -27,10 +27,10 @@ except ModuleNotFoundError as e: logger.error("In order to use NVIDIA Riva TTS, you need to `pip install pipecat-ai[riva]`.") raise Exception(f"Missing module: {e}") -FASTPITCH_TIMEOUT_SECS = 5 +RIVA_TTS_TIMEOUT_SECS = 5 -class FastPitchTTSService(TTSService): +class RivaTTSService(TTSService): class InputParams(BaseModel): language: Optional[Language] = Language.EN_US quality: Optional[int] = 20 @@ -38,11 +38,12 @@ class FastPitchTTSService(TTSService): def __init__( self, *, - api_key: str, + api_key: str = None, server: str = "grpc.nvcf.nvidia.com:443", - voice_id: str = "English-US.Female-1", + voice_id: str = "Magpie-Multilingual.EN-US.Male.Male-1", sample_rate: Optional[int] = None, - function_id: str = "0149dedb-2be8-4195-b9a0-e57e0e14f972", + function_id: str = "877104f7-e885-42b9-8de8-f6e4c6303969", + model_name: str = "magpie-tts-multilingual", params: InputParams = InputParams(), **kwargs, ): @@ -52,7 +53,7 @@ class FastPitchTTSService(TTSService): self._language_code = params.language self._quality = params.quality - self.set_model_name("fastpitch-hifigan-tts") + self.set_model_name(model_name) self.set_voice(voice_id) metadata = [ @@ -100,7 +101,7 @@ class FastPitchTTSService(TTSService): await asyncio.to_thread(read_audio_responses, queue) # Wait for the thread to start. - resp = await asyncio.wait_for(queue.get(), FASTPITCH_TIMEOUT_SECS) + resp = await asyncio.wait_for(queue.get(), RIVA_TTS_TIMEOUT_SECS) while resp: await self.stop_ttfb_metrics() frame = TTSAudioRawFrame( @@ -109,9 +110,46 @@ class FastPitchTTSService(TTSService): num_channels=1, ) yield frame - resp = await asyncio.wait_for(queue.get(), FASTPITCH_TIMEOUT_SECS) + resp = await asyncio.wait_for(queue.get(), RIVA_TTS_TIMEOUT_SECS) except asyncio.TimeoutError: logger.error(f"{self} timeout waiting for audio response") await self.start_tts_usage_metrics(text) yield TTSStoppedFrame() + + +class FastPitchTTSService(RivaTTSService): + class InputParams(BaseModel): + language: Optional[Language] = Language.EN_US + quality: Optional[int] = 20 + + def __init__( + self, + *, + api_key: str = None, + server: str = "grpc.nvcf.nvidia.com:443", + voice_id: str = "English-US.Female-1", + sample_rate: Optional[int] = None, + function_id: str = "0149dedb-2be8-4195-b9a0-e57e0e14f972", + model_name: str = "fastpitch-hifigan-tts", + params: InputParams = InputParams(), + **kwargs, + ): + super().__init__( + api_key=api_key, + voice_id=voice_id, + sample_rate=sample_rate, + function_id=function_id, + model_name=model_name, + params=params, + **kwargs, + ) + import warnings + + with warnings.catch_warnings(): + warnings.simplefilter("always") + warnings.warn( + "`FastPitchTTSService` is deprecated, use `RivaTTSService` instead.", + DeprecationWarning, + ) + From daf9d47e58269a9f1338415843294462ed7eaf07 Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Fri, 2 May 2025 09:52:23 -0400 Subject: [PATCH 05/13] Update RivaSegmentedSTTService --- .../07r-interruptible-riva-nim.py | 10 +- src/pipecat/services/riva/stt.py | 306 ++++++++++++------ src/pipecat/services/riva/tts.py | 5 +- 3 files changed, 219 insertions(+), 102 deletions(-) diff --git a/examples/foundational/07r-interruptible-riva-nim.py b/examples/foundational/07r-interruptible-riva-nim.py index ca74dfd7a..070467844 100644 --- a/examples/foundational/07r-interruptible-riva-nim.py +++ b/examples/foundational/07r-interruptible-riva-nim.py @@ -16,8 +16,12 @@ from pipecat.pipeline.runner import PipelineRunner from pipecat.pipeline.task import PipelineParams, PipelineTask from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext from pipecat.services.nim.llm import NimLLMService -from pipecat.services.riva.stt import ParakeetSTTService, RivaOfflineSTTService -from pipecat.services.riva.tts import RivaTTSService, FastPitchTTSService +from pipecat.services.riva.stt import ( + ParakeetSTTService, + RivaSegmentedSTTService, + RivaSTTService, +) +from pipecat.services.riva.tts import FastPitchTTSService, RivaTTSService from pipecat.transports.base_transport import TransportParams from pipecat.transports.network.small_webrtc import SmallWebRTCTransport from pipecat.transports.network.webrtc_connection import SmallWebRTCConnection @@ -37,7 +41,7 @@ async def run_bot(webrtc_connection: SmallWebRTCConnection, _: argparse.Namespac ), ) - stt = RivaOfflineSTTService(api_key=os.getenv("NVIDIA_API_KEY")) + stt = RivaSegmentedSTTService(api_key=os.getenv("NVIDIA_API_KEY")) # stt = ParakeetSTTService(api_key=os.getenv("NVIDIA_API_KEY")) llm = NimLLMService(api_key=os.getenv("NVIDIA_API_KEY"), model="meta/llama-3.1-405b-instruct") diff --git a/src/pipecat/services/riva/stt.py b/src/pipecat/services/riva/stt.py index 3676befa3..d3b826060 100644 --- a/src/pipecat/services/riva/stt.py +++ b/src/pipecat/services/riva/stt.py @@ -5,7 +5,7 @@ # import asyncio -from typing import AsyncGenerator, Optional +from typing import AsyncGenerator, List, Optional from loguru import logger from pydantic import BaseModel @@ -19,8 +19,7 @@ from pipecat.frames.frames import ( StartFrame, TranscriptionFrame, ) -from pipecat.services.stt_service import SegmentedSTTService -from pipecat.services.stt_service import STTService +from pipecat.services.stt_service import SegmentedSTTService, STTService from pipecat.transcriptions.language import Language from pipecat.utils.time import time_now_iso8601 @@ -29,10 +28,62 @@ try: except ModuleNotFoundError as e: logger.error(f"Exception: {e}") - logger.error("In order to use NVIDIA Riva STT, you need to `pip install pipecat-ai[riva]`. Also set NVIDIA_API_KEY env var.") + logger.error("In order to use NVIDIA Riva STT, you need to `pip install pipecat-ai[riva]`.") raise Exception(f"Missing module: {e}") +def language_to_riva_language(language: Language) -> Optional[str]: + """Maps Language enum to Riva ASR language codes. + + Source: + https://docs.nvidia.com/deeplearning/riva/user-guide/docs/asr/asr-riva-build-table.html?highlight=fr%20fr + + Args: + language: Language enum value. + + Returns: + Optional[str]: Riva language code or None if not supported. + """ + language_map = { + # Arabic + Language.AR: "ar-AR", + # English + Language.EN: "en-US", # Default to US + Language.EN_US: "en-US", + Language.EN_GB: "en-GB", + # French + Language.FR: "fr-FR", + Language.FR_FR: "fr-FR", + # German + Language.DE: "de-DE", + Language.DE_DE: "de-DE", + # Hindi + Language.HI: "hi-IN", + Language.HI_IN: "hi-IN", + # Italian + Language.IT: "it-IT", + Language.IT_IT: "it-IT", + # Japanese + Language.JA: "ja-JP", + Language.JA_JP: "ja-JP", + # Korean + Language.KO: "ko-KR", + Language.KO_KR: "ko-KR", + # Portuguese + Language.PT: "pt-BR", # Default to Brazilian + Language.PT_BR: "pt-BR", + # Russian + Language.RU: "ru-RU", + Language.RU_RU: "ru-RU", + # Spanish + Language.ES: "es-ES", # Default to Spain + Language.ES_ES: "es-ES", + Language.ES_US: "es-US", # US Spanish + } + + return language_map.get(language) + + class RivaSTTService(STTService): class InputParams(BaseModel): language: Optional[Language] = Language.EN_US @@ -200,38 +251,38 @@ class RivaSTTService(STTService): def __iter__(self): return self -class RivaOfflineSTTService(SegmentedSTTService): - """Speech-to-text service using Fal's Wizper API. - This service uses Fal's Wizper API to perform speech-to-text transcription on audio - segments. It inherits from SegmentedSTTService to handle audio buffering and speech detection. +class RivaSegmentedSTTService(SegmentedSTTService): + """Speech-to-text service using NVIDIA Riva Canary ASR API. + + This service uses NVIDIA's Riva Canary ASR API to perform speech-to-text + transcription on audio segments. It inherits from SegmentedSTTService to handle + audio buffering and speech detection. Args: - api_key: NVIDIA_API_KEY. - sample_rate: Audio sample rate in Hz. If not provided, uses the pipeline's rate. - params: Configuration parameters for Riva. - **kwargs: Additional arguments passed to SegmentedSTTService. + api_key: NVIDIA API key for authentication + server: Riva server address (defaults to NVIDIA Cloud Function endpoint) + function_id: NVIDIA Cloud Function ID for the Canary ASR service + model_name: Name of the Canary ASR model to use + sample_rate: Audio sample rate in Hz. If not provided, uses the pipeline's rate + params: Additional configuration parameters for Riva + **kwargs: Additional arguments passed to SegmentedSTTService """ class InputParams(BaseModel): - """Configuration parameters for Fal's Wizper API. + """Configuration parameters for Riva Canary ASR API.""" - Attributes: - language: Language of the audio input. Defaults to English. - task: Task to perform ('transcribe' or 'translate'). Defaults to 'transcribe'. - chunk_level: Level of chunking ('segment'). Defaults to 'segment'. - version: Version of Wizper model to use. Defaults to '3'. - """ - - language: Optional[Language] = Language.EN - task: str = "transcribe" - chunk_level: str = "segment" - version: str = "3" + language: Optional[Language] = Language.EN_US + profanity_filter: bool = False + automatic_punctuation: bool = True + verbatim_transcripts: bool = False + boosted_lm_words: Optional[List[str]] = None + boosted_lm_score: float = 4.0 def __init__( self, *, - api_key: str = None, + api_key: str, server: str = "grpc.nvcf.nvidia.com:443", function_id: str = "ee8dc628-76de-4acc-8595-1836e7e857bd", model_name: str = "canary-1b-asr", @@ -240,13 +291,27 @@ class RivaOfflineSTTService(SegmentedSTTService): **kwargs, ): super().__init__(sample_rate=sample_rate, **kwargs) + + # Set model name + self.set_model_name(model_name) + + # Initialize Riva settings self._api_key = api_key - self._profanity_filter = False - self._automatic_punctuation = False - self._no_verbatim_transcripts = False - self._language_code = params.language - self._boosted_lm_words = None - self._boosted_lm_score = 4.0 + self._server = server + self._function_id = function_id + + # Store the language as a Language enum and as a string + self._language_enum = params.language or Language.EN_US + self._language = self.language_to_service_language(self._language_enum) or "en-US" + + # Configure transcription parameters + self._profanity_filter = params.profanity_filter + self._automatic_punctuation = params.automatic_punctuation + self._verbatim_transcripts = params.verbatim_transcripts + self._boosted_lm_words = params.boosted_lm_words + self._boosted_lm_score = params.boosted_lm_score + + # Voice activity detection thresholds (use Riva defaults) self._start_history = -1 self._start_threshold = -1.0 self._stop_history = -1 @@ -255,50 +320,50 @@ class RivaOfflineSTTService(SegmentedSTTService): self._stop_threshold_eou = -1.0 self._custom_configuration = "" - self.set_model_name(model_name) - - metadata = [ - ["function-id", function_id], - ["authorization", f"Bearer {api_key}"], - ] - auth = riva.client.Auth(None, True, server, metadata) - - self._asr_service = riva.client.ASRService(auth) - - self._queue = asyncio.Queue() + # Create Riva client self._config = None - self._thread_task = None - self._response_task = None + self._asr_service = None + self._settings = {"language": self._language_enum} - def can_generate_metrics(self) -> bool: - return False + def language_to_service_language(self, language: Language) -> Optional[str]: + """Convert pipecat Language enum to Riva's language code.""" + return language_to_riva_language(language) - async def start(self, frame: StartFrame): - await super().start(frame) - - if self._config: + def _initialize_client(self): + """Initialize the Riva ASR client with authentication metadata.""" + if self._asr_service is not None: return - # config = riva.client.StreamingRecognitionConfig( - config=riva.client.RecognitionConfig( - # encoding=riva.client.AudioEncoding.LINEAR_PCM, - language_code=self._language_code, - # model="", + # Set up authentication metadata for NVIDIA Cloud Functions + metadata = [ + ["function-id", self._function_id], + ["authorization", f"Bearer {self._api_key}"], + ] + + # Create authenticated client + auth = riva.client.Auth(None, True, self._server, metadata) + self._asr_service = riva.client.ASRService(auth) + + logger.info(f"Initialized Riva Canary ASR service with model: {self.model_name}") + + def _create_recognition_config(self): + """Create the Riva ASR recognition configuration.""" + # Create base configuration + config = riva.client.RecognitionConfig( + language_code=self._language, # Now using the string, not a tuple max_alternatives=1, profanity_filter=self._profanity_filter, enable_automatic_punctuation=self._automatic_punctuation, - verbatim_transcripts=not self._no_verbatim_transcripts, - # sample_rate_hertz=self.sample_rate, - # audio_channel_count=1, - # enable_word_time_offsets=args.word_time_offsets or args.speaker_diarization,?? - # ), - # interim_results=True, + verbatim_transcripts=self._verbatim_transcripts, ) - riva.client.add_word_boosting_to_config( - config, self._boosted_lm_words, self._boosted_lm_score - ) + # Add word boosting if specified + if self._boosted_lm_words: + riva.client.add_word_boosting_to_config( + config, self._boosted_lm_words, self._boosted_lm_score + ) + # Add voice activity detection parameters riva.client.add_endpoint_parameters_to_config( config, self._start_history, @@ -308,56 +373,102 @@ class RivaOfflineSTTService(SegmentedSTTService): self._stop_threshold, self._stop_threshold_eou, ) - riva.client.add_custom_configuration_to_config(config, self._custom_configuration) - self._config = config + # Add any custom configuration + if self._custom_configuration: + riva.client.add_custom_configuration_to_config(config, self._custom_configuration) + return config + def can_generate_metrics(self) -> bool: + """Indicates whether this service can generate processing metrics.""" + return True + + async def start(self, frame: StartFrame): + """Initialize the service when the pipeline starts.""" + await super().start(frame) + self._initialize_client() + self._config = self._create_recognition_config() + + async def set_language(self, language: Language): + """Set the language for the STT service.""" + logger.info(f"Switching STT language to: [{language}]") + self._language_enum = language + self._language = self.language_to_service_language(language) or "en-US" + self._settings["language"] = language + + # Update configuration with new language + if self._config: + self._config.language_code = self._language async def run_stt(self, audio: bytes) -> AsyncGenerator[Frame, None]: - """Transcribe an audio segment + """Transcribe an audio segment using Riva Canary ASR. Args: audio: Raw audio bytes in WAV format (already converted by base class). Yields: Frame: TranscriptionFrame containing the transcribed text. - - Note: - The audio is already in WAV format from the SegmentedSTTService. - Only non-empty transcriptions are yielded. """ try: - response = self._asr_service.offline_recognize(audio, self._config) - # response = riva.client.print_offline(response=self._asr_service.offline_recognize(audio, self._config)) - print(f"_____stt.py * response: {response}") - # # Send to Fal directly (audio is already in WAV format from base class) - # data_uri = fal_client.encode(audio, "audio/x-wav") - # response = await self._fal_client.run( - # "fal-ai/wizper", - # arguments={"audio_url": data_uri, **self._settings}, - # ) + await self.start_processing_metrics() + await self.start_ttfb_metrics() - if response and "text" in response: - text = response["text"].strip() - if text: # Only yield non-empty text - logger.debug(f"Transcription: [{text}]") - yield TranscriptionFrame( - text, "", time_now_iso8601(), Language(self._settings["language"]) - ) + # Make sure the client is initialized + if self._asr_service is None: + self._initialize_client() + + # Make sure the config is created + if self._config is None: + self._config = self._create_recognition_config() + + # Type assertion to satisfy the IDE + assert self._asr_service is not None, "ASR service not initialized" + assert self._config is not None, "Recognition config not created" + + # Process audio with Riva ASR - explicitly request non-future response + raw_response = self._asr_service.offline_recognize(audio, self._config, future=False) + + await self.stop_ttfb_metrics() + await self.stop_processing_metrics() + + # Process the response - handle different possible return types + try: + # If it's a future-like object, get the result + if hasattr(raw_response, "result"): + response = raw_response.result() + else: + response = raw_response + + # Process transcription results + transcription_found = False + + # Now we can safely check results + # Type hint for the IDE + results = getattr(response, "results", []) + + for result in results: + alternatives = getattr(result, "alternatives", []) + if alternatives: + text = alternatives[0].transcript.strip() + if text: + logger.debug(f"Transcription: [{text}]") + yield TranscriptionFrame( + text, "", time_now_iso8601(), self._language_enum + ) + transcription_found = True + + if not transcription_found: + logger.debug("No transcription results found in Riva response") + + except AttributeError as ae: + logger.error(f"Unexpected response structure from Riva: {ae}") + yield ErrorFrame(f"Unexpected Riva response format: {str(ae)}") except Exception as e: - logger.error(f"Riva Offline STT error: {e}") - yield ErrorFrame(f"Riva Offline STT error: {str(e)}") + logger.exception(f"Riva Canary ASR error: {e}") + yield ErrorFrame(f"Riva Canary ASR error: {str(e)}") - def __next__(self) -> bytes: - if not self._thread_running: - raise StopIteration - future = asyncio.run_coroutine_threadsafe(self._queue.get(), self.get_event_loop()) - return future.result() - - def __iter__(self): - return self class ParakeetSTTService(RivaSTTService): class InputParams(BaseModel): @@ -391,4 +502,3 @@ class ParakeetSTTService(RivaSTTService): "`ParakeetSTTService` is deprecated, use `RivaSTTService` instead.", DeprecationWarning, ) - diff --git a/src/pipecat/services/riva/tts.py b/src/pipecat/services/riva/tts.py index f37064acb..bfa732678 100644 --- a/src/pipecat/services/riva/tts.py +++ b/src/pipecat/services/riva/tts.py @@ -5,8 +5,12 @@ # import asyncio +import os from typing import AsyncGenerator, Optional +# Suppress gRPC fork warnings +os.environ["GRPC_ENABLE_FORK_SUPPORT"] = "false" + from loguru import logger from pydantic import BaseModel @@ -152,4 +156,3 @@ class FastPitchTTSService(RivaTTSService): "`FastPitchTTSService` is deprecated, use `RivaTTSService` instead.", DeprecationWarning, ) - From 725ab5ec219888e42f5d090a2fb976f3ccce1099 Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Fri, 2 May 2025 09:57:39 -0400 Subject: [PATCH 06/13] Small fixes: No default api_key of None, ParakeetSTTService uses RivaSTTService.InputParams --- src/pipecat/services/riva/stt.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/pipecat/services/riva/stt.py b/src/pipecat/services/riva/stt.py index d3b826060..0ee7b3e5e 100644 --- a/src/pipecat/services/riva/stt.py +++ b/src/pipecat/services/riva/stt.py @@ -91,7 +91,7 @@ class RivaSTTService(STTService): def __init__( self, *, - api_key: str = None, + api_key: str, server: str = "grpc.nvcf.nvidia.com:443", function_id: str = "1598d209-5e27-4d3c-8079-4751568b1081", model_name: str = "parakeet-ctc-1.1b-asr", @@ -102,7 +102,7 @@ class RivaSTTService(STTService): super().__init__(sample_rate=sample_rate, **kwargs) self._api_key = api_key self._profanity_filter = False - self._automatic_punctuation = False + self._automatic_punctuation = True self._no_verbatim_transcripts = False self._language_code = params.language self._boosted_lm_words = None @@ -471,18 +471,17 @@ class RivaSegmentedSTTService(SegmentedSTTService): class ParakeetSTTService(RivaSTTService): - class InputParams(BaseModel): - language: Optional[Language] = Language.EN_US + """Deprecated: Use RivaSTTService instead.""" def __init__( self, *, - api_key: str = None, + api_key: str, server: str = "grpc.nvcf.nvidia.com:443", function_id: str = "1598d209-5e27-4d3c-8079-4751568b1081", model_name: str = "parakeet-ctc-1.1b-asr", sample_rate: Optional[int] = None, - params: InputParams = InputParams(), + params: RivaSTTService.InputParams, # Use parent class's type **kwargs, ): super().__init__( From c14406a3b96a9d14b3cc30648676b657aaadec32 Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Fri, 2 May 2025 09:59:01 -0400 Subject: [PATCH 07/13] Demos use the latest services --- examples/foundational/07r-interruptible-riva-nim.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/foundational/07r-interruptible-riva-nim.py b/examples/foundational/07r-interruptible-riva-nim.py index 070467844..7b82871ab 100644 --- a/examples/foundational/07r-interruptible-riva-nim.py +++ b/examples/foundational/07r-interruptible-riva-nim.py @@ -41,13 +41,14 @@ async def run_bot(webrtc_connection: SmallWebRTCConnection, _: argparse.Namespac ), ) - stt = RivaSegmentedSTTService(api_key=os.getenv("NVIDIA_API_KEY")) + stt = RivaSTTService(api_key=os.getenv("NVIDIA_API_KEY")) + # stt = RivaSegmentedSTTService(api_key=os.getenv("NVIDIA_API_KEY")) # stt = ParakeetSTTService(api_key=os.getenv("NVIDIA_API_KEY")) llm = NimLLMService(api_key=os.getenv("NVIDIA_API_KEY"), model="meta/llama-3.1-405b-instruct") - tts = FastPitchTTSService(api_key=os.getenv("NVIDIA_API_KEY")) - # tts = RivaTTSService(api_key=os.getenv("NVIDIA_API_KEY")) + # tts = FastPitchTTSService(api_key=os.getenv("NVIDIA_API_KEY")) + tts = RivaTTSService(api_key=os.getenv("NVIDIA_API_KEY")) messages = [ { From da60e7069be5db30776365ea8c929d57f92a5764 Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Fri, 2 May 2025 10:08:49 -0400 Subject: [PATCH 08/13] Update pyproject.toml to use nvidia-riva-client 2.19.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6d1cf840c..30d72f161 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,7 +78,7 @@ perplexity = [] playht = [ "pyht~=0.1.12", "websockets~=13.1" ] qwen = [] rime = [ "websockets~=13.1" ] -riva = [ "nvidia-riva-client~=2.19.0" ] +riva = [ "nvidia-riva-client~=2.19.1" ] sentry = [ "sentry-sdk~=2.23.1" ] local-smart-turn = [ "coremltools>=8.0", "transformers", "torch==2.5.0", "torchaudio==2.5.0" ] remote-smart-turn = [] From e7d889a1435eef3cf6bd187b9237f448bf5bba79 Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Fri, 2 May 2025 10:49:35 -0400 Subject: [PATCH 09/13] Update RivaSTTService to use by default --- src/pipecat/services/riva/stt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pipecat/services/riva/stt.py b/src/pipecat/services/riva/stt.py index 0ee7b3e5e..0d15ab0af 100644 --- a/src/pipecat/services/riva/stt.py +++ b/src/pipecat/services/riva/stt.py @@ -94,7 +94,7 @@ class RivaSTTService(STTService): api_key: str, server: str = "grpc.nvcf.nvidia.com:443", function_id: str = "1598d209-5e27-4d3c-8079-4751568b1081", - model_name: str = "parakeet-ctc-1.1b-asr", + model_name: str = "parakeet-1.1b-rnnt-multilingual-asr", sample_rate: Optional[int] = None, params: InputParams = InputParams(), **kwargs, From 6c428c303bda85b75c2bb53977f3323033c73697 Mon Sep 17 00:00:00 2001 From: vipyne Date: Fri, 2 May 2025 10:27:50 -0500 Subject: [PATCH 10/13] update magpie voice --- src/pipecat/services/riva/tts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pipecat/services/riva/tts.py b/src/pipecat/services/riva/tts.py index bfa732678..5ab0a0eaa 100644 --- a/src/pipecat/services/riva/tts.py +++ b/src/pipecat/services/riva/tts.py @@ -44,7 +44,7 @@ class RivaTTSService(TTSService): *, api_key: str = None, server: str = "grpc.nvcf.nvidia.com:443", - voice_id: str = "Magpie-Multilingual.EN-US.Male.Male-1", + voice_id: str = "Magpie-Multilingual.EN-US.Ray", sample_rate: Optional[int] = None, function_id: str = "877104f7-e885-42b9-8de8-f6e4c6303969", model_name: str = "magpie-tts-multilingual", From b2754117c859417e513c3227f8da3ee46981d44b Mon Sep 17 00:00:00 2001 From: vipyne Date: Fri, 2 May 2025 11:34:40 -0500 Subject: [PATCH 11/13] Riva: refactor function_id and model_name --- .../07r-interruptible-riva-nim.py | 8 +-- src/pipecat/services/riva/stt.py | 62 ++++++++++++------- src/pipecat/services/riva/tts.py | 29 ++++++--- 3 files changed, 64 insertions(+), 35 deletions(-) diff --git a/examples/foundational/07r-interruptible-riva-nim.py b/examples/foundational/07r-interruptible-riva-nim.py index 7b82871ab..efe9b5479 100644 --- a/examples/foundational/07r-interruptible-riva-nim.py +++ b/examples/foundational/07r-interruptible-riva-nim.py @@ -41,14 +41,14 @@ async def run_bot(webrtc_connection: SmallWebRTCConnection, _: argparse.Namespac ), ) - stt = RivaSTTService(api_key=os.getenv("NVIDIA_API_KEY")) - # stt = RivaSegmentedSTTService(api_key=os.getenv("NVIDIA_API_KEY")) + # stt = RivaSTTService(api_key=os.getenv("NVIDIA_API_KEY")) # parakeet og + stt = RivaSegmentedSTTService(api_key=os.getenv("NVIDIA_API_KEY")) # canary # stt = ParakeetSTTService(api_key=os.getenv("NVIDIA_API_KEY")) llm = NimLLMService(api_key=os.getenv("NVIDIA_API_KEY"), model="meta/llama-3.1-405b-instruct") - # tts = FastPitchTTSService(api_key=os.getenv("NVIDIA_API_KEY")) - tts = RivaTTSService(api_key=os.getenv("NVIDIA_API_KEY")) + tts = FastPitchTTSService(api_key=os.getenv("NVIDIA_API_KEY")) + # tts = RivaTTSService(api_key=os.getenv("NVIDIA_API_KEY")) messages = [ { diff --git a/src/pipecat/services/riva/stt.py b/src/pipecat/services/riva/stt.py index 0d15ab0af..2b263f03e 100644 --- a/src/pipecat/services/riva/stt.py +++ b/src/pipecat/services/riva/stt.py @@ -5,7 +5,7 @@ # import asyncio -from typing import AsyncGenerator, List, Optional +from typing import AsyncGenerator, List, Mapping, Optional from loguru import logger from pydantic import BaseModel @@ -93,8 +93,10 @@ class RivaSTTService(STTService): *, api_key: str, server: str = "grpc.nvcf.nvidia.com:443", - function_id: str = "1598d209-5e27-4d3c-8079-4751568b1081", - model_name: str = "parakeet-1.1b-rnnt-multilingual-asr", + model_function_map: Mapping[str, str] = { + "function_id": "1598d209-5e27-4d3c-8079-4751568b1081", + "model_name": "parakeet-ctc-1.1b-asr", + }, sample_rate: Optional[int] = None, params: InputParams = InputParams(), **kwargs, @@ -114,11 +116,12 @@ class RivaSTTService(STTService): self._stop_history_eou = -1 self._stop_threshold_eou = -1.0 self._custom_configuration = "" + self._function_id = model_function_map.get("function_id") - self.set_model_name(model_name) + self.set_model_name(model_function_map.get("model_name")) metadata = [ - ["function-id", function_id], + ["function-id", self._function_id], ["authorization", f"Bearer {api_key}"], ] auth = riva.client.Auth(None, True, server, metadata) @@ -133,6 +136,13 @@ class RivaSTTService(STTService): def can_generate_metrics(self) -> bool: return False + async def set_model(self, model: str): + logger.warning(f"Cannot set model after initialization. Set model and function id like so:") + example = {"function_id": "", "model_name": ""} + logger.warning( + f"{self.__class__.__name__}(api_key=, model_function_map={example})" + ) + async def start(self, frame: StartFrame): await super().start(frame) @@ -253,25 +263,22 @@ class RivaSTTService(STTService): class RivaSegmentedSTTService(SegmentedSTTService): - """Speech-to-text service using NVIDIA Riva Canary ASR API. + """Speech-to-text service using NVIDIA Riva's offline/batch models. - This service uses NVIDIA's Riva Canary ASR API to perform speech-to-text + By default, his service uses NVIDIA's Riva Canary ASR API to perform speech-to-text transcription on audio segments. It inherits from SegmentedSTTService to handle audio buffering and speech detection. Args: api_key: NVIDIA API key for authentication server: Riva server address (defaults to NVIDIA Cloud Function endpoint) - function_id: NVIDIA Cloud Function ID for the Canary ASR service - model_name: Name of the Canary ASR model to use + model_function_map: Mapping of model name and its corresponding NVIDIA Cloud Function ID sample_rate: Audio sample rate in Hz. If not provided, uses the pipeline's rate params: Additional configuration parameters for Riva **kwargs: Additional arguments passed to SegmentedSTTService """ class InputParams(BaseModel): - """Configuration parameters for Riva Canary ASR API.""" - language: Optional[Language] = Language.EN_US profanity_filter: bool = False automatic_punctuation: bool = True @@ -284,8 +291,10 @@ class RivaSegmentedSTTService(SegmentedSTTService): *, api_key: str, server: str = "grpc.nvcf.nvidia.com:443", - function_id: str = "ee8dc628-76de-4acc-8595-1836e7e857bd", - model_name: str = "canary-1b-asr", + model_function_map: Mapping[str, str] = { + "function_id": "ee8dc628-76de-4acc-8595-1836e7e857bd", + "model_name": "canary-1b-asr", + }, sample_rate: Optional[int] = None, params: InputParams = InputParams(), **kwargs, @@ -293,12 +302,13 @@ class RivaSegmentedSTTService(SegmentedSTTService): super().__init__(sample_rate=sample_rate, **kwargs) # Set model name - self.set_model_name(model_name) + self.set_model_name(model_function_map.get("model_name")) # Initialize Riva settings self._api_key = api_key self._server = server - self._function_id = function_id + self._function_id = model_function_map.get("function_id") + self._model_name = model_function_map.get("model_name") # Store the language as a Language enum and as a string self._language_enum = params.language or Language.EN_US @@ -344,7 +354,7 @@ class RivaSegmentedSTTService(SegmentedSTTService): auth = riva.client.Auth(None, True, self._server, metadata) self._asr_service = riva.client.ASRService(auth) - logger.info(f"Initialized Riva Canary ASR service with model: {self.model_name}") + logger.info(f"Initialized RivaSegmentedSTTService with model: {self.model_name}") def _create_recognition_config(self): """Create the Riva ASR recognition configuration.""" @@ -384,6 +394,13 @@ class RivaSegmentedSTTService(SegmentedSTTService): """Indicates whether this service can generate processing metrics.""" return True + async def set_model(self, model: str): + logger.warning(f"Cannot set model after initialization. Set model and function id like so:") + example = {"function_id": "", "model_name": ""} + logger.warning( + f"{self.__class__.__name__}(api_key=, model_function_map={example})" + ) + async def start(self, frame: StartFrame): """Initialize the service when the pipeline starts.""" await super().start(frame) @@ -402,7 +419,7 @@ class RivaSegmentedSTTService(SegmentedSTTService): self._config.language_code = self._language async def run_stt(self, audio: bytes) -> AsyncGenerator[Frame, None]: - """Transcribe an audio segment using Riva Canary ASR. + """Transcribe an audio segment. Args: audio: Raw audio bytes in WAV format (already converted by base class). @@ -478,17 +495,18 @@ class ParakeetSTTService(RivaSTTService): *, api_key: str, server: str = "grpc.nvcf.nvidia.com:443", - function_id: str = "1598d209-5e27-4d3c-8079-4751568b1081", - model_name: str = "parakeet-ctc-1.1b-asr", + model_function_map: Mapping[str, str] = { + "function_id": "1598d209-5e27-4d3c-8079-4751568b1081", + "model_name": "parakeet-ctc-1.1b-asr", + }, sample_rate: Optional[int] = None, - params: RivaSTTService.InputParams, # Use parent class's type + params: RivaSTTService.InputParams = RivaSTTService.InputParams(), # Use parent class's type **kwargs, ): super().__init__( api_key=api_key, server=server, - function_id=function_id, - model_name=model_name, + model_function_map=model_function_map, sample_rate=sample_rate, params=params, **kwargs, diff --git a/src/pipecat/services/riva/tts.py b/src/pipecat/services/riva/tts.py index 5ab0a0eaa..4fc8b8489 100644 --- a/src/pipecat/services/riva/tts.py +++ b/src/pipecat/services/riva/tts.py @@ -6,7 +6,7 @@ import asyncio import os -from typing import AsyncGenerator, Optional +from typing import AsyncGenerator, Mapping, Optional # Suppress gRPC fork warnings os.environ["GRPC_ENABLE_FORK_SUPPORT"] = "false" @@ -46,8 +46,10 @@ class RivaTTSService(TTSService): server: str = "grpc.nvcf.nvidia.com:443", voice_id: str = "Magpie-Multilingual.EN-US.Ray", sample_rate: Optional[int] = None, - function_id: str = "877104f7-e885-42b9-8de8-f6e4c6303969", - model_name: str = "magpie-tts-multilingual", + model_function_map: Mapping[str, str] = { + "function_id": "877104f7-e885-42b9-8de8-f6e4c6303969", + "model_name": "magpie-tts-multilingual", + }, params: InputParams = InputParams(), **kwargs, ): @@ -56,12 +58,13 @@ class RivaTTSService(TTSService): self._voice_id = voice_id self._language_code = params.language self._quality = params.quality + self._function_id = model_function_map.get("function_id") - self.set_model_name(model_name) + self.set_model_name(model_function_map.get("model_name")) self.set_voice(voice_id) metadata = [ - ["function-id", function_id], + ["function-id", self._function_id], ["authorization", f"Bearer {api_key}"], ] auth = riva.client.Auth(None, True, server, metadata) @@ -73,6 +76,13 @@ class RivaTTSService(TTSService): riva.client.proto.riva_tts_pb2.RivaSynthesisConfigRequest() ) + async def set_model(self, model: str): + logger.warning(f"Cannot set model after initialization. Set model and function id like so:") + example = {"function_id": "", "model_name": ""} + logger.warning( + f"{self.__class__.__name__}(api_key=, model_function_map={example})" + ) + async def run_tts(self, text: str) -> AsyncGenerator[Frame, None]: def read_audio_responses(queue: asyncio.Queue): def add_response(r): @@ -134,8 +144,10 @@ class FastPitchTTSService(RivaTTSService): server: str = "grpc.nvcf.nvidia.com:443", voice_id: str = "English-US.Female-1", sample_rate: Optional[int] = None, - function_id: str = "0149dedb-2be8-4195-b9a0-e57e0e14f972", - model_name: str = "fastpitch-hifigan-tts", + model_function_map: Mapping[str, str] = { + "function_id": "0149dedb-2be8-4195-b9a0-e57e0e14f972", + "model_name": "fastpitch-hifigan-tts", + }, params: InputParams = InputParams(), **kwargs, ): @@ -143,8 +155,7 @@ class FastPitchTTSService(RivaTTSService): api_key=api_key, voice_id=voice_id, sample_rate=sample_rate, - function_id=function_id, - model_name=model_name, + model_function_map=model_function_map, params=params, **kwargs, ) From b2ecd83706de83f6e65236cc0d4844d46ab85b89 Mon Sep 17 00:00:00 2001 From: vipyne Date: Fri, 2 May 2025 14:04:32 -0500 Subject: [PATCH 12/13] update CHANGELOG with Riva details --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5954e6baf..1f4811184 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -119,6 +119,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 case there's no need to push audio to the rest of the pipeline, but this is not a very common case. +- Added `RivaSegmentedSTTService`, which allows Riva offline/batch models, such + as to be "canary-1b-asr" used in Pipecat. + ### Deprecated - Function calls with parameters @@ -134,6 +137,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `TransportParams.vad_audio_passthrough` parameter is now deprecated, use `TransportParams.audio_in_passthrough` instead. +- `ParakeetSTTService` is now deprecated, use `RivaSTTService` instead, which uses + the model "parakeet-ctc-1.1b-asr" by default. + +- `FastPitchTTSService` is now deprecated, use `RivaTTSService` instead, which uses + the model "magpie-tts-multilingual" by default. + ### Fixed - Fixed an issue where `OpenAIRealtimeBetaLLMService` would add two assistant From 667bd32e6a5471a47eae30c139007fd271f15675 Mon Sep 17 00:00:00 2001 From: vipyne Date: Fri, 2 May 2025 15:32:33 -0500 Subject: [PATCH 13/13] Riva: remove deprecated lines in example --- examples/foundational/07r-interruptible-riva-nim.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/examples/foundational/07r-interruptible-riva-nim.py b/examples/foundational/07r-interruptible-riva-nim.py index efe9b5479..ddb80181c 100644 --- a/examples/foundational/07r-interruptible-riva-nim.py +++ b/examples/foundational/07r-interruptible-riva-nim.py @@ -41,14 +41,11 @@ async def run_bot(webrtc_connection: SmallWebRTCConnection, _: argparse.Namespac ), ) - # stt = RivaSTTService(api_key=os.getenv("NVIDIA_API_KEY")) # parakeet og - stt = RivaSegmentedSTTService(api_key=os.getenv("NVIDIA_API_KEY")) # canary - # stt = ParakeetSTTService(api_key=os.getenv("NVIDIA_API_KEY")) + stt = RivaSTTService(api_key=os.getenv("NVIDIA_API_KEY")) llm = NimLLMService(api_key=os.getenv("NVIDIA_API_KEY"), model="meta/llama-3.1-405b-instruct") - tts = FastPitchTTSService(api_key=os.getenv("NVIDIA_API_KEY")) - # tts = RivaTTSService(api_key=os.getenv("NVIDIA_API_KEY")) + tts = RivaTTSService(api_key=os.getenv("NVIDIA_API_KEY")) messages = [ {