Public Access
Implement meeting assistant v1
This commit is contained in:
+180
-1
@@ -1,4 +1,51 @@
|
||||
using MeetingAssistant;
|
||||
using MeetingAssistant.Hotkeys;
|
||||
using MeetingAssistant.MeetingNotes;
|
||||
using MeetingAssistant.Recording;
|
||||
using MeetingAssistant.Summary;
|
||||
using MeetingAssistant.Transcription;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
builder.Services.Configure<MeetingAssistantOptions>(builder.Configuration.GetSection("MeetingAssistant"));
|
||||
#if WINDOWS
|
||||
builder.Services.AddSingleton<MicrophoneAudioSource>();
|
||||
builder.Services.AddSingleton<SystemAudioSource>();
|
||||
builder.Services.AddSingleton<IMeetingAudioSource>(services => new CompositeMeetingAudioSource(
|
||||
services.GetRequiredService<MicrophoneAudioSource>(),
|
||||
services.GetRequiredService<SystemAudioSource>(),
|
||||
services.GetRequiredService<ILogger<CompositeMeetingAudioSource>>()));
|
||||
builder.Services.AddSingleton<IMeetingMetadataProvider, OutlookClassicMeetingMetadataProvider>();
|
||||
#else
|
||||
builder.Services.AddSingleton<IMeetingAudioSource, UnavailableMeetingAudioSource>();
|
||||
builder.Services.AddSingleton<IMeetingMetadataProvider, NoopMeetingMetadataProvider>();
|
||||
#endif
|
||||
builder.Services.AddSingleton<ITranscriptStore, VaultTranscriptStore>();
|
||||
builder.Services.AddSingleton<IMeetingNoteStore, MarkdownMeetingNoteStore>();
|
||||
builder.Services.AddSingleton<IMeetingNoteOpener, ObsidianMeetingNoteOpener>();
|
||||
builder.Services.AddSingleton<IMeetingArtifactStore, MarkdownMeetingArtifactStore>();
|
||||
builder.Services.AddSingleton<IRecordedAudioStore, TemporaryRecordedAudioStore>();
|
||||
builder.Services.AddSingleton<IMeetingSummaryArtifactResolver, MeetingSummaryArtifactResolver>();
|
||||
builder.Services.AddSingleton<IMeetingSummaryFailureWriter, MeetingSummaryFailureWriter>();
|
||||
builder.Services.AddSingleton<IMeetingSummaryPipeline, OpenAiMeetingSummaryAgentPipeline>();
|
||||
builder.Services.AddSingleton<AsrDiagnosticService>();
|
||||
builder.Services.AddSingleton<ICommandRunner, ProcessCommandRunner>();
|
||||
builder.Services.AddSingleton<IFunAsrBackendReadinessProbe, FunAsrWebSocketBackendReadinessProbe>();
|
||||
builder.Services.AddSingleton<IFunAsrBackendLifecycle, FunAsrDockerBackendLifecycle>();
|
||||
builder.Services.AddSingleton<IFunAsrWebSocketConnectionFactory, ClientFunAsrWebSocketConnectionFactory>();
|
||||
builder.Services.AddTransient<FunAsrStreamingTranscriptionProvider>();
|
||||
builder.Services.AddTransient<WhisperLocalStreamingTranscriptionProvider>();
|
||||
builder.Services.AddTransient<AzureSpeechStreamingTranscriptionProvider>();
|
||||
builder.Services.AddTransient<FunAsrTranscriptFinalizer>();
|
||||
builder.Services.AddTransient<PyannoteTranscriptFinalizer>();
|
||||
builder.Services.AddSingleton<ISpeechRecognitionPipelineFactory, ConfiguredSpeechRecognitionPipelineFactory>();
|
||||
builder.Services.AddSingleton<MeetingRecordingCoordinator>();
|
||||
builder.Services.AddHostedService<TemporaryRecordingCleanupHostedService>();
|
||||
builder.Services.AddHostedService<SpeechRecognitionPipelineHostedService>();
|
||||
#if WINDOWS
|
||||
builder.Services.AddHostedService<GlobalHotkeyService>();
|
||||
#endif
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.MapGet("/health", () => Results.Ok(new
|
||||
@@ -7,6 +54,138 @@ app.MapGet("/health", () => Results.Ok(new
|
||||
status = "ok"
|
||||
}));
|
||||
|
||||
app.Run();
|
||||
app.MapGet("/recording/status", (MeetingRecordingCoordinator coordinator) => Results.Ok(coordinator.CurrentStatus));
|
||||
app.MapPost("/recording/toggle", async (MeetingRecordingCoordinator coordinator, CancellationToken cancellationToken) =>
|
||||
Results.Ok(await coordinator.ToggleAsync(cancellationToken)));
|
||||
app.MapPost("/recording/start", async (MeetingRecordingCoordinator coordinator, CancellationToken cancellationToken) =>
|
||||
Results.Ok(await coordinator.StartAsync(cancellationToken)));
|
||||
app.MapPost("/recording/stop", async (MeetingRecordingCoordinator coordinator, CancellationToken cancellationToken) =>
|
||||
Results.Ok(await coordinator.StopAsync(cancellationToken)));
|
||||
app.MapPost("/meetings/current/summary/run", async (
|
||||
MeetingRecordingCoordinator coordinator,
|
||||
IMeetingSummaryPipeline summaryPipeline,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (coordinator.CurrentArtifacts is null)
|
||||
{
|
||||
return Results.Conflict(new { error = "No meeting session has been started yet." });
|
||||
}
|
||||
|
||||
return Results.Ok(await summaryPipeline.RunAsync(coordinator.CurrentArtifacts, cancellationToken));
|
||||
});
|
||||
app.MapPost("/meetings/summary/retry", async (
|
||||
SummaryRetryRequest request,
|
||||
IMeetingSummaryArtifactResolver artifactResolver,
|
||||
IMeetingSummaryPipeline summaryPipeline,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.SummaryPath))
|
||||
{
|
||||
return Results.BadRequest(new { error = "A summary path is required." });
|
||||
}
|
||||
|
||||
var artifacts = await artifactResolver.ResolveBySummaryPathAsync(request.SummaryPath, cancellationToken);
|
||||
if (artifacts is null)
|
||||
{
|
||||
return Results.NotFound(new { error = $"No meeting note links to summary '{request.SummaryPath}'." });
|
||||
}
|
||||
|
||||
return Results.Ok(await summaryPipeline.RunAsync(artifacts, cancellationToken));
|
||||
});
|
||||
app.MapGet("/meetings/summary/retry", async (
|
||||
string summaryPath,
|
||||
IMeetingSummaryArtifactResolver artifactResolver,
|
||||
IMeetingSummaryPipeline summaryPipeline,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(summaryPath))
|
||||
{
|
||||
return Results.BadRequest(new { error = "A summary path is required." });
|
||||
}
|
||||
|
||||
var artifacts = await artifactResolver.ResolveBySummaryPathAsync(summaryPath, cancellationToken);
|
||||
if (artifacts is null)
|
||||
{
|
||||
return Results.NotFound(new { error = $"No meeting note links to summary '{summaryPath}'." });
|
||||
}
|
||||
|
||||
return Results.Ok(await summaryPipeline.RunAsync(artifacts, cancellationToken));
|
||||
});
|
||||
app.MapPost("/asr/transcribe-file", async (
|
||||
AsrDiagnosticRequest request,
|
||||
AsrDiagnosticService diagnostics,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Path))
|
||||
{
|
||||
return Results.BadRequest(new { error = "A WAV file path is required." });
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return Results.Ok(await diagnostics.TranscribeWavAsync(request.Path, cancellationToken));
|
||||
}
|
||||
catch (FileNotFoundException exception)
|
||||
{
|
||||
return Results.NotFound(new { error = exception.Message });
|
||||
}
|
||||
catch (InvalidDataException exception)
|
||||
{
|
||||
return Results.BadRequest(new { error = exception.Message });
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
return Results.Problem(
|
||||
detail: exception.Message,
|
||||
statusCode: StatusCodes.Status502BadGateway,
|
||||
title: "ASR backend failed while processing the WAV file.");
|
||||
}
|
||||
});
|
||||
app.MapPost("/asr/diarize-file", async (
|
||||
AsrDiagnosticRequest request,
|
||||
AsrDiagnosticService diagnostics,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Path))
|
||||
{
|
||||
return Results.BadRequest(new { error = "A WAV file path is required." });
|
||||
}
|
||||
|
||||
var fullPath = Path.GetFullPath(Environment.ExpandEnvironmentVariables(request.Path));
|
||||
if (!File.Exists(fullPath))
|
||||
{
|
||||
return Results.NotFound(new { error = $"WAV file was not found at '{fullPath}'." });
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return Results.Ok(await diagnostics.DiarizeWavAsync(
|
||||
fullPath,
|
||||
new SpeechRecognitionPipelineOptions(request.NumSpeakers),
|
||||
cancellationToken));
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
return Results.Problem(
|
||||
detail: exception.Message,
|
||||
statusCode: StatusCodes.Status502BadGateway,
|
||||
title: "ASR diarization backend failed while processing the WAV file.");
|
||||
}
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
app.Run();
|
||||
}
|
||||
catch (TimeoutException exception)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"Meeting Assistant failed to start because a required operation timed out: {exception.Message}");
|
||||
Environment.ExitCode = 1;
|
||||
}
|
||||
|
||||
public partial class Program;
|
||||
|
||||
public sealed record AsrDiagnosticRequest(string Path, int? NumSpeakers = null);
|
||||
|
||||
public sealed record SummaryRetryRequest(string SummaryPath);
|
||||
|
||||
Reference in New Issue
Block a user