""" hume_api.py This file defines the interaction with the Hume text-to-speech (TTS) API. It includes functionality for API request handling and processing API responses. Key Features: - Encapsulates all logic related to the Hume TTS API. - Implements retry logic for handling transient API errors. - Handles received audio and processes it for playback on the web. - Provides detailed logging for debugging and error tracking. Classes: - HumeConfig: Immutable configuration for interacting with Hume's TTS API. - HumeError: Custom exception for Hume API-related errors. Functions: - text_to_speech_with_hume: Synthesizes speech from text using Hume's TTS API. """ # Standard Library Imports from dataclasses import dataclass import logging import random from typing import List, Literal, Optional, Tuple # Third-Party Library Imports import requests from tenacity import retry, stop_after_attempt, wait_fixed, before_log, after_log # Local Application Imports from src.config import logger from src.utils import validate_env_var, truncate_text HumeVoiceName = Literal["ITO", "KORA", "STELLA", "DACHER"] @dataclass(frozen=True) class HumeConfig: """Immutable configuration for interacting with the Hume TTS API.""" api_key: str = validate_env_var("HUME_API_KEY") tts_endpoint_url: str = "https://api.hume.ai/v0/tts" voice_names: List[HumeVoiceName] = ("ITO", "KORA", "STELLA", "DACHER") audio_format: str = "wav" headers: dict = None def __post_init__(self): # Validate required attributes if not self.api_key: raise ValueError("Hume API key is not set.") if not self.tts_endpoint_url: raise ValueError("Hume TTS endpoint URL is not set.") if not self.voice_names: raise ValueError("Hume voice names list is not set.") if not self.audio_format: raise ValueError("Hume audio format is not set.") # Set headers dynamically after validation object.__setattr__( self, "headers", { "X-Hume-Api-Key": f"{self.api_key}", "Content-Type": "application/json", }, ) class HumeError(Exception): """Custom exception for errors related to the Hume TTS API.""" def __init__(self, message: str, original_exception: Optional[Exception] = None): super().__init__(message) self.original_exception = original_exception # Initialize the Hume client hume_config = HumeConfig() @retry( stop=stop_after_attempt(1), wait=wait_fixed(2), before=before_log(logger, logging.DEBUG), after=after_log(logger, logging.DEBUG), reraise=True, ) def text_to_speech_with_hume( prompt: str, text: str, voice_name: HumeVoiceName ) -> bytes: """ Synthesizes text to speech using the Hume TTS API and processes raw binary audio data. Args: prompt (str): The original user prompt (for debugging). text (str): The generated text to be converted to speech. voice_name (HumeVoiceName): Name of the voice Hume will use when synthesizing speech. Returns: voice_name: The name of the voice used for speech synthesis. bytes: The raw binary audio data for playback. Raises: HumeError: If there is an error communicating with the Hume TTS API. """ logger.debug( f"Processing TTS with Hume. Prompt length: {len(prompt)} characters. Text length: {len(text)} characters." ) request_body = { "text": text, "voice": {"name": voice_name}, } try: # Synthesize speech using the Hume TTS API response = requests.post( url=hume_config.tts_endpoint_url, headers=hume_config.headers, json=request_body, ) # Validate response if response.status_code != 200: logger.error( f"Hume TTS API Error: {response.status_code} - {response.text[:200]}... (truncated)" ) raise HumeError( f"Hume TTS API responded with status {response.status_code}: {response.text[:200]}" ) # Process response audio if response.headers.get("Content-Type", "").startswith("audio/"): audio = response.content # Raw binary audio data logger.info(f"Received audio data from Hume ({len(audio)} bytes).") return voice_name, audio raise HumeError( f'Unexpected Content-Type: {response.headers.get("Content-Type", "Unknown")}' ) except Exception as e: logger.exception(f"Error synthesizing speech from text with Hume: {e}") raise HumeError( message=f"Failed to synthesize speech from text with Hume: {e}", original_exception=e, ) def get_random_hume_voice_names() -> Tuple[HumeVoiceName, HumeVoiceName]: """ Get two random Hume voice names. Voices: - ITO - KORA - STELLA - DACHER """ return tuple(random.sample(hume_config.voice_names, 2))