Prevent sampleless speaker identities
PR and Push Build/Test / build-and-test (push) Successful in 8m38s

This commit is contained in:
2026-06-02 10:39:00 +02:00
parent a3bad1bdd4
commit e3f4e87319
10 changed files with 101 additions and 50 deletions
@@ -82,6 +82,15 @@ public sealed class SpeakerIdentityService : ISpeakerIdentificationService
if (target is null)
{
if (sourceCandidate is null && snippet.Length == 0)
{
logger.LogWarning(
"Skipping speaker override from {SourceSpeaker} to {TargetSpeaker} because no source sample could be resolved",
sourceLabel,
targetName);
return;
}
target = sourceCandidate ?? new SpeakerIdentity
{
CreatedAt = now,
@@ -317,7 +317,7 @@ public sealed class WorkflowRulesEditorChatPipeline : IWorkflowRulesEditorChatPi
AIFunctionFactory.Create(
tools.CreateIdentity,
"create_identity",
"Create a speaker identity with optional canonicalName, aliases, and candidateNames. Returns the created identity JSON."),
"Refuse sampleless speaker identity creation and explain that identities must be created from an existing transcript speaker sample."),
AIFunctionFactory.Create(
tools.UpdateIdentity,
"update_identity",
@@ -61,7 +61,7 @@ public sealed class WorkflowRulesEditorInstructionBuilder : IWorkflowRulesEditor
"Project tools can create project folders and read, write, list, and search files in configured projects." + Environment.NewLine + Environment.NewLine +
"Meeting artifact tools can list recent summaries and read/search/write summaries, transcripts, meeting notes, and assistant context files for note post-processing and repair. Prefer frontmatter-specific write tools for metadata-only fixes." + Environment.NewLine + Environment.NewLine +
"Diagnostic tools mirror the local health, recording status, Outlook metadata, speaker identity merge, workflow reload, and ASR diagnostic endpoints without requiring HTTP." + Environment.NewLine + Environment.NewLine +
"Speaker identity tools can search/list/read/create/update/delete/merge identities, list/read/delete identity samples, and queue a sample for local playback." + Environment.NewLine + Environment.NewLine +
"Speaker identity tools can search/list/read/update/delete/merge identities, refuse sampleless identity creation, list/read/delete identity samples, and queue a sample for local playback. Do not delete the last sample from an identity; delete the identity instead." + Environment.NewLine + Environment.NewLine +
"Workflow rules reference documentation:" + Environment.NewLine +
"```markdown" + Environment.NewLine +
docs.Trim() + Environment.NewLine +
@@ -723,36 +723,14 @@ public sealed class WorkflowRulesEditorTools
}
}
public async Task<string> CreateIdentity(
public Task<string> CreateIdentity(
string? canonicalName = null,
string[]? aliases = null,
string[]? candidateNames = null)
{
var context = await CreatePreparedIdentityContextAsync(CancellationToken.None);
if (context is null)
{
return "Speaker identity database is not configured.";
}
await using (context)
{
var now = DateTimeOffset.UtcNow;
var identity = new SpeakerIdentity
{
CanonicalName = NormalizeNullable(canonicalName),
CreatedAt = now,
UpdatedAt = now,
Aliases = NormalizeNames(aliases)
.Select(name => new SpeakerAlias { Name = name })
.ToList(),
CandidateNames = NormalizeNames(candidateNames)
.Select(name => new SpeakerCandidateName { Name = name })
.ToList()
};
context.SpeakerIdentities.Add(identity);
await context.SaveChangesAsync();
return ToJson(ToIdentityDetail(identity));
}
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.");
}
public async Task<string> UpdateIdentity(
@@ -902,6 +880,13 @@ public sealed class WorkflowRulesEditorTools
return $"Sample {sampleId} was not found.";
}
var sampleCount = await context.SpeakerSnippets.CountAsync(candidate =>
candidate.SpeakerIdentityId == sample.SpeakerIdentityId);
if (sampleCount <= 1)
{
return $"Refused: sample {sampleId} is the last sample for identity {sample.SpeakerIdentityId}. Delete the identity instead.";
}
context.SpeakerSnippets.Remove(sample);
await context.SaveChangesAsync();
return $"Deleted sample {sampleId}.";