using System.Threading.Channels; using MeetingAssistant.Recording; namespace MeetingAssistant.Transcription; public abstract class StreamingSpeechRecognitionPipeline : ISpeechRecognitionPipeline { private readonly IStreamingTranscriptionProvider transcriptionProvider; private readonly Channel audio = Channel.CreateUnbounded(); private readonly Channel liveTranscript = Channel.CreateUnbounded(); private readonly List liveSegments = []; private Task? transcriptionTask; private bool initialized; protected StreamingSpeechRecognitionPipeline(IStreamingTranscriptionProvider transcriptionProvider) { this.transcriptionProvider = transcriptionProvider; } public virtual Task InitializeAsync(CancellationToken cancellationToken) { if (initialized) { return Task.CompletedTask; } initialized = true; transcriptionTask = Task.Run(() => TranscribeAsync(cancellationToken), CancellationToken.None); return Task.CompletedTask; } public virtual Task WaitUntilReadyAsync(CancellationToken cancellationToken) { return Task.CompletedTask; } public ValueTask WriteAsync(AudioChunk chunk, CancellationToken cancellationToken) { EnsureInitialized(); return audio.Writer.WriteAsync(chunk, cancellationToken); } public async Task CompleteAsync(CancellationToken cancellationToken) { EnsureInitialized(); audio.Writer.TryComplete(); if (transcriptionTask is not null) { await transcriptionTask.WaitAsync(cancellationToken); } } public async IAsyncEnumerable ReadLiveTranscriptAsync( [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken) { EnsureInitialized(); await foreach (var segment in liveTranscript.Reader.ReadAllAsync(cancellationToken)) { yield return segment; } } public async Task> ReadFinishedTranscriptAsync( string audioPath, SpeechRecognitionPipelineOptions options, CancellationToken cancellationToken) { EnsureInitialized(); await CompleteAsync(cancellationToken); var finalSegments = await BuildFinishedTranscriptAsync(audioPath, liveSegments, options, cancellationToken); return finalSegments.Count == 0 ? liveSegments : finalSegments; } public virtual ValueTask DisposeAsync() { audio.Writer.TryComplete(); liveTranscript.Writer.TryComplete(); return ValueTask.CompletedTask; } protected abstract Task> BuildFinishedTranscriptAsync( string audioPath, IReadOnlyList liveSegments, SpeechRecognitionPipelineOptions options, CancellationToken cancellationToken); private async Task TranscribeAsync(CancellationToken cancellationToken) { try { await foreach (var segment in transcriptionProvider.TranscribeAsync( audio.Reader.ReadAllAsync(cancellationToken), cancellationToken)) { liveSegments.Add(segment); await liveTranscript.Writer.WriteAsync(segment, cancellationToken); } liveTranscript.Writer.TryComplete(); } catch (Exception exception) { liveTranscript.Writer.TryComplete(exception); } } private void EnsureInitialized() { if (!initialized) { throw new InvalidOperationException("Speech recognition pipeline must be initialized before use."); } } }