Make meeting lifecycle stateful

This commit is contained in:
2026-05-27 12:55:17 +02:00
parent d607b957bb
commit e85274829a
91 changed files with 4076 additions and 479 deletions
@@ -0,0 +1,153 @@
using Microsoft.Extensions.Configuration;
namespace MeetingAssistant.LaunchProfiles;
public interface ILaunchProfileOptionsProvider
{
LaunchProfile GetRequiredProfile(string? name);
IReadOnlyList<LaunchProfileHotkey> GetHotkeys();
}
public sealed record LaunchProfile(string Name, MeetingAssistantOptions Options);
public sealed record LaunchProfileHotkey(string ProfileName, string Toggle);
public sealed class ConfigurationLaunchProfileOptionsProvider : ILaunchProfileOptionsProvider
{
public const string DefaultProfileName = "default";
private readonly IConfiguration configuration;
public ConfigurationLaunchProfileOptionsProvider(IConfiguration configuration)
{
this.configuration = configuration;
}
public LaunchProfile GetRequiredProfile(string? name)
{
var profileName = NormalizeProfileName(name);
var options = BindDefaultOptions();
if (profileName.Equals(DefaultProfileName, StringComparison.OrdinalIgnoreCase))
{
return new LaunchProfile(DefaultProfileName, options);
}
var profileSection = GetProfilesSection().GetSection(profileName);
if (!profileSection.Exists())
{
throw new KeyNotFoundException($"Launch profile '{profileName}' is not configured.");
}
profileSection.Bind(options);
ApplyArrayOverrides(profileSection, options);
return new LaunchProfile(profileName, options);
}
public IReadOnlyList<LaunchProfileHotkey> GetHotkeys()
{
var hotkeys = GetProfileNames()
.Select(profileName =>
{
var profile = GetRequiredProfile(profileName);
return new LaunchProfileHotkey(profile.Name, profile.Options.Hotkey.Toggle);
})
.Where(hotkey => !string.IsNullOrWhiteSpace(hotkey.Toggle))
.ToList();
var duplicate = hotkeys
.GroupBy(hotkey => hotkey.Toggle.Trim(), StringComparer.OrdinalIgnoreCase)
.FirstOrDefault(group => group.Count() > 1);
if (duplicate is not null)
{
throw new InvalidOperationException(
$"Launch profile hotkey '{duplicate.Key}' is configured more than once for profiles {string.Join(", ", duplicate.Select(hotkey => hotkey.ProfileName))}.");
}
return hotkeys;
}
private IReadOnlyList<string> GetProfileNames()
{
return new[] { DefaultProfileName }
.Concat(GetProfilesSection()
.GetChildren()
.Select(section => section.Key)
.Where(key => !key.Equals(DefaultProfileName, StringComparison.OrdinalIgnoreCase)))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
}
private MeetingAssistantOptions BindDefaultOptions()
{
var options = new MeetingAssistantOptions();
GetRootSection().Bind(options);
return options;
}
private IConfigurationSection GetRootSection()
{
return configuration.GetSection("MeetingAssistant");
}
private IConfigurationSection GetProfilesSection()
{
return GetRootSection().GetSection("LaunchProfiles");
}
private static string NormalizeProfileName(string? name)
{
return string.IsNullOrWhiteSpace(name) ||
name.Equals(DefaultProfileName, StringComparison.OrdinalIgnoreCase)
? DefaultProfileName
: name.Trim();
}
private static void ApplyArrayOverrides(
IConfigurationSection profileSection,
MeetingAssistantOptions options)
{
var autoDetectLanguages = ReadArrayOverride(profileSection.GetSection("AzureSpeech:AutoDetectLanguages"));
if (autoDetectLanguages is not null)
{
options.AzureSpeech.AutoDetectLanguages = autoDetectLanguages;
}
var funAsrChunkSize = ReadIntArrayOverride(profileSection.GetSection("FunAsr:ChunkSize"));
if (funAsrChunkSize is not null)
{
options.FunAsr.ChunkSize = funAsrChunkSize;
}
}
private static string[]? ReadArrayOverride(IConfigurationSection section)
{
if (!section.Exists())
{
return null;
}
return section
.GetChildren()
.OrderBy(child => int.TryParse(child.Key, out var index) ? index : int.MaxValue)
.Select(child => child.Value)
.Where(value => !string.IsNullOrWhiteSpace(value))
.Select(value => value!)
.ToArray();
}
private static int[]? ReadIntArrayOverride(IConfigurationSection section)
{
if (!section.Exists())
{
return null;
}
return section
.GetChildren()
.OrderBy(child => int.TryParse(child.Key, out var index) ? index : int.MaxValue)
.Select(child => int.TryParse(child.Value, out var value) ? value : (int?)null)
.Where(value => value.HasValue)
.Select(value => value!.Value)
.ToArray();
}
}