transition Hathora service to use the unified API and apply PR feedback

add Hathora to root files

Hathora run linter

added hathora changelog
This commit is contained in:
Mike Seese
2025-12-17 19:16:58 -08:00
parent 1510fb4fc0
commit e5632a9339
8 changed files with 152 additions and 205 deletions

View File

@@ -73,9 +73,9 @@ Catch new features, interviews, and how-tos on our [Pipecat TV](https://www.yout
| Category | Services | | Category | Services |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Speech-to-Text | [AssemblyAI](https://docs.pipecat.ai/server/services/stt/assemblyai), [AWS](https://docs.pipecat.ai/server/services/stt/aws), [Azure](https://docs.pipecat.ai/server/services/stt/azure), [Cartesia](https://docs.pipecat.ai/server/services/stt/cartesia), [Deepgram](https://docs.pipecat.ai/server/services/stt/deepgram), [ElevenLabs](https://docs.pipecat.ai/server/services/stt/elevenlabs), [Fal Wizper](https://docs.pipecat.ai/server/services/stt/fal), [Gladia](https://docs.pipecat.ai/server/services/stt/gladia), [Google](https://docs.pipecat.ai/server/services/stt/google), [Gradium](https://docs.pipecat.ai/server/services/stt/gradium), [Groq (Whisper)](https://docs.pipecat.ai/server/services/stt/groq), [NVIDIA Riva](https://docs.pipecat.ai/server/services/stt/riva), [OpenAI (Whisper)](https://docs.pipecat.ai/server/services/stt/openai), [SambaNova (Whisper)](https://docs.pipecat.ai/server/services/stt/sambanova), [Sarvam](https://docs.pipecat.ai/server/services/stt/sarvam), [Soniox](https://docs.pipecat.ai/server/services/stt/soniox), [Speechmatics](https://docs.pipecat.ai/server/services/stt/speechmatics), [Whisper](https://docs.pipecat.ai/server/services/stt/whisper) | | Speech-to-Text | [AssemblyAI](https://docs.pipecat.ai/server/services/stt/assemblyai), [AWS](https://docs.pipecat.ai/server/services/stt/aws), [Azure](https://docs.pipecat.ai/server/services/stt/azure), [Cartesia](https://docs.pipecat.ai/server/services/stt/cartesia), [Deepgram](https://docs.pipecat.ai/server/services/stt/deepgram), [ElevenLabs](https://docs.pipecat.ai/server/services/stt/elevenlabs), [Fal Wizper](https://docs.pipecat.ai/server/services/stt/fal), [Gladia](https://docs.pipecat.ai/server/services/stt/gladia), [Google](https://docs.pipecat.ai/server/services/stt/google), [Gradium](https://docs.pipecat.ai/server/services/stt/gradium), [Groq (Whisper)](https://docs.pipecat.ai/server/services/stt/groq), [Hathora](https://docs.pipecat.ai/server/services/stt/hathora), [NVIDIA Riva](https://docs.pipecat.ai/server/services/stt/riva), [OpenAI (Whisper)](https://docs.pipecat.ai/server/services/stt/openai), [SambaNova (Whisper)](https://docs.pipecat.ai/server/services/stt/sambanova), [Sarvam](https://docs.pipecat.ai/server/services/stt/sarvam), [Soniox](https://docs.pipecat.ai/server/services/stt/soniox), [Speechmatics](https://docs.pipecat.ai/server/services/stt/speechmatics), [Whisper](https://docs.pipecat.ai/server/services/stt/whisper) |
| LLMs | [Anthropic](https://docs.pipecat.ai/server/services/llm/anthropic), [AWS](https://docs.pipecat.ai/server/services/llm/aws), [Azure](https://docs.pipecat.ai/server/services/llm/azure), [Cerebras](https://docs.pipecat.ai/server/services/llm/cerebras), [DeepSeek](https://docs.pipecat.ai/server/services/llm/deepseek), [Fireworks AI](https://docs.pipecat.ai/server/services/llm/fireworks), [Gemini](https://docs.pipecat.ai/server/services/llm/gemini), [Grok](https://docs.pipecat.ai/server/services/llm/grok), [Groq](https://docs.pipecat.ai/server/services/llm/groq), [Mistral](https://docs.pipecat.ai/server/services/llm/mistral), [NVIDIA NIM](https://docs.pipecat.ai/server/services/llm/nim), [Ollama](https://docs.pipecat.ai/server/services/llm/ollama), [OpenAI](https://docs.pipecat.ai/server/services/llm/openai), [OpenRouter](https://docs.pipecat.ai/server/services/llm/openrouter), [Perplexity](https://docs.pipecat.ai/server/services/llm/perplexity), [Qwen](https://docs.pipecat.ai/server/services/llm/qwen), [SambaNova](https://docs.pipecat.ai/server/services/llm/sambanova) [Together AI](https://docs.pipecat.ai/server/services/llm/together) | | LLMs | [Anthropic](https://docs.pipecat.ai/server/services/llm/anthropic), [AWS](https://docs.pipecat.ai/server/services/llm/aws), [Azure](https://docs.pipecat.ai/server/services/llm/azure), [Cerebras](https://docs.pipecat.ai/server/services/llm/cerebras), [DeepSeek](https://docs.pipecat.ai/server/services/llm/deepseek), [Fireworks AI](https://docs.pipecat.ai/server/services/llm/fireworks), [Gemini](https://docs.pipecat.ai/server/services/llm/gemini), [Grok](https://docs.pipecat.ai/server/services/llm/grok), [Groq](https://docs.pipecat.ai/server/services/llm/groq), [Mistral](https://docs.pipecat.ai/server/services/llm/mistral), [NVIDIA NIM](https://docs.pipecat.ai/server/services/llm/nim), [Ollama](https://docs.pipecat.ai/server/services/llm/ollama), [OpenAI](https://docs.pipecat.ai/server/services/llm/openai), [OpenRouter](https://docs.pipecat.ai/server/services/llm/openrouter), [Perplexity](https://docs.pipecat.ai/server/services/llm/perplexity), [Qwen](https://docs.pipecat.ai/server/services/llm/qwen), [SambaNova](https://docs.pipecat.ai/server/services/llm/sambanova) [Together AI](https://docs.pipecat.ai/server/services/llm/together) |
| Text-to-Speech | [Async](https://docs.pipecat.ai/server/services/tts/asyncai), [AWS](https://docs.pipecat.ai/server/services/tts/aws), [Azure](https://docs.pipecat.ai/server/services/tts/azure), [Camb AI](https://docs.pipecat.ai/server/services/tts/camb), [Cartesia](https://docs.pipecat.ai/server/services/tts/cartesia), [Deepgram](https://docs.pipecat.ai/server/services/tts/deepgram), [ElevenLabs](https://docs.pipecat.ai/server/services/tts/elevenlabs), [Fish](https://docs.pipecat.ai/server/services/tts/fish), [Google](https://docs.pipecat.ai/server/services/tts/google), [Gradium](https://docs.pipecat.ai/server/services/tts/gradium), [Groq](https://docs.pipecat.ai/server/services/tts/groq), [Hume](https://docs.pipecat.ai/server/services/tts/hume), [Inworld](https://docs.pipecat.ai/server/services/tts/inworld), [LMNT](https://docs.pipecat.ai/server/services/tts/lmnt), [MiniMax](https://docs.pipecat.ai/server/services/tts/minimax), [Neuphonic](https://docs.pipecat.ai/server/services/tts/neuphonic), [NVIDIA Riva](https://docs.pipecat.ai/server/services/tts/riva), [OpenAI](https://docs.pipecat.ai/server/services/tts/openai), [Piper](https://docs.pipecat.ai/server/services/tts/piper), [PlayHT](https://docs.pipecat.ai/server/services/tts/playht), [Rime](https://docs.pipecat.ai/server/services/tts/rime), [Sarvam](https://docs.pipecat.ai/server/services/tts/sarvam), [Speechmatics](https://docs.pipecat.ai/server/services/tts/speechmatics), [XTTS](https://docs.pipecat.ai/server/services/tts/xtts) | | Text-to-Speech | [Async](https://docs.pipecat.ai/server/services/tts/asyncai), [AWS](https://docs.pipecat.ai/server/services/tts/aws), [Azure](https://docs.pipecat.ai/server/services/tts/azure), [Camb AI](https://docs.pipecat.ai/server/services/tts/camb), [Cartesia](https://docs.pipecat.ai/server/services/tts/cartesia), [Deepgram](https://docs.pipecat.ai/server/services/tts/deepgram), [ElevenLabs](https://docs.pipecat.ai/server/services/tts/elevenlabs), [Fish](https://docs.pipecat.ai/server/services/tts/fish), [Google](https://docs.pipecat.ai/server/services/tts/google), [Gradium](https://docs.pipecat.ai/server/services/tts/gradium), [Groq](https://docs.pipecat.ai/server/services/tts/groq), [Hathora](https://docs.pipecat.ai/server/services/tts/hathora), [Hume](https://docs.pipecat.ai/server/services/tts/hume), [Inworld](https://docs.pipecat.ai/server/services/tts/inworld), [LMNT](https://docs.pipecat.ai/server/services/tts/lmnt), [MiniMax](https://docs.pipecat.ai/server/services/tts/minimax), [Neuphonic](https://docs.pipecat.ai/server/services/tts/neuphonic), [NVIDIA Riva](https://docs.pipecat.ai/server/services/tts/riva), [OpenAI](https://docs.pipecat.ai/server/services/tts/openai), [Piper](https://docs.pipecat.ai/server/services/tts/piper), [PlayHT](https://docs.pipecat.ai/server/services/tts/playht), [Rime](https://docs.pipecat.ai/server/services/tts/rime), [Sarvam](https://docs.pipecat.ai/server/services/tts/sarvam), [Speechmatics](https://docs.pipecat.ai/server/services/tts/speechmatics), [XTTS](https://docs.pipecat.ai/server/services/tts/xtts) |
| Speech-to-Speech | [AWS Nova Sonic](https://docs.pipecat.ai/server/services/s2s/aws), [Gemini Multimodal Live](https://docs.pipecat.ai/server/services/s2s/gemini), [Grok Voice Agent](https://docs.pipecat.ai/server/services/s2s/grok), [OpenAI Realtime](https://docs.pipecat.ai/server/services/s2s/openai), [Ultravox](https://docs.pipecat.ai/server/services/s2s/ultravox), | | Speech-to-Speech | [AWS Nova Sonic](https://docs.pipecat.ai/server/services/s2s/aws), [Gemini Multimodal Live](https://docs.pipecat.ai/server/services/s2s/gemini), [Grok Voice Agent](https://docs.pipecat.ai/server/services/s2s/grok), [OpenAI Realtime](https://docs.pipecat.ai/server/services/s2s/openai), [Ultravox](https://docs.pipecat.ai/server/services/s2s/ultravox), |
| Transport | [Daily (WebRTC)](https://docs.pipecat.ai/server/services/transport/daily), [FastAPI Websocket](https://docs.pipecat.ai/server/services/transport/fastapi-websocket), [SmallWebRTCTransport](https://docs.pipecat.ai/server/services/transport/small-webrtc), [WebSocket Server](https://docs.pipecat.ai/server/services/transport/websocket-server), Local | | Transport | [Daily (WebRTC)](https://docs.pipecat.ai/server/services/transport/daily), [FastAPI Websocket](https://docs.pipecat.ai/server/services/transport/fastapi-websocket), [SmallWebRTCTransport](https://docs.pipecat.ai/server/services/transport/small-webrtc), [WebSocket Server](https://docs.pipecat.ai/server/services/transport/websocket-server), Local |
| Serializers | [Exotel](https://docs.pipecat.ai/server/utilities/serializers/exotel), [Plivo](https://docs.pipecat.ai/server/utilities/serializers/plivo), [Twilio](https://docs.pipecat.ai/server/utilities/serializers/twilio), [Telnyx](https://docs.pipecat.ai/server/utilities/serializers/telnyx), [Vonage](https://docs.pipecat.ai/server/utilities/serializers/vonage) | | Serializers | [Exotel](https://docs.pipecat.ai/server/utilities/serializers/exotel), [Plivo](https://docs.pipecat.ai/server/utilities/serializers/plivo), [Twilio](https://docs.pipecat.ai/server/utilities/serializers/twilio), [Telnyx](https://docs.pipecat.ai/server/utilities/serializers/telnyx), [Vonage](https://docs.pipecat.ai/server/utilities/serializers/vonage) |

1
changelog/3169.added.md Normal file
View File

@@ -0,0 +1 @@
- Added Hathora service to support Hathora-hosted TTS and STT models (only non-streaming)

View File

@@ -85,6 +85,9 @@ GROK_API_KEY=...
# Groq # Groq
GROQ_API_KEY=... GROQ_API_KEY=...
# Hathora
HATHORA_API_KEY=...
# Heygen # Heygen
HEYGEN_API_KEY=... HEYGEN_API_KEY=...
HEYGEN_LIVE_AVATAR_API_KEY=... HEYGEN_LIVE_AVATAR_API_KEY=...

View File

@@ -21,8 +21,8 @@ from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import LLMContextAggregatorPair from pipecat.processors.aggregators.llm_response_universal import LLMContextAggregatorPair
from pipecat.runner.types import RunnerArguments from pipecat.runner.types import RunnerArguments
from pipecat.runner.utils import create_transport from pipecat.runner.utils import create_transport
from pipecat.services.hathora.stt import ParakeetSTTService from pipecat.services.hathora.stt import HathoraSTTService
from pipecat.services.hathora.tts import ChatterboxTTSService, KokoroTTSService from pipecat.services.hathora.tts import HathoraTTSService
from pipecat.services.openai.llm import OpenAILLMService from pipecat.services.openai.llm import OpenAILLMService
from pipecat.transports.base_transport import BaseTransport, TransportParams from pipecat.transports.base_transport import BaseTransport, TransportParams
from pipecat.transports.daily.transport import DailyParams from pipecat.transports.daily.transport import DailyParams
@@ -38,13 +38,19 @@ transport_params = {
audio_in_enabled=True, audio_in_enabled=True,
audio_out_enabled=True, audio_out_enabled=True,
vad_analyzer=SileroVADAnalyzer(params=VADParams(stop_secs=0.2)), vad_analyzer=SileroVADAnalyzer(params=VADParams(stop_secs=0.2)),
turn_analyzer=LocalSmartTurnAnalyzerV3(), turn_analyzer=LocalSmartTurnAnalyzerV3(params=SmartTurnParams()),
),
"twilio": lambda: FastAPIWebsocketParams(
audio_in_enabled=True,
audio_out_enabled=True,
vad_analyzer=SileroVADAnalyzer(params=VADParams(stop_secs=0.2)),
turn_analyzer=LocalSmartTurnAnalyzerV3(params=SmartTurnParams()),
), ),
"webrtc": lambda: TransportParams( "webrtc": lambda: TransportParams(
audio_in_enabled=True, audio_in_enabled=True,
audio_out_enabled=True, audio_out_enabled=True,
vad_analyzer=SileroVADAnalyzer(params=VADParams(stop_secs=0.2)), vad_analyzer=SileroVADAnalyzer(params=VADParams(stop_secs=0.2)),
turn_analyzer=LocalSmartTurnAnalyzerV3(), turn_analyzer=LocalSmartTurnAnalyzerV3(params=SmartTurnParams()),
), ),
} }
@@ -52,24 +58,14 @@ transport_params = {
async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
logger.info(f"Starting bot") logger.info(f"Starting bot")
# See https://models.hathora.dev/model/nvidia-parakeet-tdt-0.6b-v3 stt = HathoraSTTService(
stt = ParakeetTDTSTTService( model="nvidia-parakeet-tdt-0.6b-v3",
base_url="https://app-1c7bebb9-6977-4101-9619-833b251b86d1.app.hathora.dev/v1/transcribe",
api_key=os.getenv("HATHORA_API_KEY")
) )
# See https://models.hathora.dev/model/hexgrad-kokoro-82m tts = HathoraTTSService(
tts = KokoroTTSService( model="hexgrad-kokoro-82m",
base_url="https://app-01312daf-6e53-4b9d-a4ad-13039f35adc4.app.hathora.dev/synthesize",
api_key=os.getenv("HATHORA_API_KEY"),
) )
# See https://models.hathora.dev/model/resemble-ai-chatterbox
# tts = ChatterboxTTSService(
# base_url="https://app-efbc8fe2-df55-4f96-bbe3-74f6ea9d986b.app.hathora.dev/v1/generate",
# api_key=os.getenv("HATHORA_API_KEY")
# )
# See https://models.hathora.dev/model/qwen3-30b-a3b # See https://models.hathora.dev/model/qwen3-30b-a3b
llm = OpenAILLMService( llm = OpenAILLMService(
base_url="https://app-362f7ca1-6975-4e18-a605-ab202bf2c315.app.hathora.dev/v1", base_url="https://app-362f7ca1-6975-4e18-a605-ab202bf2c315.app.hathora.dev/v1",

View File

@@ -1,14 +0,0 @@
#
# Copyright (c) 20242025, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#
import sys
from pipecat.services import DeprecatedModuleProxy
from .stt import *
from .tts import *
sys.modules[__name__] = DeprecatedModuleProxy(globals(), "hathora", "hathora.[stt,tts]")

View File

@@ -6,102 +6,126 @@
"""[Hathora-hosted](https://models.hathora.dev) speech-to-text services.""" """[Hathora-hosted](https://models.hathora.dev) speech-to-text services."""
import base64
import os import os
from typing import Optional from typing import AsyncGenerator, Optional
import aiohttp import aiohttp
from loguru import logger
from pipecat.frames.frames import ( from pipecat.frames.frames import (
ErrorFrame, ErrorFrame,
Frame,
TranscriptionFrame, TranscriptionFrame,
) )
from pipecat.services.stt_service import SegmentedSTTService from pipecat.services.stt_service import SegmentedSTTService
from pipecat.transcriptions.language import Language from pipecat.transcriptions.language import Language
from pipecat.utils.time import time_now_iso8601 from pipecat.utils.time import time_now_iso8601
from pipecat.utils.tracing.service_decorators import traced_stt
class ParakeetTDTSTTService(SegmentedSTTService): from .utils import ConfigOption
"""Parakeet TDT is a multilingual automatic speech recognition model
with word-level timestamps.
This service uses the Hathora-hosted Parakeet model via the HTTP API.
[Documentation](https://models.hathora.dev/model/nvidia-parakeet-tdt-0.6b-v3) class HathoraSTTService(SegmentedSTTService):
"""This service supports several different speech-to-text models hosted by Hathora.
[Documentation](https://models.hathora.dev)
""" """
def __init__( def __init__(
self, self,
*, *,
base_url = None, model: str,
api_key = None, language: Optional[str] = None,
start_time: Optional[int] = None, model_config: Optional[list[ConfigOption]] = None,
end_time: Optional[int] = None, base_url: str = "https://api.models.hathora.dev/inference/v1/stt",
api_key: Optional[str] = None,
**kwargs, **kwargs,
): ):
"""Initialize the Hathora-hosted Parakeet STT service. """Initialize the Hathora STT service.
Args: Args:
base_url: Base URL for the Hathora Parakeet STT API. model: Model to use; find available models
[here](https://models.hathora.dev).
language: Language code (if supported by model).
model_config: Some models support additional config, refer to
[docs](https://models.hathora.dev) for each model to see
what is supported.
base_url: Base API URL for the Hathora STT service.
api_key: API key for authentication with the Hathora service; api_key: API key for authentication with the Hathora service;
provisiion one [here](https://models.hathora.dev/tokens). provision one [here](https://models.hathora.dev/tokens).
start_time: Start time in seconds for the time window. **kwargs: Additional arguments passed to the parent class.
end_time: End time in seconds for the time window.
""" """
super().__init__( super().__init__(
**kwargs, **kwargs,
) )
self._model = model
self._language = language
self._model_config = model_config
self._base_url = base_url self._base_url = base_url
self._api_key = api_key self._api_key = api_key or os.getenv("HATHORA_API_KEY")
self._start_time = start_time
self._end_time = end_time
def can_generate_metrics(self) -> bool: @traced_stt
return True async def _handle_transcription(
self, transcript: str, is_final: bool, language: Optional[Language] = None
):
"""Handle a transcription result with tracing."""
pass
async def run_stt(self, audio: bytes): @traced_stt
async def run_stt(self, audio: bytes) -> AsyncGenerator[Frame, None]:
"""Run speech-to-text on the provided audio data.
Args:
audio: Raw audio bytes to transcribe.
Yields:
Frame: Frames containing transcription results (typically TextFrame).
"""
try: try:
await self.start_processing_metrics() await self.start_processing_metrics()
await self.start_ttfb_metrics() await self.start_ttfb_metrics()
url = f"{self._base_url}" url = f"{self._base_url}"
url_query_params = [] payload = {
if self._start_time is not None: "model": self._model,
url_query_params.append(f"start_time={self._start_time}") }
if self._end_time is not None:
url_query_params.append(f"end_time={self._end_time}")
url_query_params.append(f"sample_rate={self.sample_rate}")
if len(url_query_params) > 0: if self._language is not None:
url += "?" + "&".join(url_query_params) payload["language"] = self._language
if self._model_config is not None:
payload["model_config"] = [
{"name": option.name, "value": option.value} for option in self._model_config
]
api_key = self._api_key or os.getenv("HATHORA_API_KEY") base64_audio = base64.b64encode(audio).decode("utf-8")
payload["audio"] = base64_audio
form_data = aiohttp.FormData()
form_data.add_field("file", audio, filename="audio.wav", content_type="application/octet-stream")
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.post( async with session.post(
url, url,
headers={"Authorization": f"Bearer {api_key}"}, headers={"Authorization": f"Bearer {self._api_key}"},
data=form_data, json=payload,
) as resp: ) as resp:
response = await resp.json() response = await resp.json()
if response and "text" in response: if response and "text" in response:
text = response["text"].strip() text = response["text"].strip()
if text: # Only yield non-empty text if text: # Only yield non-empty text
await self.stop_ttfb_metrics() # Hathora's API currently doesn't return language info
await self.stop_processing_metrics() # so we default to the requested language or "en"
logger.debug(f"Transcription: [{text}]") response_language = self._language or "en"
await self._handle_transcription(text, True, response_language)
yield TranscriptionFrame( yield TranscriptionFrame(
text, text,
self._user_id, self._user_id,
time_now_iso8601(), time_now_iso8601(),
Language("en"), # TODO: the parakeet hathora API doesn't accept a language but says it's multilingual Language(response_language),
result=response, result=response,
) )
await self.stop_ttfb_metrics()
await self.stop_processing_metrics()
except Exception as e: except Exception as e:
logger.error(f"Hathora error: {e}") yield ErrorFrame(error=f"Unknown error occurred: {e}")
yield ErrorFrame(f"Hathora error: {str(e)}")

View File

@@ -9,18 +9,22 @@
import io import io
import os import os
import wave import wave
from typing import Optional, Tuple from typing import AsyncGenerator, Optional, Tuple
import aiohttp import aiohttp
from loguru import logger
from pipecat.frames.frames import ( from pipecat.frames.frames import (
ErrorFrame, ErrorFrame,
Frame,
TTSAudioRawFrame, TTSAudioRawFrame,
TTSStartedFrame, TTSStartedFrame,
TTSStoppedFrame, TTSStoppedFrame,
) )
from pipecat.services.tts_service import TTSService from pipecat.services.tts_service import TTSService
from pipecat.utils.tracing.service_decorators import traced_tts
from .utils import ConfigOption
def _decode_audio_payload( def _decode_audio_payload(
audio_bytes: bytes, audio_bytes: bytes,
@@ -29,7 +33,6 @@ def _decode_audio_payload(
fallback_channels: int = 1, fallback_channels: int = 1,
) -> Tuple[bytes, int, int]: ) -> Tuple[bytes, int, int]:
"""Convert a WAV/PCM payload into raw PCM samples for TTSAudioRawFrame.""" """Convert a WAV/PCM payload into raw PCM samples for TTSAudioRawFrame."""
try: try:
with wave.open(io.BytesIO(audio_bytes), "rb") as wav_reader: with wave.open(io.BytesIO(audio_bytes), "rb") as wav_reader:
channels = wav_reader.getnchannels() channels = wav_reader.getnchannels()
@@ -40,69 +43,82 @@ def _decode_audio_payload(
# If the payload is already raw PCM, just pass it through. # If the payload is already raw PCM, just pass it through.
return audio_bytes, fallback_sample_rate, fallback_channels return audio_bytes, fallback_sample_rate, fallback_channels
class KokoroTTSService(TTSService):
"""Kokoro is an open-weight TTS model with 82 million parameters.
This service uses the Hathora-hosted Kokoro model via the HTTP API. class HathoraTTSService(TTSService):
"""This service supports several different text-to-speech models hosted by Hathora.
[Documentation](https://models.hathora.dev/model/hexgrad-kokoro-82m) [Documentation](https://models.hathora.dev)
""" """
def __init__( def __init__(
self, self,
*, *,
base_url = None, model: str,
api_key = None,
voice: Optional[str] = None, voice: Optional[str] = None,
speed: Optional[float] = None, speed: Optional[float] = None,
model_config: Optional[list[ConfigOption]] = None,
base_url: str = "https://api.models.hathora.dev/inference/v1/tts",
api_key: Optional[str] = None,
**kwargs, **kwargs,
): ):
"""Initialize the Hathora-hosted Kokoro TTS service. """Initialize the Hathora TTS service.
Args: Args:
base_url: Base URL for the Hathora Kokoro TTS API, . model: Model to use; find available models
[here](https://models.hathora.dev).
voice: Voice to use for synthesis (if supported by model).
speed: Speech speed multiplier (if supported by model).
model_config: Some models support additional config, refer to
[docs](https://models.hathora.dev) for each model to see
what is supported.
base_url: Base API URL for the Hathora TTS service.
api_key: API key for authentication with the Hathora service; api_key: API key for authentication with the Hathora service;
provisiion one [here](https://models.hathora.dev/tokens). provision one [here](https://models.hathora.dev/tokens).
voice: Voice to use for synthesis (see the **kwargs: Additional arguments passed to the parent class.
[Hathora docs](https://models.hathora.dev/model/hexgrad-kokoro-82m)
for the default value; [list of voices](https://huggingface.co/hexgrad/Kokoro-82M/blob/main/VOICES.md)).
speed: Speech speed multiplier (0.5 = half speed, 2.0 = double speed, default: 1).
""" """
super().__init__( super().__init__(
**kwargs, **kwargs,
) )
self._base_url = base_url self._model = model
self._api_key = api_key
self._voice = voice self._voice = voice
self._speed = speed self._speed = speed
self._model_config = model_config
self._base_url = base_url
self._api_key = api_key or os.getenv("HATHORA_API_KEY")
def can_generate_metrics(self) -> bool: @traced_tts
return True async def run_tts(self, text: str) -> AsyncGenerator[Frame, None]:
"""Run text-to-speech synthesis on the provided text.
async def run_tts(self, text: str): Args:
text: The text to synthesize into speech.
Yields:
Frame: Audio frames containing the synthesized speech.
"""
try: try:
await self.start_processing_metrics() await self.start_processing_metrics()
await self.start_ttfb_metrics() await self.start_ttfb_metrics()
url = f"{self._base_url}" url = f"{self._base_url}"
api_key = self._api_key or os.getenv("HATHORA_API_KEY") payload = {"model": self._model, "text": text}
payload = {
"text": text
}
if self._voice is not None: if self._voice is not None:
payload["voice"] = self._voice payload["voice"] = self._voice
if self._speed is not None: if self._speed is not None:
payload["speed"] = self._speed payload["speed"] = self._speed
if self._model_config is not None:
payload["model_config"] = [
{"name": option.name, "value": option.value} for option in self._model_config
]
yield TTSStartedFrame() yield TTSStartedFrame()
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.post( async with session.post(
url, url,
headers={"Authorization": f"Bearer {api_key}", "Accept": "application/octet-stream"}, headers={"Authorization": f"Bearer {self._api_key}"},
json=payload, json=payload,
) as resp: ) as resp:
audio_data = await resp.read() audio_data = await resp.read()
@@ -112,8 +128,6 @@ class KokoroTTSService(TTSService):
fallback_sample_rate=self.sample_rate or self._init_sample_rate or 24000, fallback_sample_rate=self.sample_rate or self._init_sample_rate or 24000,
) )
await self.stop_ttfb_metrics()
frame = TTSAudioRawFrame( frame = TTSAudioRawFrame(
audio=pcm_audio, audio=pcm_audio,
sample_rate=sample_rate, sample_rate=sample_rate,
@@ -123,107 +137,8 @@ class KokoroTTSService(TTSService):
yield frame yield frame
except Exception as e: except Exception as e:
logger.error(f"Hathora error: {e}") yield ErrorFrame(error=f"Unknown error occurred: {e}")
yield ErrorFrame(f"Hathora error: {str(e)}")
finally: finally:
await self.stop_ttfb_metrics() await self.stop_ttfb_metrics()
await self.stop_processing_metrics() await self.stop_processing_metrics()
yield TTSStoppedFrame() yield TTSStoppedFrame()
class ChatterboxTTSService(TTSService):
"""Chatterbox is a public text-to-speech model optimized for natural and expressive voice synthesis.
This service uses the Hathora-hosted Chatterbox model via the HTTP API.
[Documentation](https://models.hathora.dev/model/resemble-ai-chatterbox)
"""
def __init__(
self,
*,
base_url = None,
api_key = None,
exaggeration: Optional[float] = None,
audio_prompt: Optional[bytes] = None,
cfg_weight: Optional[float] = None,
**kwargs,
):
"""Initialize the Hathora-hosted Chatterbox TTS service.
Args:
base_url: Base URL for the Hathora Chatterbox TTS API.
api_key: API key for authentication with the Hathora service;
provisiion one [here](https://models.hathora.dev/tokens).
exaggeration: Controls emotional intensity (default: 0.5).
audio_prompt: Reference audio file for voice cloning.
cfg_weight: Controls adherence to reference voice (default: 0.5).
"""
super().__init__(
**kwargs,
)
self._base_url = base_url
self._api_key = api_key
self._exaggeration = exaggeration
self._audio_prompt = audio_prompt
self._cfg_weight = cfg_weight
def can_generate_metrics(self) -> bool:
return True
async def run_tts(self, text: str):
try:
await self.start_ttfb_metrics()
url = f"{self._base_url}"
url_query_params = []
if self._exaggeration is not None:
url_query_params.append(f"exaggeration={self._exaggeration}")
if self._cfg_weight is not None:
url_query_params.append(f"cfg_weight={self._cfg_weight}")
if len(url_query_params) > 0:
url += "?" + "&".join(url_query_params)
api_key = self._api_key or os.getenv("HATHORA_API_KEY")
form_data = aiohttp.FormData()
form_data.add_field("text", text)
if self._audio_prompt is not None:
form_data.add_field("audio_prompt", self._audio_prompt, filename="audio.wav", content_type="application/octet-stream")
yield TTSStartedFrame()
async with aiohttp.ClientSession() as session:
async with session.post(
url,
headers={"Authorization": f"Bearer {api_key}"},
data=form_data,
) as resp:
audio_data = await resp.read()
await self.start_tts_usage_metrics(text)
pcm_audio, sample_rate, num_channels = _decode_audio_payload(
audio_data,
fallback_sample_rate=self.sample_rate or self._init_sample_rate or 24000,
)
await self.stop_ttfb_metrics()
frame = TTSAudioRawFrame(
audio=pcm_audio,
sample_rate=sample_rate,
num_channels=num_channels,
)
yield frame
except Exception as e:
logger.error(f"Hathora error: {e}")
yield ErrorFrame(f"Hathora error: {str(e)}")
finally:
await self.stop_ttfb_metrics()
yield TTSStoppedFrame()

View File

@@ -0,0 +1,22 @@
#
# Copyright (c) 20242025, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#
"""Utilities and types for [Hathora-hosted](https://models.hathora.dev) voice services."""
from dataclasses import dataclass
@dataclass
class ConfigOption:
"""Extra configuration option passed into model_config for Hathora (if supported by model).
Args:
name: Name of the configuration option.
value: Value of the configuration option.
"""
name: str
value: str