Add meeting workflow automation

This commit is contained in:
2026-05-27 12:55:18 +02:00
parent e85274829a
commit b114071957
29 changed files with 1703 additions and 12 deletions
@@ -4,6 +4,7 @@ using MeetingAssistant.Speakers;
using MeetingAssistant.Transcription;
using MeetingAssistant.MeetingNotes;
using MeetingAssistant.Summary;
using MeetingAssistant.Workflow;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
@@ -634,6 +635,41 @@ public sealed class RecordingCoordinatorTests
Assert.Equal(["Chris <chris@example.com>"], attendees);
}
[Fact]
public async Task StopRemovesIdentifiedSpeakerAliasDuplicatesWhenCanonicalAttendeeExists()
{
var audioSource = new CapturedChunkThenCancelAudioSource(new AudioChunk(new byte[] { 1, 0, 2, 0 }, 16000, 1));
var noteStore = new InMemoryMeetingNoteStore();
var finalizer = new CapturingTranscriptFinalizer(
[
new TranscriptionSegment(TimeSpan.Zero, TimeSpan.FromSeconds(1), "Guest03", "hello")
]);
var coordinator = new MeetingRecordingCoordinator(
audioSource,
new TestSpeechRecognitionPipelineFactory(new FinalSegmentOnAudioCompletionProvider(), finalizer.FinalizeAsync),
new InMemoryTranscriptStore(),
noteStore,
new CapturingMeetingNoteOpener(),
new InMemoryMeetingArtifactStore(),
new InMemoryRecordedAudioStore(),
new CapturingMeetingSummaryPipeline(),
Options.Create(new MeetingAssistantOptions()),
NullLogger<MeetingRecordingCoordinator>.Instance,
speakerIdentificationService: new FixedSpeakerIdentificationService(
"Guest03",
"Christopher",
["Christopher", "Chris"]));
await coordinator.StartAsync(CancellationToken.None);
noteStore.UpdateAttendees(["Ada", "Christopher", "Chris <chris@example.com>", "Chris"]);
await audioSource.WaitUntilCapturedAsync();
await coordinator.StopAsync(CancellationToken.None);
var attendees = Assert.IsType<List<string>>(noteStore.SavedNote?.Frontmatter.Attendees);
Assert.Equal(["Ada", "Christopher"], attendees);
}
[Fact]
public async Task LiveSpeakerIdentityMatchRelabelsCurrentAndFutureTranscriptSegments()
{
@@ -975,6 +1011,43 @@ public sealed class RecordingCoordinatorTests
artifactStore.States);
}
[Fact]
public async Task RecordingLifecycleRunsMeetingWorkflowEvents()
{
var audioSource = new CapturedChunkThenCancelAudioSource(new AudioChunk([1, 0, 2, 0], 16000, 1));
var workflowEngine = new CapturingMeetingWorkflowEngine();
var finalizer = new CapturingTranscriptFinalizer(
[new TranscriptionSegment(TimeSpan.Zero, TimeSpan.FromSeconds(1), "Guest03", "hello")]);
var coordinator = new MeetingRecordingCoordinator(
audioSource,
new TestSpeechRecognitionPipelineFactory(
new FinalSegmentOnAudioCompletionProvider(),
finalizer.FinalizeAsync),
new InMemoryTranscriptStore(),
new InMemoryMeetingNoteStore(),
new CapturingMeetingNoteOpener(),
new InMemoryMeetingArtifactStore(),
new InMemoryRecordedAudioStore(),
new CapturingMeetingSummaryPipeline(),
Options.Create(CreateOptionsWithoutFinalizer()),
NullLogger<MeetingRecordingCoordinator>.Instance,
speakerIdentificationService: new FixedSpeakerIdentificationService("Guest03", "Chris"),
meetingWorkflowEngine: workflowEngine);
await coordinator.StartAsync(CancellationToken.None);
await audioSource.WaitUntilCapturedAsync();
await coordinator.StopAsync(CancellationToken.None);
Assert.Contains(workflowEngine.Events, e => e.Type == MeetingWorkflowEventType.Created);
Assert.Contains(workflowEngine.Events, e =>
e.Type == MeetingWorkflowEventType.StateTransition &&
e.FromState == AssistantContextState.CollectingMetadata &&
e.ToState == AssistantContextState.Transcribing);
Assert.Contains(workflowEngine.Events, e =>
e.Type == MeetingWorkflowEventType.SpeakerIdentified &&
e.SpeakerName == "Chris");
}
[Fact]
public async Task VaultTranscriptStoreCreatesConfiguredFolderAndAppendsSegments()
{
@@ -1309,6 +1382,20 @@ public sealed class RecordingCoordinatorTests
}
}
private sealed class CapturingMeetingWorkflowEngine : IMeetingWorkflowEngine
{
public List<MeetingWorkflowEvent> Events { get; } = [];
public Task RunAsync(
MeetingWorkflowEvent workflowEvent,
MeetingAssistantOptions options,
CancellationToken cancellationToken)
{
Events.Add(workflowEvent);
return Task.CompletedTask;
}
}
private sealed class InMemoryMeetingArtifactStore : IMeetingArtifactStore
{
public MeetingSessionArtifacts? CreatedArtifacts { get; private set; }