diff --git a/Makefile b/Makefile index 0696486e..6a838ff4 100644 --- a/Makefile +++ b/Makefile @@ -43,9 +43,6 @@ ensure-deps: #### Ensure that all required dependency utilities are downloaded o GO_MODULES=$(shell find . -path "*/go.mod" | xargs -I _ dirname _) -# pystatic: #### Performs Python static analysis -# pylint --rcfile .pylintrc deepgram - PYTHON_FILES=. lint_files: PYTHON_FILES=deepgram/ examples/ lint_diff: PYTHON_FILES=$(shell git diff --name-only --diff-filter=d main | grep -E '\.py$$') @@ -56,7 +53,7 @@ lint_files lint_diff: #### Performs Python formatting black blackformat format: lint_files pylint: lint_files #### Performs Python linting - pylint --rcfile .pylintrc deepgram + pylint --disable=W0622 --disable=W0404 --disable=W0611 --rcfile .pylintrc deepgram lint: pylint #### Performs Golang programming lint diff --git a/deepgram/__init__.py b/deepgram/__init__.py index 219a8c11..040e82df 100644 --- a/deepgram/__init__.py +++ b/deepgram/__init__.py @@ -38,20 +38,27 @@ UrlSource, Sentiment, ) +from .client import ( + OpenResponse, + MetadataResponse, + CloseResponse, + UnhandledResponse, + ErrorResponse, +) # live from .client import LiveTranscriptionEvents from .client import LiveClient, AsyncLiveClient from .client import LiveOptions from .client import ( - OpenResponse, + # OpenResponse, LiveResultResponse, - MetadataResponse, + # MetadataResponse, SpeechStartedResponse, UtteranceEndResponse, - CloseResponse, - UnhandledResponse, - ErrorResponse, + # CloseResponse, + # UnhandledResponse, + # ErrorResponse, ) # prerecorded @@ -82,11 +89,41 @@ ) # speak -from .client import SpeakClient, AsyncSpeakClient -from .client import SpeakStreamClient, AsyncSpeakStreamClient -from .client import SpeakOptions, SpeakStreamSource, SpeakSource -from .client import SpeakResponse -from .client import SpeakStreamEvents +from .client import ( + SpeakOptions, + # FileSource, + SpeakWebSocketSource, + SpeakSource, +) +from .client import SpeakWebSocketEvents + +## speak REST +from .client import ( + SpeakClient, # backward compat + SpeakRESTClient, + AsyncSpeakRESTClient, +) + +from .client import ( + SpeakResponse, # backward compat + SpeakRESTResponse, +) + +## speak WebSocket +from .client import ( + SpeakWebSocketClient, + AsyncSpeakWebSocketClient, +) +from .client import ( + SpeakWebSocketResponse, + # OpenResponse, + # MetadataResponse, + FlushedResponse, + # CloseResponse, + # UnhandledResponse, + WarningResponse, + # ErrorResponse, +) # manage from .client import ManageClient, AsyncManageClient diff --git a/deepgram/client.py b/deepgram/client.py index 77fb2781..50cc2449 100644 --- a/deepgram/client.py +++ b/deepgram/client.py @@ -5,8 +5,10 @@ from typing import Optional from importlib import import_module import os - import logging +import deprecation # type: ignore + +from . import __version__ from .utils import verboselogs # common @@ -19,9 +21,16 @@ UrlSource, Sentiment, ) +from .clients import ( + OpenResponse, + MetadataResponse, + CloseResponse, + UnhandledResponse, + ErrorResponse, +) # listen client -from .clients import Listen, Read +from .clients import Listen, Read, Speak # live from .clients import LiveClient, AsyncLiveClient @@ -32,14 +41,14 @@ # live client responses from .clients import ( - OpenResponse, + # OpenResponse, LiveResultResponse, - MetadataResponse, + # MetadataResponse, SpeechStartedResponse, UtteranceEndResponse, - CloseResponse, - ErrorResponse, - UnhandledResponse, + # CloseResponse, + # ErrorResponse, + # UnhandledResponse, ) # prerecorded @@ -76,15 +85,42 @@ SyncAnalyzeResponse, ) -# speak client classes/input -from .clients import SpeakClient, AsyncSpeakClient -from .clients import SpeakStreamClient, AsyncSpeakStreamClient -from .clients import SpeakOptions -from .clients import SpeakStreamSource, SpeakSource -from .clients import SpeakStreamEvents +# speak +from .clients import ( + SpeakOptions, + # FileSource, + SpeakWebSocketSource, + SpeakSource, +) +from .clients import SpeakWebSocketEvents + +## speak REST +from .clients import ( + SpeakClient, # backward compat + SpeakRESTClient, + AsyncSpeakRESTClient, +) + +from .clients import ( + SpeakResponse, # backward compat + SpeakRESTResponse, +) -# speak client responses -from .clients import SpeakResponse +## speak WebSocket +from .clients import ( + SpeakWebSocketClient, + AsyncSpeakWebSocketClient, +) +from .clients import ( + SpeakWebSocketResponse, + # OpenResponse, + # MetadataResponse, + FlushedResponse, + # CloseResponse, + # UnhandledResponse, + WarningResponse, + # ErrorResponse, +) # manage client classes/input from .clients import ManageClient, AsyncManageClient @@ -228,29 +264,22 @@ def speak(self): """ Returns a SpeakClient instance for interacting with Deepgram's speak services. """ - return self.Version(self._config, "speak") + return Speak(self._config) @property + @deprecation.deprecated( + deprecated_in="3.4.0", + removed_in="4.0.0", + current_version=__version__, + details="deepgram.asyncspeak is deprecated. Use deepgram.speak.asyncrest instead.", + ) def asyncspeak(self): """ - Returns an AsyncSpeakClient instance for interacting with Deepgram's speak services. + DEPRECATED: deepgram.asyncspeak is deprecated. Use deepgram.speak.asyncrest instead. """ return self.Version(self._config, "asyncspeak") @property - def speakstream(self): - """ - Returns a SpeakStreamClient instance for interacting with Deepgram's speak services. - """ - return self.Version(self._config, "speak-stream") - - @property - def asyncspeakstream(self): - """ - Returns an AsyncSpeakStreamClient instance for interacting with Deepgram's speak services. - """ - return self.Version(self._config, "asyncspeak-stream") - @property def manage(self): """ Returns a ManageClient instance for managing Deepgram resources. @@ -264,11 +293,16 @@ def asyncmanage(self): """ return self.Version(self._config, "asyncmanage") - # for backwards compatibility @property + @deprecation.deprecated( + deprecated_in="3.4.0", + removed_in="4.0.0", + current_version=__version__, + details="deepgram.onprem is deprecated. Use deepgram.speak.selfhosted instead.", + ) def onprem(self): """ - Returns an SelfHostedClient instance for interacting with Deepgram's on-premises API. + DEPRECATED: deepgram.onprem is deprecated. Use deepgram.speak.selfhosted instead. """ return self.Version(self._config, "selfhosted") @@ -279,11 +313,16 @@ def selfhosted(self): """ return self.Version(self._config, "selfhosted") - # for backwards compatibility @property + @deprecation.deprecated( + deprecated_in="3.4.0", + removed_in="4.0.0", + current_version=__version__, + details="deepgram.asynconprem is deprecated. Use deepgram.speak.asyncselfhosted instead.", + ) def asynconprem(self): """ - Returns an AsyncSelfHostedClient instance for interacting with Deepgram's on-premises API. + DEPRECATED: deepgram.asynconprem is deprecated. Use deepgram.speak.asyncselfhosted instead. """ return self.Version(self._config, "asyncselfhosted") @@ -348,22 +387,8 @@ def v(self, version: str = ""): parent = "manage" filename = "async_client" classname = "AsyncManageClient" - case "speak": - parent = "speak" - filename = "client" - classname = "SpeakClient" case "asyncspeak": - parent = "speak" - filename = "async_client" - classname = "AsyncSpeakClient" - case "speak-stream": - parent = "speak" - filename = "client_stream" - classname = "SpeakStreamClient" - case "asyncspeak-stream": - parent = "speak" - filename = "async_client_stream" - classname = "AsyncSpeakStreamClient" + return AsyncSpeakRESTClient(self._config) case "selfhosted": parent = "selfhosted" filename = "client" @@ -400,4 +425,5 @@ def v(self, version: str = ""): self._logger.notice("Version.v succeeded") self._logger.debug("Version.v LEAVE") return my_class_instance + # pylint: enable-msg=too-many-statements diff --git a/deepgram/clients/__init__.py b/deepgram/clients/__init__.py index a89fab48..11f59400 100644 --- a/deepgram/clients/__init__.py +++ b/deepgram/clients/__init__.py @@ -11,39 +11,74 @@ UrlSource, Sentiment, ) - -# listen -from .listen import Listen -from .read import Read - -# live -from .live import LiveClient, AsyncLiveClient -from .live import LiveOptions -from .live import LiveTranscriptionEvents -from .live import ( +from .listen import ( OpenResponse, - LiveResultResponse, MetadataResponse, - SpeechStartedResponse, - UtteranceEndResponse, CloseResponse, - ErrorResponse, UnhandledResponse, + ErrorResponse, ) -# prerecorded -from .prerecorded import PreRecordedClient, AsyncPreRecordedClient -from .prerecorded import PrerecordedOptions +from .listen_router import Listen +from .read_router import Read +from .speak_router import Speak + +# listen +from .listen import LiveTranscriptionEvents + +## backward compat from .prerecorded import ( + PreRecordedClient, + AsyncPreRecordedClient, +) +from .live import ( + LiveClient, + AsyncLiveClient, +) + +# rest +## input +from .listen import ( + PrerecordedOptions, PreRecordedStreamSource, + # UrlSource, + # FileSource, PrerecordedSource, ) -from .prerecorded import ( + +## output +from .listen import ( AsyncPrerecordedResponse, PrerecordedResponse, SyncPrerecordedResponse, ) + +# websocket +## input +from .listen import ( + LiveOptions, +) + +## output +from .listen import ( + # OpenResponse, + LiveResultResponse, + # MetadataResponse, + SpeechStartedResponse, + UtteranceEndResponse, + # CloseResponse, + # ErrorResponse, + # UnhandledResponse, +) + +## clients +from .listen import ( + ListenWebSocketClient, + AsyncListenWebSocketClient, +) + + # read from .analyze import ReadClient, AsyncReadClient from .analyze import AnalyzeClient, AsyncAnalyzeClient @@ -59,17 +94,43 @@ ) # speak -from .speak import SpeakClient, AsyncSpeakClient -from .speak import SpeakOptions +## common from .speak import ( - SpeakStreamSource, + SpeakOptions, + # FileSource, + SpeakWebSocketSource, SpeakSource, ) -from .speak import SpeakStreamEvents -from .speak import SpeakResponse -# speak-stream -from .speak import SpeakStreamClient, AsyncSpeakStreamClient +from .speak import SpeakWebSocketEvents + +## speak REST +from .speak import ( + SpeakClient, # backward compat + SpeakRESTClient, + AsyncSpeakRESTClient, +) + +from .speak import ( + SpeakResponse, # backward compat + SpeakRESTResponse, +) + +## speak WebSocket +from .speak import ( + SpeakWebSocketClient, + AsyncSpeakWebSocketClient, +) +from .speak import ( + SpeakWebSocketResponse, + # OpenResponse, + # MetadataResponse, + FlushedResponse, + # CloseResponse, + # UnhandledResponse, + WarningResponse, + # ErrorResponse, +) # manage from .manage import ManageClient, AsyncManageClient diff --git a/deepgram/clients/listen/__init__.py b/deepgram/clients/listen/__init__.py new file mode 100644 index 00000000..49faed2e --- /dev/null +++ b/deepgram/clients/listen/__init__.py @@ -0,0 +1,56 @@ +# Copyright 2023-2024 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 .enums import LiveTranscriptionEvents +from ...options import DeepgramClientOptions, ClientOptionsFromEnv + +# backward compat +from .client import ( + PreRecordedClient, + AsyncPreRecordedClient, + LiveClient, + AsyncLiveClient, +) + +# rest +## input +from .client import ( + PrerecordedOptions, + PreRecordedStreamSource, + UrlSource, + FileSource, + PrerecordedSource, +) + +## output +from .client import ( + AsyncPrerecordedResponse, + PrerecordedResponse, + SyncPrerecordedResponse, +) + + +# websocket +## input +from .client import ( + LiveOptions, +) + +## output +from .client import ( + OpenResponse, + LiveResultResponse, + MetadataResponse, + SpeechStartedResponse, + UtteranceEndResponse, + CloseResponse, + ErrorResponse, + UnhandledResponse, +) + +# clients +from .client import ( + ListenWebSocketClient, + AsyncListenWebSocketClient, +) diff --git a/deepgram/clients/listen/client.py b/deepgram/clients/listen/client.py new file mode 100644 index 00000000..34cea6bd --- /dev/null +++ b/deepgram/clients/listen/client.py @@ -0,0 +1,85 @@ +# Copyright 2023-2024 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, ClientOptionsFromEnv + +# rest +from .v1 import ( + ListenRESTClient as ListenRESTClientLatest, + AsyncListenRESTClient as AsyncListenRESTClientLatest, +) +from .v1 import PrerecordedOptions as PrerecordedOptionsLatest + +from .v1 import ( + UrlSource as UrlSourceLatest, + FileSource as FileSourceLatest, + PreRecordedStreamSource as PreRecordedStreamSourceLatest, + PrerecordedSource as PrerecordedSourceLatest, +) +from .v1 import ( + AsyncPrerecordedResponse as AsyncPrerecordedResponseLatest, + PrerecordedResponse as PrerecordedResponseLatest, + SyncPrerecordedResponse as SyncPrerecordedResponseLatest, +) + +# websocket +from .v1 import ( + ListenWebSocketClient as ListenWebSocketClientLatest, + AsyncListenWebSocketClient as AsyncListenWebSocketClientLatest, +) +from .v1 import LiveOptions as LiveOptionsLatest, LiveOptions as SteamingOptionsLatest +from .v1 import ( + OpenResponse as OpenResponseLatest, + LiveResultResponse as LiveResultResponseLatest, + MetadataResponse as MetadataResponseLatest, + SpeechStartedResponse as SpeechStartedResponseLatest, + UtteranceEndResponse as UtteranceEndResponseLatest, + CloseResponse as CloseResponseLatest, + ErrorResponse as ErrorResponseLatest, + UnhandledResponse as UnhandledResponseLatest, +) + +# The vX/client.py points to the current supported version in the SDK. +# Older versions are supported in the SDK for backwards compatibility. + + +# backward compat +PreRecordedClient = ListenRESTClientLatest +AsyncPreRecordedClient = AsyncListenRESTClientLatest +LiveClient = ListenWebSocketClientLatest +AsyncLiveClient = ListenWebSocketClientLatest + +# rest +## input +PrerecordedOptions = PrerecordedOptionsLatest +PreRecordedStreamSource = PreRecordedStreamSourceLatest +UrlSource = UrlSourceLatest +FileSource = FileSourceLatest +PrerecordedSource = PrerecordedSourceLatest + +## output +AsyncPrerecordedResponse = AsyncPrerecordedResponseLatest +PrerecordedResponse = PrerecordedResponseLatest +SyncPrerecordedResponse = SyncPrerecordedResponseLatest + + +# websocket +## input +LiveOptions = LiveOptionsLatest + +## output +OpenResponse = OpenResponseLatest +LiveResultResponse = LiveResultResponseLatest +MetadataResponse = MetadataResponseLatest +SpeechStartedResponse = SpeechStartedResponseLatest +UtteranceEndResponse = UtteranceEndResponseLatest +CloseResponse = CloseResponseLatest +ErrorResponse = ErrorResponseLatest +UnhandledResponse = UnhandledResponseLatest + + +# clients +ListenWebSocketClient = ListenWebSocketClientLatest +AsyncListenWebSocketClient = AsyncListenWebSocketClientLatest diff --git a/deepgram/clients/live/enums.py b/deepgram/clients/listen/enums.py similarity index 100% rename from deepgram/clients/live/enums.py rename to deepgram/clients/listen/enums.py diff --git a/deepgram/clients/live/errors.py b/deepgram/clients/listen/errors.py similarity index 70% rename from deepgram/clients/live/errors.py rename to deepgram/clients/listen/errors.py index 3a498916..b61f7441 100644 --- a/deepgram/clients/live/errors.py +++ b/deepgram/clients/listen/errors.py @@ -20,6 +20,23 @@ def __str__(self): return f"{self.name}: {self.message}" +class DeepgramTypeError(Exception): + """ + Exception raised for unknown errors related to unknown Types for Transcription. + + Attributes: + message (str): The error message describing the exception. + """ + + def __init__(self, message: str): + super().__init__(message) + self.name = "DeepgramTypeError" + self.message = message + + def __str__(self): + return f"{self.name}: {self.message}" + + class DeepgramWebsocketError(Exception): """ Exception raised for known errors related to Websocket library. diff --git a/deepgram/clients/listen/v1/__init__.py b/deepgram/clients/listen/v1/__init__.py new file mode 100644 index 00000000..6806412a --- /dev/null +++ b/deepgram/clients/listen/v1/__init__.py @@ -0,0 +1,44 @@ +# Copyright 2024 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, ClientOptionsFromEnv + +# backward compat +from .rest import ( + ListenRESTClient as PreRecordedClient, + AsyncListenRESTClient as AsyncPreRecordedClient, +) +from .websocket import ( + ListenWebSocketClient as LiveClient, + AsyncListenWebSocketClient as AsyncLiveClient, +) + +# rest +from .rest import ListenRESTClient, AsyncListenRESTClient +from .rest import PrerecordedOptions +from .rest import ( + UrlSource, + FileSource, + PreRecordedStreamSource, + PrerecordedSource, +) +from .rest import ( + AsyncPrerecordedResponse, + PrerecordedResponse, + SyncPrerecordedResponse, +) + +# websocket +from .websocket import ListenWebSocketClient, AsyncListenWebSocketClient +from .websocket import LiveOptions, LiveOptions as SteamingOptions +from .websocket import ( + OpenResponse, + LiveResultResponse, + MetadataResponse, + SpeechStartedResponse, + UtteranceEndResponse, + CloseResponse, + ErrorResponse, + UnhandledResponse, +) diff --git a/deepgram/clients/live/helpers.py b/deepgram/clients/listen/v1/helpers.py similarity index 100% rename from deepgram/clients/live/helpers.py rename to deepgram/clients/listen/v1/helpers.py diff --git a/deepgram/clients/listen/v1/rest/__init__.py b/deepgram/clients/listen/v1/rest/__init__.py new file mode 100644 index 00000000..56bd0d20 --- /dev/null +++ b/deepgram/clients/listen/v1/rest/__init__.py @@ -0,0 +1,21 @@ +# Copyright 2023-2024 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 .client import ListenRESTClient +from .async_client import AsyncListenRESTClient +from .options import ( + PrerecordedOptions, + FileSource, + UrlSource, + PreRecordedStreamSource, + PrerecordedSource, +) +from .response import ( + AsyncPrerecordedResponse, + PrerecordedResponse, + SyncPrerecordedResponse, + Sentiment, +) + +from .....options import DeepgramClientOptions, ClientOptionsFromEnv diff --git a/deepgram/clients/prerecorded/v1/async_client.py b/deepgram/clients/listen/v1/rest/async_client.py similarity index 88% rename from deepgram/clients/prerecorded/v1/async_client.py rename to deepgram/clients/listen/v1/rest/async_client.py index 435ddded..a84f0d33 100644 --- a/deepgram/clients/prerecorded/v1/async_client.py +++ b/deepgram/clients/listen/v1/rest/async_client.py @@ -7,10 +7,10 @@ import httpx -from ....utils import verboselogs -from ....options import DeepgramClientOptions -from ...abstract_async_client import AbstractAsyncRestClient -from ..errors import DeepgramError, DeepgramTypeError +from .....utils import verboselogs +from .....options import DeepgramClientOptions +from ....abstract_async_client import AbstractAsyncRestClient +from ...errors import DeepgramError, DeepgramTypeError from .helpers import is_buffer_source, is_readstream_source, is_url_source from .options import ( @@ -21,7 +21,7 @@ from .response import AsyncPrerecordedResponse, PrerecordedResponse -class AsyncPreRecordedClient(AbstractAsyncRestClient): +class AsyncListenRESTClient(AbstractAsyncRestClient): """ A client class for handling pre-recorded audio data. Provides methods for transcribing audio from URLs and files. @@ -60,14 +60,14 @@ async def transcribe_url( Raises: DeepgramTypeError: Raised for known API errors. """ - self._logger.debug("PreRecordedClient.transcribe_url ENTER") + self._logger.debug("ListenRESTClient.transcribe_url ENTER") if ( isinstance(options, dict) and "callback" in options and options["callback"] is not None ) or (isinstance(options, PrerecordedOptions) and options.callback is not None): - self._logger.debug("PreRecordedClient.transcribe_url LEAVE") + self._logger.debug("ListenRESTClient.transcribe_url LEAVE") return await self.transcribe_url_callback( source, callback=options["callback"], @@ -83,12 +83,12 @@ async def transcribe_url( body = source else: self._logger.error("Unknown transcription source type") - self._logger.debug("PreRecordedClient.transcribe_url LEAVE") + self._logger.debug("ListenRESTClient.transcribe_url LEAVE") raise DeepgramTypeError("Unknown transcription source type") if isinstance(options, PrerecordedOptions) and not options.check(): self._logger.error("options.check failed") - self._logger.debug("PreRecordedClient.transcribe_url LEAVE") + self._logger.debug("ListenRESTClient.transcribe_url LEAVE") raise DeepgramError("Fatal transcription options error") self._logger.info("url: %s", url) @@ -111,7 +111,7 @@ async def transcribe_url( res = PrerecordedResponse.from_json(result) self._logger.verbose("result: %s", res) self._logger.notice("transcribe_url succeeded") - self._logger.debug("PreRecordedClient.transcribe_url LEAVE") + self._logger.debug("ListenRESTClient.transcribe_url LEAVE") return res async def transcribe_url_callback( @@ -139,7 +139,7 @@ async def transcribe_url_callback( Raises: DeepgramTypeError: Raised for known API errors. """ - self._logger.debug("PreRecordedClient.transcribe_url_callback ENTER") + self._logger.debug("ListenRESTClient.transcribe_url_callback ENTER") url = f"{self._config.url}/{endpoint}" if options is None: @@ -152,12 +152,12 @@ async def transcribe_url_callback( body = source else: self._logger.error("Unknown transcription source type") - self._logger.debug("PreRecordedClient.transcribe_url_callback LEAVE") + self._logger.debug("ListenRESTClient.transcribe_url_callback LEAVE") raise DeepgramTypeError("Unknown transcription source type") if isinstance(options, PrerecordedOptions) and not options.check(): self._logger.error("options.check failed") - self._logger.debug("PreRecordedClient.transcribe_url_callback LEAVE") + self._logger.debug("ListenRESTClient.transcribe_url_callback LEAVE") raise DeepgramError("Fatal transcription options error") self._logger.info("url: %s", url) @@ -180,7 +180,7 @@ async def transcribe_url_callback( res = AsyncPrerecordedResponse.from_json(result) self._logger.verbose("result: %s", res) self._logger.notice("transcribe_url_callback succeeded") - self._logger.debug("PreRecordedClient.transcribe_url_callback LEAVE") + self._logger.debug("ListenRESTClient.transcribe_url_callback LEAVE") return res async def transcribe_file( @@ -206,14 +206,14 @@ async def transcribe_file( Raises: DeepgramTypeError: Raised for known API errors. """ - self._logger.debug("PreRecordedClient.transcribe_file ENTER") + self._logger.debug("ListenRESTClient.transcribe_file ENTER") if ( isinstance(options, dict) and "callback" in options and options["callback"] is not None ) or (isinstance(options, PrerecordedOptions) and options.callback is not None): - self._logger.debug("PreRecordedClient.transcribe_file LEAVE") + self._logger.debug("ListenRESTClient.transcribe_file LEAVE") return await self.transcribe_file_callback( source, callback=options["callback"], @@ -231,12 +231,12 @@ async def transcribe_file( body = source["stream"] # type: ignore else: self._logger.error("Unknown transcription source type") - self._logger.debug("PreRecordedClient.transcribe_file LEAVE") + self._logger.debug("ListenRESTClient.transcribe_file LEAVE") raise DeepgramTypeError("Unknown transcription source type") if isinstance(options, PrerecordedOptions) and not options.check(): self._logger.error("options.check failed") - self._logger.debug("PreRecordedClient.transcribe_file LEAVE") + self._logger.debug("ListenRESTClient.transcribe_file LEAVE") raise DeepgramError("Fatal transcription options error") self._logger.info("url: %s", url) @@ -258,7 +258,7 @@ async def transcribe_file( res = PrerecordedResponse.from_json(result) self._logger.verbose("result: %s", res) self._logger.notice("transcribe_file succeeded") - self._logger.debug("PreRecordedClient.transcribe_file LEAVE") + self._logger.debug("ListenRESTClient.transcribe_file LEAVE") return res async def transcribe_file_callback( @@ -286,7 +286,7 @@ async def transcribe_file_callback( Raises: DeepgramTypeError: Raised for known API errors. """ - self._logger.debug("PreRecordedClient.transcribe_file_callback ENTER") + self._logger.debug("ListenRESTClient.transcribe_file_callback ENTER") url = f"{self._config.url}/{endpoint}" if options is None: @@ -301,12 +301,12 @@ async def transcribe_file_callback( body = source["stream"] # type: ignore else: self._logger.error("Unknown transcription source type") - self._logger.debug("PreRecordedClient.transcribe_file_callback LEAVE") + self._logger.debug("ListenRESTClient.transcribe_file_callback LEAVE") raise DeepgramTypeError("Unknown transcription source type") if isinstance(options, PrerecordedOptions) and not options.check(): self._logger.error("options.check failed") - self._logger.debug("PreRecordedClient.transcribe_file_callback LEAVE") + self._logger.debug("ListenRESTClient.transcribe_file_callback LEAVE") raise DeepgramError("Fatal transcription options error") self._logger.info("url: %s", url) @@ -328,5 +328,5 @@ async def transcribe_file_callback( res = AsyncPrerecordedResponse.from_json(result) self._logger.verbose("result: %s", res) self._logger.notice("transcribe_file_callback succeeded") - self._logger.debug("PreRecordedClient.transcribe_file_callback LEAVE") + self._logger.debug("ListenRESTClient.transcribe_file_callback LEAVE") return res diff --git a/deepgram/clients/listen/v1/rest/client.py b/deepgram/clients/listen/v1/rest/client.py new file mode 100644 index 00000000..4e23f4d7 --- /dev/null +++ b/deepgram/clients/listen/v1/rest/client.py @@ -0,0 +1,333 @@ +# Copyright 2023-2024 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 logging +from typing import Dict, Union, Optional + +import httpx + +from .....utils import verboselogs +from .....options import DeepgramClientOptions +from ....abstract_sync_client import AbstractSyncRestClient +from ...errors import DeepgramError, DeepgramTypeError + +from .helpers import is_buffer_source, is_readstream_source, is_url_source +from .options import ( + PrerecordedOptions, + FileSource, + UrlSource, +) +from .response import AsyncPrerecordedResponse, PrerecordedResponse + + +class ListenRESTClient(AbstractSyncRestClient): + """ + A client class for handling pre-recorded audio data. + Provides methods for transcribing audio from URLs and files. + """ + + _logger: verboselogs.VerboseLogger + _config: DeepgramClientOptions + + def __init__(self, config: DeepgramClientOptions): + self._logger = verboselogs.VerboseLogger(__name__) + self._logger.addHandler(logging.StreamHandler()) + self._logger.setLevel(config.verbose) + self._config = config + super().__init__(config) + + def transcribe_url( + self, + source: UrlSource, + options: Optional[Union[Dict, PrerecordedOptions]] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + endpoint: str = "v1/listen", + ) -> Union[AsyncPrerecordedResponse, PrerecordedResponse]: + """ + Transcribes audio from a URL source. + + Args: + source (UrlSource): The URL source of the audio to transcribe. + options (PrerecordedOptions): Additional options for the transcription (default is None). + endpoint (str): The API endpoint for the transcription (default is "v1/listen"). + + Returns: + PrerecordedResponse: An object containing the transcription result. + + Raises: + DeepgramTypeError: Raised for known API errors. + """ + self._logger.debug("ListenRESTClient.transcribe_url ENTER") + + if ( + isinstance(options, dict) + and "callback" in options + and options["callback"] is not None + ) or (isinstance(options, PrerecordedOptions) and options.callback is not None): + self._logger.debug("ListenRESTClient.transcribe_url LEAVE") + return self.transcribe_url_callback( + source, + callback=options["callback"], + options=options, + addons=addons, + headers=headers, + timeout=timeout, + endpoint=endpoint, + ) + + url = f"{self._config.url}/{endpoint}" + if is_url_source(source): + body = source + else: + self._logger.error("Unknown transcription source type") + self._logger.debug("ListenRESTClient.transcribe_url LEAVE") + raise DeepgramTypeError("Unknown transcription source type") + + if isinstance(options, PrerecordedOptions) and not options.check(): + self._logger.error("options.check failed") + self._logger.debug("ListenRESTClient.transcribe_url LEAVE") + raise DeepgramError("Fatal transcription options error") + + self._logger.info("url: %s", url) + self._logger.info("source: %s", source) + if isinstance(options, PrerecordedOptions): + self._logger.info("PrerecordedOptions switching class -> dict") + options = options.to_dict() + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = self.post( + url, + options=options, + addons=addons, + headers=headers, + json=body, + timeout=timeout, + ) + self._logger.info("json: %s", result) + res = PrerecordedResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("transcribe_url succeeded") + self._logger.debug("ListenRESTClient.transcribe_url LEAVE") + return res + + def transcribe_url_callback( + self, + source: UrlSource, + callback: str, + options: Optional[Union[Dict, PrerecordedOptions]] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + endpoint: str = "v1/listen", + ) -> AsyncPrerecordedResponse: + """ + Transcribes audio from a URL source and sends the result to a callback URL. + + Args: + source (UrlSource): The URL source of the audio to transcribe. + callback (str): The callback URL where the transcription results will be sent. + options (PrerecordedOptions): Additional options for the transcription (default is None). + endpoint (str): The API endpoint for the transcription (default is "v1/listen"). + + Returns: + AsyncPrerecordedResponse: An object containing the request_id or an error message. + + Raises: + DeepgramTypeError: Raised for known API errors. + """ + self._logger.debug("ListenRESTClient.transcribe_url_callback ENTER") + + url = f"{self._config.url}/{endpoint}" + if options is None: + options = {} + if isinstance(options, PrerecordedOptions): + options.callback = callback + else: + options["callback"] = callback + if is_url_source(source): + body = source + else: + self._logger.error("Unknown transcription source type") + self._logger.debug("ListenRESTClient.transcribe_url_callback LEAVE") + raise DeepgramTypeError("Unknown transcription source type") + + if isinstance(options, PrerecordedOptions) and not options.check(): + self._logger.error("options.check failed") + self._logger.debug("ListenRESTClient.transcribe_url_callback LEAVE") + raise DeepgramError("Fatal transcription options error") + + self._logger.info("url: %s", url) + self._logger.info("source: %s", source) + if isinstance(options, PrerecordedOptions): + self._logger.info("PrerecordedOptions switching class -> dict") + options = options.to_dict() + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = self.post( + url, + options=options, + addons=addons, + headers=headers, + json=body, + timeout=timeout, + ) + self._logger.info("json: %s", result) + res = AsyncPrerecordedResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("transcribe_url_callback succeeded") + self._logger.debug("ListenRESTClient.transcribe_url_callback LEAVE") + return res + + def transcribe_file( + self, + source: FileSource, + options: Optional[Union[Dict, PrerecordedOptions]] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + endpoint: str = "v1/listen", + ) -> Union[AsyncPrerecordedResponse, PrerecordedResponse]: + """ + Transcribes audio from a local file source. + + Args: + source (FileSource): The local file source of the audio to transcribe. + options (PrerecordedOptions): Additional options for the transcription (default is None). + endpoint (str): The API endpoint for the transcription (default is "v1/listen"). + + Returns: + PrerecordedResponse: An object containing the transcription result or an error message. + + Raises: + DeepgramTypeError: Raised for known API errors. + """ + self._logger.debug("ListenRESTClient.transcribe_file ENTER") + + if ( + isinstance(options, dict) + and "callback" in options + and options["callback"] is not None + ) or (isinstance(options, PrerecordedOptions) and options.callback is not None): + self._logger.debug("ListenRESTClient.transcribe_file LEAVE") + return self.transcribe_file_callback( + source, + callback=options["callback"], + options=options, + addons=addons, + headers=headers, + timeout=timeout, + endpoint=endpoint, + ) + + url = f"{self._config.url}/{endpoint}" + + if is_buffer_source(source): + body = source["buffer"] # type: ignore + elif is_readstream_source(source): + body = source["stream"] # type: ignore + else: + self._logger.error("Unknown transcription source type") + self._logger.debug("ListenRESTClient.transcribe_file LEAVE") + raise DeepgramTypeError("Unknown transcription source type") + + if isinstance(options, PrerecordedOptions) and not options.check(): + self._logger.error("options.check failed") + self._logger.debug("ListenRESTClient.transcribe_file LEAVE") + raise DeepgramError("Fatal transcription options error") + + self._logger.info("url: %s", url) + if isinstance(options, PrerecordedOptions): + self._logger.info("PrerecordedOptions switching class -> dict") + options = options.to_dict() + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = self.post( + url, + options=options, + addons=addons, + headers=headers, + content=body, + timeout=timeout, + ) + self._logger.info("json: %s", result) + res = PrerecordedResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("transcribe_file succeeded") + self._logger.debug("ListenRESTClient.transcribe_file LEAVE") + return res + + def transcribe_file_callback( + self, + source: FileSource, + callback: str, + options: Optional[Union[Dict, PrerecordedOptions]] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + timeout: Optional[httpx.Timeout] = None, + endpoint: str = "v1/listen", + ) -> AsyncPrerecordedResponse: + """ + Transcribes audio from a local file source and sends the result to a callback URL. + + Args: + source (FileSource): The local file source of the audio to transcribe. + callback (str): The callback URL where the transcription results will be sent. + options (PrerecordedOptions): Additional options for the transcription (default is None). + endpoint (str): The API endpoint for the transcription (default is "v1/listen"). + + Returns: + AsyncPrerecordedResponse: An object containing the request_id or an error message. + + Raises: + DeepgramTypeError: Raised for known API errors. + """ + self._logger.debug("ListenRESTClient.transcribe_file_callback ENTER") + + url = f"{self._config.url}/{endpoint}" + if options is None: + options = {} + if isinstance(options, PrerecordedOptions): + options.callback = callback + else: + options["callback"] = callback + if is_buffer_source(source): + body = source["buffer"] # type: ignore + elif is_readstream_source(source): + body = source["stream"] # type: ignore + else: + self._logger.error("Unknown transcription source type") + self._logger.debug("ListenRESTClient.transcribe_file_callback LEAVE") + raise DeepgramTypeError("Unknown transcription source type") + + if isinstance(options, PrerecordedOptions) and not options.check(): + self._logger.error("options.check failed") + self._logger.debug("ListenRESTClient.transcribe_file_callback LEAVE") + raise DeepgramError("Fatal transcription options error") + + self._logger.info("url: %s", url) + if isinstance(options, PrerecordedOptions): + self._logger.info("PrerecordedOptions switching class -> dict") + options = options.to_dict() + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + result = self.post( + url, + options=options, + addons=addons, + headers=headers, + content=body, + timeout=timeout, + ) + self._logger.info("json: %s", result) + res = AsyncPrerecordedResponse.from_json(result) + self._logger.verbose("result: %s", res) + self._logger.notice("transcribe_file_callback succeeded") + self._logger.debug("ListenRESTClient.transcribe_file_callback LEAVE") + return res diff --git a/deepgram/clients/prerecorded/v1/helpers.py b/deepgram/clients/listen/v1/rest/helpers.py similarity index 100% rename from deepgram/clients/prerecorded/v1/helpers.py rename to deepgram/clients/listen/v1/rest/helpers.py diff --git a/deepgram/clients/prerecorded/v1/options.py b/deepgram/clients/listen/v1/rest/options.py similarity index 99% rename from deepgram/clients/prerecorded/v1/options.py rename to deepgram/clients/listen/v1/rest/options.py index 29a1284f..16c52238 100644 --- a/deepgram/clients/prerecorded/v1/options.py +++ b/deepgram/clients/listen/v1/rest/options.py @@ -9,7 +9,7 @@ from dataclasses_json import config as dataclass_config, DataClassJsonMixin from deepgram.utils import verboselogs -from ...common import StreamSource, UrlSource, FileSource +from ....common import StreamSource, UrlSource, FileSource @dataclass diff --git a/deepgram/clients/prerecorded/v1/response.py b/deepgram/clients/listen/v1/rest/response.py similarity index 99% rename from deepgram/clients/prerecorded/v1/response.py rename to deepgram/clients/listen/v1/rest/response.py index 31571c49..8c18358d 100644 --- a/deepgram/clients/prerecorded/v1/response.py +++ b/deepgram/clients/listen/v1/rest/response.py @@ -7,7 +7,7 @@ from dataclasses import dataclass, field from dataclasses_json import config as dataclass_config, DataClassJsonMixin -from ...common import Sentiment +from ....common import Sentiment # Async Prerecorded Response Types: diff --git a/deepgram/clients/listen/v1/websocket/__init__.py b/deepgram/clients/listen/v1/websocket/__init__.py new file mode 100644 index 00000000..75bd694f --- /dev/null +++ b/deepgram/clients/listen/v1/websocket/__init__.py @@ -0,0 +1,18 @@ +# Copyright 2023-2024 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 .client import ListenWebSocketClient +from .async_client import AsyncListenWebSocketClient +from .options import LiveOptions +from .....options import DeepgramClientOptions, ClientOptionsFromEnv +from .response import ( + OpenResponse, + LiveResultResponse, + MetadataResponse, + SpeechStartedResponse, + UtteranceEndResponse, + CloseResponse, + ErrorResponse, + UnhandledResponse, +) diff --git a/deepgram/clients/live/v1/async_client.py b/deepgram/clients/listen/v1/websocket/async_client.py similarity index 82% rename from deepgram/clients/live/v1/async_client.py rename to deepgram/clients/listen/v1/websocket/async_client.py index 81b7454f..764fd531 100644 --- a/deepgram/clients/live/v1/async_client.py +++ b/deepgram/clients/listen/v1/websocket/async_client.py @@ -11,11 +11,11 @@ import websockets from websockets.client import WebSocketClientProtocol -from ....utils import verboselogs -from ....options import DeepgramClientOptions -from ..enums import LiveTranscriptionEvents +from .....utils import verboselogs +from .....options import DeepgramClientOptions +from ...enums import LiveTranscriptionEvents from ..helpers import convert_to_websocket_url, append_query_params -from ..errors import DeepgramError +from ...errors import DeepgramError from .response import ( OpenResponse, @@ -35,7 +35,7 @@ PING_INTERVAL = 20 -class AsyncLiveClient: # pylint: disable=too-many-instance-attributes +class AsyncListenWebSocketClient: # pylint: disable=too-many-instance-attributes """ Client for interacting with Deepgram's live transcription services over WebSockets. @@ -101,7 +101,7 @@ async def start( """ Starts the WebSocket connection for live transcription. """ - self._logger.debug("AsyncLiveClient.start ENTER") + self._logger.debug("AsyncListenWebSocketClient.start ENTER") self._logger.info("options: %s", options) self._logger.info("addons: %s", addons) self._logger.info("headers: %s", headers) @@ -110,7 +110,7 @@ async def start( if isinstance(options, LiveOptions) and not options.check(): self._logger.error("options.check failed") - self._logger.debug("AsyncLiveClient.start LEAVE") + self._logger.debug("AsyncListenWebSocketClient.start LEAVE") raise DeepgramError("Fatal transcription options error") self._addons = addons @@ -192,23 +192,29 @@ async def start( ) self._logger.notice("start succeeded") - self._logger.debug("AsyncLiveClient.start LEAVE") + self._logger.debug("AsyncListenWebSocketClient.start LEAVE") return True except websockets.ConnectionClosed as e: - self._logger.error("ConnectionClosed in AsyncLiveClient.start: %s", e) - self._logger.debug("AsyncLiveClient.start LEAVE") + self._logger.error( + "ConnectionClosed in AsyncListenWebSocketClient.start: %s", e + ) + self._logger.debug("AsyncListenWebSocketClient.start LEAVE") if self._config.options.get("termination_exception_connect") == "true": raise return False except websockets.exceptions.WebSocketException as e: - self._logger.error("WebSocketException in AsyncLiveClient.start: %s", e) - self._logger.debug("AsyncLiveClient.start LEAVE") + self._logger.error( + "WebSocketException in AsyncListenWebSocketClient.start: %s", e + ) + self._logger.debug("AsyncListenWebSocketClient.start LEAVE") if self._config.options.get("termination_exception_connect") == "true": raise return False except Exception as e: # pylint: disable=broad-except - self._logger.error("WebSocketException in AsyncLiveClient.start: %s", e) - self._logger.debug("AsyncLiveClient.start LEAVE") + self._logger.error( + "WebSocketException in AsyncListenWebSocketClient.start: %s", e + ) + self._logger.debug("AsyncListenWebSocketClient.start LEAVE") if self._config.options.get("termination_exception_connect") == "true": raise return False @@ -229,7 +235,7 @@ async def _emit(self, event: LiveTranscriptionEvents, *args, **kwargs) -> None: """ Emits events to the registered event handlers. """ - self._logger.debug("AsyncLiveClient._emit ENTER") + self._logger.debug("AsyncListenWebSocketClient._emit ENTER") self._logger.debug("callback handlers for: %s", event) # debug the threads @@ -252,25 +258,25 @@ async def _emit(self, event: LiveTranscriptionEvents, *args, **kwargs) -> None: self._logger.debug("after running thread: %s", thread.name) self._logger.debug("number of active threads: %s", threading.active_count()) - self._logger.debug("AsyncLiveClient._emit LEAVE") + self._logger.debug("AsyncListenWebSocketClient._emit LEAVE") # pylint: disable=too-many-return-statements,too-many-statements,too-many-locals,too-many-branches async def _listening(self) -> None: """ Listens for messages from the WebSocket connection. """ - self._logger.debug("AsyncLiveClient._listening ENTER") + self._logger.debug("AsyncListenWebSocketClient._listening ENTER") while True: try: if self._exit_event.is_set(): self._logger.notice("_listening exiting gracefully") - self._logger.debug("AsyncLiveClient._listening LEAVE") + self._logger.debug("AsyncListenWebSocketClient._listening LEAVE") return if self._socket is None: self._logger.warning("socket is empty") - self._logger.debug("AsyncLiveClient._listening LEAVE") + self._logger.debug("AsyncListenWebSocketClient._listening LEAVE") return message = str(await self._socket.recv()) @@ -379,22 +385,22 @@ async def _listening(self) -> None: except websockets.exceptions.ConnectionClosedOK as e: self._logger.notice(f"_listening({e.code}) exiting gracefully") - self._logger.debug("AsyncLiveClient._listening LEAVE") + self._logger.debug("AsyncListenWebSocketClient._listening LEAVE") return except websockets.exceptions.ConnectionClosed as e: if e.code in [1000, 1001]: self._logger.notice(f"_listening({e.code}) exiting gracefully") - self._logger.debug("AsyncLiveClient._listening LEAVE") + self._logger.debug("AsyncListenWebSocketClient._listening LEAVE") return self._logger.error( - "ConnectionClosed in AsyncLiveClient._listening with code %s: %s", + "ConnectionClosed in AsyncListenWebSocketClient._listening with code %s: %s", e.code, e.reason, ) cc_error: ErrorResponse = ErrorResponse( - "ConnectionClosed in AsyncLiveClient._listening", + "ConnectionClosed in AsyncListenWebSocketClient._listening", f"{e}", "ConnectionClosed", ) @@ -407,7 +413,7 @@ async def _listening(self) -> None: # signal exit and close await self._signal_exit() - self._logger.debug("AsyncLiveClient._listening LEAVE") + self._logger.debug("AsyncListenWebSocketClient._listening LEAVE") if self._config.options.get("termination_exception") == "true": raise @@ -415,10 +421,10 @@ async def _listening(self) -> None: except websockets.exceptions.WebSocketException as e: self._logger.error( - "WebSocketException in AsyncLiveClient._listening: %s", e + "WebSocketException in AsyncListenWebSocketClient._listening: %s", e ) ws_error: ErrorResponse = ErrorResponse( - "WebSocketException in AsyncLiveClient._listening", + "WebSocketException in AsyncListenWebSocketClient._listening", f"{e}", "WebSocketException", ) @@ -431,16 +437,18 @@ async def _listening(self) -> None: # signal exit and close await self._signal_exit() - self._logger.debug("AsyncLiveClient._listening LEAVE") + self._logger.debug("AsyncListenWebSocketClient._listening LEAVE") if self._config.options.get("termination_exception") == "true": raise return except Exception as e: # pylint: disable=broad-except - self._logger.error("Exception in AsyncLiveClient._listening: %s", e) + self._logger.error( + "Exception in AsyncListenWebSocketClient._listening: %s", e + ) e_error: ErrorResponse = ErrorResponse( - "Exception in AsyncLiveClient._listening", + "Exception in AsyncListenWebSocketClient._listening", f"{e}", "Exception", ) @@ -453,7 +461,7 @@ async def _listening(self) -> None: # signal exit and close await self._signal_exit() - self._logger.debug("AsyncLiveClient._listening LEAVE") + self._logger.debug("AsyncListenWebSocketClient._listening LEAVE") if self._config.options.get("termination_exception") == "true": raise @@ -466,7 +474,7 @@ async def _keep_alive(self) -> None: """ Sends keepalive messages to the WebSocket connection. """ - self._logger.debug("AsyncLiveClient._keep_alive ENTER") + self._logger.debug("AsyncListenWebSocketClient._keep_alive ENTER") counter = 0 while True: @@ -476,12 +484,12 @@ async def _keep_alive(self) -> None: if self._exit_event.is_set(): self._logger.notice("_keep_alive exiting gracefully") - self._logger.debug("AsyncLiveClient._keep_alive LEAVE") + self._logger.debug("AsyncListenWebSocketClient._keep_alive LEAVE") return if self._socket is None: self._logger.notice("socket is None, exiting keep_alive") - self._logger.debug("AsyncLiveClient._keep_alive LEAVE") + self._logger.debug("AsyncListenWebSocketClient._keep_alive LEAVE") return # deepgram keepalive @@ -490,22 +498,22 @@ async def _keep_alive(self) -> None: except websockets.exceptions.ConnectionClosedOK as e: self._logger.notice(f"_keep_alive({e.code}) exiting gracefully") - self._logger.debug("AsyncLiveClient._keep_alive LEAVE") + self._logger.debug("AsyncListenWebSocketClient._keep_alive LEAVE") return except websockets.exceptions.ConnectionClosed as e: if e.code == 1000: self._logger.notice(f"_keep_alive({e.code}) exiting gracefully") - self._logger.debug("AsyncLiveClient._keep_alive LEAVE") + self._logger.debug("AsyncListenWebSocketClient._keep_alive LEAVE") return self._logger.error( - "ConnectionClosed in AsyncLiveClient._keep_alive with code %s: %s", + "ConnectionClosed in AsyncListenWebSocketClient._keep_alive with code %s: %s", e.code, e.reason, ) cc_error: ErrorResponse = ErrorResponse( - "ConnectionClosed in AsyncLiveClient._keep_alive", + "ConnectionClosed in AsyncListenWebSocketClient._keep_alive", f"{e}", "ConnectionClosed", ) @@ -518,7 +526,7 @@ async def _keep_alive(self) -> None: # signal exit and close await self._signal_exit() - self._logger.debug("AsyncLiveClient._keep_alive LEAVE") + self._logger.debug("AsyncListenWebSocketClient._keep_alive LEAVE") if self._config.options.get("termination_exception") == "true": raise @@ -526,10 +534,11 @@ async def _keep_alive(self) -> None: except websockets.exceptions.WebSocketException as e: self._logger.error( - "WebSocketException in AsyncLiveClient._keep_alive: %s", e + "WebSocketException in AsyncListenWebSocketClient._keep_alive: %s", + e, ) ws_error: ErrorResponse = ErrorResponse( - "WebSocketException in AsyncLiveClient._keep_alive", + "WebSocketException in AsyncListenWebSocketClient._keep_alive", f"{e}", "Exception", ) @@ -542,21 +551,23 @@ async def _keep_alive(self) -> None: # signal exit and close await self._signal_exit() - self._logger.debug("AsyncLiveClient._keep_alive LEAVE") + self._logger.debug("AsyncListenWebSocketClient._keep_alive LEAVE") if self._config.options.get("termination_exception") == "true": raise return except Exception as e: # pylint: disable=broad-except - self._logger.error("Exception in AsyncLiveClient._keep_alive: %s", e) + self._logger.error( + "Exception in AsyncListenWebSocketClient._keep_alive: %s", e + ) e_error: ErrorResponse = ErrorResponse( - "Exception in AsyncLiveClient._keep_alive", + "Exception in AsyncListenWebSocketClient._keep_alive", f"{e}", "Exception", ) self._logger.error( - "Exception in AsyncLiveClient._keep_alive: %s", str(e) + "Exception in AsyncListenWebSocketClient._keep_alive: %s", str(e) ) await self._emit( LiveTranscriptionEvents(LiveTranscriptionEvents.Error), @@ -567,7 +578,7 @@ async def _keep_alive(self) -> None: # signal exit and close await self._signal_exit() - self._logger.debug("AsyncLiveClient._keep_alive LEAVE") + self._logger.debug("AsyncListenWebSocketClient._keep_alive LEAVE") if self._config.options.get("termination_exception") == "true": raise @@ -575,12 +586,12 @@ async def _keep_alive(self) -> None: ## pylint: disable=too-many-return-statements,too-many-statements async def _flush(self) -> None: - self._logger.debug("AsyncLiveClient._flush ENTER") + self._logger.debug("AsyncListenWebSocketClient._flush ENTER") delta_in_ms_str = self._config.options.get("auto_flush_reply_delta") if delta_in_ms_str is None: self._logger.error("auto_flush_reply_delta is None") - self._logger.debug("AsyncLiveClient._flush LEAVE") + self._logger.debug("AsyncListenWebSocketClient._flush LEAVE") return delta_in_ms = float(delta_in_ms_str) @@ -590,12 +601,12 @@ async def _flush(self) -> None: if self._exit_event.is_set(): self._logger.notice("_flush exiting gracefully") - self._logger.debug("AsyncLiveClient._flush LEAVE") + self._logger.debug("AsyncListenWebSocketClient._flush LEAVE") return if self._socket is None: self._logger.notice("socket is None, exiting flush") - self._logger.debug("AsyncLiveClient._flush LEAVE") + self._logger.debug("AsyncListenWebSocketClient._flush LEAVE") return if self._last_datagram is None: @@ -614,22 +625,22 @@ async def _flush(self) -> None: except websockets.exceptions.ConnectionClosedOK as e: self._logger.notice(f"_flush({e.code}) exiting gracefully") - self._logger.debug("AsyncLiveClient._flush LEAVE") + self._logger.debug("AsyncListenWebSocketClient._flush LEAVE") return except websockets.exceptions.ConnectionClosed as e: if e.code == 1000: self._logger.notice(f"_flush({e.code}) exiting gracefully") - self._logger.debug("AsyncLiveClient._flush LEAVE") + self._logger.debug("AsyncListenWebSocketClient._flush LEAVE") return self._logger.error( - "ConnectionClosed in AsyncLiveClient._flush with code %s: %s", + "ConnectionClosed in AsyncListenWebSocketClient._flush with code %s: %s", e.code, e.reason, ) cc_error: ErrorResponse = ErrorResponse( - "ConnectionClosed in AsyncLiveClient._flush", + "ConnectionClosed in AsyncListenWebSocketClient._flush", f"{e}", "ConnectionClosed", ) @@ -642,7 +653,7 @@ async def _flush(self) -> None: # signal exit and close await self._signal_exit() - self._logger.debug("AsyncLiveClient._flush LEAVE") + self._logger.debug("AsyncListenWebSocketClient._flush LEAVE") if self._config.options.get("termination_exception") == "true": raise @@ -650,10 +661,10 @@ async def _flush(self) -> None: except websockets.exceptions.WebSocketException as e: self._logger.error( - "WebSocketException in AsyncLiveClient._flush: %s", e + "WebSocketException in AsyncListenWebSocketClient._flush: %s", e ) ws_error: ErrorResponse = ErrorResponse( - "WebSocketException in AsyncLiveClient._flush", + "WebSocketException in AsyncListenWebSocketClient._flush", f"{e}", "Exception", ) @@ -666,20 +677,24 @@ async def _flush(self) -> None: # signal exit and close await self._signal_exit() - self._logger.debug("AsyncLiveClient._flush LEAVE") + self._logger.debug("AsyncListenWebSocketClient._flush LEAVE") if self._config.options.get("termination_exception") == "true": raise return except Exception as e: # pylint: disable=broad-except - self._logger.error("Exception in AsyncLiveClient._flush: %s", e) + self._logger.error( + "Exception in AsyncListenWebSocketClient._flush: %s", e + ) e_error: ErrorResponse = ErrorResponse( - "Exception in AsyncLiveClient._flush", + "Exception in AsyncListenWebSocketClient._flush", f"{e}", "Exception", ) - self._logger.error("Exception in AsyncLiveClient._flush: %s", str(e)) + self._logger.error( + "Exception in AsyncListenWebSocketClient._flush: %s", str(e) + ) await self._emit( LiveTranscriptionEvents(LiveTranscriptionEvents.Error), error=e_error, @@ -689,7 +704,7 @@ async def _flush(self) -> None: # signal exit and close await self._signal_exit() - self._logger.debug("AsyncLiveClient._flush LEAVE") + self._logger.debug("AsyncListenWebSocketClient._flush LEAVE") if self._config.options.get("termination_exception") == "true": raise @@ -703,11 +718,11 @@ async def send(self, data: Union[str, bytes]) -> bool: """ Sends data over the WebSocket connection. """ - self._logger.spam("AsyncLiveClient.send ENTER") + self._logger.spam("AsyncListenWebSocketClient.send ENTER") if self._exit_event.is_set(): self._logger.notice("send exiting gracefully") - self._logger.debug("AsyncLiveClient.send LEAVE") + self._logger.debug("AsyncListenWebSocketClient.send LEAVE") return False if self._socket is not None: @@ -715,42 +730,42 @@ async def send(self, data: Union[str, bytes]) -> bool: await self._socket.send(data) except websockets.exceptions.ConnectionClosedOK as e: self._logger.notice(f"send() exiting gracefully: {e.code}") - self._logger.debug("AsyncLiveClient.send LEAVE") + self._logger.debug("AsyncListenWebSocketClient.send LEAVE") if self._config.options.get("termination_exception_send") == "true": raise return True except websockets.exceptions.ConnectionClosed as e: if e.code == 1000: self._logger.notice(f"send({e.code}) exiting gracefully") - self._logger.debug("AsyncLiveClient.send LEAVE") + self._logger.debug("AsyncListenWebSocketClient.send LEAVE") if self._config.options.get("termination_exception_send") == "true": raise return True self._logger.error("send() failed - ConnectionClosed: %s", str(e)) - self._logger.spam("AsyncLiveClient.send LEAVE") + self._logger.spam("AsyncListenWebSocketClient.send LEAVE") if self._config.options.get("termination_exception_send") == "true": raise return False except websockets.exceptions.WebSocketException as e: self._logger.error("send() failed - WebSocketException: %s", str(e)) - self._logger.spam("AsyncLiveClient.send LEAVE") + self._logger.spam("AsyncListenWebSocketClient.send LEAVE") if self._config.options.get("termination_exception_send") == "true": raise return False except Exception as e: # pylint: disable=broad-except self._logger.error("send() failed - Exception: %s", str(e)) - self._logger.spam("AsyncLiveClient.send LEAVE") + self._logger.spam("AsyncListenWebSocketClient.send LEAVE") if self._config.options.get("termination_exception_send") == "true": raise return False self._logger.spam("send() succeeded") - self._logger.spam("AsyncLiveClient.send LEAVE") + self._logger.spam("AsyncListenWebSocketClient.send LEAVE") return True self._logger.spam("send() failed. socket is None") - self._logger.spam("AsyncLiveClient.send LEAVE") + self._logger.spam("AsyncListenWebSocketClient.send LEAVE") return False # pylint: enable=too-many-return-statements @@ -759,16 +774,16 @@ async def keep_alive(self) -> bool: """ Sends a KeepAlive message """ - self._logger.spam("AsyncLiveClient.keep_alive ENTER") + self._logger.spam("AsyncListenWebSocketClient.keep_alive ENTER") if self._exit_event.is_set(): self._logger.notice("keep_alive exiting gracefully") - self._logger.debug("AsyncLiveClient.keep_alive LEAVE") + self._logger.debug("AsyncListenWebSocketClient.keep_alive LEAVE") return False if self._socket is None: self._logger.notice("socket is not intialized") - self._logger.debug("AsyncLiveClient.keep_alive LEAVE") + self._logger.debug("AsyncListenWebSocketClient.keep_alive LEAVE") return False self._logger.notice("Sending KeepAlive...") @@ -776,11 +791,11 @@ async def keep_alive(self) -> bool: if not ret: self._logger.error("keep_alive failed") - self._logger.spam("AsyncLiveClient.keep_alive LEAVE") + self._logger.spam("AsyncListenWebSocketClient.keep_alive LEAVE") return False self._logger.notice("keep_alive succeeded") - self._logger.spam("AsyncLiveClient.keep_alive LEAVE") + self._logger.spam("AsyncListenWebSocketClient.keep_alive LEAVE") return True @@ -788,16 +803,16 @@ async def finalize(self) -> bool: """ Finalizes the Transcript connection by flushing it """ - self._logger.spam("AsyncLiveClient.finalize ENTER") + self._logger.spam("AsyncListenWebSocketClient.finalize ENTER") if self._exit_event.is_set(): self._logger.notice("finalize exiting gracefully") - self._logger.debug("AsyncLiveClient.finalize LEAVE") + self._logger.debug("AsyncListenWebSocketClient.finalize LEAVE") return False if self._socket is None: self._logger.notice("socket is not intialized") - self._logger.debug("AsyncLiveClient.finalize LEAVE") + self._logger.debug("AsyncListenWebSocketClient.finalize LEAVE") return False self._logger.notice("Sending Finalize...") @@ -805,11 +820,11 @@ async def finalize(self) -> bool: if not ret: self._logger.error("finalize failed") - self._logger.spam("AsyncLiveClient.finalize LEAVE") + self._logger.spam("AsyncListenWebSocketClient.finalize LEAVE") return False self._logger.notice("finalize succeeded") - self._logger.spam("AsyncLiveClient.finalize LEAVE") + self._logger.spam("AsyncListenWebSocketClient.finalize LEAVE") return True @@ -817,7 +832,7 @@ async def finish(self) -> bool: """ Closes the WebSocket connection gracefully. """ - self._logger.debug("AsyncLiveClient.finish ENTER") + self._logger.debug("AsyncListenWebSocketClient.finish ENTER") # signal exit await self._signal_exit() @@ -857,12 +872,12 @@ async def finish(self) -> bool: self._logger.debug("number of active threads: %s", threading.active_count()) self._logger.notice("finish succeeded") - self._logger.spam("AsyncLiveClient.finish LEAVE") + self._logger.spam("AsyncListenWebSocketClient.finish LEAVE") return True except asyncio.CancelledError as e: self._logger.error("tasks cancelled error: %s", e) - self._logger.debug("AsyncLiveClient.finish LEAVE") + self._logger.debug("AsyncListenWebSocketClient.finish LEAVE") return False async def _signal_exit(self) -> None: diff --git a/deepgram/clients/listen/v1/websocket/client.py b/deepgram/clients/listen/v1/websocket/client.py new file mode 100644 index 00000000..7a07a5be --- /dev/null +++ b/deepgram/clients/listen/v1/websocket/client.py @@ -0,0 +1,903 @@ +# Copyright 2023-2024 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 json +import time +import logging +from typing import Dict, Union, Optional, cast, Any +from datetime import datetime +import threading + +from websockets.sync.client import connect, ClientConnection +import websockets + +from .....utils import verboselogs +from .....options import DeepgramClientOptions +from ...enums import LiveTranscriptionEvents +from ..helpers import convert_to_websocket_url, append_query_params +from ...errors import DeepgramError + +from .response import ( + OpenResponse, + LiveResultResponse, + MetadataResponse, + SpeechStartedResponse, + UtteranceEndResponse, + CloseResponse, + ErrorResponse, + UnhandledResponse, +) +from .options import LiveOptions + +ONE_SECOND = 1 +HALF_SECOND = 0.5 +DEEPGRAM_INTERVAL = 5 +PING_INTERVAL = 20 + + +class ListenWebSocketClient: # pylint: disable=too-many-instance-attributes + """ + 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. + """ + + _logger: verboselogs.VerboseLogger + _config: DeepgramClientOptions + _endpoint: str + _websocket_url: str + + _socket: ClientConnection + _exit_event: threading.Event + _lock_send: threading.Lock + _lock_flush: threading.Lock + _event_handlers: Dict[LiveTranscriptionEvents, list] + + _last_datagram: Optional[datetime] = None + + _listen_thread: Union[threading.Thread, None] + _keep_alive_thread: Union[threading.Thread, None] + _flush_thread: Union[threading.Thread, None] + + _kwargs: Optional[Dict] = None + _addons: Optional[Dict] = None + _options: Optional[Dict] = None + _headers: Optional[Dict] = None + + def __init__(self, config: DeepgramClientOptions): + if config is None: + raise DeepgramError("Config are required") + + self._logger = verboselogs.VerboseLogger(__name__) + self._logger.addHandler(logging.StreamHandler()) + self._logger.setLevel(config.verbose) + + self._config = config + self._endpoint = "v1/listen" + self._lock_send = threading.Lock() + + self._flush_thread = None + self._keep_alive_thread = None + self._listen_thread = None + + # exit + self._exit_event = threading.Event() + + # auto flush + self._last_datagram = None + self._flush_event = threading.Event() + self._lock_flush = threading.Lock() + + self._event_handlers = { + event: [] for event in LiveTranscriptionEvents.__members__.values() + } + self._websocket_url = convert_to_websocket_url(self._config.url, self._endpoint) + + # pylint: disable=too-many-statements,too-many-branches + def start( + self, + options: Optional[Union[LiveOptions, Dict]] = None, + addons: Optional[Dict] = None, + headers: Optional[Dict] = None, + members: Optional[Dict] = None, + **kwargs, + ) -> bool: + """ + Starts the WebSocket connection for live transcription. + """ + self._logger.debug("ListenWebSocketClient.start ENTER") + self._logger.info("options: %s", options) + self._logger.info("addons: %s", addons) + self._logger.info("headers: %s", headers) + self._logger.info("members: %s", members) + self._logger.info("kwargs: %s", kwargs) + + if isinstance(options, LiveOptions) and not options.check(): + self._logger.error("options.check failed") + self._logger.debug("ListenWebSocketClient.start LEAVE") + raise DeepgramError("Fatal transcription options error") + + self._addons = addons + self._headers = headers + + # add "members" as members of the class + if members is not None: + self.__dict__.update(members) + + # set kwargs as members of the class + if kwargs is not None: + self._kwargs = kwargs + else: + self._kwargs = {} + + if isinstance(options, LiveOptions): + self._logger.info("LiveOptions switching class -> dict") + self._options = options.to_dict() + elif options is not None: + self._options = options + else: + self._options = {} + + combined_options = self._options + if self._addons is not None: + self._logger.info("merging addons to options") + combined_options.update(self._addons) + self._logger.info("new options: %s", combined_options) + self._logger.debug("combined_options: %s", combined_options) + + combined_headers = self._config.headers + if self._headers is not None: + self._logger.info("merging headers to options") + combined_headers.update(self._headers) + self._logger.info("new headers: %s", combined_headers) + self._logger.debug("combined_headers: %s", combined_headers) + + url_with_params = append_query_params(self._websocket_url, combined_options) + try: + self._socket = connect(url_with_params, additional_headers=combined_headers) + self._exit_event.clear() + + # debug the threads + for thread in threading.enumerate(): + self._logger.debug("after running thread: %s", thread.name) + self._logger.debug("number of active threads: %s", threading.active_count()) + + # listening thread + self._listen_thread = threading.Thread(target=self._listening) + self._listen_thread.start() + + # keepalive thread + if self._config.is_keep_alive_enabled(): + self._logger.notice("keepalive is enabled") + self._keep_alive_thread = threading.Thread(target=self._keep_alive) + self._keep_alive_thread.start() + else: + self._logger.notice("keepalive is disabled") + + # flush thread + if self._config.is_auto_flush_enabled(): + self._logger.notice("autoflush is enabled") + self._flush_thread = threading.Thread(target=self._flush) + self._flush_thread.start() + else: + self._logger.notice("autoflush is disabled") + + # debug the threads + for thread in threading.enumerate(): + self._logger.debug("after running thread: %s", thread.name) + self._logger.debug("number of active threads: %s", threading.active_count()) + + # push open event + self._emit( + LiveTranscriptionEvents(LiveTranscriptionEvents.Open), + OpenResponse(type=LiveTranscriptionEvents.Open), + ) + + self._logger.notice("start succeeded") + self._logger.debug("ListenWebSocketClient.start LEAVE") + return True + except websockets.ConnectionClosed as e: + self._logger.error("ConnectionClosed in ListenWebSocketClient.start: %s", e) + self._logger.debug("ListenWebSocketClient.start LEAVE") + if self._config.options.get("termination_exception_connect") == "true": + raise e + return False + except websockets.exceptions.WebSocketException as e: + self._logger.error( + "WebSocketException in ListenWebSocketClient.start: %s", e + ) + self._logger.debug("ListenWebSocketClient.start LEAVE") + if self._config.options.get("termination_exception_connect") == "true": + raise e + return False + except Exception as e: # pylint: disable=broad-except + self._logger.error( + "WebSocketException in ListenWebSocketClient.start: %s", e + ) + self._logger.debug("ListenWebSocketClient.start LEAVE") + if self._config.options.get("termination_exception_connect") == "true": + raise e + return False + + # pylint: enable=too-many-statements,too-many-branches + + def on( + self, event: LiveTranscriptionEvents, handler + ) -> None: # registers event handlers for specific events + """ + Registers event handlers for specific events. + """ + self._logger.info("event subscribed: %s", event) + if event in LiveTranscriptionEvents.__members__.values() and callable(handler): + self._event_handlers[event].append(handler) + + def _emit(self, event: LiveTranscriptionEvents, *args, **kwargs) -> None: + """ + Emits events to the registered event handlers. + """ + self._logger.debug("callback handlers for: %s", event) + for handler in self._event_handlers[event]: + handler(self, *args, **kwargs) + + # pylint: disable=too-many-return-statements,too-many-statements,too-many-locals,too-many-branches + def _listening( + self, + ) -> None: + """ + Listens for messages from the WebSocket connection. + """ + self._logger.debug("ListenWebSocketClient._listening ENTER") + + while True: + try: + if self._exit_event.is_set(): + self._logger.notice("_listening exiting gracefully") + self._logger.debug("ListenWebSocketClient._listening LEAVE") + return + + if self._socket is None: + self._logger.warning("socket is empty") + self._logger.debug("ListenWebSocketClient._listening LEAVE") + return + + message = str(self._socket.recv()) + + if message is None: + self._logger.info("message is empty") + continue + + data = json.loads(message) + response_type = data.get("type") + self._logger.debug("response_type: %s, data: %s", response_type, data) + + match response_type: + case LiveTranscriptionEvents.Open: + open_result: OpenResponse = OpenResponse.from_json(message) + self._logger.verbose("OpenResponse: %s", open_result) + self._emit( + LiveTranscriptionEvents(LiveTranscriptionEvents.Open), + open=open_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case LiveTranscriptionEvents.Transcript: + msg_result: LiveResultResponse = LiveResultResponse.from_json( + message + ) + self._logger.verbose("LiveResultResponse: %s", msg_result) + + # auto flush + if self._config.is_inspecting_messages(): + inspect_res = self._inspect(msg_result) + if not inspect_res: + self._logger.error("inspect_res failed") + + self._emit( + LiveTranscriptionEvents(LiveTranscriptionEvents.Transcript), + result=msg_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case LiveTranscriptionEvents.Metadata: + meta_result: MetadataResponse = MetadataResponse.from_json( + message + ) + self._logger.verbose("MetadataResponse: %s", meta_result) + self._emit( + LiveTranscriptionEvents(LiveTranscriptionEvents.Metadata), + metadata=meta_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case LiveTranscriptionEvents.SpeechStarted: + ss_result: SpeechStartedResponse = ( + SpeechStartedResponse.from_json(message) + ) + self._logger.verbose("SpeechStartedResponse: %s", ss_result) + self._emit( + LiveTranscriptionEvents( + LiveTranscriptionEvents.SpeechStarted + ), + speech_started=ss_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case LiveTranscriptionEvents.UtteranceEnd: + ue_result: UtteranceEndResponse = ( + UtteranceEndResponse.from_json(message) + ) + self._logger.verbose("UtteranceEndResponse: %s", ue_result) + self._emit( + LiveTranscriptionEvents( + LiveTranscriptionEvents.UtteranceEnd + ), + utterance_end=ue_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case LiveTranscriptionEvents.Close: + close_result: CloseResponse = CloseResponse.from_json(message) + self._logger.verbose("CloseResponse: %s", close_result) + self._emit( + LiveTranscriptionEvents(LiveTranscriptionEvents.Close), + close=close_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case LiveTranscriptionEvents.Error: + err_error: ErrorResponse = ErrorResponse.from_json(message) + self._logger.verbose("ErrorResponse: %s", err_error) + self._emit( + LiveTranscriptionEvents(LiveTranscriptionEvents.Error), + error=err_error, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case _: + self._logger.warning( + "Unknown Message: response_type: %s, data: %s", + response_type, + data, + ) + unhandled_error: UnhandledResponse = UnhandledResponse( + type=LiveTranscriptionEvents( + LiveTranscriptionEvents.Unhandled + ), + raw=message, + ) + self._emit( + LiveTranscriptionEvents(LiveTranscriptionEvents.Unhandled), + unhandled=unhandled_error, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + + except websockets.exceptions.ConnectionClosedOK as e: + self._logger.notice(f"_listening({e.code}) exiting gracefully") + self._logger.debug("ListenWebSocketClient._listening LEAVE") + return + + except websockets.exceptions.ConnectionClosed as e: + if e.code == 1000: + self._logger.notice(f"_listening({e.code}) exiting gracefully") + self._logger.debug("ListenWebSocketClient._listening LEAVE") + return + + self._logger.error( + "ConnectionClosed in ListenWebSocketClient._listening with code %s: %s", + e.code, + e.reason, + ) + cc_error: ErrorResponse = ErrorResponse( + "ConnectionClosed in ListenWebSocketClient._listening", + f"{e}", + "ConnectionClosed", + ) + self._emit( + LiveTranscriptionEvents(LiveTranscriptionEvents.Error), cc_error + ) + + # signal exit and close + self._signal_exit() + + self._logger.debug("ListenWebSocketClient._listening LEAVE") + + if self._config.options.get("termination_exception") == "true": + raise + return + + except websockets.exceptions.WebSocketException as e: + self._logger.error( + "WebSocketException in ListenWebSocketClient._listening with: %s", e + ) + ws_error: ErrorResponse = ErrorResponse( + "WebSocketException in ListenWebSocketClient._listening", + f"{e}", + "WebSocketException", + ) + self._emit( + LiveTranscriptionEvents(LiveTranscriptionEvents.Error), ws_error + ) + + # signal exit and close + self._signal_exit() + + self._logger.debug("ListenWebSocketClient._listening LEAVE") + + if self._config.options.get("termination_exception") == "true": + raise + return + + except Exception as e: # pylint: disable=broad-except + self._logger.error( + "Exception in ListenWebSocketClient._listening: %s", e + ) + e_error: ErrorResponse = ErrorResponse( + "Exception in ListenWebSocketClient._listening", + f"{e}", + "Exception", + ) + self._logger.error( + "Exception in ListenWebSocketClient._listening: %s", str(e) + ) + self._emit( + LiveTranscriptionEvents(LiveTranscriptionEvents.Error), e_error + ) + + # signal exit and close + self._signal_exit() + + self._logger.debug("ListenWebSocketClient._listening LEAVE") + + if self._config.options.get("termination_exception") == "true": + raise + return + + # pylint: enable=too-many-return-statements,too-many-statements + + ## pylint: disable=too-many-return-statements + def _keep_alive(self) -> None: + self._logger.debug("ListenWebSocketClient._keep_alive ENTER") + + counter = 0 + while True: + try: + counter += 1 + self._exit_event.wait(timeout=ONE_SECOND) + + if self._exit_event.is_set(): + self._logger.notice("_keep_alive exiting gracefully") + self._logger.debug("ListenWebSocketClient._keep_alive LEAVE") + return + + if self._socket is None: + self._logger.notice("socket is None, exiting keep_alive") + self._logger.debug("ListenWebSocketClient._keep_alive LEAVE") + return + + # deepgram keepalive + if counter % DEEPGRAM_INTERVAL == 0: + self.keep_alive() + + except websockets.exceptions.ConnectionClosedOK as e: + self._logger.notice(f"_keep_alive({e.code}) exiting gracefully") + self._logger.debug("ListenWebSocketClient._keep_alive LEAVE") + return + + except websockets.exceptions.ConnectionClosed as e: + if e.code == 1000: + self._logger.notice(f"_keep_alive({e.code}) exiting gracefully") + self._logger.debug("ListenWebSocketClient._keep_alive LEAVE") + return + + self._logger.error( + "ConnectionClosed in ListenWebSocketClient._keep_alive with code %s: %s", + e.code, + e.reason, + ) + cc_error: ErrorResponse = ErrorResponse( + "ConnectionClosed in ListenWebSocketClient._keep_alive", + f"{e}", + "ConnectionClosed", + ) + self._emit( + LiveTranscriptionEvents(LiveTranscriptionEvents.Error), cc_error + ) + + # signal exit and close + self._signal_exit() + + self._logger.debug("ListenWebSocketClient._keep_alive LEAVE") + + if self._config.options.get("termination_exception") == "true": + raise + return + + except websockets.exceptions.WebSocketException as e: + self._logger.error( + "WebSocketException in ListenWebSocketClient._keep_alive with: %s", + e, + ) + ws_error: ErrorResponse = ErrorResponse( + "WebSocketException in ListenWebSocketClient._keep_alive", + f"{e}", + "WebSocketException", + ) + self._emit( + LiveTranscriptionEvents(LiveTranscriptionEvents.Error), ws_error + ) + + # signal exit and close + self._signal_exit() + + self._logger.debug("ListenWebSocketClient._keep_alive LEAVE") + + if self._config.options.get("termination_exception") == "true": + raise + return + + except Exception as e: # pylint: disable=broad-except + self._logger.error( + "Exception in ListenWebSocketClient._keep_alive: %s", e + ) + e_error: ErrorResponse = ErrorResponse( + "Exception in ListenWebSocketClient._keep_alive", + f"{e}", + "Exception", + ) + self._logger.error( + "Exception in ListenWebSocketClient._keep_alive: %s", str(e) + ) + self._emit( + LiveTranscriptionEvents(LiveTranscriptionEvents.Error), e_error + ) + + # signal exit and close + self._signal_exit() + + self._logger.debug("ListenWebSocketClient._keep_alive LEAVE") + + if self._config.options.get("termination_exception") == "true": + raise + return + + # pylint: enable=too-many-return-statements + + ## pylint: disable=too-many-return-statements,too-many-statements + def _flush(self) -> None: + self._logger.debug("ListenWebSocketClient._flush ENTER") + + delta_in_ms_str = self._config.options.get("auto_flush_reply_delta") + if delta_in_ms_str is None: + self._logger.error("auto_flush_reply_delta is None") + self._logger.debug("ListenWebSocketClient._flush LEAVE") + return + delta_in_ms = float(delta_in_ms_str) + + while True: + try: + self._flush_event.wait(timeout=HALF_SECOND) + + if self._exit_event.is_set(): + self._logger.notice("_flush exiting gracefully") + self._logger.debug("ListenWebSocketClient._flush LEAVE") + return + + if self._socket is None: + self._logger.debug("socket is None, exiting flush") + self._logger.debug("ListenWebSocketClient._flush LEAVE") + return + + with self._lock_flush: + if self._last_datagram is None: + self._logger.debug("AutoFlush last_datagram is None") + continue + + delta = datetime.now() - self._last_datagram + diff_in_ms = delta.total_seconds() * 1000 + self._logger.debug("AutoFlush delta: %f", diff_in_ms) + if diff_in_ms < delta_in_ms: + self._logger.debug("AutoFlush delta is less than threshold") + continue + + with self._lock_flush: + self._last_datagram = None + self.finalize() + + except websockets.exceptions.ConnectionClosedOK as e: + self._logger.notice(f"_flush({e.code}) exiting gracefully") + self._logger.debug("ListenWebSocketClient._flush LEAVE") + return + + except websockets.exceptions.ConnectionClosed as e: + if e.code == 1000: + self._logger.notice(f"_flush({e.code}) exiting gracefully") + self._logger.debug("ListenWebSocketClient._flush LEAVE") + return + + self._logger.error( + "ConnectionClosed in ListenWebSocketClient._flush with code %s: %s", + e.code, + e.reason, + ) + cc_error: ErrorResponse = ErrorResponse( + "ConnectionClosed in ListenWebSocketClient._flush", + f"{e}", + "ConnectionClosed", + ) + self._emit( + LiveTranscriptionEvents(LiveTranscriptionEvents.Error), cc_error + ) + + # signal exit and close + self._signal_exit() + + self._logger.debug("ListenWebSocketClient._flush LEAVE") + + if self._config.options.get("termination_exception") == "true": + raise + return + + except websockets.exceptions.WebSocketException as e: + self._logger.error( + "WebSocketException in ListenWebSocketClient._flush with: %s", e + ) + ws_error: ErrorResponse = ErrorResponse( + "WebSocketException in ListenWebSocketClient._flush", + f"{e}", + "WebSocketException", + ) + self._emit( + LiveTranscriptionEvents(LiveTranscriptionEvents.Error), ws_error + ) + + # signal exit and close + self._signal_exit() + + self._logger.debug("ListenWebSocketClient._flush LEAVE") + + if self._config.options.get("termination_exception") == "true": + raise + return + + except Exception as e: # pylint: disable=broad-except + self._logger.error("Exception in ListenWebSocketClient._flush: %s", e) + e_error: ErrorResponse = ErrorResponse( + "Exception in ListenWebSocketClient._flush", + f"{e}", + "Exception", + ) + self._logger.error( + "Exception in ListenWebSocketClient._flush: %s", str(e) + ) + self._emit( + LiveTranscriptionEvents(LiveTranscriptionEvents.Error), e_error + ) + + # signal exit and close + self._signal_exit() + + self._logger.debug("ListenWebSocketClient._flush LEAVE") + + if self._config.options.get("termination_exception") == "true": + raise + return + + # pylint: enable=too-many-return-statements + + # pylint: disable=too-many-return-statements + def send(self, data: Union[str, bytes]) -> bool: + """ + Sends data over the WebSocket connection. + """ + self._logger.spam("ListenWebSocketClient.send ENTER") + + if self._exit_event.is_set(): + self._logger.notice("send exiting gracefully") + self._logger.debug("ListenWebSocketClient.send LEAVE") + return False + + if self._socket is not None: + with self._lock_send: + try: + self._socket.send(data) + except websockets.exceptions.ConnectionClosedOK as e: + self._logger.notice(f"send() exiting gracefully: {e.code}") + self._logger.debug("ListenWebSocketClient._keep_alive LEAVE") + if self._config.options.get("termination_exception_send") == "true": + raise + return True + except websockets.exceptions.ConnectionClosed as e: + if e.code == 1000: + self._logger.notice(f"send({e.code}) exiting gracefully") + self._logger.debug("ListenWebSocketClient.send LEAVE") + if ( + self._config.options.get("termination_exception_send") + == "true" + ): + raise + return True + self._logger.error("send() failed - ConnectionClosed: %s", str(e)) + self._logger.spam("ListenWebSocketClient.send LEAVE") + if self._config.options.get("termination_exception_send") == "true": + raise + return False + except websockets.exceptions.WebSocketException as e: + self._logger.error("send() failed - WebSocketException: %s", str(e)) + self._logger.spam("ListenWebSocketClient.send LEAVE") + if self._config.options.get("termination_exception_send") == "true": + raise + return False + except Exception as e: # pylint: disable=broad-except + self._logger.error("send() failed - Exception: %s", str(e)) + self._logger.spam("ListenWebSocketClient.send LEAVE") + if self._config.options.get("termination_exception_send") == "true": + raise + return False + + self._logger.spam("send() succeeded") + self._logger.spam("ListenWebSocketClient.send LEAVE") + return True + + self._logger.spam("send() failed. socket is None") + self._logger.spam("ListenWebSocketClient.send LEAVE") + return False + + # pylint: enable=too-many-return-statements + + def keep_alive(self) -> bool: + """ + Sends a KeepAlive message + """ + self._logger.spam("ListenWebSocketClient.keep_alive ENTER") + + if self._exit_event.is_set(): + self._logger.notice("keep_alive exiting gracefully") + self._logger.debug("ListenWebSocketClient.keep_alive LEAVE") + return False + + if self._socket is None: + self._logger.notice("socket is not intialized") + self._logger.debug("ListenWebSocketClient.keep_alive LEAVE") + return False + + self._logger.notice("Sending KeepAlive...") + ret = self.send(json.dumps({"type": "KeepAlive"})) + + if not ret: + self._logger.error("keep_alive failed") + self._logger.spam("ListenWebSocketClient.keep_alive LEAVE") + return False + + self._logger.notice("keep_alive succeeded") + self._logger.spam("ListenWebSocketClient.keep_alive LEAVE") + + return True + + def finalize(self) -> bool: + """ + Finalizes the Transcript connection by flushing it + """ + self._logger.spam("ListenWebSocketClient.finalize ENTER") + + if self._exit_event.is_set(): + self._logger.notice("finalize exiting gracefully") + self._logger.debug("ListenWebSocketClient.finalize LEAVE") + return False + + if self._socket is None: + self._logger.notice("socket is not intialized") + self._logger.debug("ListenWebSocketClient.finalize LEAVE") + return False + + self._logger.notice("Sending Finalize...") + ret = self.send(json.dumps({"type": "Finalize"})) + + if not ret: + self._logger.error("finalize failed") + self._logger.spam("ListenWebSocketClient.finalize LEAVE") + return False + + self._logger.notice("finalize succeeded") + self._logger.spam("ListenWebSocketClient.finalize LEAVE") + + return True + + # closes the WebSocket connection gracefully + def finish(self) -> bool: + """ + Closes the WebSocket connection gracefully. + """ + self._logger.spam("ListenWebSocketClient.finish ENTER") + + # debug the threads + for thread in threading.enumerate(): + self._logger.debug("before running thread: %s", thread.name) + self._logger.debug("number of active threads: %s", threading.active_count()) + + # signal exit + self._signal_exit() + + # stop the threads + self._logger.verbose("cancelling tasks...") + if self._flush_thread is not None: + self._flush_thread.join() + self._flush_thread = None + self._logger.notice("processing _flush_thread thread joined") + + if self._keep_alive_thread is not None: + self._keep_alive_thread.join() + self._keep_alive_thread = None + self._logger.notice("processing _keep_alive_thread thread joined") + + if self._listen_thread is not None: + self._listen_thread.join() + self._listen_thread = None + self._logger.notice("listening thread joined") + + # debug the threads + for thread in threading.enumerate(): + self._logger.debug("before running thread: %s", thread.name) + self._logger.debug("number of active threads: %s", threading.active_count()) + + self._logger.notice("finish succeeded") + self._logger.spam("ListenWebSocketClient.finish LEAVE") + return True + + # signals the WebSocket connection to exit + def _signal_exit(self) -> None: + # closes the WebSocket connection gracefully + self._logger.notice("closing socket...") + if self._socket is not None: + self._logger.notice("sending CloseStream...") + try: + # if the socket connection is closed, the following line might throw an error + self._socket.send(json.dumps({"type": "CloseStream"})) + except websockets.exceptions.ConnectionClosedOK as e: + self._logger.notice("_signal_exit - ConnectionClosedOK: %s", e.code) + except websockets.exceptions.ConnectionClosed as e: + self._logger.error("_signal_exit - ConnectionClosed: %s", e.code) + except websockets.exceptions.WebSocketException as e: + self._logger.error("_signal_exit - WebSocketException: %s", str(e)) + except Exception as e: # pylint: disable=broad-except + self._logger.error("_signal_exit - Exception: %s", str(e)) + + # push close event + try: + self._emit( + LiveTranscriptionEvents(LiveTranscriptionEvents.Close), + CloseResponse(type=LiveTranscriptionEvents.Close), + ) + except Exception as e: # pylint: disable=broad-except + self._logger.error("_signal_exit - Exception: %s", e) + + # wait for task to send + time.sleep(0.5) + + # signal exit + self._exit_event.set() + + # closes the WebSocket connection gracefully + self._logger.verbose("clean up socket...") + if self._socket is not None: + self._logger.verbose("socket.wait_closed...") + try: + self._socket.close() + except websockets.exceptions.WebSocketException as e: + self._logger.error("socket.wait_closed failed: %s", e) + + self._socket = None # type: ignore + + def _inspect(self, msg_result: LiveResultResponse) -> bool: + sentence = msg_result.channel.alternatives[0].transcript + if len(sentence) == 0: + return True + + if msg_result.is_final: + with self._lock_flush: + self._logger.debug("AutoFlush is_final received") + self._last_datagram = None + else: + with self._lock_flush: + self._last_datagram = datetime.now() + self._logger.debug( + "AutoFlush interim received: %s", + str(self._last_datagram), + ) + + return True diff --git a/deepgram/clients/live/v1/options.py b/deepgram/clients/listen/v1/websocket/options.py similarity index 100% rename from deepgram/clients/live/v1/options.py rename to deepgram/clients/listen/v1/websocket/options.py diff --git a/deepgram/clients/live/v1/response.py b/deepgram/clients/listen/v1/websocket/response.py similarity index 100% rename from deepgram/clients/live/v1/response.py rename to deepgram/clients/listen/v1/websocket/response.py diff --git a/deepgram/clients/listen.py b/deepgram/clients/listen_router.py similarity index 61% rename from deepgram/clients/listen.py rename to deepgram/clients/listen_router.py index f69254f2..20b35261 100644 --- a/deepgram/clients/listen.py +++ b/deepgram/clients/listen_router.py @@ -4,7 +4,15 @@ from importlib import import_module import logging - +import deprecation # type: ignore + +from .. import __version__ +from .listen.v1 import ( + PreRecordedClient, + AsyncPreRecordedClient, + LiveClient, + AsyncLiveClient, +) from ..utils import verboselogs from ..options import DeepgramClientOptions from .errors import DeepgramModuleError @@ -41,33 +49,85 @@ def __init__(self, config: DeepgramClientOptions): self._config = config @property + @deprecation.deprecated( + deprecated_in="3.4.0", + removed_in="4.0.0", + current_version=__version__, + details="deepgram.listen.prerecorded is deprecated. Use deepgram.listen.rest instead.", + ) def prerecorded(self): """ - Returns a PreRecordedClient instance for interacting with Deepgram's prerecorded transcription services. + DEPRECATED: deepgram.listen.prerecorded is deprecated. Use deepgram.listen.rest instead. """ return self.Version(self._config, "prerecorded") @property + @deprecation.deprecated( + deprecated_in="3.4.0", + removed_in="4.0.0", + current_version=__version__, + details="deepgram.listen.asyncprerecorded is deprecated. Use deepgram.listen.asyncrest instead.", + ) def asyncprerecorded(self): """ - Returns an AsyncPreRecordedClient instance for interacting with Deepgram's prerecorded transcription services. + DEPRECATED: deepgram.listen.asyncprerecorded is deprecated. Use deepgram.listen.asyncrest instead. """ return self.Version(self._config, "asyncprerecorded") @property + @deprecation.deprecated( + deprecated_in="3.4.0", + removed_in="4.0.0", + current_version=__version__, + details="deepgram.listen.live is deprecated. Use deepgram.listen.websocket instead.", + ) def live(self): """ - Returns a LiveClient instance for interacting with Deepgram's transcription services. + DEPRECATED: deepgram.listen.live is deprecated. Use deepgram.listen.websocket instead. """ return self.Version(self._config, "live") @property + @deprecation.deprecated( + deprecated_in="3.4.0", + removed_in="4.0.0", + current_version=__version__, + details="deepgram.listen.asynclive is deprecated. Use deepgram.listen.asyncwebsocket instead.", + ) def asynclive(self): """ - Returns an AsyncLiveClient instance for interacting with Deepgram's transcription services. + DEPRECATED: deepgram.listen.asynclive is deprecated. Use deepgram.listen.asyncwebsocket instead. """ return self.Version(self._config, "asynclive") + @property + def rest(self): + """ + Returns a ListenRESTClient instance for interacting with Deepgram's prerecorded transcription services. + """ + return self.Version(self._config, "rest") + + @property + def asyncrest(self): + """ + Returns an AsyncListenRESTClient instance for interacting with Deepgram's prerecorded transcription services. + """ + return self.Version(self._config, "asyncrest") + + @property + def websocket(self): + """ + Returns a ListenWebSocketClient instance for interacting with Deepgram's transcription services. + """ + return self.Version(self._config, "websocket") + + @property + def asyncwebsocket(self): + """ + Returns an AsyncListenWebSocketClient instance for interacting with Deepgram's transcription services. + """ + return self.Version(self._config, "asyncwebsocket") + # INTERNAL CLASSES class Version: """ @@ -108,33 +168,41 @@ def v(self, version: str = ""): self._logger.debug("Version.v LEAVE") raise DeepgramModuleError("Invalid module version") - parent = "" + protocol = "" file_name = "" class_name = "" match self._parent: case "live": - parent = "live" - file_name = "client" - class_name = "LiveClient" + return LiveClient(self._config) case "asynclive": - parent = "live" - file_name = "async_client" - class_name = "AsyncLiveClient" + return AsyncLiveClient(self._config) case "prerecorded": - parent = "prerecorded" - file_name = "client" - class_name = "PreRecordedClient" + return PreRecordedClient(self._config) case "asyncprerecorded": - parent = "prerecorded" + return AsyncPreRecordedClient(self._config) + case "websocket": + protocol = "websocket" + file_name = "client" + class_name = "ListenWebSocketClient" + case "asyncwebsocket": + protocol = "websocket" + file_name = "async_client" + class_name = "AsyncListenWebSocketClient" + case "rest": + protocol = "rest" + file_name = "client" + class_name = "ListenRESTClient" + case "asyncrest": + protocol = "rest" file_name = "async_client" - class_name = "AsyncPreRecordedClient" + class_name = "AsyncListenRESTClient" case _: self._logger.error("parent unknown: %s", self._parent) self._logger.debug("Version.v LEAVE") raise DeepgramModuleError("Invalid parent type") # create class path - path = f"deepgram.clients.{parent}.v{version}.{file_name}" + path = f"deepgram.clients.listen.v{version}.{protocol}.{file_name}" self._logger.info("path: %s", path) self._logger.info("class_name: %s", class_name) diff --git a/deepgram/clients/live/__init__.py b/deepgram/clients/live/__init__.py index 71e167c3..7c40e7f6 100644 --- a/deepgram/clients/live/__init__.py +++ b/deepgram/clients/live/__init__.py @@ -2,11 +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 -from .client import LiveClient -from .client import AsyncLiveClient -from .client import LiveOptions -from .enums import LiveTranscriptionEvents -from .client import ( +from .v1 import LiveTranscriptionEvents +from .v1 import DeepgramError, DeepgramTypeError, DeepgramWebsocketError + +from .v1 import LiveClient +from .v1 import AsyncLiveClient +from .v1 import LiveOptions +from .v1 import ( OpenResponse, LiveResultResponse, MetadataResponse, @@ -16,5 +18,3 @@ ErrorResponse, UnhandledResponse, ) - -from ...options import DeepgramClientOptions, ClientOptionsFromEnv diff --git a/deepgram/clients/live/client.py b/deepgram/clients/live/client.py deleted file mode 100644 index d884b455..00000000 --- a/deepgram/clients/live/client.py +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright 2023-2024 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 .v1.client import LiveClient as LiveClientLatest -from .v1.async_client import AsyncLiveClient as AsyncLiveClientLatest -from .v1.options import LiveOptions as LiveOptionsLatest -from .v1.response import ( - OpenResponse as OpenResponseLatest, - LiveResultResponse as LiveResultResponseLatest, - MetadataResponse as MetadataResponseLatest, - SpeechStartedResponse as SpeechStartedResponseLatest, - UtteranceEndResponse as UtteranceEndResponseLatest, - CloseResponse as CloseResponseLatest, - ErrorResponse as ErrorResponseLatest, - UnhandledResponse as UnhandledResponseLatest, -) - -# The vX/client.py points to the current supported version in the SDK. -# Older versions are supported in the SDK for backwards compatibility. - - -# input -LiveOptions = LiveOptionsLatest -OpenResponse = OpenResponseLatest -LiveResultResponse = LiveResultResponseLatest -MetadataResponse = MetadataResponseLatest -SpeechStartedResponse = SpeechStartedResponseLatest -UtteranceEndResponse = UtteranceEndResponseLatest -CloseResponse = CloseResponseLatest -ErrorResponse = ErrorResponseLatest -UnhandledResponse = UnhandledResponseLatest - - -# clients -LiveClient = LiveClientLatest -AsyncLiveClient = AsyncLiveClientLatest diff --git a/deepgram/clients/live/v1/__init__.py b/deepgram/clients/live/v1/__init__.py index 7ff55de4..8c63f963 100644 --- a/deepgram/clients/live/v1/__init__.py +++ b/deepgram/clients/live/v1/__init__.py @@ -2,11 +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 +from .enums import LiveTranscriptionEvents +from .errors import DeepgramError, DeepgramTypeError, DeepgramWebsocketError + from .client import LiveClient -from .async_client import AsyncLiveClient -from .options import LiveOptions -from ....options import DeepgramClientOptions, ClientOptionsFromEnv -from .response import ( +from .client import AsyncLiveClient +from .client import LiveOptions +from .client import ( OpenResponse, LiveResultResponse, MetadataResponse, diff --git a/deepgram/clients/live/v1/client.py b/deepgram/clients/live/v1/client.py index aaad979a..cce54611 100644 --- a/deepgram/clients/live/v1/client.py +++ b/deepgram/clients/live/v1/client.py @@ -1,888 +1,37 @@ # Copyright 2023-2024 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 json -import time -import logging -from typing import Dict, Union, Optional, cast, Any -from datetime import datetime -import threading -from websockets.sync.client import connect, ClientConnection -import websockets - -from ....utils import verboselogs -from ....options import DeepgramClientOptions -from ..enums import LiveTranscriptionEvents -from ..helpers import convert_to_websocket_url, append_query_params -from ..errors import DeepgramError - -from .response import ( - OpenResponse, - LiveResultResponse, - MetadataResponse, - SpeechStartedResponse, - UtteranceEndResponse, - CloseResponse, - ErrorResponse, - UnhandledResponse, +from ...listen.v1 import ListenWebSocketClient as LiveClientLatest +from ...listen.v1 import AsyncListenWebSocketClient as AsyncLiveClientLatest +from ...listen.v1 import LiveOptions as LiveOptionsLatest +from ...listen.v1 import ( + OpenResponse as OpenResponseLatest, + LiveResultResponse as LiveResultResponseLatest, + MetadataResponse as MetadataResponseLatest, + SpeechStartedResponse as SpeechStartedResponseLatest, + UtteranceEndResponse as UtteranceEndResponseLatest, + CloseResponse as CloseResponseLatest, + ErrorResponse as ErrorResponseLatest, + UnhandledResponse as UnhandledResponseLatest, ) -from .options import LiveOptions - -ONE_SECOND = 1 -HALF_SECOND = 0.5 -DEEPGRAM_INTERVAL = 5 -PING_INTERVAL = 20 - - -class LiveClient: # pylint: disable=too-many-instance-attributes - """ - 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. - """ - - _logger: verboselogs.VerboseLogger - _config: DeepgramClientOptions - _endpoint: str - _websocket_url: str - - _socket: ClientConnection - _exit_event: threading.Event - _lock_send: threading.Lock - _lock_flush: threading.Lock - _event_handlers: Dict[LiveTranscriptionEvents, list] - - _last_datagram: Optional[datetime] = None - - _listen_thread: Union[threading.Thread, None] - _keep_alive_thread: Union[threading.Thread, None] - _flush_thread: Union[threading.Thread, None] - - _kwargs: Optional[Dict] = None - _addons: Optional[Dict] = None - _options: Optional[Dict] = None - _headers: Optional[Dict] = None - - def __init__(self, config: DeepgramClientOptions): - if config is None: - raise DeepgramError("Config are required") - - self._logger = verboselogs.VerboseLogger(__name__) - self._logger.addHandler(logging.StreamHandler()) - self._logger.setLevel(config.verbose) - - self._config = config - self._endpoint = "v1/listen" - self._lock_send = threading.Lock() - - self._flush_thread = None - self._keep_alive_thread = None - self._listen_thread = None - - # exit - self._exit_event = threading.Event() - - # auto flush - self._last_datagram = None - self._flush_event = threading.Event() - self._lock_flush = threading.Lock() - - self._event_handlers = { - event: [] for event in LiveTranscriptionEvents.__members__.values() - } - self._websocket_url = convert_to_websocket_url(self._config.url, self._endpoint) - - # pylint: disable=too-many-statements,too-many-branches - def start( - self, - options: Optional[Union[LiveOptions, Dict]] = None, - addons: Optional[Dict] = None, - headers: Optional[Dict] = None, - members: Optional[Dict] = None, - **kwargs, - ) -> bool: - """ - Starts the WebSocket connection for live transcription. - """ - self._logger.debug("LiveClient.start ENTER") - self._logger.info("options: %s", options) - self._logger.info("addons: %s", addons) - self._logger.info("headers: %s", headers) - self._logger.info("members: %s", members) - self._logger.info("kwargs: %s", kwargs) - - if isinstance(options, LiveOptions) and not options.check(): - self._logger.error("options.check failed") - self._logger.debug("LiveClient.start LEAVE") - raise DeepgramError("Fatal transcription options error") - - self._addons = addons - self._headers = headers - - # add "members" as members of the class - if members is not None: - self.__dict__.update(members) - - # set kwargs as members of the class - if kwargs is not None: - self._kwargs = kwargs - else: - self._kwargs = {} - - if isinstance(options, LiveOptions): - self._logger.info("LiveOptions switching class -> dict") - self._options = options.to_dict() - elif options is not None: - self._options = options - else: - self._options = {} - - combined_options = self._options - if self._addons is not None: - self._logger.info("merging addons to options") - combined_options.update(self._addons) - self._logger.info("new options: %s", combined_options) - self._logger.debug("combined_options: %s", combined_options) - - combined_headers = self._config.headers - if self._headers is not None: - self._logger.info("merging headers to options") - combined_headers.update(self._headers) - self._logger.info("new headers: %s", combined_headers) - self._logger.debug("combined_headers: %s", combined_headers) - - url_with_params = append_query_params(self._websocket_url, combined_options) - try: - self._socket = connect(url_with_params, additional_headers=combined_headers) - self._exit_event.clear() - - # debug the threads - for thread in threading.enumerate(): - self._logger.debug("after running thread: %s", thread.name) - self._logger.debug("number of active threads: %s", threading.active_count()) - - # listening thread - self._listen_thread = threading.Thread(target=self._listening) - self._listen_thread.start() - - # keepalive thread - if self._config.is_keep_alive_enabled(): - self._logger.notice("keepalive is enabled") - self._keep_alive_thread = threading.Thread(target=self._keep_alive) - self._keep_alive_thread.start() - else: - self._logger.notice("keepalive is disabled") - - # flush thread - if self._config.is_auto_flush_enabled(): - self._logger.notice("autoflush is enabled") - self._flush_thread = threading.Thread(target=self._flush) - self._flush_thread.start() - else: - self._logger.notice("autoflush is disabled") - - # debug the threads - for thread in threading.enumerate(): - self._logger.debug("after running thread: %s", thread.name) - self._logger.debug("number of active threads: %s", threading.active_count()) - - # push open event - self._emit( - LiveTranscriptionEvents(LiveTranscriptionEvents.Open), - OpenResponse(type=LiveTranscriptionEvents.Open), - ) - - self._logger.notice("start succeeded") - self._logger.debug("LiveClient.start LEAVE") - return True - except websockets.ConnectionClosed as e: - self._logger.error("ConnectionClosed in LiveClient.start: %s", e) - self._logger.debug("LiveClient.start LEAVE") - if self._config.options.get("termination_exception_connect") == "true": - raise e - return False - except websockets.exceptions.WebSocketException as e: - self._logger.error("WebSocketException in LiveClient.start: %s", e) - self._logger.debug("LiveClient.start LEAVE") - if self._config.options.get("termination_exception_connect") == "true": - raise e - return False - except Exception as e: # pylint: disable=broad-except - self._logger.error("WebSocketException in LiveClient.start: %s", e) - self._logger.debug("LiveClient.start LEAVE") - if self._config.options.get("termination_exception_connect") == "true": - raise e - return False - - # pylint: enable=too-many-statements,too-many-branches - - def on( - self, event: LiveTranscriptionEvents, handler - ) -> None: # registers event handlers for specific events - """ - Registers event handlers for specific events. - """ - self._logger.info("event subscribed: %s", event) - if event in LiveTranscriptionEvents.__members__.values() and callable(handler): - self._event_handlers[event].append(handler) - - def _emit(self, event: LiveTranscriptionEvents, *args, **kwargs) -> None: - """ - Emits events to the registered event handlers. - """ - self._logger.debug("callback handlers for: %s", event) - for handler in self._event_handlers[event]: - handler(self, *args, **kwargs) - - # pylint: disable=too-many-return-statements,too-many-statements,too-many-locals,too-many-branches - def _listening( - self, - ) -> None: - """ - Listens for messages from the WebSocket connection. - """ - self._logger.debug("LiveClient._listening ENTER") - - while True: - try: - if self._exit_event.is_set(): - self._logger.notice("_listening exiting gracefully") - self._logger.debug("LiveClient._listening LEAVE") - return - - if self._socket is None: - self._logger.warning("socket is empty") - self._logger.debug("LiveClient._listening LEAVE") - return - - message = str(self._socket.recv()) - - if message is None: - self._logger.info("message is empty") - continue - - data = json.loads(message) - response_type = data.get("type") - self._logger.debug("response_type: %s, data: %s", response_type, data) - - match response_type: - case LiveTranscriptionEvents.Open: - open_result: OpenResponse = OpenResponse.from_json(message) - self._logger.verbose("OpenResponse: %s", open_result) - self._emit( - LiveTranscriptionEvents(LiveTranscriptionEvents.Open), - open=open_result, - **dict(cast(Dict[Any, Any], self._kwargs)), - ) - case LiveTranscriptionEvents.Transcript: - msg_result: LiveResultResponse = LiveResultResponse.from_json( - message - ) - self._logger.verbose("LiveResultResponse: %s", msg_result) - - # auto flush - if self._config.is_inspecting_messages(): - inspect_res = self._inspect(msg_result) - if not inspect_res: - self._logger.error("inspect_res failed") - - self._emit( - LiveTranscriptionEvents(LiveTranscriptionEvents.Transcript), - result=msg_result, - **dict(cast(Dict[Any, Any], self._kwargs)), - ) - case LiveTranscriptionEvents.Metadata: - meta_result: MetadataResponse = MetadataResponse.from_json( - message - ) - self._logger.verbose("MetadataResponse: %s", meta_result) - self._emit( - LiveTranscriptionEvents(LiveTranscriptionEvents.Metadata), - metadata=meta_result, - **dict(cast(Dict[Any, Any], self._kwargs)), - ) - case LiveTranscriptionEvents.SpeechStarted: - ss_result: SpeechStartedResponse = ( - SpeechStartedResponse.from_json(message) - ) - self._logger.verbose("SpeechStartedResponse: %s", ss_result) - self._emit( - LiveTranscriptionEvents( - LiveTranscriptionEvents.SpeechStarted - ), - speech_started=ss_result, - **dict(cast(Dict[Any, Any], self._kwargs)), - ) - case LiveTranscriptionEvents.UtteranceEnd: - ue_result: UtteranceEndResponse = ( - UtteranceEndResponse.from_json(message) - ) - self._logger.verbose("UtteranceEndResponse: %s", ue_result) - self._emit( - LiveTranscriptionEvents( - LiveTranscriptionEvents.UtteranceEnd - ), - utterance_end=ue_result, - **dict(cast(Dict[Any, Any], self._kwargs)), - ) - case LiveTranscriptionEvents.Close: - close_result: CloseResponse = CloseResponse.from_json(message) - self._logger.verbose("CloseResponse: %s", close_result) - self._emit( - LiveTranscriptionEvents(LiveTranscriptionEvents.Close), - close=close_result, - **dict(cast(Dict[Any, Any], self._kwargs)), - ) - case LiveTranscriptionEvents.Error: - err_error: ErrorResponse = ErrorResponse.from_json(message) - self._logger.verbose("ErrorResponse: %s", err_error) - self._emit( - LiveTranscriptionEvents(LiveTranscriptionEvents.Error), - error=err_error, - **dict(cast(Dict[Any, Any], self._kwargs)), - ) - case _: - self._logger.warning( - "Unknown Message: response_type: %s, data: %s", - response_type, - data, - ) - unhandled_error: UnhandledResponse = UnhandledResponse( - type=LiveTranscriptionEvents( - LiveTranscriptionEvents.Unhandled - ), - raw=message, - ) - self._emit( - LiveTranscriptionEvents(LiveTranscriptionEvents.Unhandled), - unhandled=unhandled_error, - **dict(cast(Dict[Any, Any], self._kwargs)), - ) - - except websockets.exceptions.ConnectionClosedOK as e: - self._logger.notice(f"_listening({e.code}) exiting gracefully") - self._logger.debug("LiveClient._listening LEAVE") - return - - except websockets.exceptions.ConnectionClosed as e: - if e.code == 1000: - self._logger.notice(f"_listening({e.code}) exiting gracefully") - self._logger.debug("LiveClient._listening LEAVE") - return - - self._logger.error( - "ConnectionClosed in LiveClient._listening with code %s: %s", - e.code, - e.reason, - ) - cc_error: ErrorResponse = ErrorResponse( - "ConnectionClosed in LiveClient._listening", - f"{e}", - "ConnectionClosed", - ) - self._emit( - LiveTranscriptionEvents(LiveTranscriptionEvents.Error), cc_error - ) - - # signal exit and close - self._signal_exit() - - self._logger.debug("LiveClient._listening LEAVE") - - if self._config.options.get("termination_exception") == "true": - raise - return - - except websockets.exceptions.WebSocketException as e: - self._logger.error( - "WebSocketException in LiveClient._listening with: %s", e - ) - ws_error: ErrorResponse = ErrorResponse( - "WebSocketException in LiveClient._listening", - f"{e}", - "WebSocketException", - ) - self._emit( - LiveTranscriptionEvents(LiveTranscriptionEvents.Error), ws_error - ) - - # signal exit and close - self._signal_exit() - - self._logger.debug("LiveClient._listening LEAVE") - - if self._config.options.get("termination_exception") == "true": - raise - return - - except Exception as e: # pylint: disable=broad-except - self._logger.error("Exception in LiveClient._listening: %s", e) - e_error: ErrorResponse = ErrorResponse( - "Exception in LiveClient._listening", - f"{e}", - "Exception", - ) - self._logger.error("Exception in LiveClient._listening: %s", str(e)) - self._emit( - LiveTranscriptionEvents(LiveTranscriptionEvents.Error), e_error - ) - - # signal exit and close - self._signal_exit() - - self._logger.debug("LiveClient._listening LEAVE") - - if self._config.options.get("termination_exception") == "true": - raise - return - - # pylint: enable=too-many-return-statements,too-many-statements - - ## pylint: disable=too-many-return-statements - def _keep_alive(self) -> None: - self._logger.debug("LiveClient._keep_alive ENTER") - - counter = 0 - while True: - try: - counter += 1 - self._exit_event.wait(timeout=ONE_SECOND) - - if self._exit_event.is_set(): - self._logger.notice("_keep_alive exiting gracefully") - self._logger.debug("LiveClient._keep_alive LEAVE") - return - - if self._socket is None: - self._logger.notice("socket is None, exiting keep_alive") - self._logger.debug("LiveClient._keep_alive LEAVE") - return - - # deepgram keepalive - if counter % DEEPGRAM_INTERVAL == 0: - self.keep_alive() - - except websockets.exceptions.ConnectionClosedOK as e: - self._logger.notice(f"_keep_alive({e.code}) exiting gracefully") - self._logger.debug("LiveClient._keep_alive LEAVE") - return - - except websockets.exceptions.ConnectionClosed as e: - if e.code == 1000: - self._logger.notice(f"_keep_alive({e.code}) exiting gracefully") - self._logger.debug("LiveClient._keep_alive LEAVE") - return - - self._logger.error( - "ConnectionClosed in LiveClient._keep_alive with code %s: %s", - e.code, - e.reason, - ) - cc_error: ErrorResponse = ErrorResponse( - "ConnectionClosed in LiveClient._keep_alive", - f"{e}", - "ConnectionClosed", - ) - self._emit( - LiveTranscriptionEvents(LiveTranscriptionEvents.Error), cc_error - ) - - # signal exit and close - self._signal_exit() - - self._logger.debug("LiveClient._keep_alive LEAVE") - - if self._config.options.get("termination_exception") == "true": - raise - return - - except websockets.exceptions.WebSocketException as e: - self._logger.error( - "WebSocketException in LiveClient._keep_alive with: %s", e - ) - ws_error: ErrorResponse = ErrorResponse( - "WebSocketException in LiveClient._keep_alive", - f"{e}", - "WebSocketException", - ) - self._emit( - LiveTranscriptionEvents(LiveTranscriptionEvents.Error), ws_error - ) - - # signal exit and close - self._signal_exit() - - self._logger.debug("LiveClient._keep_alive LEAVE") - - if self._config.options.get("termination_exception") == "true": - raise - return - - except Exception as e: # pylint: disable=broad-except - self._logger.error("Exception in LiveClient._keep_alive: %s", e) - e_error: ErrorResponse = ErrorResponse( - "Exception in LiveClient._keep_alive", - f"{e}", - "Exception", - ) - self._logger.error("Exception in LiveClient._keep_alive: %s", str(e)) - self._emit( - LiveTranscriptionEvents(LiveTranscriptionEvents.Error), e_error - ) - - # signal exit and close - self._signal_exit() - - self._logger.debug("LiveClient._keep_alive LEAVE") - - if self._config.options.get("termination_exception") == "true": - raise - return - - # pylint: enable=too-many-return-statements - - ## pylint: disable=too-many-return-statements,too-many-statements - def _flush(self) -> None: - self._logger.debug("LiveClient._flush ENTER") - - delta_in_ms_str = self._config.options.get("auto_flush_reply_delta") - if delta_in_ms_str is None: - self._logger.error("auto_flush_reply_delta is None") - self._logger.debug("LiveClient._flush LEAVE") - return - delta_in_ms = float(delta_in_ms_str) - - while True: - try: - self._flush_event.wait(timeout=HALF_SECOND) - - if self._exit_event.is_set(): - self._logger.notice("_flush exiting gracefully") - self._logger.debug("LiveClient._flush LEAVE") - return - - if self._socket is None: - self._logger.debug("socket is None, exiting flush") - self._logger.debug("LiveClient._flush LEAVE") - return - - with self._lock_flush: - if self._last_datagram is None: - self._logger.debug("AutoFlush last_datagram is None") - continue - - delta = datetime.now() - self._last_datagram - diff_in_ms = delta.total_seconds() * 1000 - self._logger.debug("AutoFlush delta: %f", diff_in_ms) - if diff_in_ms < delta_in_ms: - self._logger.debug("AutoFlush delta is less than threshold") - continue - - with self._lock_flush: - self._last_datagram = None - self.finalize() - - except websockets.exceptions.ConnectionClosedOK as e: - self._logger.notice(f"_flush({e.code}) exiting gracefully") - self._logger.debug("LiveClient._flush LEAVE") - return - - except websockets.exceptions.ConnectionClosed as e: - if e.code == 1000: - self._logger.notice(f"_flush({e.code}) exiting gracefully") - self._logger.debug("LiveClient._flush LEAVE") - return - - self._logger.error( - "ConnectionClosed in LiveClient._flush with code %s: %s", - e.code, - e.reason, - ) - cc_error: ErrorResponse = ErrorResponse( - "ConnectionClosed in LiveClient._flush", - f"{e}", - "ConnectionClosed", - ) - self._emit( - LiveTranscriptionEvents(LiveTranscriptionEvents.Error), cc_error - ) - - # signal exit and close - self._signal_exit() - - self._logger.debug("LiveClient._flush LEAVE") - - if self._config.options.get("termination_exception") == "true": - raise - return - - except websockets.exceptions.WebSocketException as e: - self._logger.error( - "WebSocketException in LiveClient._flush with: %s", e - ) - ws_error: ErrorResponse = ErrorResponse( - "WebSocketException in LiveClient._flush", - f"{e}", - "WebSocketException", - ) - self._emit( - LiveTranscriptionEvents(LiveTranscriptionEvents.Error), ws_error - ) - - # signal exit and close - self._signal_exit() - - self._logger.debug("LiveClient._flush LEAVE") - - if self._config.options.get("termination_exception") == "true": - raise - return - - except Exception as e: # pylint: disable=broad-except - self._logger.error("Exception in LiveClient._flush: %s", e) - e_error: ErrorResponse = ErrorResponse( - "Exception in LiveClient._flush", - f"{e}", - "Exception", - ) - self._logger.error("Exception in LiveClient._flush: %s", str(e)) - self._emit( - LiveTranscriptionEvents(LiveTranscriptionEvents.Error), e_error - ) - - # signal exit and close - self._signal_exit() - - self._logger.debug("LiveClient._flush LEAVE") - - if self._config.options.get("termination_exception") == "true": - raise - return - - # pylint: enable=too-many-return-statements - - # pylint: disable=too-many-return-statements - def send(self, data: Union[str, bytes]) -> bool: - """ - Sends data over the WebSocket connection. - """ - self._logger.spam("LiveClient.send ENTER") - - if self._exit_event.is_set(): - self._logger.notice("send exiting gracefully") - self._logger.debug("LiveClient.send LEAVE") - return False - - if self._socket is not None: - with self._lock_send: - try: - self._socket.send(data) - except websockets.exceptions.ConnectionClosedOK as e: - self._logger.notice(f"send() exiting gracefully: {e.code}") - self._logger.debug("LiveClient._keep_alive LEAVE") - if self._config.options.get("termination_exception_send") == "true": - raise - return True - except websockets.exceptions.ConnectionClosed as e: - if e.code == 1000: - self._logger.notice(f"send({e.code}) exiting gracefully") - self._logger.debug("LiveClient.send LEAVE") - if ( - self._config.options.get("termination_exception_send") - == "true" - ): - raise - return True - self._logger.error("send() failed - ConnectionClosed: %s", str(e)) - self._logger.spam("LiveClient.send LEAVE") - if self._config.options.get("termination_exception_send") == "true": - raise - return False - except websockets.exceptions.WebSocketException as e: - self._logger.error("send() failed - WebSocketException: %s", str(e)) - self._logger.spam("LiveClient.send LEAVE") - if self._config.options.get("termination_exception_send") == "true": - raise - return False - except Exception as e: # pylint: disable=broad-except - self._logger.error("send() failed - Exception: %s", str(e)) - self._logger.spam("LiveClient.send LEAVE") - if self._config.options.get("termination_exception_send") == "true": - raise - return False - - self._logger.spam("send() succeeded") - self._logger.spam("LiveClient.send LEAVE") - return True - - self._logger.spam("send() failed. socket is None") - self._logger.spam("LiveClient.send LEAVE") - return False - - # pylint: enable=too-many-return-statements - - def keep_alive(self) -> bool: - """ - Sends a KeepAlive message - """ - self._logger.spam("LiveClient.keep_alive ENTER") - - if self._exit_event.is_set(): - self._logger.notice("keep_alive exiting gracefully") - self._logger.debug("LiveClient.keep_alive LEAVE") - return False - - if self._socket is None: - self._logger.notice("socket is not intialized") - self._logger.debug("LiveClient.keep_alive LEAVE") - return False - - self._logger.notice("Sending KeepAlive...") - ret = self.send(json.dumps({"type": "KeepAlive"})) - - if not ret: - self._logger.error("keep_alive failed") - self._logger.spam("LiveClient.keep_alive LEAVE") - return False - - self._logger.notice("keep_alive succeeded") - self._logger.spam("LiveClient.keep_alive LEAVE") - - return True - - def finalize(self) -> bool: - """ - Finalizes the Transcript connection by flushing it - """ - self._logger.spam("LiveClient.finalize ENTER") - - if self._exit_event.is_set(): - self._logger.notice("finalize exiting gracefully") - self._logger.debug("LiveClient.finalize LEAVE") - return False - - if self._socket is None: - self._logger.notice("socket is not intialized") - self._logger.debug("LiveClient.finalize LEAVE") - return False - - self._logger.notice("Sending Finalize...") - ret = self.send(json.dumps({"type": "Finalize"})) - - if not ret: - self._logger.error("finalize failed") - self._logger.spam("LiveClient.finalize LEAVE") - return False - - self._logger.notice("finalize succeeded") - self._logger.spam("LiveClient.finalize LEAVE") - - return True - - # closes the WebSocket connection gracefully - def finish(self) -> bool: - """ - Closes the WebSocket connection gracefully. - """ - self._logger.spam("LiveClient.finish ENTER") - - # debug the threads - for thread in threading.enumerate(): - self._logger.debug("before running thread: %s", thread.name) - self._logger.debug("number of active threads: %s", threading.active_count()) - - # signal exit - self._signal_exit() - - # stop the threads - self._logger.verbose("cancelling tasks...") - if self._flush_thread is not None: - self._flush_thread.join() - self._flush_thread = None - self._logger.notice("processing _flush_thread thread joined") - - if self._keep_alive_thread is not None: - self._keep_alive_thread.join() - self._keep_alive_thread = None - self._logger.notice("processing _keep_alive_thread thread joined") - - if self._listen_thread is not None: - self._listen_thread.join() - self._listen_thread = None - self._logger.notice("listening thread joined") - - # debug the threads - for thread in threading.enumerate(): - self._logger.debug("before running thread: %s", thread.name) - self._logger.debug("number of active threads: %s", threading.active_count()) - - self._logger.notice("finish succeeded") - self._logger.spam("LiveClient.finish LEAVE") - return True - - # signals the WebSocket connection to exit - def _signal_exit(self) -> None: - # closes the WebSocket connection gracefully - self._logger.notice("closing socket...") - if self._socket is not None: - self._logger.notice("sending CloseStream...") - try: - # if the socket connection is closed, the following line might throw an error - self._socket.send(json.dumps({"type": "CloseStream"})) - except websockets.exceptions.ConnectionClosedOK as e: - self._logger.notice("_signal_exit - ConnectionClosedOK: %s", e.code) - except websockets.exceptions.ConnectionClosed as e: - self._logger.error("_signal_exit - ConnectionClosed: %s", e.code) - except websockets.exceptions.WebSocketException as e: - self._logger.error("_signal_exit - WebSocketException: %s", str(e)) - except Exception as e: # pylint: disable=broad-except - self._logger.error("_signal_exit - Exception: %s", str(e)) - - # push close event - try: - self._emit( - LiveTranscriptionEvents(LiveTranscriptionEvents.Close), - CloseResponse(type=LiveTranscriptionEvents.Close), - ) - except Exception as e: # pylint: disable=broad-except - self._logger.error("_signal_exit - Exception: %s", e) - - # wait for task to send - time.sleep(0.5) - - # signal exit - self._exit_event.set() - # closes the WebSocket connection gracefully - self._logger.verbose("clean up socket...") - if self._socket is not None: - self._logger.verbose("socket.wait_closed...") - try: - self._socket.close() - except websockets.exceptions.WebSocketException as e: - self._logger.error("socket.wait_closed failed: %s", e) +# The vX/client.py points to the current supported version in the SDK. +# Older versions are supported in the SDK for backwards compatibility. - self._socket = None # type: ignore - def _inspect(self, msg_result: LiveResultResponse) -> bool: - sentence = msg_result.channel.alternatives[0].transcript - if len(sentence) == 0: - return True +# input +LiveOptions = LiveOptionsLatest +OpenResponse = OpenResponseLatest +LiveResultResponse = LiveResultResponseLatest +MetadataResponse = MetadataResponseLatest +SpeechStartedResponse = SpeechStartedResponseLatest +UtteranceEndResponse = UtteranceEndResponseLatest +CloseResponse = CloseResponseLatest +ErrorResponse = ErrorResponseLatest +UnhandledResponse = UnhandledResponseLatest - if msg_result.is_final: - with self._lock_flush: - self._logger.debug("AutoFlush is_final received") - self._last_datagram = None - else: - with self._lock_flush: - self._last_datagram = datetime.now() - self._logger.debug( - "AutoFlush interim received: %s", - str(self._last_datagram), - ) - return True +# clients +LiveClient = LiveClientLatest +AsyncLiveClient = AsyncLiveClientLatest diff --git a/deepgram/clients/live/v1/enums.py b/deepgram/clients/live/v1/enums.py new file mode 100644 index 00000000..ee98540f --- /dev/null +++ b/deepgram/clients/live/v1/enums.py @@ -0,0 +1,5 @@ +# Copyright 2023-2024 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 ...listen import LiveTranscriptionEvents diff --git a/deepgram/clients/live/v1/errors.py b/deepgram/clients/live/v1/errors.py new file mode 100644 index 00000000..b156a3fe --- /dev/null +++ b/deepgram/clients/live/v1/errors.py @@ -0,0 +1,5 @@ +# Copyright 2023-2024 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 ...listen.errors import DeepgramError, DeepgramTypeError, DeepgramWebsocketError diff --git a/deepgram/clients/prerecorded/__init__.py b/deepgram/clients/prerecorded/__init__.py index cef06476..0c28ab56 100644 --- a/deepgram/clients/prerecorded/__init__.py +++ b/deepgram/clients/prerecorded/__init__.py @@ -2,19 +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 .client import PreRecordedClient -from .client import AsyncPreRecordedClient -from .client import PrerecordedOptions -from .client import ( +from ...options import DeepgramClientOptions, ClientOptionsFromEnv + +from .v1 import PreRecordedClient +from .v1 import AsyncPreRecordedClient +from .v1 import PrerecordedOptions +from .v1 import ( UrlSource, FileSource, PreRecordedStreamSource, PrerecordedSource, ) -from .client import ( +from .v1 import ( AsyncPrerecordedResponse, PrerecordedResponse, SyncPrerecordedResponse, ) - -from ...options import DeepgramClientOptions, ClientOptionsFromEnv diff --git a/deepgram/clients/prerecorded/client.py b/deepgram/clients/prerecorded/client.py deleted file mode 100644 index 4db4474c..00000000 --- a/deepgram/clients/prerecorded/client.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2023-2024 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 .v1.client import PreRecordedClient as PreRecordedClientLatest -from .v1.async_client import AsyncPreRecordedClient as AsyncPreRecordedClientLatest -from .v1.options import ( - PrerecordedOptions as PrerecordedOptionsLatest, - UrlSource as UrlSourceLatest, - FileSource as FileSourceLatest, - PreRecordedStreamSource as PreRecordedStreamSourceLatest, - PrerecordedSource as PrerecordedSourceLatest, -) -from .v1.response import ( - AsyncPrerecordedResponse as AsyncPrerecordedResponseLatest, - PrerecordedResponse as PrerecordedResponseLatest, - SyncPrerecordedResponse as SyncPrerecordedResponseLatest, -) - - -# The client.py points to the current supported version in the SDK. -# Older versions are supported in the SDK for backwards compatibility. - - -# input -PrerecordedOptions = PrerecordedOptionsLatest -PreRecordedStreamSource = PreRecordedStreamSourceLatest -UrlSource = UrlSourceLatest -FileSource = FileSourceLatest -PrerecordedSource = PrerecordedSourceLatest - - -# output -AsyncPrerecordedResponse = AsyncPrerecordedResponseLatest -PrerecordedResponse = PrerecordedResponseLatest -SyncPrerecordedResponse = SyncPrerecordedResponseLatest - - -# clients -PreRecordedClient = PreRecordedClientLatest -AsyncPreRecordedClient = AsyncPreRecordedClientLatest diff --git a/deepgram/clients/prerecorded/v1/__init__.py b/deepgram/clients/prerecorded/v1/__init__.py index 65334261..6aafeb12 100644 --- a/deepgram/clients/prerecorded/v1/__init__.py +++ b/deepgram/clients/prerecorded/v1/__init__.py @@ -2,20 +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, ClientOptionsFromEnv + from .client import PreRecordedClient -from .async_client import AsyncPreRecordedClient -from .options import ( - PrerecordedOptions, - FileSource, +from .client import AsyncPreRecordedClient +from .client import PrerecordedOptions +from .client import ( UrlSource, + FileSource, PreRecordedStreamSource, PrerecordedSource, ) -from .response import ( +from .client import ( AsyncPrerecordedResponse, PrerecordedResponse, SyncPrerecordedResponse, - Sentiment, ) - -from ....options import DeepgramClientOptions, ClientOptionsFromEnv diff --git a/deepgram/clients/prerecorded/v1/client.py b/deepgram/clients/prerecorded/v1/client.py index fa38d19b..0a8819d3 100644 --- a/deepgram/clients/prerecorded/v1/client.py +++ b/deepgram/clients/prerecorded/v1/client.py @@ -2,332 +2,36 @@ # Use of this source code is governed by a MIT license that can be found in the LICENSE file. # SPDX-License-Identifier: MIT -import logging -from typing import Dict, Union, Optional - -import httpx - -from ....utils import verboselogs -from ....options import DeepgramClientOptions -from ...abstract_sync_client import AbstractSyncRestClient -from ..errors import DeepgramError, DeepgramTypeError - -from .helpers import is_buffer_source, is_readstream_source, is_url_source -from .options import ( - PrerecordedOptions, - FileSource, - UrlSource, +from ...listen import PreRecordedClient as PreRecordedClientLatest +from ...listen import AsyncPreRecordedClient as AsyncPreRecordedClientLatest +from ...listen import ( + PrerecordedOptions as PrerecordedOptionsLatest, + UrlSource as UrlSourceLatest, + FileSource as FileSourceLatest, + PreRecordedStreamSource as PreRecordedStreamSourceLatest, + PrerecordedSource as PrerecordedSourceLatest, +) +from ...listen import ( + AsyncPrerecordedResponse as AsyncPrerecordedResponseLatest, + PrerecordedResponse as PrerecordedResponseLatest, + SyncPrerecordedResponse as SyncPrerecordedResponseLatest, ) -from .response import AsyncPrerecordedResponse, PrerecordedResponse - - -class PreRecordedClient(AbstractSyncRestClient): - """ - A client class for handling pre-recorded audio data. - Provides methods for transcribing audio from URLs and files. - """ - - _logger: verboselogs.VerboseLogger - _config: DeepgramClientOptions - - def __init__(self, config: DeepgramClientOptions): - self._logger = verboselogs.VerboseLogger(__name__) - self._logger.addHandler(logging.StreamHandler()) - self._logger.setLevel(config.verbose) - self._config = config - super().__init__(config) - - def transcribe_url( - self, - source: UrlSource, - options: Optional[Union[Dict, PrerecordedOptions]] = None, - addons: Optional[Dict] = None, - headers: Optional[Dict] = None, - timeout: Optional[httpx.Timeout] = None, - endpoint: str = "v1/listen", - ) -> Union[AsyncPrerecordedResponse, PrerecordedResponse]: - """ - Transcribes audio from a URL source. - - Args: - source (UrlSource): The URL source of the audio to transcribe. - options (PrerecordedOptions): Additional options for the transcription (default is None). - endpoint (str): The API endpoint for the transcription (default is "v1/listen"). - - Returns: - PrerecordedResponse: An object containing the transcription result. - - Raises: - DeepgramTypeError: Raised for known API errors. - """ - self._logger.debug("PreRecordedClient.transcribe_url ENTER") - - if ( - isinstance(options, dict) - and "callback" in options - and options["callback"] is not None - ) or (isinstance(options, PrerecordedOptions) and options.callback is not None): - self._logger.debug("PreRecordedClient.transcribe_url LEAVE") - return self.transcribe_url_callback( - source, - callback=options["callback"], - options=options, - addons=addons, - headers=headers, - timeout=timeout, - endpoint=endpoint, - ) - - url = f"{self._config.url}/{endpoint}" - if is_url_source(source): - body = source - else: - self._logger.error("Unknown transcription source type") - self._logger.debug("PreRecordedClient.transcribe_url LEAVE") - raise DeepgramTypeError("Unknown transcription source type") - - if isinstance(options, PrerecordedOptions) and not options.check(): - self._logger.error("options.check failed") - self._logger.debug("PreRecordedClient.transcribe_url LEAVE") - raise DeepgramError("Fatal transcription options error") - - self._logger.info("url: %s", url) - self._logger.info("source: %s", source) - if isinstance(options, PrerecordedOptions): - self._logger.info("PrerecordedOptions switching class -> dict") - options = options.to_dict() - self._logger.info("options: %s", options) - self._logger.info("addons: %s", addons) - self._logger.info("headers: %s", headers) - result = self.post( - url, - options=options, - addons=addons, - headers=headers, - json=body, - timeout=timeout, - ) - self._logger.info("json: %s", result) - res = PrerecordedResponse.from_json(result) - self._logger.verbose("result: %s", res) - self._logger.notice("transcribe_url succeeded") - self._logger.debug("PreRecordedClient.transcribe_url LEAVE") - return res - - def transcribe_url_callback( - self, - source: UrlSource, - callback: str, - options: Optional[Union[Dict, PrerecordedOptions]] = None, - addons: Optional[Dict] = None, - headers: Optional[Dict] = None, - timeout: Optional[httpx.Timeout] = None, - endpoint: str = "v1/listen", - ) -> AsyncPrerecordedResponse: - """ - Transcribes audio from a URL source and sends the result to a callback URL. - - Args: - source (UrlSource): The URL source of the audio to transcribe. - callback (str): The callback URL where the transcription results will be sent. - options (PrerecordedOptions): Additional options for the transcription (default is None). - endpoint (str): The API endpoint for the transcription (default is "v1/listen"). - - Returns: - AsyncPrerecordedResponse: An object containing the request_id or an error message. - - Raises: - DeepgramTypeError: Raised for known API errors. - """ - self._logger.debug("PreRecordedClient.transcribe_url_callback ENTER") - - url = f"{self._config.url}/{endpoint}" - if options is None: - options = {} - if isinstance(options, PrerecordedOptions): - options.callback = callback - else: - options["callback"] = callback - if is_url_source(source): - body = source - else: - self._logger.error("Unknown transcription source type") - self._logger.debug("PreRecordedClient.transcribe_url_callback LEAVE") - raise DeepgramTypeError("Unknown transcription source type") - - if isinstance(options, PrerecordedOptions) and not options.check(): - self._logger.error("options.check failed") - self._logger.debug("PreRecordedClient.transcribe_url_callback LEAVE") - raise DeepgramError("Fatal transcription options error") - - self._logger.info("url: %s", url) - self._logger.info("source: %s", source) - if isinstance(options, PrerecordedOptions): - self._logger.info("PrerecordedOptions switching class -> dict") - options = options.to_dict() - self._logger.info("options: %s", options) - self._logger.info("addons: %s", addons) - self._logger.info("headers: %s", headers) - result = self.post( - url, - options=options, - addons=addons, - headers=headers, - json=body, - timeout=timeout, - ) - self._logger.info("json: %s", result) - res = AsyncPrerecordedResponse.from_json(result) - self._logger.verbose("result: %s", res) - self._logger.notice("transcribe_url_callback succeeded") - self._logger.debug("PreRecordedClient.transcribe_url_callback LEAVE") - return res - - def transcribe_file( - self, - source: FileSource, - options: Optional[Union[Dict, PrerecordedOptions]] = None, - addons: Optional[Dict] = None, - headers: Optional[Dict] = None, - timeout: Optional[httpx.Timeout] = None, - endpoint: str = "v1/listen", - ) -> Union[AsyncPrerecordedResponse, PrerecordedResponse]: - """ - Transcribes audio from a local file source. - - Args: - source (FileSource): The local file source of the audio to transcribe. - options (PrerecordedOptions): Additional options for the transcription (default is None). - endpoint (str): The API endpoint for the transcription (default is "v1/listen"). - - Returns: - PrerecordedResponse: An object containing the transcription result or an error message. - - Raises: - DeepgramTypeError: Raised for known API errors. - """ - self._logger.debug("PreRecordedClient.transcribe_file ENTER") - - if ( - isinstance(options, dict) - and "callback" in options - and options["callback"] is not None - ) or (isinstance(options, PrerecordedOptions) and options.callback is not None): - self._logger.debug("PreRecordedClient.transcribe_file LEAVE") - return self.transcribe_file_callback( - source, - callback=options["callback"], - options=options, - addons=addons, - headers=headers, - timeout=timeout, - endpoint=endpoint, - ) - - url = f"{self._config.url}/{endpoint}" - - if is_buffer_source(source): - body = source["buffer"] # type: ignore - elif is_readstream_source(source): - body = source["stream"] # type: ignore - else: - self._logger.error("Unknown transcription source type") - self._logger.debug("PreRecordedClient.transcribe_file LEAVE") - raise DeepgramTypeError("Unknown transcription source type") - - if isinstance(options, PrerecordedOptions) and not options.check(): - self._logger.error("options.check failed") - self._logger.debug("PreRecordedClient.transcribe_file LEAVE") - raise DeepgramError("Fatal transcription options error") - - self._logger.info("url: %s", url) - if isinstance(options, PrerecordedOptions): - self._logger.info("PrerecordedOptions switching class -> dict") - options = options.to_dict() - self._logger.info("options: %s", options) - self._logger.info("addons: %s", addons) - self._logger.info("headers: %s", headers) - result = self.post( - url, - options=options, - addons=addons, - headers=headers, - content=body, - timeout=timeout, - ) - self._logger.info("json: %s", result) - res = PrerecordedResponse.from_json(result) - self._logger.verbose("result: %s", res) - self._logger.notice("transcribe_file succeeded") - self._logger.debug("PreRecordedClient.transcribe_file LEAVE") - return res - - def transcribe_file_callback( - self, - source: FileSource, - callback: str, - options: Optional[Union[Dict, PrerecordedOptions]] = None, - addons: Optional[Dict] = None, - headers: Optional[Dict] = None, - timeout: Optional[httpx.Timeout] = None, - endpoint: str = "v1/listen", - ) -> AsyncPrerecordedResponse: - """ - Transcribes audio from a local file source and sends the result to a callback URL. - Args: - source (FileSource): The local file source of the audio to transcribe. - callback (str): The callback URL where the transcription results will be sent. - options (PrerecordedOptions): Additional options for the transcription (default is None). - endpoint (str): The API endpoint for the transcription (default is "v1/listen"). - Returns: - AsyncPrerecordedResponse: An object containing the request_id or an error message. +# input +PrerecordedOptions = PrerecordedOptionsLatest +PreRecordedStreamSource = PreRecordedStreamSourceLatest +UrlSource = UrlSourceLatest +FileSource = FileSourceLatest +PrerecordedSource = PrerecordedSourceLatest - Raises: - DeepgramTypeError: Raised for known API errors. - """ - self._logger.debug("PreRecordedClient.transcribe_file_callback ENTER") - url = f"{self._config.url}/{endpoint}" - if options is None: - options = {} - if isinstance(options, PrerecordedOptions): - options.callback = callback - else: - options["callback"] = callback - if is_buffer_source(source): - body = source["buffer"] # type: ignore - elif is_readstream_source(source): - body = source["stream"] # type: ignore - else: - self._logger.error("Unknown transcription source type") - self._logger.debug("PreRecordedClient.transcribe_file_callback LEAVE") - raise DeepgramTypeError("Unknown transcription source type") +# output +AsyncPrerecordedResponse = AsyncPrerecordedResponseLatest +PrerecordedResponse = PrerecordedResponseLatest +SyncPrerecordedResponse = SyncPrerecordedResponseLatest - if isinstance(options, PrerecordedOptions) and not options.check(): - self._logger.error("options.check failed") - self._logger.debug("PreRecordedClient.transcribe_file_callback LEAVE") - raise DeepgramError("Fatal transcription options error") - self._logger.info("url: %s", url) - if isinstance(options, PrerecordedOptions): - self._logger.info("PrerecordedOptions switching class -> dict") - options = options.to_dict() - self._logger.info("options: %s", options) - self._logger.info("addons: %s", addons) - self._logger.info("headers: %s", headers) - result = self.post( - url, - options=options, - addons=addons, - headers=headers, - content=body, - timeout=timeout, - ) - self._logger.info("json: %s", result) - res = AsyncPrerecordedResponse.from_json(result) - self._logger.verbose("result: %s", res) - self._logger.notice("transcribe_file_callback succeeded") - self._logger.debug("PreRecordedClient.transcribe_file_callback LEAVE") - return res +# clients +PreRecordedClient = PreRecordedClientLatest +AsyncPreRecordedClient = AsyncPreRecordedClientLatest diff --git a/deepgram/clients/prerecorded/errors.py b/deepgram/clients/prerecorded/v1/errors.py similarity index 100% rename from deepgram/clients/prerecorded/errors.py rename to deepgram/clients/prerecorded/v1/errors.py diff --git a/deepgram/clients/read.py b/deepgram/clients/read_router.py similarity index 100% rename from deepgram/clients/read.py rename to deepgram/clients/read_router.py diff --git a/deepgram/clients/speak/__init__.py b/deepgram/clients/speak/__init__.py index 3636a94a..7e1dab7c 100644 --- a/deepgram/clients/speak/__init__.py +++ b/deepgram/clients/speak/__init__.py @@ -2,16 +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 .client import SpeakClient -from .client import SpeakStreamClient, AsyncSpeakStreamClient -from .client import AsyncSpeakClient -from .client import SpeakOptions -from .client import SpeakResponse +from .enums import SpeakWebSocketEvents +from ...options import DeepgramClientOptions, ClientOptionsFromEnv + +from .client import ( + SpeakClient, # backward compat + SpeakRESTClient, + AsyncSpeakRESTClient, + SpeakWebSocketClient, + AsyncSpeakWebSocketClient, +) from .client import ( + SpeakOptions, FileSource, - SpeakStreamSource, + SpeakWebSocketSource, SpeakSource, ) - -from .enums import SpeakStreamEvents -from ...options import DeepgramClientOptions, ClientOptionsFromEnv +from .client import ( + SpeakResponse, # backward compat + SpeakRESTResponse, + SpeakWebSocketResponse, + OpenResponse, + MetadataResponse, + FlushedResponse, + CloseResponse, + UnhandledResponse, + WarningResponse, + ErrorResponse, +) diff --git a/deepgram/clients/speak/client.py b/deepgram/clients/speak/client.py index b9975a46..629e6919 100644 --- a/deepgram/clients/speak/client.py +++ b/deepgram/clients/speak/client.py @@ -2,17 +2,29 @@ # Use of this source code is governed by a MIT license that can be found in the LICENSE file. # SPDX-License-Identifier: MIT -from .v1.client import SpeakClient as SpeakClientLatest -from .v1.async_client import AsyncSpeakClient as AsyncSpeakClientLatest -from .v1.client_stream import SpeakStreamClient as SpeakStreamClientLatest -from .v1.async_client_stream import AsyncSpeakStreamClient as AsyncSpeakStreamClientLatest -from .v1.options import ( +from .v1 import ( + SpeakRESTClient as SpeakRESTClientLatest, + AsyncSpeakRESTClient as AsyncSpeakRESTClientLatest, + SpeakWebSocketClient as SpeakWebSocketClientLatest, + AsyncSpeakWebSocketClient as AsyncSpeakWebSocketClientLatest, +) +from .v1 import ( SpeakOptions as SpeakOptionsLatest, FileSource as FileSourceLatest, - SpeakStreamSource as SpeakStreamSourceLatest, + SpeakWebSocketSource as SpeakWebSocketSourceLatest, SpeakSource as SpeakSourceLatest, ) -from .v1.response import SpeakResponse as SpeakResponseLatest +from .v1 import ( + SpeakRESTResponse as SpeakRESTResponseLatest, + SpeakWebSocketResponse as SpeakWebSocketResponseLatest, + OpenResponse as OpenResponseLatest, + MetadataResponse as MetadataResponseLatest, + FlushedResponse as FlushedResponseLatest, + CloseResponse as CloseResponseLatest, + UnhandledResponse as UnhandledResponseLatest, + WarningResponse as WarningResponseLatest, + ErrorResponse as ErrorResponseLatest, +) # The client.py points to the current supported version in the SDK. # Older versions are supported in the SDK for backwards compatibility. @@ -20,16 +32,28 @@ # input SpeakOptions = SpeakOptionsLatest -SpeakStreamSource = SpeakStreamSourceLatest - - +SpeakWebSocketSource = SpeakWebSocketSourceLatest FileSource = FileSourceLatest SpeakSource = SpeakSourceLatest - # output -SpeakResponse = SpeakResponseLatest -SpeakClient = SpeakClientLatest -AsyncSpeakClient = AsyncSpeakClientLatest -SpeakStreamClient = SpeakStreamClientLatest -AsyncSpeakStreamClient = AsyncSpeakStreamClientLatest +SpeakRESTResponse = SpeakRESTResponseLatest +SpeakWebSocketResponse = SpeakWebSocketResponseLatest +OpenResponse = OpenResponseLatest +MetadataResponse = MetadataResponseLatest +FlushedResponse = FlushedResponseLatest +CloseResponse = CloseResponseLatest +UnhandledResponse = UnhandledResponseLatest +WarningResponse = WarningResponseLatest +ErrorResponse = ErrorResponseLatest + + +# backward compatibility +SpeakResponse = SpeakRESTResponseLatest +SpeakClient = SpeakRESTClientLatest + +# clients +SpeakRESTClient = SpeakRESTClientLatest +AsyncSpeakRESTClient = AsyncSpeakRESTClientLatest +SpeakWebSocketClient = SpeakWebSocketClientLatest +AsyncSpeakWebSocketClient = AsyncSpeakWebSocketClientLatest diff --git a/deepgram/clients/speak/enums.py b/deepgram/clients/speak/enums.py index 507bf339..17007aaf 100644 --- a/deepgram/clients/speak/enums.py +++ b/deepgram/clients/speak/enums.py @@ -7,7 +7,7 @@ # Constants mapping to events from the Deepgram API -class SpeakStreamEvents(StrEnum): +class SpeakWebSocketEvents(StrEnum): """ Enumerates the possible events that can be received from the Deepgram API """ diff --git a/deepgram/clients/speak/v1/__init__.py b/deepgram/clients/speak/v1/__init__.py index d686df2b..6acfb7b1 100644 --- a/deepgram/clients/speak/v1/__init__.py +++ b/deepgram/clients/speak/v1/__init__.py @@ -2,11 +2,23 @@ # Use of this source code is governed by a MIT license that can be found in the LICENSE file. # SPDX-License-Identifier: MIT -from .client import SpeakClient -from .client_stream import SpeakStreamClient -from .async_client_stream import AsyncSpeakStreamClient -from .async_client import AsyncSpeakClient -from .options import SpeakOptions, FileSource, SpeakStreamSource, SpeakSource -from .response import SpeakResponse - +from .options import SpeakOptions, FileSource, SpeakWebSocketSource, SpeakSource from ....options import DeepgramClientOptions, ClientOptionsFromEnv + +# rest +from .rest import SpeakRESTClient, AsyncSpeakRESTClient + +from .rest import SpeakRESTResponse + +# websocket +from .websocket import SpeakWebSocketClient, AsyncSpeakWebSocketClient +from .websocket import ( + SpeakWebSocketResponse, + OpenResponse, + MetadataResponse, + FlushedResponse, + CloseResponse, + UnhandledResponse, + WarningResponse, + ErrorResponse, +) diff --git a/deepgram/clients/speak/v1/options.py b/deepgram/clients/speak/v1/options.py index 1fa11d86..edd6723a 100644 --- a/deepgram/clients/speak/v1/options.py +++ b/deepgram/clients/speak/v1/options.py @@ -65,5 +65,5 @@ def check(self): return True -SpeakStreamSource = BufferedReader -SpeakSource = Union[FileSource, SpeakStreamSource] +SpeakWebSocketSource = BufferedReader +SpeakSource = Union[FileSource, SpeakWebSocketSource] diff --git a/deepgram/clients/speak/v1/rest/__init__.py b/deepgram/clients/speak/v1/rest/__init__.py new file mode 100644 index 00000000..956abf66 --- /dev/null +++ b/deepgram/clients/speak/v1/rest/__init__.py @@ -0,0 +1,7 @@ +# Copyright 2024 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 .client import SpeakRESTClient +from .async_client import AsyncSpeakRESTClient +from .response import SpeakRESTResponse diff --git a/deepgram/clients/speak/v1/async_client.py b/deepgram/clients/speak/v1/rest/async_client.py similarity index 91% rename from deepgram/clients/speak/v1/async_client.py rename to deepgram/clients/speak/v1/rest/async_client.py index 6f60be3d..a8fed0ad 100644 --- a/deepgram/clients/speak/v1/async_client.py +++ b/deepgram/clients/speak/v1/rest/async_client.py @@ -9,17 +9,17 @@ import httpx -from ....utils import verboselogs -from ....options import DeepgramClientOptions -from ...abstract_async_client import AbstractAsyncRestClient -from ..errors import DeepgramError, DeepgramTypeError +from .....utils import verboselogs +from .....options import DeepgramClientOptions +from ....abstract_async_client import AbstractAsyncRestClient +from ...errors import DeepgramError, DeepgramTypeError from .helpers import is_text_source -from .options import SpeakOptions, FileSource -from .response import SpeakResponse +from ..options import SpeakOptions, FileSource +from .response import SpeakRESTResponse -class AsyncSpeakClient(AbstractAsyncRestClient): +class AsyncSpeakRESTClient(AbstractAsyncRestClient): """ A client class for doing Text-to-Speech. Provides methods for speaking from text. @@ -43,7 +43,7 @@ async def stream( headers: Optional[Dict] = None, timeout: Optional[httpx.Timeout] = None, endpoint: str = "v1/speak", - ) -> SpeakResponse: + ) -> SpeakRESTResponse: """ Speak from a text source and store in memory. @@ -56,7 +56,7 @@ async def stream( endpoint (str): The endpoint to use for the request (default is "v1/speak"). Returns: - SpeakResponse: The response from the speak request. + SpeakRESTResponse: The response from the speak request. Raises: DeepgramTypeError: Raised for known API errors. @@ -104,7 +104,7 @@ async def stream( file_result=return_vals, ) self._logger.info("result: %s", result) - resp = SpeakResponse( + resp = SpeakRESTResponse( content_type=str(result["content-type"]), request_id=str(result["request-id"]), model_uuid=str(result["model-uuid"]), @@ -127,7 +127,7 @@ async def file( addons: Optional[Dict] = None, timeout: Optional[httpx.Timeout] = None, endpoint: str = "v1/speak", - ) -> SpeakResponse: + ) -> SpeakRESTResponse: """ Speak from a text source and save to a file. """ @@ -149,7 +149,7 @@ async def save( headers: Optional[Dict] = None, timeout: Optional[httpx.Timeout] = None, endpoint: str = "v1/speak", - ) -> SpeakResponse: + ) -> SpeakRESTResponse: """ Speak from a text source and save to a file. @@ -162,7 +162,7 @@ async def save( endpoint (str): The endpoint to use for the request (default is "v1/speak"). Returns: - SpeakResponse: The response from the speak request. + SpeakRESTResponse: The response from the speak request. Raises: DeepgramTypeError: Raised for known API errors. diff --git a/deepgram/clients/speak/v1/client.py b/deepgram/clients/speak/v1/rest/client.py similarity index 91% rename from deepgram/clients/speak/v1/client.py rename to deepgram/clients/speak/v1/rest/client.py index 5eb40bd0..afefa0cf 100644 --- a/deepgram/clients/speak/v1/client.py +++ b/deepgram/clients/speak/v1/rest/client.py @@ -8,17 +8,17 @@ import httpx -from ....utils import verboselogs -from ....options import DeepgramClientOptions -from ...abstract_sync_client import AbstractSyncRestClient -from ..errors import DeepgramError, DeepgramTypeError +from .....utils import verboselogs +from .....options import DeepgramClientOptions +from ....abstract_sync_client import AbstractSyncRestClient +from ...errors import DeepgramError, DeepgramTypeError from .helpers import is_text_source -from .options import SpeakOptions, FileSource -from .response import SpeakResponse +from ..options import SpeakOptions, FileSource +from .response import SpeakRESTResponse -class SpeakClient(AbstractSyncRestClient): +class SpeakRESTClient(AbstractSyncRestClient): """ A client class for doing Text-to-Speech. Provides methods for speaking from text. @@ -42,7 +42,7 @@ def stream( headers: Optional[Dict] = None, timeout: Optional[httpx.Timeout] = None, endpoint: str = "v1/speak", - ) -> SpeakResponse: + ) -> SpeakRESTResponse: """ Speak from a text source and store in memory. @@ -55,7 +55,7 @@ def stream( endpoint (str): The endpoint to use for the request (default is "v1/speak"). Returns: - SpeakResponse: The response from the speak request. + SpeakRESTResponse: The response from the speak request. Raises: DeepgramTypeError: Raised for known API errors. @@ -104,7 +104,7 @@ def stream( ) self._logger.info("result: %s", result) - resp = SpeakResponse( + resp = SpeakRESTResponse( content_type=str(result["content-type"]), request_id=str(result["request-id"]), model_uuid=str(result["model-uuid"]), @@ -127,7 +127,7 @@ async def file( addons: Optional[Dict] = None, timeout: Optional[httpx.Timeout] = None, endpoint: str = "v1/speak", - ) -> SpeakResponse: + ) -> SpeakRESTResponse: """ Speak from a text source and save to a file. """ @@ -149,7 +149,7 @@ def save( headers: Optional[Dict] = None, timeout: Optional[httpx.Timeout] = None, endpoint: str = "v1/speak", - ) -> SpeakResponse: + ) -> SpeakRESTResponse: """ Speak from a text source and save to a file. @@ -162,7 +162,7 @@ def save( endpoint (str): The endpoint to use for the request (default is "v1/speak"). Returns: - SpeakResponse: The response from the speak request. + SpeakRESTResponse: The response from the speak request. Raises: DeepgramTypeError: Raised for known API errors. diff --git a/deepgram/clients/speak/v1/helpers.py b/deepgram/clients/speak/v1/rest/helpers.py similarity index 94% rename from deepgram/clients/speak/v1/helpers.py rename to deepgram/clients/speak/v1/rest/helpers.py index 3232c1e7..6b048bd5 100644 --- a/deepgram/clients/speak/v1/helpers.py +++ b/deepgram/clients/speak/v1/rest/helpers.py @@ -2,7 +2,7 @@ # 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 SpeakSource +from ..options import SpeakSource def is_text_source(provided_source: SpeakSource) -> bool: diff --git a/deepgram/clients/speak/v1/response.py b/deepgram/clients/speak/v1/rest/response.py similarity index 88% rename from deepgram/clients/speak/v1/response.py rename to deepgram/clients/speak/v1/rest/response.py index 8130f9e6..0b73bc12 100644 --- a/deepgram/clients/speak/v1/response.py +++ b/deepgram/clients/speak/v1/rest/response.py @@ -13,7 +13,9 @@ @dataclass -class SpeakResponse(DataClassJsonMixin): # pylint: disable=too-many-instance-attributes +class SpeakRESTResponse( + DataClassJsonMixin +): # pylint: disable=too-many-instance-attributes """ A class for representing a response from the speak endpoint. """ @@ -45,8 +47,11 @@ def __str__(self) -> str: my_dict = self.to_dict() return my_dict.__str__() + @dataclass -class SpeakStreamResponse(DataClassJsonMixin): # pylint: disable=too-many-instance-attributes +class SpeakStreamResponse( + DataClassJsonMixin +): # pylint: disable=too-many-instance-attributes """ A class for representing a response from the speak (streaming) endpoint. """ @@ -73,6 +78,7 @@ def __str__(self) -> str: my_dict = self.to_dict() return my_dict.__str__() + @dataclass class OpenResponse(DataClassJsonMixin): """ @@ -91,6 +97,7 @@ def __setitem__(self, key, val): def __str__(self) -> str: return self.to_json(indent=4) + @dataclass class MetadataResponse(DataClassJsonMixin): """ @@ -109,6 +116,7 @@ def __setitem__(self, key, val): def __str__(self) -> str: return self.to_json(indent=4) + @dataclass class FlushedResponse(DataClassJsonMixin): """ @@ -127,6 +135,7 @@ def __setitem__(self, key, val): def __str__(self) -> str: return self.to_json(indent=4) + @dataclass class CloseResponse(DataClassJsonMixin): """ @@ -145,6 +154,7 @@ def __setitem__(self, key, val): def __str__(self) -> str: return self.to_json(indent=4) + @dataclass class ErrorResponse(DataClassJsonMixin): """ @@ -166,29 +176,10 @@ def __setitem__(self, key, val): def __str__(self) -> str: return self.to_json(indent=4) -@dataclass -class WarningResponse(DataClassJsonMixin): - """ - Warning Message from the Deepgram Platform - """ - - warn_code: str = "" - warn_msg: str = "" - type: str = "" - - def __getitem__(self, key): - _dict = self.to_dict() - return _dict[key] - - def __setitem__(self, key, val): - self.__dict__[key] = val - - def __str__(self) -> str: - return self.to_json(indent=4) - # Unhandled Message + @dataclass class UnhandledResponse(DataClassJsonMixin): """ @@ -206,4 +197,4 @@ def __setitem__(self, key, val): self.__dict__[key] = val def __str__(self) -> str: - return self.to_json(indent=4) \ No newline at end of file + return self.to_json(indent=4) diff --git a/deepgram/clients/speak/v1/websocket/__init__.py b/deepgram/clients/speak/v1/websocket/__init__.py new file mode 100644 index 00000000..f086c38a --- /dev/null +++ b/deepgram/clients/speak/v1/websocket/__init__.py @@ -0,0 +1,16 @@ +# Copyright 2024 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 .client import SpeakWebSocketClient +from .async_client import AsyncSpeakWebSocketClient +from .response import ( + SpeakWebSocketResponse, + OpenResponse, + MetadataResponse, + FlushedResponse, + CloseResponse, + UnhandledResponse, + WarningResponse, + ErrorResponse, +) diff --git a/deepgram/clients/speak/v1/async_client_stream.py b/deepgram/clients/speak/v1/websocket/async_client.py similarity index 75% rename from deepgram/clients/speak/v1/async_client_stream.py rename to deepgram/clients/speak/v1/websocket/async_client.py index 9130ebf3..c8d2362c 100644 --- a/deepgram/clients/speak/v1/async_client_stream.py +++ b/deepgram/clients/speak/v1/websocket/async_client.py @@ -10,11 +10,11 @@ import websockets from websockets.client import WebSocketClientProtocol -from ....utils import verboselogs -from ....options import DeepgramClientOptions -from ..enums import SpeakStreamEvents -from ...live.helpers import convert_to_websocket_url, append_query_params -from ..errors import DeepgramError +from .....utils import verboselogs +from .....options import DeepgramClientOptions +from ...enums import SpeakWebSocketEvents +from .helpers import convert_to_websocket_url, append_query_params +from ...errors import DeepgramError from .response import ( OpenResponse, @@ -25,10 +25,10 @@ ErrorResponse, UnhandledResponse, ) -from .options import SpeakOptions +from ..options import SpeakOptions -class AsyncSpeakStreamClient: # pylint: disable=too-many-instance-attributes +class AsyncSpeakWebSocketClient: # pylint: disable=too-many-instance-attributes """ Client for interacting with Deepgram's text-to-speech services over WebSockets. @@ -44,7 +44,7 @@ class AsyncSpeakStreamClient: # pylint: disable=too-many-instance-attributes _websocket_url: str _socket: WebSocketClientProtocol - _event_handlers: Dict[SpeakStreamEvents, list] + _event_handlers: Dict[SpeakWebSocketEvents, list] _listen_thread: Union[asyncio.Task, None] @@ -70,7 +70,7 @@ def __init__(self, config: DeepgramClientOptions): self._exit_event = asyncio.Event() self._event_handlers = { - event: [] for event in SpeakStreamEvents.__members__.values() + event: [] for event in SpeakWebSocketEvents.__members__.values() } self._websocket_url = convert_to_websocket_url(self._config.url, self._endpoint) @@ -137,8 +137,7 @@ async def start( try: self._socket = await websockets.connect( - url_with_params, - extra_headers=combined_headers + url_with_params, extra_headers=combined_headers ) self._exit_event.clear() @@ -157,27 +156,33 @@ async def start( # push open event await self._emit( - SpeakStreamEvents(SpeakStreamEvents.Open), - OpenResponse(type=SpeakStreamEvents.Open), + SpeakWebSocketEvents(SpeakWebSocketEvents.Open), + OpenResponse(type=SpeakWebSocketEvents.Open), ) self._logger.notice("start succeeded") self._logger.debug("AsyncSpeakStreamClient.start LEAVE") return True except websockets.ConnectionClosed as e: - self._logger.error("ConnectionClosed in AsyncSpeakStreamClient.start: %s", e) + self._logger.error( + "ConnectionClosed in AsyncSpeakStreamClient.start: %s", e + ) self._logger.debug("AsyncSpeakStreamClient.start LEAVE") if self._config.options.get("termination_exception_connect") == "true": raise return False except websockets.exceptions.WebSocketException as e: - self._logger.error("WebSocketException in AsyncSpeakStreamClient.start: %s", e) + self._logger.error( + "WebSocketException in AsyncSpeakStreamClient.start: %s", e + ) self._logger.debug("AsyncSpeakStreamClient.start LEAVE") if self._config.options.get("termination_exception_connect") == "true": raise return False except Exception as e: # pylint: disable=broad-except - self._logger.error("WebSocketException in AsyncSpeakStreamClient.start: %s", e) + self._logger.error( + "WebSocketException in AsyncSpeakStreamClient.start: %s", e + ) self._logger.debug("AsyncSpeakStreamClient.start LEAVE") if self._config.options.get("termination_exception_connect") == "true": raise @@ -185,17 +190,17 @@ async def start( # pylint: enable=too-many-branches,too-many-statements - def on(self, event: SpeakStreamEvents, handler) -> None: + def on(self, event: SpeakWebSocketEvents, handler) -> None: """ Registers event handlers for specific events. """ self._logger.info("event subscribed: %s", event) - if event in SpeakStreamEvents.__members__.values() and callable(handler): + if event in SpeakWebSocketEvents.__members__.values() and callable(handler): if handler not in self._event_handlers[event]: self._event_handlers[event].append(handler) # triggers the registered event handlers for a specific event - async def _emit(self, event: SpeakStreamEvents, *args, **kwargs) -> None: + async def _emit(self, event: SpeakWebSocketEvents, *args, **kwargs) -> None: """ Emits events to the registered event handlers. """ @@ -248,91 +253,95 @@ async def _listening(self) -> None: if message is None: self._logger.spam("message is None") continue - + if isinstance(message, bytes): self._logger.debug("Binary data received") await self._emit( - SpeakStreamEvents(SpeakStreamEvents.AudioData), + SpeakWebSocketEvents(SpeakWebSocketEvents.AudioData), data=message, **dict(cast(Dict[Any, Any], self._kwargs)), - ) + ) else: - data = json.loads(message) - response_type = data.get("type") - self._logger.debug("response_type: %s, data: %s", response_type, data) - - match response_type: - case SpeakStreamEvents.Open: - open_result: OpenResponse = OpenResponse.from_json(message) - self._logger.verbose("OpenResponse: %s", open_result) - await self._emit( - SpeakStreamEvents(SpeakStreamEvents.Open), - open=open_result, - **dict(cast(Dict[Any, Any], self._kwargs)), - ) - case SpeakStreamEvents.Metadata: - meta_result: MetadataResponse = MetadataResponse.from_json( - message - ) - self._logger.verbose("MetadataResponse: %s", meta_result) - await self._emit( - SpeakStreamEvents(SpeakStreamEvents.Metadata), - metadata=meta_result, - **dict(cast(Dict[Any, Any], self._kwargs)), - ) - case SpeakStreamEvents.Flush: - fl_result: FlushedResponse = ( - FlushedResponse.from_json(message) - ) - self._logger.verbose("FlushedResponse: %s", fl_result) - await self._emit( - SpeakStreamEvents( - SpeakStreamEvents.Flush - ), - flushed=fl_result, - **dict(cast(Dict[Any, Any], self._kwargs)), - ) - case SpeakStreamEvents.Close: - close_result: CloseResponse = CloseResponse.from_json(message) - self._logger.verbose("CloseResponse: %s", close_result) - await self._emit( - SpeakStreamEvents(SpeakStreamEvents.Close), - close=close_result, - **dict(cast(Dict[Any, Any], self._kwargs)), - ) - case SpeakStreamEvents.Warning: - war_warning: WarningResponse = WarningResponse.from_json(message) + data = json.loads(message) + response_type = data.get("type") + self._logger.debug( + "response_type: %s, data: %s", response_type, data + ) + + match response_type: + case SpeakWebSocketEvents.Open: + open_result: OpenResponse = OpenResponse.from_json(message) + self._logger.verbose("OpenResponse: %s", open_result) + await self._emit( + SpeakWebSocketEvents(SpeakWebSocketEvents.Open), + open=open_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case SpeakWebSocketEvents.Metadata: + meta_result: MetadataResponse = MetadataResponse.from_json( + message + ) + self._logger.verbose("MetadataResponse: %s", meta_result) + await self._emit( + SpeakWebSocketEvents(SpeakWebSocketEvents.Metadata), + metadata=meta_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case SpeakWebSocketEvents.Flush: + fl_result: FlushedResponse = FlushedResponse.from_json( + message + ) + self._logger.verbose("FlushedResponse: %s", fl_result) + await self._emit( + SpeakWebSocketEvents(SpeakWebSocketEvents.Flush), + flushed=fl_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case SpeakWebSocketEvents.Close: + close_result: CloseResponse = CloseResponse.from_json( + message + ) + self._logger.verbose("CloseResponse: %s", close_result) + await self._emit( + SpeakWebSocketEvents(SpeakWebSocketEvents.Close), + close=close_result, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case SpeakWebSocketEvents.Warning: + war_warning: WarningResponse = WarningResponse.from_json( + message + ) self._logger.verbose("WarningResponse: %s", war_warning) await self._emit( - SpeakStreamEvents(SpeakStreamEvents.Warning), + SpeakWebSocketEvents(SpeakWebSocketEvents.Warning), warning=war_warning, **dict(cast(Dict[Any, Any], self._kwargs)), ) - case SpeakStreamEvents.Error: - err_error: ErrorResponse = ErrorResponse.from_json(message) - self._logger.verbose("ErrorResponse: %s", err_error) - await self._emit( - SpeakStreamEvents(SpeakStreamEvents.Error), - error=err_error, - **dict(cast(Dict[Any, Any], self._kwargs)), - ) - case _: - self._logger.warning( - "Unknown Message: response_type: %s, data: %s", - response_type, - data, - ) - unhandled_error: UnhandledResponse = UnhandledResponse( - type=SpeakStreamEvents( - SpeakStreamEvents.Unhandled - ), - raw=message, - ) - await self._emit( - SpeakStreamEvents(SpeakStreamEvents.Unhandled), - unhandled=unhandled_error, - **dict(cast(Dict[Any, Any], self._kwargs)), - ) + case SpeakWebSocketEvents.Error: + err_error: ErrorResponse = ErrorResponse.from_json(message) + self._logger.verbose("ErrorResponse: %s", err_error) + await self._emit( + SpeakWebSocketEvents(SpeakWebSocketEvents.Error), + error=err_error, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) + case _: + self._logger.warning( + "Unknown Message: response_type: %s, data: %s", + response_type, + data, + ) + unhandled_error: UnhandledResponse = UnhandledResponse( + type=SpeakWebSocketEvents( + SpeakWebSocketEvents.Unhandled + ), + raw=message, + ) + await self._emit( + SpeakWebSocketEvents(SpeakWebSocketEvents.Unhandled), + unhandled=unhandled_error, + **dict(cast(Dict[Any, Any], self._kwargs)), + ) except websockets.exceptions.ConnectionClosedOK as e: self._logger.notice(f"_listening({e.code}) exiting gracefully") @@ -356,7 +365,7 @@ async def _listening(self) -> None: "ConnectionClosed", ) await self._emit( - SpeakStreamEvents(SpeakStreamEvents.Error), + SpeakWebSocketEvents(SpeakWebSocketEvents.Error), error=cc_error, **dict(cast(Dict[Any, Any], self._kwargs)), ) @@ -380,7 +389,7 @@ async def _listening(self) -> None: "WebSocketException", ) await self._emit( - SpeakStreamEvents(SpeakStreamEvents.Error), + SpeakWebSocketEvents(SpeakWebSocketEvents.Error), error=ws_error, **dict(cast(Dict[Any, Any], self._kwargs)), ) @@ -395,14 +404,16 @@ async def _listening(self) -> None: return except Exception as e: # pylint: disable=broad-except - self._logger.error("Exception in AsyncSpeakStreamClient._listening: %s", e) + self._logger.error( + "Exception in AsyncSpeakStreamClient._listening: %s", e + ) e_error: ErrorResponse = ErrorResponse( "Exception in AsyncSpeakStreamClient._listening", f"{e}", "Exception", ) await self._emit( - SpeakStreamEvents(SpeakStreamEvents.Error), + SpeakWebSocketEvents(SpeakWebSocketEvents.Error), error=e_error, **dict(cast(Dict[Any, Any], self._kwargs)), ) @@ -431,7 +442,9 @@ async def send(self, text_input: str) -> bool: if self._socket is not None: try: - await self._socket.send(json.dumps({"type": "Speak", "text": text_input})) + await self._socket.send( + json.dumps({"type": "Speak", "text": text_input}) + ) except websockets.exceptions.ConnectionClosedOK as e: self._logger.notice(f"send() exiting gracefully: {e.code}") self._logger.debug("AsyncSpeakStreamClient.send LEAVE") @@ -502,7 +515,7 @@ async def flush(self) -> bool: self._logger.spam("AsyncSpeakStreamClient.flush LEAVE") return True - + async def finish(self) -> bool: """ Closes the WebSocket connection gracefully. @@ -529,7 +542,9 @@ async def finish(self) -> bool: self._logger.notice("processing _listen_thread cancel...") # Use asyncio.gather to wait for tasks to be cancelled - await asyncio.wait_for(asyncio.gather(*tasks), timeout=10) # Prevent indefinite waiting + await asyncio.wait_for( + asyncio.gather(*tasks), timeout=10 + ) # Prevent indefinite waiting self._logger.notice("threads joined") # debug the threads @@ -545,7 +560,7 @@ async def finish(self) -> bool: self._logger.error("tasks cancelled error: %s", e) self._logger.debug("AsyncSpeakStreamClient.finish LEAVE") return False - + except asyncio.TimeoutError as e: self._logger.error("tasks cancellation timed out: %s", e) self._logger.debug("AsyncSpeakStreamClient.finish LEAVE") @@ -571,8 +586,8 @@ async def _signal_exit(self) -> None: # push close event try: await self._emit( - SpeakStreamEvents(SpeakStreamEvents.Close), - close=CloseResponse(type=SpeakStreamEvents.Close), + SpeakWebSocketEvents(SpeakWebSocketEvents.Close), + close=CloseResponse(type=SpeakWebSocketEvents.Close), **dict(cast(Dict[Any, Any], self._kwargs)), ) except Exception as e: # pylint: disable=broad-except @@ -593,4 +608,4 @@ async def _signal_exit(self) -> None: except websockets.exceptions.WebSocketException as e: self._logger.error("socket.wait_closed failed: %s", e) - self._socket = None # type: ignore \ No newline at end of file + self._socket = None # type: ignore diff --git a/deepgram/clients/speak/v1/client_stream.py b/deepgram/clients/speak/v1/websocket/client.py similarity index 87% rename from deepgram/clients/speak/v1/client_stream.py rename to deepgram/clients/speak/v1/websocket/client.py index 49ae9d99..4637559d 100644 --- a/deepgram/clients/speak/v1/client_stream.py +++ b/deepgram/clients/speak/v1/websocket/client.py @@ -11,11 +11,11 @@ from websockets.sync.client import connect, ClientConnection import websockets -from ....utils import verboselogs -from ....options import DeepgramClientOptions -from ..enums import SpeakStreamEvents -from ...live.helpers import convert_to_websocket_url, append_query_params -from ..errors import DeepgramError +from .....utils import verboselogs +from .....options import DeepgramClientOptions +from ...enums import SpeakWebSocketEvents +from .helpers import convert_to_websocket_url, append_query_params +from ...errors import DeepgramError from .response import ( OpenResponse, @@ -26,10 +26,10 @@ ErrorResponse, UnhandledResponse, ) -from .options import SpeakOptions +from ..options import SpeakOptions -class SpeakStreamClient: # pylint: disable=too-many-instance-attributes +class SpeakWebSocketClient: # pylint: disable=too-many-instance-attributes """ Client for interacting with Deepgram's text-to-speech services over WebSockets. @@ -47,8 +47,7 @@ class SpeakStreamClient: # pylint: disable=too-many-instance-attributes _socket: ClientConnection _exit_event: threading.Event _lock_send: threading.Lock - _event_handlers: Dict[SpeakStreamEvents, list] - + _event_handlers: Dict[SpeakWebSocketEvents, list] _listen_thread: Union[threading.Thread, None] @@ -75,7 +74,7 @@ def __init__(self, config: DeepgramClientOptions): self._exit_event = threading.Event() self._event_handlers = { - event: [] for event in SpeakStreamEvents.__members__.values() + event: [] for event in SpeakWebSocketEvents.__members__.values() } self._websocket_url = convert_to_websocket_url(self._config.url, self._endpoint) @@ -159,8 +158,8 @@ def start( # push open event self._emit( - SpeakStreamEvents(SpeakStreamEvents.Open), - OpenResponse(type=SpeakStreamEvents.Open), + SpeakWebSocketEvents(SpeakWebSocketEvents.Open), + OpenResponse(type=SpeakWebSocketEvents.Open), ) self._logger.notice("start succeeded") @@ -188,16 +187,16 @@ def start( # pylint: enable=too-many-statements,too-many-branches def on( - self, event: SpeakStreamEvents, handler + self, event: SpeakWebSocketEvents, handler ) -> None: # registers event handlers for specific events """ Registers event handlers for specific events. """ self._logger.info("event subscribed: %s", event) - if event in SpeakStreamEvents.__members__.values() and callable(handler): + if event in SpeakWebSocketEvents.__members__.values() and callable(handler): self._event_handlers[event].append(handler) - def _emit(self, event: SpeakStreamEvents, *args, **kwargs) -> None: + def _emit(self, event: SpeakWebSocketEvents, *args, **kwargs) -> None: """ Emits events to the registered event handlers. """ @@ -231,71 +230,75 @@ def _listening( if message is None: self._logger.info("message is empty") continue - + if isinstance(message, bytes): self._logger.debug("Binary data received") self._emit( - SpeakStreamEvents(SpeakStreamEvents.AudioData), + SpeakWebSocketEvents(SpeakWebSocketEvents.AudioData), data=message, **dict(cast(Dict[Any, Any], self._kwargs)), - ) + ) else: data = json.loads(message) response_type = data.get("type") - self._logger.debug("response_type: %s, data: %s", response_type, data) + self._logger.debug( + "response_type: %s, data: %s", response_type, data + ) match response_type: - case SpeakStreamEvents.Open: + case SpeakWebSocketEvents.Open: open_result: OpenResponse = OpenResponse.from_json(message) self._logger.verbose("OpenResponse: %s", open_result) self._emit( - SpeakStreamEvents(SpeakStreamEvents.Open), + SpeakWebSocketEvents(SpeakWebSocketEvents.Open), open=open_result, **dict(cast(Dict[Any, Any], self._kwargs)), ) - case SpeakStreamEvents.Metadata: + case SpeakWebSocketEvents.Metadata: meta_result: MetadataResponse = MetadataResponse.from_json( message ) self._logger.verbose("MetadataResponse: %s", meta_result) self._emit( - SpeakStreamEvents(SpeakStreamEvents.Metadata), + SpeakWebSocketEvents(SpeakWebSocketEvents.Metadata), metadata=meta_result, **dict(cast(Dict[Any, Any], self._kwargs)), ) - case SpeakStreamEvents.Flush: - fl_result: FlushedResponse = ( - FlushedResponse.from_json(message) + case SpeakWebSocketEvents.Flush: + fl_result: FlushedResponse = FlushedResponse.from_json( + message ) self._logger.verbose("FlushedResponse: %s", fl_result) self._emit( - SpeakStreamEvents( - SpeakStreamEvents.Flush - ), + SpeakWebSocketEvents(SpeakWebSocketEvents.Flush), flushed=fl_result, **dict(cast(Dict[Any, Any], self._kwargs)), ) - case SpeakStreamEvents.Close: - close_result: CloseResponse = CloseResponse.from_json(message) + case SpeakWebSocketEvents.Close: + close_result: CloseResponse = CloseResponse.from_json( + message + ) self._logger.verbose("CloseResponse: %s", close_result) self._emit( - SpeakStreamEvents(SpeakStreamEvents.Close), + SpeakWebSocketEvents(SpeakWebSocketEvents.Close), close=close_result, **dict(cast(Dict[Any, Any], self._kwargs)), ) - case SpeakStreamEvents.Warning: - war_warning: WarningResponse = WarningResponse.from_json(message) + case SpeakWebSocketEvents.Warning: + war_warning: WarningResponse = WarningResponse.from_json( + message + ) self._logger.verbose("WarningResponse: %s", war_warning) self._emit( - SpeakStreamEvents(SpeakStreamEvents.Warning), + SpeakWebSocketEvents(SpeakWebSocketEvents.Warning), warning=war_warning, **dict(cast(Dict[Any, Any], self._kwargs)), ) - case SpeakStreamEvents.Error: + case SpeakWebSocketEvents.Error: err_error: ErrorResponse = ErrorResponse.from_json(message) self._logger.verbose("ErrorResponse: %s", err_error) self._emit( - SpeakStreamEvents(SpeakStreamEvents.Error), + SpeakWebSocketEvents(SpeakWebSocketEvents.Error), error=err_error, **dict(cast(Dict[Any, Any], self._kwargs)), ) @@ -306,13 +309,13 @@ def _listening( data, ) unhandled_error: UnhandledResponse = UnhandledResponse( - type=SpeakStreamEvents( - SpeakStreamEvents.Unhandled + type=SpeakWebSocketEvents( + SpeakWebSocketEvents.Unhandled ), raw=message, ) self._emit( - SpeakStreamEvents(SpeakStreamEvents.Unhandled), + SpeakWebSocketEvents(SpeakWebSocketEvents.Unhandled), unhandled=unhandled_error, **dict(cast(Dict[Any, Any], self._kwargs)), ) @@ -338,9 +341,7 @@ def _listening( f"{e}", "ConnectionClosed", ) - self._emit( - SpeakStreamEvents(SpeakStreamEvents.Error), cc_error - ) + self._emit(SpeakWebSocketEvents(SpeakWebSocketEvents.Error), cc_error) # signal exit and close self._signal_exit() @@ -360,9 +361,7 @@ def _listening( f"{e}", "WebSocketException", ) - self._emit( - SpeakStreamEvents(SpeakStreamEvents.Error), ws_error - ) + self._emit(SpeakWebSocketEvents(SpeakWebSocketEvents.Error), ws_error) # signal exit and close self._signal_exit() @@ -380,10 +379,10 @@ def _listening( f"{e}", "Exception", ) - self._logger.error("Exception in SpeakStreamClient._listening: %s", str(e)) - self._emit( - SpeakStreamEvents(SpeakStreamEvents.Error), e_error + self._logger.error( + "Exception in SpeakStreamClient._listening: %s", str(e) ) + self._emit(SpeakWebSocketEvents(SpeakWebSocketEvents.Error), e_error) # signal exit and close self._signal_exit() @@ -394,7 +393,6 @@ def _listening( raise return - # pylint: disable=too-many-return-statements def send(self, text_input: str) -> bool: """ @@ -455,7 +453,6 @@ def send(self, text_input: str) -> bool: # pylint: enable=too-many-return-statements - def flush(self) -> bool: """ Flushes the current buffer and returns generated audio @@ -537,8 +534,8 @@ def _signal_exit(self) -> None: # push close event try: self._emit( - SpeakStreamEvents(SpeakStreamEvents.Close), - CloseResponse(type=SpeakStreamEvents.Close), + SpeakWebSocketEvents(SpeakWebSocketEvents.Close), + CloseResponse(type=SpeakWebSocketEvents.Close), ) except Exception as e: # pylint: disable=broad-except self._logger.error("_signal_exit - Exception: %s", e) diff --git a/deepgram/clients/speak/v1/websocket/helpers.py b/deepgram/clients/speak/v1/websocket/helpers.py new file mode 100644 index 00000000..dffcdb06 --- /dev/null +++ b/deepgram/clients/speak/v1/websocket/helpers.py @@ -0,0 +1,43 @@ +from urllib.parse import urlparse, urlunparse, parse_qs, urlencode +from typing import Dict, Optional +import re + + +# This function appends query parameters to a URL +def append_query_params(url: str, params: Optional[Dict] = None): + """ + Appends query parameters to a URL + """ + parsed_url = urlparse(url) + query_params = parse_qs(parsed_url.query) + + if params is not None: + for key, value in params.items(): + if value is None: + continue + if isinstance(value, bool): + value = str(value).lower() + if isinstance(value, list): + for item in value: + query_params[key] = query_params.get(key, []) + [str(item)] + else: + query_params[key] = [str(value)] + + updated_query_string = urlencode(query_params, doseq=True) + updated_url = parsed_url._replace(query=updated_query_string).geturl() + return updated_url + + +# This function converts a URL to a WebSocket URL +def convert_to_websocket_url(base_url: str, endpoint: str): + """ + Converts a URL to a WebSocket URL + """ + if re.match(r"^https?://", base_url, re.IGNORECASE): + base_url = base_url.replace("https://", "").replace("http://", "") + if not re.match(r"^wss?://", base_url, re.IGNORECASE): + base_url = "wss://" + base_url + parsed_url = urlparse(base_url) + domain = parsed_url.netloc + websocket_url = urlunparse((parsed_url.scheme, domain, endpoint, "", "", "")) + return websocket_url diff --git a/deepgram/clients/speak/v1/websocket/response.py b/deepgram/clients/speak/v1/websocket/response.py new file mode 100644 index 00000000..87f1d466 --- /dev/null +++ b/deepgram/clients/speak/v1/websocket/response.py @@ -0,0 +1,185 @@ +# Copyright 2024 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 typing import Optional +import io + +from dataclasses import dataclass, field +from dataclasses_json import config as dataclass_config, DataClassJsonMixin + +# Speak Response Types: + + +@dataclass +class SpeakWebSocketResponse( + DataClassJsonMixin +): # pylint: disable=too-many-instance-attributes + """ + A class for representing a response from the speak (streaming) endpoint. + """ + + content_type: str = "" + request_id: str = "" + model_uuid: str = "" + model_name: str = "" + date: str = "" + stream: Optional[io.BytesIO] = field( + default=None, metadata=dataclass_config(exclude=lambda f: f is None) + ) + + def __getitem__(self, key): + _dict = self.to_dict() + return _dict[key] + + def __setitem__(self, key, val): + self.__dict__[key] = val + + # this is a hack to make the response look like a dict because of the io.BytesIO object + # otherwise it will throw an exception on printing + def __str__(self) -> str: + my_dict = self.to_dict() + return my_dict.__str__() + + +@dataclass +class OpenResponse(DataClassJsonMixin): + """ + Open Message from the Deepgram Platform + """ + + type: str = "" + + def __getitem__(self, key): + _dict = self.to_dict() + return _dict[key] + + def __setitem__(self, key, val): + self.__dict__[key] = val + + def __str__(self) -> str: + return self.to_json(indent=4) + + +@dataclass +class MetadataResponse(DataClassJsonMixin): + """ + Metadata object + """ + + request_id: str = "" + + def __getitem__(self, key): + _dict = self.to_dict() + return _dict[key] + + def __setitem__(self, key, val): + self.__dict__[key] = val + + def __str__(self) -> str: + return self.to_json(indent=4) + + +@dataclass +class FlushedResponse(DataClassJsonMixin): + """ + Flushed Message from the Deepgram Platform + """ + + type: str = "" + + def __getitem__(self, key): + _dict = self.to_dict() + return _dict[key] + + def __setitem__(self, key, val): + self.__dict__[key] = val + + def __str__(self) -> str: + return self.to_json(indent=4) + + +@dataclass +class CloseResponse(DataClassJsonMixin): + """ + Close Message from the Deepgram Platform + """ + + type: str = "" + + def __getitem__(self, key): + _dict = self.to_dict() + return _dict[key] + + def __setitem__(self, key, val): + self.__dict__[key] = val + + def __str__(self) -> str: + return self.to_json(indent=4) + + +@dataclass +class ErrorResponse(DataClassJsonMixin): + """ + Error Message from the Deepgram Platform + """ + + description: str = "" + message: str = "" + type: str = "" + variant: Optional[str] = "" + + def __getitem__(self, key): + _dict = self.to_dict() + return _dict[key] + + def __setitem__(self, key, val): + self.__dict__[key] = val + + def __str__(self) -> str: + return self.to_json(indent=4) + + +@dataclass +class WarningResponse(DataClassJsonMixin): + """ + Warning Message from the Deepgram Platform + """ + + warn_code: str = "" + warn_msg: str = "" + type: str = "" + + def __getitem__(self, key): + _dict = self.to_dict() + return _dict[key] + + def __setitem__(self, key, val): + self.__dict__[key] = val + + def __str__(self) -> str: + return self.to_json(indent=4) + + +# Unhandled Message + + +@dataclass +class UnhandledResponse(DataClassJsonMixin): + """ + Unhandled Message from the Deepgram Platform + """ + + type: str = "" + raw: str = "" + + def __getitem__(self, key): + _dict = self.to_dict() + return _dict[key] + + def __setitem__(self, key, val): + self.__dict__[key] = val + + def __str__(self) -> str: + return self.to_json(indent=4) diff --git a/deepgram/clients/speak_router.py b/deepgram/clients/speak_router.py new file mode 100644 index 00000000..5e5dec2f --- /dev/null +++ b/deepgram/clients/speak_router.py @@ -0,0 +1,171 @@ +# Copyright 2023-2024 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 importlib import import_module +import logging +import deprecation # type: ignore + +from .. import __version__ +from .speak.v1.rest.client import SpeakRESTClient +from ..utils import verboselogs +from ..options import DeepgramClientOptions +from .errors import DeepgramModuleError + + +class Speak: + """ + This class provides a Speak Clients for making requests to the Deepgram API with various configuration options. + + Attributes: + config_options (DeepgramClientOptions): An optional configuration object specifying client options. + + Methods: + rest: (Preferred) Returns a Threaded REST Client instance for interacting with Deepgram's transcription services. + websocket: (Preferred) Returns an Threaded WebSocket Client instance for interacting with Deepgram's prerecorded transcription services. + + asyncrest: Returns an Async REST Client instance for interacting with Deepgram's transcription services. + asyncwebsocket: Returns an Async WebSocket Client instance for interacting with Deepgram's prerecorded transcription services. + """ + + _logger: verboselogs.VerboseLogger + _config: DeepgramClientOptions + + def __init__(self, config: DeepgramClientOptions): + self._logger = verboselogs.VerboseLogger(__name__) + self._logger.addHandler(logging.StreamHandler()) + self._logger.setLevel(config.verbose) + self._config = config + + # when this is removed, remove --disable=W0622 from Makefile + # pylint: disable=unused-argument + @deprecation.deprecated( + deprecated_in="3.4.0", + removed_in="4.0.0", + current_version=__version__, + details="deepgram.speak.v1 is deprecated. Use deepgram.speak.rest or deepgram.speak.websocket instead.", + ) + def v(self, version: str = ""): + """ + DEPRECATED: deepgram.speak.v1 is deprecated. Use deepgram.speak.rest or deepgram.speak.websocket instead. + """ + return SpeakRESTClient(self._config) + + # pylint: enable=unused-argument + + @property + def rest(self): + """ + Returns a Threaded REST Client instance for interacting with Deepgram's prerecorded Text-to-Speech services. + """ + return self.Version(self._config, "rest") + + @property + def asyncrest(self): + """ + Returns an Async REST Client instance for interacting with Deepgram's prerecorded Text-to-Speech services. + """ + return self.Version(self._config, "asyncrest") + + @property + def websocket(self): + """ + Returns a Threaded WebSocket Client instance for interacting with Deepgram's Text-to-Speech services. + """ + return self.Version(self._config, "websocket") + + @property + def asyncwebsocket(self): + """ + Returns an Async WebSocket Client instance for interacting with Deepgram's Text-to-Speech services. + """ + return self.Version(self._config, "asyncwebsocket") + + # INTERNAL CLASSES + class Version: + """ + Represents a version of the Deepgram API. + """ + + _logger: verboselogs.VerboseLogger + _config: DeepgramClientOptions + _parent: str + + def __init__(self, config, parent: str): + self._logger = verboselogs.VerboseLogger(__name__) + self._logger.addHandler(logging.StreamHandler()) + self._logger.setLevel(config.verbose) + self._config = config + self._parent = parent + + # FUTURE VERSIONING: + # When v2 or v1.1beta1 or etc. This allows easy access to the latest version of the API. + # @property + # def latest(self): + # match self._parent: + # case "live": + # return LiveClient(self._config) + # case "prerecorded": + # return PreRecordedClient(self._config) + # case _: + # raise DeepgramModuleError("Invalid parent") + + def v(self, version: str = ""): + """ + Returns a specific version of the Deepgram API. + """ + self._logger.debug("Version.v ENTER") + self._logger.info("version: %s", version) + if len(version) == 0: + self._logger.error("version is empty") + self._logger.debug("Version.v LEAVE") + raise DeepgramModuleError("Invalid module version") + + type = "" + file_name = "" + class_name = "" + match self._parent: + case "websocket": + type = "websocket" + file_name = "client" + class_name = "SpeakWebSocketClient" + case "asyncwebsocket": + type = "websocket" + file_name = "async_client" + class_name = "AsyncSpeakWebSocketClient" + case "rest": + type = "rest" + file_name = "client" + class_name = "SpeakRESTClient" + case "asyncrest": + type = "rest" + file_name = "async_client" + class_name = "AsyncSpeakRESTClient" + case _: + self._logger.error("parent unknown: %s", self._parent) + self._logger.debug("Version.v LEAVE") + raise DeepgramModuleError("Invalid parent type") + + # create class path + path = f"deepgram.clients.speak.v{version}.{type}.{file_name}" + self._logger.info("path: %s", path) + self._logger.info("class_name: %s", class_name) + + # import class + mod = import_module(path) + if mod is None: + self._logger.error("module path is None") + self._logger.debug("Version.v LEAVE") + raise DeepgramModuleError("Unable to find package") + + my_class = getattr(mod, class_name) + if my_class is None: + self._logger.error("my_class is None") + self._logger.debug("Version.v LEAVE") + raise DeepgramModuleError("Unable to find class") + + # instantiate class + my_class = my_class(self._config) + self._logger.notice("Version.v succeeded") + self._logger.debug("Version.v LEAVE") + return my_class diff --git a/examples/prerecorded/async_url/main.py b/examples/speech-to-text/rest/async_url/main.py similarity index 100% rename from examples/prerecorded/async_url/main.py rename to examples/speech-to-text/rest/async_url/main.py diff --git a/examples/prerecorded/callback/README.md b/examples/speech-to-text/rest/callback/README.md similarity index 100% rename from examples/prerecorded/callback/README.md rename to examples/speech-to-text/rest/callback/README.md diff --git a/examples/prerecorded/callback/callback/main.py b/examples/speech-to-text/rest/callback/callback/main.py similarity index 100% rename from examples/prerecorded/callback/callback/main.py rename to examples/speech-to-text/rest/callback/callback/main.py diff --git a/examples/prerecorded/callback/callback/preamble.wav b/examples/speech-to-text/rest/callback/callback/preamble.wav similarity index 100% rename from examples/prerecorded/callback/callback/preamble.wav rename to examples/speech-to-text/rest/callback/callback/preamble.wav diff --git a/examples/prerecorded/callback/endpoint/localhost.crt b/examples/speech-to-text/rest/callback/endpoint/localhost.crt similarity index 100% rename from examples/prerecorded/callback/endpoint/localhost.crt rename to examples/speech-to-text/rest/callback/endpoint/localhost.crt diff --git a/examples/prerecorded/callback/endpoint/localhost.csr b/examples/speech-to-text/rest/callback/endpoint/localhost.csr similarity index 100% rename from examples/prerecorded/callback/endpoint/localhost.csr rename to examples/speech-to-text/rest/callback/endpoint/localhost.csr diff --git a/examples/prerecorded/callback/endpoint/localhost.key b/examples/speech-to-text/rest/callback/endpoint/localhost.key similarity index 100% rename from examples/prerecorded/callback/endpoint/localhost.key rename to examples/speech-to-text/rest/callback/endpoint/localhost.key diff --git a/examples/prerecorded/callback/endpoint/main.py b/examples/speech-to-text/rest/callback/endpoint/main.py similarity index 100% rename from examples/prerecorded/callback/endpoint/main.py rename to examples/speech-to-text/rest/callback/endpoint/main.py diff --git a/examples/prerecorded/file/main.py b/examples/speech-to-text/rest/file/main.py similarity index 100% rename from examples/prerecorded/file/main.py rename to examples/speech-to-text/rest/file/main.py diff --git a/examples/prerecorded/file/preamble.wav b/examples/speech-to-text/rest/file/preamble.wav similarity index 100% rename from examples/prerecorded/file/preamble.wav rename to examples/speech-to-text/rest/file/preamble.wav diff --git a/examples/prerecorded/intent/CallCenterPhoneCall.mp3 b/examples/speech-to-text/rest/intent/CallCenterPhoneCall.mp3 similarity index 100% rename from examples/prerecorded/intent/CallCenterPhoneCall.mp3 rename to examples/speech-to-text/rest/intent/CallCenterPhoneCall.mp3 diff --git a/examples/prerecorded/intent/main.py b/examples/speech-to-text/rest/intent/main.py similarity index 100% rename from examples/prerecorded/intent/main.py rename to examples/speech-to-text/rest/intent/main.py diff --git a/examples/prerecorded/legacy_dict_url/main.py b/examples/speech-to-text/rest/legacy_dict_url/main.py similarity index 100% rename from examples/prerecorded/legacy_dict_url/main.py rename to examples/speech-to-text/rest/legacy_dict_url/main.py diff --git a/examples/prerecorded/sentiment/CallCenterPhoneCall.mp3 b/examples/speech-to-text/rest/sentiment/CallCenterPhoneCall.mp3 similarity index 100% rename from examples/prerecorded/sentiment/CallCenterPhoneCall.mp3 rename to examples/speech-to-text/rest/sentiment/CallCenterPhoneCall.mp3 diff --git a/examples/prerecorded/sentiment/main.py b/examples/speech-to-text/rest/sentiment/main.py similarity index 100% rename from examples/prerecorded/sentiment/main.py rename to examples/speech-to-text/rest/sentiment/main.py diff --git a/examples/prerecorded/stream_file/main.py b/examples/speech-to-text/rest/stream_file/main.py similarity index 100% rename from examples/prerecorded/stream_file/main.py rename to examples/speech-to-text/rest/stream_file/main.py diff --git a/examples/prerecorded/stream_file/preamble.wav b/examples/speech-to-text/rest/stream_file/preamble.wav similarity index 100% rename from examples/prerecorded/stream_file/preamble.wav rename to examples/speech-to-text/rest/stream_file/preamble.wav diff --git a/examples/prerecorded/summary/CallCenterPhoneCall.mp3 b/examples/speech-to-text/rest/summary/CallCenterPhoneCall.mp3 similarity index 100% rename from examples/prerecorded/summary/CallCenterPhoneCall.mp3 rename to examples/speech-to-text/rest/summary/CallCenterPhoneCall.mp3 diff --git a/examples/prerecorded/summary/main.py b/examples/speech-to-text/rest/summary/main.py similarity index 100% rename from examples/prerecorded/summary/main.py rename to examples/speech-to-text/rest/summary/main.py diff --git a/examples/prerecorded/topic/CallCenterPhoneCall.mp3 b/examples/speech-to-text/rest/topic/CallCenterPhoneCall.mp3 similarity index 100% rename from examples/prerecorded/topic/CallCenterPhoneCall.mp3 rename to examples/speech-to-text/rest/topic/CallCenterPhoneCall.mp3 diff --git a/examples/prerecorded/topic/main.py b/examples/speech-to-text/rest/topic/main.py similarity index 100% rename from examples/prerecorded/topic/main.py rename to examples/speech-to-text/rest/topic/main.py diff --git a/examples/prerecorded/url/main.py b/examples/speech-to-text/rest/url/main.py similarity index 100% rename from examples/prerecorded/url/main.py rename to examples/speech-to-text/rest/url/main.py diff --git a/examples/streaming/async_http/main.py b/examples/speech-to-text/websocket/async_http/main.py similarity index 100% rename from examples/streaming/async_http/main.py rename to examples/speech-to-text/websocket/async_http/main.py diff --git a/examples/streaming/async_microphone/README.md b/examples/speech-to-text/websocket/async_microphone/README.md similarity index 100% rename from examples/streaming/async_microphone/README.md rename to examples/speech-to-text/websocket/async_microphone/README.md diff --git a/examples/streaming/async_microphone/main.py b/examples/speech-to-text/websocket/async_microphone/main.py similarity index 100% rename from examples/streaming/async_microphone/main.py rename to examples/speech-to-text/websocket/async_microphone/main.py diff --git a/examples/streaming/http/main.py b/examples/speech-to-text/websocket/http/main.py similarity index 100% rename from examples/streaming/http/main.py rename to examples/speech-to-text/websocket/http/main.py diff --git a/examples/streaming/legacy_dict_microphone/README.md b/examples/speech-to-text/websocket/legacy_dict_microphone/README.md similarity index 100% rename from examples/streaming/legacy_dict_microphone/README.md rename to examples/speech-to-text/websocket/legacy_dict_microphone/README.md diff --git a/examples/streaming/legacy_dict_microphone/main.py b/examples/speech-to-text/websocket/legacy_dict_microphone/main.py similarity index 100% rename from examples/streaming/legacy_dict_microphone/main.py rename to examples/speech-to-text/websocket/legacy_dict_microphone/main.py diff --git a/examples/streaming/microphone/README.md b/examples/speech-to-text/websocket/microphone/README.md similarity index 100% rename from examples/streaming/microphone/README.md rename to examples/speech-to-text/websocket/microphone/README.md diff --git a/examples/streaming/microphone/main.py b/examples/speech-to-text/websocket/microphone/main.py similarity index 100% rename from examples/streaming/microphone/main.py rename to examples/speech-to-text/websocket/microphone/main.py diff --git a/examples/speak/file/async_hello_world/main.py b/examples/text-to-speech/rest/file/async_hello_world/main.py similarity index 100% rename from examples/speak/file/async_hello_world/main.py rename to examples/text-to-speech/rest/file/async_hello_world/main.py diff --git a/examples/speak/file/hello_world/main.py b/examples/text-to-speech/rest/file/hello_world/main.py similarity index 100% rename from examples/speak/file/hello_world/main.py rename to examples/text-to-speech/rest/file/hello_world/main.py diff --git a/examples/text-to-speech/rest/file/hello_world_new/main.py b/examples/text-to-speech/rest/file/hello_world_new/main.py new file mode 100644 index 00000000..96254593 --- /dev/null +++ b/examples/text-to-speech/rest/file/hello_world_new/main.py @@ -0,0 +1,41 @@ +# Copyright 2024 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 dotenv import load_dotenv +import logging +from deepgram.utils import verboselogs + +from deepgram import ( + DeepgramClient, + ClientOptionsFromEnv, + SpeakOptions, +) + +load_dotenv() + +SPEAK_OPTIONS = {"text": "Hello world!"} +filename = "test.mp3" + + +def main(): + try: + # STEP 1 Create a Deepgram client using the API key from environment variables + deepgram = DeepgramClient( + api_key="", config=ClientOptionsFromEnv(verbose=verboselogs.SPAM) + ) + + # STEP 2 Call the save method on the speak property + options = SpeakOptions( + model="aura-asteria-en", + ) + + response = deepgram.speak.rest.v("1").save(filename, SPEAK_OPTIONS, options) + print(response.to_json(indent=4)) + + except Exception as e: + print(f"Exception: {e}") + + +if __name__ == "__main__": + main() diff --git a/examples/speak/file/legacy_dict_hello_world/main.py b/examples/text-to-speech/rest/file/legacy_dict_hello_world/main.py similarity index 100% rename from examples/speak/file/legacy_dict_hello_world/main.py rename to examples/text-to-speech/rest/file/legacy_dict_hello_world/main.py diff --git a/examples/speak/file/woodchuck/main.py b/examples/text-to-speech/rest/file/woodchuck/main.py similarity index 100% rename from examples/speak/file/woodchuck/main.py rename to examples/text-to-speech/rest/file/woodchuck/main.py diff --git a/examples/speak-stream/async-interactive/main.py b/examples/text-to-speech/websocket/async_interactive/main.py similarity index 72% rename from examples/speak-stream/async-interactive/main.py rename to examples/text-to-speech/websocket/async_interactive/main.py index b0bc2f71..0962d3b7 100644 --- a/examples/speak-stream/async-interactive/main.py +++ b/examples/text-to-speech/websocket/async_interactive/main.py @@ -5,7 +5,7 @@ from deepgram import ( DeepgramClient, DeepgramClientOptions, - SpeakStreamEvents, + SpeakWebSocketEvents, SpeakOptions, ) @@ -14,16 +14,19 @@ TTS_TEXT = "Hello, this is a text to speech example using Deepgram." AUDIO_FILE = "output.mp3" + async def main(): try: # example of setting up a client config. logging values: WARNING, VERBOSE, DEBUG, SPAM - # config: DeepgramClientOptions = DeepgramClientOptions(verbose=verboselogs.DEBUG) - # deepgram: DeepgramClient = DeepgramClient("", config) + config: DeepgramClientOptions = DeepgramClientOptions( + url="api.beta.deepgram.com", verbose=verboselogs.DEBUG + ) + deepgram: DeepgramClient = DeepgramClient("", config) # otherwise, use default config - deepgram: DeepgramClient = DeepgramClient() + # deepgram: DeepgramClient = DeepgramClient() # Create a websocket connection to Deepgram - dg_connection = deepgram.asyncspeakstream.v("1") + dg_connection = deepgram.speak.asyncwebsocket.v("1") async def on_open(client, open_response, **kwargs): print(f"\n\nOpen: {open_response}\n\n") @@ -44,7 +47,7 @@ async def on_close(client, close, **kwargs): async def on_warning(client, warning, **kwargs): print(f"\n\nWarning: {warning}\n\n") - + async def on_error(client, error, **kwargs): print(f"\n\nError: {error}\n\n") @@ -61,14 +64,14 @@ async def write_binary_to_mp3(data): finally: print("File operation completed.") - dg_connection.on(SpeakStreamEvents.Open, on_open) - dg_connection.on(SpeakStreamEvents.AudioData, on_binary_data) - dg_connection.on(SpeakStreamEvents.Metadata, on_metadata) - dg_connection.on(SpeakStreamEvents.Flush, on_flush) - dg_connection.on(SpeakStreamEvents.Close, on_close) - dg_connection.on(SpeakStreamEvents.Warning, on_warning) - dg_connection.on(SpeakStreamEvents.Error, on_error) - dg_connection.on(SpeakStreamEvents.Unhandled, on_unhandled) + dg_connection.on(SpeakWebSocketEvents.Open, on_open) + dg_connection.on(SpeakWebSocketEvents.AudioData, on_binary_data) + dg_connection.on(SpeakWebSocketEvents.Metadata, on_metadata) + dg_connection.on(SpeakWebSocketEvents.Flush, on_flush) + dg_connection.on(SpeakWebSocketEvents.Close, on_close) + dg_connection.on(SpeakWebSocketEvents.Warning, on_warning) + dg_connection.on(SpeakWebSocketEvents.Error, on_error) + dg_connection.on(SpeakWebSocketEvents.Unhandled, on_unhandled) async def send_tts_text(client): await client.send(TTS_TEXT) @@ -81,7 +84,9 @@ async def send_tts_text(client): return # Wait for user input to finish - await asyncio.get_event_loop().run_in_executor(None, input, "\n\nPress Enter to stop...\n\n") + await asyncio.get_event_loop().run_in_executor( + None, input, "\n\nPress Enter to stop...\n\n" + ) await dg_connection.finish() print("Finished") @@ -95,5 +100,6 @@ async def send_tts_text(client): except Exception as e: print(f"An unexpected error occurred: {e}") + if __name__ == "__main__": asyncio.run(main()) diff --git a/examples/speak-stream/interactive/main.py b/examples/text-to-speech/websocket/interactive/main.py similarity index 67% rename from examples/speak-stream/interactive/main.py rename to examples/text-to-speech/websocket/interactive/main.py index f13b0864..3a560213 100644 --- a/examples/speak-stream/interactive/main.py +++ b/examples/text-to-speech/websocket/interactive/main.py @@ -10,7 +10,7 @@ from deepgram import ( DeepgramClient, DeepgramClientOptions, - SpeakStreamEvents, + SpeakWebSocketEvents, SpeakOptions, ) @@ -23,14 +23,16 @@ def main(): try: - # example of setting up a client config. logging values: WARNING, VERBOSE, DEBUG, SPAM - # config: DeepgramClientOptions = DeepgramClientOptions(verbose=verboselogs.DEBUG) - # deepgram: DeepgramClient = DeepgramClient("", config) + # example of setting up a client config. logging values: WARNING, VERBOSE, DEBUG, SPAM + config: DeepgramClientOptions = DeepgramClientOptions( + url="api.beta.deepgram.com", verbose=verboselogs.DEBUG + ) + deepgram: DeepgramClient = DeepgramClient("", config) # otherwise, use default config - deepgram: DeepgramClient = DeepgramClient() + # deepgram: DeepgramClient = DeepgramClient() # Create a websocket connection to Deepgram - dg_connection = deepgram.speakstream.v("1") + dg_connection = deepgram.speak.websocket.v("1") # print(dg_connection) def on_open(self, open, **kwargs): @@ -40,19 +42,19 @@ def on_open(self, open, **kwargs): thread.join() def on_binary_data(self, data, **kwargs): - print("Received binary data:") - with open(AUDIO_FILE, "ab") as f: - f.write(data) + print("Received binary data:") + with open(AUDIO_FILE, "ab") as f: + f.write(data) def on_metadata(self, metadata, **kwargs): print(f"\n\n{metadata}\n\n") def on_flush(self, flush, **kwargs): - print(f"\n\n{flush}\n\n") + print(f"\n\n{flush}\n\n") def on_close(self, close, **kwargs): print(f"\n\n{close}\n\n") - + def on_warning(self, warning, **kwargs): print(f"\n\n{warning}\n\n") @@ -62,17 +64,17 @@ def on_error(self, error, **kwargs): def on_unhandled(self, unhandled, **kwargs): print(f"\n\n{unhandled}\n\n") - dg_connection.on(SpeakStreamEvents.Open, on_open) - dg_connection.on(SpeakStreamEvents.AudioData, on_binary_data) - dg_connection.on(SpeakStreamEvents.Metadata, on_metadata) - dg_connection.on(SpeakStreamEvents.Flush, on_flush) - dg_connection.on(SpeakStreamEvents.Close, on_close) - dg_connection.on(SpeakStreamEvents.Error, on_error) - dg_connection.on(SpeakStreamEvents.Warning, on_warning) - dg_connection.on(SpeakStreamEvents.Unhandled, on_unhandled) + dg_connection.on(SpeakWebSocketEvents.Open, on_open) + dg_connection.on(SpeakWebSocketEvents.AudioData, on_binary_data) + dg_connection.on(SpeakWebSocketEvents.Metadata, on_metadata) + dg_connection.on(SpeakWebSocketEvents.Flush, on_flush) + dg_connection.on(SpeakWebSocketEvents.Close, on_close) + dg_connection.on(SpeakWebSocketEvents.Error, on_error) + dg_connection.on(SpeakWebSocketEvents.Warning, on_warning) + dg_connection.on(SpeakWebSocketEvents.Unhandled, on_unhandled) lock = threading.Lock() - + def send_tts_text(dg_connection): with lock: dg_connection.send(TTS_TEXT) diff --git a/mypy.ini b/mypy.ini index fb624ca2..112d0f24 100644 --- a/mypy.ini +++ b/mypy.ini @@ -23,4 +23,4 @@ ignore_missing_imports = True ignore_missing_imports = True [mypy-aenum] -ignore_missing_imports = True \ No newline at end of file +ignore_missing_imports = True diff --git a/pyproject.toml b/pyproject.toml index 8f62bacf..34893aac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,7 @@ dataclasses-json = "^0.6.3" aiohttp = "^3.9.1" aiofiles = "^23.2.1" aenum = "^3.1.0" +deprecation = "^2.1.0" # needed only if you are looking to develop/work-on the SDK # black = "^24.0" # pylint = "^3.0" diff --git a/requirements.txt b/requirements.txt index 7b5d360a..53571822 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,14 @@ # pip install -r requirements.txt # standard libs -websockets -httpx -dataclasses-json -dataclasses -typing_extensions -aenum +websockets==12.* +httpx==0.* +dataclasses-json==0.* +dataclasses==0.* +typing_extensions==4.* +aenum==3.* +deprecation==2.* # Async functionality, likely to be already installed -aiohttp -aiofiles +aiohttp==3.* +aiofiles==23.* diff --git a/setup.py b/setup.py index ffa7e436..36445198 100644 --- a/setup.py +++ b/setup.py @@ -34,6 +34,7 @@ "aiohttp>=3.9.1", "aiofiles>=23.2.1", "aenum>=3.1.0", + "deprecation>=2.1.0", ], keywords=["deepgram", "deepgram speech-to-text"], classifiers=[