using MeetingAssistant.LaunchProfiles; namespace MeetingAssistant.Transcription; public sealed class SpeechRecognitionPipelineHostedService : IHostedService { private readonly ISpeechRecognitionPipelineFactory pipelineFactory; private readonly ILaunchProfileOptionsProvider? launchProfiles; private readonly ILogger logger; private CancellationTokenSource? startupCancellation; private Task? startupTask; private List startupPipelines = []; public SpeechRecognitionPipelineHostedService( ISpeechRecognitionPipelineFactory pipelineFactory, ILogger logger) : this(pipelineFactory, null, logger) { } public SpeechRecognitionPipelineHostedService( ISpeechRecognitionPipelineFactory pipelineFactory, ILaunchProfileOptionsProvider? launchProfiles, ILogger logger) { this.pipelineFactory = pipelineFactory; this.launchProfiles = launchProfiles; this.logger = logger; } public Task StartAsync(CancellationToken cancellationToken) { var cancellation = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); var pipelines = CreateStartupPipelines(); startupCancellation = cancellation; startupPipelines = pipelines; startupTask = Task.Run( () => WarmUpPipelinesAsync(pipelines, cancellation.Token), CancellationToken.None); return Task.CompletedTask; } public async Task StopAsync(CancellationToken cancellationToken) { var cancellation = startupCancellation; var task = startupTask; var pipelines = startupPipelines; if (cancellation is null || task is null) { return; } startupCancellation = null; startupTask = null; startupPipelines = []; await cancellation.CancelAsync(); try { await task.WaitAsync(cancellationToken); } catch (OperationCanceledException) { } finally { foreach (var pipeline in pipelines) { await pipeline.DisposeAsync(); } cancellation.Dispose(); } } private List CreateStartupPipelines() { if (launchProfiles is null) { return [pipelineFactory.Create()]; } return launchProfiles.GetProfiles() .Where(ShouldWarmUpProfile) .Select(profile => pipelineFactory.Create(profile.Name)) .ToList(); } private static bool ShouldWarmUpProfile(LaunchProfile profile) { return profile.Name.Equals( ConfigurationLaunchProfileOptionsProvider.DefaultProfileName, StringComparison.OrdinalIgnoreCase) || profile.Options.Recording.TranscriptionProvider.Equals("funasr", StringComparison.OrdinalIgnoreCase); } private async Task WarmUpPipelinesAsync( IReadOnlyList pipelines, CancellationToken cancellationToken) { await Task.WhenAll(pipelines.Select(pipeline => WarmUpPipelineAsync(pipeline, cancellationToken))); } private async Task WarmUpPipelineAsync( ISpeechRecognitionPipeline pipeline, CancellationToken cancellationToken) { try { await pipeline.InitializeAsync(cancellationToken); await pipeline.WaitUntilReadyAsync(cancellationToken); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { logger.LogInformation("Speech recognition pipeline warm-up was cancelled during application shutdown"); } catch (Exception exception) { logger.LogError( exception, "Speech recognition pipeline warm-up failed; recording can still start and recognition will retry when audio is processed"); } } }