using Microsoft.Extensions.Options; namespace MeetingAssistant.Speakers; public sealed record SpeakerSampleCollectionPolicy( int MinimumRetainedSamples, bool RequireNonOverlappingSamples) { public static SpeakerSampleCollectionPolicy ExistingBackend { get; } = new(1, false); public static SpeakerSampleCollectionPolicy IndependentVectors(int maximumVectors) { return new SpeakerSampleCollectionPolicy(Math.Max(1, maximumVectors), true); } public int ResolveRetainedSampleLimit(int configuredLimit) { return Math.Max(Math.Max(1, configuredLimit), MinimumRetainedSamples); } } internal static class SpeakerSampleDurationConfiguration { public static void ValidateOrThrow( SpeakerIdentificationOptions options, string context) { ValidateOrThrow( options.MinimumSampleSpeechDuration, options.MaximumSampleDuration, context); } public static void ValidateOrThrow( TimeSpan minimumDuration, TimeSpan maximumDuration, string context) { var error = GetError(minimumDuration, maximumDuration); if (error is not null) { throw new InvalidOperationException($"{context}: {error}"); } } public static string? GetError( TimeSpan minimumDuration, TimeSpan maximumDuration) { if (minimumDuration < TimeSpan.Zero) { return "SpeakerIdentification:MinimumSampleSpeechDuration must not be negative."; } if (maximumDuration <= TimeSpan.Zero) { return "SpeakerIdentification:MaximumSampleDuration must be greater than zero."; } return minimumDuration > maximumDuration ? "SpeakerIdentification:MinimumSampleSpeechDuration must not exceed SpeakerIdentification:MaximumSampleDuration." : null; } } internal static class ResemblyzerSpeakerRecognitionConfiguration { public static void ValidateOrThrow( ResemblyzerSpeakerRecognitionOptions options, string context) { var error = GetError(options); if (error is not null) { throw new InvalidOperationException($"{context}: {error}"); } } public static string? GetError(ResemblyzerSpeakerRecognitionOptions options) { if (options.RequiredVectorsPerSpeaker < 1) { return "SpeakerIdentification:Resemblyzer:RequiredVectorsPerSpeaker must be at least one."; } if (options.MaxVectorsPerIdentity < options.RequiredVectorsPerSpeaker) { return "SpeakerIdentification:Resemblyzer:MaxVectorsPerIdentity must not be less than RequiredVectorsPerSpeaker."; } if (options.OutlierPruningMinimumVectors < options.RequiredVectorsPerSpeaker) { return "SpeakerIdentification:Resemblyzer:OutlierPruningMinimumVectors must not be less than RequiredVectorsPerSpeaker."; } if (options.OutlierPruningMinimumVectors > options.MaxVectorsPerIdentity) { return "SpeakerIdentification:Resemblyzer:OutlierPruningMinimumVectors must not exceed MaxVectorsPerIdentity."; } if (options.OutlierPruningMinimumNeighbors < 1 || options.OutlierPruningMinimumNeighbors > options.OutlierPruningMinimumVectors) { return "SpeakerIdentification:Resemblyzer:OutlierPruningMinimumNeighbors must be between one and OutlierPruningMinimumVectors."; } if (!IsCosine(options.OutlierPruningNeighborSimilarity)) { return "SpeakerIdentification:Resemblyzer:OutlierPruningNeighborSimilarity must be finite and between -1 and 1."; } if (!double.IsFinite(options.OutlierPruningMinimumClusterRatio) || options.OutlierPruningMinimumClusterRatio is <= 0 or > 1) { return "SpeakerIdentification:Resemblyzer:OutlierPruningMinimumClusterRatio must be finite, greater than zero, and at most one."; } if (!IsCosine(options.MinimumClusterCohesion)) { return "SpeakerIdentification:Resemblyzer:MinimumClusterCohesion must be finite and between -1 and 1."; } if (!IsCosine(options.MinimumIdentitySimilarity)) { return "SpeakerIdentification:Resemblyzer:MinimumIdentitySimilarity must be finite and between -1 and 1."; } return !double.IsFinite(options.MinimumSimilarityMargin) || options.MinimumSimilarityMargin is < 0 or > 2 ? "SpeakerIdentification:Resemblyzer:MinimumSimilarityMargin must be finite and between 0 and 2." : null; } private static bool IsCosine(double value) { return double.IsFinite(value) && value is >= -1 and <= 1; } } internal sealed class MeetingAssistantSpeakerSampleOptionsValidator : IValidateOptions { public ValidateOptionsResult Validate(string? name, MeetingAssistantOptions options) { var error = SpeakerSampleDurationConfiguration.GetError( options.SpeakerIdentification.MinimumSampleSpeechDuration, options.SpeakerIdentification.MaximumSampleDuration) ?? ResemblyzerSpeakerRecognitionConfiguration.GetError( options.SpeakerIdentification.Resemblyzer); return error is null ? ValidateOptionsResult.Success : ValidateOptionsResult.Fail(error); } }