feat: add local Resemblyzer speaker recognition
PR and Push Build/Test / build-and-test (push) Successful in 12m37s

This commit is contained in:
2026-09-11 13:49:27 +02:00
parent 43fc8aaec0
commit f86af983e8
48 changed files with 5250 additions and 273 deletions
@@ -24,6 +24,7 @@ public sealed class WorkflowRulesEditorChatPipeline : IWorkflowRulesEditorChatPi
private readonly IMeetingMetadataProvider meetingMetadataProvider;
private readonly ILaunchProfileOptionsProvider launchProfiles;
private readonly ISpeakerIdentityMergeService identityMergeService;
private readonly ResemblyzerVoiceVectorOutlierPruner outlierPruner;
private readonly AsrDiagnosticService asrDiagnosticService;
private readonly IConfiguration configuration;
@@ -38,6 +39,7 @@ public sealed class WorkflowRulesEditorChatPipeline : IWorkflowRulesEditorChatPi
IMeetingMetadataProvider meetingMetadataProvider,
ILaunchProfileOptionsProvider launchProfiles,
ISpeakerIdentityMergeService identityMergeService,
ResemblyzerVoiceVectorOutlierPruner outlierPruner,
AsrDiagnosticService asrDiagnosticService,
IConfiguration configuration)
{
@@ -51,6 +53,7 @@ public sealed class WorkflowRulesEditorChatPipeline : IWorkflowRulesEditorChatPi
this.meetingMetadataProvider = meetingMetadataProvider;
this.launchProfiles = launchProfiles;
this.identityMergeService = identityMergeService;
this.outlierPruner = outlierPruner;
this.asrDiagnosticService = asrDiagnosticService;
this.configuration = configuration;
}
@@ -78,7 +81,8 @@ public sealed class WorkflowRulesEditorChatPipeline : IWorkflowRulesEditorChatPi
identityMergeService,
asrDiagnosticService,
configuration,
logger: logger);
logger: logger,
outlierPruner: outlierPruner);
var messages = conversation
.Select(ToChatMessage)
.Append(new ChatMessage(ChatRole.User, userMessage.Trim()))
@@ -10,6 +10,7 @@ using MeetingAssistant.Transcription;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using YamlDotNet.Core;
using YamlDotNet.Serialization;
@@ -42,6 +43,7 @@ public sealed class WorkflowRulesEditorTools
private readonly IMeetingMetadataProvider? meetingMetadataProvider;
private readonly ILaunchProfileOptionsProvider? launchProfiles;
private readonly ISpeakerIdentityMergeService? identityMergeService;
private readonly ResemblyzerVoiceVectorOutlierPruner outlierPruner;
private readonly AsrDiagnosticService? asrDiagnosticService;
private readonly IConfiguration? configuration;
private readonly ILogger? logger;
@@ -61,7 +63,8 @@ public sealed class WorkflowRulesEditorTools
string? logDirectory = null,
string? specRootPath = null,
string? projectAgentsTemplatePath = null,
ILogger? logger = null)
ILogger? logger = null,
ResemblyzerVoiceVectorOutlierPruner? outlierPruner = null)
{
this.options = options;
rulesPath = WorkflowRulesPathResolver.Resolve(options.Automation.RulesPath);
@@ -82,6 +85,9 @@ public sealed class WorkflowRulesEditorTools
this.meetingMetadataProvider = meetingMetadataProvider;
this.launchProfiles = launchProfiles;
this.identityMergeService = identityMergeService;
this.outlierPruner = outlierPruner ?? new ResemblyzerVoiceVectorOutlierPruner(
speakerOptions.Resemblyzer,
NullLogger<ResemblyzerVoiceVectorOutlierPruner>.Instance);
this.asrDiagnosticService = asrDiagnosticService;
this.configuration = configuration;
this.logger = logger;
@@ -743,8 +749,8 @@ public sealed class WorkflowRulesEditorTools
string[]? candidateNames = null)
{
return Task.FromResult(
"Refused: speaker identities require at least one audio sample. " +
"Use a speaker override from an existing transcript speaker sample, or update/merge an existing sampled identity.");
"Refused: speaker identities require audio evidence (a WAV sample or voice vector). " +
"Use a speaker override from an existing transcript speaker, or update/merge an existing identity.");
}
public async Task<string> UpdateIdentity(
@@ -826,10 +832,12 @@ public sealed class WorkflowRulesEditorTools
return $"Could not find target {targetIdentityId} or source {sourceIdentityId}.";
}
SpeakerIdentityMerger.MergeInto(
SpeakerIdentityMerger.MergeIntoAndPrune(
target,
source,
speakerOptions.MaxSnippetsPerSpeaker);
speakerOptions.MaxSnippetsPerSpeaker,
speakerOptions.Resemblyzer.MaxVectorsPerIdentity,
outlierPruner);
context.SpeakerIdentities.Remove(source);
await context.SaveChangesAsync();
return ToJson(ToIdentityDetail(target));
@@ -1567,9 +1575,11 @@ public sealed class WorkflowRulesEditorTools
private static IQueryable<SpeakerIdentity> LoadIdentities(SpeakerIdentityDbContext context)
{
return context.SpeakerIdentities
.AsSplitQuery()
.Include(identity => identity.Aliases)
.Include(identity => identity.CandidateNames)
.Include(identity => identity.Snippets)
.Include(identity => identity.VoiceVectors)
.Include(identity => identity.References);
}
@@ -1591,6 +1601,7 @@ public sealed class WorkflowRulesEditorTools
identity.Aliases.Select(alias => alias.Name).Order(StringComparer.OrdinalIgnoreCase).ToArray(),
identity.CandidateNames.Select(candidate => candidate.Name).Order(StringComparer.OrdinalIgnoreCase).ToArray(),
identity.Snippets.Count,
identity.VoiceVectors.Count,
identity.References.Count,
identity.UpdatedAt);
}
@@ -1650,6 +1661,7 @@ public sealed class WorkflowRulesEditorTools
IReadOnlyList<string> Aliases,
IReadOnlyList<string> CandidateNames,
int SampleCount,
int VoiceVectorCount,
int ReferenceCount,
DateTimeOffset UpdatedAt);