Spaces:
Paused
Paused
| """ | |
| Translates from OpenAI's `/v1/audio/transcriptions` to Deepgram's `/v1/listen` | |
| """ | |
| import io | |
| from typing import List, Optional, Union | |
| from urllib.parse import urlencode | |
| from httpx import Headers, Response | |
| from litellm.llms.base_llm.chat.transformation import BaseLLMException | |
| from litellm.secret_managers.main import get_secret_str | |
| from litellm.types.llms.openai import ( | |
| AllMessageValues, | |
| OpenAIAudioTranscriptionOptionalParams, | |
| ) | |
| from litellm.types.utils import FileTypes, TranscriptionResponse | |
| from ...base_llm.audio_transcription.transformation import ( | |
| BaseAudioTranscriptionConfig, | |
| LiteLLMLoggingObj, | |
| ) | |
| from ..common_utils import DeepgramException | |
| class DeepgramAudioTranscriptionConfig(BaseAudioTranscriptionConfig): | |
| def get_supported_openai_params( | |
| self, model: str | |
| ) -> List[OpenAIAudioTranscriptionOptionalParams]: | |
| return ["language"] | |
| def map_openai_params( | |
| self, | |
| non_default_params: dict, | |
| optional_params: dict, | |
| model: str, | |
| drop_params: bool, | |
| ) -> dict: | |
| supported_params = self.get_supported_openai_params(model) | |
| for k, v in non_default_params.items(): | |
| if k in supported_params: | |
| optional_params[k] = v | |
| return optional_params | |
| def get_error_class( | |
| self, error_message: str, status_code: int, headers: Union[dict, Headers] | |
| ) -> BaseLLMException: | |
| return DeepgramException( | |
| message=error_message, status_code=status_code, headers=headers | |
| ) | |
| def transform_audio_transcription_request( | |
| self, | |
| model: str, | |
| audio_file: FileTypes, | |
| optional_params: dict, | |
| litellm_params: dict, | |
| ) -> Union[dict, bytes]: | |
| """ | |
| Processes the audio file input based on its type and returns the binary data. | |
| Args: | |
| audio_file: Can be a file path (str), a tuple (filename, file_content), or binary data (bytes). | |
| Returns: | |
| The binary data of the audio file. | |
| """ | |
| binary_data: bytes # Explicitly declare the type | |
| # Handle the audio file based on type | |
| if isinstance(audio_file, str): | |
| # If it's a file path | |
| with open(audio_file, "rb") as f: | |
| binary_data = f.read() # `f.read()` always returns `bytes` | |
| elif isinstance(audio_file, tuple): | |
| # Handle tuple case | |
| _, file_content = audio_file[:2] | |
| if isinstance(file_content, str): | |
| with open(file_content, "rb") as f: | |
| binary_data = f.read() # `f.read()` always returns `bytes` | |
| elif isinstance(file_content, bytes): | |
| binary_data = file_content | |
| else: | |
| raise TypeError( | |
| f"Unexpected type in tuple: {type(file_content)}. Expected str or bytes." | |
| ) | |
| elif isinstance(audio_file, bytes): | |
| # Assume it's already binary data | |
| binary_data = audio_file | |
| elif isinstance(audio_file, io.BufferedReader) or isinstance( | |
| audio_file, io.BytesIO | |
| ): | |
| # Handle file-like objects | |
| binary_data = audio_file.read() | |
| else: | |
| raise TypeError(f"Unsupported type for audio_file: {type(audio_file)}") | |
| return binary_data | |
| def transform_audio_transcription_response( | |
| self, | |
| model: str, | |
| raw_response: Response, | |
| model_response: TranscriptionResponse, | |
| logging_obj: LiteLLMLoggingObj, | |
| request_data: dict, | |
| optional_params: dict, | |
| litellm_params: dict, | |
| api_key: Optional[str] = None, | |
| ) -> TranscriptionResponse: | |
| """ | |
| Transforms the raw response from Deepgram to the TranscriptionResponse format | |
| """ | |
| try: | |
| response_json = raw_response.json() | |
| # Get the first alternative from the first channel | |
| first_channel = response_json["results"]["channels"][0] | |
| first_alternative = first_channel["alternatives"][0] | |
| # Extract the full transcript | |
| text = first_alternative["transcript"] | |
| # Create TranscriptionResponse object | |
| response = TranscriptionResponse(text=text) | |
| # Add additional metadata matching OpenAI format | |
| response["task"] = "transcribe" | |
| response["language"] = ( | |
| "english" # Deepgram auto-detects but doesn't return language | |
| ) | |
| response["duration"] = response_json["metadata"]["duration"] | |
| # Transform words to match OpenAI format | |
| if "words" in first_alternative: | |
| response["words"] = [ | |
| {"word": word["word"], "start": word["start"], "end": word["end"]} | |
| for word in first_alternative["words"] | |
| ] | |
| # Store full response in hidden params | |
| response._hidden_params = response_json | |
| return response | |
| except Exception as e: | |
| raise ValueError( | |
| f"Error transforming Deepgram response: {str(e)}\nResponse: {raw_response.text}" | |
| ) | |
| def get_complete_url( | |
| self, | |
| api_base: Optional[str], | |
| api_key: Optional[str], | |
| model: str, | |
| optional_params: dict, | |
| litellm_params: dict, | |
| stream: Optional[bool] = None, | |
| ) -> str: | |
| if api_base is None: | |
| api_base = ( | |
| get_secret_str("DEEPGRAM_API_BASE") or "https://api.deepgram.com/v1" | |
| ) | |
| api_base = api_base.rstrip("/") # Remove trailing slash if present | |
| # Build query parameters including the model | |
| all_query_params = {"model": model} | |
| # Add filtered optional parameters | |
| additional_params = self._build_query_params(optional_params, model) | |
| all_query_params.update(additional_params) | |
| # Construct URL with proper query string encoding | |
| base_url = f"{api_base}/listen" | |
| query_string = urlencode(all_query_params) | |
| url = f"{base_url}?{query_string}" | |
| return url | |
| def _should_exclude_param( | |
| self, | |
| param_name: str, | |
| model: str, | |
| ) -> bool: | |
| """ | |
| Determines if a parameter should be excluded from the query string. | |
| Args: | |
| param_name: Parameter name | |
| model: Model name | |
| Returns: | |
| True if the parameter should be excluded | |
| """ | |
| # Parameters that are handled elsewhere or not relevant to Deepgram API | |
| excluded_params = { | |
| "model", # Already in the URL path | |
| "OPENAI_TRANSCRIPTION_PARAMS", # Internal litellm parameter | |
| } | |
| # Skip if it's an excluded parameter | |
| if param_name in excluded_params: | |
| return True | |
| # Skip if it's an OpenAI-specific parameter that we handle separately | |
| if param_name in self.get_supported_openai_params(model): | |
| return True | |
| return False | |
| def _format_param_value(self, value) -> str: | |
| """ | |
| Formats a parameter value for use in query string. | |
| Args: | |
| value: The parameter value to format | |
| Returns: | |
| Formatted string value | |
| """ | |
| if isinstance(value, bool): | |
| return str(value).lower() | |
| return str(value) | |
| def _build_query_params(self, optional_params: dict, model: str) -> dict: | |
| """ | |
| Builds a dictionary of query parameters from optional_params. | |
| Args: | |
| optional_params: Dictionary of optional parameters | |
| model: Model name | |
| Returns: | |
| Dictionary of filtered and formatted query parameters | |
| """ | |
| query_params = {} | |
| for key, value in optional_params.items(): | |
| # Skip None values | |
| if value is None: | |
| continue | |
| # Skip excluded parameters | |
| if self._should_exclude_param( | |
| param_name=key, | |
| model=model, | |
| ): | |
| continue | |
| # Format and add the parameter | |
| formatted_value = self._format_param_value(value) | |
| query_params[key] = formatted_value | |
| return query_params | |
| def validate_environment( | |
| self, | |
| headers: dict, | |
| model: str, | |
| messages: List[AllMessageValues], | |
| optional_params: dict, | |
| litellm_params: dict, | |
| api_key: Optional[str] = None, | |
| api_base: Optional[str] = None, | |
| ) -> dict: | |
| api_key = api_key or get_secret_str("DEEPGRAM_API_KEY") | |
| return { | |
| "Authorization": f"Token {api_key}", | |
| } | |