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(builder.Configuration.GetSection("MeetingAssistant")); #if WINDOWS builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(services => new CompositeMeetingAudioSource( services.GetRequiredService(), services.GetRequiredService(), services.GetRequiredService>())); builder.Services.AddSingleton(); #else builder.Services.AddSingleton(); builder.Services.AddSingleton(); #endif builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); #if WINDOWS builder.Services.AddHostedService(); #endif var app = builder.Build(); app.MapGet("/health", () => Results.Ok(new { service = "meeting-assistant", status = "ok" })); 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);