Public Access
155 lines
5.4 KiB
C#
155 lines
5.4 KiB
C#
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<MeetingAssistantOptions>
|
|
{
|
|
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);
|
|
}
|
|
}
|