Public Access
feat: add local Resemblyzer speaker recognition
PR and Push Build/Test / build-and-test (push) Successful in 12m37s
PR and Push Build/Test / build-and-test (push) Successful in 12m37s
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user