Public Access
Add meeting workflow automation
This commit is contained in:
@@ -5,6 +5,7 @@ using MeetingAssistant.MeetingNotes;
|
||||
using MeetingAssistant.Speakers;
|
||||
using MeetingAssistant.Summary;
|
||||
using MeetingAssistant.Transcription;
|
||||
using MeetingAssistant.Workflow;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace MeetingAssistant.Recording;
|
||||
@@ -24,6 +25,7 @@ public sealed class MeetingRecordingCoordinator
|
||||
private readonly ISpeakerIdentityAttendeeCanonicalizer attendeeCanonicalizer;
|
||||
private readonly IDictationWordStore dictationWordStore;
|
||||
private readonly ILaunchProfileOptionsProvider? launchProfiles;
|
||||
private readonly IMeetingWorkflowEngine meetingWorkflowEngine;
|
||||
private readonly MeetingAssistantOptions options;
|
||||
private readonly ILogger<MeetingRecordingCoordinator> logger;
|
||||
private readonly SemaphoreSlim gate = new(1, 1);
|
||||
@@ -47,7 +49,8 @@ public sealed class MeetingRecordingCoordinator
|
||||
ISpeakerIdentificationService? speakerIdentificationService = null,
|
||||
IDictationWordStore? dictationWordStore = null,
|
||||
ISpeakerIdentityAttendeeCanonicalizer? attendeeCanonicalizer = null,
|
||||
ILaunchProfileOptionsProvider? launchProfiles = null)
|
||||
ILaunchProfileOptionsProvider? launchProfiles = null,
|
||||
IMeetingWorkflowEngine? meetingWorkflowEngine = null)
|
||||
{
|
||||
this.audioSource = audioSource;
|
||||
this.speechRecognitionPipelineFactory = speechRecognitionPipelineFactory;
|
||||
@@ -62,6 +65,7 @@ public sealed class MeetingRecordingCoordinator
|
||||
this.speakerIdentificationService = speakerIdentificationService;
|
||||
this.attendeeCanonicalizer = attendeeCanonicalizer ?? PassthroughSpeakerIdentityAttendeeCanonicalizer.Instance;
|
||||
this.launchProfiles = launchProfiles;
|
||||
this.meetingWorkflowEngine = meetingWorkflowEngine ?? NoopMeetingWorkflowEngine.Instance;
|
||||
this.options = options.Value;
|
||||
this.logger = logger;
|
||||
}
|
||||
@@ -130,6 +134,15 @@ public sealed class MeetingRecordingCoordinator
|
||||
assistantContextPath,
|
||||
summaryPath);
|
||||
await meetingArtifactStore.CreateAssistantContextAsync(currentArtifacts, currentMeetingNote, "", null, cancellationToken);
|
||||
await meetingWorkflowEngine.RunAsync(
|
||||
MeetingWorkflowEvent.Created(currentArtifacts),
|
||||
runOptions,
|
||||
cancellationToken);
|
||||
currentMeetingNote = await meetingNoteStore.ReadAsync(currentMeetingNote.Path, cancellationToken);
|
||||
await meetingArtifactStore.UpdateAssistantContextMeetingAsync(
|
||||
currentArtifacts,
|
||||
currentMeetingNote,
|
||||
cancellationToken);
|
||||
await transcriptStore.UpdateMetadataAsync(
|
||||
currentSession,
|
||||
currentArtifacts,
|
||||
@@ -407,6 +420,7 @@ public sealed class MeetingRecordingCoordinator
|
||||
run.MarkLiveIdentificationAttempted(checkpoint);
|
||||
await AddIdentifiedSpeakersToMeetingAttendeesAsync(
|
||||
result.AttendeeMatches,
|
||||
run.Artifacts,
|
||||
run.MeetingNotePath,
|
||||
run.Options,
|
||||
cancellationToken);
|
||||
@@ -607,6 +621,7 @@ public sealed class MeetingRecordingCoordinator
|
||||
finishedSegments,
|
||||
run.GetSpeakerSamplesSnapshot(),
|
||||
run.GetSpeakerMappingsSnapshot(),
|
||||
run.Artifacts,
|
||||
run.MeetingNotePath,
|
||||
run.Options,
|
||||
cancellationToken);
|
||||
@@ -618,6 +633,7 @@ public sealed class MeetingRecordingCoordinator
|
||||
IReadOnlyList<TranscriptionSegment> finishedSegments,
|
||||
IReadOnlyList<SpeakerAudioSample> samples,
|
||||
IReadOnlyDictionary<string, string> knownSpeakerMappings,
|
||||
MeetingSessionArtifacts artifacts,
|
||||
string meetingNotePath,
|
||||
MeetingAssistantOptions runOptions,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -640,6 +656,7 @@ public sealed class MeetingRecordingCoordinator
|
||||
cancellationToken);
|
||||
await AddIdentifiedSpeakersToMeetingAttendeesAsync(
|
||||
result.AttendeeMatches,
|
||||
artifacts,
|
||||
meetingNotePath,
|
||||
runOptions,
|
||||
cancellationToken);
|
||||
@@ -658,6 +675,7 @@ public sealed class MeetingRecordingCoordinator
|
||||
|
||||
private async Task AddIdentifiedSpeakersToMeetingAttendeesAsync(
|
||||
IReadOnlyList<SpeakerIdentityAttendeeMatch>? matches,
|
||||
MeetingSessionArtifacts artifacts,
|
||||
string meetingNotePath,
|
||||
MeetingAssistantOptions runOptions,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -685,7 +703,17 @@ public sealed class MeetingRecordingCoordinator
|
||||
.Append(match.DisplayName)
|
||||
.Select(NormalizeAttendeeName)
|
||||
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
if (RemoveDuplicateAcceptedAliases(meetingNote.Frontmatter.Attendees, match.DisplayName, acceptedNames))
|
||||
{
|
||||
existingNames = meetingNote.Frontmatter.Attendees
|
||||
.Select(NormalizeAttendeeName)
|
||||
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (acceptedNames.Any(existingNames.Contains))
|
||||
{
|
||||
continue;
|
||||
@@ -697,20 +725,31 @@ public sealed class MeetingRecordingCoordinator
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (!changed)
|
||||
if (changed)
|
||||
{
|
||||
return;
|
||||
var savedMeetingNote = await meetingNoteStore.SaveAsync(meetingNote, runOptions, cancellationToken);
|
||||
if (string.Equals(currentMeetingNote?.Path, savedMeetingNote.Path, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
currentMeetingNote = savedMeetingNote;
|
||||
}
|
||||
|
||||
logger.LogInformation(
|
||||
"Added identified speaker(s) to meeting note attendees for {MeetingNotePath}",
|
||||
savedMeetingNote.Path);
|
||||
}
|
||||
|
||||
var savedMeetingNote = await meetingNoteStore.SaveAsync(meetingNote, runOptions, cancellationToken);
|
||||
if (string.Equals(currentMeetingNote?.Path, savedMeetingNote.Path, StringComparison.OrdinalIgnoreCase))
|
||||
foreach (var match in matches)
|
||||
{
|
||||
currentMeetingNote = savedMeetingNote;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(match.DisplayName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.LogInformation(
|
||||
"Added identified speaker(s) to meeting note attendees for {MeetingNotePath}",
|
||||
savedMeetingNote.Path);
|
||||
await meetingWorkflowEngine.RunAsync(
|
||||
MeetingWorkflowEvent.SpeakerIdentified(artifacts, match.DisplayName.Trim()),
|
||||
runOptions,
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<MeetingNote?> CompleteMeetingNoteAsync(
|
||||
@@ -739,6 +778,59 @@ public sealed class MeetingRecordingCoordinator
|
||||
return MeetingAttendeeNames.NormalizeDisplayName(attendee);
|
||||
}
|
||||
|
||||
private static bool RemoveDuplicateAcceptedAliases(
|
||||
List<string> attendees,
|
||||
string displayName,
|
||||
IReadOnlyCollection<string> acceptedNames)
|
||||
{
|
||||
var normalizedDisplayName = NormalizeAttendeeName(displayName);
|
||||
if (string.IsNullOrWhiteSpace(normalizedDisplayName) ||
|
||||
!attendees.Any(attendee => string.Equals(
|
||||
NormalizeAttendeeName(attendee),
|
||||
normalizedDisplayName,
|
||||
StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var changed = false;
|
||||
var displayNameKept = false;
|
||||
var cleaned = new List<string>();
|
||||
foreach (var attendee in attendees)
|
||||
{
|
||||
var normalizedAttendee = NormalizeAttendeeName(attendee);
|
||||
if (string.Equals(normalizedAttendee, normalizedDisplayName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (displayNameKept)
|
||||
{
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
displayNameKept = true;
|
||||
cleaned.Add(attendee);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (acceptedNames.Contains(normalizedAttendee, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
cleaned.Add(attendee);
|
||||
}
|
||||
|
||||
if (!changed)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
attendees.Clear();
|
||||
attendees.AddRange(cleaned);
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task RunSummaryAsync(
|
||||
RecordingRun run,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -774,7 +866,7 @@ public sealed class MeetingRecordingCoordinator
|
||||
AssistantContextState state,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!run.TryTransitionTo(state))
|
||||
if (!run.TryTransitionTo(state, out var fromState))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -783,6 +875,10 @@ public sealed class MeetingRecordingCoordinator
|
||||
run.Artifacts,
|
||||
state,
|
||||
cancellationToken);
|
||||
await meetingWorkflowEngine.RunAsync(
|
||||
MeetingWorkflowEvent.StateTransition(run.Artifacts, fromState, state),
|
||||
run.Options,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private bool HasConfiguredFinalizer(MeetingAssistantOptions runOptions)
|
||||
@@ -959,15 +1055,19 @@ public sealed class MeetingRecordingCoordinator
|
||||
LiveIdentificationCancellationSource.Cancel();
|
||||
}
|
||||
|
||||
public bool TryTransitionTo(AssistantContextState state)
|
||||
public bool TryTransitionTo(
|
||||
AssistantContextState state,
|
||||
out AssistantContextState fromState)
|
||||
{
|
||||
lock (stateGate)
|
||||
{
|
||||
if (StateRank(state) <= StateRank(ContextState))
|
||||
{
|
||||
fromState = ContextState;
|
||||
return false;
|
||||
}
|
||||
|
||||
fromState = ContextState;
|
||||
ContextState = state;
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user