diff --git a/Deepgram.Dev.sln b/Deepgram.Dev.sln index 744e5ce0..d0f59b96 100644 --- a/Deepgram.Dev.sln +++ b/Deepgram.Dev.sln @@ -169,6 +169,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "reconnect_same_object", "re EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReconnectStreaming", "tests\edge_cases\reconnect_same_object\ReconnectStreaming.csproj", "{64AB4BAC-6917-424D-A5EA-BA023BD7795A}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "stt_v1_client_example", "stt_v1_client_example", "{0BF29CA2-1CD6-4FF0-BC7B-B33C6B41E9A1}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tts_v1_client_example", "tts_v1_client_example", "{5CEEB2F0-F284-4BB3-8999-6E91151C0C06}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Streaming", "tests\edge_cases\stt_v1_client_example\Streaming.csproj", "{964A87B4-31F8-4D68-AE64-64E66C9959FD}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Speak", "tests\edge_cases\tts_v1_client_example\Speak.csproj", "{AB053DDA-2487-476C-9793-A50C37F10CCA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -311,6 +319,14 @@ Global {64AB4BAC-6917-424D-A5EA-BA023BD7795A}.Debug|Any CPU.Build.0 = Debug|Any CPU {64AB4BAC-6917-424D-A5EA-BA023BD7795A}.Release|Any CPU.ActiveCfg = Release|Any CPU {64AB4BAC-6917-424D-A5EA-BA023BD7795A}.Release|Any CPU.Build.0 = Release|Any CPU + {964A87B4-31F8-4D68-AE64-64E66C9959FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {964A87B4-31F8-4D68-AE64-64E66C9959FD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {964A87B4-31F8-4D68-AE64-64E66C9959FD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {964A87B4-31F8-4D68-AE64-64E66C9959FD}.Release|Any CPU.Build.0 = Release|Any CPU + {AB053DDA-2487-476C-9793-A50C37F10CCA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB053DDA-2487-476C-9793-A50C37F10CCA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB053DDA-2487-476C-9793-A50C37F10CCA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB053DDA-2487-476C-9793-A50C37F10CCA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -392,6 +408,10 @@ Global {ECB0B55E-54C1-4723-8641-9249E7507FB0} = {2F92D959-D3C7-4EFF-8549-C6162E517644} {6E328CB9-8C9B-446B-B83E-3796804497F7} = {1280E66D-A375-422A-ACB4-48F17E9C190E} {64AB4BAC-6917-424D-A5EA-BA023BD7795A} = {6E328CB9-8C9B-446B-B83E-3796804497F7} + {0BF29CA2-1CD6-4FF0-BC7B-B33C6B41E9A1} = {1280E66D-A375-422A-ACB4-48F17E9C190E} + {5CEEB2F0-F284-4BB3-8999-6E91151C0C06} = {1280E66D-A375-422A-ACB4-48F17E9C190E} + {964A87B4-31F8-4D68-AE64-64E66C9959FD} = {0BF29CA2-1CD6-4FF0-BC7B-B33C6B41E9A1} + {AB053DDA-2487-476C-9793-A50C37F10CCA} = {5CEEB2F0-F284-4BB3-8999-6E91151C0C06} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {8D4ABC6D-7126-4EE2-9303-43A954616B2A} diff --git a/Deepgram.Tests/Fakes/ConcreteRestClient.cs b/Deepgram.Tests/Fakes/ConcreteRestClient.cs index 8c57fdd3..ca7bb61a 100644 --- a/Deepgram.Tests/Fakes/ConcreteRestClient.cs +++ b/Deepgram.Tests/Fakes/ConcreteRestClient.cs @@ -3,6 +3,7 @@ // SPDX-License-Identifier: MIT using Deepgram.Models.Authenticate.v1; +using Deepgram.Abstractions.v1; namespace Deepgram.Tests.Fakes; diff --git a/Deepgram.Tests/UnitTests/ClientTests/AbstractRestClientTests.cs b/Deepgram.Tests/UnitTests/ClientTests/AbstractRestClientTests.cs index 91f6edb1..bbc93d6d 100644 --- a/Deepgram.Tests/UnitTests/ClientTests/AbstractRestClientTests.cs +++ b/Deepgram.Tests/UnitTests/ClientTests/AbstractRestClientTests.cs @@ -5,7 +5,6 @@ using Deepgram.Models.Authenticate.v1; using Deepgram.Models.Manage.v1; using Deepgram.Models.PreRecorded.v1; -using Deepgram.Models.Exceptions.v1; using Deepgram.Clients.Manage.v1; diff --git a/Deepgram.Tests/UnitTests/ClientTests/AnalyzeClientTests.cs b/Deepgram.Tests/UnitTests/ClientTests/AnalyzeClientTests.cs index 3eae0f22..e38678d9 100644 --- a/Deepgram.Tests/UnitTests/ClientTests/AnalyzeClientTests.cs +++ b/Deepgram.Tests/UnitTests/ClientTests/AnalyzeClientTests.cs @@ -5,6 +5,7 @@ using Deepgram.Models.Authenticate.v1; using Deepgram.Clients.Analyze.v1; using Deepgram.Models.Analyze.v1; +using Deepgram.Abstractions.v1; namespace Deepgram.Tests.UnitTests.ClientTests; diff --git a/Deepgram.Tests/UnitTests/ClientTests/ManageClientTest.cs b/Deepgram.Tests/UnitTests/ClientTests/ManageClientTest.cs index 6732e5b6..671970ac 100644 --- a/Deepgram.Tests/UnitTests/ClientTests/ManageClientTest.cs +++ b/Deepgram.Tests/UnitTests/ClientTests/ManageClientTest.cs @@ -5,8 +5,7 @@ using Deepgram.Models.Authenticate.v1; using Deepgram.Models.Manage.v1; using Deepgram.Clients.Manage.v1; -using Microsoft.VisualStudio.TestPlatform.ObjectModel.DataCollection; -using NSubstitute; +using Deepgram.Abstractions.v1; namespace Deepgram.Tests.UnitTests.ClientTests; diff --git a/Deepgram.Tests/UnitTests/ClientTests/OnPremClientTests.cs b/Deepgram.Tests/UnitTests/ClientTests/OnPremClientTests.cs index 2b786c8a..575c676b 100644 --- a/Deepgram.Tests/UnitTests/ClientTests/OnPremClientTests.cs +++ b/Deepgram.Tests/UnitTests/ClientTests/OnPremClientTests.cs @@ -5,6 +5,7 @@ using Deepgram.Models.Authenticate.v1; using Deepgram.Models.SelfHosted.v1; using Deepgram.Clients.SelfHosted.v1; +using Deepgram.Abstractions.v1; namespace Deepgram.Tests.UnitTests.ClientTests; diff --git a/Deepgram.Tests/UnitTests/ClientTests/PrerecordedClientTests.cs b/Deepgram.Tests/UnitTests/ClientTests/PrerecordedClientTests.cs index 4ca51130..335f1e08 100644 --- a/Deepgram.Tests/UnitTests/ClientTests/PrerecordedClientTests.cs +++ b/Deepgram.Tests/UnitTests/ClientTests/PrerecordedClientTests.cs @@ -5,6 +5,7 @@ using Deepgram.Models.Authenticate.v1; using Deepgram.Clients.Listen.v1.REST; using Deepgram.Models.Listen.v1.REST; +using Deepgram.Abstractions.v1; namespace Deepgram.Tests.UnitTests.ClientTests; diff --git a/Deepgram.Tests/UnitTests/ClientTests/SpeakClientTests.cs b/Deepgram.Tests/UnitTests/ClientTests/SpeakClientTests.cs index b9b84e5e..47375c6f 100644 --- a/Deepgram.Tests/UnitTests/ClientTests/SpeakClientTests.cs +++ b/Deepgram.Tests/UnitTests/ClientTests/SpeakClientTests.cs @@ -5,6 +5,7 @@ using Deepgram.Models.Authenticate.v1; using Deepgram.Models.Speak.v1.REST; using Deepgram.Clients.Speak.v1.REST; +using Deepgram.Abstractions.v1; namespace Deepgram.Tests.UnitTests.ClientTests; diff --git a/Deepgram.Tests/UnitTests/UtilitiesTests/RequestContentUtilTests.cs b/Deepgram.Tests/UnitTests/UtilitiesTests/RequestContentUtilTests.cs index cdba2d3e..c13e2b6b 100644 --- a/Deepgram.Tests/UnitTests/UtilitiesTests/RequestContentUtilTests.cs +++ b/Deepgram.Tests/UnitTests/UtilitiesTests/RequestContentUtilTests.cs @@ -3,7 +3,7 @@ // SPDX-License-Identifier: MIT using Deepgram.Models.Manage.v1; -using Deepgram.Abstractions; +using Deepgram.Abstractions.v1; namespace Deepgram.Tests.UnitTests.UtilitiesTests; diff --git a/Deepgram/Abstractions/LocalFileWithMetadata.cs b/Deepgram/Abstractions/LocalFileWithMetadata.cs deleted file mode 100644 index 9035750d..00000000 --- a/Deepgram/Abstractions/LocalFileWithMetadata.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2024 Deepgram .NET 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 - -namespace Deepgram.Abstractions; - -/// -/// LocalFileWithMetadata is a class that represents a file with metadata. -/// -public class LocalFileWithMetadata -{ - public Dictionary Metadata { get; set; } - - public MemoryStream Content { get; set; } -} diff --git a/Deepgram/Abstractions/AbstractRestClient.cs b/Deepgram/Abstractions/v1/AbstractRestClient.cs similarity index 95% rename from Deepgram/Abstractions/AbstractRestClient.cs rename to Deepgram/Abstractions/v1/AbstractRestClient.cs index 56ba7901..07decabd 100644 --- a/Deepgram/Abstractions/AbstractRestClient.cs +++ b/Deepgram/Abstractions/v1/AbstractRestClient.cs @@ -1,782 +1,782 @@ -// Copyright 2021-2024 Deepgram .NET 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 - -using Deepgram.Encapsulations; -using Deepgram.Models.Authenticate.v1; -using Deepgram.Models.Exceptions.v1; - -namespace Deepgram.Abstractions; - -public abstract class AbstractRestClient -{ - /// - /// HttpClient created by the factory - internal HttpClient _httpClient; - - /// - /// Copy of the options for the client - /// - internal IDeepgramClientOptions _options; - - /// - /// Constructor that take the options and a httpClient - /// - /// Options for the Deepgram client - - internal AbstractRestClient(string? apiKey = null, IDeepgramClientOptions? options = null, string? httpId = null) - { - Log.Verbose("AbstractRestClient", "ENTER"); - - if (options == null) - { - options = (IDeepgramClientOptions) new DeepgramHttpClientOptions(apiKey); - } - _httpClient = HttpClientFactory.Create(httpId); - _httpClient = HttpClientFactory.ConfigureDeepgram(_httpClient, options); - _options = options; - - Log.Debug("AbstractRestClient", $"APIVersion: {options.APIVersion}"); - Log.Debug("AbstractRestClient", $"BaseAddress: {options.BaseAddress}"); - Log.Debug("AbstractRestClient", $"options: {options.OnPrem}"); - Log.Verbose("AbstractRestClient", "LEAVE"); - } - - /// - /// GET Rest Request - /// - /// Type of class of response expected - /// request uri Endpoint - /// Instance of T - public virtual async Task GetAsync(string uriSegment, CancellationTokenSource? cancellationToken = null, - Dictionary? addons = null, Dictionary? headers = null) - { - Log.Verbose("AbstractRestClient.GetAsync", "ENTER"); - Log.Debug("GetAsync", $"uriSegment: {uriSegment}"); - Log.Debug("GetAsync", $"addons: {addons}"); - - try - { - // if not defined, use default timeout - if (cancellationToken == null) - { - Log.Information("GetAsync", $"Using default timeout: {Constants.DefaultRESTTimeout}"); - cancellationToken = new CancellationTokenSource(); - cancellationToken.CancelAfter(Constants.DefaultRESTTimeout); - } - - // create request message and add custom query parameters - NoopSchema? parameter = null; - var request = new HttpRequestMessage(HttpMethod.Get, QueryParameterUtil.FormatURL(uriSegment, parameter, addons)); - - // add custom headers - if (headers != null) - { - foreach (var header in headers) - { - Log.Debug("GetAsync", $"Add Header {header.Key}={header.Value}"); - request.Headers.Add(header.Key, header.Value); - } - } - - // do the request - Log.Verbose("GetAsync", "Calling _httpClient.SendAsync..."); - var response = await _httpClient.SendAsync(request, cancellationToken.Token); - - var resultStr = response.Content.ReadAsStringAsync().Result; - if (!response.IsSuccessStatusCode) - { - await ThrowException("GetAsync", response, resultStr); - } - - Log.Verbose("GetAsync", $"Response:\n{resultStr}"); - var result = await HttpRequestUtil.DeserializeAsync(response); - - Log.Debug("GetAsync", "Succeeded"); - Log.Verbose("AbstractRestClient.GetAsync", "LEAVE"); - return result; - } - catch (OperationCanceledException ex) - { - Log.Information("GetAsync", "Task was cancelled."); - Log.Verbose("GetAsync", $"Connect cancelled. Info: {ex}"); - Log.Verbose("AbstractRestClient.GetAsync", "LEAVE"); - throw; - } - catch (Exception ex) - { - Log.Error("GetAsync", $"{ex.GetType()} thrown {ex.Message}"); - Log.Verbose("GetAsync", $"Excepton: {ex}"); - Log.Verbose("AbstractRestClient.GetAsync", "LEAVE"); - throw; - } - } - - public virtual async Task GetAsync(string uriSegment, S? parameter, CancellationTokenSource? cancellationToken = null, - Dictionary? addons = null, Dictionary? headers = null) - { - Log.Verbose("AbstractRestClient.GetAsync", "ENTER"); - Log.Debug("GetAsync", $"uriSegment: {uriSegment}"); - Log.Debug("GetAsync", $"addons: {addons}"); - - try - { - // if not defined, use default timeout - if (cancellationToken == null) - { - Log.Information("GetAsync", $"Using default timeout: {Constants.DefaultRESTTimeout}"); - cancellationToken = new CancellationTokenSource(); - cancellationToken.CancelAfter(Constants.DefaultRESTTimeout); - } - - // create request message and add custom query parameters - var request = new HttpRequestMessage(HttpMethod.Get, QueryParameterUtil.FormatURL(uriSegment, parameter, addons)); - - // add custom headers - if (headers != null) - { - foreach (var header in headers) - { - Log.Debug("GetAsync", $"Add Header {header.Key}={header.Value}"); - request.Headers.Add(header.Key, header.Value); - } - } - - // do the request - Log.Verbose("GetAsync", "Calling _httpClient.SendAsync..."); - var response = await _httpClient.SendAsync(request, cancellationToken.Token); - - var resultStr = response.Content.ReadAsStringAsync().Result; - if (!response.IsSuccessStatusCode) - { - await ThrowException("GetAsync", response, resultStr); - } - - Log.Verbose("GetAsync", $"Response:\n{resultStr}"); - var result = await HttpRequestUtil.DeserializeAsync(response); - - Log.Debug("GetAsync", "Succeeded"); - Log.Verbose("AbstractRestClient.GetAsync", "LEAVE"); - - return result; - } - catch (OperationCanceledException ex) - { - Log.Information("GetAsync", "Task was cancelled."); - Log.Verbose("GetAsync", $"Connect cancelled. Info: {ex}"); - Log.Verbose("AbstractRestClient.GetAsync", "LEAVE"); - throw; - } - catch (Exception ex) - { - Log.Error("GetAsync", $"{ex.GetType()} thrown {ex.Message}"); - Log.Verbose("GetAsync", $"Excepton: {ex}"); - Log.Verbose("AbstractRestClient.GetAsync", "LEAVE"); - throw; - } - } - - /// - /// Post method - /// - /// Class type of what return type is expected - /// Uri for the api including the query parameters - /// HttpContent as content for HttpRequestMessage - /// Instance of T - public virtual async Task PostRetrieveLocalFileAsync(string uriSegment, S? parameter, R? content, - List? keys = null, CancellationTokenSource? cancellationToken = null, Dictionary? addons = null, - Dictionary? headers = null - ) - { - Log.Verbose("AbstractRestClient.PostRetrieveLocalFileAsync", "ENTER"); - Log.Debug("PostRetrieveLocalFileAsync", $"uriSegment: {uriSegment}"); - Log.Debug("PostRetrieveLocalFileAsync", $"keys: {keys}"); - Log.Debug("PostRetrieveLocalFileAsync", $"addons: {addons}"); - - try - { - // if not defined, use default timeout - if (cancellationToken == null) - { - Log.Information("PostRetrieveLocalFileAsync", $"Using default timeout: {Constants.DefaultRESTTimeout}"); - cancellationToken = new CancellationTokenSource(); - cancellationToken.CancelAfter(Constants.DefaultRESTTimeout); - } - - // create request message and add custom query parameters - var request = new HttpRequestMessage(HttpMethod.Post, QueryParameterUtil.FormatURL(uriSegment, parameter, addons)) - { - Content = HttpRequestUtil.CreatePayload(content) - }; - - // add custom headers - if (headers != null) - { - foreach (var header in headers) - { - Log.Debug("PostRetrieveLocalFileAsync", $"Add Header {header.Key}={header.Value}"); - request.Headers.Add(header.Key, header.Value); - } - } - - // do the request - Log.Verbose("PostRetrieveLocalFileAsync", "Calling _httpClient.SendAsync..."); - var response = await _httpClient.SendAsync(request, cancellationToken.Token); - - if (!response.IsSuccessStatusCode) - { - await ThrowException("PostRetrieveLocalFileAsync", response, response.Content.ReadAsStringAsync().Result); - } - - var result = new Dictionary(); - - if (keys != null) - { - for (int i = 0; i < response.Headers.Count(); i++) - { - var key = response.Headers.ElementAt(i).Key.ToLower(); - var value = response.Headers.GetValues(key).FirstOrDefault() ?? ""; - - var index = key.IndexOf("x-dg-"); - if (index == 0) - { - var newKey = key.Substring(5); - if (keys.Contains(newKey)) - { - result.Add(newKey, value); - continue; - } - } - index = key.IndexOf("dg-"); - if (index == 0) - { - var newKey = key.Substring(3); - if (keys.Contains(newKey)) - { - result.Add(newKey, value); - continue; - } - } - if (keys.Contains(key)) - { - result.Add(key, value); - } - } - - if (keys.Contains("content-type")) - { - result.Add("content-type", response.Content.Headers.ContentType?.MediaType ?? ""); - } - } - - MemoryStream stream = new MemoryStream(); - await response.Content.CopyToAsync(stream); - - Log.Verbose("PostRetrieveLocalFileAsync", "Succeeded"); - Log.Verbose("AbstractRestClient.PostRetrieveLocalFileAsync", "LEAVE"); - - return new LocalFileWithMetadata() - { - Metadata = result, - Content = stream, - }; - - } - catch (OperationCanceledException ex) - { - Log.Information("PostRetrieveLocalFileAsync", "Task was cancelled."); - Log.Verbose("PostRetrieveLocalFileAsync", $"Connect cancelled. Info: {ex}"); - Log.Verbose("AbstractRestClient.PostRetrieveLocalFileAsync", "LEAVE"); - throw; - } - catch (Exception ex) - { - Log.Error("PostRetrieveLocalFileAsync", $"{ex.GetType()} thrown {ex.Message}"); - Log.Verbose("PostRetrieveLocalFileAsync", $"Excepton: {ex}"); - Log.Verbose("AbstractRestClient.PostRetrieveLocalFileAsync", "LEAVE"); - throw; - } - } - - /// - /// Post method - /// - /// Class type of what return type is expected - /// Uri for the api including the query parameters - /// HttpContent as content for HttpRequestMessage - /// Instance of T - public virtual async Task PostAsync(string uriSegment, S? parameter, CancellationTokenSource? cancellationToken = null, - Dictionary? addons = null, Dictionary? headers = null) - { - Log.Verbose("AbstractRestClient.PostAsync", "ENTER"); - Log.Debug("PostAsync", $"uriSegment: {uriSegment}"); - Log.Debug("PostAsync", $"addons: {addons}"); - - try - { - // if not defined, use default timeout - if (cancellationToken == null) - { - Log.Information("PostAsync", $"Using default timeout: {Constants.DefaultRESTTimeout}"); - cancellationToken = new CancellationTokenSource(); - cancellationToken.CancelAfter(Constants.DefaultRESTTimeout); - } - - // create request message and add custom query parameters - var request = new HttpRequestMessage(HttpMethod.Post, QueryParameterUtil.FormatURL(uriSegment, parameter, addons)) - { - Content = HttpRequestUtil.CreatePayload(parameter) - }; - - // add custom headers - if (headers != null) - { - foreach (var header in headers) - { - Log.Debug("PostAsync", $"Add Header {header.Key}={header.Value}"); - request.Headers.Add(header.Key, header.Value); - } - } - - // do the request - Log.Verbose("PostAsync", "Calling _httpClient.SendAsync..."); - var response = await _httpClient.SendAsync(request, cancellationToken.Token); - - var resultStr = response.Content.ReadAsStringAsync().Result; - if (!response.IsSuccessStatusCode) - { - await ThrowException("PostAsync", response, resultStr); - } - - Log.Verbose("PostAsync", $"Response:\n{resultStr}"); - var result = await HttpRequestUtil.DeserializeAsync(response); - - Log.Debug("PostAsync", $"Succeeded"); - Log.Verbose("AbstractRestClient.PostAsync", "LEAVE"); - - return result; - } - catch (OperationCanceledException ex) - { - Log.Information("PostAsync", "Task was cancelled."); - Log.Verbose("PostAsync", $"Connect cancelled. Info: {ex}"); - Log.Verbose("AbstractRestClient.PostAsync", "LEAVE"); - throw; - } - catch (Exception ex) - { - Log.Error("PostAsync", $"{ex.GetType()} thrown {ex.Message}"); - Log.Verbose("PostAsync", $"Excepton: {ex}"); - Log.Verbose("AbstractRestClient.PostAsync", "LEAVE"); - throw; - } - } - - public virtual async Task PostAsync(string uriSegment, S? parameter, R? content, CancellationTokenSource? cancellationToken = null, - Dictionary? addons = null, Dictionary? headers = null) - { - Log.Verbose("AbstractRestClient.PostAsync", "ENTER"); - Log.Debug("PostAsync", $"uriSegment: {uriSegment}"); - Log.Debug("PostAsync", $"addons: {addons}"); - - try - { - // if not defined, use default timeout - if (cancellationToken == null) - { - Log.Information("PostAsync", $"Using default timeout: {Constants.DefaultRESTTimeout}"); - cancellationToken = new CancellationTokenSource(); - cancellationToken.CancelAfter(Constants.DefaultRESTTimeout); - } - - // create request message and add custom query parameters - var request = new HttpRequestMessage(HttpMethod.Post, QueryParameterUtil.FormatURL(uriSegment, parameter, addons)); - if (typeof(R) == typeof(Stream)) - { - Stream? stream = content as Stream; - if (stream == null) - { - stream = new MemoryStream(); - } - request.Content = HttpRequestUtil.CreateStreamPayload(stream); - } - else - { - request.Content = HttpRequestUtil.CreatePayload(content); - } - - // add custom headers - if (headers != null) - { - foreach (var header in headers) - { - Log.Debug("PostAsync", $"Add Header {header.Key}={header.Value}"); - request.Headers.Add(header.Key, header.Value); - } - } - - // do the request - Log.Verbose("PostAsync", "Calling _httpClient.SendAsync..."); - var response = await _httpClient.SendAsync(request, cancellationToken.Token); - - var resultStr = response.Content.ReadAsStringAsync().Result; - if (!response.IsSuccessStatusCode) - { - await ThrowException("PostAsync", response, resultStr); - } - - Log.Verbose("PostAsync", $"Response:\n{resultStr}"); - var result = await HttpRequestUtil.DeserializeAsync(response); - - Log.Debug("PostAsync", $"Succeeded"); - Log.Verbose("AbstractRestClient.PostAsync", "LEAVE"); - - return result; - } - catch (OperationCanceledException ex) - { - Log.Information("PostAsync", "Task was cancelled."); - Log.Verbose("PostAsync", $"Connect cancelled. Info: {ex}"); - Log.Verbose("AbstractRestClient.PostAsync", "LEAVE"); - throw; - } - catch (Exception ex) - { - Log.Error("PostAsync", $"{ex.GetType()} thrown {ex.Message}"); - Log.Verbose("PostAsync", $"Excepton: {ex}"); - Log.Verbose("AbstractRestClient.PostAsync", "LEAVE"); - throw; - } - } - - /// - /// Patch method call that takes a body object - /// - /// Class type of what return type is expected - /// Uri for the api including the query parameters - /// Instance of T - public virtual async Task PatchAsync(string uriSegment, S? parameter, CancellationTokenSource? cancellationToken = null, - Dictionary? addons = null, Dictionary? headers = null) - { - Log.Verbose("AbstractRestClient.PatchAsync", "ENTER"); - Log.Debug("PatchAsync", $"uriSegment: {uriSegment}"); - Log.Debug("PatchAsync", $"addons: {addons}"); - - try - { - // if not defined, use default timeout - if (cancellationToken == null) - { - Log.Information("PatchAsync", $"Using default timeout: {Constants.DefaultRESTTimeout}"); - cancellationToken = new CancellationTokenSource(); - cancellationToken.CancelAfter(Constants.DefaultRESTTimeout); - } - - // create request message and add custom query parameters -#if NETSTANDARD2_0 - var request = new HttpRequestMessage(new HttpMethod("PATCH"), QueryParameterUtil.FormatURL(uriSegment, parameter, addons)) - { - Content = HttpRequestUtil.CreatePayload(parameter) - }; -#else - var request = new HttpRequestMessage(HttpMethod.Patch, QueryParameterUtil.FormatURL(uriSegment, parameter, addons)) - { - Content = HttpRequestUtil.CreatePayload(parameter) - }; -#endif - - // add custom headers - if (headers != null) - { - foreach (var header in headers) - { - Log.Debug("PatchAsync", $"Add Header {header.Key}={header.Value}"); - request.Headers.Add(header.Key, header.Value); - } - } - - // do the request - Log.Verbose("PatchAsync", "Calling _httpClient.SendAsync..."); - var response = await _httpClient.SendAsync(request, cancellationToken.Token); - - var resultStr = response.Content.ReadAsStringAsync().Result; - if (!response.IsSuccessStatusCode) - { - await ThrowException("PatchAsync", response, resultStr); - } - - Log.Verbose("PatchAsync", $"Response:\n{resultStr}"); - var result = await HttpRequestUtil.DeserializeAsync(response); - - Log.Debug("PatchAsync", $"Succeeded"); - Log.Verbose("AbstractRestClient.PatchAsync", "LEAVE"); - - return result; - - } - catch (OperationCanceledException ex) - { - Log.Information("PatchAsync", "Task was cancelled."); - Log.Verbose("PatchAsync", $"Connect cancelled. Info: {ex}"); - Log.Verbose("AbstractRestClient.PatchAsync", "LEAVE"); - throw; - } - catch (Exception ex) - { - Log.Error("PatchAsync", $"{ex.GetType()} thrown {ex.Message}"); - Log.Verbose("PatchAsync", $"Excepton: {ex}"); - Log.Verbose("AbstractRestClient.PatchAsync", "LEAVE"); - throw; - } - } - - /// - /// Put method call that takes a body object - /// - /// Class type of what return type is expected - /// Uri for the api - /// Instance of T - public virtual async Task PutAsync(string uriSegment, S? parameter, CancellationTokenSource? cancellationToken = null, - Dictionary? addons = null, Dictionary? headers = null) - { - Log.Verbose("AbstractRestClient.PutAsync", "ENTER"); - Log.Debug("PutAsync", $"uriSegment: {uriSegment}"); - Log.Debug("PutAsync", $"addons: {addons}"); - - try - { - // if not defined, use default timeout - if (cancellationToken == null) - { - Log.Information("PutAsync", $"Using default timeout: {Constants.DefaultRESTTimeout}"); - cancellationToken = new CancellationTokenSource(); - cancellationToken.CancelAfter(Constants.DefaultRESTTimeout); - } - - // create request message and add custom query parameters - var request = new HttpRequestMessage(HttpMethod.Put, QueryParameterUtil.FormatURL(uriSegment, parameter, addons)) - { - Content = HttpRequestUtil.CreatePayload(parameter) - }; - - // add custom headers - if (headers != null) - { - foreach (var header in headers) - { - var tmp = header.Key.ToLower(); - if ( !(tmp.Contains("password") || tmp.Contains("token") || tmp.Contains("authorization") || tmp.Contains("auth")) ) - { - Log.Debug("PutAsync", $"Add Header {header.Key}={header.Value}"); - } - request.Headers.Add(header.Key, header.Value); - } - } - - // do the request - Log.Verbose("PutAsync", "Calling _httpClient.SendAsync..."); - var response = await _httpClient.SendAsync(request, cancellationToken.Token); - - var resultStr = response.Content.ReadAsStringAsync().Result; - if (!response.IsSuccessStatusCode) - { - await ThrowException("PutAsync", response, resultStr); - } - - Log.Verbose("PutAsync", $"Response:\n{resultStr}"); - var result = await HttpRequestUtil.DeserializeAsync(response); - - Log.Debug("PutAsync", $"Succeeded"); - Log.Verbose("AbstractRestClient.PutAsync", "LEAVE"); - - return result; - } - catch (OperationCanceledException ex) - { - Log.Information("PutAsync", "Task was cancelled."); - Log.Verbose("PutAsync", $"Connect cancelled. Info: {ex}"); - Log.Verbose("AbstractRestClient.PutAsync", "LEAVE"); - throw; - } - catch (Exception ex) - { - Log.Error("PutAsync", $"{ex.GetType()} thrown {ex.Message}"); - Log.Verbose("PutAsync", $"Excepton: {ex}"); - Log.Verbose("AbstractRestClient.PutAsync", "LEAVE"); - throw; - } - } - - /// - /// Delete Method for use with calls that do not expect a response - /// - /// Uri for the api including the query parameters - public virtual async Task DeleteAsync(string uriSegment, CancellationTokenSource? cancellationToken = null, - Dictionary? addons = null, Dictionary? headers = null) - { - Log.Verbose("AbstractRestClient.DeleteAsync", "ENTER"); - Log.Debug("DeleteAsync", $"uriSegment: {uriSegment}"); - Log.Debug("DeleteAsync", $"addons: {addons}"); - - try - { - // if not defined, use default timeout - if (cancellationToken == null) - { - Log.Information("DeleteAsync", $"Using default timeout: {Constants.DefaultRESTTimeout}"); - cancellationToken = new CancellationTokenSource(); - cancellationToken.CancelAfter(Constants.DefaultRESTTimeout); - } - - // create request message and add custom query parameters - var request = new HttpRequestMessage(HttpMethod.Delete, QueryParameterUtil.FormatURL(uriSegment, new NoopSchema(), addons)); - - // add custom headers - if (headers != null) - { - foreach (var header in headers) - { - Log.Debug("DeleteAsync", $"Add Header {header.Key}={header.Value}"); - request.Headers.Add(header.Key, header.Value); - } - } - - // do the request - Log.Verbose("DeleteAsync", "Calling _httpClient.SendAsync..."); - var response = await _httpClient.SendAsync(request, cancellationToken.Token); - - var resultStr = response.Content.ReadAsStringAsync().Result; - if (!response.IsSuccessStatusCode) - { - await ThrowException("DeleteAsync", response, resultStr); - } - - Log.Verbose("DeleteAsync", $"Response:\n{resultStr}"); - var result = await HttpRequestUtil.DeserializeAsync(response); - - Log.Debug("DeleteAsync", $"Succeeded"); - Log.Verbose("AbstractRestClient.DeleteAsync", "LEAVE"); - - return result; - } - catch (OperationCanceledException ex) - { - Log.Information("DeleteAsync", "Task was cancelled."); - Log.Verbose("DeleteAsync", $"Connect cancelled. Info: {ex}"); - Log.Verbose("AbstractRestClient.DeleteAsync", "LEAVE"); - throw; - } - catch (Exception ex) - { - Log.Error("DeleteAsync", $"{ex.GetType()} thrown {ex.Message}"); - Log.Verbose("DeleteAsync", $"Excepton: {ex}"); - Log.Verbose("AbstractRestClient.DeleteAsync", "LEAVE"); - throw; - } - } - - /// - /// Delete method that returns the type of response specified - /// - /// Class Type of expected response - /// Uri for the api including the query parameters - /// Instance of T or throws Exception - public virtual async Task DeleteAsync(string uriSegment, S? parameter, CancellationTokenSource? cancellationToken = null, Dictionary? addons = null, - Dictionary? headers = null) - { - Log.Verbose("AbstractRestClient.DeleteAsync", "ENTER"); - Log.Debug("DeleteAsync", $"uriSegment: {uriSegment}"); - Log.Debug("DeleteAsync", $"addons: {addons}"); - - try - { - // if not defined, use default timeout - if (cancellationToken == null) - { - Log.Information("DeleteAsync", $"Using default timeout: {Constants.DefaultRESTTimeout}"); - cancellationToken = new CancellationTokenSource(); - cancellationToken.CancelAfter(Constants.DefaultRESTTimeout); - } - - // create request message and add custom query parameters - var request = new HttpRequestMessage(HttpMethod.Delete, QueryParameterUtil.FormatURL(uriSegment, parameter, addons)); - - // add custom headers - if (headers != null) - { - foreach (var header in headers) - { - Log.Debug("DeleteAsync", $"Add Header {header.Key}={header.Value}"); - request.Headers.Add(header.Key, header.Value); - } - } - - // do the request - Log.Verbose("DeleteAsync", "Calling _httpClient.SendAsync..."); - var response = await _httpClient.SendAsync(request, cancellationToken.Token); - - var resultStr = response.Content.ReadAsStringAsync().Result; - if (!response.IsSuccessStatusCode) - { - await ThrowException("DeleteAsync", response, resultStr); - } - - Log.Verbose("DeleteAsync", $"Response:\n{resultStr}"); - var result = await HttpRequestUtil.DeserializeAsync(response); - - Log.Debug("DeleteAsync", $"Succeeded"); - Log.Verbose("AbstractRestClient.DeleteAsync", "LEAVE"); - - return result; - } - catch (OperationCanceledException ex) - { - Log.Information("DeleteAsync", "Task was cancelled."); - Log.Verbose("DeleteAsync", $"Connect cancelled. Info: {ex}"); - Log.Verbose("AbstractRestClient.DeleteAsync", "LEAVE"); - throw; - } - catch (Exception ex) - { - Log.Error("DeleteAsync", $"{ex.GetType()} thrown {ex.Message}"); - Log.Verbose("DeleteAsync", $"Excepton: {ex}"); - Log.Verbose("AbstractRestClient.DeleteAsync", "LEAVE"); - throw; - } - } - - private static async Task ThrowException(string module, HttpResponseMessage response, string errMsg) - { - if (errMsg == null || errMsg.Length == 0) - { - Log.Verbose(module, $"HTTP/REST Exception thrown"); - response.EnsureSuccessStatusCode(); // this throws the exception - } - - Log.Verbose(module, $"Deepgram Exception: {errMsg}"); - DeepgramRESTException? resException = null; - try - { - resException = await HttpRequestUtil.DeserializeAsync(response); - } - catch (Exception ex) - { - Log.Verbose(module, $"DeserializeAsync Error Exception: {ex}"); - } - - if (resException != null) - { - Log.Verbose(module, "DeepgramRESTException thrown"); - throw resException; - } - - Log.Verbose(module, $"Deepgram Generic Exception thrown"); - throw new DeepgramException(errMsg); - } - - internal static string GetUri(IDeepgramClientOptions options, string path) - { - return $"{options.BaseAddress}/{path}"; - } -} - +// Copyright 2021-2024 Deepgram .NET 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 + +using Deepgram.Encapsulations; +using Deepgram.Models.Authenticate.v1; +using Deepgram.Models.Exceptions.v1; + +namespace Deepgram.Abstractions.v1; + +public abstract class AbstractRestClient +{ + /// + /// HttpClient created by the factory + internal HttpClient _httpClient; + + /// + /// Copy of the options for the client + /// + internal IDeepgramClientOptions _options; + + /// + /// Constructor that take the options and a httpClient + /// + /// Options for the Deepgram client + + internal AbstractRestClient(string? apiKey = null, IDeepgramClientOptions? options = null, string? httpId = null) + { + Log.Verbose("AbstractRestClient", "ENTER"); + + if (options == null) + { + options = new DeepgramHttpClientOptions(apiKey); + } + _httpClient = HttpClientFactory.Create(httpId); + _httpClient = HttpClientFactory.ConfigureDeepgram(_httpClient, options); + _options = options; + + Log.Debug("AbstractRestClient", $"APIVersion: {options.APIVersion}"); + Log.Debug("AbstractRestClient", $"BaseAddress: {options.BaseAddress}"); + Log.Debug("AbstractRestClient", $"options: {options.OnPrem}"); + Log.Verbose("AbstractRestClient", "LEAVE"); + } + + /// + /// GET Rest Request + /// + /// Type of class of response expected + /// request uri Endpoint + /// Instance of T + public virtual async Task GetAsync(string uriSegment, CancellationTokenSource? cancellationToken = null, + Dictionary? addons = null, Dictionary? headers = null) + { + Log.Verbose("AbstractRestClient.GetAsync", "ENTER"); + Log.Debug("GetAsync", $"uriSegment: {uriSegment}"); + Log.Debug("GetAsync", $"addons: {addons}"); + + try + { + // if not defined, use default timeout + if (cancellationToken == null) + { + Log.Information("GetAsync", $"Using default timeout: {Constants.DefaultRESTTimeout}"); + cancellationToken = new CancellationTokenSource(); + cancellationToken.CancelAfter(Constants.DefaultRESTTimeout); + } + + // create request message and add custom query parameters + NoopSchema? parameter = null; + var request = new HttpRequestMessage(HttpMethod.Get, QueryParameterUtil.FormatURL(uriSegment, parameter, addons)); + + // add custom headers + if (headers != null) + { + foreach (var header in headers) + { + Log.Debug("GetAsync", $"Add Header {header.Key}={header.Value}"); + request.Headers.Add(header.Key, header.Value); + } + } + + // do the request + Log.Verbose("GetAsync", "Calling _httpClient.SendAsync..."); + var response = await _httpClient.SendAsync(request, cancellationToken.Token); + + var resultStr = response.Content.ReadAsStringAsync().Result; + if (!response.IsSuccessStatusCode) + { + await ThrowException("GetAsync", response, resultStr); + } + + Log.Verbose("GetAsync", $"Response:\n{resultStr}"); + var result = await HttpRequestUtil.DeserializeAsync(response); + + Log.Debug("GetAsync", "Succeeded"); + Log.Verbose("AbstractRestClient.GetAsync", "LEAVE"); + return result; + } + catch (OperationCanceledException ex) + { + Log.Information("GetAsync", "Task was cancelled."); + Log.Verbose("GetAsync", $"Connect cancelled. Info: {ex}"); + Log.Verbose("AbstractRestClient.GetAsync", "LEAVE"); + throw; + } + catch (Exception ex) + { + Log.Error("GetAsync", $"{ex.GetType()} thrown {ex.Message}"); + Log.Verbose("GetAsync", $"Excepton: {ex}"); + Log.Verbose("AbstractRestClient.GetAsync", "LEAVE"); + throw; + } + } + + public virtual async Task GetAsync(string uriSegment, S? parameter, CancellationTokenSource? cancellationToken = null, + Dictionary? addons = null, Dictionary? headers = null) + { + Log.Verbose("AbstractRestClient.GetAsync", "ENTER"); + Log.Debug("GetAsync", $"uriSegment: {uriSegment}"); + Log.Debug("GetAsync", $"addons: {addons}"); + + try + { + // if not defined, use default timeout + if (cancellationToken == null) + { + Log.Information("GetAsync", $"Using default timeout: {Constants.DefaultRESTTimeout}"); + cancellationToken = new CancellationTokenSource(); + cancellationToken.CancelAfter(Constants.DefaultRESTTimeout); + } + + // create request message and add custom query parameters + var request = new HttpRequestMessage(HttpMethod.Get, QueryParameterUtil.FormatURL(uriSegment, parameter, addons)); + + // add custom headers + if (headers != null) + { + foreach (var header in headers) + { + Log.Debug("GetAsync", $"Add Header {header.Key}={header.Value}"); + request.Headers.Add(header.Key, header.Value); + } + } + + // do the request + Log.Verbose("GetAsync", "Calling _httpClient.SendAsync..."); + var response = await _httpClient.SendAsync(request, cancellationToken.Token); + + var resultStr = response.Content.ReadAsStringAsync().Result; + if (!response.IsSuccessStatusCode) + { + await ThrowException("GetAsync", response, resultStr); + } + + Log.Verbose("GetAsync", $"Response:\n{resultStr}"); + var result = await HttpRequestUtil.DeserializeAsync(response); + + Log.Debug("GetAsync", "Succeeded"); + Log.Verbose("AbstractRestClient.GetAsync", "LEAVE"); + + return result; + } + catch (OperationCanceledException ex) + { + Log.Information("GetAsync", "Task was cancelled."); + Log.Verbose("GetAsync", $"Connect cancelled. Info: {ex}"); + Log.Verbose("AbstractRestClient.GetAsync", "LEAVE"); + throw; + } + catch (Exception ex) + { + Log.Error("GetAsync", $"{ex.GetType()} thrown {ex.Message}"); + Log.Verbose("GetAsync", $"Excepton: {ex}"); + Log.Verbose("AbstractRestClient.GetAsync", "LEAVE"); + throw; + } + } + + /// + /// Post method + /// + /// Class type of what return type is expected + /// Uri for the api including the query parameters + /// HttpContent as content for HttpRequestMessage + /// Instance of T + public virtual async Task PostRetrieveLocalFileAsync(string uriSegment, S? parameter, R? content, + List? keys = null, CancellationTokenSource? cancellationToken = null, Dictionary? addons = null, + Dictionary? headers = null + ) + { + Log.Verbose("AbstractRestClient.PostRetrieveLocalFileAsync", "ENTER"); + Log.Debug("PostRetrieveLocalFileAsync", $"uriSegment: {uriSegment}"); + Log.Debug("PostRetrieveLocalFileAsync", $"keys: {keys}"); + Log.Debug("PostRetrieveLocalFileAsync", $"addons: {addons}"); + + try + { + // if not defined, use default timeout + if (cancellationToken == null) + { + Log.Information("PostRetrieveLocalFileAsync", $"Using default timeout: {Constants.DefaultRESTTimeout}"); + cancellationToken = new CancellationTokenSource(); + cancellationToken.CancelAfter(Constants.DefaultRESTTimeout); + } + + // create request message and add custom query parameters + var request = new HttpRequestMessage(HttpMethod.Post, QueryParameterUtil.FormatURL(uriSegment, parameter, addons)) + { + Content = HttpRequestUtil.CreatePayload(content) + }; + + // add custom headers + if (headers != null) + { + foreach (var header in headers) + { + Log.Debug("PostRetrieveLocalFileAsync", $"Add Header {header.Key}={header.Value}"); + request.Headers.Add(header.Key, header.Value); + } + } + + // do the request + Log.Verbose("PostRetrieveLocalFileAsync", "Calling _httpClient.SendAsync..."); + var response = await _httpClient.SendAsync(request, cancellationToken.Token); + + if (!response.IsSuccessStatusCode) + { + await ThrowException("PostRetrieveLocalFileAsync", response, response.Content.ReadAsStringAsync().Result); + } + + var result = new Dictionary(); + + if (keys != null) + { + for (var i = 0; i < response.Headers.Count(); i++) + { + var key = response.Headers.ElementAt(i).Key.ToLower(); + var value = response.Headers.GetValues(key).FirstOrDefault() ?? ""; + + var index = key.IndexOf("x-dg-"); + if (index == 0) + { + var newKey = key.Substring(5); + if (keys.Contains(newKey)) + { + result.Add(newKey, value); + continue; + } + } + index = key.IndexOf("dg-"); + if (index == 0) + { + var newKey = key.Substring(3); + if (keys.Contains(newKey)) + { + result.Add(newKey, value); + continue; + } + } + if (keys.Contains(key)) + { + result.Add(key, value); + } + } + + if (keys.Contains("content-type")) + { + result.Add("content-type", response.Content.Headers.ContentType?.MediaType ?? ""); + } + } + + var stream = new MemoryStream(); + await response.Content.CopyToAsync(stream); + + Log.Verbose("PostRetrieveLocalFileAsync", "Succeeded"); + Log.Verbose("AbstractRestClient.PostRetrieveLocalFileAsync", "LEAVE"); + + return new LocalFileWithMetadata() + { + Metadata = result, + Content = stream, + }; + + } + catch (OperationCanceledException ex) + { + Log.Information("PostRetrieveLocalFileAsync", "Task was cancelled."); + Log.Verbose("PostRetrieveLocalFileAsync", $"Connect cancelled. Info: {ex}"); + Log.Verbose("AbstractRestClient.PostRetrieveLocalFileAsync", "LEAVE"); + throw; + } + catch (Exception ex) + { + Log.Error("PostRetrieveLocalFileAsync", $"{ex.GetType()} thrown {ex.Message}"); + Log.Verbose("PostRetrieveLocalFileAsync", $"Excepton: {ex}"); + Log.Verbose("AbstractRestClient.PostRetrieveLocalFileAsync", "LEAVE"); + throw; + } + } + + /// + /// Post method + /// + /// Class type of what return type is expected + /// Uri for the api including the query parameters + /// HttpContent as content for HttpRequestMessage + /// Instance of T + public virtual async Task PostAsync(string uriSegment, S? parameter, CancellationTokenSource? cancellationToken = null, + Dictionary? addons = null, Dictionary? headers = null) + { + Log.Verbose("AbstractRestClient.PostAsync", "ENTER"); + Log.Debug("PostAsync", $"uriSegment: {uriSegment}"); + Log.Debug("PostAsync", $"addons: {addons}"); + + try + { + // if not defined, use default timeout + if (cancellationToken == null) + { + Log.Information("PostAsync", $"Using default timeout: {Constants.DefaultRESTTimeout}"); + cancellationToken = new CancellationTokenSource(); + cancellationToken.CancelAfter(Constants.DefaultRESTTimeout); + } + + // create request message and add custom query parameters + var request = new HttpRequestMessage(HttpMethod.Post, QueryParameterUtil.FormatURL(uriSegment, parameter, addons)) + { + Content = HttpRequestUtil.CreatePayload(parameter) + }; + + // add custom headers + if (headers != null) + { + foreach (var header in headers) + { + Log.Debug("PostAsync", $"Add Header {header.Key}={header.Value}"); + request.Headers.Add(header.Key, header.Value); + } + } + + // do the request + Log.Verbose("PostAsync", "Calling _httpClient.SendAsync..."); + var response = await _httpClient.SendAsync(request, cancellationToken.Token); + + var resultStr = response.Content.ReadAsStringAsync().Result; + if (!response.IsSuccessStatusCode) + { + await ThrowException("PostAsync", response, resultStr); + } + + Log.Verbose("PostAsync", $"Response:\n{resultStr}"); + var result = await HttpRequestUtil.DeserializeAsync(response); + + Log.Debug("PostAsync", $"Succeeded"); + Log.Verbose("AbstractRestClient.PostAsync", "LEAVE"); + + return result; + } + catch (OperationCanceledException ex) + { + Log.Information("PostAsync", "Task was cancelled."); + Log.Verbose("PostAsync", $"Connect cancelled. Info: {ex}"); + Log.Verbose("AbstractRestClient.PostAsync", "LEAVE"); + throw; + } + catch (Exception ex) + { + Log.Error("PostAsync", $"{ex.GetType()} thrown {ex.Message}"); + Log.Verbose("PostAsync", $"Excepton: {ex}"); + Log.Verbose("AbstractRestClient.PostAsync", "LEAVE"); + throw; + } + } + + public virtual async Task PostAsync(string uriSegment, S? parameter, R? content, CancellationTokenSource? cancellationToken = null, + Dictionary? addons = null, Dictionary? headers = null) + { + Log.Verbose("AbstractRestClient.PostAsync", "ENTER"); + Log.Debug("PostAsync", $"uriSegment: {uriSegment}"); + Log.Debug("PostAsync", $"addons: {addons}"); + + try + { + // if not defined, use default timeout + if (cancellationToken == null) + { + Log.Information("PostAsync", $"Using default timeout: {Constants.DefaultRESTTimeout}"); + cancellationToken = new CancellationTokenSource(); + cancellationToken.CancelAfter(Constants.DefaultRESTTimeout); + } + + // create request message and add custom query parameters + var request = new HttpRequestMessage(HttpMethod.Post, QueryParameterUtil.FormatURL(uriSegment, parameter, addons)); + if (typeof(R) == typeof(Stream)) + { + var stream = content as Stream; + if (stream == null) + { + stream = new MemoryStream(); + } + request.Content = HttpRequestUtil.CreateStreamPayload(stream); + } + else + { + request.Content = HttpRequestUtil.CreatePayload(content); + } + + // add custom headers + if (headers != null) + { + foreach (var header in headers) + { + Log.Debug("PostAsync", $"Add Header {header.Key}={header.Value}"); + request.Headers.Add(header.Key, header.Value); + } + } + + // do the request + Log.Verbose("PostAsync", "Calling _httpClient.SendAsync..."); + var response = await _httpClient.SendAsync(request, cancellationToken.Token); + + var resultStr = response.Content.ReadAsStringAsync().Result; + if (!response.IsSuccessStatusCode) + { + await ThrowException("PostAsync", response, resultStr); + } + + Log.Verbose("PostAsync", $"Response:\n{resultStr}"); + var result = await HttpRequestUtil.DeserializeAsync(response); + + Log.Debug("PostAsync", $"Succeeded"); + Log.Verbose("AbstractRestClient.PostAsync", "LEAVE"); + + return result; + } + catch (OperationCanceledException ex) + { + Log.Information("PostAsync", "Task was cancelled."); + Log.Verbose("PostAsync", $"Connect cancelled. Info: {ex}"); + Log.Verbose("AbstractRestClient.PostAsync", "LEAVE"); + throw; + } + catch (Exception ex) + { + Log.Error("PostAsync", $"{ex.GetType()} thrown {ex.Message}"); + Log.Verbose("PostAsync", $"Excepton: {ex}"); + Log.Verbose("AbstractRestClient.PostAsync", "LEAVE"); + throw; + } + } + + /// + /// Patch method call that takes a body object + /// + /// Class type of what return type is expected + /// Uri for the api including the query parameters + /// Instance of T + public virtual async Task PatchAsync(string uriSegment, S? parameter, CancellationTokenSource? cancellationToken = null, + Dictionary? addons = null, Dictionary? headers = null) + { + Log.Verbose("AbstractRestClient.PatchAsync", "ENTER"); + Log.Debug("PatchAsync", $"uriSegment: {uriSegment}"); + Log.Debug("PatchAsync", $"addons: {addons}"); + + try + { + // if not defined, use default timeout + if (cancellationToken == null) + { + Log.Information("PatchAsync", $"Using default timeout: {Constants.DefaultRESTTimeout}"); + cancellationToken = new CancellationTokenSource(); + cancellationToken.CancelAfter(Constants.DefaultRESTTimeout); + } + + // create request message and add custom query parameters +#if NETSTANDARD2_0 + var request = new HttpRequestMessage(new HttpMethod("PATCH"), QueryParameterUtil.FormatURL(uriSegment, parameter, addons)) + { + Content = HttpRequestUtil.CreatePayload(parameter) + }; +#else + var request = new HttpRequestMessage(HttpMethod.Patch, QueryParameterUtil.FormatURL(uriSegment, parameter, addons)) + { + Content = HttpRequestUtil.CreatePayload(parameter) + }; +#endif + + // add custom headers + if (headers != null) + { + foreach (var header in headers) + { + Log.Debug("PatchAsync", $"Add Header {header.Key}={header.Value}"); + request.Headers.Add(header.Key, header.Value); + } + } + + // do the request + Log.Verbose("PatchAsync", "Calling _httpClient.SendAsync..."); + var response = await _httpClient.SendAsync(request, cancellationToken.Token); + + var resultStr = response.Content.ReadAsStringAsync().Result; + if (!response.IsSuccessStatusCode) + { + await ThrowException("PatchAsync", response, resultStr); + } + + Log.Verbose("PatchAsync", $"Response:\n{resultStr}"); + var result = await HttpRequestUtil.DeserializeAsync(response); + + Log.Debug("PatchAsync", $"Succeeded"); + Log.Verbose("AbstractRestClient.PatchAsync", "LEAVE"); + + return result; + + } + catch (OperationCanceledException ex) + { + Log.Information("PatchAsync", "Task was cancelled."); + Log.Verbose("PatchAsync", $"Connect cancelled. Info: {ex}"); + Log.Verbose("AbstractRestClient.PatchAsync", "LEAVE"); + throw; + } + catch (Exception ex) + { + Log.Error("PatchAsync", $"{ex.GetType()} thrown {ex.Message}"); + Log.Verbose("PatchAsync", $"Excepton: {ex}"); + Log.Verbose("AbstractRestClient.PatchAsync", "LEAVE"); + throw; + } + } + + /// + /// Put method call that takes a body object + /// + /// Class type of what return type is expected + /// Uri for the api + /// Instance of T + public virtual async Task PutAsync(string uriSegment, S? parameter, CancellationTokenSource? cancellationToken = null, + Dictionary? addons = null, Dictionary? headers = null) + { + Log.Verbose("AbstractRestClient.PutAsync", "ENTER"); + Log.Debug("PutAsync", $"uriSegment: {uriSegment}"); + Log.Debug("PutAsync", $"addons: {addons}"); + + try + { + // if not defined, use default timeout + if (cancellationToken == null) + { + Log.Information("PutAsync", $"Using default timeout: {Constants.DefaultRESTTimeout}"); + cancellationToken = new CancellationTokenSource(); + cancellationToken.CancelAfter(Constants.DefaultRESTTimeout); + } + + // create request message and add custom query parameters + var request = new HttpRequestMessage(HttpMethod.Put, QueryParameterUtil.FormatURL(uriSegment, parameter, addons)) + { + Content = HttpRequestUtil.CreatePayload(parameter) + }; + + // add custom headers + if (headers != null) + { + foreach (var header in headers) + { + var tmp = header.Key.ToLower(); + if (!(tmp.Contains("password") || tmp.Contains("token") || tmp.Contains("authorization") || tmp.Contains("auth"))) + { + Log.Debug("PutAsync", $"Add Header {header.Key}={header.Value}"); + } + request.Headers.Add(header.Key, header.Value); + } + } + + // do the request + Log.Verbose("PutAsync", "Calling _httpClient.SendAsync..."); + var response = await _httpClient.SendAsync(request, cancellationToken.Token); + + var resultStr = response.Content.ReadAsStringAsync().Result; + if (!response.IsSuccessStatusCode) + { + await ThrowException("PutAsync", response, resultStr); + } + + Log.Verbose("PutAsync", $"Response:\n{resultStr}"); + var result = await HttpRequestUtil.DeserializeAsync(response); + + Log.Debug("PutAsync", $"Succeeded"); + Log.Verbose("AbstractRestClient.PutAsync", "LEAVE"); + + return result; + } + catch (OperationCanceledException ex) + { + Log.Information("PutAsync", "Task was cancelled."); + Log.Verbose("PutAsync", $"Connect cancelled. Info: {ex}"); + Log.Verbose("AbstractRestClient.PutAsync", "LEAVE"); + throw; + } + catch (Exception ex) + { + Log.Error("PutAsync", $"{ex.GetType()} thrown {ex.Message}"); + Log.Verbose("PutAsync", $"Excepton: {ex}"); + Log.Verbose("AbstractRestClient.PutAsync", "LEAVE"); + throw; + } + } + + /// + /// Delete Method for use with calls that do not expect a response + /// + /// Uri for the api including the query parameters + public virtual async Task DeleteAsync(string uriSegment, CancellationTokenSource? cancellationToken = null, + Dictionary? addons = null, Dictionary? headers = null) + { + Log.Verbose("AbstractRestClient.DeleteAsync", "ENTER"); + Log.Debug("DeleteAsync", $"uriSegment: {uriSegment}"); + Log.Debug("DeleteAsync", $"addons: {addons}"); + + try + { + // if not defined, use default timeout + if (cancellationToken == null) + { + Log.Information("DeleteAsync", $"Using default timeout: {Constants.DefaultRESTTimeout}"); + cancellationToken = new CancellationTokenSource(); + cancellationToken.CancelAfter(Constants.DefaultRESTTimeout); + } + + // create request message and add custom query parameters + var request = new HttpRequestMessage(HttpMethod.Delete, QueryParameterUtil.FormatURL(uriSegment, new NoopSchema(), addons)); + + // add custom headers + if (headers != null) + { + foreach (var header in headers) + { + Log.Debug("DeleteAsync", $"Add Header {header.Key}={header.Value}"); + request.Headers.Add(header.Key, header.Value); + } + } + + // do the request + Log.Verbose("DeleteAsync", "Calling _httpClient.SendAsync..."); + var response = await _httpClient.SendAsync(request, cancellationToken.Token); + + var resultStr = response.Content.ReadAsStringAsync().Result; + if (!response.IsSuccessStatusCode) + { + await ThrowException("DeleteAsync", response, resultStr); + } + + Log.Verbose("DeleteAsync", $"Response:\n{resultStr}"); + var result = await HttpRequestUtil.DeserializeAsync(response); + + Log.Debug("DeleteAsync", $"Succeeded"); + Log.Verbose("AbstractRestClient.DeleteAsync", "LEAVE"); + + return result; + } + catch (OperationCanceledException ex) + { + Log.Information("DeleteAsync", "Task was cancelled."); + Log.Verbose("DeleteAsync", $"Connect cancelled. Info: {ex}"); + Log.Verbose("AbstractRestClient.DeleteAsync", "LEAVE"); + throw; + } + catch (Exception ex) + { + Log.Error("DeleteAsync", $"{ex.GetType()} thrown {ex.Message}"); + Log.Verbose("DeleteAsync", $"Excepton: {ex}"); + Log.Verbose("AbstractRestClient.DeleteAsync", "LEAVE"); + throw; + } + } + + /// + /// Delete method that returns the type of response specified + /// + /// Class Type of expected response + /// Uri for the api including the query parameters + /// Instance of T or throws Exception + public virtual async Task DeleteAsync(string uriSegment, S? parameter, CancellationTokenSource? cancellationToken = null, Dictionary? addons = null, + Dictionary? headers = null) + { + Log.Verbose("AbstractRestClient.DeleteAsync", "ENTER"); + Log.Debug("DeleteAsync", $"uriSegment: {uriSegment}"); + Log.Debug("DeleteAsync", $"addons: {addons}"); + + try + { + // if not defined, use default timeout + if (cancellationToken == null) + { + Log.Information("DeleteAsync", $"Using default timeout: {Constants.DefaultRESTTimeout}"); + cancellationToken = new CancellationTokenSource(); + cancellationToken.CancelAfter(Constants.DefaultRESTTimeout); + } + + // create request message and add custom query parameters + var request = new HttpRequestMessage(HttpMethod.Delete, QueryParameterUtil.FormatURL(uriSegment, parameter, addons)); + + // add custom headers + if (headers != null) + { + foreach (var header in headers) + { + Log.Debug("DeleteAsync", $"Add Header {header.Key}={header.Value}"); + request.Headers.Add(header.Key, header.Value); + } + } + + // do the request + Log.Verbose("DeleteAsync", "Calling _httpClient.SendAsync..."); + var response = await _httpClient.SendAsync(request, cancellationToken.Token); + + var resultStr = response.Content.ReadAsStringAsync().Result; + if (!response.IsSuccessStatusCode) + { + await ThrowException("DeleteAsync", response, resultStr); + } + + Log.Verbose("DeleteAsync", $"Response:\n{resultStr}"); + var result = await HttpRequestUtil.DeserializeAsync(response); + + Log.Debug("DeleteAsync", $"Succeeded"); + Log.Verbose("AbstractRestClient.DeleteAsync", "LEAVE"); + + return result; + } + catch (OperationCanceledException ex) + { + Log.Information("DeleteAsync", "Task was cancelled."); + Log.Verbose("DeleteAsync", $"Connect cancelled. Info: {ex}"); + Log.Verbose("AbstractRestClient.DeleteAsync", "LEAVE"); + throw; + } + catch (Exception ex) + { + Log.Error("DeleteAsync", $"{ex.GetType()} thrown {ex.Message}"); + Log.Verbose("DeleteAsync", $"Excepton: {ex}"); + Log.Verbose("AbstractRestClient.DeleteAsync", "LEAVE"); + throw; + } + } + + private static async Task ThrowException(string module, HttpResponseMessage response, string errMsg) + { + if (errMsg == null || errMsg.Length == 0) + { + Log.Verbose(module, $"HTTP/REST Exception thrown"); + response.EnsureSuccessStatusCode(); // this throws the exception + } + + Log.Verbose(module, $"Deepgram Exception: {errMsg}"); + DeepgramRESTException? resException = null; + try + { + resException = await HttpRequestUtil.DeserializeAsync(response); + } + catch (Exception ex) + { + Log.Verbose(module, $"DeserializeAsync Error Exception: {ex}"); + } + + if (resException != null) + { + Log.Verbose(module, "DeepgramRESTException thrown"); + throw resException; + } + + Log.Verbose(module, $"Deepgram Generic Exception thrown"); + throw new DeepgramException(errMsg); + } + + internal static string GetUri(IDeepgramClientOptions options, string path) + { + return $"{options.BaseAddress}/{path}"; + } +} + diff --git a/Deepgram/Abstractions/Constants.cs b/Deepgram/Abstractions/v1/Constants.cs similarity index 92% rename from Deepgram/Abstractions/Constants.cs rename to Deepgram/Abstractions/v1/Constants.cs index 97fa3ebd..9dec95fb 100644 --- a/Deepgram/Abstractions/Constants.cs +++ b/Deepgram/Abstractions/v1/Constants.cs @@ -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 -namespace Deepgram.Abstractions; +namespace Deepgram.Abstractions.v1; /// /// Defaults for the REST Client diff --git a/Deepgram/Abstractions/v1/LocalFileWithMetadata.cs b/Deepgram/Abstractions/v1/LocalFileWithMetadata.cs new file mode 100644 index 00000000..ee1f1830 --- /dev/null +++ b/Deepgram/Abstractions/v1/LocalFileWithMetadata.cs @@ -0,0 +1,33 @@ +// Copyright 2024 Deepgram .NET 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 + +namespace Deepgram.Abstractions.v1; + +/// +/// LocalFileWithMetadata is a class that represents a file with metadata. +/// +public class LocalFileWithMetadata +{ + /// + /// Gets or sets the metadata associated with the file content. + /// + public Dictionary Metadata { get; set; } + + /// + /// Gets or sets the file content as a MemoryStream. + /// The caller is responsible for disposing of this stream when no longer needed. + /// + /// + /// This property should be properly disposed of to prevent memory leaks. + /// + public MemoryStream Content { get; set; } + + /// + /// Releases the resources used by the Content stream. + /// + public void Dispose() + { + Content?.Dispose(); + } +} diff --git a/Deepgram/Abstractions/NoopSchema.cs b/Deepgram/Abstractions/v1/NoopSchema.cs similarity index 90% rename from Deepgram/Abstractions/NoopSchema.cs rename to Deepgram/Abstractions/v1/NoopSchema.cs index 4af49920..1ff0ab67 100644 --- a/Deepgram/Abstractions/NoopSchema.cs +++ b/Deepgram/Abstractions/v1/NoopSchema.cs @@ -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 -namespace Deepgram.Abstractions; +namespace Deepgram.Abstractions.v1; /// /// Just a NoopSchema where you dont need to marshall JSON into a Async function. diff --git a/Deepgram/Abstractions/Utilities.cs b/Deepgram/Abstractions/v1/Utilities.cs similarity index 98% rename from Deepgram/Abstractions/Utilities.cs rename to Deepgram/Abstractions/v1/Utilities.cs index 1a4e8b38..b6b1e4d3 100644 --- a/Deepgram/Abstractions/Utilities.cs +++ b/Deepgram/Abstractions/v1/Utilities.cs @@ -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 -namespace Deepgram.Abstractions; +namespace Deepgram.Abstractions.v1; /// /// Helper funcitons for HttpRequests diff --git a/Deepgram/Abstractions/v2/AbstractRestClient.cs b/Deepgram/Abstractions/v2/AbstractRestClient.cs new file mode 100644 index 00000000..0e165d18 --- /dev/null +++ b/Deepgram/Abstractions/v2/AbstractRestClient.cs @@ -0,0 +1,783 @@ +// Copyright 2021-2024 Deepgram .NET 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 + +using Deepgram.Encapsulations; +using Deepgram.Models.Authenticate.v1; +using Deepgram.Models.Exceptions.v1; + +namespace Deepgram.Abstractions.v2; + +public abstract class AbstractRestClient +{ + /// + /// HttpClient created by the factory + internal HttpClient _httpClient; + + /// + /// Copy of the options for the client + /// + internal IDeepgramClientOptions _options; + + /// + /// Constructor that take the options and a httpClient + /// + /// Options for the Deepgram client + + internal AbstractRestClient(string? apiKey = null, IDeepgramClientOptions? options = null, string? httpId = null) + { + Log.Verbose("AbstractRestClient", "ENTER"); + + if (options == null) + { + options = (IDeepgramClientOptions) new DeepgramHttpClientOptions(apiKey); + } + _httpClient = HttpClientFactory.Create(httpId); + _httpClient = HttpClientFactory.ConfigureDeepgram(_httpClient, options); + _options = options; + + Log.Debug("AbstractRestClient", $"APIVersion: {options.APIVersion}"); + Log.Debug("AbstractRestClient", $"BaseAddress: {options.BaseAddress}"); + Log.Debug("AbstractRestClient", $"options: {options.OnPrem}"); + Log.Verbose("AbstractRestClient", "LEAVE"); + } + + /// + /// GET Rest Request + /// + /// Type of class of response expected + /// request uri Endpoint + /// Instance of T + public virtual async Task GetAsync(string uriSegment, CancellationTokenSource? cancellationToken = null, + Dictionary? addons = null, Dictionary? headers = null) + { + Log.Verbose("AbstractRestClient.GetAsync", "ENTER"); + Log.Debug("GetAsync", $"uriSegment: {uriSegment}"); + Log.Debug("GetAsync", $"addons: {addons}"); + + try + { + // if not defined, use default timeout + if (cancellationToken == null) + { + Log.Information("GetAsync", $"Using default timeout: {Constants.DefaultRESTTimeout}"); + cancellationToken = new CancellationTokenSource(); + cancellationToken.CancelAfter(Constants.DefaultRESTTimeout); + } + + // create request message and add custom query parameters + NoopSchema? parameter = null; + var request = new HttpRequestMessage(HttpMethod.Get, QueryParameterUtil.FormatURL(uriSegment, parameter, addons)); + + // add custom headers + if (headers != null) + { + foreach (var header in headers) + { + Log.Debug("GetAsync", $"Add Header {header.Key}={header.Value}"); + request.Headers.Add(header.Key, header.Value); + } + } + + // do the request + Log.Verbose("GetAsync", "Calling _httpClient.SendAsync..."); + var response = await _httpClient.SendAsync(request, cancellationToken.Token); + + var resultStr = await response.Content.ReadAsStringAsync(); + if (!response.IsSuccessStatusCode) + { + await ThrowException("GetAsync", response, resultStr); + } + + Log.Verbose("GetAsync", $"Response:\n{resultStr}"); + var result = await HttpRequestUtil.DeserializeAsync(response); + + Log.Debug("GetAsync", "Succeeded"); + Log.Verbose("AbstractRestClient.GetAsync", "LEAVE"); + return result; + } + catch (OperationCanceledException ex) + { + Log.Information("GetAsync", "Task was cancelled."); + Log.Verbose("GetAsync", $"Connect cancelled. Info: {ex}"); + Log.Verbose("AbstractRestClient.GetAsync", "LEAVE"); + throw; + } + catch (Exception ex) + { + Log.Error("GetAsync", $"{ex.GetType()} thrown {ex.Message}"); + Log.Verbose("GetAsync", $"Excepton: {ex}"); + Log.Verbose("AbstractRestClient.GetAsync", "LEAVE"); + throw; + } + } + + public virtual async Task GetAsync(string uriSegment, S? parameter, CancellationTokenSource? cancellationToken = null, + Dictionary? addons = null, Dictionary? headers = null) + { + Log.Verbose("AbstractRestClient.GetAsync", "ENTER"); + Log.Debug("GetAsync", $"uriSegment: {uriSegment}"); + Log.Debug("GetAsync", $"addons: {addons}"); + + try + { + // if not defined, use default timeout + if (cancellationToken == null) + { + Log.Information("GetAsync", $"Using default timeout: {Constants.DefaultRESTTimeout}"); + cancellationToken = new CancellationTokenSource(); + cancellationToken.CancelAfter(Constants.DefaultRESTTimeout); + } + + // create request message and add custom query parameters + var request = new HttpRequestMessage(HttpMethod.Get, QueryParameterUtil.FormatURL(uriSegment, parameter, addons)); + + // add custom headers + if (headers != null) + { + foreach (var header in headers) + { + Log.Debug("GetAsync", $"Add Header {header.Key}={header.Value}"); + request.Headers.Add(header.Key, header.Value); + } + } + + // do the request + Log.Verbose("GetAsync", "Calling _httpClient.SendAsync..."); + var response = await _httpClient.SendAsync(request, cancellationToken.Token); + + var resultStr = await response.Content.ReadAsStringAsync(); + if (!response.IsSuccessStatusCode) + { + await ThrowException("GetAsync", response, resultStr); + } + + Log.Verbose("GetAsync", $"Response:\n{resultStr}"); + var result = await HttpRequestUtil.DeserializeAsync(response); + + Log.Debug("GetAsync", "Succeeded"); + Log.Verbose("AbstractRestClient.GetAsync", "LEAVE"); + + return result; + } + catch (OperationCanceledException ex) + { + Log.Information("GetAsync", "Task was cancelled."); + Log.Verbose("GetAsync", $"Connect cancelled. Info: {ex}"); + Log.Verbose("AbstractRestClient.GetAsync", "LEAVE"); + throw; + } + catch (Exception ex) + { + Log.Error("GetAsync", $"{ex.GetType()} thrown {ex.Message}"); + Log.Verbose("GetAsync", $"Excepton: {ex}"); + Log.Verbose("AbstractRestClient.GetAsync", "LEAVE"); + throw; + } + } + + /// + /// Post method + /// + /// Class type of what return type is expected + /// Uri for the api including the query parameters + /// HttpContent as content for HttpRequestMessage + /// Instance of T + public virtual async Task PostRetrieveLocalFileAsync(string uriSegment, S? parameter, R? content, + List? keys = null, CancellationTokenSource? cancellationToken = null, Dictionary? addons = null, + Dictionary? headers = null + ) + { + Log.Verbose("AbstractRestClient.PostRetrieveLocalFileAsync", "ENTER"); + Log.Debug("PostRetrieveLocalFileAsync", $"uriSegment: {uriSegment}"); + Log.Debug("PostRetrieveLocalFileAsync", $"keys: {keys}"); + Log.Debug("PostRetrieveLocalFileAsync", $"addons: {addons}"); + + try + { + // if not defined, use default timeout + if (cancellationToken == null) + { + Log.Information("PostRetrieveLocalFileAsync", $"Using default timeout: {Constants.DefaultRESTTimeout}"); + cancellationToken = new CancellationTokenSource(); + cancellationToken.CancelAfter(Constants.DefaultRESTTimeout); + } + + // create request message and add custom query parameters + var request = new HttpRequestMessage(HttpMethod.Post, QueryParameterUtil.FormatURL(uriSegment, parameter, addons)) + { + Content = HttpRequestUtil.CreatePayload(content) + }; + + // add custom headers + if (headers != null) + { + foreach (var header in headers) + { + Log.Debug("PostRetrieveLocalFileAsync", $"Add Header {header.Key}={header.Value}"); + request.Headers.Add(header.Key, header.Value); + } + } + + // do the request + Log.Verbose("PostRetrieveLocalFileAsync", "Calling _httpClient.SendAsync..."); + var response = await _httpClient.SendAsync(request, cancellationToken.Token); + + if (!response.IsSuccessStatusCode) + { + var resultStr = await response.Content.ReadAsStringAsync(); + await ThrowException("PostRetrieveLocalFileAsync", response, resultStr); + } + + var result = new Dictionary(); + + if (keys != null) + { + for (int i = 0; i < response.Headers.Count(); i++) + { + var key = response.Headers.ElementAt(i).Key.ToLower(); + var value = response.Headers.GetValues(key).FirstOrDefault() ?? ""; + + var index = key.IndexOf("x-dg-"); + if (index == 0) + { + var newKey = key.Substring(5); + if (keys.Contains(newKey)) + { + result.Add(newKey, value); + continue; + } + } + index = key.IndexOf("dg-"); + if (index == 0) + { + var newKey = key.Substring(3); + if (keys.Contains(newKey)) + { + result.Add(newKey, value); + continue; + } + } + if (keys.Contains(key)) + { + result.Add(key, value); + } + } + + if (keys.Contains("content-type")) + { + result.Add("content-type", response.Content.Headers.ContentType?.MediaType ?? ""); + } + } + + MemoryStream stream = new MemoryStream(); + await response.Content.CopyToAsync(stream); + + Log.Verbose("PostRetrieveLocalFileAsync", "Succeeded"); + Log.Verbose("AbstractRestClient.PostRetrieveLocalFileAsync", "LEAVE"); + + return new LocalFileWithMetadata() + { + Metadata = result, + Content = stream, + }; + + } + catch (OperationCanceledException ex) + { + Log.Information("PostRetrieveLocalFileAsync", "Task was cancelled."); + Log.Verbose("PostRetrieveLocalFileAsync", $"Connect cancelled. Info: {ex}"); + Log.Verbose("AbstractRestClient.PostRetrieveLocalFileAsync", "LEAVE"); + throw; + } + catch (Exception ex) + { + Log.Error("PostRetrieveLocalFileAsync", $"{ex.GetType()} thrown {ex.Message}"); + Log.Verbose("PostRetrieveLocalFileAsync", $"Excepton: {ex}"); + Log.Verbose("AbstractRestClient.PostRetrieveLocalFileAsync", "LEAVE"); + throw; + } + } + + /// + /// Post method + /// + /// Class type of what return type is expected + /// Uri for the api including the query parameters + /// HttpContent as content for HttpRequestMessage + /// Instance of T + public virtual async Task PostAsync(string uriSegment, S? parameter, CancellationTokenSource? cancellationToken = null, + Dictionary? addons = null, Dictionary? headers = null) + { + Log.Verbose("AbstractRestClient.PostAsync", "ENTER"); + Log.Debug("PostAsync", $"uriSegment: {uriSegment}"); + Log.Debug("PostAsync", $"addons: {addons}"); + + try + { + // if not defined, use default timeout + if (cancellationToken == null) + { + Log.Information("PostAsync", $"Using default timeout: {Constants.DefaultRESTTimeout}"); + cancellationToken = new CancellationTokenSource(); + cancellationToken.CancelAfter(Constants.DefaultRESTTimeout); + } + + // create request message and add custom query parameters + var request = new HttpRequestMessage(HttpMethod.Post, QueryParameterUtil.FormatURL(uriSegment, parameter, addons)) + { + Content = HttpRequestUtil.CreatePayload(parameter) + }; + + // add custom headers + if (headers != null) + { + foreach (var header in headers) + { + Log.Debug("PostAsync", $"Add Header {header.Key}={header.Value}"); + request.Headers.Add(header.Key, header.Value); + } + } + + // do the request + Log.Verbose("PostAsync", "Calling _httpClient.SendAsync..."); + var response = await _httpClient.SendAsync(request, cancellationToken.Token); + + var resultStr = await response.Content.ReadAsStringAsync(); + if (!response.IsSuccessStatusCode) + { + await ThrowException("PostAsync", response, resultStr); + } + + Log.Verbose("PostAsync", $"Response:\n{resultStr}"); + var result = await HttpRequestUtil.DeserializeAsync(response); + + Log.Debug("PostAsync", $"Succeeded"); + Log.Verbose("AbstractRestClient.PostAsync", "LEAVE"); + + return result; + } + catch (OperationCanceledException ex) + { + Log.Information("PostAsync", "Task was cancelled."); + Log.Verbose("PostAsync", $"Connect cancelled. Info: {ex}"); + Log.Verbose("AbstractRestClient.PostAsync", "LEAVE"); + throw; + } + catch (Exception ex) + { + Log.Error("PostAsync", $"{ex.GetType()} thrown {ex.Message}"); + Log.Verbose("PostAsync", $"Excepton: {ex}"); + Log.Verbose("AbstractRestClient.PostAsync", "LEAVE"); + throw; + } + } + + public virtual async Task PostAsync(string uriSegment, S? parameter, R? content, CancellationTokenSource? cancellationToken = null, + Dictionary? addons = null, Dictionary? headers = null) + { + Log.Verbose("AbstractRestClient.PostAsync", "ENTER"); + Log.Debug("PostAsync", $"uriSegment: {uriSegment}"); + Log.Debug("PostAsync", $"addons: {addons}"); + + try + { + // if not defined, use default timeout + if (cancellationToken == null) + { + Log.Information("PostAsync", $"Using default timeout: {Constants.DefaultRESTTimeout}"); + cancellationToken = new CancellationTokenSource(); + cancellationToken.CancelAfter(Constants.DefaultRESTTimeout); + } + + // create request message and add custom query parameters + var request = new HttpRequestMessage(HttpMethod.Post, QueryParameterUtil.FormatURL(uriSegment, parameter, addons)); + if (typeof(R) == typeof(Stream)) + { + Stream? stream = content as Stream; + if (stream == null) + { + stream = new MemoryStream(); + } + request.Content = HttpRequestUtil.CreateStreamPayload(stream); + } + else + { + request.Content = HttpRequestUtil.CreatePayload(content); + } + + // add custom headers + if (headers != null) + { + foreach (var header in headers) + { + Log.Debug("PostAsync", $"Add Header {header.Key}={header.Value}"); + request.Headers.Add(header.Key, header.Value); + } + } + + // do the request + Log.Verbose("PostAsync", "Calling _httpClient.SendAsync..."); + var response = await _httpClient.SendAsync(request, cancellationToken.Token); + + var resultStr = await response.Content.ReadAsStringAsync(); + if (!response.IsSuccessStatusCode) + { + await ThrowException("PostAsync", response, resultStr); + } + + Log.Verbose("PostAsync", $"Response:\n{resultStr}"); + var result = await HttpRequestUtil.DeserializeAsync(response); + + Log.Debug("PostAsync", $"Succeeded"); + Log.Verbose("AbstractRestClient.PostAsync", "LEAVE"); + + return result; + } + catch (OperationCanceledException ex) + { + Log.Information("PostAsync", "Task was cancelled."); + Log.Verbose("PostAsync", $"Connect cancelled. Info: {ex}"); + Log.Verbose("AbstractRestClient.PostAsync", "LEAVE"); + throw; + } + catch (Exception ex) + { + Log.Error("PostAsync", $"{ex.GetType()} thrown {ex.Message}"); + Log.Verbose("PostAsync", $"Excepton: {ex}"); + Log.Verbose("AbstractRestClient.PostAsync", "LEAVE"); + throw; + } + } + + /// + /// Patch method call that takes a body object + /// + /// Class type of what return type is expected + /// Uri for the api including the query parameters + /// Instance of T + public virtual async Task PatchAsync(string uriSegment, S? parameter, CancellationTokenSource? cancellationToken = null, + Dictionary? addons = null, Dictionary? headers = null) + { + Log.Verbose("AbstractRestClient.PatchAsync", "ENTER"); + Log.Debug("PatchAsync", $"uriSegment: {uriSegment}"); + Log.Debug("PatchAsync", $"addons: {addons}"); + + try + { + // if not defined, use default timeout + if (cancellationToken == null) + { + Log.Information("PatchAsync", $"Using default timeout: {Constants.DefaultRESTTimeout}"); + cancellationToken = new CancellationTokenSource(); + cancellationToken.CancelAfter(Constants.DefaultRESTTimeout); + } + + // create request message and add custom query parameters +#if NETSTANDARD2_0 + var request = new HttpRequestMessage(new HttpMethod("PATCH"), QueryParameterUtil.FormatURL(uriSegment, parameter, addons)) + { + Content = HttpRequestUtil.CreatePayload(parameter) + }; +#else + var request = new HttpRequestMessage(HttpMethod.Patch, QueryParameterUtil.FormatURL(uriSegment, parameter, addons)) + { + Content = HttpRequestUtil.CreatePayload(parameter) + }; +#endif + + // add custom headers + if (headers != null) + { + foreach (var header in headers) + { + Log.Debug("PatchAsync", $"Add Header {header.Key}={header.Value}"); + request.Headers.Add(header.Key, header.Value); + } + } + + // do the request + Log.Verbose("PatchAsync", "Calling _httpClient.SendAsync..."); + var response = await _httpClient.SendAsync(request, cancellationToken.Token); + + var resultStr = await response.Content.ReadAsStringAsync(); + if (!response.IsSuccessStatusCode) + { + await ThrowException("PatchAsync", response, resultStr); + } + + Log.Verbose("PatchAsync", $"Response:\n{resultStr}"); + var result = await HttpRequestUtil.DeserializeAsync(response); + + Log.Debug("PatchAsync", $"Succeeded"); + Log.Verbose("AbstractRestClient.PatchAsync", "LEAVE"); + + return result; + + } + catch (OperationCanceledException ex) + { + Log.Information("PatchAsync", "Task was cancelled."); + Log.Verbose("PatchAsync", $"Connect cancelled. Info: {ex}"); + Log.Verbose("AbstractRestClient.PatchAsync", "LEAVE"); + throw; + } + catch (Exception ex) + { + Log.Error("PatchAsync", $"{ex.GetType()} thrown {ex.Message}"); + Log.Verbose("PatchAsync", $"Excepton: {ex}"); + Log.Verbose("AbstractRestClient.PatchAsync", "LEAVE"); + throw; + } + } + + /// + /// Put method call that takes a body object + /// + /// Class type of what return type is expected + /// Uri for the api + /// Instance of T + public virtual async Task PutAsync(string uriSegment, S? parameter, CancellationTokenSource? cancellationToken = null, + Dictionary? addons = null, Dictionary? headers = null) + { + Log.Verbose("AbstractRestClient.PutAsync", "ENTER"); + Log.Debug("PutAsync", $"uriSegment: {uriSegment}"); + Log.Debug("PutAsync", $"addons: {addons}"); + + try + { + // if not defined, use default timeout + if (cancellationToken == null) + { + Log.Information("PutAsync", $"Using default timeout: {Constants.DefaultRESTTimeout}"); + cancellationToken = new CancellationTokenSource(); + cancellationToken.CancelAfter(Constants.DefaultRESTTimeout); + } + + // create request message and add custom query parameters + var request = new HttpRequestMessage(HttpMethod.Put, QueryParameterUtil.FormatURL(uriSegment, parameter, addons)) + { + Content = HttpRequestUtil.CreatePayload(parameter) + }; + + // add custom headers + if (headers != null) + { + foreach (var header in headers) + { + var tmp = header.Key.ToLower(); + if ( !(tmp.Contains("password") || tmp.Contains("token") || tmp.Contains("authorization") || tmp.Contains("auth")) ) + { + Log.Debug("PutAsync", $"Add Header {header.Key}={header.Value}"); + } + request.Headers.Add(header.Key, header.Value); + } + } + + // do the request + Log.Verbose("PutAsync", "Calling _httpClient.SendAsync..."); + var response = await _httpClient.SendAsync(request, cancellationToken.Token); + + var resultStr = await response.Content.ReadAsStringAsync(); + if (!response.IsSuccessStatusCode) + { + await ThrowException("PutAsync", response, resultStr); + } + + Log.Verbose("PutAsync", $"Response:\n{resultStr}"); + var result = await HttpRequestUtil.DeserializeAsync(response); + + Log.Debug("PutAsync", $"Succeeded"); + Log.Verbose("AbstractRestClient.PutAsync", "LEAVE"); + + return result; + } + catch (OperationCanceledException ex) + { + Log.Information("PutAsync", "Task was cancelled."); + Log.Verbose("PutAsync", $"Connect cancelled. Info: {ex}"); + Log.Verbose("AbstractRestClient.PutAsync", "LEAVE"); + throw; + } + catch (Exception ex) + { + Log.Error("PutAsync", $"{ex.GetType()} thrown {ex.Message}"); + Log.Verbose("PutAsync", $"Excepton: {ex}"); + Log.Verbose("AbstractRestClient.PutAsync", "LEAVE"); + throw; + } + } + + /// + /// Delete Method for use with calls that do not expect a response + /// + /// Uri for the api including the query parameters + public virtual async Task DeleteAsync(string uriSegment, CancellationTokenSource? cancellationToken = null, + Dictionary? addons = null, Dictionary? headers = null) + { + Log.Verbose("AbstractRestClient.DeleteAsync", "ENTER"); + Log.Debug("DeleteAsync", $"uriSegment: {uriSegment}"); + Log.Debug("DeleteAsync", $"addons: {addons}"); + + try + { + // if not defined, use default timeout + if (cancellationToken == null) + { + Log.Information("DeleteAsync", $"Using default timeout: {Constants.DefaultRESTTimeout}"); + cancellationToken = new CancellationTokenSource(); + cancellationToken.CancelAfter(Constants.DefaultRESTTimeout); + } + + // create request message and add custom query parameters + var request = new HttpRequestMessage(HttpMethod.Delete, QueryParameterUtil.FormatURL(uriSegment, new NoopSchema(), addons)); + + // add custom headers + if (headers != null) + { + foreach (var header in headers) + { + Log.Debug("DeleteAsync", $"Add Header {header.Key}={header.Value}"); + request.Headers.Add(header.Key, header.Value); + } + } + + // do the request + Log.Verbose("DeleteAsync", "Calling _httpClient.SendAsync..."); + var response = await _httpClient.SendAsync(request, cancellationToken.Token); + + var resultStr = await response.Content.ReadAsStringAsync(); + if (!response.IsSuccessStatusCode) + { + await ThrowException("DeleteAsync", response, resultStr); + } + + Log.Verbose("DeleteAsync", $"Response:\n{resultStr}"); + var result = await HttpRequestUtil.DeserializeAsync(response); + + Log.Debug("DeleteAsync", $"Succeeded"); + Log.Verbose("AbstractRestClient.DeleteAsync", "LEAVE"); + + return result; + } + catch (OperationCanceledException ex) + { + Log.Information("DeleteAsync", "Task was cancelled."); + Log.Verbose("DeleteAsync", $"Connect cancelled. Info: {ex}"); + Log.Verbose("AbstractRestClient.DeleteAsync", "LEAVE"); + throw; + } + catch (Exception ex) + { + Log.Error("DeleteAsync", $"{ex.GetType()} thrown {ex.Message}"); + Log.Verbose("DeleteAsync", $"Excepton: {ex}"); + Log.Verbose("AbstractRestClient.DeleteAsync", "LEAVE"); + throw; + } + } + + /// + /// Delete method that returns the type of response specified + /// + /// Class Type of expected response + /// Uri for the api including the query parameters + /// Instance of T or throws Exception + public virtual async Task DeleteAsync(string uriSegment, S? parameter, CancellationTokenSource? cancellationToken = null, Dictionary? addons = null, + Dictionary? headers = null) + { + Log.Verbose("AbstractRestClient.DeleteAsync", "ENTER"); + Log.Debug("DeleteAsync", $"uriSegment: {uriSegment}"); + Log.Debug("DeleteAsync", $"addons: {addons}"); + + try + { + // if not defined, use default timeout + if (cancellationToken == null) + { + Log.Information("DeleteAsync", $"Using default timeout: {Constants.DefaultRESTTimeout}"); + cancellationToken = new CancellationTokenSource(); + cancellationToken.CancelAfter(Constants.DefaultRESTTimeout); + } + + // create request message and add custom query parameters + var request = new HttpRequestMessage(HttpMethod.Delete, QueryParameterUtil.FormatURL(uriSegment, parameter, addons)); + + // add custom headers + if (headers != null) + { + foreach (var header in headers) + { + Log.Debug("DeleteAsync", $"Add Header {header.Key}={header.Value}"); + request.Headers.Add(header.Key, header.Value); + } + } + + // do the request + Log.Verbose("DeleteAsync", "Calling _httpClient.SendAsync..."); + var response = await _httpClient.SendAsync(request, cancellationToken.Token); + + var resultStr = await response.Content.ReadAsStringAsync(); + if (!response.IsSuccessStatusCode) + { + await ThrowException("DeleteAsync", response, resultStr); + } + + Log.Verbose("DeleteAsync", $"Response:\n{resultStr}"); + var result = await HttpRequestUtil.DeserializeAsync(response); + + Log.Debug("DeleteAsync", $"Succeeded"); + Log.Verbose("AbstractRestClient.DeleteAsync", "LEAVE"); + + return result; + } + catch (OperationCanceledException ex) + { + Log.Information("DeleteAsync", "Task was cancelled."); + Log.Verbose("DeleteAsync", $"Connect cancelled. Info: {ex}"); + Log.Verbose("AbstractRestClient.DeleteAsync", "LEAVE"); + throw; + } + catch (Exception ex) + { + Log.Error("DeleteAsync", $"{ex.GetType()} thrown {ex.Message}"); + Log.Verbose("DeleteAsync", $"Excepton: {ex}"); + Log.Verbose("AbstractRestClient.DeleteAsync", "LEAVE"); + throw; + } + } + + private static async Task ThrowException(string module, HttpResponseMessage response, string errMsg) + { + if (errMsg == null || errMsg.Length == 0) + { + Log.Verbose(module, $"HTTP/REST Exception thrown"); + response.EnsureSuccessStatusCode(); // this throws the exception + } + + Log.Verbose(module, $"Deepgram Exception: {errMsg}"); + DeepgramRESTException? resException = null; + try + { + resException = await HttpRequestUtil.DeserializeAsync(response); + } + catch (Exception ex) + { + Log.Verbose(module, $"DeserializeAsync Error Exception: {ex}"); + } + + if (resException != null) + { + Log.Verbose(module, "DeepgramRESTException thrown"); + throw resException; + } + + Log.Verbose(module, $"Deepgram Generic Exception thrown"); + throw new DeepgramException(errMsg); + } + + internal static string GetUri(IDeepgramClientOptions options, string path) + { + return $"{options.BaseAddress}/{path}"; + } +} + diff --git a/Deepgram/Abstractions/v2/AbstractWebSocketClient.cs b/Deepgram/Abstractions/v2/AbstractWebSocketClient.cs new file mode 100644 index 00000000..16bdbec1 --- /dev/null +++ b/Deepgram/Abstractions/v2/AbstractWebSocketClient.cs @@ -0,0 +1,766 @@ +// Copyright 2021-2024 Deepgram .NET 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 + + +using Deepgram.Models.Authenticate.v1; +using Deepgram.Models.Exceptions.v1; +using Deepgram.Models.Common.v2.WebSocket; + +namespace Deepgram.Abstractions.v2; + +/// +/// Implements version 1 of the Live Client. +/// +public abstract class AbstractWebSocketClient : IDisposable +{ + #region Fields + protected readonly IDeepgramClientOptions _deepgramClientOptions; + + protected ClientWebSocket? _clientWebSocket; + protected CancellationTokenSource? _cancellationTokenSource; + + protected readonly SemaphoreSlim _mutexSubscribe = new SemaphoreSlim(1, 1); + protected readonly SemaphoreSlim _mutexSend = new SemaphoreSlim(1, 1); + #endregion + + /// Required DeepgramApiKey + /// for HttpClient Configuration + public AbstractWebSocketClient(string? apiKey = null, IDeepgramClientOptions? options = null) + { + Log.Verbose("AbstractWebSocketClient", "ENTER"); + + options ??= new DeepgramWsClientOptions(apiKey); + _deepgramClientOptions = options; + + Log.Debug("AbstractWebSocketClient", $"APIVersion: {options.APIVersion}"); + Log.Debug("AbstractWebSocketClient", $"BaseAddress: {options.BaseAddress}"); + Log.Debug("AbstractWebSocketClient", $"OnPrem: {options.OnPrem}"); + Log.Verbose("AbstractWebSocketClient", "LEAVE"); + } + + #region Event Handlers + /// + /// Fires when an event is received from the Deepgram API + /// + protected event EventHandler? _openReceived; + protected event EventHandler? _closeReceived; + protected event EventHandler? _unhandledReceived; + protected event EventHandler? _errorReceived; + #endregion + + /// + /// Connect to a Deepgram API Web Socket to begin transcribing audio + /// + /// Options to use when transcribing audio + /// The task object representing the asynchronous operation. + public async Task Connect(string uri, CancellationTokenSource? cancelToken = null, Dictionary? headers = null) + { + Log.Verbose("AbstractWebSocketClient.Connect", "ENTER"); + Log.Debug("Connect", $"headers: {headers}"); + + // check if the client is disposed + if (_clientWebSocket != null) + { + // client has already connected + var exStr = "Client has already been initialized"; + Log.Error("Connect", exStr); + Log.Verbose("AbstractWebSocketClient.Connect", "LEAVE"); + + return true; + } + + if (cancelToken == null) + { + Log.Information("Connect", "Using default connect cancellation token"); + cancelToken = new CancellationTokenSource(Constants.DefaultConnectTimeout); + } + + // create client + _clientWebSocket = new ClientWebSocket(); + + // set headers + _clientWebSocket.Options.SetRequestHeader("Authorization", $"token {_deepgramClientOptions.ApiKey}"); + if (_deepgramClientOptions.Headers is not null) { + foreach (var header in _deepgramClientOptions.Headers) + { + var tmp = header.Key.ToLower(); + if (!(tmp.Contains("password") || tmp.Contains("token") || tmp.Contains("authorization") || tmp.Contains("auth"))) + { + Log.Debug("PutAsync", $"Add Header {header.Key}={header.Value}"); + } + _clientWebSocket.Options.SetRequestHeader(header.Key, header.Value); + } + } + if (headers is not null) + { + foreach (var header in headers) + { + var tmp = header.Key.ToLower(); + if (!(tmp.Contains("password") || tmp.Contains("token") || tmp.Contains("authorization") || tmp.Contains("auth"))) + { + Log.Debug("PutAsync", $"Add Header {header.Key}={header.Value}"); + } + _clientWebSocket.Options.SetRequestHeader(header.Key, header.Value); + } + } + + // internal cancellation token for internal threads + _cancellationTokenSource = new CancellationTokenSource(); + + try + { + var myUri = new Uri(uri); + Log.Debug("Connect", $"uri: {uri}"); + + Log.Debug("Connect", "Connecting to Deepgram API..."); + await _clientWebSocket.ConnectAsync(myUri, cancelToken.Token).ConfigureAwait(false); + + if (!IsConnected()) + { + Log.Error("Connect", "Failed to connect to Deepgram API"); + Log.Verbose("AbstractWebSocketClient.Connect", "LEAVE"); + + return false; + } + + Log.Debug("Connect", "Starting Sender Thread..."); + StartSenderBackgroundThread(); + + Log.Debug("Connect", "Starting Receiver Thread..."); + StartReceiverBackgroundThread(); + + // send an OpenResponse event + if (_openReceived != null) + { + Log.Debug("Connect", "Sending OpenResponse event..."); + var data = new OpenResponse(); + data.Type = WebSocketType.Open; + _openReceived.Invoke(null, data); + } + + Log.Debug("Connect", "Connect Succeeded"); + Log.Verbose("AbstractWebSocketClient.Connect", "LEAVE"); + + return true; + } + catch (TaskCanceledException ex) + { + Log.Debug("Connect", "Connect cancelled."); + Log.Verbose("Connect", $"Connect cancelled. Info: {ex}"); + Log.Verbose("AbstractWebSocketClient.Connect", "LEAVE"); + + return false; + } + catch (Exception ex) + { + Log.Error("Connect", $"{ex.GetType()} thrown {ex.Message}"); + Log.Verbose("Connect", $"Exception: {ex}"); + Log.Verbose("AbstractWebSocketClient.Connect", "LEAVE"); + throw; + } + + void StartSenderBackgroundThread() => Task.Run(() => ProcessSendQueue()); + + void StartReceiverBackgroundThread() => Task.Run(() => ProcessReceiveQueue()); + } + + #region Subscribe Event + /// + /// Subscribe to an Open event from the Deepgram API + /// + /// + /// True if successful + public async Task Subscribe(EventHandler eventHandler) + { + await _mutexSubscribe.WaitAsync(); + try + { + _openReceived += (sender, e) => eventHandler(sender, e); + } + finally + { + _mutexSubscribe.Release(); + } + + return true; + } + + /// + /// Subscribe to a Close event from the Deepgram API + /// + /// True if successful + public async Task Subscribe(EventHandler eventHandler) + { + await _mutexSubscribe.WaitAsync(); + try + { + _closeReceived += (sender, e) => eventHandler(sender, e); + } + finally + { + _mutexSubscribe.Release(); + } + return true; + } + + /// + /// Subscribe to an Unhandled event from the Deepgram API + /// + /// True if successful + public async Task Subscribe(EventHandler eventHandler) + { + await _mutexSubscribe.WaitAsync(); + try + { + _unhandledReceived += (sender, e) => eventHandler(sender, e); + } + finally + { + _mutexSubscribe.Release(); + } + return true; + } + + /// + /// Subscribe to an Error event from the Deepgram API + /// + /// True if successful + public async Task Subscribe(EventHandler eventHandler) + { + await _mutexSubscribe.WaitAsync(); + try + { + _errorReceived += (sender, e) => eventHandler(sender, e); + } + finally + { + _mutexSubscribe.Release(); + } + return true; + } + #endregion + + #region Send Functions + /// + /// Sends a Close message to Deepgram + /// + public virtual Task SendClose(bool nullByte = false) + { + throw new DeepgramException("Unimplemented"); + } + + /// + /// Sends a binary message over the WebSocket connection. + /// + /// The data to be sent over the WebSocket. + public virtual void Send(byte[] data, int length = Constants.UseArrayLengthForSend) => SendBinary(data, length); + + /// + /// This method sends a binary message over the WebSocket connection. + /// + /// + public virtual void SendBinary(byte[] data, int length = Constants.UseArrayLengthForSend) => + EnqueueSendMessage(new WebSocketMessage(data, WebSocketMessageType.Binary, length)); + + /// + /// This method sends a text message over the WebSocket connection. + /// + public virtual void SendMessage(byte[] data, int length = Constants.UseArrayLengthForSend) => + EnqueueSendMessage(new WebSocketMessage(data, WebSocketMessageType.Text, length)); + + /// + /// This method sends a binary message over the WebSocket connection immediately without queueing. + /// + public virtual async Task SendBinaryImmediately(byte[] data, int length = Constants.UseArrayLengthForSend) + { + if (!IsConnected()) + { + Log.Debug("SendBinaryImmediately", "WebSocket is not connected. Exiting..."); + return; + } + + await _mutexSend.WaitAsync(_cancellationTokenSource.Token); + try + { + Log.Verbose("SendBinaryImmediately", "Sending binary message immediately..."); + if (length == Constants.UseArrayLengthForSend) + { + length = data.Length; + } + await _clientWebSocket.SendAsync(new ArraySegment(data, 0, length), WebSocketMessageType.Binary, true, _cancellationTokenSource.Token) + .ConfigureAwait(false); + } + finally + { + _mutexSend.Release(); + } + } + + /// + /// This method sends a text message over the WebSocket connection immediately without queueing. + /// + public virtual async Task SendMessageImmediately(byte[] data, int length = Constants.UseArrayLengthForSend) + { + if (!IsConnected()) + { + Log.Debug("SendBinaryImmediately", "WebSocket is not connected. Exiting..."); + return; + } + + await _mutexSend.WaitAsync(_cancellationTokenSource.Token); + try + { + Log.Verbose("SendMessageImmediately", "Sending text message immediately..."); + if (length == Constants.UseArrayLengthForSend) + { + length = data.Length; + } + await _clientWebSocket.SendAsync(new ArraySegment(data, 0, length), WebSocketMessageType.Text, true, _cancellationTokenSource.Token) + .ConfigureAwait(false); + } + finally + { + _mutexSend.Release(); + } + } + #endregion + + internal void EnqueueSendMessage(WebSocketMessage message) + { + try + { + _sendChannel.Writer.TryWrite(message); + } + catch (Exception ex) + { + Log.Error("EnqueueSendMessage", $"{ex.GetType()} thrown {ex.Message}"); + Log.Verbose("EnqueueSendMessage", $"Exception: {ex}"); + } + } + + internal async Task ProcessSendQueue() + { + Log.Verbose("AbstractWebSocketClient.ProcessSendQueue", "ENTER"); + + if (_clientWebSocket == null) + { + var exStr = "Attempting to start a sender queue when the WebSocket has been disposed is not allowed."; + Log.Error("EnqueueSendMessage", exStr); + Log.Verbose("ProcessSendQueue", "LEAVE"); + + throw new InvalidOperationException(exStr); + } + + try + { + while (await _sendChannel.Reader.WaitToReadAsync(_cancellationTokenSource.Token)) + { + if (_cancellationTokenSource.Token.IsCancellationRequested) + { + Log.Information("ProcessSendQueue", "ProcessSendQueue cancelled"); + break; + } + if (!IsConnected()) + { + Log.Debug("ProcessSendQueue", "WebSocket is not connected. Exiting..."); + break; + } + + Log.Verbose("ProcessSendQueue", "Reading message off queue..."); + while (_sendChannel.Reader.TryRead(out var message)) + { + // TODO: Add logging for message capturing for possible playback + Log.Verbose("ProcessSendQueue", "Sending message..."); + await _mutexSend.WaitAsync(_cancellationTokenSource.Token); + try + { + await _clientWebSocket.SendAsync(message.Message, message.MessageType, true, _cancellationTokenSource.Token) + .ConfigureAwait(false); + } + finally + { + _mutexSend.Release(); + } + } + } + + Log.Verbose("ProcessSendQueue", "Exit"); + Log.Verbose("AbstractWebSocketClient.ProcessSendQueue", "LEAVE"); + } + catch (OperationCanceledException ex) + { + Log.Debug("ProcessSendQueue", "SendThread cancelled."); + Log.Verbose("ProcessSendQueue", $"SendThread cancelled. Info: {ex}"); + Log.Verbose("AbstractWebSocketClient.ProcessSendQueue", "LEAVE"); + } + catch (Exception ex) + { + Log.Error("ProcessSendQueue", $"{ex.GetType()} thrown {ex.Message}"); + Log.Verbose("ProcessSendQueue", $"Exception: {ex}"); + Log.Verbose("AbstractWebSocketClient.ProcessSendQueue", "LEAVE"); + } + } + + internal async Task ProcessReceiveQueue() + { + Log.Verbose("AbstractWebSocketClient.ProcessReceiveQueue", "ENTER"); + + while (_clientWebSocket?.State == WebSocketState.Open) + { + try + { + if (_cancellationTokenSource.Token.IsCancellationRequested) + { + Log.Information("ProcessReceiveQueue", "ReceiveThread cancelled"); + await Stop(); + Log.Verbose("ProcessReceiveQueue", "LEAVE"); + return; + } + if (!IsConnected()) + { + Log.Debug("ProcessReceiveQueue", "WebSocket is not connected. Exiting..."); + return; + } + + var buffer = new ArraySegment(new byte[Constants.BufferSize]); + WebSocketReceiveResult result; + + using (var ms = new MemoryStream()) + { + do + { + // get the result of the receive operation + result = await _clientWebSocket.ReceiveAsync(buffer, _cancellationTokenSource.Token); + + ms.Write( + buffer.Array ?? throw new InvalidOperationException("buffer cannot be null"), + buffer.Offset, + result.Count + ); + } while (!result.EndOfMessage); + + if (result.MessageType != WebSocketMessageType.Close) + { + Log.Verbose("ProcessReceiveQueue", $"Received message: {result} / {ms}"); + ProcessDataReceived(result, ms); + } + } + + if (result.MessageType == WebSocketMessageType.Close) + { + Log.Information("ProcessReceiveQueue", "Received WebSocket Close. Trigger cancel..."); + await Stop(); + Log.Verbose("ProcessReceiveQueue", "LEAVE"); + return; + } + } + catch (TaskCanceledException ex) + { + Log.Debug("ProcessReceiveQueue", "ReceiveThread cancelled."); + Log.Verbose("ProcessReceiveQueue", $"ReceiveThread cancelled. Info: {ex}"); + Log.Verbose("AbstractWebSocketClient.ProcessReceiveQueue", "LEAVE"); + } + catch (Exception ex) + { + Log.Error("ProcessReceiveQueue", $"{ex.GetType()} thrown {ex.Message}"); + Log.Verbose("ProcessReceiveQueue", $"Exception: {ex}"); + Log.Verbose("AbstractWebSocketClient.ProcessReceiveQueue", "LEAVE"); + } + } + } + + internal virtual void ProcessDataReceived(WebSocketReceiveResult result, MemoryStream ms) + { + Log.Verbose("AbstractWebSocketClient.ProcessDataReceived", "ENTER"); + + ms.Seek(0, SeekOrigin.Begin); + + if (result.MessageType == WebSocketMessageType.Binary) + { + ProcessBinaryMessage(result, ms); + } + else + { + ProcessTextMessage(result, ms); + } + } + + internal virtual void ProcessBinaryMessage(WebSocketReceiveResult result, MemoryStream ms) + { + throw new DeepgramException("Unimplemented"); + } + + internal virtual void ProcessTextMessage(WebSocketReceiveResult result, MemoryStream ms) + { + Log.Verbose("AbstractWebSocketClient.ProcessTextMessage", "ENTER"); + + ms.Seek(0, SeekOrigin.Begin); + + var response = Encoding.UTF8.GetString(ms.ToArray()); + if (response == null) + { + Log.Warning("ProcessTextMessage", "Response is null"); + Log.Verbose("AbstractWebSocketClient.ProcessTextMessage", "LEAVE"); + return; + } + + try + { + Log.Verbose("ProcessTextMessage", $"raw response: {response}"); + var data = JsonDocument.Parse(response); + var val = Enum.Parse(typeof(WebSocketType), data.RootElement.GetProperty("type").GetString()!); + + Log.Verbose("ProcessTextMessage", $"Type: {val}"); + + switch (val) + { + case WebSocketType.Open: + var openResponse = data.Deserialize(); + if (_openReceived == null) + { + Log.Debug("ProcessTextMessage", "_openReceived has no listeners"); + Log.Verbose("ProcessTextMessage", "LEAVE"); + return; + } + if (openResponse == null) + { + Log.Warning("ProcessTextMessage", "OpenResponse is invalid"); + Log.Verbose("ProcessTextMessage", "LEAVE"); + return; + } + + Log.Debug("ProcessTextMessage", $"Invoking OpenResponse. event: {openResponse}"); + InvokeParallel(_openReceived, openResponse); + break; + case WebSocketType.Error: + var errorResponse = data.Deserialize(); + if (_errorReceived == null) + { + Log.Debug("ProcessTextMessage", "_errorReceived has no listeners"); + Log.Verbose("ProcessTextMessage", "LEAVE"); + return; + } + if (errorResponse == null) + { + Log.Warning("ProcessTextMessage", "ErrorResponse is invalid"); + Log.Verbose("ProcessTextMessage", "LEAVE"); + return; + } + + Log.Debug("ProcessTextMessage", $"Invoking ErrorResponse. event: {errorResponse}"); + InvokeParallel(_errorReceived, errorResponse); + break; + default: + if (_unhandledReceived == null) + { + Log.Debug("ProcessTextMessage", "_unhandledReceived has no listeners"); + Log.Verbose("ProcessTextMessage", "LEAVE"); + return; + } + + var unhandledResponse = new UnhandledResponse(); + unhandledResponse.Type = WebSocketType.Unhandled; + unhandledResponse.Raw = response; + + Log.Debug("ProcessTextMessage", $"Invoking UnhandledResponse. event: {unhandledResponse}"); + InvokeParallel(_unhandledReceived, unhandledResponse); + break; + } + + Log.Debug("ProcessTextMessage", "Succeeded"); + Log.Verbose("AbstractWebSocketClient.ProcessTextMessage", "LEAVE"); + } + catch (JsonException ex) + { + Log.Error("ProcessTextMessage", $"{ex.GetType()} thrown {ex.Message}"); + Log.Verbose("ProcessTextMessage", $"Exception: {ex}"); + Log.Verbose("AbstractWebSocketClient.ProcessTextMessage", "LEAVE"); + } + catch (Exception ex) + { + Log.Error("ProcessTextMessage", $"{ex.GetType()} thrown {ex.Message}"); + Log.Verbose("ProcessTextMessage", $"Exception: {ex}"); + Log.Verbose("AbstractWebSocketClient.ProcessTextMessage", "LEAVE"); + } + } + + /// + /// Closes the Web Socket connection to the Deepgram API + /// + /// The task object representing the asynchronous operation. + public async Task Stop(CancellationTokenSource? cancelToken = null, bool nullByte = false) + { + Log.Verbose("AbstractWebSocketClient.Stop", "ENTER"); + + // client is already disposed + if (_clientWebSocket == null) + { + Log.Information("Stop", "Client has already been disposed"); + Log.Verbose("AbstractWebSocketClient.Stop", "LEAVE"); + return true; + } + + if (cancelToken == null) + { + Log.Information("Stop", "Using default disconnect cancellation token"); + cancelToken = new CancellationTokenSource(Constants.DefaultDisconnectTimeout); + } + + try + { + // if websocket is open, send a close message + if (_clientWebSocket!.State == WebSocketState.Open) + { + Log.Debug("Stop", "Sending Close message..."); + await SendClose(nullByte); + } + + // small delay to wait for any final transcription + await Task.Delay(100, cancelToken.Token).ConfigureAwait(false); + + // send a CloseResponse event + if (_closeReceived != null) + { + Log.Debug("Stop", "Sending CloseResponse event..."); + var data = new CloseResponse(); + data.Type = WebSocketType.Close; + InvokeParallel(_closeReceived, data); + } + + // attempt to stop the connection + if (_clientWebSocket!.State != WebSocketState.Closed && _clientWebSocket!.State != WebSocketState.Aborted) + { + Log.Debug("Stop", "Closing WebSocket connection..."); + await _clientWebSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, cancelToken.Token) + .ConfigureAwait(false); + } + + // clean up internal token + if (_cancellationTokenSource != null) + { + Log.Debug("Stop", "Disposing internal token..."); + _cancellationTokenSource.Dispose(); + _cancellationTokenSource = null; + } + + // release the socket + Log.Debug("Stop", "Disposing WebSocket socket..."); + _clientWebSocket = null; + + Log.Debug("Stop", "Succeeded"); + Log.Verbose("AbstractWebSocketClient.Stop", "LEAVE"); + + return true; + } + catch (TaskCanceledException ex) + { + Log.Debug("Stop", "Stop cancelled."); + Log.Verbose("Stop", $"Stop cancelled. Info: {ex}"); + Log.Verbose("AbstractWebSocketClient.Stop", "LEAVE"); + + return true; + } + catch (Exception ex) + { + Log.Error("Stop", $"{ex.GetType()} thrown {ex.Message}"); + Log.Verbose("Stop", $"Exception: {ex}"); + Log.Verbose("AbstractWebSocketClient.Stop", "LEAVE"); + throw; + } + } + + #region Helpers + /// + /// Retrieves the connection state of the WebSocket + /// + /// Returns the connection state of the WebSocket + public WebSocketState State() + { + if (_clientWebSocket == null) + { + return WebSocketState.None; + } + Log.Debug("State", $"WebSocket State: {_clientWebSocket.State}"); + return _clientWebSocket.State; + } + + /// + /// Indicates whether the WebSocket is connected + /// + /// Returns true if the WebSocket is connected + public bool IsConnected() { + if (_clientWebSocket == null) + { + return false; + } + + Log.Debug("State", $"WebSocket State: {_clientWebSocket.State}"); + return _clientWebSocket.State == WebSocketState.Open; + } + + /// + /// Handle channel options + /// + internal readonly Channel _sendChannel = System.Threading.Channels.Channel + .CreateUnbounded(new UnboundedChannelOptions { SingleReader = true, SingleWriter = true, }); + + internal void InvokeParallel(EventHandler? eventHandler, T e) + { + if (eventHandler != null) + { + try + { + Parallel.ForEach( + eventHandler.GetInvocationList().Cast>(), + (handler) => + handler(null, e)); + } + catch (AggregateException ae) + { + Log.Error("InvokeParallel", $"AggregateException occurred in one or more event handlers: {ae}"); + } + catch (Exception ex) + { + Log.Error("InvokeParallel", $"Exception occurred in event handler: {ex}"); + } + } + } + #endregion + + #region Dispose + /// + /// Disposes of the resources used by the client + /// + public void Dispose() + { + if (_clientWebSocket == null) + { + return; + } + + if (_cancellationTokenSource != null) + { + if (!_cancellationTokenSource.Token.IsCancellationRequested) + { + _cancellationTokenSource.Cancel(); + } + _cancellationTokenSource.Dispose(); + _cancellationTokenSource = null; + } + + if (_sendChannel != null) + { + _sendChannel.Writer.Complete(); + } + + if (_clientWebSocket != null) + { + _clientWebSocket.Dispose(); + _clientWebSocket = null; + } + + GC.SuppressFinalize(this); + } + #endregion +} diff --git a/Deepgram/Abstractions/v2/Constants.cs b/Deepgram/Abstractions/v2/Constants.cs new file mode 100644 index 00000000..50d3a72f --- /dev/null +++ b/Deepgram/Abstractions/v2/Constants.cs @@ -0,0 +1,24 @@ +// Copyright 2024 Deepgram .NET 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 + +namespace Deepgram.Abstractions.v2; + +/// +/// Defaults for the REST and WS AbstractClient +/// +public static class Constants +{ + // For REST + public const int OneSecond = 1000; + public const int OneMinute = 60 * OneSecond; + public const int DefaultRESTTimeout = 30 * OneSecond; + + // For WS + public const int BufferSize = 1024 * 16; + public const int UseArrayLengthForSend = -1; + + public const int DefaultConnectTimeout = 5000; + public const int DefaultDisconnectTimeout = 5000; +} + diff --git a/Deepgram/Abstractions/v2/LocalFileWithMetadata.cs b/Deepgram/Abstractions/v2/LocalFileWithMetadata.cs new file mode 100644 index 00000000..5d3a9524 --- /dev/null +++ b/Deepgram/Abstractions/v2/LocalFileWithMetadata.cs @@ -0,0 +1,33 @@ +// Copyright 2024 Deepgram .NET 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 + +namespace Deepgram.Abstractions.v2; + +/// +/// LocalFileWithMetadata is a class that represents a file with metadata. +/// +public class LocalFileWithMetadata : IDisposable +{ + /// + /// Gets or sets the metadata associated with the file content. + /// + public Dictionary Metadata { get; set; } + + /// + /// Gets or sets the file content as a MemoryStream. + /// The caller is responsible for disposing of this stream when no longer needed. + /// + /// + /// This property should be properly disposed of to prevent memory leaks. + /// + public MemoryStream Content { get; set; } + + /// + /// Releases the resources used by the Content stream. + /// + public void Dispose() + { + Content?.Dispose(); + } +} diff --git a/Deepgram/Abstractions/v2/NoopSchema.cs b/Deepgram/Abstractions/v2/NoopSchema.cs new file mode 100644 index 00000000..d56de536 --- /dev/null +++ b/Deepgram/Abstractions/v2/NoopSchema.cs @@ -0,0 +1,13 @@ +// Copyright 2024 Deepgram .NET 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 + +namespace Deepgram.Abstractions.v2; + +/// +/// Just a NoopSchema where you dont need to marshall JSON into a Async function. +/// +public class NoopSchema +{ +} + diff --git a/Deepgram/Abstractions/v2/Utilities.cs b/Deepgram/Abstractions/v2/Utilities.cs new file mode 100644 index 00000000..65f134d1 --- /dev/null +++ b/Deepgram/Abstractions/v2/Utilities.cs @@ -0,0 +1,61 @@ +// Copyright 2021-2024 Deepgram .NET 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 + +namespace Deepgram.Abstractions.v2; + +/// +/// Helper funcitons for HttpRequests +/// +internal static class HttpRequestUtil +{ + public const string DEFAULT_CONTENT_TYPE = "application/json"; + + static readonly JsonSerializerOptions _jsonSerializerOptions = new() + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + NumberHandling = JsonNumberHandling.AllowReadingFromString, + }; + + /// + /// Create the body payload of a HttpRequestMessage + /// + /// Type of the body to be sent + /// instance value for the body + /// What type of content is being sent default is : application/json + /// + internal static StringContent CreatePayload(T body) + { + var serialized = JsonSerializer.Serialize(body, _jsonSerializerOptions); + return new(serialized, Encoding.UTF8, DEFAULT_CONTENT_TYPE); + } + + + /// + /// Create the stream payload of a HttpRequestMessage + /// + /// of type stream + /// HttpContent + internal static HttpContent CreateStreamPayload(Stream body) + { + body.Seek(0, SeekOrigin.Begin); + HttpContent httpContent = new StreamContent(body); + httpContent.Headers.Add("Content-Length", body.Length.ToString()); + return httpContent; + } + + + /// + /// method that deserializes DeepgramResponse and performs null checks on values + /// + /// Class Type of expected response + /// Http Response to be deserialized + /// instance of TResponse or a Exception + internal static async Task DeserializeAsync(HttpResponseMessage httpResponseMessage) + { + var content = await httpResponseMessage.Content.ReadAsStringAsync(); + var deepgramResponse = JsonSerializer.Deserialize(content); + return deepgramResponse; + } + +} diff --git a/Deepgram/Abstractions/v2/WebSocketMessage.cs b/Deepgram/Abstractions/v2/WebSocketMessage.cs new file mode 100644 index 00000000..31a5182b --- /dev/null +++ b/Deepgram/Abstractions/v2/WebSocketMessage.cs @@ -0,0 +1,32 @@ +// Copyright 2021-2024 Deepgram .NET 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 + +namespace Deepgram.Abstractions.v2; + +internal readonly struct WebSocketMessage +{ + public WebSocketMessage(byte[] message, WebSocketMessageType type) + : this(message, type, Constants.UseArrayLengthForSend) + { + } + + public WebSocketMessage(byte[] message, WebSocketMessageType type, int length) + { + if (length != Constants.UseArrayLengthForSend && length <= message.Length && length > 0) + { + Message = new ArraySegment(message, 0, length); + } + else + { + Message = new ArraySegment(message, 0, message.Length); + } + MessageType = type; + } + + public int Length => Message.Count; + + public ArraySegment Message { get; } + + public WebSocketMessageType MessageType { get; } +} diff --git a/Deepgram/ClientFactory.cs b/Deepgram/ClientFactory.cs index 7fed455a..17e2f7d6 100644 --- a/Deepgram/ClientFactory.cs +++ b/Deepgram/ClientFactory.cs @@ -3,7 +3,13 @@ // SPDX-License-Identifier: MIT using Deepgram.Models.Authenticate.v1; -using Deepgram.Clients.Interfaces.v1; +using V1 = Deepgram.Clients.Interfaces.v1; +using V2 = Deepgram.Clients.Interfaces.v2; + +using ListenV1 = Deepgram.Clients.Listen.v1.WebSocket; +using ListenV2 = Deepgram.Clients.Listen.v2.WebSocket; +using SpeakV1 = Deepgram.Clients.Speak.v1.WebSocket; +using SpeakV2 = Deepgram.Clients.Speak.v2.WebSocket; namespace Deepgram; @@ -16,11 +22,20 @@ public static class ClientFactory /// /// /// - public static IAnalyzeClient CreateAnalyzeClient(string apiKey = "", DeepgramHttpClientOptions? options = null, string? httpId = null) + public static V1.IAnalyzeClient CreateAnalyzeClient(string apiKey = "", DeepgramHttpClientOptions? options = null, string? httpId = null) { return new AnalyzeClient(apiKey, options, httpId); } + /// + /// This method allows you to create an AnalyzeClient with a specific version of the client. + /// + public static object CreateAnalyzeClient(int version, string apiKey = "", DeepgramHttpClientOptions? options = null, string? httpId = null) + { + // Currently only a single version of the AnalyzeClient exists + return new AnalyzeClient(apiKey, options, httpId); + } + /// // *********** WARNING *********** // This function creates a LiveClient for the Deepgram API @@ -32,22 +47,41 @@ public static IAnalyzeClient CreateAnalyzeClient(string apiKey = "", DeepgramHtt // *********** WARNING *********** /// [Obsolete("Please use CreateListenWebSocketClient instead", false)] - public static ILiveClient CreateLiveClient(string apiKey = "", DeepgramWsClientOptions? options = null) + public static V1.ILiveClient CreateLiveClient(string apiKey = "", DeepgramWsClientOptions? options = null) { return new LiveClient(apiKey, options); } /// - /// Create a new ListenWebSocketClient + /// Create a new ListenWebSocketClient using the latest version /// /// /// /// - public static IListenWebSocketClient CreateListenWebSocketClient(string apiKey = "", DeepgramWsClientOptions? options = null) + public static V2.IListenWebSocketClient CreateListenWebSocketClient(string apiKey = "", DeepgramWsClientOptions? options = null) { return new ListenWebSocketClient(apiKey, options); } + /// + /// This method allows you to create an AnalyzeClient with a specific version of the client. + /// + public static object CreateListenWebSocketClient(int version, string apiKey = "", DeepgramWsClientOptions? options = null) + { + // at some point this needs to be changed to use reflection to get the type of the client + switch(version) + { + case 1: + Log.Information("ClientFactory", $"Version 1 of the ListenWebSocketClient is being deprecated in the next major version."); + Log.Information("ClientFactory", $"Transition to the latest version at your earliest convenience."); + return new ListenV1.Client(apiKey, options); + case 2: + return new ListenV2.Client(apiKey, options); + default: + throw new ArgumentException("Invalid version", nameof(version)); + } + } + /// /// Create a new ManageClient /// @@ -55,8 +89,17 @@ public static IListenWebSocketClient CreateListenWebSocketClient(string apiKey = /// /// /// - public static IManageClient CreateManageClient(string apiKey = "", DeepgramHttpClientOptions? options = null, string? httpId = null) + public static V1.IManageClient CreateManageClient(string apiKey = "", DeepgramHttpClientOptions? options = null, string? httpId = null) + { + return new ManageClient(apiKey, options, httpId); + } + + /// + /// This method allows you to create an ManageClient with a specific version of the client. + /// + public static object CreateManageClient(int version, string apiKey = "", DeepgramHttpClientOptions? options = null, string? httpId = null) { + // Currently only a single version of the ManageClient exists return new ManageClient(apiKey, options, httpId); } @@ -71,7 +114,7 @@ public static IManageClient CreateManageClient(string apiKey = "", DeepgramHttpC // *********** WARNING *********** /// [Obsolete("Please use CreateSelfHostedClient instead", false)] - public static IOnPremClient CreateOnPremClient(string apiKey = "", DeepgramHttpClientOptions? options = null, string? httpId = null) + public static V1.IOnPremClient CreateOnPremClient(string apiKey = "", DeepgramHttpClientOptions? options = null, string? httpId = null) { return new OnPremClient(apiKey, options, httpId); } @@ -83,11 +126,20 @@ public static IOnPremClient CreateOnPremClient(string apiKey = "", DeepgramHttpC /// /// /// - public static ISelfHostedClient CreateSelfHostedClient(string apiKey = "", DeepgramHttpClientOptions? options = null, string? httpId = null) + public static V1.ISelfHostedClient CreateSelfHostedClient(string apiKey = "", DeepgramHttpClientOptions? options = null, string? httpId = null) { return new SelfHostedClient(apiKey, options, httpId); } + /// + /// This method allows you to create an SelfHostedClient with a specific version of the client. + /// + public static object CreateSelfHostedClient(int version, string apiKey = "", DeepgramHttpClientOptions? options = null, string? httpId = null) + { + // Currently only a single version of the SelfHostedClient exists + return new SelfHostedClient(apiKey, options, httpId); + } + /// // *********** WARNING *********** // This function creates a PreRecordedClient for the Deepgram API @@ -99,7 +151,7 @@ public static ISelfHostedClient CreateSelfHostedClient(string apiKey = "", Deepg // *********** WARNING *********** /// [Obsolete("Please use CreateListenRESTClient instead", false)] - public static IPreRecordedClient CreatePreRecordedClient(string apiKey = "", DeepgramHttpClientOptions? options = null, string? httpId = null) + public static V1.IPreRecordedClient CreatePreRecordedClient(string apiKey = "", DeepgramHttpClientOptions? options = null, string? httpId = null) { return new PreRecordedClient(apiKey, options, httpId); } @@ -111,8 +163,17 @@ public static IPreRecordedClient CreatePreRecordedClient(string apiKey = "", Dee /// /// /// - public static IListenRESTClient CreateListenRESTClient(string apiKey = "", DeepgramHttpClientOptions? options = null, string? httpId = null) + public static V1.IListenRESTClient CreateListenRESTClient(string apiKey = "", DeepgramHttpClientOptions? options = null, string? httpId = null) + { + return new ListenRESTClient(apiKey, options, httpId); + } + + /// + /// This method allows you to create an ListenRESTClient with a specific version of the client. + /// + public static object CreateListenRESTClient(int version, string apiKey = "", DeepgramHttpClientOptions? options = null, string? httpId = null) { + // Currently only a single version of the ListenRESTClient exists return new ListenRESTClient(apiKey, options, httpId); } @@ -127,7 +188,7 @@ public static IListenRESTClient CreateListenRESTClient(string apiKey = "", Deepg // *********** WARNING *********** /// [Obsolete("Please use CreateSpeakRESTClient instead", false)] - public static ISpeakClient CreateSpeakClient(string apiKey = "", DeepgramHttpClientOptions? options = null, string? httpId = null) + public static V1.ISpeakClient CreateSpeakClient(string apiKey = "", DeepgramHttpClientOptions? options = null, string? httpId = null) { return new SpeakClient(apiKey, options, httpId); } @@ -139,8 +200,17 @@ public static ISpeakClient CreateSpeakClient(string apiKey = "", DeepgramHttpCli /// /// /// - public static ISpeakRESTClient CreateSpeakRESTClient(string apiKey = "", DeepgramHttpClientOptions? options = null, string? httpId = null) + public static V1.ISpeakRESTClient CreateSpeakRESTClient(string apiKey = "", DeepgramHttpClientOptions? options = null, string? httpId = null) + { + return new SpeakRESTClient(apiKey, options, httpId); + } + + /// + /// This method allows you to create an SpeakRESTClient with a specific version of the client. + /// + public static object CreateSpeakRESTClient(int version, string apiKey = "", DeepgramHttpClientOptions? options = null, string? httpId = null) { + // Currently only a single version of the SpeakRESTClient exists return new SpeakRESTClient(apiKey, options, httpId); } @@ -150,8 +220,27 @@ public static ISpeakRESTClient CreateSpeakRESTClient(string apiKey = "", Deepgra /// /// /// - public static ISpeakWebSocketClient CreateSpeakWebSocketClient(string apiKey = "", DeepgramWsClientOptions? options = null) + public static V2.ISpeakWebSocketClient CreateSpeakWebSocketClient(string apiKey = "", DeepgramWsClientOptions? options = null) { return new SpeakWebSocketClient(apiKey, options); } + + /// + /// This method allows you to create an SpeakWebSocketClient with a specific version of the client. + /// + public static object CreateSpeakWebSocketClient(int version, string apiKey = "", DeepgramHttpClientOptions? options = null) + { + // at some point this needs to be changed to use reflection to get the type of the client + switch (version) + { + case 1: + Log.Information("ClientFactory", $"Version 1 of the ListenWebSocketClient is being deprecated in the next major version."); + Log.Information("ClientFactory", $"Transition to the latest version at your earliest convenience."); + return new SpeakV1.Client(apiKey, options); + case 2: + return new SpeakV2.Client(apiKey, options); + default: + throw new ArgumentException("Invalid version", nameof(version)); + } + } } diff --git a/Deepgram/Clients/Analyze/v1/Client.cs b/Deepgram/Clients/Analyze/v1/Client.cs index e4bec72a..0dbba654 100644 --- a/Deepgram/Clients/Analyze/v1/Client.cs +++ b/Deepgram/Clients/Analyze/v1/Client.cs @@ -5,6 +5,7 @@ using Deepgram.Models.Analyze.v1; using Deepgram.Models.Authenticate.v1; using Deepgram.Clients.Interfaces.v1; +using Deepgram.Abstractions.v1; namespace Deepgram.Clients.Analyze.v1; diff --git a/Deepgram/Clients/Interfaces/v1/ISpeakClient.cs b/Deepgram/Clients/Interfaces/v1/ISpeakClient.cs index 411eb2f8..e20bb4af 100644 --- a/Deepgram/Clients/Interfaces/v1/ISpeakClient.cs +++ b/Deepgram/Clients/Interfaces/v1/ISpeakClient.cs @@ -5,14 +5,14 @@ namespace Deepgram.Clients.Interfaces.v1; /// -// *********** WARNING *********** -// This class provides the ISpeakClient implementation for the Deepgram API -// -// Deprecated: This class is deprecated. Use the ISpeakRESTClient interface instead. -// This will be removed in a future release. -// -// This package is frozen and no new functionality will be added. -// *********** WARNING *********** +/// *********** WARNING *********** +/// This class provides the ISpeakClient implementation for the Deepgram API +/// +/// Deprecated: This class is deprecated. Use the ISpeakRESTClient interface instead. +/// This will be removed in a future release. +/// +/// This package is frozen and no new functionality will be added. +/// *********** WARNING *********** /// [Obsolete("Please use ISpeakRESTClient instead", false)] public interface ISpeakClient : ISpeakRESTClient diff --git a/Deepgram/Clients/Interfaces/v1/IResponseEvent.cs b/Deepgram/Clients/Interfaces/v1/ResponseEvent.cs similarity index 64% rename from Deepgram/Clients/Interfaces/v1/IResponseEvent.cs rename to Deepgram/Clients/Interfaces/v1/ResponseEvent.cs index fbcf5daa..f48d41f7 100644 --- a/Deepgram/Clients/Interfaces/v1/IResponseEvent.cs +++ b/Deepgram/Clients/Interfaces/v1/ResponseEvent.cs @@ -4,8 +4,13 @@ namespace Deepgram.Clients.Interfaces.v1; -public class IResponseEvent +public class ResponseEvent { - public T? Response { get; set; } + public T? Response { get; } + + public ResponseEvent(T? response) + { + Response = response; + } } diff --git a/Deepgram/Clients/Interfaces/v2/Constants.cs b/Deepgram/Clients/Interfaces/v2/Constants.cs new file mode 100644 index 00000000..8b016fe7 --- /dev/null +++ b/Deepgram/Clients/Interfaces/v2/Constants.cs @@ -0,0 +1,15 @@ +// Copyright 2024 Deepgram .NET 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 + +namespace Deepgram.Clients.Interfaces.v2; + +/// +/// Headers of interest in the return values from the Deepgram Speak API. +/// +public static class Constants +{ + // WS buffer size + public const int UseArrayLengthForSend = -1; +} + diff --git a/Deepgram/Clients/Interfaces/v2/IAnalyzeClient.cs b/Deepgram/Clients/Interfaces/v2/IAnalyzeClient.cs new file mode 100644 index 00000000..ce16cd02 --- /dev/null +++ b/Deepgram/Clients/Interfaces/v2/IAnalyzeClient.cs @@ -0,0 +1,94 @@ +// Copyright 2021-2024 Deepgram .NET 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 + +using Deepgram.Models.Analyze.v1; + +namespace Deepgram.Clients.Interfaces.v2; + +/// +/// Not currently being used +/// +public interface IAnalyzeClient +{ + #region NoneCallBacks + /// + /// Analyze a file by providing a url + /// + /// Url to the file that is to be analyzed + /// Options for the transcription + /// + public Task AnalyzeUrl(UrlSource source, AnalyzeSchema? analyzeSchema, CancellationTokenSource? cancellationToken = default, + Dictionary? addons = null, Dictionary? headers = null); + + /// + /// Analyze by providing text + /// + /// Text that is to be analyzed + /// Options for the transcription + /// + public Task AnalyzeText(TextSource source, AnalyzeSchema? analyzeSchema, CancellationTokenSource? cancellationToken = default, Dictionary? addons = null, Dictionary? headers = null); + + /// + /// Analyzes a file using the provided byte array + /// + /// file is the form of a byte[] + /// Options for the transcription + /// + public Task AnalyzeFile(byte[] source, AnalyzeSchema? analyzeSchema, CancellationTokenSource? cancellationToken = default, + Dictionary? addons = null, Dictionary? headers = null); + + /// + /// Analyzes a file using the provided stream + /// + /// file is the form of a stream + /// Options for the transcription + /// + public Task AnalyzeFile(Stream source, AnalyzeSchema? analyzeSchema, CancellationTokenSource? cancellationToken = default, + Dictionary? addons = null, Dictionary? headers = null); + #endregion + + #region CallBack Methods + /// + /// Analyzes a file using the provided byte array and providing a CallBack + /// + /// file is the form of a byte[] + /// CallBack url + /// Options for the transcription + /// + public Task AnalyzeFileCallBack(byte[] source, string? callBack, AnalyzeSchema? analyzeSchema, + CancellationTokenSource? cancellationToken = default, Dictionary? addons = null, + Dictionary? headers = null); + + /// + /// Analyzes a file using the provided stream and providing a CallBack + /// + /// file is the form of a stream + /// CallBack url + /// Options for the transcription + /// + public Task AnalyzeFileCallBack(Stream source, string? callBack, AnalyzeSchema? analyzeSchema, + CancellationTokenSource? cancellationToken = default, Dictionary? addons = null, + Dictionary? headers = null); + + /// + /// Analyze a file by providing a url and a CallBack + /// + /// Url to the file that is to be analyzed + /// CallBack url + /// Options for the transcription + /// + public Task AnalyzeUrlCallBack(UrlSource source, string? callBack, AnalyzeSchema? analyzeSchema, + CancellationTokenSource? cancellationToken = default, Dictionary? addons = null, + Dictionary? headers = null); + + /// + /// Analyze by providing text and a CallBack + /// + /// Text that is to be analyzed + /// CallBack url + /// Options for the transcription + /// + public Task AnalyzeTextCallBack(TextSource source, string? callBack, AnalyzeSchema? analyzeSchema, CancellationTokenSource? cancellationToken = default, Dictionary? addons = null, Dictionary? headers = null); + #endregion +} diff --git a/Deepgram/Clients/Interfaces/v2/IListenRESTClient.cs b/Deepgram/Clients/Interfaces/v2/IListenRESTClient.cs new file mode 100644 index 00000000..d76b3d29 --- /dev/null +++ b/Deepgram/Clients/Interfaces/v2/IListenRESTClient.cs @@ -0,0 +1,79 @@ +// Copyright 2021-2024 Deepgram .NET 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 + +using Deepgram.Models.Listen.v1.REST; + +namespace Deepgram.Clients.Interfaces.v2; + +/// +/// Not currently being used +/// +public interface IListenRESTClient + +{ + #region NoneCallBacks + /// + /// Transcribe a file by providing a url + /// + /// Url to the file that is to be transcribed + /// Options for the transcription + /// + public Task TranscribeUrl(UrlSource source, PreRecordedSchema? prerecordedSchema, + CancellationTokenSource? cancellationToken = default, Dictionary? addons = null, Dictionary? headers = null); + + /// + /// Transcribes a file using the provided byte array + /// + /// file is the form of a byte[] + /// Options for the transcription + /// + public Task TranscribeFile(byte[] source, PreRecordedSchema? prerecordedSchema, + CancellationTokenSource? cancellationToken = default, Dictionary? addons = null, Dictionary? headers = null); + + /// + /// Transcribes a file using the provided stream + /// + /// file is the form of a streasm + /// Options for the transcription + /// + public Task TranscribeFile(Stream source, PreRecordedSchema? prerecordedSchema, + CancellationTokenSource? cancellationToken = default, Dictionary? addons = null, + Dictionary? headers = null); + #endregion + + #region CallBack Methods + /// + /// Transcribes a file using the provided byte array and providing a CallBack + /// + /// file is the form of a byte[] + /// CallBack url + /// Options for the transcription + /// + public Task TranscribeFileCallBack(byte[] source, string? callBack, PreRecordedSchema? prerecordedSchema, + CancellationTokenSource? cancellationToken = default, Dictionary? addons = null, + Dictionary? headers = null); + + /// + /// Transcribes a file using the provided stream and providing a CallBack + /// + /// file is the form of a stream + /// CallBack url + /// Options for the transcription + /// + public Task TranscribeFileCallBack(Stream source, string? callBack, PreRecordedSchema? prerecordedSchema, + CancellationTokenSource? cancellationToken = default, Dictionary? addons = null, + Dictionary? headers = null); + + /// + /// Transcribe a file by providing a url and a CallBack + /// + /// Url to the file that is to be transcribed + /// CallBack url + /// Options for the transcription + /// + public Task TranscribeUrlCallBack(UrlSource source, string? callBack, PreRecordedSchema? prerecordedSchema, + CancellationTokenSource? cancellationToken = default, Dictionary? addons = null, + Dictionary? headers = null); + #endregion +} diff --git a/Deepgram/Clients/Interfaces/v2/IListenWebSocketClient.cs b/Deepgram/Clients/Interfaces/v2/IListenWebSocketClient.cs new file mode 100644 index 00000000..d59cce6d --- /dev/null +++ b/Deepgram/Clients/Interfaces/v2/IListenWebSocketClient.cs @@ -0,0 +1,137 @@ +// Copyright 2024 Deepgram .NET 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 + +using Deepgram.Models.Listen.v2.WebSocket; + +namespace Deepgram.Clients.Interfaces.v2; + +/// +/// Implements version 2 of the Live Client. +/// +public interface IListenWebSocketClient +{ + #region Connect and Disconnect + public Task Connect(LiveSchema options, CancellationTokenSource? cancelToken = null, Dictionary? addons = null, + Dictionary? headers = null); + + public Task Stop(CancellationTokenSource? cancelToken = null, bool nullByte = false); + #endregion + + #region Subscribe Event + /// + /// Subscribe to an Open event from the Deepgram API + /// + /// + /// True if successful + public Task Subscribe(EventHandler eventHandler); + + /// + /// Subscribe to a Metadata event from the Deepgram API + /// + /// + /// True if successful + public Task Subscribe(EventHandler eventHandler); + + /// + /// Subscribe to a Results event from the Deepgram API + /// + /// True if successful + public Task Subscribe(EventHandler eventHandler); + + /// + /// Subscribe to an UtteranceEnd event from the Deepgram API + /// + /// True if successful + public Task Subscribe(EventHandler eventHandler); + + /// + /// Subscribe to a SpeechStarted event from the Deepgram API + /// + /// True if successful + public Task Subscribe(EventHandler eventHandler); + + /// + /// Subscribe to a Close event from the Deepgram API + /// + /// True if successful + public Task Subscribe(EventHandler eventHandler); + + /// + /// Subscribe to an Unhandled event from the Deepgram API + /// + /// True if successful + public Task Subscribe(EventHandler eventHandler); + + /// + /// Subscribe to an Error event from the Deepgram API + /// + /// True if successful + public Task Subscribe(EventHandler eventHandler); + #endregion + + #region Send Functions + /// + /// Sends a KeepAlive message to Deepgram + /// + public Task SendKeepAlive(); + + /// + /// Sends a Finalize message to Deepgram + /// + public Task SendFinalize(); + + /// + /// Sends a Close message to Deepgram + /// + public Task SendClose(bool nullByte = false); + + /// + /// Sends a binary message over the WebSocket connection. + /// + /// The data to be sent over the WebSocket. + public void Send(byte[] data, int length = Constants.UseArrayLengthForSend); + + /// + /// This method sends a binary message over the WebSocket connection. + /// + /// + /// The number of bytes from the data to send. Use `Constants.UseArrayLengthForSend` to send the entire array. + public void SendBinary(byte[] data, int length = Constants.UseArrayLengthForSend); + + /// + /// This method sends a text message over the WebSocket connection. + /// + /// + /// The number of bytes from the data to send. Use `Constants.UseArrayLengthForSend` to send the entire array. + public void SendMessage(byte[] data, int length = Constants.UseArrayLengthForSend); + + /// + /// This method sends a binary message over the WebSocket connection immediately without queueing. + /// + /// + /// The number of bytes from the data to send. Use `Constants.UseArrayLengthForSend` to send the entire array. + public Task SendBinaryImmediately(byte[] data, int length = Constants.UseArrayLengthForSend); + + /// + /// This method sends a text message over the WebSocket connection immediately without queueing. + /// + /// + /// The number of bytes from the data to send. Use `Constants.UseArrayLengthForSend` to send the entire array. + public Task SendMessageImmediately(byte[] data, int length = Constants.UseArrayLengthForSend); + #endregion + + #region Helpers + /// + /// Retrieves the connection state of the WebSocket + /// + /// Returns the connection state of the WebSocket + public WebSocketState State(); + + /// + /// Indicates whether the WebSocket is connected + /// + /// Returns true if the WebSocket is connected + public bool IsConnected(); + #endregion +} diff --git a/Deepgram/Clients/Interfaces/v2/ILiveClient.cs b/Deepgram/Clients/Interfaces/v2/ILiveClient.cs new file mode 100644 index 00000000..d9d574c1 --- /dev/null +++ b/Deepgram/Clients/Interfaces/v2/ILiveClient.cs @@ -0,0 +1,73 @@ +// Copyright 2024 Deepgram .NET 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 + +using Deepgram.Models.Live.v1; + +namespace Deepgram.Clients.Interfaces.v2; + +/// +// *********** WARNING *********** +// This is the ILiveClient interface +// +// Deprecated: This class is deprecated. Use the `IListenWebSocketClient` function instead. +// This will be removed in a future release. +// +// This class is frozen and no new functionality will be added. +// *********** WARNING *********** +/// +[Obsolete("Please use IListenWebSocketClient instead", false)] +public interface ILiveClient : IListenWebSocketClient +{ + #region Subscribe Event + /// + /// Subscribe to an Open event from the Deepgram API + /// + /// + /// True if successful + public Task Subscribe(EventHandler eventHandler); + + /// + /// Subscribe to a Metadata event from the Deepgram API + /// + /// + /// True if successful + public Task Subscribe(EventHandler eventHandler); + + /// + /// Subscribe to a Results event from the Deepgram API + /// + /// True if successful + public Task Subscribe(EventHandler eventHandler); + + /// + /// Subscribe to an UtteranceEnd event from the Deepgram API + /// + /// True if successful + public Task Subscribe(EventHandler eventHandler); + + /// + /// Subscribe to a SpeechStarted event from the Deepgram API + /// + /// True if successful + public Task Subscribe(EventHandler eventHandler); + + /// + /// Subscribe to a Close event from the Deepgram API + /// + /// True if successful + public Task Subscribe(EventHandler eventHandler); + + /// + /// Subscribe to an Unhandled event from the Deepgram API + /// + /// True if successful + public Task Subscribe(EventHandler eventHandler); + + /// + /// Subscribe to an Error event from the Deepgram API + /// + /// True if successful + public Task Subscribe(EventHandler eventHandler); + #endregion +} diff --git a/Deepgram/Clients/Interfaces/v2/IManageClient.cs b/Deepgram/Clients/Interfaces/v2/IManageClient.cs new file mode 100644 index 00000000..c40f1bb2 --- /dev/null +++ b/Deepgram/Clients/Interfaces/v2/IManageClient.cs @@ -0,0 +1,251 @@ +// Copyright 2021-2024 Deepgram .NET 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 + +using Deepgram.Models.Manage.v1; + +namespace Deepgram.Clients.Interfaces.v2; + +/// +/// Not currently being used +/// +public interface IManageClient +{ + #region Projects + /// + /// Gets projects associated to ApiKey + /// + /// + public Task GetProjects(CancellationTokenSource? cancellationToken = default, + Dictionary? addons = null, Dictionary? headers = null); + + /// + /// Gets project associated with project Id + /// + /// Id of Project + /// + public Task GetProject(string projectId, CancellationTokenSource? cancellationToken = default, + Dictionary? addons = null, Dictionary? headers = null); + + /// + /// Update a project associated with the projectID + /// + /// ID of project + /// for project + /// + // USES PATCH + public Task UpdateProject(string projectId, ProjectSchema updateProjectSchema, CancellationTokenSource? cancellationToken = default, + Dictionary? addons = null, Dictionary? headers = null); + + /// + /// Deletes a project, no response will be returned + /// + /// Id of project + // No response expected + public Task DeleteProject(string projectId, CancellationTokenSource? cancellationToken = default, + Dictionary? addons = null, Dictionary? headers = null); + + /// + /// leave project associated with the project Id + /// + /// Id of project + /// + public Task LeaveProject(string projectId, CancellationTokenSource? cancellationToken = default, + Dictionary? addons = null, Dictionary? headers = null); + + /// + /// Get all models associated with the project Id + /// + /// Id of project + /// + public Task GetProjectModels(string projectId, ModelSchema? modelSchema = null, CancellationTokenSource? cancellationToken = default, + Dictionary? addons = null, Dictionary? headers = null); + + /// + /// Get a specific model associated with the project Id + /// + /// Id of project + /// Id of model + /// + public Task GetProjectModel(string projectId, string modelId, CancellationTokenSource? cancellationToken = default, + Dictionary? addons = null, Dictionary? headers = null); + #endregion + + #region Models + /// + /// Gets models available in Deepgram + /// + /// + public Task GetModels(ModelSchema? modelSchema = null, CancellationTokenSource? cancellationToken = default, + Dictionary? addons = null, Dictionary? headers = null); + + /// + /// Gets a specific model within Deepgram + /// + /// ID of model + /// + public Task GetModel(string modelId, CancellationTokenSource? cancellationToken = default, + Dictionary? addons = null, Dictionary? headers = null); + #endregion + + #region ProjectKeys + /// + /// Get the keys associated with the project + /// + /// Id of project + /// + public Task GetKeys(string projectId, CancellationTokenSource? cancellationToken = default, + Dictionary? addons = null, Dictionary? headers = null); + + /// + /// Get details of key associated with the key ID + /// + /// Id of project + /// Id of key + /// + public Task GetKey(string projectId, string keyId, CancellationTokenSource? cancellationToken = default, + Dictionary? addons = null, Dictionary? headers = null); + + /// + /// Create a key in the associated project + /// + /// Id of project + /// for the key to be created + /// + public Task CreateKey(string projectId, KeySchema keySchema, CancellationTokenSource? cancellationToken = default, + Dictionary? addons = null, Dictionary? headers = null); + + /// + /// Remove key from project, No response returned + /// + /// Id of project + /// Id of key + // Nothing being returned + public Task DeleteKey(string projectId, string keyId, CancellationTokenSource? cancellationToken = default, + Dictionary? addons = null, Dictionary? headers = null); + #endregion + + #region ProjectInvites + /// + /// Get any invites that are associated with project + /// + /// Id of project + /// + public Task GetInvites(string projectId, CancellationTokenSource? cancellationToken = default, + Dictionary? addons = null, Dictionary? headers = null); + + /// + /// Delete a project invite that has been sent + /// + /// Id of project + /// email of the invite to be removed + //no response expected + public Task DeleteInvite(string projectId, string email, CancellationTokenSource? cancellationToken = default, + Dictionary? addons = null, Dictionary? headers = null); + + /// + /// Send a invite to the associated project + /// + /// Id of project + /// for a invite to project + /// + public Task SendInvite(string projectId, InviteSchema inviteSchema, CancellationTokenSource? cancellationToken = default, + Dictionary? addons = null, Dictionary? headers = null); + #endregion + + #region Members + /// + /// Get the members associated with the project + /// + /// Id of project + /// + public Task GetMembers(string projectId, CancellationTokenSource? cancellationToken = default, + Dictionary? addons = null, Dictionary? headers = null); + + /// + /// Get the scopes associated with member + /// + /// Id of project + /// Id of member + /// + public Task GetMemberScopes(string projectId, string memberId, CancellationTokenSource? cancellationToken = default, + Dictionary? addons = null, Dictionary? headers = null); + + /// + /// Update the scopes fot the member + /// + /// Id of project + /// Id of member + /// Updates scope options for member + /// + public Task UpdateMemberScope(string projectId, string memberId, MemberScopeSchema scopeSchema, + CancellationTokenSource? cancellationToken = default, Dictionary? addons = null, + Dictionary? headers = null); + + /// + /// Remove member from project, there is no response + /// + /// Id of project + /// Id of member + //No response expected + public Task RemoveMember(string projectId, string memberId, CancellationTokenSource? cancellationToken = default, + Dictionary? addons = null, Dictionary? headers = null); + #endregion + + #region Usage + /// + /// Get usage request associated with the project + /// + /// Id of project + /// Project usage request options + /// + public Task GetUsageRequests(string projectId, UsageRequestsSchema usageRequestsSchema, + CancellationTokenSource? cancellationToken = default, Dictionary? addons = null, Dictionary? headers = null); + + /// + /// Get the details associated with the requestID + /// + /// Id of project + /// Id of request + /// + public Task GetUsageRequest(string projectId, string requestId, CancellationTokenSource? cancellationToken = default, + Dictionary? addons = null, Dictionary? headers = null); + + /// + /// Gets a summary of usage + /// + /// Id of project + /// Usage summary options + /// + public Task GetUsageSummary(string projectId, UsageSummarySchema summarySchema, + CancellationTokenSource? cancellationToken = default, Dictionary? addons = null, Dictionary? headers = null); + + /// + /// Get usage fields + /// + /// Id of project + /// Project usage request field options + /// + public Task GetUsageFields(string projectId, UsageFieldsSchema fieldsSchema, + CancellationTokenSource? cancellationToken = default, Dictionary? addons = null, Dictionary? headers = null); + #endregion + + #region Balances + /// + /// Gets a list of balances + /// + /// Id of project + /// + public Task GetBalances(string projectId, CancellationTokenSource? cancellationToken = default, + Dictionary? addons = null, Dictionary? headers = null); + + /// + /// Get the balance details associated with the balance id + /// + /// Id of project + /// Id of balance + /// + public Task GetBalance(string projectId, string balanceId, CancellationTokenSource? cancellationToken = default, + Dictionary? addons = null, Dictionary? headers = null); + #endregion +} diff --git a/Deepgram/Clients/Interfaces/v2/IOnPremClient.cs b/Deepgram/Clients/Interfaces/v2/IOnPremClient.cs new file mode 100644 index 00000000..a62f2f27 --- /dev/null +++ b/Deepgram/Clients/Interfaces/v2/IOnPremClient.cs @@ -0,0 +1,20 @@ +// Copyright 2021-2024 Deepgram .NET 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 + +namespace Deepgram.Clients.Interfaces.v2; + +/// +// *********** WARNING *********** +// This class provides the IOnPremClient implementation +// +// Deprecated: This class is deprecated. Use ISelfHostedClient instead. +// This will be removed in a future release. +// +// This package is frozen and no new functionality will be added. +// *********** WARNING *********** +/// +[Obsolete("Please use ISelfHostedClient instead", false)] +public interface IOnPremClient : ISelfHostedClient +{ +} diff --git a/Deepgram/Clients/Interfaces/v2/IPreRecordedClient.cs b/Deepgram/Clients/Interfaces/v2/IPreRecordedClient.cs new file mode 100644 index 00000000..7fcf8749 --- /dev/null +++ b/Deepgram/Clients/Interfaces/v2/IPreRecordedClient.cs @@ -0,0 +1,20 @@ +// Copyright 2021-2024 Deepgram .NET 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 + +namespace Deepgram.Clients.Interfaces.v2; + +/// +// *********** WARNING *********** +// This is the IPreRecordedClient interface +// +// Deprecated: This class is deprecated. Use the `IListenRESTClient` function instead. +// This will be removed in a future release. +// +// This class is frozen and no new functionality will be added. +// *********** WARNING *********** +/// +[Obsolete("Please use IListenRESTClient instead", false)] +public interface IPreRecordedClient : IListenRESTClient +{ +} diff --git a/Deepgram/Clients/Interfaces/v2/ISelfHostedClient.cs b/Deepgram/Clients/Interfaces/v2/ISelfHostedClient.cs new file mode 100644 index 00000000..86a29064 --- /dev/null +++ b/Deepgram/Clients/Interfaces/v2/ISelfHostedClient.cs @@ -0,0 +1,48 @@ +// Copyright 2021-2024 Deepgram .NET 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 + +using Deepgram.Models.SelfHosted.v1; + +namespace Deepgram.Clients.Interfaces.v2; + +/// +/// Not currently being used +/// +public interface ISelfHostedClient +{ + /// + /// get a list of credentials associated with project + /// + /// Id of project + /// + public Task ListCredentials(string projectId, CancellationTokenSource? cancellationToken = default, + Dictionary? addons = null, Dictionary? headers = null); + + /// + /// Get credentials for the project that is associated with credential ID + /// + /// Id of project + /// Id of credentials + /// + public Task GetCredentials(string projectId, string credentialsId, CancellationTokenSource? cancellationToken = default, + Dictionary? addons = null, Dictionary? headers = null); + + /// + /// Remove credentials in the project associated with the credentials ID + /// + /// Id of project + /// Id of credentials + /// + public Task DeleteCredentials(string projectId, string credentialsId, CancellationTokenSource? cancellationToken = default, + Dictionary? addons = null, Dictionary? headers = null); + + /// + /// Create credentials for the associated projects + /// + /// Id of project + /// for credentials to be created + /// + public Task CreateCredentials(string projectId, CredentialsSchema credentialsSchema, + CancellationTokenSource? cancellationToken = default, Dictionary? addons = null, Dictionary? headers = null); +} diff --git a/Deepgram/Clients/Interfaces/v2/ISpeakClient.cs b/Deepgram/Clients/Interfaces/v2/ISpeakClient.cs new file mode 100644 index 00000000..d5ad0807 --- /dev/null +++ b/Deepgram/Clients/Interfaces/v2/ISpeakClient.cs @@ -0,0 +1,20 @@ +// Copyright 2024 Deepgram .NET 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 + +namespace Deepgram.Clients.Interfaces.v2; + +/// +/// *********** WARNING *********** +/// This class provides the ISpeakClient implementation for the Deepgram API +/// +/// Deprecated: This class is deprecated. Use the ISpeakRESTClient interface instead. +/// This will be removed in a future release. +/// +/// This package is frozen and no new functionality will be added. +/// *********** WARNING *********** +/// +[Obsolete("Please use ISpeakRESTClient instead", false)] +public interface ISpeakClient : ISpeakRESTClient +{ +} diff --git a/Deepgram/Clients/Interfaces/v2/ISpeakRESTClient.cs b/Deepgram/Clients/Interfaces/v2/ISpeakRESTClient.cs new file mode 100644 index 00000000..8a1d4576 --- /dev/null +++ b/Deepgram/Clients/Interfaces/v2/ISpeakRESTClient.cs @@ -0,0 +1,39 @@ +// Copyright 2024 Deepgram .NET 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 + +using Deepgram.Models.Speak.v1.REST; + +namespace Deepgram.Clients.Interfaces.v2; + +/// +/// Not currently being used +/// +public interface ISpeakRESTClient +{ + #region NoneCallBacks + /// + /// Speaks a file using the provided stream + /// + /// file is the form of a stream + /// Options for the transcription + /// + public Task ToStream(TextSource source, SpeakSchema? speakSchema, CancellationTokenSource? cancellationToken = default, + Dictionary? addons = null, Dictionary? headers = null); + + public Task ToFile(TextSource source, string filename, SpeakSchema? speakSchema, CancellationTokenSource? cancellationToken = default, + Dictionary? addons = null, Dictionary? headers = null); + #endregion + + #region CallBack Methods + /// + /// Speaks a file using the provided byte array and providing a CallBack + /// + /// file is the form of a byte[] + /// CallBack url + /// Options for the transcription + /// + public Task StreamCallBack(TextSource source, string? callBack, SpeakSchema? speakSchema, CancellationTokenSource? cancellationToken = default, + Dictionary? addons = null, Dictionary? headers = null); + #endregion +} diff --git a/Deepgram/Clients/Interfaces/v2/ISpeakWebSocketClient.cs b/Deepgram/Clients/Interfaces/v2/ISpeakWebSocketClient.cs new file mode 100644 index 00000000..48f01290 --- /dev/null +++ b/Deepgram/Clients/Interfaces/v2/ISpeakWebSocketClient.cs @@ -0,0 +1,159 @@ +// Copyright 2024 Deepgram .NET 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 + +using Deepgram.Models.Speak.v2.WebSocket; + +namespace Deepgram.Clients.Interfaces.v2; + +/// +/// Implements version 2 of the Live Client. +/// +public interface ISpeakWebSocketClient +{ + #region Connect and Disconnect + public Task Connect(SpeakSchema options, CancellationTokenSource? cancelToken = null, Dictionary? addons = null, + Dictionary? headers = null); + + public Task Stop(CancellationTokenSource? cancelToken = null, bool nullByte = false); + #endregion + + #region Subscribe Event + /// + /// Subscribe to an Open event from the Deepgram API + /// + /// + /// True if successful + public Task Subscribe(EventHandler eventHandler); + + /// + /// Subscribe to a Metadata event from the Deepgram API + /// + /// + /// True if successful + public Task Subscribe(EventHandler eventHandler); + + /// + /// Subscribe to a Flushed event from the Deepgram API + /// + /// True if successful + public Task Subscribe(EventHandler eventHandler); + + /// + /// Subscribe to a Cleared event from the Deepgram API + /// + /// True if successful + public Task Subscribe(EventHandler eventHandler); + + /// + /// Subscribe to a Audio buffer/event from the Deepgram API + /// + /// True if successful + public Task Subscribe(EventHandler eventHandler); + + /// + /// Subscribe to a Close event from the Deepgram API + /// + /// True if successful + public Task Subscribe(EventHandler eventHandler); + + /// + /// Subscribe to an Warning event from the Deepgram API + /// + /// True if successful + public Task Subscribe(EventHandler eventHandler); + + + /// + /// Subscribe to an Error event from the Deepgram API + /// + /// True if successful + public Task Subscribe(EventHandler eventHandler); + + /// + /// Subscribe to an Unhandled event from the Deepgram API + /// + /// True if successful + public Task Subscribe(EventHandler eventHandler); + #endregion + + #region Send Functions + /// + /// Sends text data over the WebSocket connection. + /// + /// + public void SpeakWithText(string data); + + ///// + ///// This method Flushes the text buffer on Deepgram to be converted to audio + ///// + public void Flush(); + + ///// + ///// This method Resets the text buffer on Deepgram to be converted to audio + ///// + public void Clear(); + + ///// + ///// This method tells Deepgram to initiate the close server-side. + ///// + public void Close(bool nullByte = false); + + ///// + ///// This method sends a binary message over the WebSocket connection. + ///// + ///// + //public void SpeakWithStream(byte[] data); + + /// + /// Sends a Close message to Deepgram + /// + public Task SendClose(bool nullByte = false); + + /// + /// Sends a binary message over the WebSocket connection. + /// + /// + /// The number of bytes from the data to send. Use `Constants.UseArrayLengthForSend` to send the entire array. + public void Send(byte[] data, int length = Constants.UseArrayLengthForSend); + + ///// + ///// This method sends a binary message over the WebSocket connection. + ///// + ///// + //public void SendBinary(byte[] data, int length = Constants.UseArrayLengthForSend); + + /// + /// This method sends a text message over the WebSocket connection. + /// + /// + /// The number of bytes from the data to send. Use `Constants.UseArrayLengthForSend` to send the entire array. + public void SendMessage(byte[] data, int length = Constants.UseArrayLengthForSend); + + ///// + ///// This method sends a binary message over the WebSocket connection immediately without queueing. + ///// + //public Task SendBinaryImmediately(byte[] data, int length = Constants.UseArrayLengthForSend); + + /// + /// This method sends a text message over the WebSocket connection immediately without queueing. + /// + /// + /// The number of bytes from the data to send. Use `Constants.UseArrayLengthForSend` to send the entire array. + public Task SendMessageImmediately(byte[] data, int length = Constants.UseArrayLengthForSend); + #endregion + + #region Helpers + /// + /// Retrieves the connection state of the WebSocket + /// + /// Returns the connection state of the WebSocket + public WebSocketState State(); + + /// + /// Indicates whether the WebSocket is connected + /// + /// Returns true if the WebSocket is connected + public bool IsConnected(); + #endregion +} diff --git a/Deepgram/Clients/Interfaces/v2/ResponseEvent.cs b/Deepgram/Clients/Interfaces/v2/ResponseEvent.cs new file mode 100644 index 00000000..e2440366 --- /dev/null +++ b/Deepgram/Clients/Interfaces/v2/ResponseEvent.cs @@ -0,0 +1,16 @@ +// Copyright 2021-2024 Deepgram .NET 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 + +namespace Deepgram.Clients.Interfaces.v2; + +public class ResponseEvent +{ + public T? Response { get; } + + public ResponseEvent(T? response) + { + Response = response; + } +} + diff --git a/Deepgram/Clients/Listen/v1/REST/Client.cs b/Deepgram/Clients/Listen/v1/REST/Client.cs index 24c0effe..f4ae7f6d 100644 --- a/Deepgram/Clients/Listen/v1/REST/Client.cs +++ b/Deepgram/Clients/Listen/v1/REST/Client.cs @@ -5,6 +5,7 @@ using Deepgram.Models.Authenticate.v1; using Deepgram.Models.Listen.v1.REST; using Deepgram.Clients.Interfaces.v1; +using Deepgram.Abstractions.v1; namespace Deepgram.Clients.Listen.v1.REST; diff --git a/Deepgram/Clients/Listen/v1/WebSocket/Client.cs b/Deepgram/Clients/Listen/v1/WebSocket/Client.cs index b6c6c74b..c9529472 100644 --- a/Deepgram/Clients/Listen/v1/WebSocket/Client.cs +++ b/Deepgram/Clients/Listen/v1/WebSocket/Client.cs @@ -11,8 +11,16 @@ namespace Deepgram.Clients.Listen.v1.WebSocket; /// -/// Implements version 1 of the Live Client. +// *********** WARNING *********** +// Implements version 1 of the Listen WebSocket Client +// +// Deprecated: This class is deprecated. Use the `v2` of the client instead. +// This will be removed in a future release. +// +// This class is frozen and no new functionality will be added. +// *********** WARNING *********** /// +[Obsolete("Please use Deepgram.Clients.Listen.v2.WebSocket instead", false)] public class Client : IDisposable, IListenWebSocketClient { #region Fields diff --git a/Deepgram/Clients/Listen/v2/WebSocket/Client.cs b/Deepgram/Clients/Listen/v2/WebSocket/Client.cs new file mode 100644 index 00000000..096cea4b --- /dev/null +++ b/Deepgram/Clients/Listen/v2/WebSocket/Client.cs @@ -0,0 +1,658 @@ +// Copyright 2021-2024 Deepgram .NET 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 + +using Deepgram.Abstractions.v2; +using Deepgram.Models.Authenticate.v1; +using Deepgram.Models.Listen.v2.WebSocket; +using Common = Deepgram.Models.Common.v2.WebSocket; +using Deepgram.Clients.Interfaces.v2; + +namespace Deepgram.Clients.Listen.v2.WebSocket; + +/// +/// Implements version 2 of the Listen WebSocket Client. +/// +public class Client : AbstractWebSocketClient, IListenWebSocketClient +{ + #region Fields + private DateTime? _lastReceived = null; + private readonly SemaphoreSlim _mutexLastDatagram = new SemaphoreSlim(1, 1); + #endregion + + /// Required DeepgramApiKey + /// for HttpClient Configuration + public Client(string? apiKey = null, IDeepgramClientOptions? options = null) : base(apiKey, options) + { + Log.Verbose("ListenWSClient", "ENTER"); + Log.Debug("ListenWSClient", $"KeepAlive: {_deepgramClientOptions.KeepAlive}"); + Log.Debug("ListenWSClient", $"Autoflush: {_deepgramClientOptions.AutoFlushReplyDelta}"); + Log.Verbose("ListenWSClient", "LEAVE"); + } + + #region Event Handlers + /// + /// Fires when an event is received from the Deepgram API + /// + private event EventHandler? _metadataReceived; + private event EventHandler? _resultsReceived; + private event EventHandler? _utteranceEndReceived; + private event EventHandler? _speechStartedReceived; + #endregion + + /// + /// Connect to a Deepgram API Web Socket to begin transcribing audio + /// + /// Options to use when transcribing audio + /// The task object representing the asynchronous operation. + public async Task Connect(LiveSchema options, CancellationTokenSource? cancelToken = null, Dictionary? addons = null, + Dictionary? headers = null) + { + Log.Verbose("ListenWSClient.Connect", "ENTER"); + Log.Information("Connect", $"options:\n{JsonSerializer.Serialize(options, JsonSerializeOptions.DefaultOptions)}"); + Log.Debug("Connect", $"addons: {addons}"); + + try + { + var myURI = GetUri(_deepgramClientOptions, options, addons); + Log.Debug("Connect", $"uri: {myURI}"); + bool bConnected = await base.Connect(myURI.ToString(), cancelToken, headers); + if (!bConnected) + { + Log.Warning("Connect", "Connect failed"); + Log.Verbose("ListenWSClient.Connect", "LEAVE"); + return false; + } + + if (_deepgramClientOptions.KeepAlive) + { + Log.Debug("Connect", "Starting KeepAlive Thread..."); + StartKeepAliveBackgroundThread(); + } + + if (_deepgramClientOptions.AutoFlushReplyDelta > 0) + { + Log.Debug("Connect", "Starting AutoFlush Thread..."); + StartAutoFlushBackgroundThread(); + } + + Log.Debug("Connect", "Connect Succeeded"); + Log.Verbose("ListenWSClient.Connect", "LEAVE"); + + return true; + } + catch (TaskCanceledException ex) + { + Log.Debug("Connect", "Connect cancelled."); + Log.Verbose("Connect", $"Connect cancelled. Info: {ex}"); + Log.Verbose("ListenWSClient.Connect", "LEAVE"); + + return false; + } + catch (Exception ex) + { + Log.Error("Connect", $"{ex.GetType()} thrown {ex.Message}"); + Log.Verbose("Connect", $"Exception: {ex}"); + Log.Verbose("ListenWSClient.Connect", "LEAVE"); + throw; + } + + void StartKeepAliveBackgroundThread() => Task.Run(async () => await ProcessKeepAlive()); + + void StartAutoFlushBackgroundThread() => Task.Run(async () => await ProcessAutoFlush()); + } + + #region Subscribe Event + /// + /// Subscribe to an Open event from the Deepgram API + /// + /// + /// True if successful + public async Task Subscribe(EventHandler eventHandler) + { + // Create a new event handler that wraps the original one + EventHandler wrappedHandler = (sender, args) => + { + // Cast the event arguments to the desired type + var castedArgs = new OpenResponse(); + castedArgs.Copy(args); + if (castedArgs != null) + { + // Invoke the original event handler with the casted arguments + eventHandler(sender, castedArgs); + } + }; + + // Pass the new event handler to the base Subscribe method + return await base.Subscribe(wrappedHandler); + } + + /// + /// Subscribe to a Metadata event from the Deepgram API + /// + /// + /// True if successful + public async Task Subscribe(EventHandler eventHandler) + { + await _mutexSubscribe.WaitAsync(); + try + { + _metadataReceived += (sender, e) => eventHandler(sender, e); + } + finally + { + _mutexSubscribe.Release(); + } + return true; + } + + /// + /// Subscribe to a Results event from the Deepgram API + /// + /// True if successful + public async Task Subscribe(EventHandler eventHandler) + { + await _mutexSubscribe.WaitAsync(); + try + { + _resultsReceived += (sender, e) => eventHandler(sender, e); + } + finally + { + _mutexSubscribe.Release(); + } + return true; + } + + /// + /// Subscribe to an UtteranceEnd event from the Deepgram API + /// + /// True if successful + public async Task Subscribe(EventHandler eventHandler) + { + await _mutexSubscribe.WaitAsync(); + try + { + _utteranceEndReceived += (sender, e) => eventHandler(sender, e); + } + finally + { + _mutexSubscribe.Release(); + } + return true; + } + + /// + /// Subscribe to a SpeechStarted event from the Deepgram API + /// + /// True if successful + public async Task Subscribe(EventHandler eventHandler) + { + await _mutexSubscribe.WaitAsync(); + try + { + _speechStartedReceived += (sender, e) => eventHandler(sender, e); + } + finally + { + _mutexSubscribe.Release(); + } + return true; + } + + /// + /// Subscribe to an Close event from the Deepgram API + /// + /// + /// True if successful + public async Task Subscribe(EventHandler eventHandler) + { + // Create a new event handler that wraps the original one + EventHandler wrappedHandler = (sender, args) => + { + // Cast the event arguments to the desired type + var castedArgs = new CloseResponse(); + castedArgs.Copy(args); + if (castedArgs != null) + { + // Invoke the original event handler with the casted arguments + eventHandler(sender, castedArgs); + } + }; + + return await base.Subscribe(wrappedHandler); + } + + /// + /// Subscribe to an Error event from the Deepgram API + /// + /// + /// True if successful + public async Task Subscribe(EventHandler eventHandler) + { + // Create a new event handler that wraps the original one + EventHandler wrappedHandler = (sender, args) => + { + // Cast the event arguments to the desired type + var castedArgs = new ErrorResponse(); + castedArgs.Copy(args); + if (castedArgs != null) + { + // Invoke the original event handler with the casted arguments + eventHandler(sender, castedArgs); + } + }; + + return await base.Subscribe(wrappedHandler); + } + + /// + /// Subscribe to an Unhandled event from the Deepgram API + /// + /// + /// True if successful + public async Task Subscribe(EventHandler eventHandler) + { + // Create a new event handler that wraps the original one + EventHandler wrappedHandler = (sender, args) => + { + // Cast the event arguments to the desired type + var castedArgs = new UnhandledResponse(); + castedArgs.Copy(args); + if (castedArgs != null) + { + // Invoke the original event handler with the casted arguments + eventHandler(sender, castedArgs); + } + }; + + return await base.Subscribe(wrappedHandler); + } + #endregion + + #region Send Functions + /// + /// Sends a KeepAlive message to Deepgram + /// + public async Task SendKeepAlive() + { + Log.Debug("SendKeepAlive", "Sending KeepAlive Message Immediately..."); + byte[] data = Encoding.ASCII.GetBytes("{\"type\": \"KeepAlive\"}"); + await SendMessageImmediately(data); + } + + /// + /// Sends a Finalize message to Deepgram + /// + public async Task SendFinalize() + { + Log.Debug("SendFinalize", "Sending Finalize Message Immediately..."); + byte[] data = Encoding.ASCII.GetBytes("{\"type\": \"Finalize\"}"); + await SendMessageImmediately(data); + } + + /// + /// Sends a Close message to Deepgram + /// + public override async Task SendClose(bool nullByte = false) + { + if (_clientWebSocket == null || !IsConnected()) + { + Log.Warning("SendClose", "ClientWebSocket is null or not connected. Skipping..."); + return; + } + + Log.Debug("SendClose", "Sending Close Message Immediately..."); + if (nullByte) + { + // send a close to Deepgram + await _mutexSend.WaitAsync(_cancellationTokenSource.Token); + try + { + await _clientWebSocket.SendAsync(new ArraySegment(new byte[1] { 0 }), WebSocketMessageType.Binary, true, _cancellationTokenSource.Token) + .ConfigureAwait(false); + } + finally + { + _mutexSend.Release(); + } + return; + } + + byte[] data = Encoding.ASCII.GetBytes("{\"type\": \"CloseStream\"}"); + await SendMessageImmediately(data); + } + #endregion + + internal async Task ProcessKeepAlive() + { + Log.Verbose("ListenWSClient.ProcessKeepAlive", "ENTER"); + + try + { + while (true) + { + Log.Verbose("ProcessKeepAlive", "Waiting for KeepAlive..."); + await Task.Delay(5000, _cancellationTokenSource.Token); + + if (_cancellationTokenSource.Token.IsCancellationRequested) + { + Log.Information("ProcessKeepAlive", "KeepAliveThread cancelled"); + break; + } + if (!IsConnected()) + { + Log.Debug("ProcessAutoFlush", "WebSocket is not connected. Exiting..."); + break; + } + + await SendKeepAlive(); + } + + Log.Verbose("ProcessKeepAlive", "Exit"); + Log.Verbose("ListenWSClient.ProcessKeepAlive", "LEAVE"); + } + catch (TaskCanceledException ex) + { + Log.Debug("ProcessKeepAlive", "KeepAliveThread cancelled."); + Log.Verbose("ProcessKeepAlive", $"KeepAliveThread cancelled. Info: {ex}"); + Log.Verbose("ListenWSClient.ProcessKeepAlive", "LEAVE"); + } + catch (Exception ex) + { + Log.Error("ProcessKeepAlive", $"{ex.GetType()} thrown {ex.Message}"); + Log.Verbose("ProcessKeepAlive", $"Exception: {ex}"); + Log.Verbose("ListenWSClient.ProcessKeepAlive", "LEAVE"); + } + } + + internal async Task ProcessAutoFlush() + { + Log.Verbose("ListenWSClient.ProcessAutoFlush", "ENTER"); + + var diffTicks = TimeSpan.FromMilliseconds((double)_deepgramClientOptions.AutoFlushReplyDelta); + + try + { + while (true) + { + Log.Verbose("ProcessAutoFlush", "Waiting for AutoFlush..."); + await Task.Delay(Constants.DefaultFlushPeriodInMs, _cancellationTokenSource.Token); + + if (_cancellationTokenSource.Token.IsCancellationRequested) + { + Log.Information("ProcessAutoFlush", "ProcessAutoFlush cancelled"); + break; + } + if (!IsConnected()) + { + Log.Debug("ProcessAutoFlush", "WebSocket is not connected. Exiting..."); + break; + } + + await _mutexLastDatagram.WaitAsync(); + try + { + if (_lastReceived == null) + { + Log.Debug("ProcessAutoFlush", "No datagram received. Skipping..."); + continue; + } + + var deltaTicks = DateTime.Now - _lastReceived; + if (deltaTicks < diffTicks) + { + Log.Debug("ProcessAutoFlush", $"AutoFlush delta is less than threshold: {deltaTicks}. Skipping..."); + continue; + } + + await SendFinalize(); + _lastReceived = null; + } + finally + { + _mutexLastDatagram.Release(); + } + } + + Log.Verbose("ProcessAutoFlush", "Exit"); + Log.Verbose("ListenWSClient.ProcessAutoFlush", "LEAVE"); + } + catch (TaskCanceledException ex) + { + Log.Debug("ProcessAutoFlush", "ProcessAutoFlush cancelled."); + Log.Verbose("ProcessAutoFlush", $"ProcessAutoFlush cancelled. Info: {ex}"); + Log.Verbose("ListenWSClient.ProcessAutoFlush", "LEAVE"); + } + catch (Exception ex) + { + Log.Error("ProcessAutoFlush", $"{ex.GetType()} thrown {ex.Message}"); + Log.Verbose("ProcessAutoFlush", $"Exception: {ex}"); + Log.Verbose("ListenWSClient.ProcessAutoFlush", "LEAVE"); + } + } + + internal override void ProcessTextMessage(WebSocketReceiveResult result, MemoryStream ms) + { + Log.Verbose("ListenWSClient.ProcessTextMessage", "ENTER"); + + ms.Seek(0, SeekOrigin.Begin); + + var response = Encoding.UTF8.GetString(ms.ToArray()); + if (response == null) + { + Log.Warning("ProcessTextMessage", "Response is null"); + Log.Verbose("ListenWSClient.ProcessTextMessage", "LEAVE"); + return; + } + + try + { + Log.Verbose("ProcessTextMessage", $"raw response: {response}"); + var data = JsonDocument.Parse(response); + var val = Enum.Parse(typeof(ListenType), data.RootElement.GetProperty("type").GetString()!); + + Log.Verbose("ProcessTextMessage", $"Type: {val}"); + + + if (_deepgramClientOptions.InspectListenMessage()) + { + Log.Debug("ProcessTextMessage", "Call InspectMessage..."); + InspectMessage(val, data).Wait(); + } + + switch (val) + { + case ListenType.Open: + case ListenType.Close: + case ListenType.Error: + Log.Debug("ProcessTextMessage", "Calling base.ProcessTextMessage..."); + base.ProcessTextMessage(result, ms); + break; + case ListenType.Results: + var resultResponse = data.Deserialize(); + if (_resultsReceived == null) + { + Log.Debug("ProcessTextMessage", "_resultsReceived has no listeners"); + Log.Verbose("ProcessTextMessage", "LEAVE"); + return; + } + if (resultResponse == null) + { + Log.Warning("ProcessTextMessage", "ResultResponse is invalid"); + Log.Verbose("ProcessTextMessage", "LEAVE"); + return; + } + + Log.Debug("ProcessTextMessage", $"Invoking ResultsResponse. event: {resultResponse}"); + InvokeParallel(_resultsReceived, resultResponse); + break; + case ListenType.Metadata: + var metadataResponse = data.Deserialize(); + if (_metadataReceived == null) + { + Log.Debug("ProcessTextMessage", "_metadataReceived has no listeners"); + Log.Verbose("ProcessTextMessage", "LEAVE"); + return; + } + if (metadataResponse == null) + { + Log.Warning("ProcessTextMessage", "MetadataResponse is invalid"); + Log.Verbose("ProcessTextMessage", "LEAVE"); + return; + } + + Log.Debug("ProcessTextMessage", $"Invoking MetadataResponse. event: {metadataResponse}"); + InvokeParallel(_metadataReceived, metadataResponse); + break; + case ListenType.UtteranceEnd: + var utteranceEndResponse = data.Deserialize(); + if (_utteranceEndReceived == null) + { + Log.Debug("ProcessTextMessage", "_utteranceEndReceived has no listeners"); + Log.Verbose("ProcessTextMessage", "LEAVE"); + return; + } + if (utteranceEndResponse == null) + { + Log.Warning("ProcessTextMessage", "UtteranceEndResponse is invalid"); + Log.Verbose("ProcessTextMessage", "LEAVE"); + return; + } + + Log.Debug("ProcessTextMessage", $"Invoking UtteranceEndResponse. event: {utteranceEndResponse}"); + InvokeParallel(_utteranceEndReceived, utteranceEndResponse); + break; + case ListenType.SpeechStarted: + var speechStartedResponse = data.Deserialize(); + if (_speechStartedReceived == null) + { + Log.Debug("ProcessTextMessage", "_speechStartedReceived has no listeners"); + Log.Verbose("ProcessTextMessage", "LEAVE"); + return; + } + if (speechStartedResponse == null) + { + Log.Warning("ProcessTextMessage", "SpeechStartedResponse is invalid"); + Log.Verbose("ProcessTextMessage", "LEAVE"); + return; + } + + Log.Debug("ProcessTextMessage", $"Invoking SpeechStartedResponse. event: {speechStartedResponse}"); + InvokeParallel(_speechStartedReceived, speechStartedResponse); + break; + default: + Log.Debug("ProcessTextMessage", "Calling base.ProcessTextMessage..."); + base.ProcessTextMessage(result, ms); + break; + } + + Log.Debug("ProcessTextMessage", "Succeeded"); + Log.Verbose("ListenWSClient.ProcessTextMessage", "LEAVE"); + } + catch (JsonException ex) + { + Log.Error("ProcessTextMessage", $"{ex.GetType()} thrown {ex.Message}"); + Log.Verbose("ProcessTextMessage", $"Exception: {ex}"); + Log.Verbose("ListenWSClient.ProcessTextMessage", "LEAVE"); + } + catch (Exception ex) + { + Log.Error("ProcessTextMessage", $"{ex.GetType()} thrown {ex.Message}"); + Log.Verbose("ProcessTextMessage", $"Exception: {ex}"); + Log.Verbose("ListenWSClient.ProcessTextMessage", "LEAVE"); + } + } + + #region Helpers + /// + /// Get the URI for the WebSocket connection + /// + internal static Uri GetUri(IDeepgramClientOptions options, LiveSchema parameter, Dictionary? addons = null) + { + var propertyInfoList = parameter.GetType() + .GetProperties() + .Where(v => v.GetValue(parameter) is not null); + + var queryString = QueryParameterUtil.UrlEncode(parameter, propertyInfoList, addons); + + return new Uri($"{options.BaseAddress}/{UriSegments.LISTEN}?{queryString}"); + } + + private async Task InspectMessage(object type, JsonDocument data) + { + Log.Verbose("InspectMessage", "ENTER"); + + try + { + switch (type) + { + case ListenType.Results: + var resultResponse = data.Deserialize(); + if (resultResponse == null) + { + Log.Warning("InspectMessage", "ResultResponse is invalid"); + Log.Verbose("InspectMessage", "LEAVE"); + return; + } + + var sentence = resultResponse.Channel.Alternatives[0].Transcript; + + if (resultResponse.Channel.Alternatives.Count == 0 || sentence == "") { + Log.Verbose("InspectMessage", $"resultResponse has empty message"); + Log.Verbose("InspectMessage", "LEAVE"); + return; + } + + if (_deepgramClientOptions.AutoFlushReplyDelta > 0) + { + if ((bool)resultResponse.IsFinal) + { + var now = DateTime.Now; + Log.Debug("InspectMessage", $"AutoFlush IsFinal received. Time: {now}"); + await _mutexLastDatagram.WaitAsync(); + try + { + _lastReceived = null; + } + finally + { + _mutexLastDatagram.Release(); + } + } + else + { + var now = DateTime.Now; + Log.Debug("InspectMessage", $"AutoFlush Interim received. Time: {now}"); + await _mutexLastDatagram.WaitAsync(); + try + { + _lastReceived = now; + } + finally + { + _mutexLastDatagram.Release(); + } + } + } + break; + } + + Log.Debug("InspectMessage", "Succeeded"); + Log.Verbose("InspectMessage", "LEAVE"); + } + catch (JsonException ex) + { + Log.Error("InspectMessage", $"{ex.GetType()} thrown {ex.Message}"); + Log.Verbose("InspectMessage", $"Exception: {ex}"); + Log.Verbose("ListenWSClient.InspectMessage", "LEAVE"); + } + catch (Exception ex) + { + Log.Error("InspectMessage", $"{ex.GetType()} thrown {ex.Message}"); + Log.Verbose("InspectMessage", $"Exception: {ex}"); + Log.Verbose("ListenWSClient.InspectMessage", "LEAVE"); + } + } + #endregion +} diff --git a/Deepgram/Clients/Listen/v2/WebSocket/Constants.cs b/Deepgram/Clients/Listen/v2/WebSocket/Constants.cs new file mode 100644 index 00000000..7bddab15 --- /dev/null +++ b/Deepgram/Clients/Listen/v2/WebSocket/Constants.cs @@ -0,0 +1,15 @@ +// Copyright 2024 Deepgram .NET 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 + +namespace Deepgram.Clients.Listen.v2.WebSocket; + +/// +/// Headers of interest in the return values from the Deepgram Speak API. +/// +public static class Constants +{ + // Default flush period + public const int DefaultFlushPeriodInMs = 500; +} + diff --git a/Deepgram/Clients/Listen/v2/WebSocket/ResponseEvent.cs b/Deepgram/Clients/Listen/v2/WebSocket/ResponseEvent.cs new file mode 100644 index 00000000..2cd1a118 --- /dev/null +++ b/Deepgram/Clients/Listen/v2/WebSocket/ResponseEvent.cs @@ -0,0 +1,11 @@ +// Copyright 2021-2024 Deepgram .NET 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 + +namespace Deepgram.Clients.Listen.v2.WebSocket; + +public class ResponseEvent(T? response) : EventArgs +{ + public T? Response { get; set; } = response; +} + diff --git a/Deepgram/Clients/Listen/v2/WebSocket/UriSegments.cs b/Deepgram/Clients/Listen/v2/WebSocket/UriSegments.cs new file mode 100644 index 00000000..a51f2f8a --- /dev/null +++ b/Deepgram/Clients/Listen/v2/WebSocket/UriSegments.cs @@ -0,0 +1,12 @@ +// Copyright 2021-2024 Deepgram .NET 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 + +namespace Deepgram.Clients.Listen.v2.WebSocket; + +public static class UriSegments +{ + //using constants instead of inline value(magic strings) make consistence + //across SDK And Test Projects Simpler and Easier to change + public const string LISTEN = "listen"; +} diff --git a/Deepgram/Clients/Manage/v1/Client.cs b/Deepgram/Clients/Manage/v1/Client.cs index 990530f2..2f6e72b2 100644 --- a/Deepgram/Clients/Manage/v1/Client.cs +++ b/Deepgram/Clients/Manage/v1/Client.cs @@ -5,6 +5,7 @@ using Deepgram.Models.Authenticate.v1; using Deepgram.Models.Manage.v1; using Deepgram.Clients.Interfaces.v1; +using Deepgram.Abstractions.v1; namespace Deepgram.Clients.Manage.v1; diff --git a/Deepgram/Clients/SelfHosted/v1/Client.cs b/Deepgram/Clients/SelfHosted/v1/Client.cs index d3864562..5560e3d6 100644 --- a/Deepgram/Clients/SelfHosted/v1/Client.cs +++ b/Deepgram/Clients/SelfHosted/v1/Client.cs @@ -5,6 +5,7 @@ using Deepgram.Models.Authenticate.v1; using Deepgram.Models.SelfHosted.v1; using Deepgram.Clients.Interfaces.v1; +using Deepgram.Abstractions.v1; namespace Deepgram.Clients.SelfHosted.v1; diff --git a/Deepgram/Clients/Speak/v1/REST/Client.cs b/Deepgram/Clients/Speak/v1/REST/Client.cs index 114d07be..c554fc6d 100644 --- a/Deepgram/Clients/Speak/v1/REST/Client.cs +++ b/Deepgram/Clients/Speak/v1/REST/Client.cs @@ -5,6 +5,7 @@ using Deepgram.Models.Speak.v1.REST; using Deepgram.Models.Authenticate.v1; using Deepgram.Clients.Interfaces.v1; +using Deepgram.Abstractions.v1; namespace Deepgram.Clients.Speak.v1.REST; diff --git a/Deepgram/Clients/Speak/v1/WebSocket/Client.cs b/Deepgram/Clients/Speak/v1/WebSocket/Client.cs index 7403693a..97bc2979 100644 --- a/Deepgram/Clients/Speak/v1/WebSocket/Client.cs +++ b/Deepgram/Clients/Speak/v1/WebSocket/Client.cs @@ -11,8 +11,16 @@ namespace Deepgram.Clients.Speak.v1.WebSocket; /// -/// Implements version 1 of the Live Client. +// *********** WARNING *********** +// Implements version 1 of the Speak WebSocket Client +// +// Deprecated: This class is deprecated. Use the `v2` of the client instead. +// This will be removed in a future release. +// +// This class is frozen and no new functionality will be added. +// *********** WARNING *********** /// +[Obsolete("Please use Deepgram.Clients.Speak.v2.WebSocket instead", false)] public class Client : IDisposable, ISpeakWebSocketClient { #region Fields diff --git a/Deepgram/Clients/Speak/v1/WebSocket/ResponseEvent.cs b/Deepgram/Clients/Speak/v1/WebSocket/ResponseEvent.cs index 01dbcc7a..4741f887 100644 --- a/Deepgram/Clients/Speak/v1/WebSocket/ResponseEvent.cs +++ b/Deepgram/Clients/Speak/v1/WebSocket/ResponseEvent.cs @@ -4,8 +4,13 @@ namespace Deepgram.Clients.Speak.v1.WebSocket; -public class ResponseEvent(T? response) : EventArgs +public class ResponseEvent : EventArgs { - public T? Response { get; set; } = response; + public T? Response { get; } + + public ResponseEvent(T? response) + { + Response = response; + } } diff --git a/Deepgram/Clients/Speak/v2/WebSocket/Client.cs b/Deepgram/Clients/Speak/v2/WebSocket/Client.cs new file mode 100644 index 00000000..0677917a --- /dev/null +++ b/Deepgram/Clients/Speak/v2/WebSocket/Client.cs @@ -0,0 +1,711 @@ +// Copyright 2024 Deepgram .NET 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 + +using Deepgram.Abstractions.v2; +using Deepgram.Models.Authenticate.v1; +using Deepgram.Models.Speak.v2.WebSocket; +using Common = Deepgram.Models.Common.v2.WebSocket; +using Deepgram.Clients.Interfaces.v2; + +namespace Deepgram.Clients.Speak.v2.WebSocket; + +/// +/// Implements version 2 of the Speak WebSocket Client. +/// +public class Client : AbstractWebSocketClient, ISpeakWebSocketClient +{ + #region Fields + private DateTime? _lastReceived = null; + private int _flushCount = 0; + private readonly SemaphoreSlim _mutexLastDatagram = new SemaphoreSlim(1, 1); + #endregion + + /// Required DeepgramApiKey + /// for HttpClient Configuration + public Client(string? apiKey = null, IDeepgramClientOptions? options = null) : base(apiKey, options) + { + Log.Verbose("SpeakWSClient", "ENTER"); + Log.Debug("SpeakWSClient", $"Autoflush: {_deepgramClientOptions.AutoFlushSpeakDelta}"); + Log.Verbose("SpeakWSClient", "LEAVE"); + } + + #region Event Handlers + /// + /// Fires when an event is received from the Deepgram API + /// + private event EventHandler? _metadataReceived; + private event EventHandler? _flushedReceived; + private event EventHandler? _clearedReceived; + private event EventHandler? _audioReceived; + private event EventHandler? _warningReceived; + #endregion + + /// + /// Connect to a Deepgram API Web Socket to begin transcribing audio + /// + /// Options to use when transcribing audio + /// The task object representing the asynchronous operation. + public async Task Connect(SpeakSchema options, CancellationTokenSource? cancelToken = null, Dictionary? addons = null, + Dictionary? headers = null) + { + Log.Verbose("SpeakWSClient.Connect", "ENTER"); + Log.Information("Connect", $"options:\n{JsonSerializer.Serialize(options, JsonSerializeOptions.DefaultOptions)}"); + Log.Debug("Connect", $"addons: {addons}"); + + try + { + var myURI = GetUri(_deepgramClientOptions, options, addons); + Log.Debug("Connect", $"uri: {myURI}"); + bool bConnected = await base.Connect(myURI.ToString(), cancelToken, headers); + if (!bConnected) + { + Log.Warning("Connect", "Connect failed"); + Log.Verbose("SpeakWSClient.Connect", "LEAVE"); + return false; + } + + if (_deepgramClientOptions.AutoFlushSpeakDelta > 0) + { + Log.Debug("Connect", "Starting AutoFlush Thread..."); + StartAutoFlushBackgroundThread(); + } + + Log.Debug("Connect", "Connect Succeeded"); + Log.Verbose("SpeakWSClient.Connect", "LEAVE"); + + return true; + } + catch (TaskCanceledException ex) + { + Log.Debug("Connect", "Connect cancelled."); + Log.Verbose("Connect", $"Connect cancelled. Info: {ex}"); + Log.Verbose("SpeakWSClient.Connect", "LEAVE"); + + return false; + } + catch (Exception ex) + { + Log.Error("Connect", $"{ex.GetType()} thrown {ex.Message}"); + Log.Verbose("Connect", $"Excepton: {ex}"); + Log.Verbose("SpeakWSClient.Connect", "LEAVE"); + throw; + } + + void StartAutoFlushBackgroundThread() => Task.Run(async () => await ProcessAutoFlush()); + } + + #region Subscribe Event + /// + /// Subscribe to an Open event from the Deepgram API + /// + /// + /// True if successful + public async Task Subscribe(EventHandler eventHandler) + { + // Create a new event handler that wraps the original one + EventHandler wrappedHandler = (sender, args) => + { + // Cast the event arguments to the desired type + var castedArgs = new OpenResponse(); + castedArgs.Copy(args); + if (castedArgs != null) + { + // Invoke the original event handler with the casted arguments + eventHandler(sender, castedArgs); + } + }; + + // Pass the new event handler to the base Subscribe method + return await base.Subscribe(wrappedHandler); + } + + /// + /// Subscribe to a Metadata event from the Deepgram API + /// + /// + /// True if successful + public async Task Subscribe(EventHandler eventHandler) + { + await _mutexSubscribe.WaitAsync(); + try + { + _metadataReceived += (sender, e) => eventHandler(sender, e); + } + finally + { + _mutexSubscribe.Release(); + } + return true; + } + + /// + /// Subscribe to a Flushed event from the Deepgram API + /// + /// True if successful + public async Task Subscribe(EventHandler eventHandler) + { + await _mutexSubscribe.WaitAsync(); + try + { + _flushedReceived += (sender, e) => eventHandler(sender, e); + } + finally + { + _mutexSubscribe.Release(); + } + return true; + } + + /// + /// Subscribe to a Cleared event from the Deepgram API + /// + /// True if successful + public async Task Subscribe(EventHandler eventHandler) + { + await _mutexSubscribe.WaitAsync(); + try + { + _clearedReceived += (sender, e) => eventHandler(sender, e); + } + finally + { + _mutexSubscribe.Release(); + } + return true; + } + + /// + /// Subscribe to an Audio event from the Deepgram API + /// + /// True if successful + public async Task Subscribe(EventHandler eventHandler) + { + await _mutexSubscribe.WaitAsync(); + try + { + _audioReceived += (sender, e) => eventHandler(sender, e); + } + finally + { + _mutexSubscribe.Release(); + } + return true; + } + + /// + /// Subscribe to a Close event from the Deepgram API + /// + /// True if successful + public async Task Subscribe(EventHandler eventHandler) + { + // Create a new event handler that wraps the original one + EventHandler wrappedHandler = (sender, args) => + { + // Cast the event arguments to the desired type + var castedArgs = new CloseResponse(); + castedArgs.Copy(args); + if (castedArgs != null) + { + // Invoke the original event handler with the casted arguments + eventHandler(sender, castedArgs); + } + }; + + return await base.Subscribe(wrappedHandler); + } + + /// + /// Subscribe to an Warning event from the Deepgram API + /// + /// True if successful + public async Task Subscribe(EventHandler eventHandler) + { + await _mutexSubscribe.WaitAsync(); + try + { + _warningReceived += (sender, e) => eventHandler(sender, e); + } + finally + { + _mutexSubscribe.Release(); + } + return true; + } + + /// + /// Subscribe to an Error event from the Deepgram API + /// + /// True if successful + public async Task Subscribe(EventHandler eventHandler) + { + // Create a new event handler that wraps the original one + EventHandler wrappedHandler = (sender, args) => + { + // Cast the event arguments to the desired type + var castedArgs = new ErrorResponse(); + castedArgs.Copy(args); + if (castedArgs != null) + { + // Invoke the original event handler with the casted arguments + eventHandler(sender, castedArgs); + } + }; + + return await base.Subscribe(wrappedHandler); + } + + /// + /// Subscribe to an Unhandled event from the Deepgram API + /// + /// True if successful + public async Task Subscribe(EventHandler eventHandler) + { + // Create a new event handler that wraps the original one + EventHandler wrappedHandler = (sender, args) => + { + // Cast the event arguments to the desired type + var castedArgs = new UnhandledResponse(); + castedArgs.Copy(args); + if (castedArgs != null) + { + // Invoke the original event handler with the casted arguments + eventHandler(sender, castedArgs); + } + }; + + return await base.Subscribe(wrappedHandler); + } + #endregion + + #region Send Functions + /// + /// This method sends a string to Deepgram for conversion to audio. + /// This is a convenience functions that will wrap the provided string in a TextSource object. + /// NOTE: These should never use the SendImmediately methods because they would by-pass the flow of text messages queued. + /// + /// The string of text you want to be converted to audio. + public void SpeakWithText(string text) + { + TextSource textSource = new TextSource(text); + byte[] byteArray = Encoding.UTF8.GetBytes(textSource.ToString()); + SendMessage(byteArray); + } + + /// + /// This method Flushes the text buffer on Deepgram to be converted to audio. + /// NOTE: These should never use the SendImmediately methods because they would by-pass the flow of text messages queued. + /// + public void Flush() + { + ControlMessage controlMessage = new ControlMessage(Constants.Flush); + byte[] byteArray = Encoding.UTF8.GetBytes(controlMessage.ToString()); + SendMessage(byteArray); + } + + /// + /// This method Clears the text buffer on Deepgram to be converted to audio + /// NOTE: These should never use the SendImmediately methods because they would by-pass the flow of text messages queued. + /// + public void Clear() + { + ControlMessage controlMessage = new ControlMessage(Constants.Clear); + byte[] byteArray = Encoding.UTF8.GetBytes(controlMessage.ToString()); + SendMessage(byteArray); + } + + /// + /// This method tells Deepgram to initiate the close server-side. + /// NOTE: This is fine to use the SendImmediately methods because you want to shutdown the websocket ASAP. + /// + public void Close(bool nullByte = false) + { + SendClose(nullByte).Wait(); + } + + /// + /// This method sends a close message over the WebSocket connection. + /// NOTE: This is fine to use the SendImmediately methods because you want to shutdown the websocket ASAP. + /// + public override async Task SendClose(bool nullByte = false) + { + if (_clientWebSocket == null || !IsConnected()) + { + Log.Warning("SendClose", "ClientWebSocket is null or not connected. Skipping..."); + return; + } + + Log.Debug("SendClose", "Sending Close Message Immediately..."); + if (nullByte) + { + // send a close to Deepgram + await _mutexSend.WaitAsync(_cancellationTokenSource.Token); + try + { + await _clientWebSocket.SendAsync(new ArraySegment(new byte[1] { 0 }), WebSocketMessageType.Binary, true, _cancellationTokenSource.Token) + .ConfigureAwait(false); + } + finally + { + _mutexSend.Release(); + } + return; + } + + ControlMessage controlMessage = new ControlMessage(Constants.Close); + byte[] data = Encoding.UTF8.GetBytes(controlMessage.ToString()); + await SendMessageImmediately(data); + } + + /// + /// The SendMessage function needs to be overridden to handle the auto flush feature. + /// + public override void SendMessage(byte[] data, int length = Constants.UseArrayLengthForSend) + { + // auto flush + if (_deepgramClientOptions.InspectSpeakMessage()) + { + string type = GetMessageType(data); + Log.Debug("SendMessage", $"Inspecting Message: Sending {type}"); + switch (type) + { + case Constants.Flush: + IncrementCounter().Wait(); + break; + case Constants.Speak: + InspectMessage(); + break; + } + } + + // send message + EnqueueSendMessage(new WebSocketMessage(data, WebSocketMessageType.Text, length)); + } + + /// + /// We need to override the Send function to use the SendMessage function. This is different than STT + /// because we only deal in text messages for TTS where STT is sending binary (or audio) messages. + /// + public override void Send(byte[] data, int length = Constants.UseArrayLengthForSend) + { + SendMessage(data, length); + } + #endregion + + internal async Task ProcessAutoFlush() + { + Log.Verbose("LiveClient.ProcessAutoFlush", "ENTER"); + + var diffTicks = TimeSpan.FromMilliseconds((double)_deepgramClientOptions.AutoFlushSpeakDelta); + + try + { + while (true) + { + Log.Verbose("ProcessAutoFlush", "Waiting for AutoFlush..."); + await Task.Delay(Constants.DefaultFlushPeriodInMs, _cancellationTokenSource.Token); + + if (_cancellationTokenSource.Token.IsCancellationRequested) + { + Log.Information("ProcessAutoFlush", "ProcessAutoFlush cancelled"); + break; + } + if (!IsConnected()) + { + Log.Debug("ProcessAutoFlush", "WebSocket is not connected. Exiting..."); + return; + } + + await _mutexLastDatagram.WaitAsync(); + try + { + if (_lastReceived == null) + { + Log.Debug("ProcessAutoFlush", "No datagram received. Skipping..."); + continue; + } + + var deltaTicks = DateTime.Now - _lastReceived; + if (deltaTicks < diffTicks) + { + Log.Debug("ProcessAutoFlush", $"AutoFlush delta is less than threshold: {deltaTicks}. Skipping..."); + continue; + } + + Log.Debug("ProcessAutoFlush", $"AutoFlush delta exceeded threshold: {deltaTicks}. Skipping..."); + Flush(); + _lastReceived = null; + } + finally + { + _mutexLastDatagram.Release(); + } + } + + Log.Verbose("ProcessAutoFlush", "Exit"); + Log.Verbose("LiveClient.ProcessAutoFlush", "LEAVE"); + } + catch (TaskCanceledException ex) + { + Log.Debug("ProcessAutoFlush", "KeepAliveThread cancelled."); + Log.Verbose("ProcessAutoFlush", $"KeepAliveThread cancelled. Info: {ex}"); + Log.Verbose("LiveClient.ProcessAutoFlush", "LEAVE"); + } + catch (Exception ex) + { + Log.Error("ProcessAutoFlush", $"{ex.GetType()} thrown {ex.Message}"); + Log.Verbose("ProcessAutoFlush", $"Excepton: {ex}"); + Log.Verbose("LiveClient.ProcessAutoFlush", "LEAVE"); + } + } + + internal override void ProcessBinaryMessage(WebSocketReceiveResult result, MemoryStream ms) + { + try + { + Log.Debug("ProcessBinaryMessage", "Received WebSocketMessageType.Binary"); + + if (_audioReceived == null) + { + Log.Debug("ProcessBinaryMessage", "_audioReceived has no listeners"); + Log.Verbose("ProcessBinaryMessage", "LEAVE"); + return; + } + + var audioResponse = new AudioResponse() + { + Stream = ms + }; + + Log.Debug("ProcessBinaryMessage", "Invoking AudioResponse"); + InvokeParallel(_audioReceived, audioResponse); + } + catch (JsonException ex) + { + Log.Error("ProcessDataReceived", $"{ex.GetType()} thrown {ex.Message}"); + Log.Verbose("ProcessDataReceived", $"Exception: {ex}"); + Log.Verbose("SpeakWSClient.ProcessDataReceived", "LEAVE"); + } + catch (Exception ex) + { + Log.Error("ProcessDataReceived", $"{ex.GetType()} thrown {ex.Message}"); + Log.Verbose("ProcessDataReceived", $"Excepton: {ex}"); + Log.Verbose("SpeakWSClient.ProcessDataReceived", "LEAVE"); + } + } + + internal override void ProcessTextMessage(WebSocketReceiveResult result, MemoryStream ms) + { + try + { + Log.Debug("ProcessDataReceived", "Received WebSocketMessageType.Text"); + + var response = Encoding.UTF8.GetString(ms.ToArray()); + if (response == null) + { + Log.Warning("ProcessDataReceived", "Response is null"); + Log.Verbose("SpeakWSClient.ProcessDataReceived", "LEAVE"); + return; + } + + Log.Verbose("ProcessDataReceived", $"raw response: {response}"); + var data = JsonDocument.Parse(response); + var val = Enum.Parse(typeof(SpeakType), data.RootElement.GetProperty("type").GetString()!); + + Log.Verbose("ProcessDataReceived", $"Type: {val}"); + + switch (val) + { + case SpeakType.Open: + case SpeakType.Close: + case SpeakType.Error: + Log.Debug("ProcessTextMessage", "Calling base.ProcessTextMessage..."); + base.ProcessTextMessage(result, ms); + break; + case SpeakType.Metadata: + var metadataResponse = data.Deserialize(); + if (_metadataReceived == null) + { + Log.Debug("ProcessDataReceived", "_metadataReceived has no listeners"); + Log.Verbose("ProcessDataReceived", "LEAVE"); + return; + } + if (metadataResponse == null) + { + Log.Warning("ProcessDataReceived", "MetadataResponse is invalid"); + Log.Verbose("ProcessDataReceived", "LEAVE"); + return; + } + + Log.Debug("ProcessDataReceived", $"Invoking MetadataResponse. event: {metadataResponse}"); + InvokeParallel(_metadataReceived, metadataResponse); + break; + case SpeakType.Flushed: + var flushedResponse = data.Deserialize(); + if (_flushedReceived == null) + { + Log.Debug("ProcessDataReceived", "_flushedReceived has no listeners"); + Log.Verbose("ProcessDataReceived", "LEAVE"); + return; + } + if (flushedResponse == null) + { + Log.Warning("ProcessDataReceived", "FlushedResponse is invalid"); + Log.Verbose("ProcessDataReceived", "LEAVE"); + return; + } + + // auto flush + if (_deepgramClientOptions.InspectSpeakMessage()) + { + DecrementCounter().Wait(); + } + + Log.Debug("ProcessDataReceived", $"Invoking FlushedResponse. event: {flushedResponse}"); + InvokeParallel(_flushedReceived, flushedResponse); + break; + case SpeakType.Cleared: + var clearResponse = data.Deserialize(); + if (_clearedReceived == null) + { + Log.Debug("ProcessDataReceived", "_clearedReceived has no listeners"); + Log.Verbose("ProcessDataReceived", "LEAVE"); + return; + } + if (clearResponse == null) + { + Log.Warning("ProcessDataReceived", "ClearedResponse is invalid"); + Log.Verbose("ProcessDataReceived", "LEAVE"); + return; + } + + Log.Debug("ProcessDataReceived", $"Invoking ClearedResponse. event: {clearResponse}"); + InvokeParallel(_clearedReceived, clearResponse); + break; + case SpeakType.Warning: + var warningResponse = data.Deserialize(); + if (_warningReceived == null) + { + Log.Debug("ProcessDataReceived", "_warningReceived has no listeners"); + Log.Verbose("ProcessDataReceived", "LEAVE"); + return; + } + if (warningResponse == null) + { + Log.Warning("ProcessDataReceived", "WarningResponse is invalid"); + Log.Verbose("ProcessDataReceived", "LEAVE"); + return; + } + + Log.Debug("ProcessDataReceived", $"Invoking WarningResponse. event: {warningResponse}"); + InvokeParallel(_warningReceived, warningResponse); + break; + default: + Log.Debug("ProcessTextMessage", "Calling base.ProcessTextMessage..."); + base.ProcessTextMessage(result, ms); + break; + } + } + catch (JsonException ex) + { + Log.Error("ProcessDataReceived", $"{ex.GetType()} thrown {ex.Message}"); + Log.Verbose("ProcessDataReceived", $"Excepton: {ex}"); + Log.Verbose("SpeakWSClient.ProcessDataReceived", "LEAVE"); + } + catch (Exception ex) + { + Log.Error("ProcessDataReceived", $"{ex.GetType()} thrown {ex.Message}"); + Log.Verbose("ProcessDataReceived", $"Excepton: {ex}"); + Log.Verbose("SpeakWSClient.ProcessDataReceived", "LEAVE"); + } + } + + #region Helpers + /// + /// Get the URI for the WebSocket connection + /// + internal static Uri GetUri(IDeepgramClientOptions options, SpeakSchema parameter, Dictionary? addons = null) + { + var propertyInfoList = parameter.GetType() + .GetProperties() + .Where(v => v.GetValue(parameter) is not null); + + var queryString = QueryParameterUtil.UrlEncode(parameter, propertyInfoList, addons); + + return new Uri($"{options.BaseAddress}/{UriSegments.SPEAK}?{queryString}"); + } + + private async void InspectMessage() + { + Log.Verbose("InspectMessage", "ENTER"); + + if (_deepgramClientOptions.AutoFlushSpeakDelta > 0) + { + var now = DateTime.Now; + Log.Debug("InspectMessage", $"AutoFlush last received. Time: {now}"); + await _mutexLastDatagram.WaitAsync(); + try + { + _lastReceived = now; + } + finally + { + _mutexLastDatagram.Release(); + } + } + + Log.Debug("InspectMessage", "Succeeded"); + Log.Verbose("InspectMessage", "LEAVE"); + } + + private async Task DecrementCounter() + { + await _mutexLastDatagram.WaitAsync(); + try + { + _flushCount -= 1; + Log.Debug("DecrementCounter", $"Decrement Flush count: {_flushCount}"); + } + finally + { + _mutexLastDatagram.Release(); + } + + return true; + } + + private async Task IncrementCounter() + { + await _mutexLastDatagram.WaitAsync(); + try + { + _flushCount += 1; + Log.Debug("IncrementCounter", $"Increment Flush count: {_flushCount}"); + } + finally + { + _mutexLastDatagram.Release(); + } + + return true; + } + + internal string GetMessageType(byte[] msg) + { + // Convert the byte array to a string + string response = Encoding.UTF8.GetString(msg); + if (response == null) + { + return ""; + } + + Log.Verbose("ProcessDataReceived", $"raw response: {response}"); + var data = JsonDocument.Parse(response); + + string val = data.RootElement.GetProperty("type").GetString() ?? ""; + Log.Debug("ProcessDataReceived", $"Type: {val}"); + + return val; + } + #endregion +} diff --git a/Deepgram/Clients/Speak/v2/WebSocket/Constants.cs b/Deepgram/Clients/Speak/v2/WebSocket/Constants.cs new file mode 100644 index 00000000..783d2c6f --- /dev/null +++ b/Deepgram/Clients/Speak/v2/WebSocket/Constants.cs @@ -0,0 +1,28 @@ +// Copyright 2024 Deepgram .NET 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 + +namespace Deepgram.Clients.Speak.v2.WebSocket; + +/// +/// Headers of interest in the return values from the Deepgram Speak API. +/// +public static class Constants +{ + // WS buffer size + public const int BufferSize = 1024 * 16; + public const int UseArrayLengthForSend = -1; + + // Default timeout for connect/disconnect + public const int DefaultConnectTimeout = 5000; + public const int DefaultDisconnectTimeout = 5000; + + public const int DefaultFlushPeriodInMs = 500; + + // user message types + public const string Speak = "Speak"; + public const string Flush = "Flush"; + public const string Clear = "Clear"; + public const string Close = "Close"; +} + diff --git a/Deepgram/Clients/Speak/v2/WebSocket/ResponseEvent.cs b/Deepgram/Clients/Speak/v2/WebSocket/ResponseEvent.cs new file mode 100644 index 00000000..b4e249df --- /dev/null +++ b/Deepgram/Clients/Speak/v2/WebSocket/ResponseEvent.cs @@ -0,0 +1,16 @@ +// Copyright 2024 Deepgram .NET 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 + +namespace Deepgram.Clients.Speak.v2.WebSocket; + +public class ResponseEvent : EventArgs +{ + public T? Response { get; } + + public ResponseEvent(T? response) + { + Response = response; + } +} + diff --git a/Deepgram/Clients/Speak/v2/WebSocket/UriSegments.cs b/Deepgram/Clients/Speak/v2/WebSocket/UriSegments.cs new file mode 100644 index 00000000..cc06799e --- /dev/null +++ b/Deepgram/Clients/Speak/v2/WebSocket/UriSegments.cs @@ -0,0 +1,12 @@ +// Copyright 2024 Deepgram .NET 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 + +namespace Deepgram.Clients.Speak.v2.WebSocket; + +public static class UriSegments +{ + //using constants instead of inline value(magic strings) make consistence + //across SDK And Test Projects Simpler and Easier to change + public const string SPEAK = "speak"; +} diff --git a/Deepgram/GlobalUsings.cs b/Deepgram/GlobalUsings.cs index d3864d06..998aa8cb 100644 --- a/Deepgram/GlobalUsings.cs +++ b/Deepgram/GlobalUsings.cs @@ -2,7 +2,6 @@ // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT -global using System.Collections.Concurrent; global using System.Net.Http.Headers; global using System.Net.WebSockets; global using System.Reflection; @@ -12,7 +11,6 @@ global using System.Text.RegularExpressions; global using System.Threading.Channels; global using System.Web; -global using Deepgram.Abstractions; global using Deepgram.Constants; global using Deepgram.Logger; global using Deepgram.Utilities; diff --git a/Deepgram/ListenWebSocketClient.cs b/Deepgram/ListenWebSocketClient.cs index 09d69229..704bc27d 100644 --- a/Deepgram/ListenWebSocketClient.cs +++ b/Deepgram/ListenWebSocketClient.cs @@ -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 -using Deepgram.Clients.Listen.v1.WebSocket; +using Deepgram.Clients.Listen.v2.WebSocket; using Deepgram.Models.Authenticate.v1; namespace Deepgram; diff --git a/Deepgram/Models/Common/v2/WebSocket/CloseResponse.cs b/Deepgram/Models/Common/v2/WebSocket/CloseResponse.cs new file mode 100644 index 00000000..d34245eb --- /dev/null +++ b/Deepgram/Models/Common/v2/WebSocket/CloseResponse.cs @@ -0,0 +1,37 @@ +// Copyright 2024 Deepgram .NET 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 + +namespace Deepgram.Models.Common.v2.WebSocket; + +public record CloseResponse +{ + /// + /// Close event type. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("type")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public WebSocketType? Type { get; set; } = WebSocketType.Close; + + /// + /// Copy method to copy the object + /// + public void Copy(CloseResponse other) + { + if (other is null) + { + return; + } + + Type = other.Type; + } + + /// + /// Override ToString method to serialize the object + /// + public override string ToString() + { + return Regex.Unescape(JsonSerializer.Serialize(this, JsonSerializeOptions.DefaultOptions)); + } +} diff --git a/Deepgram/Models/Common/v2/WebSocket/ErrorResponse.cs b/Deepgram/Models/Common/v2/WebSocket/ErrorResponse.cs new file mode 100644 index 00000000..3ca5e705 --- /dev/null +++ b/Deepgram/Models/Common/v2/WebSocket/ErrorResponse.cs @@ -0,0 +1,61 @@ +// Copyright 2024 Deepgram .NET 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 + +namespace Deepgram.Models.Common.v2.WebSocket; + +public record ErrorResponse +{ + /// + /// Error Description + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("description")] + public string? Description { get; set; } = ""; + + /// + /// Error Message + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("message")] + public string? Message { get; set; } = ""; + + /// + /// Error Variant + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("variant")] + public string? Variant { get; set; } = ""; + + /// + /// Error event type. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("type")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public WebSocketType? Type { get; set; } = WebSocketType.Error; + + /// + /// Copy method to copy the object + /// + public void Copy(ErrorResponse other) + { + if (other is null) + { + return; + } + + Description = other.Description; + Message = other.Message; + Variant = other.Variant; + Type = other.Type; + } + + /// + /// Override ToString method to serialize the object + /// + public override string ToString() + { + return Regex.Unescape(JsonSerializer.Serialize(this, JsonSerializeOptions.DefaultOptions)); + } +} diff --git a/Deepgram/Models/Common/v2/WebSocket/OpenResponse.cs b/Deepgram/Models/Common/v2/WebSocket/OpenResponse.cs new file mode 100644 index 00000000..67ad422e --- /dev/null +++ b/Deepgram/Models/Common/v2/WebSocket/OpenResponse.cs @@ -0,0 +1,37 @@ +// Copyright 2024 Deepgram .NET 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 + +namespace Deepgram.Models.Common.v2.WebSocket; + +public record OpenResponse +{ + /// + /// Open event type. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("type")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public WebSocketType? Type { get; set; } = WebSocketType.Open; + + /// + /// Copy method to copy the object + /// + public void Copy(OpenResponse other) + { + if (other is null) + { + return; + } + + Type = other.Type; + } + + /// + /// Override ToString method to serialize the object + /// + public override string ToString() + { + return Regex.Unescape(JsonSerializer.Serialize(this, JsonSerializeOptions.DefaultOptions)); + } +} diff --git a/Deepgram/Models/Common/v2/WebSocket/UnhandledResponse.cs b/Deepgram/Models/Common/v2/WebSocket/UnhandledResponse.cs new file mode 100644 index 00000000..502fa036 --- /dev/null +++ b/Deepgram/Models/Common/v2/WebSocket/UnhandledResponse.cs @@ -0,0 +1,45 @@ +// Copyright 2024 Deepgram .NET 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 + +namespace Deepgram.Models.Common.v2.WebSocket; + +public record UnhandledResponse +{ + /// + /// Raw JSON + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("raw")] + public string? Raw { get; set; } = ""; + + /// + /// Unhandled event type. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("type")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public WebSocketType? Type { get; set; } = WebSocketType.Unhandled; + + /// + /// Copy method to copy the object + /// + public void Copy(UnhandledResponse other) + { + if (other == null) + { + return; + } + + Raw = other.Raw; + Type = other.Type; + } + + /// + /// Override ToString method to serialize the object + /// + public override string ToString() + { + return Regex.Unescape(JsonSerializer.Serialize(this, JsonSerializeOptions.DefaultOptions)); + } +} diff --git a/Deepgram/Models/Common/v2/WebSocket/WebSocketType.cs b/Deepgram/Models/Common/v2/WebSocket/WebSocketType.cs new file mode 100644 index 00000000..1825b190 --- /dev/null +++ b/Deepgram/Models/Common/v2/WebSocket/WebSocketType.cs @@ -0,0 +1,13 @@ +// Copyright 2021-2024 Deepgram .NET 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 + +namespace Deepgram.Models.Common.v2.WebSocket; + +public enum WebSocketType +{ + Open, + Close, + Unhandled, + Error, +} diff --git a/Deepgram/Models/Listen/v2/WebSocket/Alternative.cs b/Deepgram/Models/Listen/v2/WebSocket/Alternative.cs new file mode 100644 index 00000000..0a59fb61 --- /dev/null +++ b/Deepgram/Models/Listen/v2/WebSocket/Alternative.cs @@ -0,0 +1,43 @@ +// Copyright 2021-2024 Deepgram .NET 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 + +namespace Deepgram.Models.Listen.v2.WebSocket; + +public record Alternative +{ + /// + /// Single-string transcript containing what the model hears in this channel of audio. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("transcript")] + public string? Transcript { get; set; } + /// + /// Value between 0 and 1 indicating the model's relative confidence in this transcript. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("confidence")] + public double? Confidence { get; set; } + + /// + /// ReadOnly List of objects. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("words")] + public IReadOnlyList? Words { get; set; } + + /// + /// ReadOnlyList of Languages Detected + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("languages")] + public IReadOnlyList? Languages { get; set; } + + /// + /// Override ToString method to serialize the object + /// + public override string ToString() + { + return Regex.Unescape(JsonSerializer.Serialize(this, JsonSerializeOptions.DefaultOptions)); + } +} diff --git a/Deepgram/Models/Listen/v2/WebSocket/Average.cs b/Deepgram/Models/Listen/v2/WebSocket/Average.cs new file mode 100644 index 00000000..3809bb74 --- /dev/null +++ b/Deepgram/Models/Listen/v2/WebSocket/Average.cs @@ -0,0 +1,30 @@ +// Copyright 2021-2024 Deepgram .NET 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 + +namespace Deepgram.Models.Listen.v2.WebSocket; + +public record Average +{ + /// + /// Sentiment: Positive, Negative, or Neutral. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("sentiment")] + public string? Sentiment { get; set; } + + /// + /// Sentiment score. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("sentiment_score")] + public double? SentimentScore { get; set; } + + /// + /// Override ToString method to serialize the object + /// + public override string ToString() + { + return Regex.Unescape(JsonSerializer.Serialize(this, JsonSerializeOptions.DefaultOptions)); + } +} diff --git a/Deepgram/Models/Listen/v2/WebSocket/Channel.cs b/Deepgram/Models/Listen/v2/WebSocket/Channel.cs new file mode 100644 index 00000000..51fab15d --- /dev/null +++ b/Deepgram/Models/Listen/v2/WebSocket/Channel.cs @@ -0,0 +1,30 @@ +// Copyright 2021-2024 Deepgram .NET 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 + +namespace Deepgram.Models.Listen.v2.WebSocket; + +public record Channel +{ + /// + /// ReadOnlyList of objects. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("alternatives")] + public IReadOnlyList? Alternatives { get; set; } + + /// + /// ReadOnlyList of Search objects. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("search")] + public IReadOnlyList? Search { get; set; } + + /// + /// Override ToString method to serialize the object + /// + public override string ToString() + { + return Regex.Unescape(JsonSerializer.Serialize(this, JsonSerializeOptions.DefaultOptions)); + } +} diff --git a/Deepgram/Models/Listen/v2/WebSocket/CloseResponse.cs b/Deepgram/Models/Listen/v2/WebSocket/CloseResponse.cs new file mode 100644 index 00000000..ec7c50a8 --- /dev/null +++ b/Deepgram/Models/Listen/v2/WebSocket/CloseResponse.cs @@ -0,0 +1,11 @@ +// Copyright 2024 Deepgram .NET 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 + +namespace Deepgram.Models.Listen.v2.WebSocket; + +using Common = Deepgram.Models.Common.v2.WebSocket; + +public record CloseResponse : Common.CloseResponse +{ +} diff --git a/Deepgram/Models/Listen/v2/WebSocket/ErrorResponse.cs b/Deepgram/Models/Listen/v2/WebSocket/ErrorResponse.cs new file mode 100644 index 00000000..e90e7cf3 --- /dev/null +++ b/Deepgram/Models/Listen/v2/WebSocket/ErrorResponse.cs @@ -0,0 +1,11 @@ +// Copyright 2024 Deepgram .NET 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 + +namespace Deepgram.Models.Listen.v2.WebSocket; + +using Common = Deepgram.Models.Common.v2.WebSocket; + +public record ErrorResponse : Common.ErrorResponse +{ +} diff --git a/Deepgram/Models/Listen/v2/WebSocket/Hit.cs b/Deepgram/Models/Listen/v2/WebSocket/Hit.cs new file mode 100644 index 00000000..4f6dedc7 --- /dev/null +++ b/Deepgram/Models/Listen/v2/WebSocket/Hit.cs @@ -0,0 +1,45 @@ +// Copyright 2021-2024 Deepgram .NET 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 + +namespace Deepgram.Models.Listen.v2.WebSocket; + +public record Hit +{ + /// + /// Value between 0 and 1 that indicates the model's relative confidence in this hit. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("confidence")] + public double? Confidence { get; set; } + + + /// + /// Offset in seconds from the start of the audio to where the hit ends. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("end")] + public decimal? End { get; set; } + + /// + /// Transcript that corresponds to the time between start and end. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("snippet")] + public string? Snippet { get; set; } + + /// + /// Offset in seconds from the start of the audio to where the hit occurs. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("start")] + public decimal? Start { get; set; } + + /// + /// Override ToString method to serialize the object + /// + public override string ToString() + { + return Regex.Unescape(JsonSerializer.Serialize(this, JsonSerializeOptions.DefaultOptions)); + } +} diff --git a/Deepgram/Models/Listen/v2/WebSocket/ListenType.cs b/Deepgram/Models/Listen/v2/WebSocket/ListenType.cs new file mode 100644 index 00000000..5f2744d2 --- /dev/null +++ b/Deepgram/Models/Listen/v2/WebSocket/ListenType.cs @@ -0,0 +1,19 @@ +// Copyright 2021-2024 Deepgram .NET 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 + +namespace Deepgram.Models.Listen.v2.WebSocket; + +using Deepgram.Models.Common.v2.WebSocket; + +public enum ListenType +{ + Open = WebSocketType.Open, + Close = WebSocketType.Close, + Unhandled = WebSocketType.Unhandled, + Error = WebSocketType.Error, + Metadata, + Results, + UtteranceEnd, + SpeechStarted, +} diff --git a/Deepgram/Models/Listen/v2/WebSocket/LiveSchema.cs b/Deepgram/Models/Listen/v2/WebSocket/LiveSchema.cs new file mode 100644 index 00000000..67fe0e82 --- /dev/null +++ b/Deepgram/Models/Listen/v2/WebSocket/LiveSchema.cs @@ -0,0 +1,262 @@ +// Copyright 2021-2024 Deepgram .NET 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 + +namespace Deepgram.Models.Listen.v2.WebSocket; + +public class LiveSchema +{ + + /// + /// Number of transcripts to return per request + /// + /// Default is 1 + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("alternatives")] + public int? Alternatives { get; set; } + + /// + /// CallBack allows you to have your submitted audio processed asynchronously. + /// + /// default is null + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("callback")] + public string? CallBack { get; set; } + + /// + /// Enables callback method + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("callback_method")] + public bool? CallbackMethod { get; set; } + + /// + /// Channels allows you to specify the number of independent audio channels your submitted audio contains. + /// Used when the Encoding feature is also being used to submit streaming raw audio + /// + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("channels")] + public int? Channels { get; set; } + + /// + /// Diarize recognizes speaker changes and assigns a speaker to each word in the transcript. + /// + /// default is false + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("diarize")] + public bool? Diarize { get; set; } + + // + /// + /// default is null, only applies if Diarize is set to true + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("diarize_version")] + public string? DiarizeVersion { get; set; } + + /// + /// Dictation is a feature of Deepgram’s Speech-to-Text API that converts spoken dictation commands into their corresponding punctuation marks. + /// + /// default is false + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("dictation")] + public bool? Dictation { get; set; } + + /// + /// Encoding allows you to specify the expected encoding of your submitted audio. + /// + /// supported encodings + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("encoding")] + public string? Encoding { get; set; } + + /// + /// Endpointing returns transcripts when pauses in speech are detected. + /// + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("endpointing")] + public string? EndPointing { get; set; } + + /// + /// Deepgram’s Extra Metadata feature allows you to attach arbitrary key-value pairs to your API requests that are attached to the API response for usage in downstream processing. + /// Extra metadata is limited to 2048 characters per key-value pair. + /// + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("extra")] + public Dictionary? Extra { get; set; } + + /// + /// Whether to include words like "uh" and "um" in transcription output. + /// + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("filler_words")] + public bool? FillerWords { get; set; } + + /// + /// Interim Results provides preliminary results for streaming audio to solve the need for immediate results combined with high levels of accuracy. + /// + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("interim_results")] + public bool? InterimResults { get; set; } + + /// + /// Keywords can boost or suppress specialized terminology. + /// + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("keywords")] + public List? Keywords { get; set; } + + /// + /// Primary spoken language of submitted audio + /// + /// default value is 'en' + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("language")] + public string? Language { get; set; } + + /// + /// AI model used to process submitted audio + /// + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("model")] + public string? Model { get; set; } + + /// + /// Multichannel transcribes each channel in submitted audio independently. + /// + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("multichannel")] + public bool? MultiChannel { get; set; } + + /// + /// Enables No Delay + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("no_delay")] + public bool? NoDelay { get; set; } + + /// + /// Numerals converts numbers from written format to numerical format. + /// + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("numerals")] + public bool? Numerals { get; set; } + + /// + /// Profanity Filter looks for recognized profanity and converts it to the nearest recognized + /// non-profane word or removes it from the transcript completely. + /// + /// for use with base model tier only + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("profanity_filter")] + public bool? ProfanityFilter { get; set; } + + /// + /// Adds punctuation and capitalization to transcript + /// + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("punctuate")] + public bool? Punctuate { get; set; } + + /// + /// Indicates whether to redact sensitive information, replacing redacted content with asterisks (*). Can send multiple instances in query string (for example, redact=pci&redact=numbers). + /// + /// default is List("false") + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("redact")] + public List? Redact { get; set; } + + /// + /// Find and Replace searches for terms or phrases in submitted audio and replaces them. + /// + /// default is null + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("replace")] + public List? Replace { get; set; } + + /// + /// Sample Rate allows you to specify the sample rate of your submitted audio. + /// + /// only applies when Encoding has a value + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("sample_rate")] + public int? SampleRate { get; set; } + + /// + /// Search searches for terms or phrases in submitted audio. + /// + /// default is null + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("search")] + public List? Search { get; set; } + + /// + /// Smart Format formats transcripts to improve readability. + /// + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("smart_format")] + public bool? SmartFormat { get; set; } + + /// + /// Tagging allows you to label your requests with one or more tags in a list,for the purpose of identification during usage reporting. + /// + /// Default is a null + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("tag")] + public List? Tag { get; set; } + + /// + /// Indicates how long Deepgram will wait to send a {"type": "UtteranceEnd"} message after a word has been transcribed + /// + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("utterance_end_ms")] + public string? UtteranceEnd { get; set; } + + /// + /// Enables voice activity detection (VAD) events + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("vad_events")] + public bool? VadEvents { get; set; } + + /// + /// Version of the model to use. + /// + /// default value is "latest" + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("version")] + public string? Version { get; set; } + + /// + /// Override ToString method to serialize the object + /// + public override string ToString() + { + return Regex.Unescape(JsonSerializer.Serialize(this, JsonSerializeOptions.DefaultOptions)); + } +} diff --git a/Deepgram/Models/Listen/v2/WebSocket/Metadata.cs b/Deepgram/Models/Listen/v2/WebSocket/Metadata.cs new file mode 100644 index 00000000..9ace5968 --- /dev/null +++ b/Deepgram/Models/Listen/v2/WebSocket/Metadata.cs @@ -0,0 +1,46 @@ +// Copyright 2021-2024 Deepgram .NET 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 + +namespace Deepgram.Models.Listen.v2.WebSocket; + +public record MetaData +{ + /// + /// The request ID is a unique identifier for the request. It is useful for troubleshooting and support. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("request_id")] + public string? RequestId { get; set; } + + /// + /// Model UUID + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("model_uuid")] + public string? ModelUUID { get; set; } + + /// + /// IReadonlyDictionary of + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("model_info")] + public ModelInfo? ModelInfo { get; set; } + + /// + /// Deepgram’s Extra Metadata feature allows you to attach arbitrary key-value pairs to your API requests that are attached to the API response for usage in downstream processing. + /// Extra metadata is limited to 2048 characters per key-value pair. + /// + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("extra")] + public Dictionary? Extra { get; set; } + + /// + /// Override ToString method to serialize the object + /// + public override string ToString() + { + return Regex.Unescape(JsonSerializer.Serialize(this, JsonSerializeOptions.DefaultOptions)); + } +} diff --git a/Deepgram/Models/Listen/v2/WebSocket/MetadataResponse.cs b/Deepgram/Models/Listen/v2/WebSocket/MetadataResponse.cs new file mode 100644 index 00000000..170aea34 --- /dev/null +++ b/Deepgram/Models/Listen/v2/WebSocket/MetadataResponse.cs @@ -0,0 +1,89 @@ +// Copyright 2021-2024 Deepgram .NET 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 + +namespace Deepgram.Models.Listen.v2.WebSocket; + +public record MetadataResponse +{ + /// + /// Channel count + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("channels")] + public int? Channels { get; set; } + + /// + /// Created date/time + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("created")] + public DateTime? Created { get; set; } + + /// + /// Duration of the audio + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("duration")] + public double? Duration { get; set; } + + /// + /// Model Information + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("model_info")] + public IReadOnlyDictionary? ModelInfo { get; set; } + + /// + /// Models used containing UUIDs + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("models")] + public IReadOnlyList? Models { get; set; } + + /// + /// Request ID is a unique identifier for the request. It is useful for troubleshooting and support. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("request_id")] + public string? RequestId { get; set; } + + /// + /// Sha256 information + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("sha256")] + public string? Sha256 { get; set; } + + /// + /// (Obsolete?) his field is only present if the request was made with a transaction key. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("transaction_key")] + public string? TransactionKey { get; set; } + + /// + /// Metadata event type. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("type")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public ListenType? Type { get; set; } = ListenType.Metadata; + + /// + /// Deepgram’s Extra Metadata feature allows you to attach arbitrary key-value pairs to your API requests that are attached to the API response for usage in downstream processing. + /// Extra metadata is limited to 2048 characters per key-value pair. + /// + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("extra")] + public Dictionary? Extra { get; set; } + + /// + /// Override ToString method to serialize the object + /// + public override string ToString() + { + return Regex.Unescape(JsonSerializer.Serialize(this, JsonSerializeOptions.DefaultOptions)); + } +} diff --git a/Deepgram/Models/Listen/v2/WebSocket/ModelInfo.cs b/Deepgram/Models/Listen/v2/WebSocket/ModelInfo.cs new file mode 100644 index 00000000..4bd44eb6 --- /dev/null +++ b/Deepgram/Models/Listen/v2/WebSocket/ModelInfo.cs @@ -0,0 +1,37 @@ +// Copyright 2021-2024 Deepgram .NET 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 + +namespace Deepgram.Models.Listen.v2.WebSocket; + +public record ModelInfo +{ + /// + /// Architecture of the model + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("arch")] + public string? Arch { get; set; } + + /// + /// Name of the model + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("name")] + public string? Name { get; set; } + + /// + /// Version of the model + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("version")] + public string? Version { get; set; } + + /// + /// Override ToString method to serialize the object + /// + public override string ToString() + { + return Regex.Unescape(JsonSerializer.Serialize(this, JsonSerializeOptions.DefaultOptions)); + } +} diff --git a/Deepgram/Models/Listen/v2/WebSocket/OpenResponse.cs b/Deepgram/Models/Listen/v2/WebSocket/OpenResponse.cs new file mode 100644 index 00000000..e0e65a60 --- /dev/null +++ b/Deepgram/Models/Listen/v2/WebSocket/OpenResponse.cs @@ -0,0 +1,11 @@ +// Copyright 2024 Deepgram .NET 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 + +namespace Deepgram.Models.Listen.v2.WebSocket; + +using Common = Deepgram.Models.Common.v2.WebSocket; + +public record OpenResponse : Common.OpenResponse +{ +} diff --git a/Deepgram/Models/Listen/v2/WebSocket/ResultResponse.cs b/Deepgram/Models/Listen/v2/WebSocket/ResultResponse.cs new file mode 100644 index 00000000..e55ed77d --- /dev/null +++ b/Deepgram/Models/Listen/v2/WebSocket/ResultResponse.cs @@ -0,0 +1,86 @@ +// Copyright 2021-2024 Deepgram .NET 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 + +namespace Deepgram.Models.Listen.v2.WebSocket; + +public record ResultResponse +{ + /// + /// Contains the channel information. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("channel")] + public Channel? Channel { get; set; } + + /// + /// Channel index. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("channel_index")] + public IReadOnlyList? ChannelIndex { get; set; } + + /// + /// Duration of the result. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("duration")] + public decimal? Duration { get; set; } + + /// + /// Is the result final. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("is_final")] + public bool? IsFinal { get; set; } = false; + + /// + /// Indicates whether this result was generated during the finalization process. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("from_finalize")] + public bool? FromFinalize { get; set; } + + /// + /// Metadata information. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("metadata")] + public MetaData? MetaData { get; set; } + + /// + /// Is the result a partial result. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("speech_final")] + public bool? SpeechFinal { get; set; } + + /// + /// Start time of the result. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("start")] + public decimal? Start { get; set; } + + /// + /// Result event type. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("type")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public ListenType? Type { get; set; } = ListenType.Results; + + // TODO: DYV is this needed??? + /// + /// Error information. + /// + public Exception? Error { get; set; } + + /// + /// Override ToString method to serialize the object + /// + public override string ToString() + { + return Regex.Unescape(JsonSerializer.Serialize(this, JsonSerializeOptions.DefaultOptions)); + } +} diff --git a/Deepgram/Models/Listen/v2/WebSocket/Search.cs b/Deepgram/Models/Listen/v2/WebSocket/Search.cs new file mode 100644 index 00000000..d003e816 --- /dev/null +++ b/Deepgram/Models/Listen/v2/WebSocket/Search.cs @@ -0,0 +1,31 @@ +// Copyright 2021-2024 Deepgram .NET 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 + +namespace Deepgram.Models.Listen.v2.WebSocket; + +public record Search +{ + /// + /// Term for which Deepgram is searching. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("query")] + public string? Query { get; set; } + + /// + /// ReadonlyList of + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("hits")] + public IReadOnlyList? Hits { get; set; } + + /// + /// Override ToString method to serialize the object + /// + public override string ToString() + { + return Regex.Unescape(JsonSerializer.Serialize(this, JsonSerializeOptions.DefaultOptions)); + } +} + diff --git a/Deepgram/Models/Listen/v2/WebSocket/SpeechStartedResponse.cs b/Deepgram/Models/Listen/v2/WebSocket/SpeechStartedResponse.cs new file mode 100644 index 00000000..568410c2 --- /dev/null +++ b/Deepgram/Models/Listen/v2/WebSocket/SpeechStartedResponse.cs @@ -0,0 +1,38 @@ +// Copyright 2021-2024 Deepgram .NET 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 + +namespace Deepgram.Models.Listen.v2.WebSocket; + +public record SpeechStartedResponse +{ + /// + /// SpeechStarted event type. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("type")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public ListenType? Type { get; set; } = ListenType.SpeechStarted; + + /// + /// Channel index information + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("channel_index")] + public int[]? Channel { get; set; } + + /// + /// Timestamp of the event. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("timestamp")] + public decimal? Timestamp { get; set; } + + /// + /// Override ToString method to serialize the object + /// + public override string ToString() + { + return Regex.Unescape(JsonSerializer.Serialize(this, JsonSerializeOptions.DefaultOptions)); + } +} diff --git a/Deepgram/Models/Listen/v2/WebSocket/UnhandledResponse.cs b/Deepgram/Models/Listen/v2/WebSocket/UnhandledResponse.cs new file mode 100644 index 00000000..72895792 --- /dev/null +++ b/Deepgram/Models/Listen/v2/WebSocket/UnhandledResponse.cs @@ -0,0 +1,11 @@ +// Copyright 2024 Deepgram .NET 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 + +namespace Deepgram.Models.Listen.v2.WebSocket; + +using Common = Deepgram.Models.Common.v2.WebSocket; + +public record UnhandledResponse : Common.UnhandledResponse +{ +} diff --git a/Deepgram/Models/Listen/v2/WebSocket/UtteranceEndResponse.cs b/Deepgram/Models/Listen/v2/WebSocket/UtteranceEndResponse.cs new file mode 100644 index 00000000..947c5d6d --- /dev/null +++ b/Deepgram/Models/Listen/v2/WebSocket/UtteranceEndResponse.cs @@ -0,0 +1,38 @@ +// Copyright 2021-2024 Deepgram .NET 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 + +namespace Deepgram.Models.Listen.v2.WebSocket; + +public record UtteranceEndResponse +{ + /// + /// Utterance end event type. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("type")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public ListenType? Type { get; set; } = ListenType.UtteranceEnd; + + /// + /// Channel index information + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("channel_index")] + public int[]? Channel { get; set; } + + /// + /// Timestamp of the event. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("last_word_end")] + public decimal? LastWordEnd { get; set; } + + /// + /// Override ToString method to serialize the object + /// + public override string ToString() + { + return Regex.Unescape(JsonSerializer.Serialize(this, JsonSerializeOptions.DefaultOptions)); + } +} diff --git a/Deepgram/Models/Listen/v2/WebSocket/Word.cs b/Deepgram/Models/Listen/v2/WebSocket/Word.cs new file mode 100644 index 00000000..55ebe3ca --- /dev/null +++ b/Deepgram/Models/Listen/v2/WebSocket/Word.cs @@ -0,0 +1,65 @@ +// Copyright 2021-2024 Deepgram .NET 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 + +namespace Deepgram.Models.Listen.v2.WebSocket; + +public record Word +{ + /// + /// Distinct word heard by the model. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("word")] + public string? HeardWord { get; set; } + + /// + /// Offset in seconds from the start of the audio to where the spoken word starts. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("start")] + public decimal? Start { get; set; } + + /// + /// Offset in seconds from the start of the audio to where the spoken word ends. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("end")] + public decimal? End { get; set; } + + /// + /// Value between 0 and 1 indicating the model's relative confidence in this word. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("confidence")] + public double? Confidence { get; set; } + + /// + /// Punctuated version of the word + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("punctuated_word")] + public string? PunctuatedWord { get; set; } + + /// + /// Language detected + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("language")] + public string? Language { get; set; } + + /// + /// Speaker index of who said this word + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("speaker")] + public int? Speaker { get; set; } + + /// + /// Override ToString method to serialize the object + /// + public override string ToString() + { + return Regex.Unescape(JsonSerializer.Serialize(this, JsonSerializeOptions.DefaultOptions)); + } +} diff --git a/Deepgram/Models/Speak/v1/WebSocket/AudioResponse.cs b/Deepgram/Models/Speak/v1/WebSocket/AudioResponse.cs index 0f58ff32..051ae506 100644 --- a/Deepgram/Models/Speak/v1/WebSocket/AudioResponse.cs +++ b/Deepgram/Models/Speak/v1/WebSocket/AudioResponse.cs @@ -7,7 +7,7 @@ namespace Deepgram.Models.Speak.v1.WebSocket; public record AudioResponse : IDisposable { /// - /// Open event type. + /// The type of speak response, defaults to Audio. /// [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("type")] diff --git a/Deepgram/Models/Speak/v1/WebSocket/ControlMessage.cs b/Deepgram/Models/Speak/v1/WebSocket/ControlMessage.cs index 7d67d049..046b2c3e 100644 --- a/Deepgram/Models/Speak/v1/WebSocket/ControlMessage.cs +++ b/Deepgram/Models/Speak/v1/WebSocket/ControlMessage.cs @@ -7,7 +7,7 @@ namespace Deepgram.Models.Speak.v1.WebSocket; public class ControlMessage(string text) { /// - /// Text of the words to speak + /// Gets or sets the type of control message. /// [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("type")] diff --git a/Deepgram/Models/Speak/v2/WebSocket/AudioResponse.cs b/Deepgram/Models/Speak/v2/WebSocket/AudioResponse.cs new file mode 100644 index 00000000..a66f7941 --- /dev/null +++ b/Deepgram/Models/Speak/v2/WebSocket/AudioResponse.cs @@ -0,0 +1,30 @@ +// Copyright 2024 Deepgram .NET 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 + +namespace Deepgram.Models.Speak.v2.WebSocket; + +public record AudioResponse : IDisposable +{ + /// + /// The type of speak response, defaults to Audio. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("type")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public SpeakType? Type { get; set; } = SpeakType.Audio; + + /// + /// A stream of the audio file + /// + public MemoryStream? Stream { get; set; } + + // NOTE: There isn't a ToString() function because this will cause an odd Exception to be thrown: + // InvalidOperationException: "Timeouts are not supported on this stream." + + public void Dispose() + { + Stream?.Dispose(); + Stream = null; + } +} diff --git a/Deepgram/Models/Speak/v2/WebSocket/ClearedResponse.cs b/Deepgram/Models/Speak/v2/WebSocket/ClearedResponse.cs new file mode 100644 index 00000000..3974e0d5 --- /dev/null +++ b/Deepgram/Models/Speak/v2/WebSocket/ClearedResponse.cs @@ -0,0 +1,31 @@ +// Copyright 2024 Deepgram .NET 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 + +namespace Deepgram.Models.Speak.v2.WebSocket; + +public record ClearedResponse +{ + /// + /// Clear event type. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("type")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public SpeakType? Type { get; set; } = SpeakType.Cleared; + + /// + /// Sequence ID + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("sequence_id")] + public int? SequenceId { get; set; } + + /// + /// Override ToString method to serialize the object + /// + public override string ToString() + { + return Regex.Unescape(JsonSerializer.Serialize(this, JsonSerializeOptions.DefaultOptions)); + } +} diff --git a/Deepgram/Models/Speak/v2/WebSocket/CloseResponse.cs b/Deepgram/Models/Speak/v2/WebSocket/CloseResponse.cs new file mode 100644 index 00000000..7f6009dd --- /dev/null +++ b/Deepgram/Models/Speak/v2/WebSocket/CloseResponse.cs @@ -0,0 +1,11 @@ +// Copyright 2024 Deepgram .NET 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 + +namespace Deepgram.Models.Speak.v2.WebSocket; + +using Common = Deepgram.Models.Common.v2.WebSocket; + +public record CloseResponse : Common.CloseResponse +{ +} diff --git a/Deepgram/Models/Speak/v2/WebSocket/ControlMessage.cs b/Deepgram/Models/Speak/v2/WebSocket/ControlMessage.cs new file mode 100644 index 00000000..f5b5e806 --- /dev/null +++ b/Deepgram/Models/Speak/v2/WebSocket/ControlMessage.cs @@ -0,0 +1,24 @@ +// Copyright 2024 Deepgram .NET 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 + +namespace Deepgram.Models.Speak.v2.WebSocket; + +public class ControlMessage(string text) +{ + /// + /// Gets or sets the type of control message. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("type")] + public string? Type { get; set; } = text; + + /// + /// Override ToString method to serialize the object + /// + public override string ToString() + { + return Regex.Unescape(JsonSerializer.Serialize(this, JsonSerializeOptions.DefaultOptions)); + } +} + diff --git a/Deepgram/Models/Speak/v2/WebSocket/ErrorResponse.cs b/Deepgram/Models/Speak/v2/WebSocket/ErrorResponse.cs new file mode 100644 index 00000000..ad3b8c28 --- /dev/null +++ b/Deepgram/Models/Speak/v2/WebSocket/ErrorResponse.cs @@ -0,0 +1,11 @@ +// Copyright 2024 Deepgram .NET 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 + +namespace Deepgram.Models.Speak.v2.WebSocket; + +using Common = Deepgram.Models.Common.v2.WebSocket; + +public record ErrorResponse : Common.ErrorResponse +{ +} diff --git a/Deepgram/Models/Speak/v2/WebSocket/FlushedResponse.cs b/Deepgram/Models/Speak/v2/WebSocket/FlushedResponse.cs new file mode 100644 index 00000000..f94f54ac --- /dev/null +++ b/Deepgram/Models/Speak/v2/WebSocket/FlushedResponse.cs @@ -0,0 +1,31 @@ +// Copyright 2024 Deepgram .NET 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 + +namespace Deepgram.Models.Speak.v2.WebSocket; + +public record FlushedResponse +{ + /// + /// Flush event type. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("type")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public SpeakType? Type { get; set; } = SpeakType.Flushed; + + /// + /// Sequence ID + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("sequence_id")] + public int? SequenceId { get; set; } + + /// + /// Override ToString method to serialize the object + /// + public override string ToString() + { + return Regex.Unescape(JsonSerializer.Serialize(this, JsonSerializeOptions.DefaultOptions)); + } +} diff --git a/Deepgram/Models/Speak/v2/WebSocket/MetadataResponse.cs b/Deepgram/Models/Speak/v2/WebSocket/MetadataResponse.cs new file mode 100644 index 00000000..d0a4b9c8 --- /dev/null +++ b/Deepgram/Models/Speak/v2/WebSocket/MetadataResponse.cs @@ -0,0 +1,31 @@ +// Copyright 2024 Deepgram .NET 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 + +namespace Deepgram.Models.Speak.v2.WebSocket; + +public record MetadataResponse +{ + /// + /// Request ID is a unique identifier for the request. It is useful for troubleshooting and support. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("request_id")] + public string? RequestId { get; set; } + + /// + /// Metadata event type. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("type")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public SpeakType? Type { get; set; } = SpeakType.Metadata; + + /// + /// Override ToString method to serialize the object + /// + public override string ToString() + { + return Regex.Unescape(JsonSerializer.Serialize(this, JsonSerializeOptions.DefaultOptions)); + } +} diff --git a/Deepgram/Models/Speak/v2/WebSocket/OpenResponse.cs b/Deepgram/Models/Speak/v2/WebSocket/OpenResponse.cs new file mode 100644 index 00000000..6b01feef --- /dev/null +++ b/Deepgram/Models/Speak/v2/WebSocket/OpenResponse.cs @@ -0,0 +1,11 @@ +// Copyright 2024 Deepgram .NET 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 + +namespace Deepgram.Models.Speak.v2.WebSocket; + +using Common = Deepgram.Models.Common.v2.WebSocket; + +public record OpenResponse : Common.OpenResponse +{ +} diff --git a/Deepgram/Models/Speak/v2/WebSocket/SpeakSchema.cs b/Deepgram/Models/Speak/v2/WebSocket/SpeakSchema.cs new file mode 100644 index 00000000..4b519aea --- /dev/null +++ b/Deepgram/Models/Speak/v2/WebSocket/SpeakSchema.cs @@ -0,0 +1,56 @@ +// Copyright 2024 Deepgram .NET 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 + +namespace Deepgram.Models.Speak.v2.WebSocket; + +public class SpeakSchema +{ + /// + /// AI model used to process submitted audio + /// + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("model")] + public string? Model { get; set; } = "aura-asteria-en"; + + /// + /// Bit Rate allows you to specify the bit rate of your desired audio. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("bit_rate")] + public int? BitRate { get; set; } + + ///// + ///// Audio container format + ///// + //[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + //[JsonPropertyName("container")] + //public string? Container { get; set; } + + /// + /// Encoding allows you to specify the expected encoding of your submitted audio. + /// + /// supported encodings + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("encoding")] + public string? Encoding { get; set; } + + /// + /// Sample Rate allows you to specify the sample rate of your submitted audio. + /// + /// Only applies when Encoding has a value + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("sample_rate")] + public int? SampleRate { get; set; } + + /// + /// Override ToString method to serialize the object + /// + public override string ToString() + { + return Regex.Unescape(JsonSerializer.Serialize(this, JsonSerializeOptions.DefaultOptions)); + } +} diff --git a/Deepgram/Models/Speak/v2/WebSocket/SpeakType.cs b/Deepgram/Models/Speak/v2/WebSocket/SpeakType.cs new file mode 100644 index 00000000..4eb36a5f --- /dev/null +++ b/Deepgram/Models/Speak/v2/WebSocket/SpeakType.cs @@ -0,0 +1,21 @@ +// Copyright 2024 Deepgram .NET 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 + +using Deepgram.Models.Common.v2.WebSocket; + +namespace Deepgram.Models.Speak.v2.WebSocket; + +public enum SpeakType +{ + Open = WebSocketType.Open, + Close = WebSocketType.Close, + Unhandled = WebSocketType.Unhandled, + Error = WebSocketType.Error, + Metadata, + Flushed, + Cleared, + Reset, + Audio, + Warning, +} diff --git a/Deepgram/Models/Speak/v2/WebSocket/TextSource.cs b/Deepgram/Models/Speak/v2/WebSocket/TextSource.cs new file mode 100644 index 00000000..559fa9f1 --- /dev/null +++ b/Deepgram/Models/Speak/v2/WebSocket/TextSource.cs @@ -0,0 +1,31 @@ +// Copyright 2024 Deepgram .NET 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 + +namespace Deepgram.Models.Speak.v2.WebSocket; + +public class TextSource(string text) +{ + /// + /// Text of the words to speak + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("type")] + public string? Type { get; set; } = "Speak"; + + /// + /// Text of the words to speak + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("text")] + public string? Text { get; set; } = text; + + /// + /// Override ToString method to serialize the object + /// + public override string ToString() + { + return Regex.Unescape(JsonSerializer.Serialize(this, JsonSerializeOptions.DefaultOptions)); + } +} + diff --git a/Deepgram/Models/Speak/v2/WebSocket/UnhandledResponse.cs b/Deepgram/Models/Speak/v2/WebSocket/UnhandledResponse.cs new file mode 100644 index 00000000..a1cb3117 --- /dev/null +++ b/Deepgram/Models/Speak/v2/WebSocket/UnhandledResponse.cs @@ -0,0 +1,11 @@ +// Copyright 2024 Deepgram .NET 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 + +namespace Deepgram.Models.Speak.v2.WebSocket; + +using Common = Deepgram.Models.Common.v2.WebSocket; + +public record UnhandledResponse : Common.UnhandledResponse +{ +} diff --git a/Deepgram/Models/Speak/v2/WebSocket/WarningResponse.cs b/Deepgram/Models/Speak/v2/WebSocket/WarningResponse.cs new file mode 100644 index 00000000..d08b1ab1 --- /dev/null +++ b/Deepgram/Models/Speak/v2/WebSocket/WarningResponse.cs @@ -0,0 +1,45 @@ +// Copyright 2024 Deepgram .NET 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 + +namespace Deepgram.Models.Speak.v2.WebSocket; + +public record WarningResponse +{ + /// + /// Error Description + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("warn_code")] + public string? WarnCode { get; set; } = ""; + + /// + /// Error Message + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("warn_msg")] + public string? WarnMsg { get; set; } = ""; + + /// + /// Error Variant + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("variant")] + public string? Variant { get; set; } = ""; + + /// + /// Error event type. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("type")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public SpeakType? Type { get; set; } = SpeakType.Warning; + + /// + /// Override ToString method to serialize the object + /// + public override string ToString() + { + return Regex.Unescape(JsonSerializer.Serialize(this, JsonSerializeOptions.DefaultOptions)); + } +} diff --git a/Deepgram/SpeakWebSocketClient.cs b/Deepgram/SpeakWebSocketClient.cs index a6e184c5..de8c204e 100644 --- a/Deepgram/SpeakWebSocketClient.cs +++ b/Deepgram/SpeakWebSocketClient.cs @@ -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 -using Deepgram.Clients.Speak.v1.WebSocket; +using Deepgram.Clients.Speak.v2.WebSocket; using Deepgram.Models.Authenticate.v1; namespace Deepgram; diff --git a/examples/speech-to-text/websocket/file/Program.cs b/examples/speech-to-text/websocket/file/Program.cs index 04f2609f..b105cfab 100644 --- a/examples/speech-to-text/websocket/file/Program.cs +++ b/examples/speech-to-text/websocket/file/Program.cs @@ -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 -using Deepgram.Models.Listen.v1.WebSocket; +using Deepgram.Models.Listen.v2.WebSocket; namespace SampleApp { @@ -10,46 +10,58 @@ class Program { static async Task Main(string[] args) { - // Initialize Library with default logging - // Normal logging is "Info" level - Library.Initialize(); + try + { + // Initialize Library with default logging + // Normal logging is "Info" level + Library.Initialize(); - // use the client factory with a API Key set with the "DEEPGRAM_API_KEY" environment variable - var liveClient = ClientFactory.CreateListenWebSocketClient(); + // use the client factory with a API Key set with the "DEEPGRAM_API_KEY" environment variable + var liveClient = ClientFactory.CreateListenWebSocketClient(); - // Subscribe to the EventResponseReceived event - liveClient.Subscribe(new EventHandler((sender, e) => - { - if (e.Channel.Alternatives[0].Transcript == "") + // Subscribe to the EventResponseReceived event + await liveClient.Subscribe(new EventHandler((sender, e) => { - return; - } + if (e.Channel.Alternatives[0].Transcript == "") + { + return; + } - // Console.WriteLine("Transcription received: " + JsonSerializer.Serialize(e.Transcription)); - Console.WriteLine($"\n\n\nSpeaker: {e.Channel.Alternatives[0].Transcript}\n\n\n"); - })); + // Console.WriteLine("Transcription received: " + JsonSerializer.Serialize(e.Transcription)); + Console.WriteLine($"\n\n\nSpeaker: {e.Channel.Alternatives[0].Transcript}\n\n\n"); + })); - // Start the connection - var liveSchema = new LiveSchema() - { - Model = "nova-2", - Punctuate = true, - SmartFormat = true, - }; - await liveClient.Connect(liveSchema); + // Start the connection + var liveSchema = new LiveSchema() + { + Model = "nova-2", + Punctuate = true, + SmartFormat = true, + }; + bool bConnected = await liveClient.Connect(liveSchema); + if (!bConnected) + { + Console.WriteLine("Failed to connect to the server"); + return; + } - // Send some audio data - var audioData = File.ReadAllBytes(@"preamble.wav"); - liveClient.Send(audioData); + // Send some audio data + var audioData = File.ReadAllBytes(@"preamble.wav"); + liveClient.Send(audioData); - // Wait for a while to receive responses - await Task.Delay(45000); + // Wait for a while to receive responses + await Task.Delay(45000); - // Stop the connection - await liveClient.Stop(); + // Stop the connection + await liveClient.Stop(); - // Teardown Library - Library.Terminate(); + // Teardown Library + Library.Terminate(); + } + catch (Exception ex) + { + Console.WriteLine($"Exception: {ex.Message}"); + } } } } diff --git a/examples/speech-to-text/websocket/http/Program.cs b/examples/speech-to-text/websocket/http/Program.cs index 7767d4df..75f67889 100644 --- a/examples/speech-to-text/websocket/http/Program.cs +++ b/examples/speech-to-text/websocket/http/Program.cs @@ -2,7 +2,8 @@ // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT -using Deepgram.Models.Listen.v1.WebSocket; +using Deepgram.Models.Listen.v2.WebSocket; +using System.Linq.Expressions; namespace SampleApp { @@ -10,58 +11,70 @@ class Program { static async Task Main(string[] args) { - // Initialize Library with default logging - Library.Initialize(); + try + { + // Initialize Library with default logging + Library.Initialize(); - // use the client factory with a API Key set with the "DEEPGRAM_API_KEY" environment variable - var liveClient = new ListenWebSocketClient(); + // use the client factory with a API Key set with the "DEEPGRAM_API_KEY" environment variable + var liveClient = new ListenWebSocketClient(); - // Subscribe to the EventResponseReceived event - liveClient.Subscribe(new EventHandler((sender, e) => - { - if (e.Channel.Alternatives[0].Transcript == "") + // Subscribe to the EventResponseReceived event + await liveClient.Subscribe(new EventHandler((sender, e) => { + if (e.Channel.Alternatives[0].Transcript == "") + { + return; + } + Console.WriteLine($"Speaker: {e.Channel.Alternatives[0].Transcript}"); + })); + + // Start the connection + var liveSchema = new LiveSchema() + { + Model = "nova-2", + Punctuate = true, + SmartFormat = true, + }; + bool bConnected = await liveClient.Connect(liveSchema); + if (!bConnected) + { + Console.WriteLine("Failed to connect to the server"); return; } - Console.WriteLine($"Speaker: {e.Channel.Alternatives[0].Transcript}"); - })); - - // Start the connection - var liveSchema = new LiveSchema() - { - Model = "nova-2", - Punctuate = true, - SmartFormat = true, - }; - await liveClient.Connect(liveSchema); - // get the webcast data... this is a blocking operation - try - { - var url = "http://stream.live.vc.bbcmedia.co.uk/bbc_world_service"; - using (HttpClient client = new HttpClient()) + // get the webcast data... this is a blocking operation + try { - using (Stream receiveStream = await client.GetStreamAsync(url)) + var url = "http://stream.live.vc.bbcmedia.co.uk/bbc_world_service"; + using (HttpClient client = new HttpClient()) { - while (liveClient.IsConnected()) + using (Stream receiveStream = await client.GetStreamAsync(url)) { - byte[] buffer = new byte[2048]; - await receiveStream.ReadAsync(buffer, 0, buffer.Length); - liveClient.Send(buffer); + while (liveClient.IsConnected()) + { + byte[] buffer = new byte[2048]; + await receiveStream.ReadAsync(buffer, 0, buffer.Length); + liveClient.Send(buffer); + } } } } + catch (Exception e) + { + Console.WriteLine(e.Message); + } + + // Stop the connection + await liveClient.Stop(); + + // Teardown Library + Library.Terminate(); } catch (Exception e) { Console.WriteLine(e.Message); } - - // Stop the connection - await liveClient.Stop(); - - // Teardown Library - Library.Terminate(); } } } diff --git a/examples/speech-to-text/websocket/microphone/Program.cs b/examples/speech-to-text/websocket/microphone/Program.cs index 08f73e76..f0a4c1c5 100644 --- a/examples/speech-to-text/websocket/microphone/Program.cs +++ b/examples/speech-to-text/websocket/microphone/Program.cs @@ -5,7 +5,7 @@ using Deepgram.Logger; using Deepgram.Microphone; using Deepgram.Models.Authenticate.v1; -using Deepgram.Models.Listen.v1.WebSocket; +using Deepgram.Models.Listen.v2.WebSocket; namespace SampleApp { @@ -13,93 +13,105 @@ class Program { static async Task Main(string[] args) { - // Initialize Library with default logging - // Normal logging is "Info" level - Deepgram.Library.Initialize(); - // OR very chatty logging - //Deepgram.Library.Initialize(LogLevel.Debug); // LogLevel.Default, LogLevel.Debug, LogLevel.Verbose + try + { + // Initialize Library with default logging + // Normal logging is "Info" level + //Deepgram.Library.Initialize(); + // OR very chatty logging + Deepgram.Library.Initialize(LogLevel.Verbose); // LogLevel.Default, LogLevel.Debug, LogLevel.Verbose - // Initialize the microphone library - Deepgram.Microphone.Library.Initialize(); + // Initialize the microphone library + Deepgram.Microphone.Library.Initialize(); - Console.WriteLine("\n\nPress any key to stop and exit...\n\n\n"); + Console.WriteLine("\n\nPress any key to stop and exit...\n\n\n"); - // Set "DEEPGRAM_API_KEY" environment variable to your Deepgram API Key - DeepgramWsClientOptions options = new DeepgramWsClientOptions(null, null, true); - //options.AutoFlushReplyDelta = 2000; // if your live stream application is like "push to talk". - var liveClient = ClientFactory.CreateListenWebSocketClient(apiKey: "", options: options); + // Set "DEEPGRAM_API_KEY" environment variable to your Deepgram API Key + DeepgramWsClientOptions options = new DeepgramWsClientOptions(null, null, true); + //options.AutoFlushReplyDelta = 2000; // if your live stream application is like "push to talk". + var liveClient = ClientFactory.CreateListenWebSocketClient(apiKey: "", options: options); - // Subscribe to the EventResponseReceived event - liveClient.Subscribe(new EventHandler((sender, e) => - { - Console.WriteLine($"\n\n----> {e.Type} received"); - })); - liveClient.Subscribe(new EventHandler((sender, e) => - { - Console.WriteLine($"----> {e.Type} received"); - })); - liveClient.Subscribe(new EventHandler((sender, e) => - { - Console.WriteLine($"----> {e.Type} received"); - if (e.Channel.Alternatives[0].Transcript.Trim() == "") + // Subscribe to the EventResponseReceived event + await liveClient.Subscribe(new EventHandler((sender, e) => { - return; - } + Console.WriteLine($"----> {e.Type} received"); + })); + await liveClient.Subscribe(new EventHandler((sender, e) => + { + Console.WriteLine($"----> {e.Type} received"); + })); + await liveClient.Subscribe(new EventHandler((sender, e) => + { + Console.WriteLine($"----> {e.Type} received"); + if (e.Channel.Alternatives[0].Transcript.Trim() == "") + { + return; + } - // Console.WriteLine("Transcription received: " + JsonSerializer.Serialize(e.Transcription)); - Console.WriteLine($"----> Speaker: {e.Channel.Alternatives[0].Transcript}"); - })); - liveClient.Subscribe(new EventHandler((sender, e) => - { - Console.WriteLine($"----> {e.Type} received"); - })); - liveClient.Subscribe(new EventHandler((sender, e) => - { - Console.WriteLine($"----> {e.Type} received"); - })); - liveClient.Subscribe(new EventHandler((sender, e) => - { - Console.WriteLine($"----> {e.Type} received"); - })); - liveClient.Subscribe(new EventHandler((sender, e) => - { - Console.WriteLine($"----> {e.Type} received"); - })); - liveClient.Subscribe(new EventHandler((sender, e) => - { - Console.WriteLine($"----> {e.Type} received. Error: {e.Message}"); - })); + // Console.WriteLine("Transcription received: " + JsonSerializer.Serialize(e.Transcription)); + Console.WriteLine($"----> Speaker: {e.Channel.Alternatives[0].Transcript}"); + })); + await liveClient.Subscribe(new EventHandler((sender, e) => + { + Console.WriteLine($"----> {e.Type} received"); + })); + await liveClient.Subscribe(new EventHandler((sender, e) => + { + Console.WriteLine($"----> {e.Type} received"); + })); + await liveClient.Subscribe(new EventHandler((sender, e) => + { + Console.WriteLine($"----> {e.Type} received"); + })); + await liveClient.Subscribe(new EventHandler((sender, e) => + { + Console.WriteLine($"----> {e.Type} received"); + })); + await liveClient.Subscribe(new EventHandler((sender, e) => + { + Console.WriteLine($"----> {e.Type} received. Error: {e.Message}"); + })); - // Start the connection - var liveSchema = new LiveSchema() - { - Model = "nova-2", - Encoding = "linear16", - SampleRate = 16000, - Punctuate = true, - SmartFormat = true, - InterimResults = true, - UtteranceEnd = "1000", - VadEvents = true, - }; - await liveClient.Connect(liveSchema); + // Start the connection + var liveSchema = new LiveSchema() + { + Model = "nova-2", + Encoding = "linear16", + SampleRate = 16000, + Punctuate = true, + SmartFormat = true, + InterimResults = true, + UtteranceEnd = "1000", + VadEvents = true, + }; + bool bConnected = await liveClient.Connect(liveSchema); + if (!bConnected) + { + Console.WriteLine("Failed to connect to Deepgram WebSocket server."); + return; + } - // Microphone streaming - var microphone = new Microphone(liveClient.Send); - microphone.Start(); + // Microphone streaming + var microphone = new Microphone(liveClient.Send); + microphone.Start(); - // Wait for the user to press a key - Console.ReadKey(); + // Wait for the user to press a key + Console.ReadKey(); - // Stop the microphone - microphone.Stop(); + // Stop the microphone + microphone.Stop(); - // Stop the connection - await liveClient.Stop(); + // Stop the connection + await liveClient.Stop(); - // Terminate Libraries - Deepgram.Microphone.Library.Terminate(); - Deepgram.Library.Terminate(); + // Terminate Libraries + Deepgram.Microphone.Library.Terminate(); + Deepgram.Library.Terminate(); + } + catch (Exception ex) + { + Console.WriteLine($"Exception: {ex.Message}"); + } } } } diff --git a/examples/text-to-speech/websocket/simple/Program.cs b/examples/text-to-speech/websocket/simple/Program.cs index 89f37448..712bca87 100644 --- a/examples/text-to-speech/websocket/simple/Program.cs +++ b/examples/text-to-speech/websocket/simple/Program.cs @@ -2,10 +2,8 @@ // Use of this source code is governed by a MIT license that can be found in the LICENSE file. // SPDX-License-Identifier: MIT -using System.Text; - using Deepgram.Models.Authenticate.v1; -using Deepgram.Models.Speak.v1.WebSocket; +using Deepgram.Models.Speak.v2.WebSocket; using Deepgram.Logger; @@ -15,154 +13,166 @@ class Program { static async Task Main(string[] args) { - // Initialize Library with default logging - // Normal logging is "Info" level - //Library.Initialize(); - // OR very chatty logging - Library.Initialize(LogLevel.Verbose); // LogLevel.Default, LogLevel.Debug, LogLevel.Verbose - - //// use the client factory with a API Key set with the "DEEPGRAM_API_KEY" environment variable - //DeepgramWsClientOptions options = new DeepgramWsClientOptions(null, "ENTER URL HERE"); - //options.AutoFlushSpeakDelta = 1000; - //var speakClient = ClientFactory.CreateSpeakWebSocketClient("", options); - var speakClient = ClientFactory.CreateSpeakWebSocketClient(); - - // append wav header only once - bool appendWavHeader = true; - - // Subscribe to the EventResponseReceived event - speakClient.Subscribe(new EventHandler((sender, e) => - { - Console.WriteLine($"\n\n----> {e.Type} received"); - })); - speakClient.Subscribe(new EventHandler((sender, e) => + try { - Console.WriteLine($"----> {e.Type} received"); - Console.WriteLine($"----> RequestId: {e.RequestId}"); - })); - speakClient.Subscribe(new EventHandler((sender, e) => - { - Console.WriteLine($"----> {e.Type} received"); - - // add a wav header - if (appendWavHeader) + // Initialize Library with default logging + // Normal logging is "Info" level + //Library.Initialize(); + // OR very chatty logging + Library.Initialize(LogLevel.Verbose); // LogLevel.Default, LogLevel.Debug, LogLevel.Verbose + + //// use the client factory with a API Key set with the "DEEPGRAM_API_KEY" environment variable + //DeepgramWsClientOptions options = new DeepgramWsClientOptions(); + //options.AutoFlushSpeakDelta = 1000; + //var speakClient = ClientFactory.CreateSpeakWebSocketClient("", options); + var speakClient = ClientFactory.CreateSpeakWebSocketClient(); + + // append wav header only once + bool appendWavHeader = true; + + // Subscribe to the EventResponseReceived event + await speakClient.Subscribe(new EventHandler((sender, e) => { - using (BinaryWriter writer = new BinaryWriter(File.Open("output.wav", FileMode.Append))) + Console.WriteLine($"\n\n----> {e.Type} received"); + })); + await speakClient.Subscribe(new EventHandler((sender, e) => + { + Console.WriteLine($"----> {e.Type} received"); + Console.WriteLine($"----> RequestId: {e.RequestId}"); + })); + await speakClient.Subscribe(new EventHandler((sender, e) => + { + Console.WriteLine($"----> {e.Type} received"); + + // add a wav header + if (appendWavHeader) { - Console.WriteLine("Adding WAV header to output.wav"); - byte[] wavHeader = new byte[44]; - int sampleRate = 48000; - short bitsPerSample = 16; - short channels = 1; - int byteRate = sampleRate * channels * (bitsPerSample / 8); - short blockAlign = (short)(channels * (bitsPerSample / 8)); - - wavHeader[0] = 0x52; // R - wavHeader[1] = 0x49; // I - wavHeader[2] = 0x46; // F - wavHeader[3] = 0x46; // F - wavHeader[4] = 0x00; // Placeholder for file size (will be updated later) - wavHeader[5] = 0x00; // Placeholder for file size (will be updated later) - wavHeader[6] = 0x00; // Placeholder for file size (will be updated later) - wavHeader[7] = 0x00; // Placeholder for file size (will be updated later) - wavHeader[8] = 0x57; // W - wavHeader[9] = 0x41; // A - wavHeader[10] = 0x56; // V - wavHeader[11] = 0x45; // E - wavHeader[12] = 0x66; // f - wavHeader[13] = 0x6D; // m - wavHeader[14] = 0x74; // t - wavHeader[15] = 0x20; // Space - wavHeader[16] = 0x10; // Subchunk1Size (16 for PCM) - wavHeader[17] = 0x00; // Subchunk1Size - wavHeader[18] = 0x00; // Subchunk1Size - wavHeader[19] = 0x00; // Subchunk1Size - wavHeader[20] = 0x01; // AudioFormat (1 for PCM) - wavHeader[21] = 0x00; // AudioFormat - wavHeader[22] = (byte)channels; // NumChannels - wavHeader[23] = 0x00; // NumChannels - wavHeader[24] = (byte)(sampleRate & 0xFF); // SampleRate - wavHeader[25] = (byte)((sampleRate >> 8) & 0xFF); // SampleRate - wavHeader[26] = (byte)((sampleRate >> 16) & 0xFF); // SampleRate - wavHeader[27] = (byte)((sampleRate >> 24) & 0xFF); // SampleRate - wavHeader[28] = (byte)(byteRate & 0xFF); // ByteRate - wavHeader[29] = (byte)((byteRate >> 8) & 0xFF); // ByteRate - wavHeader[30] = (byte)((byteRate >> 16) & 0xFF); // ByteRate - wavHeader[31] = (byte)((byteRate >> 24) & 0xFF); // ByteRate - wavHeader[32] = (byte)blockAlign; // BlockAlign - wavHeader[33] = 0x00; // BlockAlign - wavHeader[34] = (byte)bitsPerSample; // BitsPerSample - wavHeader[35] = 0x00; // BitsPerSample - wavHeader[36] = 0x64; // d - wavHeader[37] = 0x61; // a - wavHeader[38] = 0x74; // t - wavHeader[39] = 0x61; // a - wavHeader[40] = 0x00; // Placeholder for data chunk size (will be updated later) - wavHeader[41] = 0x00; // Placeholder for data chunk size (will be updated later) - wavHeader[42] = 0x00; // Placeholder for data chunk size (will be updated later) - wavHeader[43] = 0x00; // Placeholder for data chunk size (will be updated later) - - writer.Write(wavHeader); - appendWavHeader = false; + using (BinaryWriter writer = new BinaryWriter(File.Open("output.wav", FileMode.Append))) + { + Console.WriteLine("Adding WAV header to output.wav"); + byte[] wavHeader = new byte[44]; + int sampleRate = 48000; + short bitsPerSample = 16; + short channels = 1; + int byteRate = sampleRate * channels * (bitsPerSample / 8); + short blockAlign = (short)(channels * (bitsPerSample / 8)); + + wavHeader[0] = 0x52; // R + wavHeader[1] = 0x49; // I + wavHeader[2] = 0x46; // F + wavHeader[3] = 0x46; // F + wavHeader[4] = 0x00; // Placeholder for file size (will be updated later) + wavHeader[5] = 0x00; // Placeholder for file size (will be updated later) + wavHeader[6] = 0x00; // Placeholder for file size (will be updated later) + wavHeader[7] = 0x00; // Placeholder for file size (will be updated later) + wavHeader[8] = 0x57; // W + wavHeader[9] = 0x41; // A + wavHeader[10] = 0x56; // V + wavHeader[11] = 0x45; // E + wavHeader[12] = 0x66; // f + wavHeader[13] = 0x6D; // m + wavHeader[14] = 0x74; // t + wavHeader[15] = 0x20; // Space + wavHeader[16] = 0x10; // Subchunk1Size (16 for PCM) + wavHeader[17] = 0x00; // Subchunk1Size + wavHeader[18] = 0x00; // Subchunk1Size + wavHeader[19] = 0x00; // Subchunk1Size + wavHeader[20] = 0x01; // AudioFormat (1 for PCM) + wavHeader[21] = 0x00; // AudioFormat + wavHeader[22] = (byte)channels; // NumChannels + wavHeader[23] = 0x00; // NumChannels + wavHeader[24] = (byte)(sampleRate & 0xFF); // SampleRate + wavHeader[25] = (byte)((sampleRate >> 8) & 0xFF); // SampleRate + wavHeader[26] = (byte)((sampleRate >> 16) & 0xFF); // SampleRate + wavHeader[27] = (byte)((sampleRate >> 24) & 0xFF); // SampleRate + wavHeader[28] = (byte)(byteRate & 0xFF); // ByteRate + wavHeader[29] = (byte)((byteRate >> 8) & 0xFF); // ByteRate + wavHeader[30] = (byte)((byteRate >> 16) & 0xFF); // ByteRate + wavHeader[31] = (byte)((byteRate >> 24) & 0xFF); // ByteRate + wavHeader[32] = (byte)blockAlign; // BlockAlign + wavHeader[33] = 0x00; // BlockAlign + wavHeader[34] = (byte)bitsPerSample; // BitsPerSample + wavHeader[35] = 0x00; // BitsPerSample + wavHeader[36] = 0x64; // d + wavHeader[37] = 0x61; // a + wavHeader[38] = 0x74; // t + wavHeader[39] = 0x61; // a + wavHeader[40] = 0x00; // Placeholder for data chunk size (will be updated later) + wavHeader[41] = 0x00; // Placeholder for data chunk size (will be updated later) + wavHeader[42] = 0x00; // Placeholder for data chunk size (will be updated later) + wavHeader[43] = 0x00; // Placeholder for data chunk size (will be updated later) + + writer.Write(wavHeader); + appendWavHeader = false; + } } - } - if (e.Stream != null) - { - using (BinaryWriter writer = new BinaryWriter(File.Open("output.wav", FileMode.Append))) + if (e.Stream != null) { - writer.Write(e.Stream.ToArray()); + using (BinaryWriter writer = new BinaryWriter(File.Open("output.wav", FileMode.Append))) + { + writer.Write(e.Stream.ToArray()); + } } - } - })); - speakClient.Subscribe(new EventHandler((sender, e) => - { - Console.WriteLine($"----> {e.Type} received"); - })); - speakClient.Subscribe(new EventHandler((sender, e) => - { - Console.WriteLine($"----> {e.Type} received"); - })); - speakClient.Subscribe(new EventHandler((sender, e) => - { - Console.WriteLine($"----> {e.Type} received"); - })); - speakClient.Subscribe(new EventHandler((sender, e) => - { - Console.WriteLine($"----> {e.Type} received"); - })); - speakClient.Subscribe(new EventHandler((sender, e) => - { - Console.WriteLine($"----> {e.Type} received"); - })); - speakClient.Subscribe(new EventHandler((sender, e) => - { - Console.WriteLine($"----> {e.Type} received. Error: {e.Message}"); - })); + })); + await speakClient.Subscribe(new EventHandler((sender, e) => + { + Console.WriteLine($"----> {e.Type} received"); + })); + await speakClient.Subscribe(new EventHandler((sender, e) => + { + Console.WriteLine($"----> {e.Type} received"); + })); + await speakClient.Subscribe(new EventHandler((sender, e) => + { + Console.WriteLine($"----> {e.Type} received"); + })); + await speakClient.Subscribe(new EventHandler((sender, e) => + { + Console.WriteLine($"----> {e.Type} received"); + })); + await speakClient.Subscribe(new EventHandler((sender, e) => + { + Console.WriteLine($"----> {e.Type} received"); + })); + await speakClient.Subscribe(new EventHandler((sender, e) => + { + Console.WriteLine($"----> {e.Type} received. Error: {e.Message}"); + })); - // Start the connection - var speakSchema = new SpeakSchema() - { - Encoding = "linear16", - SampleRate = 48000 - }; - await speakClient.Connect(speakSchema); + // Start the connection + var speakSchema = new SpeakSchema() + { + Encoding = "linear16", + SampleRate = 48000, + }; + bool bConnected = await speakClient.Connect(speakSchema); + if (!bConnected) + { + Console.WriteLine("Failed to connect to the server"); + return; + } - // Send some Text to convert to audio - speakClient.SpeakWithText("Hello World!"); + // Send some Text to convert to audio + speakClient.SpeakWithText("Hello World!"); - //Flush the audio - speakClient.Flush(); + //Flush the audio + speakClient.Flush(); - // Wait for the user to press a key - Console.WriteLine("\n\nPress any key to stop and exit...\n\n\n"); - Console.ReadKey(); + // Wait for the user to press a key + Console.WriteLine("\n\nPress any key to stop and exit...\n\n\n"); + Console.ReadKey(); - // Stop the connection - await speakClient.Stop(); + // Stop the connection + await speakClient.Stop(); - // Terminate Libraries - Library.Terminate(); + // Terminate Libraries + Library.Terminate(); + } + catch (Exception ex) + { + Console.WriteLine($"Exception: {ex.Message}"); + } } } } diff --git a/tests/edge_cases/keepalive/Program.cs b/tests/edge_cases/keepalive/Program.cs index e60393af..d343a90c 100644 --- a/tests/edge_cases/keepalive/Program.cs +++ b/tests/edge_cases/keepalive/Program.cs @@ -3,7 +3,7 @@ // SPDX-License-Identifier: MIT using Deepgram.Models.Authenticate.v1; -using Deepgram.Models.Listen.v1.WebSocket; +using Deepgram.Models.Listen.v2.WebSocket; using Deepgram.Logger; namespace SampleApp @@ -25,11 +25,11 @@ static async Task Main(string[] args) var liveClient = ClientFactory.CreateListenWebSocketClient("", options); // Subscribe to the EventResponseReceived event - liveClient.Subscribe(new EventHandler((sender, e) => + await liveClient.Subscribe(new EventHandler((sender, e) => { Console.WriteLine($"----> {e.Type} received"); })); - liveClient.Subscribe(new EventHandler((sender, e) => + await liveClient.Subscribe(new EventHandler((sender, e) => { if (e.Channel.Alternatives[0].Transcript == "") { @@ -39,11 +39,11 @@ static async Task Main(string[] args) // Console.WriteLine("Transcription received: " + JsonSerializer.Serialize(e.Transcription)); Console.WriteLine($"\n\n\n----> Speaker: {e.Channel.Alternatives[0].Transcript}\n\n\n"); })); - liveClient.Subscribe(new EventHandler((sender, e) => + await liveClient.Subscribe(new EventHandler((sender, e) => { Console.WriteLine($"----> {e.Type} received"); })); - liveClient.Subscribe(new EventHandler((sender, e) => + await liveClient.Subscribe(new EventHandler((sender, e) => { Console.WriteLine($"----> {e.Type} received. Error: {e.Message}"); })); diff --git a/tests/edge_cases/reconnect_same_object/Program.cs b/tests/edge_cases/reconnect_same_object/Program.cs index d7639d95..4ebf5825 100644 --- a/tests/edge_cases/reconnect_same_object/Program.cs +++ b/tests/edge_cases/reconnect_same_object/Program.cs @@ -5,7 +5,7 @@ using Deepgram.Logger; using Deepgram.Microphone; using Deepgram.Models.Authenticate.v1; -using Deepgram.Models.Listen.v1.WebSocket; +using Deepgram.Models.Listen.v2.WebSocket; namespace SampleApp { @@ -29,15 +29,15 @@ static async Task Main(string[] args) //var liveClient = new LiveClienkt("set your DEEPGRAM_API_KEY here"); // Subscribe to the EventResponseReceived event - liveClient.Subscribe(new EventHandler((sender, e) => + await liveClient.Subscribe(new EventHandler((sender, e) => { Console.WriteLine($"\n\n----> {e.Type} received"); })); - liveClient.Subscribe(new EventHandler((sender, e) => + await liveClient.Subscribe(new EventHandler((sender, e) => { Console.WriteLine($"----> {e.Type} received"); })); - liveClient.Subscribe(new EventHandler((sender, e) => + await liveClient.Subscribe(new EventHandler((sender, e) => { Console.WriteLine($"----> {e.Type} received"); if (e.Channel.Alternatives[0].Transcript.Trim() == "") @@ -48,23 +48,23 @@ static async Task Main(string[] args) // Console.WriteLine("Transcription received: " + JsonSerializer.Serialize(e.Transcription)); Console.WriteLine($"----> Speaker: {e.Channel.Alternatives[0].Transcript}"); })); - liveClient.Subscribe(new EventHandler((sender, e) => + await liveClient.Subscribe(new EventHandler((sender, e) => { Console.WriteLine($"----> {e.Type} received"); })); - liveClient.Subscribe(new EventHandler((sender, e) => + await liveClient.Subscribe(new EventHandler((sender, e) => { Console.WriteLine($"----> {e.Type} received"); })); - liveClient.Subscribe(new EventHandler((sender, e) => + await liveClient.Subscribe(new EventHandler((sender, e) => { Console.WriteLine($"----> {e.Type} received"); })); - liveClient.Subscribe(new EventHandler((sender, e) => + await liveClient.Subscribe(new EventHandler((sender, e) => { Console.WriteLine($"----> {e.Type} received"); })); - liveClient.Subscribe(new EventHandler((sender, e) => + await liveClient.Subscribe(new EventHandler((sender, e) => { Console.WriteLine($"----> {e.Type} received. Error: {e.Message}"); })); diff --git a/tests/edge_cases/stt_v1_client_example/Program.cs b/tests/edge_cases/stt_v1_client_example/Program.cs new file mode 100644 index 00000000..d18994be --- /dev/null +++ b/tests/edge_cases/stt_v1_client_example/Program.cs @@ -0,0 +1,73 @@ +// Copyright 2024 Deepgram .NET 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 + +using Deepgram.Models.Listen.v1.WebSocket; +using ListenV1 = Deepgram.Clients.Listen.v1.WebSocket; + +namespace SampleApp +{ + class Program + { + static async Task Main(string[] args) + { + // Initialize Library with default logging + Library.Initialize(); + + // use the client factory with a API Key set with the "DEEPGRAM_API_KEY" environment variable + var liveClient = ClientFactory.CreateListenWebSocketClient(1) as ListenV1.Client; + if (liveClient == null) + { + Console.WriteLine("Failed to create ListenWebSocketClient"); + return; + } + + // Subscribe to the EventResponseReceived event + liveClient.Subscribe(new EventHandler((sender, e) => + { + if (e.Channel.Alternatives[0].Transcript == "") + { + return; + } + Console.WriteLine($"Speaker: {e.Channel.Alternatives[0].Transcript}"); + })); + + // Start the connection + var liveSchema = new LiveSchema() + { + Model = "nova-2", + Punctuate = true, + SmartFormat = true, + }; + await liveClient.Connect(liveSchema); + + // get the webcast data... this is a blocking operation + try + { + var url = "http://stream.live.vc.bbcmedia.co.uk/bbc_world_service"; + using (HttpClient client = new HttpClient()) + { + using (Stream receiveStream = await client.GetStreamAsync(url)) + { + while (liveClient.IsConnected()) + { + byte[] buffer = new byte[2048]; + await receiveStream.ReadAsync(buffer, 0, buffer.Length); + liveClient.Send(buffer); + } + } + } + } + catch (Exception e) + { + Console.WriteLine(e.Message); + } + + // Stop the connection + await liveClient.Stop(); + + // Teardown Library + Library.Terminate(); + } + } +} diff --git a/tests/edge_cases/stt_v1_client_example/Streaming.csproj b/tests/edge_cases/stt_v1_client_example/Streaming.csproj new file mode 100644 index 00000000..f02f97b4 --- /dev/null +++ b/tests/edge_cases/stt_v1_client_example/Streaming.csproj @@ -0,0 +1,25 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + + + + + + Always + + + + + diff --git a/tests/edge_cases/tts_v1_client_example/Program.cs b/tests/edge_cases/tts_v1_client_example/Program.cs new file mode 100644 index 00000000..bb87e447 --- /dev/null +++ b/tests/edge_cases/tts_v1_client_example/Program.cs @@ -0,0 +1,172 @@ +// Copyright 2024 Deepgram .NET 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 + +using Deepgram.Models.Authenticate.v1; +using Deepgram.Models.Speak.v1.WebSocket; +using SpeakV1 = Deepgram.Clients.Speak.v1.WebSocket; +using Deepgram.Logger; + + +namespace SampleApp +{ + class Program + { + static async Task Main(string[] args) + { + // Initialize Library with default logging + // Normal logging is "Info" level + //Library.Initialize(); + // OR very chatty logging + Library.Initialize(LogLevel.Verbose); // LogLevel.Default, LogLevel.Debug, LogLevel.Verbose + + //// use the client factory with a API Key set with the "DEEPGRAM_API_KEY" environment variable + //DeepgramWsClientOptions options = new DeepgramWsClientOptions(); + //options.AutoFlushSpeakDelta = 1000; + //var speakClient = ClientFactory.CreateSpeakWebSocketClient("", options); + var speakClient = ClientFactory.CreateSpeakWebSocketClient(1) as SpeakV1.Client; + if (speakClient == null) + { + Console.WriteLine("Failed to create SpeakWebSocketClient"); + return; + } + + // append wav header only once + bool appendWavHeader = true; + + // Subscribe to the EventResponseReceived event + speakClient.Subscribe(new EventHandler((sender, e) => + { + Console.WriteLine($"\n\n----> {e.Type} received"); + })); + speakClient.Subscribe(new EventHandler((sender, e) => + { + Console.WriteLine($"----> {e.Type} received"); + Console.WriteLine($"----> RequestId: {e.RequestId}"); + })); + speakClient.Subscribe(new EventHandler((sender, e) => + { + Console.WriteLine($"----> {e.Type} received"); + + // add a wav header + if (appendWavHeader) + { + using (BinaryWriter writer = new BinaryWriter(File.Open("output.wav", FileMode.Append))) + { + Console.WriteLine("Adding WAV header to output.wav"); + byte[] wavHeader = new byte[44]; + int sampleRate = 48000; + short bitsPerSample = 16; + short channels = 1; + int byteRate = sampleRate * channels * (bitsPerSample / 8); + short blockAlign = (short)(channels * (bitsPerSample / 8)); + + wavHeader[0] = 0x52; // R + wavHeader[1] = 0x49; // I + wavHeader[2] = 0x46; // F + wavHeader[3] = 0x46; // F + wavHeader[4] = 0x00; // Placeholder for file size (will be updated later) + wavHeader[5] = 0x00; // Placeholder for file size (will be updated later) + wavHeader[6] = 0x00; // Placeholder for file size (will be updated later) + wavHeader[7] = 0x00; // Placeholder for file size (will be updated later) + wavHeader[8] = 0x57; // W + wavHeader[9] = 0x41; // A + wavHeader[10] = 0x56; // V + wavHeader[11] = 0x45; // E + wavHeader[12] = 0x66; // f + wavHeader[13] = 0x6D; // m + wavHeader[14] = 0x74; // t + wavHeader[15] = 0x20; // Space + wavHeader[16] = 0x10; // Subchunk1Size (16 for PCM) + wavHeader[17] = 0x00; // Subchunk1Size + wavHeader[18] = 0x00; // Subchunk1Size + wavHeader[19] = 0x00; // Subchunk1Size + wavHeader[20] = 0x01; // AudioFormat (1 for PCM) + wavHeader[21] = 0x00; // AudioFormat + wavHeader[22] = (byte)channels; // NumChannels + wavHeader[23] = 0x00; // NumChannels + wavHeader[24] = (byte)(sampleRate & 0xFF); // SampleRate + wavHeader[25] = (byte)((sampleRate >> 8) & 0xFF); // SampleRate + wavHeader[26] = (byte)((sampleRate >> 16) & 0xFF); // SampleRate + wavHeader[27] = (byte)((sampleRate >> 24) & 0xFF); // SampleRate + wavHeader[28] = (byte)(byteRate & 0xFF); // ByteRate + wavHeader[29] = (byte)((byteRate >> 8) & 0xFF); // ByteRate + wavHeader[30] = (byte)((byteRate >> 16) & 0xFF); // ByteRate + wavHeader[31] = (byte)((byteRate >> 24) & 0xFF); // ByteRate + wavHeader[32] = (byte)blockAlign; // BlockAlign + wavHeader[33] = 0x00; // BlockAlign + wavHeader[34] = (byte)bitsPerSample; // BitsPerSample + wavHeader[35] = 0x00; // BitsPerSample + wavHeader[36] = 0x64; // d + wavHeader[37] = 0x61; // a + wavHeader[38] = 0x74; // t + wavHeader[39] = 0x61; // a + wavHeader[40] = 0x00; // Placeholder for data chunk size (will be updated later) + wavHeader[41] = 0x00; // Placeholder for data chunk size (will be updated later) + wavHeader[42] = 0x00; // Placeholder for data chunk size (will be updated later) + wavHeader[43] = 0x00; // Placeholder for data chunk size (will be updated later) + + writer.Write(wavHeader); + appendWavHeader = false; + } + } + + if (e.Stream != null) + { + using (BinaryWriter writer = new BinaryWriter(File.Open("output.wav", FileMode.Append))) + { + writer.Write(e.Stream.ToArray()); + } + } + })); + speakClient.Subscribe(new EventHandler((sender, e) => + { + Console.WriteLine($"----> {e.Type} received"); + })); + speakClient.Subscribe(new EventHandler((sender, e) => + { + Console.WriteLine($"----> {e.Type} received"); + })); + speakClient.Subscribe(new EventHandler((sender, e) => + { + Console.WriteLine($"----> {e.Type} received"); + })); + speakClient.Subscribe(new EventHandler((sender, e) => + { + Console.WriteLine($"----> {e.Type} received"); + })); + speakClient.Subscribe(new EventHandler((sender, e) => + { + Console.WriteLine($"----> {e.Type} received"); + })); + speakClient.Subscribe(new EventHandler((sender, e) => + { + Console.WriteLine($"----> {e.Type} received. Error: {e.Message}"); + })); + + // Start the connection + var speakSchema = new SpeakSchema() + { + Encoding = "linear16", + SampleRate = 48000, + }; + await speakClient.Connect(speakSchema); + + // Send some Text to convert to audio + speakClient.SpeakWithText("Hello World!"); + + //Flush the audio + speakClient.Flush(); + + // Wait for the user to press a key + Console.WriteLine("\n\nPress any key to stop and exit...\n\n\n"); + Console.ReadKey(); + + // Stop the connection + await speakClient.Stop(); + + // Terminate Libraries + Library.Terminate(); + } + } +} diff --git a/tests/edge_cases/tts_v1_client_example/Speak.csproj b/tests/edge_cases/tts_v1_client_example/Speak.csproj new file mode 100644 index 00000000..33d8f4ee --- /dev/null +++ b/tests/edge_cases/tts_v1_client_example/Speak.csproj @@ -0,0 +1,22 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + + + + + + + + diff --git a/tests/expected_failures/websocket/exercise_timeout/Program.cs b/tests/expected_failures/websocket/exercise_timeout/Program.cs index 763aa062..859e4290 100644 --- a/tests/expected_failures/websocket/exercise_timeout/Program.cs +++ b/tests/expected_failures/websocket/exercise_timeout/Program.cs @@ -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 -using Deepgram.Models.Listen.v1.WebSocket; +using Deepgram.Models.Listen.v2.WebSocket; using Deepgram.Logger; namespace SampleApp @@ -20,11 +20,11 @@ static async Task Main(string[] args) var liveClient = ClientFactory.CreateListenWebSocketClient(); // Subscribe to the EventResponseReceived event - liveClient.Subscribe(new EventHandler((sender, e) => + await liveClient.Subscribe(new EventHandler((sender, e) => { Console.WriteLine($"----> {e.Type} received"); })); - liveClient.Subscribe(new EventHandler((sender, e) => + await liveClient.Subscribe(new EventHandler((sender, e) => { if (e.Channel.Alternatives[0].Transcript == "") { @@ -34,11 +34,11 @@ static async Task Main(string[] args) // Console.WriteLine("Transcription received: " + JsonSerializer.Serialize(e.Transcription)); Console.WriteLine($"\n\n\n----> Speaker: {e.Channel.Alternatives[0].Transcript}\n\n\n"); })); - liveClient.Subscribe(new EventHandler((sender, e) => + await liveClient.Subscribe(new EventHandler((sender, e) => { Console.WriteLine($"----> {e.Type} received"); })); - liveClient.Subscribe(new EventHandler((sender, e) => + await liveClient.Subscribe(new EventHandler((sender, e) => { Console.WriteLine($"----> {e.Type} received. Error: {e.Message}"); }));