Public Access
Warm up pyannote and FunASR runtimes
PR and Push Build/Test / build-and-test (push) Successful in 6m48s
PR and Push Build/Test / build-and-test (push) Successful in 6m48s
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
using MeetingAssistant;
|
||||
using MeetingAssistant.LaunchProfiles;
|
||||
using MeetingAssistant.Transcription;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace MeetingAssistant.Tests;
|
||||
|
||||
public sealed class PyannoteDiarizationWarmupHostedServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task HostedServiceWarmsEnabledValidationRuntimeWithoutBlockingStartup()
|
||||
{
|
||||
var commandRunner = new BlockingCommandRunner();
|
||||
var finalizer = new PyannoteTranscriptFinalizer(
|
||||
commandRunner,
|
||||
Options.Create(new MeetingAssistantOptions()),
|
||||
NullLogger<PyannoteTranscriptFinalizer>.Instance);
|
||||
var service = new PyannoteDiarizationWarmupHostedService(
|
||||
finalizer,
|
||||
new FakeLaunchProfileOptionsProvider(new MeetingAssistantOptions
|
||||
{
|
||||
Recording = { TranscriptionProvider = "azure-speech" },
|
||||
SpeakerIdentification =
|
||||
{
|
||||
PyannoteValidation =
|
||||
{
|
||||
Enabled = true,
|
||||
Diarization =
|
||||
{
|
||||
Enabled = true,
|
||||
DockerCommand = "docker",
|
||||
Image = "meeting-assistant-pyannote-validation:local",
|
||||
ModelsFolder = Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
"meeting-assistant-tests",
|
||||
Guid.NewGuid().ToString("N"),
|
||||
"models"),
|
||||
Token = "hf_test",
|
||||
TokenEnv = "",
|
||||
CommandTimeout = TimeSpan.FromMinutes(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
NullLogger<PyannoteDiarizationWarmupHostedService>.Instance);
|
||||
|
||||
await service.StartAsync(CancellationToken.None).WaitAsync(TimeSpan.FromSeconds(1));
|
||||
await commandRunner.WaitForRunAsync();
|
||||
|
||||
await service.StopAsync(CancellationToken.None);
|
||||
|
||||
Assert.Contains(commandRunner.Commands, command => command.Arguments.Contains("meeting-assistant-pyannote-validation:local"));
|
||||
Assert.True(commandRunner.RunCancellationWasObserved);
|
||||
}
|
||||
|
||||
private sealed class FakeLaunchProfileOptionsProvider : ILaunchProfileOptionsProvider
|
||||
{
|
||||
private readonly MeetingAssistantOptions options;
|
||||
|
||||
public FakeLaunchProfileOptionsProvider(MeetingAssistantOptions options)
|
||||
{
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
public LaunchProfile GetRequiredProfile(string? name)
|
||||
{
|
||||
return new LaunchProfile(ConfigurationLaunchProfileOptionsProvider.DefaultProfileName, options);
|
||||
}
|
||||
|
||||
public IReadOnlyList<LaunchProfile> GetProfiles()
|
||||
{
|
||||
return [GetRequiredProfile(null)];
|
||||
}
|
||||
|
||||
public IReadOnlyList<LaunchProfileHotkey> GetHotkeys()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class BlockingCommandRunner : ICommandRunner
|
||||
{
|
||||
private readonly TaskCompletionSource runStarted = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
public IReadOnlyList<CapturedCommand> Commands { get; private set; } = [];
|
||||
|
||||
public bool RunCancellationWasObserved { get; private set; }
|
||||
|
||||
public Task WaitForRunAsync()
|
||||
{
|
||||
return runStarted.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
public async Task<CommandResult> RunAsync(
|
||||
string fileName,
|
||||
IReadOnlyList<string> arguments,
|
||||
CancellationToken cancellationToken,
|
||||
IReadOnlyDictionary<string, string>? environment = null)
|
||||
{
|
||||
Commands = Commands.Append(new CapturedCommand(fileName, arguments)).ToList();
|
||||
if (arguments.Contains("inspect"))
|
||||
{
|
||||
return new CommandResult(0, "image-id", "");
|
||||
}
|
||||
|
||||
runStarted.TrySetResult();
|
||||
try
|
||||
{
|
||||
await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
RunCancellationWasObserved = true;
|
||||
throw;
|
||||
}
|
||||
|
||||
return new CommandResult(0, "", "");
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record CapturedCommand(string FileName, IReadOnlyList<string> Arguments);
|
||||
}
|
||||
@@ -181,6 +181,54 @@ public sealed class PyannoteTranscriptFinalizerTests
|
||||
Assert.Equal("hf_azure", commandRunner.Environment["HF_TOKEN"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WarmUpBuildsImageAndDownloadsConfiguredModelWithoutAudioInput()
|
||||
{
|
||||
var commandRunner = new CapturingCommandRunner("");
|
||||
var finalizer = CreateFinalizer(commandRunner, token: "hf_test");
|
||||
var diarization = new PyannoteDiarizationOptions
|
||||
{
|
||||
Enabled = true,
|
||||
DockerCommand = "docker",
|
||||
BaseImage = "python:3.11-slim",
|
||||
Image = "meeting-assistant-pyannote-warmup:local",
|
||||
ModelsFolder = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N"), "models"),
|
||||
Model = "pyannote/speaker-diarization-3.1",
|
||||
Token = "hf_test",
|
||||
TokenEnv = "",
|
||||
CommandTimeout = TimeSpan.FromMinutes(1)
|
||||
};
|
||||
|
||||
await finalizer.WarmUpAsync(diarization, CancellationToken.None);
|
||||
|
||||
Assert.Contains(commandRunner.Commands, command => command.Arguments.Contains("inspect"));
|
||||
Assert.Contains(commandRunner.Commands, command => command.Arguments.Contains("build"));
|
||||
var runCommand = commandRunner.Commands.Last();
|
||||
Assert.Contains("run", runCommand.Arguments);
|
||||
Assert.Contains("meeting-assistant-pyannote-warmup:local", runCommand.Arguments);
|
||||
Assert.DoesNotContain(runCommand.Arguments, argument => argument.Contains("/workspace/input.wav", StringComparison.Ordinal));
|
||||
Assert.Contains(runCommand.Arguments, argument => argument.Contains("Pipeline.from_pretrained", StringComparison.Ordinal));
|
||||
Assert.Contains(runCommand.Arguments, argument => argument.Contains("pyannote/speaker-diarization-3.1", StringComparison.Ordinal));
|
||||
Assert.Equal("hf_test", commandRunner.Environment["HF_TOKEN"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WarmUpSkipsWhenTokenIsMissing()
|
||||
{
|
||||
var commandRunner = new CapturingCommandRunner("");
|
||||
var finalizer = CreateFinalizer(commandRunner, token: null);
|
||||
|
||||
await finalizer.WarmUpAsync(new PyannoteDiarizationOptions
|
||||
{
|
||||
Enabled = true,
|
||||
Token = "",
|
||||
TokenEnv = "",
|
||||
ModelsFolder = Path.Combine(Path.GetTempPath(), "meeting-assistant-tests", Guid.NewGuid().ToString("N"), "models")
|
||||
}, CancellationToken.None);
|
||||
|
||||
Assert.Empty(commandRunner.Commands);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FinalizerSkipsPyannoteWhenTokenIsMissing()
|
||||
{
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using MeetingAssistant;
|
||||
using MeetingAssistant.LaunchProfiles;
|
||||
using MeetingAssistant.Transcription;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
@@ -27,18 +29,118 @@ public sealed class SpeechRecognitionPipelineHostedServiceTests
|
||||
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 ISpeechRecognitionPipeline pipeline;
|
||||
private readonly IReadOnlyDictionary<string, ISpeechRecognitionPipeline> pipelines;
|
||||
|
||||
public CapturingSpeechRecognitionPipelineFactory(ISpeechRecognitionPipeline pipeline)
|
||||
: this(new Dictionary<string, ISpeechRecognitionPipeline>
|
||||
{
|
||||
[ConfigurationLaunchProfileOptionsProvider.DefaultProfileName] = pipeline
|
||||
})
|
||||
{
|
||||
this.pipeline = pipeline;
|
||||
}
|
||||
|
||||
public CapturingSpeechRecognitionPipelineFactory(
|
||||
IReadOnlyDictionary<string, ISpeechRecognitionPipeline> pipelines)
|
||||
{
|
||||
this.pipelines = pipelines;
|
||||
}
|
||||
|
||||
public ISpeechRecognitionPipeline Create()
|
||||
{
|
||||
return pipeline;
|
||||
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 [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user