Public Access
Add meeting assistant speech and summary automation
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
using System.Threading.Channels;
|
||||
using MeetingAssistant.Transcription;
|
||||
using Microsoft.CognitiveServices.Speech;
|
||||
using Microsoft.CognitiveServices.Speech.Audio;
|
||||
using Microsoft.CognitiveServices.Speech.Transcription;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace MeetingAssistant.Speakers;
|
||||
|
||||
public sealed class AzureSpeechSpeakerIdentityDiarizationClient : ISpeakerIdentityDiarizationClient
|
||||
{
|
||||
private readonly AzureSpeechOptions options;
|
||||
private readonly ILogger<AzureSpeechSpeakerIdentityDiarizationClient> logger;
|
||||
|
||||
public AzureSpeechSpeakerIdentityDiarizationClient(
|
||||
IOptions<MeetingAssistantOptions> options,
|
||||
ILogger<AzureSpeechSpeakerIdentityDiarizationClient> logger)
|
||||
{
|
||||
this.options = options.Value.SpeakerIdentification.AzureSpeech;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<TranscriptionSegment>> DiarizeAsync(
|
||||
string wavPath,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var key = AzureSpeechStreamingTranscriptionProvider.ResolveKey(options);
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
logger.LogWarning(
|
||||
"Speaker identity Azure Speech matching is enabled but no key is configured directly or via {KeyEnv}",
|
||||
options.KeyEnv);
|
||||
return [];
|
||||
}
|
||||
|
||||
var speechConfig = string.IsNullOrWhiteSpace(options.Region)
|
||||
? SpeechConfig.FromEndpoint(new Uri(options.Endpoint), key)
|
||||
: SpeechConfig.FromSubscription(key, options.Region);
|
||||
speechConfig.SpeechRecognitionLanguage = ResolveRecognitionLanguage(options);
|
||||
speechConfig.OutputFormat = OutputFormat.Detailed;
|
||||
speechConfig.SetProperty(
|
||||
PropertyId.SpeechServiceResponse_DiarizeIntermediateResults,
|
||||
options.DiarizeIntermediateResults ? "true" : "false");
|
||||
|
||||
using var audioConfig = AudioConfig.FromWavFileInput(wavPath);
|
||||
using var recognizer = new ConversationTranscriber(speechConfig, audioConfig);
|
||||
var channel = Channel.CreateUnbounded<TranscriptionSegment>();
|
||||
|
||||
recognizer.Transcribed += (_, args) =>
|
||||
{
|
||||
if (args.Result.Reason != ResultReason.RecognizedSpeech)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var start = TimeSpan.FromTicks(args.Result.OffsetInTicks);
|
||||
channel.Writer.TryWrite(new TranscriptionSegment(
|
||||
start,
|
||||
start + args.Result.Duration,
|
||||
NormalizeSpeakerId(args.Result.SpeakerId),
|
||||
args.Result.Text.Trim()));
|
||||
};
|
||||
recognizer.Canceled += (_, args) =>
|
||||
{
|
||||
if (args.Reason == CancellationReason.Error)
|
||||
{
|
||||
channel.Writer.TryComplete(new InvalidOperationException(
|
||||
$"Azure Speech identity matching canceled: {args.ErrorCode} {args.ErrorDetails}"));
|
||||
return;
|
||||
}
|
||||
|
||||
channel.Writer.TryComplete();
|
||||
};
|
||||
recognizer.SessionStopped += (_, _) => channel.Writer.TryComplete();
|
||||
|
||||
await recognizer.StartTranscribingAsync();
|
||||
var segments = new List<TranscriptionSegment>();
|
||||
try
|
||||
{
|
||||
await foreach (var segment in channel.Reader.ReadAllAsync(cancellationToken))
|
||||
{
|
||||
segments.Add(segment);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
var stopTask = recognizer.StopTranscribingAsync();
|
||||
if (options.RecognitionStopTimeout > TimeSpan.Zero)
|
||||
{
|
||||
await stopTask.WaitAsync(options.RecognitionStopTimeout, CancellationToken.None);
|
||||
}
|
||||
else
|
||||
{
|
||||
await stopTask.WaitAsync(CancellationToken.None);
|
||||
}
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
logger.LogWarning("Timed out while stopping Azure Speech identity diarization recognizer");
|
||||
}
|
||||
}
|
||||
|
||||
return segments;
|
||||
}
|
||||
|
||||
internal static string ResolveRecognitionLanguage(AzureSpeechOptions options)
|
||||
{
|
||||
if (!options.Language.Equals("auto", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return options.Language;
|
||||
}
|
||||
|
||||
return options.AutoDetectLanguages
|
||||
.Select(language => language.Trim())
|
||||
.FirstOrDefault(language => !string.IsNullOrWhiteSpace(language))
|
||||
?? "de-DE";
|
||||
}
|
||||
|
||||
private static string NormalizeSpeakerId(string? speakerId)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(speakerId) || speakerId.Equals("Unknown", StringComparison.OrdinalIgnoreCase)
|
||||
? "Unknown"
|
||||
: speakerId;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user