Public Access
238 lines
8.5 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|