From b0f02940b0f2c8ce9481d0d70cf124df2aa852ae Mon Sep 17 00:00:00 2001 From: Mykola Mozghovyi Date: Sun, 15 Sep 2024 20:38:39 +0300 Subject: [PATCH] Word audio 2 (#24) - added audio generation on word creation - added logs - added results for azure blob --- My1kWordsEe/Components/Pages/Word.razor | 4 +- My1kWordsEe/Models/Ee1kWords.cs | 9 +- My1kWordsEe/Models/EeWord.cs | 4 + My1kWordsEe/Models/PartsOfSpeech.cs | 3 + My1kWordsEe/Models/SampleSentence.cs | 30 +++- My1kWordsEe/Models/SampleWord.cs | 14 +- My1kWordsEe/Program.cs | 15 +- My1kWordsEe/Services/Cqs/AddAudioCommand.cs | 24 ++++ ...Command.cs => AddSampleSentenceCommand.cs} | 19 ++- .../Services/Cqs/AddSampleWordCommand.cs | 47 ++++++ My1kWordsEe/Services/Cqs/EnsureWordCommand.cs | 41 ------ .../Services/Cqs/GetOrAddSampleWordCommand.cs | 42 ++++++ My1kWordsEe/Services/Db/AzureBlobService.cs | 134 +++++++++++------- My1kWordsEe/Services/OpenAiService.cs | 38 ++--- My1kWordsEe/Services/StabilityAiService.cs | 3 + My1kWordsEe/Services/TartuNlpService.cs | 42 ++++-- 16 files changed, 325 insertions(+), 144 deletions(-) create mode 100644 My1kWordsEe/Services/Cqs/AddAudioCommand.cs rename My1kWordsEe/Services/Cqs/{CreateSampleCommand.cs => AddSampleSentenceCommand.cs} (80%) create mode 100644 My1kWordsEe/Services/Cqs/AddSampleWordCommand.cs delete mode 100644 My1kWordsEe/Services/Cqs/EnsureWordCommand.cs create mode 100644 My1kWordsEe/Services/Cqs/GetOrAddSampleWordCommand.cs diff --git a/My1kWordsEe/Components/Pages/Word.razor b/My1kWordsEe/Components/Pages/Word.razor index f1b8d05d..1c6ff41a 100644 --- a/My1kWordsEe/Components/Pages/Word.razor +++ b/My1kWordsEe/Components/Pages/Word.razor @@ -8,8 +8,8 @@ @using My1kWordsEe.Services.Cqs; @inject IJSRuntime JS -@inject EnsureWordCommand EnsureWordCommand -@inject CreateSampleCommand CreateSampleCommand +@inject GetOrAddSampleWordCommand EnsureWordCommand +@inject AddSampleSentenceCommand CreateSampleCommand @code { [Parameter] diff --git a/My1kWordsEe/Models/Ee1kWords.cs b/My1kWordsEe/Models/Ee1kWords.cs index 21ccb3f3..ea842826 100644 --- a/My1kWordsEe/Models/Ee1kWords.cs +++ b/My1kWordsEe/Models/Ee1kWords.cs @@ -4,6 +4,9 @@ namespace My1kWordsEe.Models { + /// + /// Represents a collection of 1k most common Estonian words. + /// public class Ee1kWords { public Ee1kWords() @@ -32,7 +35,7 @@ public Ee1kWords WithSearch(string search) Search = search, SelectedWords = AllWords .Where(w => w.Value.Contains(search, StringComparison.InvariantCultureIgnoreCase) - || _allWordsDiacriticsFree[w.Value].Contains(search, StringComparison.InvariantCultureIgnoreCase)) + || AllWordsDiacriticsFree[w.Value].Contains(search, StringComparison.InvariantCultureIgnoreCase)) .ToArray() }; } @@ -56,10 +59,10 @@ public Ee1kWords WithSelectedWord(string selectedWord) public static readonly EeWord[] AllWords = Load1kEeWords(); - private static readonly IReadOnlyDictionary _allWordsDiacriticsFree = + private static readonly IReadOnlyDictionary AllWordsDiacriticsFree = AllWords.ToDictionary(w => w.Value, q => RemoveDiacritics(q.Value)); - static string RemoveDiacritics(string stIn) + private static string RemoveDiacritics(string stIn) { string stFormD = stIn.Normalize(NormalizationForm.FormD); StringBuilder sb = new StringBuilder(); diff --git a/My1kWordsEe/Models/EeWord.cs b/My1kWordsEe/Models/EeWord.cs index 2cc710eb..1f79c5d5 100644 --- a/My1kWordsEe/Models/EeWord.cs +++ b/My1kWordsEe/Models/EeWord.cs @@ -2,6 +2,10 @@ namespace My1kWordsEe.Models { + /// + /// currently serves as view model for the 1k words page + /// todo: must be unified with + /// [JsonSourceGenerationOptions(UseStringEnumConverter = true)] [JsonSerializable(typeof(EePartOfSpeech))] public record EeWord diff --git a/My1kWordsEe/Models/PartsOfSpeech.cs b/My1kWordsEe/Models/PartsOfSpeech.cs index 31eede81..d3139694 100644 --- a/My1kWordsEe/Models/PartsOfSpeech.cs +++ b/My1kWordsEe/Models/PartsOfSpeech.cs @@ -54,6 +54,9 @@ public enum EePartOfSpeech Kaassõna } + /// + /// Standard English parts of speech + /// public enum EnPartOfSpeech { Noun, diff --git a/My1kWordsEe/Models/SampleSentence.cs b/My1kWordsEe/Models/SampleSentence.cs index 3503635a..6c6adf45 100644 --- a/My1kWordsEe/Models/SampleSentence.cs +++ b/My1kWordsEe/Models/SampleSentence.cs @@ -1,15 +1,33 @@ namespace My1kWordsEe.Models { - public class SampleSentence + /// + /// Sample sentence illustrating the use of a give Estonian word + /// + public record SampleSentence { - public string EeWord { get; set; } + /// + /// Target Estonian word + /// + public string EeWord { get; init; } - public string EeSentence { get; set; } + /// + /// Illustrative sentence in Estonian + /// + public string EeSentence { get; init; } - public string EnSentence { get; set; } + /// + /// Translation of the illustrative sentence in English + /// + public string EnSentence { get; init; } - public Uri EeAudioUrl { get; set; } + /// + /// Sentence spoken in Estonian + /// + public Uri EeAudioUrl { get; init; } - public Uri ImageUrl { get; set; } + /// + /// Image associated with the sentence + /// + public Uri ImageUrl { get; init; } } } diff --git a/My1kWordsEe/Models/SampleWord.cs b/My1kWordsEe/Models/SampleWord.cs index 5bcbedc0..04edf74f 100644 --- a/My1kWordsEe/Models/SampleWord.cs +++ b/My1kWordsEe/Models/SampleWord.cs @@ -1,5 +1,8 @@ namespace My1kWordsEe.Models { + /// + /// A word of the Estonian language with the respective translations and usage examples + /// public record SampleWord { private readonly string eeWord = ""; @@ -21,7 +24,7 @@ public string EeWord public string EnWord { get; init; } /// - /// Alternatives to EnWord + /// Alternative translations to English /// public string[] EnWords { @@ -29,10 +32,19 @@ public string[] EnWords init => this.enWords = value ?? this.enWords; } + /// + /// Explaining the word in English + /// public string EnExplanation { get; init; } + /// + /// Sample pronunciation of the word + /// public Uri EeAudioUrl { get; init; } + /// + /// Word usage examples + /// public SampleSentence[] Samples { get => this.samples; diff --git a/My1kWordsEe/Program.cs b/My1kWordsEe/Program.cs index dc51ae2b..0e0955e4 100644 --- a/My1kWordsEe/Program.cs +++ b/My1kWordsEe/Program.cs @@ -41,11 +41,16 @@ public static void Main(string[] args) } builder.Services.AddSingleton(new StabilityAiService(stabilityAiKey)); - builder.Services.AddSingleton((p) => new OpenAiService(p.GetRequiredService>(), openAiKey)); - builder.Services.AddSingleton(new AzureBlobService(azureBlobConnectionString)); - builder.Services.AddSingleton(new TartuNlpService()); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + builder.Services.AddSingleton((p) => new OpenAiService( + p.GetRequiredService>(), openAiKey)); + builder.Services.AddSingleton((p) => new AzureBlobService( + p.GetRequiredService>(), azureBlobConnectionString)); + builder.Services.AddSingleton((p) => new TartuNlpService( + p.GetRequiredService>())); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); // Add services to the container. builder.Services.AddRazorComponents() diff --git a/My1kWordsEe/Services/Cqs/AddAudioCommand.cs b/My1kWordsEe/Services/Cqs/AddAudioCommand.cs new file mode 100644 index 00000000..df3184a6 --- /dev/null +++ b/My1kWordsEe/Services/Cqs/AddAudioCommand.cs @@ -0,0 +1,24 @@ +using CSharpFunctionalExtensions; + +using My1kWordsEe.Services.Db; + +namespace My1kWordsEe.Services.Cqs +{ + public class AddAudioCommand + { + private readonly TartuNlpService tartuNlpService; + private readonly AzureBlobService azureBlobService; + + public AddAudioCommand( + TartuNlpService tartuNlpService, + AzureBlobService azureBlobService) + { + this.tartuNlpService = tartuNlpService; + this.azureBlobService = azureBlobService; + } + + public Task> Invoke(string text) => + this.tartuNlpService.GetSpeech(text).Bind( + this.azureBlobService.SaveAudio); + } +} diff --git a/My1kWordsEe/Services/Cqs/CreateSampleCommand.cs b/My1kWordsEe/Services/Cqs/AddSampleSentenceCommand.cs similarity index 80% rename from My1kWordsEe/Services/Cqs/CreateSampleCommand.cs rename to My1kWordsEe/Services/Cqs/AddSampleSentenceCommand.cs index 3dc11ac7..ff6075b8 100644 --- a/My1kWordsEe/Services/Cqs/CreateSampleCommand.cs +++ b/My1kWordsEe/Services/Cqs/AddSampleSentenceCommand.cs @@ -5,22 +5,22 @@ namespace My1kWordsEe.Services.Cqs { - public class CreateSampleCommand + public class AddSampleSentenceCommand { private readonly AzureBlobService azureBlobService; private readonly OpenAiService openAiService; - private readonly TartuNlpService tartuNlpService; + private readonly AddAudioCommand addAudioCommand; private readonly StabilityAiService stabilityAiService; - public CreateSampleCommand( + public AddSampleSentenceCommand( AzureBlobService azureBlobService, OpenAiService openAiService, - TartuNlpService tartuNlpService, + AddAudioCommand createAudioCommand, StabilityAiService stabilityAiService) { this.azureBlobService = azureBlobService; this.openAiService = openAiService; - this.tartuNlpService = tartuNlpService; + this.addAudioCommand = createAudioCommand; this.stabilityAiService = stabilityAiService; } @@ -64,12 +64,11 @@ public async Task> Invoke(SampleWord word) } private Task> GenerateImage(Sentence sentence) => - this.openAiService.GetDallEPrompt(sentence.En) - .Bind(this.stabilityAiService.GenerateImage) - .Bind(image => Result.Of(this.azureBlobService.SaveImage(image))); + this.openAiService.GetDallEPrompt(sentence.En).Bind( + this.stabilityAiService.GenerateImage).Bind( + this.azureBlobService.SaveImage); private Task> GenerateSpeech(Sentence sentence) => - this.tartuNlpService.GetSpeech(sentence.Ee) - .Bind(speech => Result.Of(this.azureBlobService.SaveAudio(speech))); + this.addAudioCommand.Invoke(sentence.Ee); } } \ No newline at end of file diff --git a/My1kWordsEe/Services/Cqs/AddSampleWordCommand.cs b/My1kWordsEe/Services/Cqs/AddSampleWordCommand.cs new file mode 100644 index 00000000..d261d259 --- /dev/null +++ b/My1kWordsEe/Services/Cqs/AddSampleWordCommand.cs @@ -0,0 +1,47 @@ +using CSharpFunctionalExtensions; + +using My1kWordsEe.Models; +using My1kWordsEe.Services.Db; + +namespace My1kWordsEe.Services.Cqs +{ + public class AddSampleWordCommand + { + private readonly OpenAiService openAiService; + private readonly AzureBlobService azureBlobService; + private readonly AddAudioCommand addAudioCommand; + + public AddSampleWordCommand( + OpenAiService openAiService, + AzureBlobService azureBlobService, + AddAudioCommand createAudioCommand) + { + this.azureBlobService = azureBlobService; + this.openAiService = openAiService; + this.addAudioCommand = createAudioCommand; + } + + public async Task> Invoke(string eeWord) + { + (await openAiService.GetWordMetadata(eeWord)).Deconstruct( + out bool _, + out bool isAiFailure, + out SampleWord sampleWord, + out string aiError); + + if (isAiFailure) + { + return Result.Failure(aiError); + } + + (await this.addAudioCommand.Invoke(eeWord)).Deconstruct( + out bool isAudioSaved, + out bool _, + out Uri audioUri); + + sampleWord = isAudioSaved ? sampleWord with { EeAudioUrl = audioUri } : sampleWord; + + return (await azureBlobService.SaveWordData(sampleWord)).Bind(_ => Result.Of(sampleWord)); + } + } +} diff --git a/My1kWordsEe/Services/Cqs/EnsureWordCommand.cs b/My1kWordsEe/Services/Cqs/EnsureWordCommand.cs deleted file mode 100644 index 7a91d787..00000000 --- a/My1kWordsEe/Services/Cqs/EnsureWordCommand.cs +++ /dev/null @@ -1,41 +0,0 @@ -using CSharpFunctionalExtensions; - -using My1kWordsEe.Models; -using My1kWordsEe.Services.Db; - -namespace My1kWordsEe.Services.Cqs -{ - public class EnsureWordCommand - { - private readonly AzureBlobService azureBlobService; - private readonly OpenAiService openAiService; - - public EnsureWordCommand( - AzureBlobService azureBlobService, - OpenAiService openAiService) - { - this.azureBlobService = azureBlobService; - this.openAiService = openAiService; - } - - public async Task> Invoke(string eeWord) - { - var existingRecord = await azureBlobService.GetWordData(eeWord); - - if (existingRecord.IsSuccess) - { - return existingRecord; - } - - var sampleWord = await openAiService.GetWordMetadata(eeWord); - - if (sampleWord.IsSuccess) - { - // generate audio - await azureBlobService.SaveWordData(sampleWord.Value); - } - - return sampleWord; - } - } -} \ No newline at end of file diff --git a/My1kWordsEe/Services/Cqs/GetOrAddSampleWordCommand.cs b/My1kWordsEe/Services/Cqs/GetOrAddSampleWordCommand.cs new file mode 100644 index 00000000..628b11c1 --- /dev/null +++ b/My1kWordsEe/Services/Cqs/GetOrAddSampleWordCommand.cs @@ -0,0 +1,42 @@ +using CSharpFunctionalExtensions; + +using My1kWordsEe.Models; +using My1kWordsEe.Services.Db; + +namespace My1kWordsEe.Services.Cqs +{ + public class GetOrAddSampleWordCommand + { + private readonly AzureBlobService azureBlobService; + private readonly AddSampleWordCommand addSampleWordCommand; + + public GetOrAddSampleWordCommand( + AzureBlobService azureBlobService, + AddSampleWordCommand addSampleWordCommand) + { + this.azureBlobService = azureBlobService; + this.addSampleWordCommand = addSampleWordCommand; + } + + public async Task> Invoke(string eeWord) + { + (await azureBlobService.GetWordData(eeWord)).Deconstruct( + out bool _, + out bool isBlobAccessFailure, + out Maybe savedWord, + out string blobAccessError); + + if (isBlobAccessFailure) + { + return Result.Failure(blobAccessError); + } + + if (savedWord.HasValue) + { + return savedWord.Value; + } + + return await this.addSampleWordCommand.Invoke(eeWord); + } + } +} \ No newline at end of file diff --git a/My1kWordsEe/Services/Db/AzureBlobService.cs b/My1kWordsEe/Services/Db/AzureBlobService.cs index 392079b0..67a07388 100644 --- a/My1kWordsEe/Services/Db/AzureBlobService.cs +++ b/My1kWordsEe/Services/Db/AzureBlobService.cs @@ -1,5 +1,6 @@ using System.Text.Json; +using Azure; using Azure.Storage.Blobs; using CSharpFunctionalExtensions; @@ -8,82 +9,119 @@ namespace My1kWordsEe.Services.Db { + /// + /// Facade for Azure blob storage API + /// public class AzureBlobService { + private readonly ILogger logger; private readonly string connectionString; - public AzureBlobService(string connectionString) + public AzureBlobService( + ILogger logger, + string connectionString) { + this.logger = logger; this.connectionString = connectionString; } - public async Task> GetWordData(string word) + public async Task>> GetWordData(string word) { - BlobContainerClient container = await GetWordsContainer(); - BlobClient blob = container.GetBlobClient(JsonBlobName(word)); + var container = await GetWordsContainer(); - if (await blob.ExistsAsync()) + if (container.IsFailure) + { + return Result.Failure>(container.Error); + } + + BlobClient blob = container.Value.GetBlobClient(JsonBlobName(word)); + + if (!await blob.ExistsAsync()) + { + return Maybe.None; + } + + try { var response = await blob.DownloadContentAsync(); if (response != null && response.HasValue) { var sampleWord = JsonSerializer.Deserialize(response.Value.Content); - - if (sampleWord != null) - { - return Result.Success(sampleWord); - } + return Maybe.From(sampleWord); + } + else + { + return Maybe.None; } } - - return Result.Failure($"Word '{word}' is not recorded"); + catch (RequestFailedException ex) + { + this.logger.LogError(ex, "Failure to download data from blob {name}", blob.Name); + return Result.Failure>("Failure to download data from blob"); + } + catch (Exception ex) when (ex is JsonException || ex is NotSupportedException) + { + this.logger.LogError(ex, "Failure to parse JSON from blob {name}", blob.Name); + return Result.Failure>("Failure to parse JSON data from blob"); + } } - public async Task SaveWordData(SampleWord word) - { - BlobContainerClient container = await GetWordsContainer(); + public Task> SaveWordData(SampleWord word) => + this.GetWordsContainer().Bind(container => + this.UploadStreamAsync( + container.GetBlobClient(JsonBlobName(word.EeWord)), + new MemoryStream(JsonSerializer.SerializeToUtf8Bytes(word)))); - // Get a reference to a blob - BlobClient blob = container.GetBlobClient(JsonBlobName(word.EeWord)); + public Task> SaveAudio(Stream audioStream, string blobName) => + this.GetAudioContainer().Bind((container) => + this.UploadStreamAsync( + container.GetBlobClient(blobName), + audioStream)); - // Upload file data - await blob.UploadAsync( - new MemoryStream(JsonSerializer.SerializeToUtf8Bytes(word)), - overwrite: true); - } + public Task> SaveAudio(Stream audioStream) => + this.SaveAudio(audioStream, WavBlobName()); - public async Task SaveAudio(Stream audioStream, string blobName) - { - BlobContainerClient container = await GetAudioContainer(); - BlobClient blob = container.GetBlobClient(blobName); - await blob.UploadAsync(audioStream, overwrite: true); - return blob.Uri; - } - - public Task SaveAudio(Stream audioStream) => SaveAudio(audioStream, WavBlobName()); - - public async Task SaveImage(Stream imageStream) - { - BlobContainerClient container = await GetImageContainer(); - BlobClient blob = container.GetBlobClient(JpgBlobName()); - await blob.UploadAsync(imageStream); - return blob.Uri; - } + public Task> SaveImage(Stream imageStream) => + this.GetImageContainer().Bind((container) => + this.UploadStreamAsync( + container.GetBlobClient(JpgBlobName()), + imageStream)); + private Task> GetWordsContainer() => this.GetOrCreateContainer("words"); - private async Task GetWordsContainer() => await this.GetContainer("words"); + private Task> GetAudioContainer() => this.GetOrCreateContainer("audio"); - private async Task GetAudioContainer() => await this.GetContainer("audio"); + private Task> GetImageContainer() => this.GetOrCreateContainer("image"); - private async Task GetImageContainer() => await this.GetContainer("image"); + private async Task> GetOrCreateContainer(string containerId) + { + try + { + var container = new BlobContainerClient( + this.connectionString, + containerId); + await container.CreateIfNotExistsAsync(); + return container; + } + catch (RequestFailedException exception) + { + this.logger.LogError(exception, "Failure to get or create container {ContainerId}", containerId); + return Result.Failure("Azure storage access error"); + } + } - private async Task GetContainer(string containerId) + private async Task> UploadStreamAsync(BlobClient blob, Stream stream) { - BlobContainerClient container = new BlobContainerClient( - this.connectionString, - containerId); - await container.CreateIfNotExistsAsync(); - return container; + try + { + await blob.UploadAsync(stream, overwrite: true); + return blob.Uri; + } + catch (RequestFailedException exception) + { + this.logger.LogError(exception, "Failure to upload data to blob {name}", blob.Name); + return Result.Failure("Azure storage upload error"); + } } static string JsonBlobName(string word) => word.ToLower() + ".json"; diff --git a/My1kWordsEe/Services/OpenAiService.cs b/My1kWordsEe/Services/OpenAiService.cs index e3f30687..f67118d6 100644 --- a/My1kWordsEe/Services/OpenAiService.cs +++ b/My1kWordsEe/Services/OpenAiService.cs @@ -180,28 +180,28 @@ public async Task> GetWordMetadata(string word) return Result.Failure("Empty response"); } } -} -public class Sentence -{ - [JsonPropertyName("ee_sentence")] - public string Ee { get; set; } + public class Sentence + { + [JsonPropertyName("ee_sentence")] + public string Ee { get; set; } - [JsonPropertyName("en_sentence")] - public string En { get; set; } -} + [JsonPropertyName("en_sentence")] + public string En { get; set; } + } -public class WordMetadata -{ - [JsonPropertyName("ee_word")] - public string EeWord { get; set; } + public class WordMetadata + { + [JsonPropertyName("ee_word")] + public string EeWord { get; set; } - [JsonPropertyName("en_word")] - public string EnWord { get; set; } + [JsonPropertyName("en_word")] + public string EnWord { get; set; } - [JsonPropertyName("en_explanation")] - public string EnExplanation { get; set; } + [JsonPropertyName("en_explanation")] + public string EnExplanation { get; set; } - [JsonPropertyName("en_words")] - public string[] EnWords { get; set; } -} \ No newline at end of file + [JsonPropertyName("en_words")] + public string[] EnWords { get; set; } + } +} diff --git a/My1kWordsEe/Services/StabilityAiService.cs b/My1kWordsEe/Services/StabilityAiService.cs index 0ce4970e..214c789e 100644 --- a/My1kWordsEe/Services/StabilityAiService.cs +++ b/My1kWordsEe/Services/StabilityAiService.cs @@ -7,6 +7,9 @@ namespace My1kWordsEe.Services { + /// + /// Facade for https://platform.stability.ai/docs/getting-started/stable-image + /// public class StabilityAiService { public const string ApiHost = "https://api.stability.ai"; diff --git a/My1kWordsEe/Services/TartuNlpService.cs b/My1kWordsEe/Services/TartuNlpService.cs index 2478dd84..69c495a3 100644 --- a/My1kWordsEe/Services/TartuNlpService.cs +++ b/My1kWordsEe/Services/TartuNlpService.cs @@ -1,10 +1,21 @@ using System.Net.Http.Headers; + using CSharpFunctionalExtensions; namespace My1kWordsEe.Services { + /// + /// Facade for https://neurokone.ee/. + /// public class TartuNlpService { + private readonly ILogger logger; + + public TartuNlpService(ILogger logger) + { + this.logger = logger; + } + public async Task> GetSpeech(string text) { using HttpClient client = new HttpClient(); @@ -13,20 +24,33 @@ public async Task> GetSpeech(string text) request.Headers.Add("accept", "audio/wav"); - request.Content = new StringContent($"{{\n\"text\": \"{text}\",\n\"speaker\": \"mari\",\n\"speed\": 0.64\n}}"); + request.Content = new StringContent( + $"{{\n\"text\": \"{text}\",\n\"speaker\": \"mari\",\n\"speed\": 0.64\n}}"); request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json; charset=utf-8"); - HttpResponseMessage response = await client.SendAsync(request); - - if (response.IsSuccessStatusCode) + try { - var stream = await response.Content.ReadAsStreamAsync(); - return Result.Success(stream); + HttpResponseMessage response = await client.SendAsync(request); + + if (response.IsSuccessStatusCode) + { + var stream = await response.Content.ReadAsStreamAsync(); + return Result.Success(stream); + } + else + { + var errorStr = await response.Content.ReadAsStringAsync(); + this.logger.LogError( + "Tartu NLP HTTP error. Reason phrase: {reason}. Content: {content}", + response.ReasonPhrase, + errorStr); + return Result.Failure($"Tartu NLP HTTP error. {response.ReasonPhrase}. {errorStr}"); + } } - else + catch (HttpRequestException httpException) { - var errorStr = await response.Content.ReadAsStringAsync(); - return Result.Failure($"{response.ReasonPhrase}: {errorStr}"); + this.logger.LogError(httpException, "Tartu NLP HTTP exception"); + return Result.Failure($"Tartu NLP HTTP exception"); } } }