using MeetingAssistant.Recording; using NAudio.Wave; namespace MeetingAssistant.Transcription; public sealed class AsrDiagnosticService { private readonly ISpeechRecognitionPipelineFactory pipelineFactory; public AsrDiagnosticService(ISpeechRecognitionPipelineFactory pipelineFactory) { this.pipelineFactory = pipelineFactory; } public Task TranscribeWavAsync(string path, CancellationToken cancellationToken) { return TranscribeWavAsync(path, launchProfileName: null, cancellationToken); } public async Task TranscribeWavAsync( string path, string? launchProfileName, CancellationToken cancellationToken) { var fullPath = ResolveExistingWavPath(path); await using var pipeline = pipelineFactory.Create(launchProfileName); await pipeline.InitializeAsync(cancellationToken); var liveTranscriptTask = CollectLiveTranscriptAsync(pipeline, cancellationToken); await WriteWavToPipelineAsync(fullPath, pipeline, paceAudio: true, cancellationToken); await pipeline.CompleteAsync(cancellationToken); return new AsrDiagnosticResult(fullPath, await liveTranscriptTask); } public async Task DiarizeWavAsync( string path, SpeechRecognitionPipelineOptions options, CancellationToken cancellationToken) { var fullPath = ResolveExistingWavPath(path); return await DiarizeWavAsync(fullPath, options, launchProfileName: null, cancellationToken); } public async Task DiarizeWavAsync( string path, SpeechRecognitionPipelineOptions options, string? launchProfileName, CancellationToken cancellationToken) { var fullPath = ResolveExistingWavPath(path); await using var pipeline = pipelineFactory.Create(launchProfileName); await pipeline.InitializeAsync(cancellationToken); var liveTranscriptTask = CollectLiveTranscriptAsync(pipeline, cancellationToken); await WriteWavToPipelineAsync(fullPath, pipeline, paceAudio: true, cancellationToken); await pipeline.CompleteAsync(cancellationToken); await liveTranscriptTask; var finishedSegments = await pipeline.ReadFinishedTranscriptAsync(fullPath, options, cancellationToken); return new AsrDiagnosticResult(fullPath, finishedSegments); } private static async Task> CollectLiveTranscriptAsync( ISpeechRecognitionPipeline pipeline, CancellationToken cancellationToken) { var segments = new List(); await foreach (var segment in pipeline.ReadLiveTranscriptAsync(cancellationToken)) { segments.Add(segment); } return segments; } private static async Task WriteWavToPipelineAsync( string fullPath, ISpeechRecognitionPipeline pipeline, bool paceAudio, CancellationToken cancellationToken) { await foreach (var chunk in ReadPcmWavChunks(fullPath, cancellationToken)) { await pipeline.WriteAsync(chunk, cancellationToken); if (paceAudio && chunk.Duration > TimeSpan.Zero) { await Task.Delay(chunk.Duration, cancellationToken); } } } private static string ResolveExistingWavPath(string path) { if (string.IsNullOrWhiteSpace(path)) { throw new ArgumentException("A WAV file path is required.", nameof(path)); } var fullPath = Path.GetFullPath(Environment.ExpandEnvironmentVariables(path)); if (!File.Exists(fullPath)) { throw new FileNotFoundException($"WAV file was not found at '{fullPath}'.", fullPath); } return fullPath; } private static async IAsyncEnumerable ReadPcmWavChunks( string path, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default) { var reader = new WaveFileReader(path); try { var format = reader.WaveFormat; if (format.Encoding != WaveFormatEncoding.Pcm || format.BitsPerSample != 16) { throw new InvalidDataException( $"Only 16-bit PCM WAV files are supported. Found {format.Encoding} with {format.BitsPerSample} bits per sample."); } var buffer = new byte[format.AverageBytesPerSecond]; int read; while ((read = await reader.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken)) > 0) { yield return new AudioChunk(buffer[..read], format.SampleRate, format.Channels); } } finally { try { reader.Dispose(); } catch (NotSupportedException) { // NAudio can throw while disposing reader streams from async iterators; diagnostics should not fail after reading all chunks. } } } } public sealed record AsrDiagnosticResult(string Path, IReadOnlyList Segments);