diff --git a/samples/ingestion/ingestion-client/BatchIngestionClient.sln b/samples/ingestion/ingestion-client/BatchIngestionClient.sln index e1cb1f1f1..c304f1dc4 100644 --- a/samples/ingestion/ingestion-client/BatchIngestionClient.sln +++ b/samples/ingestion/ingestion-client/BatchIngestionClient.sln @@ -16,6 +16,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RealtimeTranscription", "Re EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DatabaseMigrator", "DatabaseMigrator\DatabaseMigrator.csproj", "{5BD38646-D3F3-481B-909E-353750AC5384}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7BC59BB6-7DD9-4D72-8AA0-91F774D6E45A}" + ProjectSection(SolutionItems) = preProject + Setup\ArmTemplateBatch.json = Setup\ArmTemplateBatch.json + Setup\ArmTemplateRealtime.json = Setup\ArmTemplateRealtime.json + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/samples/ingestion/ingestion-client/Connector/BatchClient.cs b/samples/ingestion/ingestion-client/Connector/BatchClient.cs index 88a91b378..c5791a3e6 100644 --- a/samples/ingestion/ingestion-client/Connector/BatchClient.cs +++ b/samples/ingestion/ingestion-client/Connector/BatchClient.cs @@ -21,7 +21,7 @@ public static class BatchClient { private const string TranscriptionsBasePath = "speechtotext/v3.0/Transcriptions/"; - private const int MaxNumberOfRetries = 3; + private const int MaxNumberOfRetries = 10; private static readonly TimeSpan PostTimeout = TimeSpan.FromMinutes(1); @@ -34,7 +34,7 @@ public static class BatchClient private static readonly AsyncRetryPolicy RetryPolicy = Policy .Handle(e => e is HttpStatusCodeException || e is HttpRequestException) - .WaitAndRetryAsync(MaxNumberOfRetries, retryAttempt => TimeSpan.FromSeconds(2)); + .WaitAndRetryAsync(MaxNumberOfRetries, retryAttempt => TimeSpan.FromSeconds(5)); public static Task GetTranscriptionReportFileFromSasAsync(string sasUri) { diff --git a/samples/ingestion/ingestion-client/Connector/Enums/TranscriptionAnalyticsJobStatus.cs b/samples/ingestion/ingestion-client/Connector/Enums/TranscriptionAnalyticsJobStatus.cs new file mode 100644 index 000000000..9e8fc659c --- /dev/null +++ b/samples/ingestion/ingestion-client/Connector/Enums/TranscriptionAnalyticsJobStatus.cs @@ -0,0 +1,14 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// + +namespace Connector.Enums +{ + public enum TranscriptionAnalyticsJobStatus + { + NotSubmitted, + Running, + Completed + } +} diff --git a/samples/ingestion/ingestion-client/Connector/Serializable/TextAnalytics/TextAnalyticsRequest.cs b/samples/ingestion/ingestion-client/Connector/Serializable/TextAnalytics/TextAnalyticsRequest.cs deleted file mode 100644 index 5870f3919..000000000 --- a/samples/ingestion/ingestion-client/Connector/Serializable/TextAnalytics/TextAnalyticsRequest.cs +++ /dev/null @@ -1,23 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. -// - -namespace Connector.Serializable -{ - public class TextAnalyticsRequest - { - public TextAnalyticsRequest(string language, string id, string text) - { - this.Language = language; - this.Id = id; - this.Text = text; - } - - public string Language { get; private set; } - - public string Id { get; private set; } - - public string Text { get; private set; } - } -} diff --git a/samples/ingestion/ingestion-client/Connector/Serializable/TextAnalytics/TextAnalyticsRequestsChunk.cs b/samples/ingestion/ingestion-client/Connector/Serializable/TextAnalytics/TextAnalyticsRequestsChunk.cs deleted file mode 100644 index 8f6fda501..000000000 --- a/samples/ingestion/ingestion-client/Connector/Serializable/TextAnalytics/TextAnalyticsRequestsChunk.cs +++ /dev/null @@ -1,19 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. -// - -namespace Connector.Serializable -{ - using System.Collections.Generic; - - public class TextAnalyticsRequestsChunk - { - public TextAnalyticsRequestsChunk(IEnumerable documents) - { - this.Documents = documents; - } - - public IEnumerable Documents { get; private set; } - } -} diff --git a/samples/ingestion/ingestion-client/Connector/Serializable/TranscriptionStartedMessage/TextAnalyticsRequests.cs b/samples/ingestion/ingestion-client/Connector/Serializable/TranscriptionStartedMessage/TextAnalyticsRequests.cs index fe0cfb021..1f2dcb839 100644 --- a/samples/ingestion/ingestion-client/Connector/Serializable/TranscriptionStartedMessage/TextAnalyticsRequests.cs +++ b/samples/ingestion/ingestion-client/Connector/Serializable/TranscriptionStartedMessage/TextAnalyticsRequests.cs @@ -14,15 +14,15 @@ public TextAnalyticsRequests( IEnumerable audioLevelRequests, IEnumerable conversationRequests) { - this.UtteranceLevelRequests = utteranceLevelRequests; - this.AudioLevelRequests = audioLevelRequests; - this.ConversationRequests = conversationRequests; + this.UtteranceLevelRequests = utteranceLevelRequests ?? new List(); + this.AudioLevelRequests = audioLevelRequests ?? new List(); + this.ConversationRequests = conversationRequests ?? new List(); } - public IEnumerable UtteranceLevelRequests { get; } + public IEnumerable UtteranceLevelRequests { get; set; } - public IEnumerable AudioLevelRequests { get; } + public IEnumerable AudioLevelRequests { get; set; } - public IEnumerable ConversationRequests { get; } + public IEnumerable ConversationRequests { get; set; } } } diff --git a/samples/ingestion/ingestion-client/Connector/StorageConnector.cs b/samples/ingestion/ingestion-client/Connector/StorageConnector.cs index 73e04d443..bd0a25ac7 100644 --- a/samples/ingestion/ingestion-client/Connector/StorageConnector.cs +++ b/samples/ingestion/ingestion-client/Connector/StorageConnector.cs @@ -168,7 +168,7 @@ public async Task MoveFileAsync(string inputContainerName, string inputFileName if (!keepSource) { - await inputBlockBlobClient.DeleteAsync().ConfigureAwait(false); + await inputBlockBlobClient.DeleteIfExistsAsync().ConfigureAwait(false); } return; @@ -178,7 +178,7 @@ public async Task MoveFileAsync(string inputContainerName, string inputFileName if (!keepSource) { - await inputBlockBlobClient.DeleteAsync().ConfigureAwait(false); + await inputBlockBlobClient.DeleteIfExistsAsync().ConfigureAwait(false); } } diff --git a/samples/ingestion/ingestion-client/FetchTranscription/Database/DatabaseConfigProvider.cs b/samples/ingestion/ingestion-client/FetchTranscription/Database/DatabaseConfigProvider.cs index 00d2b7a89..b0e5511c7 100644 --- a/samples/ingestion/ingestion-client/FetchTranscription/Database/DatabaseConfigProvider.cs +++ b/samples/ingestion/ingestion-client/FetchTranscription/Database/DatabaseConfigProvider.cs @@ -3,18 +3,16 @@ // Licensed under the MIT license. See LICENSE.md file in the project root for full license information. // -using FetchTranscription.Database; +using FetchTranscription; using Microsoft.Azure.WebJobs.Hosting; [assembly: WebJobsStartup(typeof(DatabaseInitializationService), "DatabaseInitialize")] -namespace FetchTranscription.Database +namespace FetchTranscription { using Connector.Database; - using FetchTranscriptionFunction; - using Microsoft.Azure.WebJobs.Description; using Microsoft.Azure.WebJobs.Host.Config; using Microsoft.EntityFrameworkCore; diff --git a/samples/ingestion/ingestion-client/FetchTranscription/Database/DatabaseInitializationService.cs b/samples/ingestion/ingestion-client/FetchTranscription/Database/DatabaseInitializationService.cs index 26fa430f4..149d1f8aa 100644 --- a/samples/ingestion/ingestion-client/FetchTranscription/Database/DatabaseInitializationService.cs +++ b/samples/ingestion/ingestion-client/FetchTranscription/Database/DatabaseInitializationService.cs @@ -2,15 +2,14 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE.md file in the project root for full license information. // - -using FetchTranscription.Database; +using FetchTranscription; using Microsoft.Azure.WebJobs; using Microsoft.Azure.WebJobs.Hosting; [assembly: WebJobsStartup(typeof(DatabaseInitializationService), "DatabaseInitialize")] -namespace FetchTranscription.Database +namespace FetchTranscription { public class DatabaseInitializationService : IWebJobsStartup { diff --git a/samples/ingestion/ingestion-client/FetchTranscription/FetchTranscription.cs b/samples/ingestion/ingestion-client/FetchTranscription/FetchTranscription.cs index d27673ce3..e319bd343 100644 --- a/samples/ingestion/ingestion-client/FetchTranscription/FetchTranscription.cs +++ b/samples/ingestion/ingestion-client/FetchTranscription/FetchTranscription.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE.md file in the project root for full license information. // -namespace FetchTranscriptionFunction +namespace FetchTranscription { using System; using System.Threading.Tasks; diff --git a/samples/ingestion/ingestion-client/FetchTranscription/FetchTranscriptionEnvironmentVariables.cs b/samples/ingestion/ingestion-client/FetchTranscription/FetchTranscriptionEnvironmentVariables.cs index 3d9103f30..d618a3122 100644 --- a/samples/ingestion/ingestion-client/FetchTranscription/FetchTranscriptionEnvironmentVariables.cs +++ b/samples/ingestion/ingestion-client/FetchTranscription/FetchTranscriptionEnvironmentVariables.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE.md file in the project root for full license information. // -namespace FetchTranscriptionFunction +namespace FetchTranscription { using System; using Connector; @@ -59,7 +59,7 @@ public static class FetchTranscriptionEnvironmentVariables public static readonly string TextAnalyticsKey = Environment.GetEnvironmentVariable(nameof(TextAnalyticsKey), EnvironmentVariableTarget.Process); - public static readonly string TextAnalyticsRegion = Environment.GetEnvironmentVariable(nameof(TextAnalyticsRegion), EnvironmentVariableTarget.Process); + public static readonly string TextAnalyticsEndpoint = Environment.GetEnvironmentVariable(nameof(TextAnalyticsEndpoint), EnvironmentVariableTarget.Process); public static readonly string PiiCategories = Environment.GetEnvironmentVariable(nameof(PiiCategories), EnvironmentVariableTarget.Process); diff --git a/samples/ingestion/ingestion-client/FetchTranscription/Startup.cs b/samples/ingestion/ingestion-client/FetchTranscription/Startup.cs index a5d1463e3..1d6803eb3 100644 --- a/samples/ingestion/ingestion-client/FetchTranscription/Startup.cs +++ b/samples/ingestion/ingestion-client/FetchTranscription/Startup.cs @@ -13,8 +13,6 @@ namespace FetchTranscription using Connector.Database; - using FetchTranscriptionFunction; - using Microsoft.Azure.Functions.Extensions.DependencyInjection; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; diff --git a/samples/ingestion/ingestion-client/FetchTranscription/TranscriptionAnalytics/ITranscriptionAnalyticsProvider.cs b/samples/ingestion/ingestion-client/FetchTranscription/TranscriptionAnalytics/ITranscriptionAnalyticsProvider.cs new file mode 100644 index 000000000..3eeb6bfd6 --- /dev/null +++ b/samples/ingestion/ingestion-client/FetchTranscription/TranscriptionAnalytics/ITranscriptionAnalyticsProvider.cs @@ -0,0 +1,38 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// + +namespace FetchTranscription +{ + using System.Collections.Generic; + using System.Threading.Tasks; + + using Connector; + using Connector.Enums; + using Connector.Serializable.TranscriptionStartedServiceBusMessage; + + public interface ITranscriptionAnalyticsProvider + { + /// + /// Gets the status of the transcription analytics jobs that are monitored by the provider + /// + /// The audio file infos with transcription analytics jobs info + /// The overall status of all jobs monitored by the provider + Task GetTranscriptionAnalyticsJobStatusAsync(IEnumerable audioFileInfos); + + /// + /// Submits transcription analytics jobs based on the transcript in speechtranscript and sets the job ids in the corresponding audio file infos. + /// + /// The mapping from audio file info to transcript + /// The errors if any. + Task> SubmitTranscriptionAnalyticsJobsAsync(Dictionary speechTranscriptMappings); + + /// + /// Fetches the transcription analytics results and adds them to the corresponding speech transcript + /// + /// The mapping from audio file info to transcript + /// The errors if any. + Task> AddTranscriptionAnalyticsResultsToTranscriptsAsync(Dictionary speechTranscriptMappings); + } +} diff --git a/samples/ingestion/ingestion-client/FetchTranscription/Language/AnalyzeConversationsProvider.cs b/samples/ingestion/ingestion-client/FetchTranscription/TranscriptionAnalytics/Language/AnalyzeConversationsProvider.cs similarity index 71% rename from samples/ingestion/ingestion-client/FetchTranscription/Language/AnalyzeConversationsProvider.cs rename to samples/ingestion/ingestion-client/FetchTranscription/TranscriptionAnalytics/Language/AnalyzeConversationsProvider.cs index 3068eed19..8633a7632 100644 --- a/samples/ingestion/ingestion-client/FetchTranscription/Language/AnalyzeConversationsProvider.cs +++ b/samples/ingestion/ingestion-client/FetchTranscription/TranscriptionAnalytics/Language/AnalyzeConversationsProvider.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE.md file in the project root for full license information. // -namespace Language +namespace FetchTranscription { using System; using System.Collections.Generic; @@ -17,11 +17,11 @@ namespace Language using Connector; using Connector.Constants; + using Connector.Enums; using Connector.Serializable.Language.Conversations; + using Connector.Serializable.TextAnalytics; using Connector.Serializable.TranscriptionStartedServiceBusMessage; - using FetchTranscriptionFunction; - using Microsoft.Extensions.Logging; using Newtonsoft.Json; @@ -31,7 +31,7 @@ namespace Language /// /// Analyze Conversations async client. /// - public class AnalyzeConversationsProvider + public class AnalyzeConversationsProvider : ITranscriptionAnalyticsProvider { private const string DefaultInferenceSource = "lexical"; private static readonly TimeSpan RequestTimeout = TimeSpan.FromMinutes(3); @@ -39,28 +39,171 @@ public class AnalyzeConversationsProvider private readonly string locale; private readonly ILogger log; - public AnalyzeConversationsProvider(string locale, string subscriptionKey, string region, ILogger log) + public AnalyzeConversationsProvider(string locale, string subscriptionKey, string endpoint, ILogger log) { - this.conversationAnalysisClient = new ConversationAnalysisClient(new Uri($"https://{region}.api.cognitive.microsoft.com"), new AzureKeyCredential(subscriptionKey)); - + this.conversationAnalysisClient = new ConversationAnalysisClient(new Uri(endpoint), new AzureKeyCredential(subscriptionKey)); this.locale = locale; this.log = log; } public static bool IsConversationalPiiEnabled() { - return FetchTranscriptionEnvironmentVariables.ConversationPiiSetting != Connector.Enums.ConversationPiiSetting.None; + return FetchTranscriptionEnvironmentVariables.ConversationPiiSetting != ConversationPiiSetting.None; } public static bool IsConversationalSummarizationEnabled() => FetchTranscriptionEnvironmentVariables.ConversationSummarizationOptions.Enabled; + /// + public async Task GetTranscriptionAnalyticsJobStatusAsync(IEnumerable audioFileInfos) + { + if (!IsConversationalPiiEnabled() && !IsConversationalSummarizationEnabled()) + { + return TranscriptionAnalyticsJobStatus.Completed; + } + + if (!audioFileInfos.Where(audioFileInfo => audioFileInfo.TextAnalyticsRequests?.ConversationRequests != null).Any()) + { + return TranscriptionAnalyticsJobStatus.NotSubmitted; + } + + var conversationRequests = audioFileInfos + .Where(audioFileInfo => audioFileInfo.TextAnalyticsRequests?.ConversationRequests != null) + .SelectMany(audioFileInfo => audioFileInfo.TextAnalyticsRequests.ConversationRequests) + .Where(text => text.Status == TextAnalyticsRequestStatus.Running); + + foreach (var textAnalyticsJob in conversationRequests) + { + var response = await this.conversationAnalysisClient.GetAnalyzeConversationJobStatusAsync(Guid.Parse(textAnalyticsJob.Id)).ConfigureAwait(false); + + if (response.IsError) + { + continue; + } + + var analysisResult = JsonConvert.DeserializeObject(response.Content.ToString()); + + if (analysisResult.Tasks.InProgress != 0) + { + return TranscriptionAnalyticsJobStatus.Running; + } + } + + return TranscriptionAnalyticsJobStatus.Completed; + } + + /// + public async Task> SubmitTranscriptionAnalyticsJobsAsync(Dictionary speechTranscriptMappings) + { + _ = speechTranscriptMappings ?? throw new ArgumentNullException(nameof(speechTranscriptMappings)); + + var errors = new List(); + foreach (var speechTranscriptMapping in speechTranscriptMappings) + { + var speechTranscript = speechTranscriptMapping.Value; + var audioFileInfo = speechTranscriptMapping.Key; + + var fileName = audioFileInfo.FileName; + + if (speechTranscript.RecognizedPhrases != null && speechTranscript.RecognizedPhrases.All(phrase => phrase.RecognitionStatus.Equals("Success", StringComparison.Ordinal))) + { + var textAnalyticsErrors = new List(); + + (var conversationJobIds, var conversationErrors) = await this.SubmitAnalyzeConversationsRequestAsync(speechTranscript).ConfigureAwait(false); + + var conversationalRequests = conversationJobIds?.Select(jobId => new TextAnalyticsRequest(jobId, TextAnalyticsRequestStatus.Running)); + textAnalyticsErrors.AddRange(conversationErrors); + + if (audioFileInfo.TextAnalyticsRequests == null) + { + audioFileInfo.TextAnalyticsRequests = new TextAnalyticsRequests(null, null, conversationalRequests); + } + else + { + audioFileInfo.TextAnalyticsRequests.ConversationRequests = conversationalRequests; + } + + if (textAnalyticsErrors.Any()) + { + var distinctErrors = textAnalyticsErrors.Distinct(); + var errorMessage = $"File {(string.IsNullOrEmpty(fileName) ? "unknown" : fileName)}:\n{string.Join('\n', distinctErrors)}"; + errors.Add(errorMessage); + } + } + } + + return errors; + } + + /// + public async Task> AddTranscriptionAnalyticsResultsToTranscriptsAsync(Dictionary speechTranscriptMappings) + { + _ = speechTranscriptMappings ?? throw new ArgumentNullException(nameof(speechTranscriptMappings)); + + var errors = new List(); + foreach (var speechTranscriptMapping in speechTranscriptMappings) + { + var speechTranscript = speechTranscriptMapping.Value; + var audioFileInfo = speechTranscriptMapping.Key; + var fileName = audioFileInfo.FileName; + + var textAnalyticsErrors = new List(); + if (audioFileInfo.TextAnalyticsRequests?.ConversationRequests?.Any() == true) + { + var conversationalAnalyticsErrors = await this.AddConversationalEntitiesAsync(audioFileInfo.TextAnalyticsRequests.ConversationRequests.Select(request => request.Id), speechTranscript).ConfigureAwait(false); + textAnalyticsErrors.AddRange(conversationalAnalyticsErrors); + } + + if (textAnalyticsErrors.Any()) + { + var distinctErrors = textAnalyticsErrors.Distinct(); + var errorMessage = $"File {(string.IsNullOrEmpty(fileName) ? "unknown" : fileName)}:\n{string.Join('\n', distinctErrors)}"; + errors.Add(errorMessage); + } + } + + return errors; + } + + private static IEnumerable GetAllErrorsFromResults((IEnumerable piiResults, IEnumerable summarizationResults, IEnumerable errors)[] results) + { + var resultErrors = new List(); + + if (results == null || !results.Any()) + { + return resultErrors; + } + + foreach (var (piiResults, summarizationResults, errors) in results) + { + var piiErrors = piiResults? + .Where(pr => pr.Errors?.Any() ?? false)? + .SelectMany(e => e.Errors); + + if (piiErrors != null) + { + resultErrors.AddRange(piiErrors); + } + + var summarizeErrors = summarizationResults? + .Where(sr => sr.Errors?.Any() ?? false)? + .SelectMany(sr => sr.Errors); + + if (summarizeErrors != null) + { + resultErrors.AddRange(summarizeErrors); + } + } + + return resultErrors; + } + /// /// API to submit an analyzeConversations async Request. /// /// Instance of the speech transcript. /// An enumerable of the jobs IDs and errors if any. - public async Task<(IEnumerable jobIds, IEnumerable errors)> SubmitAnalyzeConversationsRequestAsync(SpeechTranscript speechTranscript) + private async Task<(IEnumerable jobIds, IEnumerable errors)> SubmitAnalyzeConversationsRequestAsync(SpeechTranscript speechTranscript) { speechTranscript = speechTranscript ?? throw new ArgumentNullException(nameof(speechTranscript)); var data = new List(); @@ -75,7 +218,7 @@ public static bool IsConversationalSummarizationEnabled() /// /// Enumerable of conversational jobIds. /// Enumerable of results of conversation PII redaction and errors encountered if any. - public async Task<(AnalyzeConversationPiiResults piiResults, AnalyzeConversationSummarizationResults summarizationResults, IEnumerable errors)> GetConversationsOperationsResult(IEnumerable jobIds) + private async Task<(AnalyzeConversationPiiResults piiResults, AnalyzeConversationSummarizationResults summarizationResults, IEnumerable errors)> GetConversationsOperationsResult(IEnumerable jobIds) { var errors = new List(); if (!jobIds.Any()) @@ -83,10 +226,10 @@ public static bool IsConversationalSummarizationEnabled() return (null, null, errors); } - var tasks = jobIds.Select(async jobId => await this.GetConversationsOperationResults(jobId).ConfigureAwait(false)); + var tasks = jobIds.Select(this.GetConversationsOperationResults); var results = await Task.WhenAll(tasks).ConfigureAwait(false); - var resultsErrors = results.SelectMany(result => result.piiResults).SelectMany(s => s.Errors).Concat(results.SelectMany(result => result.summarizationResults).SelectMany(s => s.Errors)); + var resultsErrors = GetAllErrorsFromResults(results); if (resultsErrors.Any()) { errors.AddRange(resultsErrors.Select(s => $"Error thrown for conversation : {s.Id}")); @@ -143,50 +286,13 @@ public static bool IsConversationalSummarizationEnabled() return (piiResults, summarizationResults, errors); } - /// - /// Checks for all conversational analytics requests that were marked as running if they have completed and sets a new state accordingly. - /// - /// Enumerable for audioFiles. - /// True if all requests completed, else false. - public async Task ConversationalRequestsCompleted(IEnumerable audioFileInfos) - { - if (!(IsConversationalPiiEnabled() || IsConversationalSummarizationEnabled()) || !audioFileInfos.Where(audioFileInfo => audioFileInfo.TextAnalyticsRequests.ConversationRequests != null).Any()) - { - return true; - } - - var conversationRequests = audioFileInfos.SelectMany(audioFileInfo => audioFileInfo.TextAnalyticsRequests.ConversationRequests).Where(text => text.Status == TextAnalyticsRequestStatus.Running); - - var runningJobsCount = 0; - - foreach (var textAnalyticsJob in conversationRequests) - { - var response = await this.conversationAnalysisClient.GetAnalyzeConversationJobStatusAsync(Guid.Parse(textAnalyticsJob.Id)).ConfigureAwait(false); - - if (response.IsError) - { - continue; - } - - var analysisResult = JsonConvert.DeserializeObject(response.Content.ToString()); - - if (analysisResult.Tasks.InProgress != 0) - { - // some jobs are still running. - runningJobsCount++; - } - } - - return runningJobsCount == 0; - } - /// /// Gets the (audio-level) results from text analytics, adds the results to the speech transcript. /// /// The conversation analysis job Ids. /// The speech transcript object. /// The errors, if any. - public async Task> AddConversationalEntitiesAsync( + private async Task> AddConversationalEntitiesAsync( IEnumerable conversationJobIds, SpeechTranscript speechTranscript) { @@ -373,7 +479,7 @@ private void PreparePiiRequest(SpeechTranscript speechTranscript, List piiResults, IEnumerable summarizationResults, IEnumerable errors)> GetConversationsOperationResults(string jobId) { + var piiResults = new List(); + var summarizationResults = new List(); var errors = new List(); try { @@ -462,14 +570,14 @@ private void PreparePiiRequest(SpeechTranscript speechTranscript, List item.Kind == AnalyzeConversationsTaskResultKind.conversationalPIIResults) - .Select(s => s as ConversationPiiItem) - .Select(s => s.Results); - var summarizationResults = analysisResult.Tasks - .Items.Where(item => item.Kind == AnalyzeConversationsTaskResultKind.conversationalSummarizationResults) - .Select(s => s as ConversationSummarizationItem) - .Select(s => s.Results); + piiResults.AddRange(analysisResult.Tasks + .Items + .Where(item => item.Kind == AnalyzeConversationsTaskResultKind.conversationalPIIResults && (item as ConversationPiiItem)?.Results != null) + .Select(s => ((ConversationPiiItem)s).Results)); + summarizationResults.AddRange(analysisResult.Tasks + .Items + .Where(item => item.Kind == AnalyzeConversationsTaskResultKind.conversationalSummarizationResults && (item as ConversationSummarizationItem)?.Results != null) + .Select(s => ((ConversationSummarizationItem)s).Results)); return (piiResults, summarizationResults, errors); } } @@ -484,7 +592,7 @@ private void PreparePiiRequest(SpeechTranscript speechTranscript, List -namespace TextAnalytics +namespace FetchTranscription { using System; using System.Collections.Generic; @@ -18,34 +18,11 @@ namespace TextAnalytics using Connector.Enums; using Connector.Serializable.TranscriptionStartedServiceBusMessage; - using FetchTranscriptionFunction; - - using Language; - using Microsoft.Extensions.Logging; using static Connector.Serializable.TranscriptionStartedServiceBusMessage.TextAnalyticsRequest; - /// - /// The text analytics provide. - /// - /// General overview of text analytics request processing: - /// - /// For a succeded transcription, check if transcription has text analytics job info. - /// if true: - /// Check if text analytics job terminated. - /// if true: - /// Add text analytics results to transcript, write transcript to storage. - /// if false: - /// Re-enqueue job, check again after X minutes. - /// if false: - /// Check if text analytics is requested - /// if true: - /// Add text analytics job info to transcription. Re-enqueue job, check again after X minutes. - /// if false: - /// Write transcript to storage. - /// - public class TextAnalyticsProvider + public class TextAnalyticsProvider : ITranscriptionAnalyticsProvider { private const int MaxRecordsPerRequest = 25; @@ -57,23 +34,30 @@ public class TextAnalyticsProvider private readonly ILogger log; - public TextAnalyticsProvider(string locale, string subscriptionKey, string region, ILogger log) + public TextAnalyticsProvider(string locale, string subscriptionKey, string endpoint, ILogger log) { - this.textAnalyticsClient = new TextAnalyticsClient(new Uri($"https://{region}.api.cognitive.microsoft.com"), new AzureKeyCredential(subscriptionKey)); + this.textAnalyticsClient = new TextAnalyticsClient(new Uri(endpoint), new AzureKeyCredential(subscriptionKey)); this.locale = locale; this.log = log; } - /// - /// Checks for all text analytics requests that were marked as running if they have completed and sets a new state accordingly. - /// - /// True if all requests completed, else false. - /// - public async Task TextAnalyticsRequestsCompleted(IEnumerable audioFileInfos) + public static bool IsTextAnalyticsRequested() { - if (audioFileInfos == null || !audioFileInfos.Where(audioFileInfo => audioFileInfo.TextAnalyticsRequests != null).Any()) + return FetchTranscriptionEnvironmentVariables.SentimentAnalysisSetting != SentimentAnalysisSetting.None || + FetchTranscriptionEnvironmentVariables.PiiRedactionSetting != PiiRedactionSetting.None; + } + + /// + public async Task GetTranscriptionAnalyticsJobStatusAsync(IEnumerable audioFileInfos) + { + if (!IsTextAnalyticsRequested()) { - return true; + return TranscriptionAnalyticsJobStatus.Completed; + } + + if (!audioFileInfos.Where(audioFileInfo => audioFileInfo.TextAnalyticsRequests != null).Any()) + { + return TranscriptionAnalyticsJobStatus.NotSubmitted; } var runningTextAnalyticsRequests = new List(); @@ -90,8 +74,7 @@ public async Task TextAnalyticsRequestsCompleted(IEnumerable audioFileInfo.TextAnalyticsRequests.UtteranceLevelRequests) .Where(text => text.Status == TextAnalyticsRequestStatus.Running)); - var textAnalyticsRequestCompleted = true; - + var status = TranscriptionAnalyticsJobStatus.Completed; foreach (var textAnalyticsJob in runningTextAnalyticsRequests) { var operation = new AnalyzeActionsOperation(textAnalyticsJob.Id, this.textAnalyticsClient); @@ -106,11 +89,114 @@ public async Task TextAnalyticsRequestsCompleted(IEnumerable + public async Task> SubmitTranscriptionAnalyticsJobsAsync(Dictionary speechTranscriptMappings) + { + _ = speechTranscriptMappings ?? throw new ArgumentNullException(nameof(speechTranscriptMappings)); + + var errors = new List(); + foreach (var speechTranscriptMapping in speechTranscriptMappings) + { + var speechTranscript = speechTranscriptMapping.Value; + var audioFileInfo = speechTranscriptMapping.Key; + + var fileName = audioFileInfo.FileName; + + if (speechTranscript.RecognizedPhrases != null && speechTranscript.RecognizedPhrases.All(phrase => phrase.RecognitionStatus.Equals("Success", StringComparison.Ordinal))) + { + var textAnalyticsErrors = new List(); + + (var utteranceLevelJobIds, var utteranceLevelErrors) = await this.SubmitUtteranceLevelRequests( + speechTranscript, + FetchTranscriptionEnvironmentVariables.SentimentAnalysisSetting).ConfigureAwait(false); + + var utteranceLevelRequests = utteranceLevelJobIds?.Select(jobId => new TextAnalyticsRequest(jobId, TextAnalyticsRequestStatus.Running)); + textAnalyticsErrors.AddRange(utteranceLevelErrors); + + (var audioLevelJobIds, var audioLevelErrors) = await this.SubmitAudioLevelRequests( + speechTranscript, + FetchTranscriptionEnvironmentVariables.SentimentAnalysisSetting, + FetchTranscriptionEnvironmentVariables.PiiRedactionSetting).ConfigureAwait(false); + + var audioLevelRequests = audioLevelJobIds?.Select(jobId => new TextAnalyticsRequest(jobId, TextAnalyticsRequestStatus.Running)); + textAnalyticsErrors.AddRange(audioLevelErrors); + + if (audioFileInfo.TextAnalyticsRequests == null) + { + audioFileInfo.TextAnalyticsRequests = new TextAnalyticsRequests(utteranceLevelRequests, audioLevelRequests, null); + } + else + { + audioFileInfo.TextAnalyticsRequests.UtteranceLevelRequests = utteranceLevelRequests; + audioFileInfo.TextAnalyticsRequests.AudioLevelRequests = audioLevelRequests; + } + + if (textAnalyticsErrors.Any()) + { + var distinctErrors = textAnalyticsErrors.Distinct(); + var errorMessage = $"File {(string.IsNullOrEmpty(fileName) ? "unknown" : fileName)}:\n{string.Join('\n', distinctErrors)}"; + errors.Add(errorMessage); + } + } + } + + return errors; + } + + /// + public async Task> AddTranscriptionAnalyticsResultsToTranscriptsAsync(Dictionary speechTranscriptMappings) + { + _ = speechTranscriptMappings ?? throw new ArgumentNullException(nameof(speechTranscriptMappings)); + + var errors = new List(); + foreach (var speechTranscriptMapping in speechTranscriptMappings) + { + var speechTranscript = speechTranscriptMapping.Value; + var audioFileInfo = speechTranscriptMapping.Key; + var fileName = audioFileInfo.FileName; + if (FetchTranscriptionEnvironmentVariables.PiiRedactionSetting != PiiRedactionSetting.None) + { + speechTranscript.RecognizedPhrases.ToList().ForEach(phrase => + { + if (phrase.NBest != null && phrase.NBest.Any()) + { + var firstNBest = phrase.NBest.First(); + phrase.NBest = new[] { firstNBest }; + } + }); + } + + var textAnalyticsErrors = new List(); + + if (audioFileInfo.TextAnalyticsRequests?.AudioLevelRequests?.Any() == true) + { + var audioLevelErrors = await this.AddAudioLevelEntitiesAsync(audioFileInfo.TextAnalyticsRequests.AudioLevelRequests.Select(request => request.Id), speechTranscript).ConfigureAwait(false); + textAnalyticsErrors.AddRange(audioLevelErrors); + } + + if (audioFileInfo.TextAnalyticsRequests?.UtteranceLevelRequests?.Any() == true) + { + var utteranceLevelErrors = await this.AddUtteranceLevelEntitiesAsync(audioFileInfo.TextAnalyticsRequests.UtteranceLevelRequests.Select(request => request.Id), speechTranscript).ConfigureAwait(false); + textAnalyticsErrors.AddRange(utteranceLevelErrors); + } + + if (textAnalyticsErrors.Any()) + { + var distinctErrors = textAnalyticsErrors.Distinct(); + var errorMessage = $"File {(string.IsNullOrEmpty(fileName) ? "unknown" : fileName)}:\n{string.Join('\n', distinctErrors)}"; + errors.Add(errorMessage); } } - return textAnalyticsRequestCompleted; + return errors; } /// @@ -120,7 +206,7 @@ public async Task TextAnalyticsRequestsCompleted(IEnumerableThe speech transcript object. /// The sentiment analysis setting. /// The job ids and errors, if any were found. - public async Task<(IEnumerable jobIds, IEnumerable errors)> SubmitUtteranceLevelRequests( + private async Task<(IEnumerable jobIds, IEnumerable errors)> SubmitUtteranceLevelRequests( SpeechTranscript speechTranscript, SentimentAnalysisSetting sentimentAnalysisSetting) { @@ -150,7 +236,7 @@ public async Task TextAnalyticsRequestsCompleted(IEnumerableThe sentiment analysis setting. /// The PII redaction setting. /// The job ids and errors, if any were found. - public async Task<(IEnumerable jobIds, IEnumerable errors)> SubmitAudioLevelRequests( + private async Task<(IEnumerable jobIds, IEnumerable errors)> SubmitAudioLevelRequests( SpeechTranscript speechTranscript, SentimentAnalysisSetting sentimentAnalysisSetting, PiiRedactionSetting piiRedactionSetting) @@ -200,7 +286,7 @@ public async Task TextAnalyticsRequestsCompleted(IEnumerableThe text analytics job ids. /// The speech transcript object. /// The errors, if any. - public async Task> AddUtteranceLevelEntitiesAsync( + private async Task> AddUtteranceLevelEntitiesAsync( IEnumerable jobIds, SpeechTranscript speechTranscript) { @@ -246,7 +332,7 @@ public async Task> AddUtteranceLevelEntitiesAsync( /// The text analytics job ids. /// The speech transcript object. /// The errors, if any. - public async Task> AddAudioLevelEntitiesAsync( + private async Task> AddAudioLevelEntitiesAsync( IEnumerable jobIds, SpeechTranscript speechTranscript) { diff --git a/samples/ingestion/ingestion-client/FetchTranscription/TranscriptionAnalytics/TranscriptionAnalyticsOrchestrator.cs b/samples/ingestion/ingestion-client/FetchTranscription/TranscriptionAnalytics/TranscriptionAnalyticsOrchestrator.cs new file mode 100644 index 000000000..089dbae7c --- /dev/null +++ b/samples/ingestion/ingestion-client/FetchTranscription/TranscriptionAnalytics/TranscriptionAnalyticsOrchestrator.cs @@ -0,0 +1,108 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// + +namespace FetchTranscription +{ + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + + using Connector; + using Connector.Enums; + using Connector.Serializable.TranscriptionStartedServiceBusMessage; + + using Microsoft.Extensions.Logging; + + public sealed class TranscriptionAnalyticsOrchestrator + { + private readonly List providers; + + public TranscriptionAnalyticsOrchestrator( + string locale, + ILogger logger) + { + var textAnalyticsKey = FetchTranscriptionEnvironmentVariables.TextAnalyticsKey; + var textAnalyticsEndpoint = FetchTranscriptionEnvironmentVariables.TextAnalyticsEndpoint; + var textAnalyticsInfoProvided = !string.IsNullOrEmpty(textAnalyticsKey) && !string.IsNullOrEmpty(textAnalyticsEndpoint); + + this.providers = new List(); + + if (textAnalyticsInfoProvided) + { + this.providers.Add(new TextAnalyticsProvider(locale, textAnalyticsKey, textAnalyticsEndpoint, logger)); + this.providers.Add(new AnalyzeConversationsProvider(locale, textAnalyticsKey, textAnalyticsEndpoint, logger)); + } + } + + /// + /// Gets the merged status of all transcription analytics jobs. + /// + /// The transcription started service bus message. + /// The merged job status. + public async Task GetTranscriptionAnalyticsJobsStatusAsync(TranscriptionStartedMessage transcriptionStartedMessage) + { + _ = transcriptionStartedMessage ?? throw new ArgumentNullException(nameof(transcriptionStartedMessage)); + + foreach (var provider in this.providers) + { + var providerStatus = await provider.GetTranscriptionAnalyticsJobStatusAsync(transcriptionStartedMessage.AudioFileInfos).ConfigureAwait(false); + + // if any is not submitted, we can safely return here since we submit all requests at the same time - therefore all other providers should not have any running requests. + if (providerStatus == TranscriptionAnalyticsJobStatus.NotSubmitted) + { + return TranscriptionAnalyticsJobStatus.NotSubmitted; + } + + // if any is running, we set the status to running and fetch it again after some time. + if (providerStatus == TranscriptionAnalyticsJobStatus.Running) + { + return TranscriptionAnalyticsJobStatus.Running; + } + } + + return TranscriptionAnalyticsJobStatus.Completed; + } + + /// + /// Submit transcription analytics jobs and adds their IDs to the audio file infos, so that they can get fetched the next time the transcription job status is polled. + /// + /// The mapping from audio file infos to speech transcripts. + /// The errors if any. + public async Task> SubmitTranscriptionAnalyticsJobsAndAddToAudioFileInfos(Dictionary speechTranscriptMappings) + { + _ = speechTranscriptMappings ?? throw new ArgumentNullException(nameof(speechTranscriptMappings)); + + var errors = new List(); + + foreach (var provider in this.providers) + { + var providerErros = await provider.SubmitTranscriptionAnalyticsJobsAsync(speechTranscriptMappings).ConfigureAwait(false); + errors.AddRange(providerErros); + } + + return errors; + } + + /// + /// Adds the result of all transcription analytics jobs to the corresponding speech transcript. + /// + /// The mapping from audio file infos to speech transcripts. + /// The errors if any. + public async Task> AddTranscriptionAnalyticsResultsToTranscripts(Dictionary speechTranscriptMappings) + { + _ = speechTranscriptMappings ?? throw new ArgumentNullException(nameof(speechTranscriptMappings)); + + var errors = new List(); + + foreach (var provider in this.providers) + { + var providerErros = await provider.AddTranscriptionAnalyticsResultsToTranscriptsAsync(speechTranscriptMappings).ConfigureAwait(false); + errors.AddRange(providerErros); + } + + return errors; + } + } +} diff --git a/samples/ingestion/ingestion-client/FetchTranscription/TranscriptionProcessor.cs b/samples/ingestion/ingestion-client/FetchTranscription/TranscriptionProcessor.cs index 91d831dd1..b51ea1893 100644 --- a/samples/ingestion/ingestion-client/FetchTranscription/TranscriptionProcessor.cs +++ b/samples/ingestion/ingestion-client/FetchTranscription/TranscriptionProcessor.cs @@ -3,7 +3,7 @@ // Licensed under the MIT license. See LICENSE.md file in the project root for full license information. // -namespace FetchTranscriptionFunction +namespace FetchTranscription { using System; using System.Collections.Generic; @@ -20,14 +20,10 @@ namespace FetchTranscriptionFunction using Connector.Enums; using Connector.Serializable.TranscriptionStartedServiceBusMessage; - using Language; - using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Newtonsoft.Json; - using TextAnalytics; - using static Connector.Serializable.TranscriptionStartedServiceBusMessage.TextAnalyticsRequest; public class TranscriptionProcessor { @@ -314,36 +310,29 @@ await StorageConnectorInstance.MoveFileAsync( } } - private async Task ProcessSucceededTranscriptionAsync(string transcriptionLocation, string subscriptionKey, TranscriptionStartedMessage serviceBusMessage, string jobName, ILogger log) + private static async Task WriteErrorReportAsync(string errorString, string jobName, ILogger logger) { - log.LogInformation($"Got succeeded transcription for job {jobName}"); - - var textAnalyticsKey = FetchTranscriptionEnvironmentVariables.TextAnalyticsKey; - var textAnalyticsRegion = FetchTranscriptionEnvironmentVariables.TextAnalyticsRegion; - var textAnalyticsInfoProvided = !string.IsNullOrEmpty(textAnalyticsKey) - && !string.IsNullOrEmpty(textAnalyticsRegion) - && !textAnalyticsRegion.Equals("none", StringComparison.OrdinalIgnoreCase); + var errorTxtname = $"jobs/{jobName}.txt"; - var conversationsAnalysisProvider = textAnalyticsInfoProvided ? new AnalyzeConversationsProvider(serviceBusMessage.Locale, textAnalyticsKey, textAnalyticsRegion, log) : null; - - var textAnalyticsProvider = textAnalyticsInfoProvided ? new TextAnalyticsProvider(serviceBusMessage.Locale, textAnalyticsKey, textAnalyticsRegion, log) : null; + await StorageConnectorInstance.WriteTextFileToBlobAsync( + errorString, + FetchTranscriptionEnvironmentVariables.ErrorReportOutputContainer, + errorTxtname, + logger).ConfigureAwait(false); + } - // Check if there is a text analytics request already running: - var containsTextAnalyticsRequest = serviceBusMessage.AudioFileInfos.Where(audioFileInfo => audioFileInfo.TextAnalyticsRequests != null).Any(); + private async Task ProcessSucceededTranscriptionAsync(string transcriptionLocation, string subscriptionKey, TranscriptionStartedMessage serviceBusMessage, string jobName, ILogger log) + { + log.LogInformation($"Got succeeded transcription for job {jobName}"); - if (containsTextAnalyticsRequest && textAnalyticsProvider != null) + var transcriptionAnalyticsOrchestrator = new TranscriptionAnalyticsOrchestrator(serviceBusMessage.Locale, log); + var transcriptionAnalyticsJobStatus = await transcriptionAnalyticsOrchestrator.GetTranscriptionAnalyticsJobsStatusAsync(serviceBusMessage).ConfigureAwait(false); + if (transcriptionAnalyticsJobStatus == TranscriptionAnalyticsJobStatus.Running) { - var textAnalyticsRequestCompleted = await textAnalyticsProvider.TextAnalyticsRequestsCompleted(serviceBusMessage.AudioFileInfos).ConfigureAwait(false); - - var conversationalAnalyticsRequestCompleted = await conversationsAnalysisProvider.ConversationalRequestsCompleted(serviceBusMessage.AudioFileInfos).ConfigureAwait(false); - - // If text analytics request is still running, re-queue message and get status again after X minutes - if (!textAnalyticsRequestCompleted || !conversationalAnalyticsRequestCompleted) - { - log.LogInformation($"Text analytics request still running for job {jobName} - re-queueing message."); - await ServiceBusUtilities.SendServiceBusMessageAsync(FetchServiceBusSender, serviceBusMessage.CreateMessageString(), log, GetMessageDelayTime(serviceBusMessage.PollingCounter)).ConfigureAwait(false); - return; - } + // If transcription analytics request is still running, re-queue message and get status again after X minutes: + log.LogInformation($"Transcription analytics requests still running for job {jobName} - re-queueing message."); + await ServiceBusUtilities.SendServiceBusMessageAsync(FetchServiceBusSender, serviceBusMessage.CreateMessageString(), log, GetMessageDelayTime(serviceBusMessage.PollingCounter)).ConfigureAwait(false); + return; } var transcriptionFiles = await BatchClient.GetTranscriptionFilesAsync(transcriptionLocation, subscriptionKey).ConfigureAwait(false); @@ -392,113 +381,34 @@ private async Task ProcessSucceededTranscriptionAsync(string transcriptionLocati } } - if (textAnalyticsProvider != null && - (FetchTranscriptionEnvironmentVariables.SentimentAnalysisSetting != SentimentAnalysisSetting.None - || FetchTranscriptionEnvironmentVariables.PiiRedactionSetting != PiiRedactionSetting.None - || AnalyzeConversationsProvider.IsConversationalPiiEnabled() - || AnalyzeConversationsProvider.IsConversationalSummarizationEnabled())) + if (transcriptionAnalyticsJobStatus == TranscriptionAnalyticsJobStatus.Completed) { - // If we already got text analytics requests in the transcript (containsTextAnalyticsRequest), add the results to the transcript. - // Otherwise, submit new text analytics requests. - if (containsTextAnalyticsRequest) - { - foreach (var speechTranscriptMapping in speechTranscriptMappings) - { - var speechTranscript = speechTranscriptMapping.Value; - var audioFileInfo = speechTranscriptMapping.Key; - var fileName = audioFileInfo.FileName; - if (FetchTranscriptionEnvironmentVariables.PiiRedactionSetting != PiiRedactionSetting.None) - { - speechTranscript.RecognizedPhrases.ToList().ForEach(phrase => - { - if (phrase.NBest != null && phrase.NBest.Any()) - { - var firstNBest = phrase.NBest.First(); - phrase.NBest = new[] { firstNBest }; - } - }); - } - - var textAnalyticsErrors = new List(); - - if (audioFileInfo.TextAnalyticsRequests.AudioLevelRequests?.Any() == true) - { - var audioLevelErrors = await textAnalyticsProvider.AddAudioLevelEntitiesAsync(audioFileInfo.TextAnalyticsRequests.AudioLevelRequests.Select(request => request.Id), speechTranscript).ConfigureAwait(false); - textAnalyticsErrors.AddRange(audioLevelErrors); - } - - if (audioFileInfo.TextAnalyticsRequests.UtteranceLevelRequests?.Any() == true) - { - var utteranceLevelErrors = await textAnalyticsProvider.AddUtteranceLevelEntitiesAsync(audioFileInfo.TextAnalyticsRequests.UtteranceLevelRequests.Select(request => request.Id), speechTranscript).ConfigureAwait(false); - textAnalyticsErrors.AddRange(utteranceLevelErrors); - } + var errors = await transcriptionAnalyticsOrchestrator.AddTranscriptionAnalyticsResultsToTranscripts(speechTranscriptMappings).ConfigureAwait(false); - if (audioFileInfo.TextAnalyticsRequests.ConversationRequests?.Any() == true) - { - var conversationalAnalyticsErrors = await conversationsAnalysisProvider.AddConversationalEntitiesAsync(audioFileInfo.TextAnalyticsRequests.ConversationRequests.Select(request => request.Id), speechTranscript).ConfigureAwait(false); - textAnalyticsErrors.AddRange(conversationalAnalyticsErrors); - } - - if (textAnalyticsErrors.Any()) - { - var distinctErrors = textAnalyticsErrors.Distinct(); - var errorMessage = $"File {(string.IsNullOrEmpty(fileName) ? "unknown" : fileName)}:\n{string.Join('\n', distinctErrors)}"; - - generalErrorsStringBuilder.AppendLine(errorMessage); - } - } + foreach (var error in errors) + { + generalErrorsStringBuilder.AppendLine(error); } - else + } + else if (transcriptionAnalyticsJobStatus == TranscriptionAnalyticsJobStatus.NotSubmitted) + { + var errors = await transcriptionAnalyticsOrchestrator.SubmitTranscriptionAnalyticsJobsAndAddToAudioFileInfos(speechTranscriptMappings).ConfigureAwait(false); + foreach (var error in errors) { - foreach (var speechTranscriptMapping in speechTranscriptMappings) - { - var speechTranscript = speechTranscriptMapping.Value; - var audioFileInfo = speechTranscriptMapping.Key; - - var fileName = audioFileInfo.FileName; - - if (speechTranscript.RecognizedPhrases != null && speechTranscript.RecognizedPhrases.All(phrase => phrase.RecognitionStatus.Equals("Success", StringComparison.Ordinal))) - { - var textAnalyticsErrors = new List(); - - (var utteranceLevelJobIds, var utteranceLevelErrors) = await textAnalyticsProvider.SubmitUtteranceLevelRequests( - speechTranscript, - FetchTranscriptionEnvironmentVariables.SentimentAnalysisSetting).ConfigureAwait(false); - - var utteranceLevelRequests = utteranceLevelJobIds?.Select(jobId => new TextAnalyticsRequest(jobId, TextAnalyticsRequestStatus.Running)); - textAnalyticsErrors.AddRange(utteranceLevelErrors); - - (var audioLevelJobIds, var audioLevelErrors) = await textAnalyticsProvider.SubmitAudioLevelRequests( - speechTranscript, - FetchTranscriptionEnvironmentVariables.SentimentAnalysisSetting, - FetchTranscriptionEnvironmentVariables.PiiRedactionSetting).ConfigureAwait(false); - - var audioLevelRequests = audioLevelJobIds?.Select(jobId => new TextAnalyticsRequest(jobId, TextAnalyticsRequestStatus.Running)); - textAnalyticsErrors.AddRange(audioLevelErrors); - - (var conversationJobIds, var conversationErrors) = await conversationsAnalysisProvider.SubmitAnalyzeConversationsRequestAsync(speechTranscript).ConfigureAwait(false); - - var conversationalRequests = conversationJobIds?.Select(jobId => new TextAnalyticsRequest(jobId, TextAnalyticsRequestStatus.Running)); - textAnalyticsErrors.AddRange(conversationErrors); - - audioFileInfo.TextAnalyticsRequests = new TextAnalyticsRequests(utteranceLevelRequests, audioLevelRequests, conversationalRequests); - - if (textAnalyticsErrors.Any()) - { - var distinctErrors = textAnalyticsErrors.Distinct(); - var errorMessage = $"File {(string.IsNullOrEmpty(fileName) ? "unknown" : fileName)}:\n{string.Join('\n', distinctErrors)}"; + generalErrorsStringBuilder.AppendLine(error); + } - generalErrorsStringBuilder.AppendLine(errorMessage); - } - } - } + var textAnalyticsSubmitErrors = generalErrorsStringBuilder.ToString(); + if (!string.IsNullOrEmpty(textAnalyticsSubmitErrors)) + { + await WriteErrorReportAsync(textAnalyticsSubmitErrors, jobName, log).ConfigureAwait(false); + } - log.LogInformation($"Added text analytics requests to service bus message - re-queueing message."); + log.LogInformation($"Added text analytics requests to service bus message - re-queueing message."); - // Poll for first time with TA request after 1 minute - await ServiceBusUtilities.SendServiceBusMessageAsync(FetchServiceBusSender, serviceBusMessage.CreateMessageString(), log, TimeSpan.FromMinutes(1)).ConfigureAwait(false); - return; - } + // Poll for first time with TA request after 1 minute + await ServiceBusUtilities.SendServiceBusMessageAsync(FetchServiceBusSender, serviceBusMessage.CreateMessageString(), log, TimeSpan.FromMinutes(1)).ConfigureAwait(false); + return; } foreach (var speechTranscriptMapping in speechTranscriptMappings) @@ -584,13 +494,7 @@ await this.databaseContext.StoreTranscriptionAsync( var generalErrors = generalErrorsStringBuilder.ToString(); if (!string.IsNullOrEmpty(generalErrors)) { - var errorTxtname = $"jobs/{jobName}.txt"; - - await StorageConnectorInstance.WriteTextFileToBlobAsync( - generalErrors, - FetchTranscriptionEnvironmentVariables.ErrorReportOutputContainer, - errorTxtname, - log).ConfigureAwait(false); + await WriteErrorReportAsync(generalErrors, jobName, log).ConfigureAwait(false); } var reportFile = transcriptionFiles.Values.Where(t => t.Kind == TranscriptionFileKind.TranscriptionReport).FirstOrDefault(); diff --git a/samples/ingestion/ingestion-client/RealtimeTranscription/RealtimeTranscription.csproj b/samples/ingestion/ingestion-client/RealtimeTranscription/RealtimeTranscription.csproj index 7c732ddc5..b0a0ebf15 100644 --- a/samples/ingestion/ingestion-client/RealtimeTranscription/RealtimeTranscription.csproj +++ b/samples/ingestion/ingestion-client/RealtimeTranscription/RealtimeTranscription.csproj @@ -6,7 +6,7 @@ - + diff --git a/samples/ingestion/ingestion-client/Setup/ArmTemplateBatch.json b/samples/ingestion/ingestion-client/Setup/ArmTemplateBatch.json index c93c80789..8bdfc7853 100644 --- a/samples/ingestion/ingestion-client/Setup/ArmTemplateBatch.json +++ b/samples/ingestion/ingestion-client/Setup/ArmTemplateBatch.json @@ -1,233 +1,209 @@ { "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", - "parameters": { - "StorageAccount": { - "type": "String", - "metadata": { - "description": "The name of the storage account. It must be unique across all existing storage account names in Azure, between 3 and 24 characters long, and can contain only lowercase letters and numbers." - } - }, - "Locale": { - "defaultValue": "en-US | English (United States)", - "type": "String", - "allowedValues": [ - "ar-BH | Arabic (Bahrain)", - "ar-EG | Arabic (Egypt)", - "ar-SY | Arabic (Syria)", - "ca-ES | Catalan", - "da-DK | Danish (Denmark)", - "de-DE | German (Germany)", - "en-AU | English (Australia)", - "en-CA | English (Canada)", - "en-GB | English (United Kingdom)", - "en-IN | English (India)", - "en-NZ | English (New Zealand)", - "en-US | English (United States)", - "es-ES | Spanish (Spain)", - "es-MX | Spanish (Mexico)", - "fi-FI | Finnish (Finland)", - "fr-CA | French (Canada)", - "fr-FR | French (France)", - "gu-IN | Gujarati (Indian)", - "hi-IN | Hindi (India)", - "it-IT | Italian (Italy)", - "ja-JP | Japanese (Japan)", - "ko-KR | Korean (Korea)", - "mr-IN | Marathi (India)", - "nb-NO | Norwegian (Bokmål)", - "nl-NL | Dutch (Netherlands)", - "pl-PL | Polish (Poland)", - "pt-BR | Portuguese (Brazil)", - "pt-PT | Portuguese (Portugal)", - "ru-RU | Russian (Russia)", - "sv-SE | Swedish (Sweden)", - "ta-IN | Tamil (India)", - "te-IN | Telugu (India)", - "th-TH | Thai (Thailand)", - "tr-TR | Turkish (Turkey)", - "zh-CN | Chinese (Mandarin, simplified)", - "zh-HK | Chinese (Cantonese, Traditional)", - "zh-TW | Chinese (Taiwanese Mandarin)" - ] - }, - "CustomModelId": { - "defaultValue": "", - "type": "String", - "metadata": { - "description": "The id of the custom model for transcription. If empty, the base model will be selected." - } - }, - "AzureSpeechServicesKey": { - "type": "SecureString", - "metadata": { - "description": "The key for the Azure Speech Services subscription." - } - }, - "AzureSpeechServicesRegion": { - "defaultValue": "westus", - "type": "String", - "allowedValues": [ - "centralus", - "eastus", - "eastus2", - "northcentralus", - "southcentralus", - "westcentralus", - "westus", - "westus2", - "canadacentral", - "brazilsouth", - "eastasia", - "southeastasia", - "australiaeast", - "centralindia", - "japaneast", - "japanwest", - "koreacentral", - "northeurope", - "westeurope", - "francecentral", - "uksouth", - "usgovarizona", - "usgovvirginia" - ], - "metadata": { - "description": "The region the Azure speech services subscription is associated with." - } - }, - "CustomEndpoint": { - "defaultValue": "", - "type": "String", - "metadata": { - "description": "Enter the address of your private endpoint here (e.g. https://mycustomendpoint.cognitiveservices.azure.com/) if you are connecting with a private endpoint" - } - }, - "ProfanityFilterMode": { - "defaultValue": "None", - "type": "String", - "allowedValues": [ - "None", - "Removed", - "Tags", - "Masked" - ], - "metadata": { - "description": "The requested profanity filter mode." - } - }, - "PunctuationMode": { - "defaultValue": "DictatedAndAutomatic", - "type": "String", - "allowedValues": [ - "None", - "Dictated", - "Automatic", - "DictatedAndAutomatic" - ], - "metadata": { - "description": "The requested punctuation mode." - } - }, - "AddDiarization": { - "defaultValue": false, - "type": "bool", - "metadata": { - "description": "A value indicating whether diarization (speaker separation) is requested." - } - }, - "AddWordLevelTimestamps": { - "defaultValue": false, - "type": "bool", - "metadata": { - "description": "A value indicating whether word level timestamps are requested." - } - }, - "TextAnalyticsKey": { - "defaultValue": "", - "type": "SecureString", - "metadata": { - "description": "The key for the Text Analytics subscription." - } - }, - "TextAnalyticsRegion": { - "defaultValue": "None", - "type": "String", - "allowedValues": [ - "None", - "centralus", - "eastus", - "eastus2", - "northcentralus", - "southcentralus", - "westcentralus", - "westus", - "westus2", - "canadacentral", - "brazilsouth", - "eastasia", - "southeastasia", - "australiaeast", - "centralindia", - "japaneast", - "japanwest", - "koreacentral", - "northeurope", - "westeurope", - "francecentral", - "uksouth" - ], - "metadata": { - "description": "The region the Text Analytics subscription is associated with. If none is selected, no text analysis will be performed." - } - }, - "SentimentAnalysis": { - "defaultValue": "None", - "type": "String", - "allowedValues": [ - "None", - "UtteranceLevel", - "AudioLevel" - ], - "metadata": { - "description": "A value indicating whether sentiment analysis is requested (either per utterance or per audio). Will only be performed if a Text Analytics Key and Region is provided." - } - }, - "PiiRedaction": { - "defaultValue": "None", - "type": "String", - "allowedValues": [ - "None", - "UtteranceAndAudioLevel" - ], - "metadata": { - "description": "A value indicating whether personally identifiable information (PII) redaction is requested. Will only be performed if a Text Analytics Key and Region is provided." - } - }, - "SqlAdministratorLogin": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "The administrator username of the SQL Server, which is used to gain insights of the audio with the provided PowerBI scripts. If it is left empty, no SQL server/database will be created." - } - }, - "SqlAdministratorLoginPassword": { - "type": "securestring", - "defaultValue": "", - "metadata": { - "description": "The administrator password of the SQL Server. If it is left empty, no SQL server/database will be created." - } - }, - "DeploymentId": { - "type": "string", - "defaultValue": "[utcNow()]", - "metadata": { - "description": "Id that will be suffixed to all created resources to identify resources of a certain deployment. Leave as is to use timestamp as deployment id." - } - } + "parameters": { + "StorageAccount": { + "type": "String", + "metadata": { + "description": "The name of the storage account. It must be unique across all existing storage account names in Azure, between 3 and 24 characters long, and can contain only lowercase letters and numbers." + } + }, + "Locale": { + "defaultValue": "en-US | English (United States)", + "type": "String", + "allowedValues": [ + "ar-BH | Arabic (Bahrain)", + "ar-EG | Arabic (Egypt)", + "ar-SY | Arabic (Syria)", + "ca-ES | Catalan", + "da-DK | Danish (Denmark)", + "de-DE | German (Germany)", + "en-AU | English (Australia)", + "en-CA | English (Canada)", + "en-GB | English (United Kingdom)", + "en-IN | English (India)", + "en-NZ | English (New Zealand)", + "en-US | English (United States)", + "es-ES | Spanish (Spain)", + "es-MX | Spanish (Mexico)", + "fi-FI | Finnish (Finland)", + "fr-CA | French (Canada)", + "fr-FR | French (France)", + "gu-IN | Gujarati (Indian)", + "hi-IN | Hindi (India)", + "it-IT | Italian (Italy)", + "ja-JP | Japanese (Japan)", + "ko-KR | Korean (Korea)", + "mr-IN | Marathi (India)", + "nb-NO | Norwegian (Bokmål)", + "nl-NL | Dutch (Netherlands)", + "pl-PL | Polish (Poland)", + "pt-BR | Portuguese (Brazil)", + "pt-PT | Portuguese (Portugal)", + "ru-RU | Russian (Russia)", + "sv-SE | Swedish (Sweden)", + "ta-IN | Tamil (India)", + "te-IN | Telugu (India)", + "th-TH | Thai (Thailand)", + "tr-TR | Turkish (Turkey)", + "zh-CN | Chinese (Mandarin, simplified)", + "zh-HK | Chinese (Cantonese, Traditional)", + "zh-TW | Chinese (Taiwanese Mandarin)" + ] + }, + "CustomModelId": { + "defaultValue": "", + "type": "String", + "metadata": { + "description": "The id of the custom model for transcription. If empty, the base model will be selected." + } }, + "AzureSpeechServicesKey": { + "type": "SecureString", + "metadata": { + "description": "The key for the Azure Speech Services subscription." + } + }, + "AzureSpeechServicesRegion": { + "defaultValue": "westus", + "type": "String", + "allowedValues": [ + "centralus", + "eastus", + "eastus2", + "northcentralus", + "southcentralus", + "westcentralus", + "westus", + "westus2", + "canadacentral", + "brazilsouth", + "eastasia", + "southeastasia", + "australiaeast", + "centralindia", + "japaneast", + "japanwest", + "koreacentral", + "northeurope", + "westeurope", + "francecentral", + "uksouth", + "usgovarizona", + "usgovvirginia" + ], + "metadata": { + "description": "The region the Azure speech services subscription is associated with." + } + }, + "CustomEndpoint": { + "defaultValue": "", + "type": "String", + "metadata": { + "description": "Enter the address of your private endpoint here (e.g. https://mycustomendpoint.cognitiveservices.azure.com/) if you are connecting with a private endpoint" + } + }, + "ProfanityFilterMode": { + "defaultValue": "None", + "type": "String", + "allowedValues": [ + "None", + "Removed", + "Tags", + "Masked" + ], + "metadata": { + "description": "The requested profanity filter mode." + } + }, + "PunctuationMode": { + "defaultValue": "DictatedAndAutomatic", + "type": "String", + "allowedValues": [ + "None", + "Dictated", + "Automatic", + "DictatedAndAutomatic" + ], + "metadata": { + "description": "The requested punctuation mode." + } + }, + "AddDiarization": { + "defaultValue": false, + "type": "bool", + "metadata": { + "description": "A value indicating whether diarization (speaker separation) is requested." + } + }, + "AddWordLevelTimestamps": { + "defaultValue": false, + "type": "bool", + "metadata": { + "description": "A value indicating whether word level timestamps are requested." + } + }, + "TextAnalyticsKey": { + "defaultValue": "", + "type": "SecureString", + "metadata": { + "description": "The key for the Text Analytics subscription." + } + }, + "TextAnalyticsEndpoint": { + "defaultValue": "", + "type": "String", + "metadata": { + "description": "The endpoint the Text Analytics subscription is associated with (format should be like https://{resourceName}.cognitiveservices.azure.com or https://{region}.api.cognitive.microsoft.com or similar). If empty, no text analysis will be performed." + } + }, + "SentimentAnalysis": { + "defaultValue": "None", + "type": "String", + "allowedValues": [ + "None", + "UtteranceLevel", + "AudioLevel" + ], + "metadata": { + "description": "A value indicating whether sentiment analysis is requested (either per utterance or per audio). Will only be performed if a Text Analytics Key and Region is provided." + } + }, + "PiiRedaction": { + "defaultValue": "None", + "type": "String", + "allowedValues": [ + "None", + "UtteranceAndAudioLevel" + ], + "metadata": { + "description": "A value indicating whether personally identifiable information (PII) redaction is requested. Will only be performed if a Text Analytics Key and Region is provided." + } + }, + "SqlAdministratorLogin": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The administrator username of the SQL Server, which is used to gain insights of the audio with the provided PowerBI scripts. If it is left empty, no SQL server/database will be created." + } + }, + "SqlAdministratorLoginPassword": { + "type": "securestring", + "defaultValue": "", + "metadata": { + "description": "The administrator password of the SQL Server. If it is left empty, no SQL server/database will be created." + } + }, + "DeploymentId": { + "type": "string", + "defaultValue": "[utcNow()]", + "metadata": { + "description": "Id that will be suffixed to all created resources to identify resources of a certain deployment. Leave as is to use timestamp as deployment id." + } + } + }, "variables": { - "Version": "v2.0.5", + "Version": "v2.0.12", "AudioInputContainer": "audio-input", "AudioProcessedContainer": "audio-processed", "ErrorFilesOutputContainer": "audio-failed", @@ -258,9 +234,6 @@ "StartTranscriptionFunctionId": "[resourceId('Microsoft.Web/sites', variables('StartTranscriptionFunctionName'))]", "FetchTranscriptionFunctionName": "[take(concat('FetchTranscriptionFunction-', variables('InstanceId')),60)]", "FetchTranscriptionFunctionId": "[resourceId('Microsoft.Web/sites', variables('FetchTranscriptionFunctionName'))]", - "AuthRuleRMK": "[resourceId('Microsoft.ServiceBus/namespaces/authorizationRules', variables('ServiceBusName'),'RootManageSharedAccessKey')]", - "AuthRuleFT": "[resourceId('Microsoft.ServiceBus/namespaces/queues/authorizationRules', variables('ServiceBusName'), 'fetch_transcription_queue','FetchTranscription')]", - "AuthRuleCT": "[resourceId('Microsoft.ServiceBus/namespaces/queues/authorizationRules', variables('ServiceBusName'), 'start_transcription_queue','StartTranscription')]", "AppServicePlanName": "[concat('AppServicePlan-', variables('InstanceId'))]", "AzureSpeechServicesKeySecretName": "AzureSpeechServicesKey", "TextAnalyticsKeySecretName": "TextAnalyticsKey", @@ -507,7 +480,7 @@ } }, { - "type": "Microsoft.ServiceBus/namespaces/AuthorizationRules", + "type": "Microsoft.ServiceBus/namespaces/authorizationRules", "apiVersion": "2017-04-01", "name": "[concat(variables('ServiceBusName'), '/RootManageSharedAccessKey')]", "location": "[resourceGroup().location]", @@ -615,8 +588,7 @@ "dependsOn": [ "[resourceId('Microsoft.ServiceBus/namespaces/queues', variables('ServiceBusName'), 'fetch_transcription_queue')]", "[resourceId('Microsoft.ServiceBus/namespaces', variables('ServiceBusName'))]", - "[variables('AuthRuleRMK')]", - "[variables('AuthRuleCT')]" + "[resourceId('Microsoft.ServiceBus/namespaces/authorizationRules', variables('ServiceBusName'), 'RootManageSharedAccessKey')]" ], "properties": { "rights": [ @@ -633,7 +605,7 @@ "dependsOn": [ "[resourceId('Microsoft.ServiceBus/namespaces/queues', variables('ServiceBusName'), 'start_transcription_queue')]", "[resourceId('Microsoft.ServiceBus/namespaces', variables('ServiceBusName'))]", - "[variables('AuthRuleRMK')]" + "[resourceId('Microsoft.ServiceBus/namespaces/authorizationRules', variables('ServiceBusName'), 'RootManageSharedAccessKey')]" ], "properties": { "rights": [ @@ -835,9 +807,9 @@ "[concat('Microsoft.Web/sites/', variables('StartTranscriptionFunctionName'))]", "[resourceId('Microsoft.KeyVault/vaults', variables('KeyVaultName'))]", "[resourceId('Microsoft.KeyVault/vaults/secrets', variables('KeyVaultName'), variables('AzureSpeechServicesKeySecretName'))]", - "[variables('AuthRuleRMK')]", - "[variables('AuthRuleFT')]", - "[variables('AuthRuleCT')]" + "[resourceId('Microsoft.ServiceBus/namespaces/authorizationRules', variables('ServiceBusName'), 'RootManageSharedAccessKey')]", + "[resourceId('Microsoft.ServiceBus/namespaces/queues/authorizationRules', variables('ServiceBusName'), 'start_transcription_queue','StartTranscription')]", + "[resourceId('Microsoft.ServiceBus/namespaces/queues/authorizationRules', variables('ServiceBusName'), 'fetch_transcription_queue','FetchTranscription')]" ], "tags": { "displayName": "WebAppSettings" @@ -847,7 +819,7 @@ "AddWordLevelTimestamps": "[parameters('AddWordLevelTimestamps')]", "APPLICATIONINSIGHTS_CONNECTION_STRING": "[reference(resourceId('Microsoft.Insights/components', variables('AppInsightsName')), '2020-02-02-preview').ConnectionString]", "AudioInputContainer": "[variables('AudioInputContainer')]", - "AzureServiceBus": "[listKeys(variables('AuthRuleRMK'),'2015-08-01').primaryConnectionString]", + "AzureServiceBus": "AzureServiceBus": "[listKeys(resourceId('Microsoft.ServiceBus/namespaces/authorizationRules', variables('ServiceBusName'), 'RootManageSharedAccessKey'), '2017-04-01').primaryConnectionString]", "AzureSpeechServicesKey": "[concat('@Microsoft.KeyVault(VaultName=', variables('KeyVaultName'), ';SecretName=', variables('AzureSpeechServicesKeySecretName'), ')')]", "AzureSpeechServicesRegion": "[parameters('AzureSpeechServicesRegion')]", "AzureWebJobsStorage": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('StorageAccountName'), ';AccountKey=', listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('StorageAccountName')), providers('Microsoft.Storage', 'storageAccounts').apiVersions[0]).keys[0].value, ';EndpointSuffix=', variables('EndpointSuffix'))]", @@ -855,7 +827,7 @@ "CustomModelId": "[parameters('CustomModelId')]", "ErrorFilesOutputContainer": "[variables('ErrorFilesOutputContainer')]", "ErrorReportOutputContainer": "[variables('ErrorReportOutputContainer')]", - "FetchTranscriptionServiceBusConnectionString": "[listKeys(variables('AuthRuleFT'),'2015-08-01').primaryConnectionString]", + "FetchTranscriptionServiceBusConnectionString": "[listKeys(resourceId('Microsoft.ServiceBus/namespaces/queues/authorizationRules', variables('ServiceBusName'), 'fetch_transcription_queue','FetchTranscription'), '2017-04-01').primaryConnectionString]", "FilesPerTranscriptionJob": "[variables('FilesPerTranscriptionJob')]", "FUNCTIONS_EXTENSION_VERSION": "~4", "FUNCTIONS_WORKER_RUNTIME": "dotnet", @@ -869,7 +841,7 @@ "ProfanityFilterMode": "[parameters('ProfanityFilterMode')]", "PunctuationMode": "[parameters('PunctuationMode')]", "RetryLimit": "[variables('RetryLimit')]", - "StartTranscriptionServiceBusConnectionString": "[listKeys(variables('AuthRuleCT'),'2015-08-01').primaryConnectionString]", + "StartTranscriptionServiceBusConnectionString": "[listKeys(resourceId('Microsoft.ServiceBus/namespaces/queues/authorizationRules', variables('ServiceBusName'), 'start_transcription_queue','StartTranscription'), '2017-04-01').primaryConnectionString]", "WEBSITE_RUN_FROM_PACKAGE": "[if(variables('timerBasedExecution'), variables('StartTranscriptionByTimerBinary'), variables('StartTranscriptionByServiceBusBinary'))]" } }, @@ -902,9 +874,9 @@ "[resourceId('Microsoft.KeyVault/vaults', variables('KeyVaultName'))]", "[resourceId('Microsoft.KeyVault/vaults/secrets', variables('KeyVaultName'), variables('AzureSpeechServicesKeySecretName'))]", "[resourceId('Microsoft.KeyVault/vaults/secrets', variables('KeyVaultName'), variables('TextAnalyticsKeySecretName'))]", - "[variables('AuthRuleRMK')]", - "[variables('AuthRuleFT')]", - "[variables('AuthRuleCT')]" + "[resourceId('Microsoft.ServiceBus/namespaces/authorizationRules', variables('ServiceBusName'), 'RootManageSharedAccessKey')]", + "[resourceId('Microsoft.ServiceBus/namespaces/queues/authorizationRules', variables('ServiceBusName'), 'start_transcription_queue','StartTranscription')]", + "[resourceId('Microsoft.ServiceBus/namespaces/queues/authorizationRules', variables('ServiceBusName'), 'fetch_transcription_queue','FetchTranscription')]" ], "tags": { "displayName": "WebAppSettings" @@ -914,7 +886,7 @@ "PiiRedactionSetting": "[parameters('PiiRedaction')]", "SentimentAnalysisSetting": "[parameters('SentimentAnalysis')]", "AudioInputContainer": "[variables('AudioInputContainer')]", - "AzureServiceBus": "[listKeys(variables('AuthRuleRMK'),'2015-08-01').primaryConnectionString]", + "AzureServiceBus": "[listKeys(resourceId('Microsoft.ServiceBus/namespaces/authorizationRules', variables('ServiceBusName'), 'RootManageSharedAccessKey'), '2017-04-01').primaryConnectionString]", "AzureSpeechServicesKey": "[concat('@Microsoft.KeyVault(VaultName=', variables('KeyVaultName'), ';SecretName=', variables('AzureSpeechServicesKeySecretName'), ')')]", "AzureWebJobsStorage": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('StorageAccountName'), ';AccountKey=', listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('StorageAccountName')), providers('Microsoft.Storage', 'storageAccounts').apiVersions[0]).keys[0].value, ';EndpointSuffix=', variables('EndpointSuffix'))]", "AzureWebJobsDashboard": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('StorageAccountName'), ';AccountKey=', listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('StorageAccountName')), providers('Microsoft.Storage', 'storageAccounts').apiVersions[0]).keys[0].value, ';EndpointSuffix=', variables('EndpointSuffix'))]", @@ -922,7 +894,7 @@ "DatabaseConnectionString": "[concat('@Microsoft.KeyVault(VaultName=', variables('KeyVaultName'), ';SecretName=', variables('DatabaseConnectionStringSecretName'), ')')]", "ErrorFilesOutputContainer": "[variables('ErrorFilesOutputContainer')]", "ErrorReportOutputContainer": "[variables('ErrorReportOutputContainer')]", - "FetchTranscriptionServiceBusConnectionString": "[listKeys(variables('AuthRuleFT'),'2015-08-01').primaryConnectionString]", + "FetchTranscriptionServiceBusConnectionString": "[listKeys(resourceId('Microsoft.ServiceBus/namespaces/queues/authorizationRules', variables('ServiceBusName'), 'fetch_transcription_queue','FetchTranscription'), '2017-04-01').primaryConnectionString]", "FUNCTIONS_EXTENSION_VERSION": "~4", "FUNCTIONS_WORKER_RUNTIME": "dotnet", "HtmlResultOutputContainer": "[variables('HtmlResultOutputContainer')]", @@ -930,9 +902,9 @@ "MaxPollingDelayInMinutes": "[variables('MaxPollingDelayInMinutes')]", "JsonResultOutputContainer": "[variables('JsonResultOutputContainer')]", "RetryLimit": "[variables('RetryLimit')]", - "StartTranscriptionServiceBusConnectionString": "[listKeys(variables('AuthRuleCT'),'2015-08-01').primaryConnectionString]", + "StartTranscriptionServiceBusConnectionString": "[listKeys(resourceId('Microsoft.ServiceBus/namespaces/queues/authorizationRules', variables('ServiceBusName'), 'start_transcription_queue','StartTranscription'), '2017-04-01').primaryConnectionString]", "TextAnalyticsKey": "[concat('@Microsoft.KeyVault(VaultName=', variables('KeyVaultName'), ';SecretName=', variables('TextAnalyticsKeySecretName'), ')')]", - "TextAnalyticsRegion": "[parameters('TextAnalyticsRegion')]", + "TextAnalyticsEndpoint": "[parameters('TextAnalyticsEndpoint')]", "UseSqlDatabase": "[variables('UseSqlDatabase')]", "WEBSITE_RUN_FROM_PACKAGE": "[variables('FetchTranscriptionBinary')]", "CreateConsolidatedOutputFiles": "[variables('CreateConsolidatedOutputFiles')]", diff --git a/samples/ingestion/ingestion-client/Setup/ArmTemplateRealtime.json b/samples/ingestion/ingestion-client/Setup/ArmTemplateRealtime.json index cb5d7a7b2..0648c856b 100644 --- a/samples/ingestion/ingestion-client/Setup/ArmTemplateRealtime.json +++ b/samples/ingestion/ingestion-client/Setup/ArmTemplateRealtime.json @@ -123,7 +123,7 @@ } }, "variables": { - "Version": "v2.0.5", + "Version": "v2.0.12", "AudioInputContainer": "audio-input", "AudioProcessedContainer": "audio-processed", "ErrorFilesOutputContainer": "audio-failed", diff --git a/samples/ingestion/ingestion-client/Tests/EndToEndTests.cs b/samples/ingestion/ingestion-client/Tests/EndToEndTests.cs index 12521c5ff..b7c4faf98 100644 --- a/samples/ingestion/ingestion-client/Tests/EndToEndTests.cs +++ b/samples/ingestion/ingestion-client/Tests/EndToEndTests.cs @@ -15,7 +15,7 @@ namespace Tests using Connector.Serializable.Language.Conversations; using Connector.Serializable.TranscriptionStartedServiceBusMessage; - using Language; + using FetchTranscription; using Microsoft.CognitiveServices.Speech; using Microsoft.Extensions.Logging; @@ -83,13 +83,17 @@ public async Task AnalyzeConversationTestAsync() var provider = new AnalyzeConversationsProvider("en-US", subscriptionKey, region, Logger.Object); var body = File.ReadAllText(@"testFiles/summarizationInputSample.json"); var transcription = JsonConvert.DeserializeObject(body); - var jobIds = await provider.SubmitAnalyzeConversationsRequestAsync(transcription).ConfigureAwait(false); + + var speechTranscriptMapping = new Dictionary + { + { new AudioFileInfo("someUrl", 0, null), transcription } + }; + + var errors = await provider.SubmitTranscriptionAnalyticsJobsAsync(speechTranscriptMapping).ConfigureAwait(false); Console.WriteLine("Submit"); - Console.WriteLine(JsonConvert.SerializeObject(jobIds)); - Assert.AreEqual(0, jobIds.errors.Count()); - var req = jobIds.jobIds.Select(jobId => new AudioFileInfo(default, default, new TextAnalyticsRequests(default, default, new[] { new TextAnalyticsRequest(jobId, TextAnalyticsRequest.TextAnalyticsRequestStatus.Running) }))); + Assert.AreEqual(0, errors.Count()); - while (!await provider.ConversationalRequestsCompleted(req).ConfigureAwait(false)) + while ((await provider.GetTranscriptionAnalyticsJobStatusAsync(speechTranscriptMapping.Keys).ConfigureAwait(false)) == Connector.Enums.TranscriptionAnalyticsJobStatus.Running) { await Task.Delay(TimeSpan.FromSeconds(10)).ConfigureAwait(false); Console.WriteLine($"[{DateTime.Now}]jobs are running..."); @@ -97,7 +101,7 @@ public async Task AnalyzeConversationTestAsync() Console.WriteLine($"[{DateTime.Now}]jobs done."); - var err = await provider.AddConversationalEntitiesAsync(jobIds.jobIds, transcription); + var err = await provider.AddTranscriptionAnalyticsResultsToTranscriptsAsync(speechTranscriptMapping); Console.WriteLine($"annotation result: {JsonConvert.SerializeObject(transcription)}"); Assert.AreEqual(0, err.Count()); Assert.AreEqual(4, transcription.ConversationAnalyticsResults.AnalyzeConversationSummarizationResults.Conversations.First().Summaries.Count());