From 988a3d94e142c5507e865d1dc171e41bbab45b5c Mon Sep 17 00:00:00 2001 From: MaddoScientisto Date: Sat, 9 May 2026 12:09:05 +0200 Subject: [PATCH 1/3] feat: Implement face encoder functionality with GPU support and recursive option --- .forgejo/workflows/build-windows-avalonia.yml | 20 +- Catalog.code-workspace | 13 + NuGet.Config | 14 +- imagecatalog/AvaloniaMainWindow.axaml.cs | 25 + .../AvaloniaViews/FaceAiTabView.axaml | 20 +- .../AvaloniaViews/FaceAiTabView.axaml.cs | 198 +----- imagecatalog/DataModel.cs | 605 +++++++++++++++++- imagecatalog/Models/SettingsDto.cs | 4 + imagecatalog/Program.cs | 44 ++ .../ViewModels/AiSettingsViewModel.cs | 66 ++ 10 files changed, 790 insertions(+), 219 deletions(-) create mode 100644 Catalog.code-workspace diff --git a/.forgejo/workflows/build-windows-avalonia.yml b/.forgejo/workflows/build-windows-avalonia.yml index 3d3c18e..4666242 100644 --- a/.forgejo/workflows/build-windows-avalonia.yml +++ b/.forgejo/workflows/build-windows-avalonia.yml @@ -14,15 +14,15 @@ env: PROJECT_PATH: imagecatalog/ImageCatalog 2.csproj PUBLISH_DIR: artifacts/publish/win-x64 ARTIFACT_NAME: imagecatalog-windows-avalonia - NUGET_SOURCE_NAME: Nuget-GitLab-AIFotoONLUS - NUGET_SOURCE_URL: https://gitlab.com/api/v4/projects/79509532/packages/nuget/index.json + NUGET_SOURCE_NAME: Nuget-Forgejo-AIFotoONLUS + NUGET_SOURCE_URL: ${{ vars.AIFOTOONLUS_NUGET_SOURCE_URL || format('{0}/api/packages/{1}/nuget/index.json', github.server_url, vars.AIFOTOONLUS_PACKAGE_OWNER || github.repository_owner) }} jobs: build: runs-on: docker env: - NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }} - NUGET_PASSWORD: ${{ secrets.NUGET_PASSWORD }} + FORGEJO_PACKAGE_USERNAME: ${{ secrets.FORGEJO_PACKAGE_USERNAME }} + FORGEJO_PACKAGE_TOKEN: ${{ secrets.FORGEJO_PACKAGE_TOKEN }} steps: - name: Checkout @@ -36,12 +36,12 @@ jobs: - name: Validate NuGet secrets run: | set -eu - if [ -z "${NUGET_USERNAME}" ]; then - echo "secrets.NUGET_USERNAME is required" + if [ -z "${FORGEJO_PACKAGE_USERNAME}" ]; then + echo "secrets.FORGEJO_PACKAGE_USERNAME is required" exit 1 fi - if [ -z "${NUGET_PASSWORD}" ]; then - echo "secrets.NUGET_PASSWORD is required" + if [ -z "${FORGEJO_PACKAGE_TOKEN}" ]; then + echo "secrets.FORGEJO_PACKAGE_TOKEN is required" exit 1 fi @@ -53,8 +53,8 @@ jobs: dotnet nuget update source "${{ env.NUGET_SOURCE_NAME }}" \ --source "${{ env.NUGET_SOURCE_URL }}" \ - --username "${NUGET_USERNAME}" \ - --password "${NUGET_PASSWORD}" \ + --username "${FORGEJO_PACKAGE_USERNAME}" \ + --password "${FORGEJO_PACKAGE_TOKEN}" \ --store-password-in-clear-text \ --configfile "${temp_config}" diff --git a/Catalog.code-workspace b/Catalog.code-workspace new file mode 100644 index 0000000..36ebf33 --- /dev/null +++ b/Catalog.code-workspace @@ -0,0 +1,13 @@ +{ + "folders": [ + { + "path": "." + }, + { + "path": "../AIFotoONLUS" + } + ], + "settings": { + "commentTranslate.hover.enabled": false + } +} \ No newline at end of file diff --git a/NuGet.Config b/NuGet.Config index 79cf275..e0e2d2b 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -1,25 +1,25 @@ - + - + - - + + diff --git a/imagecatalog/AvaloniaMainWindow.axaml.cs b/imagecatalog/AvaloniaMainWindow.axaml.cs index 2de8d3f..8ebe952 100644 --- a/imagecatalog/AvaloniaMainWindow.axaml.cs +++ b/imagecatalog/AvaloniaMainWindow.axaml.cs @@ -3,6 +3,7 @@ using Avalonia.Interactivity; using Avalonia.Platform.Storage; using Avalonia.Styling; using Avalonia.Threading; +using System.ComponentModel; using System.IO; namespace ImageCatalog_2; @@ -20,6 +21,7 @@ public partial class AvaloniaMainWindow : Window DataContext = _model; Opened += (_, _) => SyncThemeStateFromCurrentTheme(); + Closing += AvaloniaMainWindow_Closing; // Let DataModel marshal callbacks onto Avalonia UI thread. _model.UiInvoker = action => Dispatcher.UIThread.Invoke(action); @@ -135,6 +137,29 @@ public partial class AvaloniaMainWindow : Window }; } + private bool _isStoppingFaceEncoderForClose; + + private async void AvaloniaMainWindow_Closing(object? sender, CancelEventArgs e) + { + if (_isStoppingFaceEncoderForClose || !_model.IsFaceEncoderRunning) + { + return; + } + + e.Cancel = true; + _isStoppingFaceEncoderForClose = true; + + try + { + await _model.StopFaceEncoderAsync("Arresto face encoder in chiusura...", waitForExit: true); + } + finally + { + _isStoppingFaceEncoderForClose = false; + Close(); + } + } + private void ToggleTheme_Click(object? sender, RoutedEventArgs e) { _isDarkTheme = !_isDarkTheme; diff --git a/imagecatalog/AvaloniaViews/FaceAiTabView.axaml b/imagecatalog/AvaloniaViews/FaceAiTabView.axaml index 7f7f3ae..2beb773 100644 --- a/imagecatalog/AvaloniaViews/FaceAiTabView.axaml +++ b/imagecatalog/AvaloniaViews/FaceAiTabView.axaml @@ -5,13 +5,13 @@ - - + + + + + + + @@ -56,12 +66,14 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/imagecatalog/MainWindow.xaml.cs b/imagecatalog/MainWindow.xaml.cs deleted file mode 100644 index 146ccb3..0000000 --- a/imagecatalog/MainWindow.xaml.cs +++ /dev/null @@ -1,383 +0,0 @@ -#if WINDOWS -using System.Windows; -using MahApps.Metro.Controls; -using ControlzEx.Theming; -using Microsoft.Extensions.DependencyInjection; -using System; -using System.IO; -using System.Windows.Media.Imaging; -using System.Diagnostics; -using Microsoft.Win32; -using System.Windows.Forms; - -namespace ImageCatalog_2 -{ - public partial class MainWindow : MetroWindow - { - private readonly DataModel _model; - private bool _isDarkTheme = false; - public MainWindow(DataModel model) - { - InitializeComponent(); - _model = model; - DataContext = _model; - // Set product version in status bar (use ProductVersion rather than AssemblyVersion) - try - { - var entry = System.Reflection.Assembly.GetEntryAssembly(); - string version = string.Empty; - if (entry is not null && !string.IsNullOrEmpty(entry.Location)) - { - try - { - version = FileVersionInfo.GetVersionInfo(entry.Location).ProductVersion ?? string.Empty; - } - catch { } - } - if (string.IsNullOrWhiteSpace(version)) - { - // fallback to assembly version - version = entry?.GetName().Version?.ToString() ?? string.Empty; - } - VersionTextBlock.Text = string.IsNullOrWhiteSpace(version) ? string.Empty : $"v{version}"; - } - catch { } - // Ensure MahApps resource dictionaries are loaded so chrome/styles are available - EnsureMahAppsResourcesLoaded(); - - // Apply theme based on user preference or system setting (default to light) - ApplyTheme(isDark: false); - // Subscribe to DataModel events that require UI dialogs - _model.SelectSourceFolderRequested += Model_SelectSourceFolderRequested; - _model.SelectDestinationFolderRequested += Model_SelectDestinationFolderRequested; - _model.SelectLogoFileRequested += Model_SelectLogoFileRequested; - _model.SaveSettingsRequested += Model_SaveSettingsRequested; - _model.LoadSettingsRequested += Model_LoadSettingsRequested; - _model.SelectColorRequested += Model_SelectColorRequested; - _model.SelectTransparentColorRequested += Model_SelectTransparentColorRequested; - _model.SelectModelsFolderRequested += Model_SelectModelsFolderRequested; - _model.SelectCsvOutputRequested += Model_SelectCsvOutputRequested; - - // Watch for logo changes to update preview - _model.PropertyChanged += Model_PropertyChanged; - } - - private void ApplyTheme(bool isDark) - { - try - { - var rd = isDark ? (ResourceDictionary)Resources["DarkTheme"] : (ResourceDictionary)Resources["LightTheme"]; - foreach (var key in rd.Keys) - { - // If the theme dictionary uses suffixed keys (e.g. "WindowBackgroundBrush.Dark"), - // map them to the base key ("WindowBackgroundBrush") so existing DynamicResource lookups update. - string outKey = key?.ToString() ?? string.Empty; - if (outKey.EndsWith(".Light", StringComparison.OrdinalIgnoreCase)) - outKey = outKey.Substring(0, outKey.Length - ".Light".Length); - else if (outKey.EndsWith(".Dark", StringComparison.OrdinalIgnoreCase)) - outKey = outKey.Substring(0, outKey.Length - ".Dark".Length); - - Resources[outKey] = rd[key]; - } - } - catch - { - // ignore theme failures - } - } - - private void Model_SelectModelsFolderRequested(object? sender, EventArgs e) - { - var dlg = new System.Windows.Forms.FolderBrowserDialog(); - var starting = string.IsNullOrWhiteSpace(_model.ModelsFolderPath) ? Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) : _model.ModelsFolderPath; - dlg.SelectedPath = starting; - if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK) - { - _model.ModelsFolderPath = dlg.SelectedPath + Path.DirectorySeparatorChar; - } - } - - private void OpenModelsFolder_Click(object sender, RoutedEventArgs e) - { - try - { - var path = _model.ModelsFolderPath; - if (string.IsNullOrWhiteSpace(path)) return; - path = path.Trim().Trim('"'); - if (File.Exists(path)) - { - System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{path}\""); - return; - } - if (Directory.Exists(path)) - { - System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = path, UseShellExecute = true }); - return; - } - } - catch { } - } - - private void Model_SelectCsvOutputRequested(object? sender, EventArgs e) - { - var dlg = new Microsoft.Win32.SaveFileDialog(); - dlg.Filter = "CSV file (*.csv)|*.csv|All files (*.*)|*.*"; - if (!string.IsNullOrWhiteSpace(_model.CsvOutputPath)) dlg.FileName = _model.CsvOutputPath; - var result = dlg.ShowDialog(this); - if (result == true) - { - _model.CsvOutputPath = dlg.FileName; - } - } - - private void OpenCsvOutputFolder_Click(object sender, RoutedEventArgs e) - { - try - { - var path = _model.CsvOutputPath; - if (string.IsNullOrWhiteSpace(path)) return; - path = path.Trim().Trim('"'); - if (File.Exists(path)) - { - System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{path}\""); - return; - } - var dir = Path.GetDirectoryName(path); - if (!string.IsNullOrWhiteSpace(dir) && Directory.Exists(dir)) - { - System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = dir, UseShellExecute = true }); - return; - } - } - catch { } - } - - private void Model_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) - { - if (e is null || string.IsNullOrWhiteSpace(e.PropertyName)) return; - if (e.PropertyName == nameof(_model.LogoFile)) - { - UpdateLogoPreview(_model.LogoFile); - } - } - - private void Model_SelectSourceFolderRequested(object? sender, EventArgs e) - { - var dlg = new System.Windows.Forms.FolderBrowserDialog(); - var starting = string.IsNullOrWhiteSpace(_model.SourcePath) ? Environment.GetFolderPath(Environment.SpecialFolder.MyPictures) : _model.SourcePath; - dlg.SelectedPath = starting; - if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK) - { - _model.SourcePath = dlg.SelectedPath + Path.DirectorySeparatorChar; - } - } - - private void OpenSourceFolder_Click(object sender, RoutedEventArgs e) - { - try - { - var path = _model.SourcePath; - if (string.IsNullOrWhiteSpace(path)) return; - path = path.Trim().Trim('"'); - if (File.Exists(path)) - { - System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{path}\""); - return; - } - if (Directory.Exists(path)) - { - System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = path, UseShellExecute = true }); - return; - } - } - catch (Exception ex) - { - // ignore for now, or could show a message - } - } - - private void Model_SelectDestinationFolderRequested(object? sender, EventArgs e) - { - var dlg = new System.Windows.Forms.FolderBrowserDialog(); - var starting = string.IsNullOrWhiteSpace(_model.DestinationPath) ? Environment.GetFolderPath(Environment.SpecialFolder.MyPictures) : _model.DestinationPath; - dlg.SelectedPath = starting; - if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK) - { - _model.DestinationPath = dlg.SelectedPath + Path.DirectorySeparatorChar; - } - - } - - private void OpenDestinationFolder_Click(object sender, RoutedEventArgs e) - { - try - { - var path = _model.DestinationPath; - if (string.IsNullOrWhiteSpace(path)) return; - path = path.Trim().Trim('"'); - if (File.Exists(path)) - { - System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{path}\""); - return; - } - if (Directory.Exists(path)) - { - System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = path, UseShellExecute = true }); - return; - } - } - catch (Exception ex) - { - // ignore for now - } - } - - private void Model_SelectLogoFileRequested(object? sender, EventArgs e) - { - var dlg = new Microsoft.Win32.OpenFileDialog(); - dlg.Filter = "Image Files|*.jpg;*.jpeg;*.png;*.bmp;*.gif"; - if (!string.IsNullOrWhiteSpace(_model.LogoFile)) dlg.FileName = _model.LogoFile; - var result = dlg.ShowDialog(this); - if (result == true) - { - _model.LogoFile = dlg.FileName; - } - } - - private async void Model_SaveSettingsRequested(object? sender, string filePath) - { - var dlg = new Microsoft.Win32.SaveFileDialog(); - dlg.Filter = "Setup (*.xml)|*.xml|All valid files (*.*)|*.*"; - var result = dlg.ShowDialog(this); - if (result == true) - { - await _model.SaveSettingsToFileAsync(dlg.FileName); - } - } - - private async void Model_LoadSettingsRequested(object? sender, string filePath) - { - var dlg = new Microsoft.Win32.OpenFileDialog(); - dlg.Filter = "Setup (*.xml)|*.xml|All valid files (*.*)|*.*"; - var result = dlg.ShowDialog(this); - if (result == true) - { - await _model.LoadSettingsFromFileAsync(dlg.FileName); - } - } - - private void Model_SelectColorRequested(object? sender, EventArgs e) - { - var dlg = new System.Windows.Forms.ColorDialog { AllowFullOpen = true }; - if (!string.IsNullOrWhiteSpace(_model.TextColorRGB)) - { - try { dlg.Color = System.Drawing.ColorTranslator.FromHtml(_model.TextColorRGB); } catch { } - } - if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK) - { - _model.TextColorRGB = System.Drawing.ColorTranslator.ToHtml(dlg.Color); - } - } - - private void Model_SelectTransparentColorRequested(object? sender, EventArgs e) - { - var dlg = new System.Windows.Forms.ColorDialog { AllowFullOpen = true }; - try { dlg.Color = System.Drawing.ColorTranslator.FromHtml(_model.TransparentColor); } catch { } - if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK) - { - _model.TransparentColor = System.Drawing.ColorTranslator.ToHtml(dlg.Color); - } - } - - private void ToggleTheme_Click(object? sender, RoutedEventArgs e) - { - ToggleTheme(); - } - - private void ToggleTheme() - { - try - { - _isDarkTheme = !_isDarkTheme; - - // Use MahApps ThemeManager to change the application theme (handles chrome and brushes) - try - { - var themeName = _isDarkTheme ? "Dark.Blue" : "Light.Blue"; - ThemeManager.Current.ChangeTheme(System.Windows.Application.Current, themeName); - } - catch - { - // Fall back silently if ThemeManager isn't available - } - - // Still apply local resource overrides so any app-specific keys update - ApplyTheme(_isDarkTheme); - } - catch - { - // ignore toggle failures - } - } - - private void EnsureMahAppsResourcesLoaded() - { - try - { - var app = System.Windows.Application.Current; - if (app is null) - return; - - var mds = app.Resources.MergedDictionaries; - - // Helper to add if missing - void AddIfMissing(string uriString) - { - if (!mds.Any(d => d.Source is not null && d.Source.OriginalString.Equals(uriString, StringComparison.OrdinalIgnoreCase))) - { - mds.Add(new ResourceDictionary { Source = new Uri(uriString) }); - } - } - - AddIfMissing("pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml"); - AddIfMissing("pack://application:,,,/MahApps.Metro;component/Styles/Fonts.xaml"); - // Ensure a default theme is present - if (!mds.Any(d => d.Source is not null && d.Source.OriginalString.IndexOf("/MahApps.Metro;component/Styles/Themes/", StringComparison.OrdinalIgnoreCase) >= 0)) - { - AddIfMissing("pack://application:,,,/MahApps.Metro;component/Styles/Themes/Light.Blue.xaml"); - _isDarkTheme = false; - } - } - catch - { - // ignore; styling will fallback to local resources - } - } - - private void UpdateLogoPreview(string? path) - { - if (string.IsNullOrWhiteSpace(path) || !File.Exists(path)) - { - LogoPreview.Source = null; - return; - } - - try - { - var bitmap = new BitmapImage(); - bitmap.BeginInit(); - bitmap.CacheOption = BitmapCacheOption.OnLoad; - bitmap.UriSource = new Uri(path); - bitmap.EndInit(); - LogoPreview.Source = bitmap; - } - catch - { - LogoPreview.Source = null; - } - } - } -} - - -#endif diff --git a/imagecatalog/Program.cs b/imagecatalog/Program.cs index 550bd01..fef9c61 100644 --- a/imagecatalog/Program.cs +++ b/imagecatalog/Program.cs @@ -11,7 +11,6 @@ using Microsoft.Extensions.Logging.Console; using System.IO; using Microsoft.Extensions.Options; using Avalonia; -using Avalonia.Controls.ApplicationLifetimes; namespace ImageCatalog_2; @@ -115,10 +114,6 @@ static class Program static void Main(string[] args) { #if WINDOWS - System.Windows.Forms.Application.SetHighDpiMode(System.Windows.Forms.HighDpiMode.SystemAware); - System.Windows.Forms.Application.EnableVisualStyles(); - System.Windows.Forms.Application.SetCompatibleTextRenderingDefault(false); - AllocConsole(); RedirectConsoleOutput(); #endif @@ -128,59 +123,7 @@ static class Program ServiceProvider = serviceCollection.BuildServiceProvider(); - var serviceProvider = ServiceProvider; - - // Determine UI based on command line. Default: WinForms. Use --wpf for WPF, --avalonia for Avalonia. - bool useWpf = args is not null && Array.Exists(args, a => string.Equals(a, "--wpf", StringComparison.OrdinalIgnoreCase)); - bool useAvalonia = args is not null && Array.Exists(args, a => string.Equals(a, "--avalonia", StringComparison.OrdinalIgnoreCase)); - - if (useAvalonia) - { - BuildAvaloniaApp().StartWithClassicDesktopLifetime(args ?? Array.Empty()); - return; - } - -#if WINDOWS - if (useWpf) - { - var wpfApp = new System.Windows.Application(); - try - { - wpfApp.Resources.MergedDictionaries.Add(new System.Windows.ResourceDictionary { Source = new Uri("pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml") }); - wpfApp.Resources.MergedDictionaries.Add(new System.Windows.ResourceDictionary { Source = new Uri("pack://application:,,,/MahApps.Metro;component/Styles/Fonts.xaml") }); - wpfApp.Resources.MergedDictionaries.Add(new System.Windows.ResourceDictionary { Source = new Uri("pack://application:,,,/MahApps.Metro;component/Styles/Themes/Light.Blue.xaml") }); - - try - { - ControlzEx.Theming.ThemeManager.Current.ChangeTheme(wpfApp, "Light.Blue"); - } - catch - { - // ignore if ThemeManager API isn't present - } - } - catch - { - // If resources fail to load, continue silently - } - - var wpfMain = serviceProvider.GetService(typeof(ImageCatalog_2.MainWindow)) as ImageCatalog_2.MainWindow; - if (wpfMain is not null) - { - wpfApp.Run(wpfMain); - return; - } - - // If WPF was requested but not available, fall through to WinForms. - } - - // Default / fallback to WinForms UI - var mainForm = serviceProvider.GetRequiredService(); - System.Windows.Forms.Application.Run(mainForm); -#else - // On non-Windows, Avalonia is the only available UI BuildAvaloniaApp().StartWithClassicDesktopLifetime(args ?? Array.Empty()); -#endif } private static void ConfigureServices(ServiceCollection services) @@ -233,11 +176,6 @@ static class Program services.AddTransient(); -#if WINDOWS - services.AddTransient(); - services.AddTransient(); -#endif - services.AddSingleton(); services.AddLogging(configure => diff --git a/imagecatalog/ViewModelBase.cs b/imagecatalog/ViewModelBase.cs index f14e44e..b999ba5 100644 --- a/imagecatalog/ViewModelBase.cs +++ b/imagecatalog/ViewModelBase.cs @@ -6,18 +6,12 @@ using System.Runtime.CompilerServices; using System.Text; using System.Threading; using System.Threading.Tasks; -#if WINDOWS -using System.Windows.Forms; -#endif namespace ImageCatalog_2 { public class ViewModelBase : INotifyPropertyChanged { private readonly SynchronizationContext? _synchronizationContext; -#if WINDOWS - private Control? _control; -#endif protected ViewModelBase() { @@ -25,19 +19,8 @@ namespace ImageCatalog_2 _synchronizationContext = SynchronizationContext.Current; } - /// - /// Set a Control to use for thread marshalling in WinForms applications. - /// This is required for proper cross-thread handling with data binding. - /// -#if WINDOWS - public void SetControl(Control control) - { - _control = control; - } -#endif - public event PropertyChangedEventHandler? PropertyChanged; - + // This method is called by the Set accessor of each property. // The CallerMemberName attribute that is applied to the optional propertyName // parameter causes the property name of the caller to be substituted as an argument. @@ -46,22 +29,6 @@ namespace ImageCatalog_2 if (PropertyChanged == null) return; -#if WINDOWS - // If we have a Control reference (WinForms), use Control.Invoke for proper marshalling - if (_control != null) - { - if (_control.InvokeRequired) - { - _control.Invoke(() => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName))); - } - else - { - PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); - } - } - // Fallback to SynchronizationContext if available - else -#endif if (_synchronizationContext != null && SynchronizationContext.Current != _synchronizationContext) { // We're on a different thread, marshal to the UI thread From 25fdb82d2f0c91c2261403a168eb5d668f764bb8 Mon Sep 17 00:00:00 2001 From: MaddoScientisto Date: Sat, 9 May 2026 15:46:41 +0200 Subject: [PATCH 3/3] feat: Add face encoder settings including GPU support, parallelism, and thumbnail options --- Catalog.code-workspace | 3 + .../DataModelCharacterizationTests.cs | 85 ++++ .../AvaloniaViews/FaceAiTabView.axaml | 50 +- .../AvaloniaViews/FaceAiTabView.axaml.cs | 87 +++- imagecatalog/DataModel.cs | 429 ++++++++++++++---- imagecatalog/Models/SettingsDto.cs | 16 + imagecatalog/Program.cs | 30 +- imagecatalog/Properties/InternalsVisibleTo.cs | 3 + .../ViewModels/AiSettingsViewModel.cs | 44 ++ 9 files changed, 603 insertions(+), 144 deletions(-) create mode 100644 imagecatalog/Properties/InternalsVisibleTo.cs diff --git a/Catalog.code-workspace b/Catalog.code-workspace index 36ebf33..15ec3a0 100644 --- a/Catalog.code-workspace +++ b/Catalog.code-workspace @@ -5,6 +5,9 @@ }, { "path": "../AIFotoONLUS" + }, + { + "path": "../../various/regalamiunsorriso" } ], "settings": { diff --git a/MaddoShared.Tests/DataModelCharacterizationTests.cs b/MaddoShared.Tests/DataModelCharacterizationTests.cs index 7ce601e..cba6ce0 100644 --- a/MaddoShared.Tests/DataModelCharacterizationTests.cs +++ b/MaddoShared.Tests/DataModelCharacterizationTests.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Threading.Tasks; using ImageCatalog_2; using ImageCatalog_2.Services; @@ -134,6 +135,61 @@ public class DataModelCharacterizationTests model.FontSize.ShouldBe(42); } + [TestMethod] + public void FaceExecutableFolder_EnablesGpuToggleWhenBothVariantsExist() + { + using var root = new TemporaryDirectory(); + CreateFaceEncoderExecutable(root.Path, "cpu"); + CreateFaceEncoderExecutable(root.Path, "gpu"); + + var model = CreateModel(); + + model.FaceExecutablePath = root.Path; + + model.FaceGpuOptionEnabled.ShouldBeTrue(); + model.UseFaceGpu.ShouldBeFalse(); + } + + [TestMethod] + public void UseFaceGpu_UpdatesUpsampleWhenUsingRecommendedDefault() + { + using var root = new TemporaryDirectory(); + CreateFaceEncoderExecutable(root.Path, "cpu"); + CreateFaceEncoderExecutable(root.Path, "gpu"); + + var model = CreateModel(); + model.FaceExecutablePath = root.Path; + model.FaceUpsample.ShouldBeTrue(); + + model.UseFaceGpu = true; + + model.FaceUpsample.ShouldBeFalse(); + } + + [TestMethod] + public void ResolveConfiguredFaceEncoderExecutablePath_UsesFolderLayoutFromPowerShellScript() + { + using var root = new TemporaryDirectory(); + var cpuExecutable = CreateFaceEncoderExecutable(root.Path, "cpu"); + var gpuExecutable = CreateFaceEncoderExecutable(root.Path, "gpu"); + + DataModel.ResolveConfiguredFaceEncoderExecutablePath(root.Path, useGpu: false).ShouldBe(cpuExecutable); + DataModel.ResolveConfiguredFaceEncoderExecutablePath(root.Path, useGpu: true).ShouldBe(gpuExecutable); + } + + [TestMethod] + public void BuildFaceEncoderOutputPaths_UsesTimestampAndSanitizedFolderName() + { + var timestamp = new DateTime(2026, 5, 9, 14, 30, 45); + var output = DataModel.BuildFaceEncoderOutputPaths( + @"C:\out", + @"C:\images\04 APRILE: gara?", + timestamp); + + output.OutputFilePath.ShouldBe(@"C:\out\face_encodings_20260509_143045_04_APRILE_gara.pkl"); + output.LogFilePath.ShouldBe(@"C:\out\encoder_log_20260509_143045_04_APRILE_gara.txt"); + } + private static DataModel CreateModel( ISettingsService? settingsService = null, ITestService? testService = null) @@ -169,4 +225,33 @@ public class DataModelCharacterizationTests Substitute.For>(), versionProvider: null); } + + private static string CreateFaceEncoderExecutable(string rootPath, string variant) + { + var variantDirectory = Path.Combine(rootPath, $"face_encoder_{variant}"); + Directory.CreateDirectory(variantDirectory); + + var executablePath = Path.Combine(variantDirectory, $"face_encoder_{variant}.exe"); + File.WriteAllText(executablePath, "stub"); + return executablePath; + } + + private sealed class TemporaryDirectory : IDisposable + { + public TemporaryDirectory() + { + Path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), System.IO.Path.GetRandomFileName()); + Directory.CreateDirectory(Path); + } + + public string Path { get; } + + public void Dispose() + { + if (Directory.Exists(Path)) + { + Directory.Delete(Path, recursive: true); + } + } + } } diff --git a/imagecatalog/AvaloniaViews/FaceAiTabView.axaml b/imagecatalog/AvaloniaViews/FaceAiTabView.axaml index 2beb773..134621c 100644 --- a/imagecatalog/AvaloniaViews/FaceAiTabView.axaml +++ b/imagecatalog/AvaloniaViews/FaceAiTabView.axaml @@ -5,16 +5,16 @@ - - + - - + + @@ -28,14 +28,24 @@ + + - + + + + + + + + @@ -49,11 +59,11 @@ - - - @@ -64,9 +74,22 @@ + -