Files
meeting-assistant/MeetingAssistant/Transcription/AsrDiagnosticService.cs
T
2026-05-27 12:55:17 +02:00

142 lines
5.1 KiB
C#

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<AsrDiagnosticResult> TranscribeWavAsync(string path, CancellationToken cancellationToken)
{
return TranscribeWavAsync(path, launchProfileName: null, cancellationToken);
}
public async Task<AsrDiagnosticResult> 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<AsrDiagnosticResult> DiarizeWavAsync(
string path,
SpeechRecognitionPipelineOptions options,
CancellationToken cancellationToken)
{
var fullPath = ResolveExistingWavPath(path);
return await DiarizeWavAsync(fullPath, options, launchProfileName: null, cancellationToken);
}
public async Task<AsrDiagnosticResult> 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<IReadOnlyList<TranscriptionSegment>> CollectLiveTranscriptAsync(
ISpeechRecognitionPipeline pipeline,
CancellationToken cancellationToken)
{
var segments = new List<TranscriptionSegment>();
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<AudioChunk> 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<TranscriptionSegment> Segments);