diff --git a/.env.example b/.env.example index 5f10400b..e7245b39 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,2 @@ DG_API_KEY="" -DG_API_KEY_MANAGE="" -DG_PROJECT_ID="85bb84f9-02d8-742a-df82-" -DG_API_KEY_ID="" -DG_MEMBER_ID="" -DG_REQUEST_ID="" -DG_BALANCE_ID="" -EMAIL="" \ No newline at end of file +DG_PROJECT_ID="" diff --git a/deepgram/__init__.py b/deepgram/__init__.py index 878f0a10..91009932 100644 --- a/deepgram/__init__.py +++ b/deepgram/__init__.py @@ -6,16 +6,18 @@ __version__ = '0.0.0' # entry point for the deepgram python sdk -from .client import DeepgramClient +from .client import DeepgramClient, DeepgramApiKeyError from .options import DeepgramClientOptions -from .errors import DeepgramError, DeepgramApiError, DeepgramUnknownApiError # live from .clients.live.enums import LiveTranscriptionEvents -from .clients.live.client import LiveClient, LiveOptions +from .clients.live.client import LiveClient, LegacyLiveClient, LiveOptions # prerecorded from .clients.prerecorded.client import PreRecordedClient, PrerecordedOptions, PrerecordedSource, FileSource, UrlSource # manage from .clients.manage.client import ManageClient, ProjectOptions, KeyOptions, ScopeOptions, InviteOptions, UsageRequestOptions, UsageSummaryOptions, UsageFieldsOptions + +# utilities +from .audio.microphone.microphone import Microphone diff --git a/deepgram/audio/microphone/errors.py b/deepgram/audio/microphone/errors.py new file mode 100644 index 00000000..3e5af99c --- /dev/null +++ b/deepgram/audio/microphone/errors.py @@ -0,0 +1,20 @@ +# Copyright 2023 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +class DeepgramMicrophoneError(Exception): + """ + Exception raised for known errors related to Microphone library. + + Attributes: + message (str): The error message describing the exception. + status (str): The HTTP status associated with the API error. + original_error (str - json): The original error that was raised. + """ + def __init__(self, message: str): + super().__init__(message) + self.name = "DeepgramMicrophoneError" + self.message = message + + def __str__(self): + return f"{self.name}: {self.message}" \ No newline at end of file diff --git a/deepgram/audio/microphone/microphone.py b/deepgram/audio/microphone/microphone.py new file mode 100644 index 00000000..fcd2c89c --- /dev/null +++ b/deepgram/audio/microphone/microphone.py @@ -0,0 +1,89 @@ +# Copyright 2023 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +import inspect +import asyncio +import threading +import pyaudio +from array import array +from sys import byteorder + +from .errors import DeepgramMicrophoneError + +FORMAT = pyaudio.paInt16 +CHANNELS = 1 +RATE = 16000 +CHUNK = 8000 + +class Microphone: + """ + TODO + """ + def __init__(self, push_callback, format=FORMAT, rate=RATE, chunk=CHUNK, channels=CHANNELS): + self.audio = pyaudio.PyAudio() + self.chunk = chunk + self.rate = rate + self.format = format + self.channels = channels + self.push_callback = push_callback + self.stream = None + + def is_active(self): + if self.stream is None: + return False + return self.stream.is_active() + + def start(self): + if self.stream is not None: + raise DeepgramMicrophoneError("Microphone already started") + + self.stream = self.audio.open( + format=self.format, + channels=self.channels, + rate=self.rate, + input=True, + frames_per_buffer=CHUNK, + ) + + self.exit = False + self.lock = threading.Lock() + + self.stream.start_stream() + self.thread = threading.Thread(target=self.processing) + self.thread.start() + + def processing(self): + try: + while True: + data = self.stream.read(self.chunk) + + self.lock.acquire() + localExit = self.exit + self.lock.release() + if localExit: + break + if data is None: + continue + + if inspect.iscoroutinefunction(self.push_callback): + asyncio.run(self.push_callback(data)) + else: + self.push_callback(data) + + except Exception as e: + print(f"Error while sending: {str(e)}") + raise + + def finish(self): + self.lock.acquire() + self.exit = True + self.lock.release() + + self.thread.join() + self.thread = None + + self.stream.stop_stream() + self.stream.close() + self.stream = None + \ No newline at end of file diff --git a/deepgram/client.py b/deepgram/client.py index d6e4f32d..0fa9602e 100644 --- a/deepgram/client.py +++ b/deepgram/client.py @@ -7,7 +7,7 @@ from typing import Optional from .options import DeepgramClientOptions -from .errors import DeepgramError +from .errors import DeepgramApiKeyError from .clients.listen import ListenClient from .clients.manage.client import ManageClient # FUTURE VERSIONINING:, ManageClientV1 @@ -24,7 +24,7 @@ class DeepgramClient: config_options (DeepgramClientOptions): An optional configuration object specifying client options. Raises: - DeepgramError: If the API key is missing or invalid. + DeepgramApiKeyError: If the API key is missing or invalid. Methods: listen: Returns a ListenClient instance for interacting with Deepgram's transcription services. @@ -32,51 +32,35 @@ class DeepgramClient: onprem: Returns an OnPremClient instance for interacting with Deepgram's on-premises API. """ - def __init__(self, api_key: str, config_options: Optional[DeepgramClientOptions] = None): + def __init__(self, api_key: str, config: Optional[DeepgramClientOptions] = None): if not api_key: - raise DeepgramError("Deepgram API key is required") + raise DeepgramApiKeyError("Deepgram API key is required") self.api_key = api_key - - """ - This block is responsible for determining the client's configuration options and headers based on the provided or default settings. - """ - - if config_options is None: # Use default configuration - self.config_options = DeepgramClientOptions(self.api_key).global_options - self.headers = DeepgramClientOptions(self.api_key).global_options['headers'] - else: # Use custom configuration - self.config_options = config_options['global_options'] - if config_options['global_options'].get('headers'): - self.headers = {**config_options['global_options']['headers'], **DeepgramClientOptions(self.api_key).global_options['headers']} - else: - self.headers = DeepgramClientOptions(self.api_key).global_options['headers'] - self.url = self._get_url(self.config_options) - - def _get_url(self, config_options): - url = config_options['url'] - if not re.match(r'^https?://', url, re.IGNORECASE): - url = 'https://' + url - return url.strip('/') + if config is None: # Use default configuration + self.config = DeepgramClientOptions(self.api_key) + else: + config.set_apikey(self.api_key) + self.config = config @property def listen(self): - return ListenClient(self.url, self.api_key, self.headers) + return ListenClient(self.config) @property def manage(self): - return ManageClient(self.url, self.headers) + return ManageClient(self.config) # FUTURE VERSIONINING: # @property # def manage_v1(self): - # return ManageClientV1(self.url, self.headers) + # return ManageClientV1(self.config) @property def onprem(self): - return OnPremClient(self.url, self.headers) + return OnPremClient(self.config) # FUTURE VERSIONINING: # @property # def onprem_v1(self): - # return OnPremClientV1(self.url, self.headers) + # return OnPremClientV1(self.config) diff --git a/deepgram/clients/abstract_client.py b/deepgram/clients/abstract_client.py index 36c39d34..1f577022 100644 --- a/deepgram/clients/abstract_client.py +++ b/deepgram/clients/abstract_client.py @@ -6,7 +6,8 @@ import json from typing import Dict, Any, Optional -from ..errors import DeepgramApiError, DeepgramUnknownApiError +from ..options import DeepgramClientOptions +from .errors import DeepgramError, DeepgramApiError, DeepgramUnknownApiError class AbstractRestfulClient: """ @@ -28,30 +29,27 @@ class AbstractRestfulClient: DeepgramApiError: Raised for known API errors. DeepgramUnknownApiError: Raised for unknown API errors. """ - def __init__(self, url: Dict[str, str], headers: Optional[Dict[str, Any]]): - self.url = url + def __init__(self, config: DeepgramClientOptions): + if config is None: + raise DeepgramError("Config are required") + + self.config = config self.client = httpx.AsyncClient() - self.headers = headers async def get(self, url: str, options=None): - headers = self.headers - return await self._handle_request('GET', url, params=options, headers=headers) + return await self._handle_request('GET', url, params=options, headers=self.config.headers) async def post(self, url: str, options=None, **kwargs): - headers = self.headers - return await self._handle_request('POST', url, params=options, headers=headers, **kwargs) + return await self._handle_request('POST', url, params=options, headers=self.config.headers, **kwargs) async def put(self, url: str, options=None, **kwargs): - headers = self.headers - return await self._handle_request('PUT', url, params=options, headers=headers, **kwargs) + return await self._handle_request('PUT', url, params=options, headers=self.config.headers, **kwargs) async def patch(self, url: str, options=None, **kwargs): - headers = self.headers - return await self._handle_request('PATCH', url, params=options, headers=headers, **kwargs) + return await self._handle_request('PATCH', url, params=options, headers=self.config.headers, **kwargs) async def delete(self, url: str): - headers = self.headers - return await self._handle_request('DELETE', url, headers=headers) + return await self._handle_request('DELETE', url, headers=self.config.headers) async def _handle_request(self, method, url, **kwargs): try: diff --git a/deepgram/clients/errors.py b/deepgram/clients/errors.py new file mode 100644 index 00000000..aba59b9b --- /dev/null +++ b/deepgram/clients/errors.py @@ -0,0 +1,55 @@ +# Copyright 2023 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +class DeepgramError(Exception): + """ + Exception raised for unknown errors related to the Deepgram API. + + Attributes: + message (str): The error message describing the exception. + status (str): The HTTP status associated with the API error. + """ + def __init__(self, message: str): + super().__init__(message) + self.name = "DeepgramError" + self.message = message + + def __str__(self): + return f"{self.name}: {self.message}" + +class DeepgramApiError(Exception): + """ + Exception raised for known errors (in json response format) related to the Deepgram API. + + Attributes: + message (str): The error message describing the exception. + status (str): The HTTP status associated with the API error. + original_error (str - json): The original error that was raised. + """ + def __init__(self, message: str, status: str, original_error = None): + super().__init__(message) + self.name = "DeepgramApiError" + self.status = status + self.message = message + self.original_error = original_error + + def __str__(self): + return f"{self.name}: {self.message} (Status: {self.status})" + +class DeepgramUnknownApiError(Exception): + """ + Exception raised for unknown errors related to the Deepgram API. + + Attributes: + message (str): The error message describing the exception. + status (str): The HTTP status associated with the API error. + """ + def __init__(self, message: str, status: str): + super().__init__(message, status) + self.name = "DeepgramUnknownApiError" + self.status = status + self.message = message + + def __str__(self): + return f"{self.name}: {self.message} (Status: {self.status})" \ No newline at end of file diff --git a/deepgram/clients/listen.py b/deepgram/clients/listen.py index 0b40da70..844f98fd 100644 --- a/deepgram/clients/listen.py +++ b/deepgram/clients/listen.py @@ -2,30 +2,33 @@ # Use of this source code is governed by a MIT license that can be found in the LICENSE file. # SPDX-License-Identifier: MIT +from ..options import DeepgramClientOptions from .prerecorded.client import PreRecordedClient # FUTURE VERSIONINING:, PreRecordedClientV1 -from .live.client import LiveClient # FUTURE VERSIONINING:, LiveClientV1 +from .live.client import LiveClient, LegacyLiveClient # FUTURE VERSIONINING:, LiveClientV1 from typing import Dict, Any, Optional class ListenClient: - def __init__(self, url: str, api_key: str, headers: Optional[Dict[str, Any]]): - self.url = url - self.api_key = api_key - self.headers = headers + def __init__(self, config: DeepgramClientOptions): + self.config = config @property def prerecorded(self): - return PreRecordedClient(self.url, self.headers) + return PreRecordedClient(self.config) # FUTURE VERSIONINING: # @property # def prerecorded_v1(self): - # return PreRecordedClientV1(self.url, self.headers) + # return PreRecordedClientV1(self.config) @property def live(self): - return LiveClient(self.url, self.api_key, self.headers) + return LiveClient(self.config) + + @property + def legacylive(self): + return LegacyLiveClient(self.config) # FUTURE VERSIONINING: # @property # def live_v1(self): - # return LiveClientV1(self.url, self.api_key, self.headers) + # return LiveClientV1(self.config) diff --git a/deepgram/clients/live/client.py b/deepgram/clients/live/client.py index ca1026bb..54d86e1d 100644 --- a/deepgram/clients/live/client.py +++ b/deepgram/clients/live/client.py @@ -3,6 +3,7 @@ # SPDX-License-Identifier: MIT from .v1_client import LiveClientV1 +from .v1_legacy_client import LegacyLiveClientV1 from .v1_options import LiveOptionsV1 ''' @@ -16,6 +17,13 @@ class LiveClient(LiveClientV1): """ Please see LiveClientV1 for details """ - def __init__(self, base_url, api_key, headers): - super().__init__(base_url, api_key, headers) + def __init__(self, config): + super().__init__(config) + +class LegacyLiveClient(LegacyLiveClientV1): + """ + Please see LiveClientV1 for details + """ + def __init__(self, config): + super().__init__(config) \ No newline at end of file diff --git a/deepgram/clients/live/errors.py b/deepgram/clients/live/errors.py new file mode 100644 index 00000000..8b63e476 --- /dev/null +++ b/deepgram/clients/live/errors.py @@ -0,0 +1,36 @@ +# Copyright 2023 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +class DeepgramError(Exception): + """ + Exception raised for unknown errors related to the Deepgram API. + + Attributes: + message (str): The error message describing the exception. + status (str): The HTTP status associated with the API error. + """ + def __init__(self, message: str): + super().__init__(message) + self.name = "DeepgramError" + self.message = message + + def __str__(self): + return f"{self.name}: {self.message}" + +class DeepgramWebsocketError(Exception): + """ + Exception raised for known errors related to Websocket library. + + Attributes: + message (str): The error message describing the exception. + status (str): The HTTP status associated with the API error. + original_error (str - json): The original error that was raised. + """ + def __init__(self, message: str): + super().__init__(message) + self.name = "DeepgramWebsocketError" + self.message = message + + def __str__(self): + return f"{self.name}: {self.message}" diff --git a/deepgram/clients/live/v1_client.py b/deepgram/clients/live/v1_client.py index d93c85ff..d19e6e79 100644 --- a/deepgram/clients/live/v1_client.py +++ b/deepgram/clients/live/v1_client.py @@ -2,14 +2,19 @@ # Use of this source code is governed by a MIT license that can be found in the LICENSE file. # SPDX-License-Identifier: MIT +from ...options import DeepgramClientOptions +from .v1_options import LiveOptionsV1 from .enums import LiveTranscriptionEvents +from .v1_response import LiveResultResponse, MetadataResponse, ErrorResponse from .helpers import convert_to_websocket_url, append_query_params +from .errors import DeepgramError, DeepgramWebsocketError -from deepgram.errors import DeepgramApiError - -import asyncio import json -import websockets +from websockets.sync.client import connect +import threading +import time + +PING_INTERVAL = 5 class LiveClientV1: """ @@ -18,9 +23,7 @@ class LiveClientV1: This class provides methods to establish a WebSocket connection for live transcription and handle real-time transcription events. Args: - base_url (str): The base URL for WebSocket connection. - api_key (str): The Deepgram API key used for authentication. - headers (dict): Additional HTTP headers for WebSocket connection. + config (DeepgramClientOptions): all the options for the client. Attributes: endpoint (str): The API endpoint for live transcription. @@ -34,72 +37,162 @@ class LiveClientV1: send: Sends data over the WebSocket connection. finish: Closes the WebSocket connection gracefully. """ - def __init__(self, base_url, api_key, headers): - self.base_url = base_url - self.api_key = api_key - self.headers = headers + def __init__(self, config: DeepgramClientOptions): + if config is None: + raise DeepgramError("Config are required") + + self.config = config self.endpoint = "v1/listen" self._socket = None + self.exit = False self._event_handlers = { event: [] for event in LiveTranscriptionEvents } - self.websocket_url = convert_to_websocket_url(base_url, self.endpoint) - - async def __call__(self, options=None): - url_with_params = append_query_params(self.websocket_url, options) - try: - self._socket = await _socket_connect(url_with_params, self.headers) - asyncio.create_task(self._start()) - return self - except websockets.ConnectionClosed as e: - await self._emit(LiveTranscriptionEvents.Close, e.code) - - + self.websocket_url = convert_to_websocket_url(self.config.url, self.endpoint) + + def __call__(self, options: LiveOptionsV1 = None): + self.options = options + return self + + def start(self): + if self._socket is not None: + raise DeepgramWebsocketError("Websocket already started") + + url_with_params = append_query_params(self.websocket_url, self.options) + self._socket = connect(url_with_params, additional_headers=self.config.headers) + + self.exit = False + self.lock_exit = threading.Lock() + self.lock_send = threading.Lock() + + # listening thread + self.listening = threading.Thread(target=self._listening) + self.listening.start() + + # keepalive thread + self.processing = None + if self.config.options.get("keepalive") == "true": + self.processing = threading.Thread(target=self._processing) + self.processing.start() + def on(self, event, handler): # registers event handlers for specific events if event in LiveTranscriptionEvents and callable(handler): self._event_handlers[event].append(handler) - async def _emit(self, event, *args, **kwargs): # triggers the registered event handlers for a specific event + def _emit(self, event, *args, **kwargs): # triggers the registered event handlers for a specific event for handler in self._event_handlers[event]: handler(*args, **kwargs) - async def _start(self) -> None: - async for message in self._socket: + def _listening(self) -> None: + while True: try: + self.lock_exit.acquire() + myExit = self.exit + self.lock_exit.release() + if myExit: + return + + message = self._socket.recv() + if len(message) == 0: + # print("empty message") + continue + data = json.loads(message) response_type = data.get("type") - if response_type == LiveTranscriptionEvents.Transcript.value: - await self._emit(LiveTranscriptionEvents.Transcript, data) - if "metadata" in data: - await self._emit(LiveTranscriptionEvents.Metadata, data["metadata"]) - except json.JSONDecodeError as e: - await self._emit(LiveTranscriptionEvents.Error, e.code) - - async def send(self, data): - if self._socket: - await self._socket.send(data) + # print(f"response_type: {response_type}") - async def finish(self): - if self._socket: - await self._socket.send(json.dumps({"type": "CloseStream"})) - # await self._socket.send("") # Send a zero-byte message - await self._socket.wait_closed() + match response_type: + case LiveTranscriptionEvents.Transcript.value: + result = LiveResultResponse.from_json(message) + self._emit(LiveTranscriptionEvents.Transcript, result=result) + case LiveTranscriptionEvents.Metadata.value: + result = MetadataResponse.from_json(message) + self._emit(LiveTranscriptionEvents.Metadata, metadata=result) + case LiveTranscriptionEvents.Error.value: + result = ErrorResponse.from_json(message) + self._emit(LiveTranscriptionEvents.Error, error=result) + case _: + error: ErrorResponse = { + 'type': 'UnhandledMessage', + 'description': 'Unknown message type', + 'message': f'Unhandle message type: {response_type}', + 'variant': '', + } + self._emit(LiveTranscriptionEvents.Error, error) -async def _socket_connect(websocket_url, headers): - destination = websocket_url - updated_headers = headers + except Exception as e: + # print(f"Exception in _listening: {e}") + if e.code == 1000: + # print("Websocket closed") + return + + error: ErrorResponse = { + 'type': 'Exception', + 'description': 'Unknown error _listening', + 'message': f'{e}', + 'variant': '', + } + self._emit(LiveTranscriptionEvents.Error, error) - async def attempt(): + def _processing(self) -> None: + # print("Starting KeepAlive") + while True: try: - return await websockets.connect( - destination, extra_headers=updated_headers, ping_interval=5 - ) - except websockets.exceptions.InvalidHandshake as exc: - raise DeepgramApiError(exc, http_library_error=exc) from exc - - # tries = 4 - # while tries > 0: - # try: - # return await attempt() - # except Exception as exc: - # tries -= 1 - # continue - return await attempt() \ No newline at end of file + time.sleep(PING_INTERVAL) + + self.lock_exit.acquire() + myExit = self.exit + self.lock_exit.release() + if myExit: + return + + # deepgram keepalive + # print("Sending KeepAlive") + self.send(json.dumps({"type": "KeepAlive"})) + + except Exception as e: + # print(f"Exception in _processing: {e}") + if e.code == 1000: + # print("Websocket closed") + return + + error: ErrorResponse = { + 'type': 'Exception', + 'description': 'Unknown error in _processing', + 'message': f'{e}', + 'variant': '', + } + self._emit(LiveTranscriptionEvents.Error, error) + + def send(self, data) -> int: + if self._socket: + self.lock_send.acquire() + ret = self._socket.send(data) + self.lock_send.release() + return ret + return 0 + + def finish(self): + # print("Send CloseStream") + if self._socket: + self._socket.send(json.dumps({"type": "CloseStream"})) + time.sleep(1) + + # print("Closing connection...") + self.lock_exit.acquire() + self.exit = True + self.lock_exit.release() + + # print("Waiting for threads to finish...") + if self.processing is not None: + self.processing.join() + self.processing = None + + # print("Waiting for threads to finish...") + if self.listening is not None: + self.listening.join() + self.listening = None + + if self._socket: + self._socket.close() + + self._socket = None + self.lock_exit = None diff --git a/deepgram/clients/live/v1_legacy_client.py b/deepgram/clients/live/v1_legacy_client.py new file mode 100644 index 00000000..22170f7a --- /dev/null +++ b/deepgram/clients/live/v1_legacy_client.py @@ -0,0 +1,96 @@ +# Copyright 2023 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from ...options import DeepgramClientOptions +from .v1_options import LiveOptionsV1 +from .enums import LiveTranscriptionEvents +from .helpers import convert_to_websocket_url, append_query_params +from .errors import DeepgramError + +import asyncio +import json +import websockets + +class LegacyLiveClientV1: + """ + Client for interacting with Deepgram's live transcription services over WebSockets. + + This class provides methods to establish a WebSocket connection for live transcription and handle real-time transcription events. + + Args: + config (DeepgramClientOptions): all the options for the client. + + Attributes: + endpoint (str): The API endpoint for live transcription. + _socket (websockets.WebSocketClientProtocol): The WebSocket connection object. + _event_handlers (dict): Dictionary of event handlers for specific events. + websocket_url (str): The WebSocket URL used for connection. + + Methods: + __call__: Establishes a WebSocket connection for live transcription. + on: Registers event handlers for specific events. + send: Sends data over the WebSocket connection. + finish: Closes the WebSocket connection gracefully. + """ + def __init__(self, config: DeepgramClientOptions): + if config is None: + raise DeepgramError("Config are required") + + self.config = config + self.endpoint = "v1/listen" + self._socket = None + self._event_handlers = { event: [] for event in LiveTranscriptionEvents } + self.websocket_url = convert_to_websocket_url(self.config.url, self.endpoint) + + async def __call__(self, options: LiveOptionsV1 = None): + self.options = options + url_with_params = append_query_params(self.websocket_url, self.options) + try: + self._socket = await _socket_connect(url_with_params, self.config.headers) + asyncio.create_task(self._start()) + return self + except websockets.ConnectionClosed as e: + await self._emit(LiveTranscriptionEvents.Close, e.code) + + def on(self, event, handler): # registers event handlers for specific events + if event in LiveTranscriptionEvents and callable(handler): + self._event_handlers[event].append(handler) + + async def _emit(self, event, *args, **kwargs): # triggers the registered event handlers for a specific event + for handler in self._event_handlers[event]: + handler(*args, **kwargs) + + async def _start(self) -> None: + async for message in self._socket: + try: + data = json.loads(message) + response_type = data.get("type") + match response_type: + case LiveTranscriptionEvents.Transcript.value: + await self._emit(LiveTranscriptionEvents.Transcript, data) + case LiveTranscriptionEvents.Error.value: + await self._emit(LiveTranscriptionEvents.Error, data) + case LiveTranscriptionEvents.Metadata.value: + await self._emit(LiveTranscriptionEvents.Metadata, data) + case _: + await self._emit(LiveTranscriptionEvents.Error, data) + except json.JSONDecodeError as e: + await self._emit(LiveTranscriptionEvents.Error, e.code) + + async def send(self, data): + if self._socket: + await self._socket.send(data) + + async def finish(self): + if self._socket: + await self._socket.send(json.dumps({"type": "CloseStream"})) + # await self._socket.send("") # Send a zero-byte message + await self._socket.wait_closed() + +async def _socket_connect(websocket_url, headers): + destination = websocket_url + updated_headers = headers + return await websockets.connect( + destination, extra_headers=updated_headers, ping_interval=5 + ) \ No newline at end of file diff --git a/deepgram/clients/live/v1_response.py b/deepgram/clients/live/v1_response.py new file mode 100644 index 00000000..787e89ab --- /dev/null +++ b/deepgram/clients/live/v1_response.py @@ -0,0 +1,150 @@ +# Copyright 2023 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +from dataclasses import dataclass +from dataclasses_json import dataclass_json +from datetime import datetime +from typing import TypedDict, List, Optional, Dict + +# Result Message + +@dataclass_json +@dataclass +class Word: + word: Optional[str] + start: Optional[float] + end: Optional[float] + confidence: Optional[float] + punctuated_word: Optional[str] + + def __getitem__(self, key): + _dict = self.to_dict() + return _dict[key] + +@dataclass_json +@dataclass +class Alternative: + transcript: Optional[str] + confidence: Optional[float] + words: Optional[List[Word]] + + def __getitem__(self, key): + _dict = self.to_dict() + if _dict["words"] is not None: + _dict["words"] = [Word.from_dict(project) for project in _dict["words"]] + return _dict[key] + +@dataclass_json +@dataclass +class Channel: + alternatives: Optional[List[Alternative]] + + def __getitem__(self, key): + _dict = self.to_dict() + if _dict["alternatives"] is not None: + _dict["alternatives"] = [Alternative.from_dict(project) for project in _dict["alternatives"]] + return _dict[key] + +@dataclass_json +@dataclass +class ModelInfo: + name: Optional[str] + version: Optional[str] + arch: Optional[str] + + def __getitem__(self, key): + _dict = self.to_dict() + return _dict[key] + +@dataclass_json +@dataclass +class Metadata: + request_id: Optional[str] + model_info: Optional[ModelInfo] + model_uuid: Optional[str] + + def __getitem__(self, key): + _dict = self.to_dict() + if _dict["model_info"] is not None: + _dict["model_info"] = [ModelInfo.from_dict(project) for project in _dict["model_info"]] + return _dict[key] + +@dataclass_json +@dataclass +class LiveResultResponse: + type: Optional[str] + channel_index: Optional[List[int]] + duration: Optional[float] + start: Optional[float] + is_final: Optional[bool] + speech_final: Optional[bool] + channel: Optional[Channel] + metadata: Optional[Metadata] + + def __getitem__(self, key): + _dict = self.to_dict() + if _dict["channel"] is not None: + _dict["channel"] = [Channel.from_dict(project) for project in _dict["channel"]] + if _dict["metadata"] is not None: + _dict["metadata"] = [Metadata.from_dict(project) for project in _dict["metadata"]] + return _dict[key] + +# Metadata Message + +@dataclass_json +@dataclass +class ModelInfo: + name: Optional[str] + version: Optional[str] + arch: Optional[str] + + def __getitem__(self, key): + _dict = self.to_dict() + return _dict[key] + +@dataclass_json +@dataclass +class MetadataResponse: + type: Optional[str] + transaction_key: Optional[str] + request_id: Optional[str] + sha256: Optional[str] + created: Optional[str] + duration: Optional[float] + channels: Optional[int] + models: Optional[List[str]] + model_info: Optional[Dict[str, ModelInfo]] + + def __init__(self, type: str, transaction_key: str, request_id: str, sha256: str, created: datetime, duration: float, channels: int, models: List[str], model_info: Dict[str, ModelInfo]) -> None: + self.type = type + self.transaction_key = transaction_key + self.request_id = request_id + self.sha256 = sha256 + self.created = created + self.duration = duration + self.channels = channels + self.models = models + self.model_info = model_info + + def __getitem__(self, key): + _dict = self.to_dict() + # TODO: fix this + # if _dict["model_info"] is not None: + # _dict["model_info"] = [ModelInfo.from_dict(value) for value in _dict["model_info"]] + return _dict[key] + +# Error Message + +@dataclass_json +@dataclass +class ErrorResponse: + description: Optional[str] + message: Optional[str] + type: Optional[str] + variant: Optional[str] + + def __getitem__(self, key): + _dict = self.to_dict() + return _dict[key] + \ No newline at end of file diff --git a/deepgram/clients/manage/client.py b/deepgram/clients/manage/client.py index 0d28cce8..6d924f12 100644 --- a/deepgram/clients/manage/client.py +++ b/deepgram/clients/manage/client.py @@ -34,5 +34,5 @@ class ManageClient(ManageClientV1): """ Please see ManageClientV1 for details """ - def __init__(self, url, headers): - super().__init__(url, headers) + def __init__(self, config): + super().__init__(config) diff --git a/deepgram/clients/manage/v1_client.py b/deepgram/clients/manage/v1_client.py index 0cef46eb..2f60d103 100644 --- a/deepgram/clients/manage/v1_client.py +++ b/deepgram/clients/manage/v1_client.py @@ -4,6 +4,7 @@ from .v1_response import Project, ProjectsResponse, Message, ProjectOptionsV1, KeysResponse, KeyResponse, KeyOptionsV1, Key, MembersResponse, ScopesResponse, ScopeOptionsV1, InvitesResponse, InviteOptionsV1, UsageRequestsResponse, UsageRequestOptionsV1, UsageRequest, UsageSummaryOptionsV1, UsageSummaryResponse, UsageFieldsResponse, UsageFieldsOptionsV1, BalancesResponse, Balance +from ...options import DeepgramClientOptions from ..abstract_client import AbstractRestfulClient class ManageClientV1(AbstractRestfulClient): @@ -18,8 +19,7 @@ class ManageClientV1(AbstractRestfulClient): - Monitoring project usage and balances Args: - url (str): The base URL of the Deepgram API. - headers (dict): Optional HTTP headers to include in requests. + config (DeepgramClientOptions): all the options for the client. Attributes: url (str): The base URL of the Deepgram API. @@ -31,87 +31,86 @@ class ManageClientV1(AbstractRestfulClient): DeepgramUnknownApiError: Raised for unknown API errors. Exception: For any other unexpected exceptions. """ - def __init__(self, url, headers): - self.url = url - self.headers = headers + def __init__(self, config : DeepgramClientOptions): + self.config = config self.endpoint = "v1/projects" - super().__init__(url, headers) + super().__init__(config) # projects async def list_projects(self): return self.get_projects() async def get_projects(self): - url = f"{self.url}/{self.endpoint}" + url = f"{self.config.url}/{self.endpoint}" return ProjectsResponse.from_json(await self.get(url)) async def get_project(self, project_id: str): - url = f"{self.url}/{self.endpoint}/{project_id}" + url = f"{self.config.url}/{self.endpoint}/{project_id}" return Project.from_json(await self.get(url)) async def update_project_option(self, project_id: str, options: ProjectOptionsV1): - url = f"{self.url}/{self.endpoint}/{project_id}" + url = f"{self.config.url}/{self.endpoint}/{project_id}" return Message.from_json(await self.patch(url, json=options)) async def update_project(self, project_id: str, name=""): - url = f"{self.url}/{self.endpoint}/{project_id}" + url = f"{self.config.url}/{self.endpoint}/{project_id}" options: ProjectOptionsV1 = { "name": name, } return Message.from_json(await self.patch(url, json=options)) async def delete_project(self, project_id: str) -> None: - url = f"{self.url}/{self.endpoint}/{project_id}" + url = f"{self.config.url}/{self.endpoint}/{project_id}" return Message.from_json(await self.delete(url)) # keys async def list_keys(self, project_id: str): return self.get_keys(project_id) async def get_keys(self, project_id: str): - url = f"{self.url}/{self.endpoint}/{project_id}/keys" + url = f"{self.config.url}/{self.endpoint}/{project_id}/keys" result = await self.get(url) return KeysResponse.from_json(result) async def get_key(self, project_id: str, key_id: str): - url = f"{self.url}/{self.endpoint}/{project_id}/keys/{key_id}" + url = f"{self.config.url}/{self.endpoint}/{project_id}/keys/{key_id}" return KeyResponse.from_json(await self.get(url)) async def create_key(self, project_id: str, options: KeyOptionsV1): - url = f"{self.url}/{self.endpoint}/{project_id}/keys" + url = f"{self.config.url}/{self.endpoint}/{project_id}/keys" return Key.from_json(await self.post(url, json=options)) async def delete_key(self, project_id: str, key_id: str) -> None: - url = f"{self.url}/{self.endpoint}/{project_id}/keys/{key_id}" + url = f"{self.config.url}/{self.endpoint}/{project_id}/keys/{key_id}" return Message.from_json(await self.delete(url)) # members async def get_members(self, project_id: str): - url = f"{self.url}/{self.endpoint}/{project_id}/members" + url = f"{self.config.url}/{self.endpoint}/{project_id}/members" return MembersResponse.from_json(await self.get(url)) async def remove_member(self, project_id: str, member_id: str) -> None: - url = f"{self.url}/{self.endpoint}/{project_id}/members/{member_id}" + url = f"{self.config.url}/{self.endpoint}/{project_id}/members/{member_id}" return Message.from_json(await self.delete(url)) # scopes async def get_member_scopes(self, project_id: str, member_id: str): - url = f"{self.url}/{self.endpoint}/{project_id}/members/{member_id}/scopes" + url = f"{self.config.url}/{self.endpoint}/{project_id}/members/{member_id}/scopes" return ScopesResponse.from_json(await self.get(url)) async def update_member_scope(self, project_id: str, member_id: str, options: ScopeOptionsV1): - url = f"{self.url}/{self.endpoint}/{project_id}/members/{member_id}/scopes" + url = f"{self.config.url}/{self.endpoint}/{project_id}/members/{member_id}/scopes" return Message.from_json(await self.put(url, json=options)) # invites async def list_invites(self, project_id: str): return self.get_invites(project_id) async def get_invites(self, project_id: str): - url = f"{self.url}/{self.endpoint}/{project_id}/invites" + url = f"{self.config.url}/{self.endpoint}/{project_id}/invites" return InvitesResponse.from_json(await self.get(url)) async def send_invite_options(self, project_id: str, options: InviteOptionsV1): - url = f"{self.url}/{self.endpoint}/{project_id}/invites" + url = f"{self.config.url}/{self.endpoint}/{project_id}/invites" return Message.from_json(await self.post(url, json=options)) async def send_invite(self, project_id: str, email: str, scope="member"): - url = f"{self.url}/{self.endpoint}/{project_id}/invites" + url = f"{self.config.url}/{self.endpoint}/{project_id}/invites" options: InviteOptionsV1 = { "email": email, "scope": scope, @@ -119,37 +118,37 @@ async def send_invite(self, project_id: str, email: str, scope="member"): return Message.from_json(await self.post(url, json=options)) async def delete_invite(self, project_id: str, email: str): - url = f"{self.url}/{self.endpoint}/{project_id}/invites/{email}" + url = f"{self.config.url}/{self.endpoint}/{project_id}/invites/{email}" return Message.from_json(await self.delete(url)) async def leave_project(self, project_id: str): - url = f"{self.url}/{self.endpoint}/{project_id}/leave" + url = f"{self.config.url}/{self.endpoint}/{project_id}/leave" return Message.from_json(await self.delete(url)) # usage async def get_usage_requests(self, project_id: str, options: UsageRequestOptionsV1): - url = f"{self.url}/{self.endpoint}/{project_id}/requests" + url = f"{self.config.url}/{self.endpoint}/{project_id}/requests" return UsageRequestsResponse.from_json(await self.get(url, options)) async def get_usage_request(self, project_id: str, request_id: str): - url = f"{self.url}/{self.endpoint}/{project_id}/requests/{request_id}" + url = f"{self.config.url}/{self.endpoint}/{project_id}/requests/{request_id}" return UsageRequest.from_json(await self.get(url)) async def get_usage_summary(self, project_id: str, options: UsageSummaryOptionsV1): - url = f"{self.url}/{self.endpoint}/{project_id}/usage" + url = f"{self.config.url}/{self.endpoint}/{project_id}/usage" return UsageSummaryResponse.from_json(await self.get(url, options)) async def get_usage_fields(self, project_id: str, options: UsageFieldsOptionsV1): - url = f"{self.url}/{self.endpoint}/{project_id}/usage/fields" + url = f"{self.config.url}/{self.endpoint}/{project_id}/usage/fields" return UsageFieldsResponse.from_json(await self.get(url, options)) # balances async def list_balances(self, project_id: str): return self.get_balances(project_id) async def get_balances(self, project_id: str): - url = f"{self.url}/{self.endpoint}/{project_id}/balances" + url = f"{self.config.url}/{self.endpoint}/{project_id}/balances" return BalancesResponse.from_json(await self.get(url)) async def get_balance(self, project_id: str, balance_id: str): - url = f"{self.url}/{self.endpoint}/{project_id}/balances/{balance_id}" + url = f"{self.config.url}/{self.endpoint}/{project_id}/balances/{balance_id}" return Balance.from_json(await self.get(url)) diff --git a/deepgram/clients/manage/v1_response.py b/deepgram/clients/manage/v1_response.py index facbf725..06cf6d8a 100644 --- a/deepgram/clients/manage/v1_response.py +++ b/deepgram/clients/manage/v1_response.py @@ -39,7 +39,6 @@ def __getitem__(self, key): _dict = self.to_dict() if _dict["projects"] is not None: _dict["projects"] = [Project.from_dict(project) for project in _dict["projects"]] - _dict["projects"] = [Project.from_dict(project) for project in _dict["projects"]] return _dict[key] class ProjectOptionsV1(TypedDict, total=False): diff --git a/deepgram/clients/onprem/client.py b/deepgram/clients/onprem/client.py index 1a8cdcbd..9072eadc 100644 --- a/deepgram/clients/onprem/client.py +++ b/deepgram/clients/onprem/client.py @@ -12,5 +12,5 @@ class OnPremClient(OnPremClientV1): """ Please see OnPremClientV1 for details """ - def __init__(self, url, headers): - super().__init__(url, headers) + def __init__(self, config): + super().__init__(config) diff --git a/deepgram/clients/onprem/v1_client.py b/deepgram/clients/onprem/v1_client.py index 75ddef31..c5b9eb23 100644 --- a/deepgram/clients/onprem/v1_client.py +++ b/deepgram/clients/onprem/v1_client.py @@ -11,8 +11,7 @@ class OnPremClientV1(AbstractRestfulClient): This class provides methods to manage and interact with on-premises projects and distribution credentials. Args: - url (str): The base URL for API requests. - headers (dict): Additional HTTP headers for API requests. + config (DeepgramClientOptions): all the options for the client. Attributes: endpoint (str): The API endpoint for on-premises projects. @@ -24,26 +23,25 @@ class OnPremClientV1(AbstractRestfulClient): delete_onprem_credentials: Deletes an on-premises distribution credential for a project. """ - def __init__(self, url, headers): - self.url = url - self.headers = headers + def __init__(self, config): + self.config = config self.endpoint = "v1/projects" - super().__init__(url, headers) + super().__init__(config) async def list_onprem_credentials(self, project_id: str): - url = f"{self.url}/{self.endpoint}/{project_id}/onprem/distribution/credentials" + url = f"{self.config.url}/{self.endpoint}/{project_id}/onprem/distribution/credentials" return await self.get(url) async def get_onprem_credentials(self, project_id: str, distribution_credentials_id: str): - url = f"{self.url}/{self.endpoint}/{project_id}/onprem/distribution/credentials/{distribution_credentials_id}" + url = f"{self.config.url}/{self.endpoint}/{project_id}/onprem/distribution/credentials/{distribution_credentials_id}" return await self.get(url) async def create_onprem_credentials(self, project_id: str, options): - url = f"{self.url}/{self.endpoint}/{project_id}/onprem/distribution/credentials/" + url = f"{self.config.url}/{self.endpoint}/{project_id}/onprem/distribution/credentials/" return await self.post(url,json=options) async def delete_onprem_credentials(self, project_id: str, distribution_credentials_id: str): - url = f"{self.url}/{self.endpoint}/{project_id}/onprem/distribution/credentials/{distribution_credentials_id}" + url = f"{self.config.url}/{self.endpoint}/{project_id}/onprem/distribution/credentials/{distribution_credentials_id}" return await self.delete(url) diff --git a/deepgram/clients/prerecorded/client.py b/deepgram/clients/prerecorded/client.py index 898661f9..fc528f7e 100644 --- a/deepgram/clients/prerecorded/client.py +++ b/deepgram/clients/prerecorded/client.py @@ -19,7 +19,6 @@ class PreRecordedClient(PreRecordedClientV1): """ Please see PreRecordedClientV1 for details """ - def __init__(self, url, headers): - self.url = url - self.headers = headers - super().__init__(url, headers) + def __init__(self, config): + self.config = config + super().__init__(config) diff --git a/deepgram/clients/prerecorded/errors.py b/deepgram/clients/prerecorded/errors.py new file mode 100644 index 00000000..94d2abb7 --- /dev/null +++ b/deepgram/clients/prerecorded/errors.py @@ -0,0 +1,19 @@ +# Copyright 2023 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +class DeepgramTypeError(Exception): + """ + Exception raised for unknown errors related to unknown Types for Transcription. + + Attributes: + message (str): The error message describing the exception. + status (str): The HTTP status associated with the API error. + """ + def __init__(self, message: str): + super().__init__(message) + self.name = "DeepgramTypeError" + self.message = message + + def __str__(self): + return f"{self.name}: {self.message}" \ No newline at end of file diff --git a/deepgram/clients/prerecorded/v1_client.py b/deepgram/clients/prerecorded/v1_client.py index 8f585150..dc3a9c62 100644 --- a/deepgram/clients/prerecorded/v1_client.py +++ b/deepgram/clients/prerecorded/v1_client.py @@ -2,33 +2,31 @@ # Use of this source code is governed by a MIT license that can be found in the LICENSE file. # SPDX-License-Identifier: MIT -from ...errors import DeepgramError +from .errors import DeepgramTypeError from ..abstract_client import AbstractRestfulClient from .helpers import is_buffer_source, is_readstream_source, is_url_source from .source import UrlSource, FileSource from .v1_options import PrerecordedOptionsV1 -from .v1_response import AsyncPrerecordedResponseV1, SyncPrerecordedResponseV1 +from .v1_response import AsyncPrerecordedResponseV1, PrerecordedResponseV1 class PreRecordedClientV1(AbstractRestfulClient): """ A client class for handling pre-recorded audio data. Provides methods for transcribing audio from URLs and files. """ - def __init__(self, url, headers): + def __init__(self, config): """ Initializes a new instance of the PreRecordedClient. Args: - url (str): The URL for API requests. - headers (dict): Headers to include in API requests. + config (DeepgramClientOptions): all the options for the client. """ - self.url = url - self.headers = headers - super().__init__(url, headers) + self.config = config + super().__init__(config) async def transcribe_url( self, source: UrlSource, options: PrerecordedOptionsV1 = None, endpoint: str="v1/listen" - ) -> SyncPrerecordedResponseV1: + ) -> PrerecordedResponseV1: """ Transcribes audio from a URL source. @@ -41,19 +39,18 @@ async def transcribe_url( SyncPrerecordedResponse: An object containing the transcription result. Raises: - DeepgramError: If the "callback" option is provided for a synchronous transcription. DeepgramApiError: Raised for known API errors. DeepgramUnknownApiError: Raised for unknown API errors. Exception: For any other unexpected exceptions. """ - url = f"{self.url}/{endpoint}" + url = f"{self.config.url}/{endpoint}" if options is not None and "callback" in options: - raise DeepgramError("Callback cannot be provided as an option to a synchronous transcription. Use `transcribe_url_callback` instead.") + return await self.transcribe_url_callback(source, options["callback"], options, endpoint) if is_url_source(source): body = source else: - raise DeepgramError("Unknown transcription source type") + raise DeepgramTypeError("Unknown transcription source type") return await self.post(url, options, json=body) async def transcribe_url_callback( self, source: UrlSource, callback:str, options: PrerecordedOptionsV1 = None, endpoint: str="v1/listen") -> AsyncPrerecordedResponseV1: @@ -75,18 +72,18 @@ async def transcribe_url_callback( self, source: UrlSource, callback:str, option DeepgramUnknownApiError: Raised for unknown API errors. Exception: For any other unexpected exceptions. """ - url = f"{self.url}/{endpoint}" + url = f"{self.config.url}/{endpoint}" if options is None: options = {} options['callback'] = callback if is_url_source(source): body = source else: - raise DeepgramError("Unknown transcription source type") + raise DeepgramTypeError("Unknown transcription source type") return await self.post(url, options, json=body) - async def transcribe_file(self, source: FileSource, options: PrerecordedOptionsV1=None, endpoint: str = "v1/listen") -> SyncPrerecordedResponseV1: + async def transcribe_file(self, source: FileSource, options: PrerecordedOptionsV1=None, endpoint: str = "v1/listen") -> PrerecordedResponseV1: """ Transcribes audio from a local file source. @@ -99,19 +96,19 @@ async def transcribe_file(self, source: FileSource, options: PrerecordedOptionsV SyncPrerecordedResponse: An object containing the transcription result or an error message. Raises: - DeepgramError: If the "callback" option is provided for a synchronous transcription. + DeepgramApiError: Raised for known API errors. DeepgramUnknownApiError: Raised for unknown API errors. Exception: For any other unexpected exceptions. """ - url = f"{self.url}/{endpoint}" + url = f"{self.config.url}/{endpoint}" if is_buffer_source(source): body = source["buffer"] elif is_readstream_source(source): body = source["stream"] else: - raise DeepgramError("Unknown transcription source type") + raise DeepgramTypeError("Unknown transcription source type") return await self.post(url, options, content=body) async def transcribe_file_callback(self, source: FileSource, callback:str, options: PrerecordedOptionsV1 = None, endpoint: str="v1/listen") -> AsyncPrerecordedResponseV1: @@ -134,7 +131,7 @@ async def transcribe_file_callback(self, source: FileSource, callback:str, optio Exception: For any other unexpected exceptions. """ - url = f"{self.url}/{endpoint}" + url = f"{self.config.url}/{endpoint}" if options is None: options = {} options['callback'] = callback @@ -143,5 +140,5 @@ async def transcribe_file_callback(self, source: FileSource, callback:str, optio elif is_readstream_source(source): body = source["stream"] else: - raise DeepgramError("Unknown transcription source type") + raise DeepgramTypeError("Unknown transcription source type") return await self.post(url, options, content=body) diff --git a/deepgram/clients/prerecorded/v1_response.py b/deepgram/clients/prerecorded/v1_response.py index add52139..b54b2ec7 100644 --- a/deepgram/clients/prerecorded/v1_response.py +++ b/deepgram/clients/prerecorded/v1_response.py @@ -132,6 +132,6 @@ class Result(TypedDict): utterances: Optional[List[Utterance]] summary: Optional[SummaryV1] -class SyncPrerecordedResponseV1(TypedDict): +class PrerecordedResponseV1(TypedDict): metadata: Optional[Metadata] results: Optional[Result] diff --git a/deepgram/errors.py b/deepgram/errors.py index 89463dea..754216ea 100644 --- a/deepgram/errors.py +++ b/deepgram/errors.py @@ -2,50 +2,13 @@ # Use of this source code is governed by a MIT license that can be found in the LICENSE file. # SPDX-License-Identifier: MIT -class DeepgramError(Exception): +class DeepgramApiKeyError(Exception): """ - Base class for exceptions raised by the Deepgram API client. + Base class for exceptions raised for a missing Deepgram API Key. Attributes: message (str): The error message describing the exception. """ def __init__(self, message: str): super().__init__(message) - self.name = "DeepgramError" - -class DeepgramApiError(Exception): - """ - Exception raised for known errors (in json response format) related to the Deepgram API. - - Attributes: - message (str): The error message describing the exception. - status (str): The HTTP status associated with the API error. - """ - def __init__(self, message: str, status: str, original_error = None): - super().__init__(message) - self.name = "DeepgramApiError" - self.status = status - self.message = message - self.original_error = original_error - - def __str__(self): - return f"{self.name}: {self.message} (Status: {self.status})" - -class DeepgramUnknownApiError(Exception): - """ - Exception raised for unknown errors related to the Deepgram API. - - Inherits from DeepgramApiError and includes the same attributes. - - Attributes: - message (str): The error message describing the exception. - status (str): The HTTP status associated with the API error. - """ - def __init__(self, message: str, status: str): - super().__init__(message, status) - self.name = "DeepgramUnknownApiError" - self.status = status - self.message = message - - def __str__(self): - return f"{self.name}: {self.message} (Status: {self.status})" \ No newline at end of file + self.name = "DeepgramApiKeyError" diff --git a/deepgram/options.py b/deepgram/options.py index ae13cc99..85d67fb9 100644 --- a/deepgram/options.py +++ b/deepgram/options.py @@ -2,6 +2,9 @@ # Use of this source code is governed by a MIT license that can be found in the LICENSE file. # SPDX-License-Identifier: MIT +from typing import Dict +import re + class DeepgramClientOptions: """ @@ -16,12 +19,33 @@ class DeepgramClientOptions: - url (str): The URL used to interact with production, On-prem, and other Deepgram environments. Defaults to `api.deepgram.com`. """ - def __init__(self, api_key): + def __init__(self, api_key: str = "", url: str = "", headers: Dict[str, str] = None, options: Dict[str, str] = None): self.api_key = api_key - self.global_options = { - "headers": { + if headers is None: + self.headers = { + "Accept": "application/json", + "Authorization": f"Token {self.api_key}" + } + else: + self.headers.update({ "Accept": "application/json", "Authorization": f"Token {self.api_key}" - }, - "url": "api.deepgram.com" - } \ No newline at end of file + }) + if len(url) == 0: + url = "api.deepgram.com" + self.url = self._get_url(url) + if options is None: + options = dict() + self.options = options + + def set_apikey(self, api_key: str): + self.api_key = api_key + self.headers.update({ + "Accept": "application/json", + "Authorization": f"Token {self.api_key}" + }) + + def _get_url(self, url): + if not re.match(r'^https?://', url, re.IGNORECASE): + url = 'https://' + url + return url.strip('/') \ No newline at end of file diff --git a/examples/.env_example b/examples/.env_example deleted file mode 100644 index 955e8100..00000000 --- a/examples/.env_example +++ /dev/null @@ -1,8 +0,0 @@ -DG_API_KEY="" -DG_API_KEY_MANAGE="" -DG_PROJECT_ID="" -DG_API_KEY_ID="" -DG_MEMBER_ID="" -DG_REQUEST_ID="" -DG_BALANCE_ID="" -EMAIL="" \ No newline at end of file diff --git a/examples/streaming/http/main.py b/examples/streaming/http/main.py index 3297de06..9cd66be8 100644 --- a/examples/streaming/http/main.py +++ b/examples/streaming/http/main.py @@ -28,7 +28,7 @@ async def main(): # Create a websocket connection to Deepgram try: - dg_connection = await deepgram.listen.live(options) + dg_connection = await deepgram.listen.legacylive(options) except Exception as e: print(f'Could not open socket: {e}') return @@ -42,7 +42,6 @@ async def main(): # Listen for the connection to close dg_connection.on(LiveTranscriptionEvents.Close, lambda c: print(f'Connection closed with code {c}.')) - # Send streaming audio from the URL to Deepgram async with aiohttp.ClientSession() as session: async with session.get(URL) as audio: diff --git a/examples/streaming/microphone/main.py b/examples/streaming/microphone/main.py new file mode 100644 index 00000000..5eb20e0a --- /dev/null +++ b/examples/streaming/microphone/main.py @@ -0,0 +1,80 @@ +# Copyright 2023 Deepgram SDK contributors. All Rights Reserved. +# Use of this source code is governed by a MIT license that can be found in the LICENSE file. +# SPDX-License-Identifier: MIT + +import os +from dotenv import load_dotenv + +from deepgram import DeepgramClient, DeepgramClientOptions, LiveTranscriptionEvents, LiveOptions, Microphone + +load_dotenv() + +options: LiveOptions = { + 'punctuate': True, + 'language': 'en-US', + 'encoding': 'linear16', + 'channels': 1, + 'sample_rate': 16000, +} + +deepgram_api_key = os.getenv('DG_API_KEY') + +def on_message(result=None): + if result is None: + return + sentence = result.channel.alternatives[0].transcript + if len(sentence) == 0: + return + print(f"speaker: {sentence}") + +def on_metadata(metadata=None): + if metadata is None: + return + print("") + print(metadata) + print("") + +def on_error(error=None): + if error is None: + return + print("") + print(error) + print("") + +def main(): + + # config: DeepgramClientOptions = DeepgramClientOptions(options={'keepalive': 'true'}) + deepgram: DeepgramClient = DeepgramClient(deepgram_api_key) + + try: + # Create a websocket connection to Deepgram + dg_connection = deepgram.listen.live(options) + dg_connection.start() + + dg_connection.on(LiveTranscriptionEvents.Transcript, on_message) + dg_connection.on(LiveTranscriptionEvents.Metadata, on_metadata) + dg_connection.on(LiveTranscriptionEvents.Error, on_error) + + # Open a microphone stream + microphone = Microphone(dg_connection.send) + + # start microphone + microphone.start() + + # wait until finished + input("Press Enter to stop recording...\n\n") + + # Wait for the connection to close + microphone.finish() + + # Indicate that we've finished sending data by sending the {"type": "CloseStream"} + dg_connection.finish() + + print("Finished") + + except Exception as e: + print(f'Could not open socket: {e}') + return + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 26f8f14c..5634373d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,8 @@ build-backend = "setuptools.build_meta" [project] name = "deepgram-sdk" -dynamic = ["version"] +dynamic = ["version", "readme", "license", "description", "authors", "keywords", "classifiers", "dependencies"] [tool.setuptools.dynamic] version = {attr = "deepgram.__version__"} +readme = {file = "README.md"} diff --git a/requirements.txt b/requirements.txt index 70e36040..2faff566 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ # pip install -r requirements.txt # standard libs +websockets httpx dataclasses-json dataclasses diff --git a/setup.py b/setup.py index 029eeeb1..8d63fcfd 100644 --- a/setup.py +++ b/setup.py @@ -5,13 +5,8 @@ import setuptools import os.path -with open(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'README.md'), encoding="utf8") as file: - long_description = file.read() - setuptools.setup( description='The official Python SDK for the Deepgram automated speech recognition platform.', - long_description=long_description, - long_description_content_type='text/markdown', license='MIT', url='https://github.com/deepgram/deepgram-python-sdk', author='Luca Todd', @@ -23,8 +18,14 @@ keywords='deepgram speech-to-text', packages=setuptools.find_packages(), install_requires=[ - 'aiohttp', + 'httpx', 'websockets', - 'typing-extensions; python_version < "3.8.0"' + 'typing-extensions; python_version < "3.8.0"', + 'dataclasses-json', + 'dataclasses', + 'typing_extensions', + 'python-dotenv', + 'asyncio', + 'aiohttp' ], ) \ No newline at end of file