Files
meeting-assistant/MeetingAssistant/Speakers/SpeakerIdentityMerger.cs
T
codex f86af983e8
PR and Push Build/Test / build-and-test (push) Successful in 12m37s
feat: add local Resemblyzer speaker recognition
2026-09-11 13:49:27 +02:00

98 lines
3.2 KiB
C#

namespace MeetingAssistant.Speakers;
internal static class SpeakerIdentityMerger
{
public static void MergeIntoAndPrune(
SpeakerIdentity target,
SpeakerIdentity source,
int maxSnippets,
int maxVoiceVectors,
ResemblyzerVoiceVectorOutlierPruner outlierPruner)
{
outlierPruner.Prune(target);
outlierPruner.Prune(source);
MergeInto(target, source, maxSnippets, maxVoiceVectors);
outlierPruner.Prune(target);
}
public static void MergeInto(
SpeakerIdentity target,
SpeakerIdentity source,
int maxSnippets,
int maxVoiceVectors = int.MaxValue)
{
AddAlias(target, source.CanonicalName);
foreach (var alias in source.Aliases)
{
AddAlias(target, alias.Name);
}
foreach (var candidate in source.CandidateNames)
{
AddAlias(target, candidate.Name);
}
foreach (var reference in source.References)
{
SpeakerIdentityReferences.AddIfMissing(target, reference);
}
var retainedSnippets = target.Snippets
.Concat(source.Snippets)
.OrderBy(snippet => StableSnippetKey(snippet.WavBytes))
.Take(Math.Max(1, maxSnippets))
.Select(snippet => new SpeakerSnippet
{
WavBytes = snippet.WavBytes,
CreatedAt = snippet.CreatedAt
})
.ToList();
target.Snippets.Clear();
target.Snippets.AddRange(retainedSnippets);
var retainedVectors = target.VoiceVectors
.Concat(source.VoiceVectors)
.GroupBy(vector => vector.Fingerprint, StringComparer.Ordinal)
.Select(group => group.OrderByDescending(vector => vector.CreatedAt).First())
.OrderByDescending(vector => vector.CreatedAt)
.ThenBy(vector => vector.Fingerprint, StringComparer.Ordinal)
.Take(Math.Max(1, maxVoiceVectors))
.Select(vector => new SpeakerVoiceVector
{
SpeakerIdentity = target,
ModelId = vector.ModelId,
Dimensions = vector.Dimensions,
VectorBytes = vector.VectorBytes.ToArray(),
Fingerprint = vector.Fingerprint,
CreatedAt = vector.CreatedAt
})
.ToList();
target.VoiceVectors.Clear();
target.VoiceVectors.AddRange(retainedVectors);
target.UpdatedAt = DateTimeOffset.UtcNow;
}
private static void AddAlias(SpeakerIdentity identity, string? alias)
{
if (string.IsNullOrWhiteSpace(alias) ||
string.Equals(identity.CanonicalName, alias, StringComparison.OrdinalIgnoreCase) ||
identity.Aliases.Any(existing => string.Equals(existing.Name, alias, StringComparison.OrdinalIgnoreCase)))
{
return;
}
identity.Aliases.Add(new SpeakerAlias { Name = alias.Trim() });
}
private static int StableSnippetKey(byte[] snippet)
{
var hash = new HashCode();
foreach (var value in snippet)
{
hash.Add(value);
}
return hash.ToHashCode();
}
}