Public Access
Add meeting assistant speech and summary automation
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
using MeetingAssistant;
|
||||
using MeetingAssistant.Speakers;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace MeetingAssistant.Tests;
|
||||
|
||||
public sealed class SpeakerIdentityMergeServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task MergeRecentIdentitiesConfirmsMatchTwiceAndMergesAliasesAndSamples()
|
||||
{
|
||||
await using var fixture = await SpeakerIdentityMergeFixture.CreateAsync();
|
||||
var older = await fixture.AddIdentityAsync(
|
||||
canonicalName: "Manuel",
|
||||
aliases: ["M. Schweigert"],
|
||||
snippets: [[1], [2], [3]],
|
||||
createdAt: DateTimeOffset.UtcNow.AddMonths(-2),
|
||||
transcriptionCount: 5);
|
||||
var recent = await fixture.AddIdentityAsync(
|
||||
canonicalName: "Guest Manuel",
|
||||
aliases: [],
|
||||
snippets: [[4], [5]],
|
||||
createdAt: DateTimeOffset.UtcNow.AddDays(-1),
|
||||
transcriptionCount: 2);
|
||||
fixture.Matcher.MatchIdentityIds.Enqueue(older.Id);
|
||||
fixture.Matcher.MatchIdentityIds.Enqueue(older.Id);
|
||||
var service = fixture.CreateService();
|
||||
|
||||
var result = await service.MergeRecentIdentitiesAsync(TimeSpan.FromDays(14), CancellationToken.None);
|
||||
|
||||
Assert.Equal(1, result.MergedPairs);
|
||||
Assert.Equal(2, fixture.Matcher.Requests.Count);
|
||||
Assert.Equal([4], fixture.Matcher.Requests[0].UnknownSnippet);
|
||||
Assert.Equal([5], fixture.Matcher.Requests[1].UnknownSnippet);
|
||||
var savedOlder = await fixture.LoadIdentityAsync(older.Id);
|
||||
Assert.Equal("Manuel", savedOlder.CanonicalName);
|
||||
Assert.Equal(7, savedOlder.TranscriptionCount);
|
||||
Assert.True(savedOlder.UpdatedAt > older.UpdatedAt);
|
||||
Assert.Equal(["Guest Manuel", "M. Schweigert"], savedOlder.Aliases.Select(alias => alias.Name).Order());
|
||||
Assert.Equal(3, savedOlder.Snippets.Count);
|
||||
Assert.False(await fixture.Context.SpeakerIdentities.AnyAsync(identity => identity.Id == recent.Id));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MergeRecentIdentitiesDoesNotMergeWithoutSecondValidation()
|
||||
{
|
||||
await using var fixture = await SpeakerIdentityMergeFixture.CreateAsync();
|
||||
var older = await fixture.AddIdentityAsync("Manuel", [], [[1]], DateTimeOffset.UtcNow.AddMonths(-2), 5);
|
||||
var recent = await fixture.AddIdentityAsync("Guest Manuel", [], [[4], [5]], DateTimeOffset.UtcNow.AddDays(-1), 2);
|
||||
fixture.Matcher.MatchIdentityIds.Enqueue(older.Id);
|
||||
var service = fixture.CreateService();
|
||||
|
||||
var result = await service.MergeRecentIdentitiesAsync(TimeSpan.FromDays(14), CancellationToken.None);
|
||||
|
||||
Assert.Equal(0, result.MergedPairs);
|
||||
Assert.True(await fixture.Context.SpeakerIdentities.AnyAsync(identity => identity.Id == recent.Id));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MergeRecentIdentitiesIgnoresOldSourceIdentities()
|
||||
{
|
||||
await using var fixture = await SpeakerIdentityMergeFixture.CreateAsync();
|
||||
await fixture.AddIdentityAsync("Older A", [], [[1], [2]], DateTimeOffset.UtcNow.AddMonths(-2), 5);
|
||||
await fixture.AddIdentityAsync("Older B", [], [[4], [5]], DateTimeOffset.UtcNow.AddMonths(-1), 2);
|
||||
var service = fixture.CreateService();
|
||||
|
||||
var result = await service.MergeRecentIdentitiesAsync(TimeSpan.FromDays(14), CancellationToken.None);
|
||||
|
||||
Assert.Equal(0, result.MergedPairs);
|
||||
Assert.Empty(fixture.Matcher.Requests);
|
||||
}
|
||||
|
||||
private sealed class SpeakerIdentityMergeFixture : IAsyncDisposable
|
||||
{
|
||||
private readonly string dbPath;
|
||||
|
||||
private SpeakerIdentityMergeFixture(string dbPath, SpeakerIdentityDbContext context)
|
||||
{
|
||||
this.dbPath = dbPath;
|
||||
Context = context;
|
||||
}
|
||||
|
||||
public SpeakerIdentityDbContext Context { get; }
|
||||
|
||||
public QueueingSpeakerIdentityMatcher Matcher { get; } = new();
|
||||
|
||||
public static async Task<SpeakerIdentityMergeFixture> CreateAsync()
|
||||
{
|
||||
var dbPath = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", $"{Guid.NewGuid():N}.db");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!);
|
||||
var context = new SpeakerIdentityDbContext(new DbContextOptionsBuilder<SpeakerIdentityDbContext>()
|
||||
.UseSqlite($"Data Source={dbPath};Pooling=False")
|
||||
.Options);
|
||||
await SpeakerIdentitySchema.EnsureCreatedOrUpdatedAsync(context, CancellationToken.None);
|
||||
return new SpeakerIdentityMergeFixture(dbPath, context);
|
||||
}
|
||||
|
||||
public SpeakerIdentityMergeService CreateService()
|
||||
{
|
||||
return new SpeakerIdentityMergeService(
|
||||
new TestSpeakerIdentityDbContextFactory(dbPath),
|
||||
Matcher,
|
||||
Options.Create(new MeetingAssistantOptions
|
||||
{
|
||||
SpeakerIdentification = new SpeakerIdentificationOptions
|
||||
{
|
||||
MatchBatchSize = 6,
|
||||
MaxSnippetsPerSpeaker = 3
|
||||
}
|
||||
}),
|
||||
NullLogger<SpeakerIdentityMergeService>.Instance);
|
||||
}
|
||||
|
||||
public async Task<SpeakerIdentity> AddIdentityAsync(
|
||||
string? canonicalName,
|
||||
IReadOnlyList<string> aliases,
|
||||
IReadOnlyList<byte[]> snippets,
|
||||
DateTimeOffset createdAt,
|
||||
int transcriptionCount)
|
||||
{
|
||||
var identity = new SpeakerIdentity
|
||||
{
|
||||
CanonicalName = canonicalName,
|
||||
CreatedAt = createdAt,
|
||||
UpdatedAt = createdAt,
|
||||
TranscriptionCount = transcriptionCount,
|
||||
Aliases = aliases.Select(alias => new SpeakerAlias { Name = alias }).ToList(),
|
||||
Snippets = snippets
|
||||
.Select((snippet, index) => new SpeakerSnippet
|
||||
{
|
||||
WavBytes = snippet,
|
||||
CreatedAt = createdAt.AddMinutes(index)
|
||||
})
|
||||
.ToList()
|
||||
};
|
||||
Context.SpeakerIdentities.Add(identity);
|
||||
await Context.SaveChangesAsync();
|
||||
return identity;
|
||||
}
|
||||
|
||||
public Task<SpeakerIdentity> LoadIdentityAsync(int id)
|
||||
{
|
||||
Context.ChangeTracker.Clear();
|
||||
return Context.SpeakerIdentities
|
||||
.Include(identity => identity.Aliases)
|
||||
.Include(identity => identity.Snippets)
|
||||
.SingleAsync(identity => identity.Id == id);
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await Context.DisposeAsync();
|
||||
if (File.Exists(dbPath))
|
||||
{
|
||||
File.Delete(dbPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestSpeakerIdentityDbContextFactory : IDbContextFactory<SpeakerIdentityDbContext>
|
||||
{
|
||||
private readonly string dbPath;
|
||||
|
||||
public TestSpeakerIdentityDbContextFactory(string dbPath)
|
||||
{
|
||||
this.dbPath = dbPath;
|
||||
}
|
||||
|
||||
public SpeakerIdentityDbContext CreateDbContext()
|
||||
{
|
||||
return new SpeakerIdentityDbContext(new DbContextOptionsBuilder<SpeakerIdentityDbContext>()
|
||||
.UseSqlite($"Data Source={dbPath};Pooling=False")
|
||||
.Options);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class QueueingSpeakerIdentityMatcher : ISpeakerIdentityMatcher
|
||||
{
|
||||
public Queue<int> MatchIdentityIds { get; } = new();
|
||||
|
||||
public List<SpeakerIdentityMatchRequest> Requests { get; } = [];
|
||||
|
||||
public Task<SpeakerIdentityMatch?> MatchAsync(
|
||||
SpeakerIdentityMatchRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Requests.Add(request);
|
||||
if (!MatchIdentityIds.TryDequeue(out var identityId))
|
||||
{
|
||||
return Task.FromResult<SpeakerIdentityMatch?>(null);
|
||||
}
|
||||
|
||||
return Task.FromResult<SpeakerIdentityMatch?>(new SpeakerIdentityMatch(identityId));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user