Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions AdaptiveRemote.sln
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdaptiveRemote.Console", "s
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AdaptiveRemote.Tests", "test\AdaptiveRemote.Tests\AdaptiveRemote.Tests.csproj", "{99181C45-EACC-45CE-87B3-20A4AA28E793}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AdaptiveRemote.Tests.Windows", "test\AdaptiveRemote.Tests.Windows\AdaptiveRemote.Tests.Windows.csproj", "{C3D4E5F6-7890-ABCD-EF12-345678901234}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AdaptiveRemote.Core", "src\AdaptiveRemote.Core\AdaptiveRemote.Core.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AdaptiveRemote.Electron", "src\AdaptiveRemote.Electron\AdaptiveRemote.Electron.csproj", "{B2C3D4E5-F678-90AB-CDEF-123456789012}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
Expand All @@ -31,10 +37,22 @@ Global
{99181C45-EACC-45CE-87B3-20A4AA28E793}.Debug|Any CPU.Build.0 = Debug|Any CPU
{99181C45-EACC-45CE-87B3-20A4AA28E793}.Release|Any CPU.ActiveCfg = Release|Any CPU
{99181C45-EACC-45CE-87B3-20A4AA28E793}.Release|Any CPU.Build.0 = Release|Any CPU
{C3D4E5F6-7890-ABCD-EF12-345678901234}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C3D4E5F6-7890-ABCD-EF12-345678901234}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C3D4E5F6-7890-ABCD-EF12-345678901234}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C3D4E5F6-7890-ABCD-EF12-345678901234}.Release|Any CPU.Build.0 = Release|Any CPU
{345B73FC-07F9-490F-B566-2677D10B1834}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{345B73FC-07F9-490F-B566-2677D10B1834}.Debug|Any CPU.Build.0 = Debug|Any CPU
{345B73FC-07F9-490F-B566-2677D10B1834}.Release|Any CPU.ActiveCfg = Release|Any CPU
{345B73FC-07F9-490F-B566-2677D10B1834}.Release|Any CPU.Build.0 = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
{B2C3D4E5-F678-90AB-CDEF-123456789012}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B2C3D4E5-F678-90AB-CDEF-123456789012}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B2C3D4E5-F678-90AB-CDEF-123456789012}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B2C3D4E5-F678-90AB-CDEF-123456789012}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
2 changes: 1 addition & 1 deletion src/AdaptiveRemote.Console/AdaptiveRemote.Console.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
</PropertyGroup>

<ItemGroup>
<Content Include="..\AdaptiveRemote\wwwroot\**\*.html;..\AdaptiveRemote\wwwroot\**\*.css;">
<Content Include="..\AdaptiveRemote.Core\wwwroot\**\*.html;..\AdaptiveRemote.Core\wwwroot\**\*.css;">
<Link>wwwroot\%(RecursiveDir)%(Filename)%(Extension)</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Visible>true</Visible>
Expand Down
51 changes: 51 additions & 0 deletions src/AdaptiveRemote.Core/AdaptiveRemote.Core.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>AdaptiveRemote</RootNamespace>

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that the root namespace is still AdaptiveRemote and not AdaptiveRemote.Core. However, I think the generated Blazor component code is going to use AdaptiveRemote.Core for its namespaces. If it's possible, I want that to be configured as AdaptiveRemote namespace as well.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already configured correctly - the Core csproj has <RootNamespace>AdaptiveRemote</RootNamespace> which means Blazor components in Components/ folder are generated with namespace AdaptiveRemote.Components, not AdaptiveRemote.Core.Components.

</PropertyGroup>

<ItemGroup>
<None Remove="Services\Conversation\static_grammar.xml" />
</ItemGroup>

<ItemGroup>
<EmbeddedResource Include="Services\Conversation\static_grammar.xml" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.3.2" />
<PackageReference Include="Azure.Identity" Version="1.11.4" />
<PackageReference Include="Azure.Monitor.OpenTelemetry.Exporter" Version="1.2.0" />
<PackageReference Include="I8Beef.TiVo" Version="1.0.0.14" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="8.0.14" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Nerdbank.GitVersioning" Version="3.9.50">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.8.1" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.8.1" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.8.1" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="8.0.0" />
<PackageReference Include="System.Text.Json" Version="8.0.5" />
</ItemGroup>

