Public Access
Implement meeting assistant v1
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using MeetingAssistant.Recording;
|
||||
using MeetingAssistant.Transcription;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.AspNetCore.TestHost;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
namespace MeetingAssistant.Tests;
|
||||
|
||||
public sealed class AsrDiagnosticEndpointTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task EndpointTranscribesLocalWavThroughConfiguredProvider()
|
||||
{
|
||||
await using var factory = CreateFactory<EndpointFakeTranscriptionProvider>();
|
||||
using var client = factory.CreateClient();
|
||||
var wavPath = Path.Combine(AppContext.BaseDirectory, "Fixtures", "sample-16khz-mono.wav");
|
||||
|
||||
using var response = await client.PostAsJsonAsync("/asr/transcribe-file", new { path = wavPath });
|
||||
var body = await response.Content.ReadFromJsonAsync<AsrDiagnosticResponse>();
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal(wavPath, body?.Path);
|
||||
var segment = Assert.Single(body?.Segments ?? []);
|
||||
Assert.Equal("Unknown", segment.Speaker);
|
||||
Assert.StartsWith("endpoint-bytes:", segment.Text, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EndpointReportsProviderFailuresAsBadGateway()
|
||||
{
|
||||
await using var factory = CreateFactory<FailingTranscriptionProvider>();
|
||||
using var client = factory.CreateClient();
|
||||
var wavPath = Path.Combine(AppContext.BaseDirectory, "Fixtures", "sample-16khz-mono.wav");
|
||||
|
||||
using var response = await client.PostAsJsonAsync("/asr/transcribe-file", new { path = wavPath });
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadGateway, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EndpointRunsFinalDiarizationForLocalWav()
|
||||
{
|
||||
SpeechRecognitionPipelineOptions? capturedOptions = null;
|
||||
await using var factory = CreateFactory<EndpointFakeTranscriptionProvider>(
|
||||
(audioPath, liveSegments, options, cancellationToken) =>
|
||||
{
|
||||
capturedOptions = options;
|
||||
return Task.FromResult<IReadOnlyList<TranscriptionSegment>>(
|
||||
liveSegments
|
||||
.Select(segment => segment with { Speaker = "Speaker 0", Text = $"diarized:{segment.Text}" })
|
||||
.ToList());
|
||||
});
|
||||
using var client = factory.CreateClient();
|
||||
var wavPath = Path.Combine(AppContext.BaseDirectory, "Fixtures", "sample-16khz-mono.wav");
|
||||
|
||||
using var response = await client.PostAsJsonAsync("/asr/diarize-file", new { path = wavPath, numSpeakers = 5 });
|
||||
var body = await response.Content.ReadFromJsonAsync<AsrDiagnosticResponse>();
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal(5, capturedOptions?.NumSpeakers);
|
||||
Assert.Equal(wavPath, body?.Path);
|
||||
var segment = Assert.Single(body?.Segments ?? []);
|
||||
Assert.Equal("Speaker 0", segment.Speaker);
|
||||
Assert.StartsWith("diarized:endpoint-bytes:", segment.Text, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static WebApplicationFactory<Program> CreateFactory<TProvider>()
|
||||
where TProvider : class, IStreamingTranscriptionProvider
|
||||
{
|
||||
return CreateFactory<TProvider>((_, _, _, _) => Task.FromResult<IReadOnlyList<TranscriptionSegment>>([]));
|
||||
}
|
||||
|
||||
private static WebApplicationFactory<Program> CreateFactory<TProvider>(
|
||||
Func<string, IReadOnlyList<TranscriptionSegment>, SpeechRecognitionPipelineOptions, CancellationToken, Task<IReadOnlyList<TranscriptionSegment>>> finalize)
|
||||
where TProvider : class, IStreamingTranscriptionProvider
|
||||
{
|
||||
return new WebApplicationFactory<Program>()
|
||||
.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureAppConfiguration((_, configuration) =>
|
||||
{
|
||||
configuration.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["MeetingAssistant:FunAsr:Backend:Enabled"] = "false"
|
||||
});
|
||||
});
|
||||
builder.ConfigureTestServices(services =>
|
||||
{
|
||||
services.RemoveAll<ISpeechRecognitionPipelineFactory>();
|
||||
services.AddSingleton<ISpeechRecognitionPipelineFactory>(
|
||||
new TestSpeechRecognitionPipelineFactory<TProvider>(finalize));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private sealed class EndpointFakeTranscriptionProvider : IStreamingTranscriptionProvider
|
||||
{
|
||||
public async IAsyncEnumerable<TranscriptionSegment> TranscribeAsync(
|
||||
IAsyncEnumerable<AudioChunk> audio,
|
||||
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
var bytes = 0;
|
||||
await foreach (var chunk in audio.WithCancellation(cancellationToken))
|
||||
{
|
||||
bytes += chunk.Pcm.Length;
|
||||
}
|
||||
|
||||
yield return new TranscriptionSegment(TimeSpan.Zero, TimeSpan.Zero, "Unknown", $"endpoint-bytes:{bytes}");
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FailingTranscriptionProvider : IStreamingTranscriptionProvider
|
||||
{
|
||||
public async IAsyncEnumerable<TranscriptionSegment> TranscribeAsync(
|
||||
IAsyncEnumerable<AudioChunk> audio,
|
||||
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
await Task.Yield();
|
||||
throw new InvalidOperationException("backend unavailable");
|
||||
#pragma warning disable CS0162
|
||||
yield break;
|
||||
#pragma warning restore CS0162
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestSpeechRecognitionPipelineFactory<TProvider> : ISpeechRecognitionPipelineFactory
|
||||
where TProvider : class, IStreamingTranscriptionProvider
|
||||
{
|
||||
private readonly Func<string, IReadOnlyList<TranscriptionSegment>, SpeechRecognitionPipelineOptions, CancellationToken, Task<IReadOnlyList<TranscriptionSegment>>> finalize;
|
||||
|
||||
public TestSpeechRecognitionPipelineFactory(
|
||||
Func<string, IReadOnlyList<TranscriptionSegment>, SpeechRecognitionPipelineOptions, CancellationToken, Task<IReadOnlyList<TranscriptionSegment>>> finalize)
|
||||
{
|
||||
this.finalize = finalize;
|
||||
}
|
||||
|
||||
public ISpeechRecognitionPipeline Create()
|
||||
{
|
||||
return new TestSpeechRecognitionPipeline(
|
||||
Activator.CreateInstance<TProvider>(),
|
||||
finalize);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestSpeechRecognitionPipeline : StreamingSpeechRecognitionPipeline
|
||||
{
|
||||
private readonly Func<string, IReadOnlyList<TranscriptionSegment>, SpeechRecognitionPipelineOptions, CancellationToken, Task<IReadOnlyList<TranscriptionSegment>>> finalize;
|
||||
|
||||
public TestSpeechRecognitionPipeline(
|
||||
IStreamingTranscriptionProvider provider,
|
||||
Func<string, IReadOnlyList<TranscriptionSegment>, SpeechRecognitionPipelineOptions, CancellationToken, Task<IReadOnlyList<TranscriptionSegment>>> finalize)
|
||||
: base(provider)
|
||||
{
|
||||
this.finalize = finalize;
|
||||
}
|
||||
|
||||
protected override Task<IReadOnlyList<TranscriptionSegment>> BuildFinishedTranscriptAsync(
|
||||
string audioPath,
|
||||
IReadOnlyList<TranscriptionSegment> liveSegments,
|
||||
SpeechRecognitionPipelineOptions options,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return finalize(audioPath, liveSegments, options, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record AsrDiagnosticResponse(string Path, IReadOnlyList<AsrDiagnosticSegment> Segments);
|
||||
|
||||
private sealed record AsrDiagnosticSegment(string Speaker, string Text);
|
||||
}
|
||||
Reference in New Issue
Block a user