Public Access
528 lines
22 KiB
C#
528 lines
22 KiB
C#
using MeetingAssistant;
|
|
using MeetingAssistant.Hotkeys;
|
|
using MeetingAssistant.LaunchProfiles;
|
|
using MeetingAssistant.Logging;
|
|
using MeetingAssistant.MeetingNotes;
|
|
using MeetingAssistant.Recording;
|
|
using MeetingAssistant.Screenshots;
|
|
using MeetingAssistant.Speakers;
|
|
using MeetingAssistant.Summary;
|
|
using MeetingAssistant.Taskbar;
|
|
using MeetingAssistant.Transcription;
|
|
using MeetingAssistant.Workflow;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Options;
|
|
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
builder.Logging.AddProvider(new MeetingAssistantFileLoggerProvider());
|
|
builder.Services.Configure<MeetingAssistantOptions>(builder.Configuration.GetSection("MeetingAssistant"));
|
|
builder.Services.AddSingleton<ILaunchProfileOptionsProvider, ConfigurationLaunchProfileOptionsProvider>();
|
|
#if WINDOWS
|
|
builder.Services.AddSingleton<MicrophoneAudioSource>();
|
|
builder.Services.AddSingleton<SystemAudioSource>();
|
|
builder.Services.AddSingleton<IAcousticEchoCancellerFactory, AdaptiveFilterAcousticEchoCancellerFactory>();
|
|
builder.Services.AddSingleton<IMeetingAudioSource>(services => new CompositeMeetingAudioSource(
|
|
services.GetRequiredService<MicrophoneAudioSource>(),
|
|
services.GetRequiredService<SystemAudioSource>(),
|
|
services.GetRequiredService<IAcousticEchoCancellerFactory>(),
|
|
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<IMeetingRunArtifactCleaner, MeetingRunArtifactCleaner>();
|
|
builder.Services.AddSingleton<IRecordedAudioStore, TemporaryRecordedAudioStore>();
|
|
builder.Services.AddSingleton<IDictationWordStore, MarkdownDictationWordStore>();
|
|
builder.Services.AddSingleton<IMeetingInactivityClock, SystemMeetingInactivityClock>();
|
|
#if WINDOWS
|
|
builder.Services.AddSingleton<IMeetingInactivityPromptService, WindowsMeetingInactivityPromptService>();
|
|
#else
|
|
builder.Services.AddSingleton<IMeetingInactivityPromptService, NoopMeetingInactivityPromptService>();
|
|
#endif
|
|
builder.Services.AddSingleton<IRecordingDictationWordProvider, RecordingDictationWordProvider>();
|
|
#if WINDOWS
|
|
builder.Services.AddSingleton<IActiveWindowScreenshotCapture, ActiveWindowScreenshotCapture>();
|
|
#else
|
|
builder.Services.AddSingleton<IActiveWindowScreenshotCapture, UnavailableActiveWindowScreenshotCapture>();
|
|
#endif
|
|
builder.Services.AddSingleton<IScreenshotOcrClient, LiteLlmScreenshotOcrClient>();
|
|
builder.Services.AddSingleton<IMeetingScreenshotService, MeetingScreenshotService>();
|
|
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<ISpeakerIdentityMatchValidator, PyannoteSpeakerIdentityMatchValidator>();
|
|
builder.Services.AddSingleton<ISpeakerIdentityMatcher, AzureSpeechSpeakerIdentityMatcher>();
|
|
builder.Services.AddSingleton<ISpeakerIdentificationService, SpeakerIdentityService>();
|
|
builder.Services.AddSingleton<ISpeakerIdentityMergeService, SpeakerIdentityMergeService>();
|
|
builder.Services.AddSingleton<ISpeakerIdentityAttendeeCanonicalizer, SpeakerIdentityAttendeeCanonicalizer>();
|
|
builder.Services.AddSingleton<IMeetingSummaryArtifactResolver, MeetingSummaryArtifactResolver>();
|
|
builder.Services.AddSingleton<IMeetingSummaryFailureWriter, MeetingSummaryFailureWriter>();
|
|
builder.Services.AddSingleton<IMeetingSummaryInstructionBuilder, MeetingSummaryInstructionBuilder>();
|
|
builder.Services.AddSingleton<IMeetingSummaryPipeline, OpenAiMeetingSummaryAgentPipeline>();
|
|
builder.Services.AddSingleton<IMeetingSummaryRetryRunner, MeetingSummaryRetryRunner>();
|
|
builder.Services.AddSingleton<IMeetingWorkflowRulesProvider, FileMeetingWorkflowRulesProvider>();
|
|
builder.Services.AddSingleton<IMeetingWorkflowEngine, MeetingWorkflowEngine>();
|
|
builder.Services.AddSingleton<IWorkflowRulesEditorInstructionBuilder, WorkflowRulesEditorInstructionBuilder>();
|
|
builder.Services.AddSingleton<IWorkflowRulesEditorSamplePlaybackQueue, WorkflowRulesEditorSamplePlaybackQueue>();
|
|
builder.Services.AddSingleton<IWorkflowRulesEditorChatPipeline, WorkflowRulesEditorChatPipeline>();
|
|
builder.Services.AddTransient<WorkflowRulesEditorChatViewModel>();
|
|
#if WINDOWS
|
|
builder.Services.AddSingleton(services => WorkflowRulesEditorMarkdownLinkResolver.FromOptions(
|
|
services.GetRequiredService<IOptions<MeetingAssistantOptions>>().Value));
|
|
builder.Services.AddSingleton<IWorkflowRulesEditorWindowService, WpfWorkflowRulesEditorWindowService>();
|
|
#else
|
|
builder.Services.AddSingleton<IWorkflowRulesEditorWindowService, NoopWorkflowRulesEditorWindowService>();
|
|
#endif
|
|
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<ISpeechRecognitionPipelineBuilder, AzureSpeechRecognitionPipelineBuilder>();
|
|
builder.Services.AddSingleton<ISpeechRecognitionPipelineBuilder, FunAsrSpeechRecognitionPipelineBuilder>();
|
|
builder.Services.AddSingleton<ISpeechRecognitionPipelineBuilder, WhisperLocalSpeechRecognitionPipelineBuilder>();
|
|
builder.Services.AddSingleton<ISpeechRecognitionPipelineFactory, ConfiguredSpeechRecognitionPipelineFactory>();
|
|
builder.Services.AddSingleton<MeetingRecordingCoordinator>();
|
|
builder.Services.AddHostedService<TemporaryRecordingCleanupHostedService>();
|
|
builder.Services.AddHostedService<SpeakerIdentityDatabaseInitializer>();
|
|
builder.Services.AddHostedService<SpeechRecognitionPipelineHostedService>();
|
|
builder.Services.AddHostedService<PyannoteDiarizationWarmupHostedService>();
|
|
#if WINDOWS
|
|
builder.Services.AddHostedService<GlobalHotkeyService>();
|
|
builder.Services.AddHostedService<UnoTaskbarIconService>();
|
|
#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.MapGet("/profiles/{launchProfile}/recording/status", (
|
|
string launchProfile,
|
|
MeetingRecordingCoordinator coordinator) => Results.Ok(coordinator.CurrentStatus));
|
|
app.MapGet("/diagnostics/outlook/current-meeting", async (
|
|
IMeetingMetadataProvider metadataProvider,
|
|
IOptions<MeetingAssistantOptions> options,
|
|
DateTimeOffset? startedAt,
|
|
double? timeoutSeconds,
|
|
CancellationToken cancellationToken) =>
|
|
await DiagnoseOutlookMeetingAsync(
|
|
null,
|
|
metadataProvider,
|
|
options.Value,
|
|
null,
|
|
startedAt,
|
|
timeoutSeconds,
|
|
cancellationToken));
|
|
app.MapGet("/profiles/{launchProfile}/diagnostics/outlook/current-meeting", async (
|
|
string launchProfile,
|
|
IMeetingMetadataProvider metadataProvider,
|
|
IOptions<MeetingAssistantOptions> options,
|
|
ILaunchProfileOptionsProvider launchProfiles,
|
|
DateTimeOffset? startedAt,
|
|
double? timeoutSeconds,
|
|
CancellationToken cancellationToken) =>
|
|
await DiagnoseOutlookMeetingAsync(
|
|
launchProfile,
|
|
metadataProvider,
|
|
options.Value,
|
|
launchProfiles,
|
|
startedAt,
|
|
timeoutSeconds,
|
|
cancellationToken));
|
|
|
|
static async Task<IResult> DiagnoseOutlookMeetingAsync(
|
|
string? launchProfile,
|
|
IMeetingMetadataProvider metadataProvider,
|
|
MeetingAssistantOptions defaultOptions,
|
|
ILaunchProfileOptionsProvider? launchProfiles,
|
|
DateTimeOffset? startedAt,
|
|
double? timeoutSeconds,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var profileOptions = ResolveProfileOptions(launchProfile, defaultOptions, launchProfiles);
|
|
var lookupStartedAt = startedAt ?? DateTimeOffset.Now;
|
|
var timeout = timeoutSeconds is > 0
|
|
? TimeSpan.FromSeconds(timeoutSeconds.Value)
|
|
: profileOptions.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));
|
|
}
|
|
|
|
static MeetingAssistantOptions ResolveProfileOptions(
|
|
string? launchProfile,
|
|
MeetingAssistantOptions defaultOptions,
|
|
ILaunchProfileOptionsProvider? launchProfiles)
|
|
{
|
|
if (launchProfiles is not null)
|
|
{
|
|
return launchProfiles.GetRequiredProfile(launchProfile).Options;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(launchProfile) ||
|
|
launchProfile.Equals(ConfigurationLaunchProfileOptionsProvider.DefaultProfileName, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return defaultOptions;
|
|
}
|
|
|
|
throw new InvalidOperationException(
|
|
$"Launch profile '{launchProfile}' was requested, but no launch profile provider is registered.");
|
|
}
|
|
|
|
app.MapPost("/diagnostics/speaker-identities/merge", async (
|
|
SpeakerIdentityMergeDiagnosticRequest request,
|
|
ISpeakerIdentityMergeService mergeService,
|
|
IOptions<MeetingAssistantOptions> options,
|
|
CancellationToken cancellationToken) =>
|
|
await MergeSpeakerIdentitiesAsync(null, request, mergeService, options.Value, null, cancellationToken));
|
|
app.MapPost("/profiles/{launchProfile}/diagnostics/speaker-identities/merge", async (
|
|
string launchProfile,
|
|
SpeakerIdentityMergeDiagnosticRequest request,
|
|
ISpeakerIdentityMergeService mergeService,
|
|
IOptions<MeetingAssistantOptions> options,
|
|
ILaunchProfileOptionsProvider launchProfiles,
|
|
CancellationToken cancellationToken) =>
|
|
await MergeSpeakerIdentitiesAsync(launchProfile, request, mergeService, options.Value, launchProfiles, cancellationToken));
|
|
|
|
static async Task<IResult> MergeSpeakerIdentitiesAsync(
|
|
string? launchProfile,
|
|
SpeakerIdentityMergeDiagnosticRequest request,
|
|
ISpeakerIdentityMergeService mergeService,
|
|
MeetingAssistantOptions defaultOptions,
|
|
ILaunchProfileOptionsProvider? launchProfiles,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var profileOptions = ResolveProfileOptions(launchProfile, defaultOptions, launchProfiles);
|
|
var recentAge = request.RecentDays is > 0
|
|
? TimeSpan.FromDays(request.RecentDays.Value)
|
|
: profileOptions.SpeakerIdentification.MergeRecentIdentityAge;
|
|
return Results.Ok(await mergeService.MergeRecentIdentitiesAsync(recentAge, cancellationToken));
|
|
}
|
|
app.MapPost("/diagnostics/workflow/reload", (IConfiguration configuration) =>
|
|
Results.Ok(ReloadWorkflowConfiguration(configuration)));
|
|
app.MapPost("/diagnostics/workflow/rules-editor/show", (
|
|
IWorkflowRulesEditorWindowService rulesEditorWindow) =>
|
|
{
|
|
rulesEditorWindow.Show();
|
|
return Results.Accepted();
|
|
});
|
|
app.MapPost("/diagnostics/settings-and-logs/show", (
|
|
IWorkflowRulesEditorWindowService rulesEditorWindow) =>
|
|
{
|
|
rulesEditorWindow.Show();
|
|
return Results.Accepted();
|
|
});
|
|
|
|
static WorkflowConfigurationReloadResponse ReloadWorkflowConfiguration(IConfiguration configuration)
|
|
{
|
|
if (configuration is not IConfigurationRoot root)
|
|
{
|
|
throw new InvalidOperationException("Application configuration does not support reload.");
|
|
}
|
|
|
|
root.Reload();
|
|
var options = new MeetingAssistantOptions();
|
|
configuration.GetSection("MeetingAssistant").Bind(options);
|
|
return new WorkflowConfigurationReloadResponse(options.Automation.RulesPath);
|
|
}
|
|
|
|
app.MapPost("/recording/toggle", async (MeetingRecordingCoordinator coordinator, CancellationToken cancellationToken) =>
|
|
Results.Ok(await coordinator.ToggleAsync(cancellationToken)));
|
|
app.MapPost("/profiles/{launchProfile}/recording/toggle", async (
|
|
string launchProfile,
|
|
MeetingRecordingCoordinator coordinator,
|
|
CancellationToken cancellationToken) =>
|
|
Results.Ok(await coordinator.ToggleAsync(launchProfile, cancellationToken)));
|
|
app.MapPost("/recording/start", async (MeetingRecordingCoordinator coordinator, CancellationToken cancellationToken) =>
|
|
Results.Ok(await coordinator.StartAsync(cancellationToken)));
|
|
app.MapPost("/profiles/{launchProfile}/recording/start", async (
|
|
string launchProfile,
|
|
MeetingRecordingCoordinator coordinator,
|
|
CancellationToken cancellationToken) =>
|
|
Results.Ok(await coordinator.StartAsync(launchProfile, cancellationToken)));
|
|
app.MapPost("/recording/stop", async (MeetingRecordingCoordinator coordinator, CancellationToken cancellationToken) =>
|
|
Results.Ok(await coordinator.StopAsync(cancellationToken)));
|
|
app.MapPost("/profiles/{launchProfile}/recording/stop", async (
|
|
string launchProfile,
|
|
MeetingRecordingCoordinator coordinator,
|
|
CancellationToken cancellationToken) =>
|
|
Results.Ok(await coordinator.StopAsync(cancellationToken)));
|
|
app.MapPost("/recording/abort", async (MeetingRecordingCoordinator coordinator, CancellationToken cancellationToken) =>
|
|
Results.Ok(await coordinator.AbortAsync(cancellationToken)));
|
|
app.MapPost("/profiles/{launchProfile}/recording/abort", async (
|
|
string launchProfile,
|
|
MeetingRecordingCoordinator coordinator,
|
|
CancellationToken cancellationToken) =>
|
|
Results.Ok(await coordinator.AbortAsync(cancellationToken)));
|
|
app.MapPost("/meetings/current/summary/run", async (
|
|
MeetingRecordingCoordinator coordinator,
|
|
IMeetingSummaryPipeline summaryPipeline,
|
|
IOptions<MeetingAssistantOptions> options,
|
|
CancellationToken cancellationToken) =>
|
|
await RunCurrentSummaryAsync(
|
|
null,
|
|
coordinator,
|
|
summaryPipeline,
|
|
options.Value,
|
|
null,
|
|
cancellationToken));
|
|
app.MapPost("/profiles/{launchProfile}/meetings/current/summary/run", async (
|
|
string launchProfile,
|
|
MeetingRecordingCoordinator coordinator,
|
|
IMeetingSummaryPipeline summaryPipeline,
|
|
IOptions<MeetingAssistantOptions> options,
|
|
ILaunchProfileOptionsProvider launchProfiles,
|
|
CancellationToken cancellationToken) =>
|
|
await RunCurrentSummaryAsync(
|
|
launchProfile,
|
|
coordinator,
|
|
summaryPipeline,
|
|
options.Value,
|
|
launchProfiles,
|
|
cancellationToken));
|
|
|
|
static async Task<IResult> RunCurrentSummaryAsync(
|
|
string? launchProfile,
|
|
MeetingRecordingCoordinator coordinator,
|
|
IMeetingSummaryPipeline summaryPipeline,
|
|
MeetingAssistantOptions defaultOptions,
|
|
ILaunchProfileOptionsProvider? launchProfiles,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
if (coordinator.CurrentArtifacts is null)
|
|
{
|
|
return Results.Conflict(new { error = "No meeting session has been started yet." });
|
|
}
|
|
|
|
var profileOptions = ResolveProfileOptions(launchProfile, defaultOptions, launchProfiles);
|
|
return Results.Ok(await summaryPipeline.RunAsync(coordinator.CurrentArtifacts, profileOptions, cancellationToken));
|
|
}
|
|
|
|
app.MapPost("/meetings/summary/retry", async (
|
|
SummaryRetryRequest request,
|
|
IMeetingSummaryRetryRunner summaryRetryRunner,
|
|
IOptions<MeetingAssistantOptions> options,
|
|
CancellationToken cancellationToken) =>
|
|
await RetrySummaryAsync(
|
|
null,
|
|
request.SummaryPath,
|
|
summaryRetryRunner,
|
|
options.Value,
|
|
null,
|
|
cancellationToken));
|
|
app.MapPost("/profiles/{launchProfile}/meetings/summary/retry", async (
|
|
string launchProfile,
|
|
SummaryRetryRequest request,
|
|
IMeetingSummaryRetryRunner summaryRetryRunner,
|
|
IOptions<MeetingAssistantOptions> options,
|
|
ILaunchProfileOptionsProvider launchProfiles,
|
|
CancellationToken cancellationToken) =>
|
|
await RetrySummaryAsync(
|
|
launchProfile,
|
|
request.SummaryPath,
|
|
summaryRetryRunner,
|
|
options.Value,
|
|
launchProfiles,
|
|
cancellationToken));
|
|
app.MapGet("/meetings/summary/retry", async (
|
|
string summaryPath,
|
|
IMeetingSummaryRetryRunner summaryRetryRunner,
|
|
IOptions<MeetingAssistantOptions> options,
|
|
CancellationToken cancellationToken) =>
|
|
await RetrySummaryAsync(
|
|
null,
|
|
summaryPath,
|
|
summaryRetryRunner,
|
|
options.Value,
|
|
null,
|
|
cancellationToken));
|
|
app.MapGet("/profiles/{launchProfile}/meetings/summary/retry", async (
|
|
string launchProfile,
|
|
string summaryPath,
|
|
IMeetingSummaryRetryRunner summaryRetryRunner,
|
|
IOptions<MeetingAssistantOptions> options,
|
|
ILaunchProfileOptionsProvider launchProfiles,
|
|
CancellationToken cancellationToken) =>
|
|
await RetrySummaryAsync(
|
|
launchProfile,
|
|
summaryPath,
|
|
summaryRetryRunner,
|
|
options.Value,
|
|
launchProfiles,
|
|
cancellationToken));
|
|
|
|
static async Task<IResult> RetrySummaryAsync(
|
|
string? launchProfile,
|
|
string? summaryPath,
|
|
IMeetingSummaryRetryRunner summaryRetryRunner,
|
|
MeetingAssistantOptions defaultOptions,
|
|
ILaunchProfileOptionsProvider? launchProfiles,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(summaryPath))
|
|
{
|
|
return Results.BadRequest(new { error = "A summary path is required." });
|
|
}
|
|
|
|
var profileOptions = ResolveProfileOptions(launchProfile, defaultOptions, launchProfiles);
|
|
var trigger = await summaryRetryRunner.TriggerAsync(summaryPath, profileOptions, cancellationToken);
|
|
if (trigger is null)
|
|
{
|
|
return Results.NotFound(new { error = $"No meeting note links to summary '{summaryPath}'." });
|
|
}
|
|
|
|
return Results.Accepted(value: new
|
|
{
|
|
summaryPath = trigger.SummaryPath,
|
|
assistantContextPath = trigger.AssistantContextPath,
|
|
state = "summarizing"
|
|
});
|
|
}
|
|
|
|
app.MapPost("/asr/transcribe-file", async (
|
|
AsrDiagnosticRequest request,
|
|
AsrDiagnosticService diagnostics,
|
|
CancellationToken cancellationToken) =>
|
|
await TranscribeFileAsync(null, request, diagnostics, cancellationToken));
|
|
app.MapPost("/profiles/{launchProfile}/asr/transcribe-file", async (
|
|
string launchProfile,
|
|
AsrDiagnosticRequest request,
|
|
AsrDiagnosticService diagnostics,
|
|
CancellationToken cancellationToken) =>
|
|
await TranscribeFileAsync(launchProfile, request, diagnostics, cancellationToken));
|
|
|
|
static async Task<IResult> TranscribeFileAsync(
|
|
string? launchProfile,
|
|
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, launchProfile, 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) =>
|
|
await DiarizeFileAsync(null, request, diagnostics, cancellationToken));
|
|
app.MapPost("/profiles/{launchProfile}/asr/diarize-file", async (
|
|
string launchProfile,
|
|
AsrDiagnosticRequest request,
|
|
AsrDiagnosticService diagnostics,
|
|
CancellationToken cancellationToken) =>
|
|
await DiarizeFileAsync(launchProfile, request, diagnostics, cancellationToken));
|
|
|
|
static async Task<IResult> DiarizeFileAsync(
|
|
string? launchProfile,
|
|
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),
|
|
launchProfile,
|
|
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);
|
|
|
|
public sealed record SpeakerIdentityMergeDiagnosticRequest(double? RecentDays = null);
|
|
|
|
public sealed record WorkflowConfigurationReloadResponse(string? RulesPath);
|