Add meeting assistant speech and summary automation

This commit is contained in:
2026-05-21 09:55:39 +02:00
parent 1e97d2b9ff
commit 48d98d9cbb
63 changed files with 5143 additions and 151 deletions
+61
View File
@@ -2,8 +2,10 @@ using MeetingAssistant;
using MeetingAssistant.Hotkeys;
using MeetingAssistant.MeetingNotes;
using MeetingAssistant.Recording;
using MeetingAssistant.Speakers;
using MeetingAssistant.Summary;
using MeetingAssistant.Transcription;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
var builder = WebApplication.CreateBuilder(args);
@@ -25,8 +27,22 @@ builder.Services.AddSingleton<IMeetingNoteStore, MarkdownMeetingNoteStore>();
builder.Services.AddSingleton<IMeetingNoteOpener, ObsidianMeetingNoteOpener>();
builder.Services.AddSingleton<IMeetingArtifactStore, MarkdownMeetingArtifactStore>();
builder.Services.AddSingleton<IRecordedAudioStore, TemporaryRecordedAudioStore>();
builder.Services.AddSingleton<IDictationWordStore, MarkdownDictationWordStore>();
builder.Services.AddDbContextFactory<SpeakerIdentityDbContext>((services, dbOptions) =>
{
var appOptions = services.GetRequiredService<IOptions<MeetingAssistantOptions>>().Value;
var databasePath = VaultPath.Resolve(appOptions.SpeakerIdentification.DatabasePath);
Directory.CreateDirectory(Path.GetDirectoryName(databasePath)!);
dbOptions.UseSqlite($"Data Source={databasePath}");
});
builder.Services.AddSingleton<ISpeakerSnippetExtractor, WavSpeakerSnippetExtractor>();
builder.Services.AddSingleton<ISpeakerIdentityDiarizationClient, AzureSpeechSpeakerIdentityDiarizationClient>();
builder.Services.AddSingleton<ISpeakerIdentityMatcher, AzureSpeechSpeakerIdentityMatcher>();
builder.Services.AddSingleton<ISpeakerIdentificationService, SpeakerIdentityService>();
builder.Services.AddSingleton<ISpeakerIdentityMergeService, SpeakerIdentityMergeService>();
builder.Services.AddSingleton<IMeetingSummaryArtifactResolver, MeetingSummaryArtifactResolver>();
builder.Services.AddSingleton<IMeetingSummaryFailureWriter, MeetingSummaryFailureWriter>();
builder.Services.AddSingleton<IMeetingSummaryInstructionBuilder, MeetingSummaryInstructionBuilder>();
builder.Services.AddSingleton<IMeetingSummaryPipeline, OpenAiMeetingSummaryAgentPipeline>();
builder.Services.AddSingleton<AsrDiagnosticService>();
builder.Services.AddSingleton<ICommandRunner, ProcessCommandRunner>();
@@ -41,6 +57,7 @@ builder.Services.AddTransient<PyannoteTranscriptFinalizer>();
builder.Services.AddSingleton<ISpeechRecognitionPipelineFactory, ConfiguredSpeechRecognitionPipelineFactory>();
builder.Services.AddSingleton<MeetingRecordingCoordinator>();
builder.Services.AddHostedService<TemporaryRecordingCleanupHostedService>();
builder.Services.AddHostedService<SpeakerIdentityDatabaseInitializer>();
builder.Services.AddHostedService<SpeechRecognitionPipelineHostedService>();
#if WINDOWS
builder.Services.AddHostedService<GlobalHotkeyService>();
@@ -55,6 +72,48 @@ app.MapGet("/health", () => Results.Ok(new
}));
app.MapGet("/recording/status", (MeetingRecordingCoordinator coordinator) => Results.Ok(coordinator.CurrentStatus));
app.MapGet("/diagnostics/outlook/current-meeting", async (
IMeetingMetadataProvider metadataProvider,
IOptions<MeetingAssistantOptions> options,
DateTimeOffset? startedAt,
double? timeoutSeconds,
CancellationToken cancellationToken) =>
{
var lookupStartedAt = startedAt ?? DateTimeOffset.Now;
var timeout = timeoutSeconds is > 0
? TimeSpan.FromSeconds(timeoutSeconds.Value)
: options.Value.Recording.MetadataLookupTimeout;
if (metadataProvider is IMeetingMetadataDiagnosticProvider diagnostics)
{
return Results.Ok(await diagnostics.DiagnoseCurrentMeetingAsync(
lookupStartedAt,
timeout,
cancellationToken));
}
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
var metadata = await metadataProvider.GetCurrentMeetingAsync(lookupStartedAt, cancellationToken);
stopwatch.Stop();
return Results.Ok(new MeetingMetadataDiagnosticResult(
metadataProvider.GetType().Name,
lookupStartedAt,
metadata is not null,
false,
stopwatch.ElapsedMilliseconds,
metadata,
metadata is null ? "No meeting metadata provider result was available." : null));
});
app.MapPost("/diagnostics/speaker-identities/merge", async (
SpeakerIdentityMergeDiagnosticRequest request,
ISpeakerIdentityMergeService mergeService,
IOptions<MeetingAssistantOptions> options,
CancellationToken cancellationToken) =>
{
var recentAge = request.RecentDays is > 0
? TimeSpan.FromDays(request.RecentDays.Value)
: options.Value.SpeakerIdentification.MergeRecentIdentityAge;
return Results.Ok(await mergeService.MergeRecentIdentitiesAsync(recentAge, cancellationToken));
});
app.MapPost("/recording/toggle", async (MeetingRecordingCoordinator coordinator, CancellationToken cancellationToken) =>
Results.Ok(await coordinator.ToggleAsync(cancellationToken)));
app.MapPost("/recording/start", async (MeetingRecordingCoordinator coordinator, CancellationToken cancellationToken) =>
@@ -189,3 +248,5 @@ public partial class Program;
public sealed record AsrDiagnosticRequest(string Path, int? NumSpeakers = null);
public sealed record SummaryRetryRequest(string SummaryPath);
public sealed record SpeakerIdentityMergeDiagnosticRequest(double? RecentDays = null);