Files
meeting-assistant/MeetingAssistant.Tests/SpeechRecognitionPipelineHostedServiceTests.cs
codex c46c587e65
PR and Push Build/Test / build-and-test (push) Successful in 6m48s
Warm up pyannote and FunASR runtimes
2026-05-28 13:05:27 +02:00

238 lines
8.5 KiB
C#

using MeetingAssistant;
using MeetingAssistant.LaunchProfiles;
using MeetingAssistant.Transcription;
using Microsoft.Extensions.Logging.Abstractions;
namespace MeetingAssistant.Tests;
public sealed class SpeechRecognitionPipelineHostedServiceTests
{
[Fact]
public async Task HostedServiceInitializesAndWarmsConfiguredPipelineWithoutBlockingStartup()
{
var pipeline = new CapturingSpeechRecognitionPipeline { BlockReadinessUntilCancelled = true };
var service = new SpeechRecognitionPipelineHostedService(
new CapturingSpeechRecognitionPipelineFactory(pipeline),
NullLogger<SpeechRecognitionPipelineHostedService>.Instance);
var startTask = service.StartAsync(CancellationToken.None);
await startTask.WaitAsync(TimeSpan.FromSeconds(1));
await pipeline.WaitForInitializeAsync();
await pipeline.WaitForReadinessAsync();
await service.StopAsync(CancellationToken.None);
Assert.Equal(1, pipeline.InitializeCount);
Assert.Equal(1, pipeline.ReadinessCount);
Assert.True(pipeline.ReadinessCancellationWasObserved);
Assert.True(pipeline.Disposed);
}
[Fact]
public async Task HostedServiceWarmsConfiguredLaunchProfilePipelinesWithoutBlockingStartup()
{
var defaultPipeline = new CapturingSpeechRecognitionPipeline();
var funAsrPipeline = new CapturingSpeechRecognitionPipeline { BlockReadinessUntilCancelled = true };
var englishPipeline = new CapturingSpeechRecognitionPipeline();
var service = new SpeechRecognitionPipelineHostedService(
new CapturingSpeechRecognitionPipelineFactory(new Dictionary<string, ISpeechRecognitionPipeline>
{
[ConfigurationLaunchProfileOptionsProvider.DefaultProfileName] = defaultPipeline,
["funasr"] = funAsrPipeline,
["english"] = englishPipeline
}),
new FakeLaunchProfileOptionsProvider(
[
new LaunchProfile(
ConfigurationLaunchProfileOptionsProvider.DefaultProfileName,
new MeetingAssistantOptions
{
Recording = { TranscriptionProvider = "azure-speech" }
}),
new LaunchProfile(
"funasr",
new MeetingAssistantOptions
{
Recording = { TranscriptionProvider = "funasr" }
}),
new LaunchProfile(
"english",
new MeetingAssistantOptions
{
Recording = { TranscriptionProvider = "azure-speech" }
})
]),
NullLogger<SpeechRecognitionPipelineHostedService>.Instance);
var startTask = service.StartAsync(CancellationToken.None);
await startTask.WaitAsync(TimeSpan.FromSeconds(1));
await defaultPipeline.WaitForInitializeAsync();
await defaultPipeline.WaitForReadinessAsync();
await funAsrPipeline.WaitForInitializeAsync();
await funAsrPipeline.WaitForReadinessAsync();
await service.StopAsync(CancellationToken.None);
Assert.Equal(1, defaultPipeline.InitializeCount);
Assert.Equal(1, funAsrPipeline.InitializeCount);
Assert.True(defaultPipeline.Disposed);
Assert.True(funAsrPipeline.Disposed);
Assert.True(funAsrPipeline.ReadinessCancellationWasObserved);
Assert.Equal(0, englishPipeline.InitializeCount);
Assert.False(englishPipeline.Disposed);
}
private sealed class CapturingSpeechRecognitionPipelineFactory : ISpeechRecognitionPipelineFactory
{
private readonly IReadOnlyDictionary<string, ISpeechRecognitionPipeline> pipelines;
public CapturingSpeechRecognitionPipelineFactory(ISpeechRecognitionPipeline pipeline)
: this(new Dictionary<string, ISpeechRecognitionPipeline>
{
[ConfigurationLaunchProfileOptionsProvider.DefaultProfileName] = pipeline
})
{
}
public CapturingSpeechRecognitionPipelineFactory(
IReadOnlyDictionary<string, ISpeechRecognitionPipeline> pipelines)
{
this.pipelines = pipelines;
}
public ISpeechRecognitionPipeline Create()
{
return Create(ConfigurationLaunchProfileOptionsProvider.DefaultProfileName);
}
public ISpeechRecognitionPipeline Create(string? launchProfileName)
{
var profileName = string.IsNullOrWhiteSpace(launchProfileName)
? ConfigurationLaunchProfileOptionsProvider.DefaultProfileName
: launchProfileName;
return pipelines[profileName];
}
}
private sealed class FakeLaunchProfileOptionsProvider : ILaunchProfileOptionsProvider
{
private readonly IReadOnlyList<LaunchProfile> profiles;
public FakeLaunchProfileOptionsProvider(IReadOnlyList<LaunchProfile> profiles)
{
this.profiles = profiles;
}
public LaunchProfile GetRequiredProfile(string? name)
{
var profileName = string.IsNullOrWhiteSpace(name)
? ConfigurationLaunchProfileOptionsProvider.DefaultProfileName
: name;
return profiles.Single(profile => profile.Name.Equals(profileName, StringComparison.OrdinalIgnoreCase));
}
public IReadOnlyList<LaunchProfile> GetProfiles()
{
return profiles;
}
public IReadOnlyList<LaunchProfileHotkey> GetHotkeys()
{
return [];
}
}
private sealed class CapturingSpeechRecognitionPipeline : ISpeechRecognitionPipeline
{
private readonly TaskCompletionSource initialized = new(TaskCreationOptions.RunContinuationsAsynchronously);
private readonly TaskCompletionSource readinessChecked = new(TaskCreationOptions.RunContinuationsAsynchronously);
public int InitializeCount { get; private set; }
public int ReadinessCount { get; private set; }
public bool BlockReadinessUntilCancelled { get; init; }
public bool ReadinessCancellationWasObserved { get; private set; }
public bool Disposed { get; private set; }
public Task WaitForInitializeAsync()
{
return initialized.Task.WaitAsync(TimeSpan.FromSeconds(5));
}
public Task WaitForReadinessAsync()
{
return readinessChecked.Task.WaitAsync(TimeSpan.FromSeconds(5));
}
public Task InitializeAsync(CancellationToken cancellationToken)
{
return InitializeAsync(SpeechRecognitionPipelineOptions.Default, cancellationToken);
}
public Task InitializeAsync(
SpeechRecognitionPipelineOptions options,
CancellationToken cancellationToken)
{
InitializeCount++;
initialized.TrySetResult();
return Task.CompletedTask;
}
public async Task WaitUntilReadyAsync(CancellationToken cancellationToken)
{
ReadinessCount++;
readinessChecked.TrySetResult();
if (!BlockReadinessUntilCancelled)
{
return;
}
try
{
await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken);
}
catch (OperationCanceledException)
{
ReadinessCancellationWasObserved = true;
throw;
}
}
public ValueTask WriteAsync(MeetingAssistant.Recording.AudioChunk chunk, CancellationToken cancellationToken)
{
return ValueTask.CompletedTask;
}
public Task CompleteAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public async IAsyncEnumerable<TranscriptionSegment> ReadLiveTranscriptAsync(
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
{
await Task.CompletedTask;
yield break;
}
public Task<IReadOnlyList<TranscriptionSegment>> ReadFinishedTranscriptAsync(
string audioPath,
SpeechRecognitionPipelineOptions options,
CancellationToken cancellationToken)
{
return Task.FromResult<IReadOnlyList<TranscriptionSegment>>([]);
}
public ValueTask DisposeAsync()
{
Disposed = true;
return ValueTask.CompletedTask;
}
}
}