<ItemGroup>
<Compile Update="Logging\LoggingMessages.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>LoggingMessages.resx</DependentUpon>
</Compile>
</ItemGroup>

<ItemGroup>
<EmbeddedResource Update="Logging\LoggingMessages.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>LoggingMessages.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace AdaptiveRemote.Configuration;

Expand All @@ -17,36 +16,18 @@ internal static IServiceCollection AddConversationServices(this IServiceCollecti
.AddScopedLifecycleService<ConversationController>()
.AddScoped<ISpeechRecognition, SpeechRecognition>()
.AddScoped<ISpeechSynthesis, SpeechSynthesis>()
.AddScoped<IGrammarProvider, StaticGrammarProvider>()
.AddScoped<ConversationStateMachine>()
.AddSingleton<ISpeechSynthesizer, SpeechSynthesizerWrapper>()
.AddSingleton<ISpeechRecognitionEngine, SpeechRecognitionEngineWrapper>()
.AddSingleton<IAudioConfigurationService, DefaultDeviceAudioConfiguration>()
.AddSingleton<IListeningController, ListeningController>()
.AddScoped(GetConversationViewModel);

internal static IServiceCollection AddConversationServices(this IServiceCollection services, IConfiguration config)
=> services
.AddConversationServices()
.OptionallyAddFakeSpeechRecognition(config)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OptionallyAddFakeSpeechRecognition and OptionallyAddSamplesRecorder shouldn't be removed. They're not related to the "Fakes" that were added for Electron. Rather, this is simulated speech and diagnostics that are used for testing without having to speak commands out loud.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 86c391a - restored FakeSpeechRecognitionEngine and SamplesRecorder support. The Windows project now registers FakeSpeechRecognitionEngine when Fake=true in config, and SamplesRecorder when RecordSamples=true, via the WindowsAcceleratedServices class.

.OptionallyAddSamplesRecorder(config)
.Configure<ConversationSettings>(config);

private static Models.ConversationView GetConversationViewModel(IServiceProvider provider)
{
IRemoteDefinitionService definition = provider.GetRequiredService<IRemoteDefinitionService>();
return definition.GetElement<Models.ConversationView>();
}

private static IServiceCollection OptionallyAddFakeSpeechRecognition(this IServiceCollection services, IConfiguration config)
=> config.GetValue<bool>(nameof(ConversationSettings.Fake)) == true
? services.AddSingleton<ISpeechRecognitionEngine, FakeSpeechRecognitionEngine>()
: services;

private static IServiceCollection OptionallyAddSamplesRecorder(this IServiceCollection services, IConfiguration config)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to self: Find out where this went

=> config.GetValue<bool>(nameof(ConversationSettings.RecordSamples)) == false
? services
: services
.AddHostedService<SamplesRecorder>()
.AddSingleton<ILoggerProvider, SamplesRecorder.LoggerProvider>();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using AdaptiveRemote.Models;
using AdaptiveRemote.Services;
using AdaptiveRemote.Services.Lifecycle;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace AdaptiveRemote.Configuration;

/// <summary>
/// Public extension methods for configuring the Electron host
/// </summary>
public static class ElectronHostBuilderExtensions

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have one configuration path, not two different ones.

What I would like to see is this:

  • Remove MainWindow from AcceleratedServices. Instead, create it in AdaptiveRemote.App using the ViewModel from AcceleratedServices (which is already public)
  • Add a hook for the host to add services to AcceleratedServices. Add a method ConfigureHostServices that calls through to _hostBuilder.ConfigureServices so that the WPF and Electron apps can add their host-specific services (e.g. the MainWindow instance and BlazorWindowScopeFactory for Windows, speech services, IScopeFactory for Electron)
  • Electron app should use the same AcceleratedServices to initialze itself, removing the need for these specialized methods.

In general, I want there to be as little code as possible in the Windows and Electron projects. Only things with Windows dependencies belong in the Windows project, and their analogs go in the Electron project. There should be no "Electron-specific" or "Windows-specific" code in the Core project. Abstractions in the Core project should be defined so they are host-agnostic and can be used exactly the same by both hosts. The goal is that the Electron app can be run in a Linux runner and we can be reasonably sure that the Windows version will run almost exactly the same way.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 86c391a - unified configuration via AcceleratedServices in Core with ConfigureAppSettings and ConfigureHostServices hooks. Windows uses WindowsAcceleratedServices wrapper that adds Windows-specific services (MainWindow, BlazorWindowScopeFactory, speech services). Electron uses the Core AcceleratedServices directly and adds Electron-specific services.

{
/// <summary>
/// Configures the application for Electron hosting with fake speech services
/// </summary>
public static IHostBuilder ConfigureElectronApp(this IHostBuilder hostBuilder)
=> hostBuilder
.ConfigureTelemetry()
.AddRemoteServices()
.AddBroadlinkSupport()
.AddTiVoSupport()
.AddConversationSystem()
.AddSystemWrapperServices();

/// <summary>
/// Adds the lifecycle view model and controller for Electron hosting
/// </summary>
public static IServiceCollection AddElectronLifecycle(this IServiceCollection services)
{
var lifecycleView = new LifecycleView();
var lifecycleController = new LifecycleViewController(lifecycleView);
services.AddSingleton(lifecycleView);
services.AddSingleton<ILifecycleViewController>(lifecycleController);
return services;
}

/// <summary>
/// Adds the scope factory for Electron hosting
/// </summary>
public static IServiceCollection AddElectronScopeFactory(this IServiceCollection services)
=> services.AddSingleton<IApplicationScopeFactory, ElectronScopeFactory>();
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ internal static IHostBuilder AddRemoteServices(this IHostBuilder builder)

internal static IServiceCollection AddRemoteServices(this IServiceCollection services)
=> services
.AddSingleton<IApplicationScopeFactory, BlazorWindowScopeFactory>()
.AddHostedService<ApplicationLifecycle>()
.AddScopedLifecycleService<LifecycleCommandService>()
.AddScoped<IRemoteDefinitionService, StaticCommandGroupProvider>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ namespace AdaptiveRemote.Configuration;

internal static class SystemWrapperHostBuilderExtensions
{
public static IHostBuilder AddSystemWrapperServices(this IHostBuilder builder)
internal static IHostBuilder AddSystemWrapperServices(this IHostBuilder builder)
=> builder.ConfigureServices(services => services.AddSystemWrappers());

public static IServiceCollection AddSystemWrappers(this IServiceCollection services)
internal static IServiceCollection AddSystemWrappers(this IServiceCollection services)
=> services
.AddSingleton<INetworking, SystemNetWrapper>()
.AddSingleton<IFileSystem, SystemIOWrapper>();
Expand Down
17 changes: 17 additions & 0 deletions src/AdaptiveRemote.Core/Services/Conversation/IGrammar.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace AdaptiveRemote.Services.Conversation;

/// <summary>
/// Platform-independent grammar interface that abstracts System.Speech.Recognition.Grammar
/// </summary>
public interface IGrammar
{
/// <summary>
/// Gets or sets the name of the grammar.
/// </summary>
string Name { get; }

/// <summary>
/// Gets or sets whether the grammar is enabled for recognition.
/// </summary>
bool Enabled { get; set; }
}
12 changes: 12 additions & 0 deletions src/AdaptiveRemote.Core/Services/Conversation/IGrammarProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace AdaptiveRemote.Services.Conversation;

/// <summary>
/// Loader for Grammar objects
/// </summary>
public interface IGrammarProvider
{
/// <summary>
/// Load a grammar that supports the given kind of phrases
/// </summary>
IGrammar LoadGrammar(PhraseKinds phraseKind);
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
using System.Speech.Recognition;

namespace AdaptiveRemote.Services.Conversation;
namespace AdaptiveRemote.Services.Conversation;

/// <summary>
/// Think wrapper around System.Speech.Recognition.SpeechRecognitionEngine.
/// Thin wrapper around System.Speech.Recognition.SpeechRecognitionEngine.
/// </summary>
internal interface ISpeechRecognitionEngine
public interface ISpeechRecognitionEngine
{
/// <summary>
/// Raised when the SpeechRecognitionEngine receives input that matches any of its
Expand All @@ -22,12 +20,12 @@ internal interface ISpeechRecognitionEngine
/// <summary>
/// Synchronously loads a Grammar object.
/// </summary>
void LoadGrammar(Grammar grammar);
void LoadGrammar(IGrammar grammar);

/// <summary>
/// Unloads a specified Grammar object from the SpeechRecognitionEngine instance.
/// </summary>
void UnloadGrammar(Grammar grammar);
void UnloadGrammar(IGrammar grammar);

/// <summary>
/// Unloads all Grammar objects from the recognizer.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
namespace AdaptiveRemote.Services.Conversation;

[Flags]
internal enum PhraseKinds
public enum PhraseKinds
{
WakeWord = 1,
Commands = 2,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

public class RecognizedSpeechEventArgs : EventArgs
{
internal RecognizedSpeechEventArgs(IRecognizedSpeech result)
public RecognizedSpeechEventArgs(IRecognizedSpeech result)
{
Result = result;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Speech.Recognition;
using System.Threading.Channels;
using System.Threading.Channels;
using AdaptiveRemote.Logging;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
Expand All @@ -21,7 +20,7 @@ internal class SpeechRecognition : ISpeechRecognition
private readonly IListeningController _listeningController;
private readonly ILogger<SpeechRecognition> _logger;

private readonly IReadOnlyDictionary<PhraseKinds, Grammar> _grammars;
private readonly IReadOnlyDictionary<PhraseKinds, IGrammar> _grammars;

public SpeechRecognition(IOptions<ConversationSettings> settings, ISpeechRecognitionEngine engine, IListeningController listeningController, IGrammarProvider grammarProvider, ILogger<SpeechRecognition> logger)
{
Expand All @@ -34,7 +33,7 @@ public SpeechRecognition(IOptions<ConversationSettings> settings, ISpeechRecogni

_grammars = GrammarKinds.ToDictionary(x => x, x => LoadGrammarIntoEngine(grammarProvider.LoadGrammar(x)));

Grammar LoadGrammarIntoEngine(Grammar grammar)
IGrammar LoadGrammarIntoEngine(IGrammar grammar)
{
grammar.Enabled = false;
_engine.LoadGrammar(grammar);
Expand All @@ -44,7 +43,7 @@ Grammar LoadGrammarIntoEngine(Grammar grammar)

void ISpeechRecognition.SetFilter(PhraseKinds filter)
{
foreach (KeyValuePair<PhraseKinds, Grammar> grammar in _grammars)
foreach (KeyValuePair<PhraseKinds, IGrammar> grammar in _grammars)
{
grammar.Value.Enabled = filter.HasFlag(grammar.Key);
}
Expand Down
37 changes: 37 additions & 0 deletions src/AdaptiveRemote.Core/Services/Lifecycle/ElectronScopeFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using Microsoft.Extensions.DependencyInjection;

namespace AdaptiveRemote.Services.Lifecycle;

/// <summary>
/// IApplicationScopeFactory implementation for Electron/Blazor Server hosting
/// </summary>
internal class ElectronScopeFactory : IApplicationScopeFactory, IApplicationScope
{
private readonly IServiceProvider _serviceProvider;
private IServiceScope? _scope;

public ElectronScopeFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}

public Task<IApplicationScope> CreateNewScopeAsync(CancellationToken cancellationToken)
{
_scope?.Dispose();
_scope = _serviceProvider.CreateScope();
return Task.FromResult<IApplicationScope>(this);
}

public async Task TryInvokeAsync(Func<IServiceProvider, CancellationToken, Task> workItem, CancellationToken cancellationToken)
{
var scopeServices = _scope?.ServiceProvider ?? _serviceProvider;
await workItem(scopeServices, cancellationToken);
}

public void Dispose()
{
_scope?.Dispose();
_scope = null;
GC.SuppressFinalize(this);
}
}
18 changes: 18 additions & 0 deletions src/AdaptiveRemote.Electron/AdaptiveRemote.Electron.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
Comment thread
jodavis marked this conversation as resolved.

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>AdaptiveRemote</RootNamespace>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="ElectronNET.API" Version="23.6.2" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\AdaptiveRemote.Core\AdaptiveRemote.Core.csproj" />
</ItemGroup>

</Project>
Loading