Files
meeting-assistant/MeetingAssistant/Speakers/AzureSpeechSpeakerIdentityDiarizationClient.cs
T

128 lines
4.6 KiB
C#

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;
}
}