diff --git a/.gitignore b/.gitignore index 7c86da2..7e95a94 100644 --- a/.gitignore +++ b/.gitignore @@ -256,5 +256,4 @@ paket-files/ .idea/ *.sln.iml .vscode/settings.json -tmp/** -TestArtifacts/** \ No newline at end of file +tmp/** \ No newline at end of file diff --git a/Catalog.Communication/Catalog.Communication.csproj b/Catalog.Communication/Catalog.Communication.csproj index 8b3c8df..cb8dc94 100644 --- a/Catalog.Communication/Catalog.Communication.csproj +++ b/Catalog.Communication/Catalog.Communication.csproj @@ -8,10 +8,10 @@ - - - - + + + + diff --git a/Catalog.code-workspace b/Catalog.code-workspace index 93418a7..15ec3a0 100644 --- a/Catalog.code-workspace +++ b/Catalog.code-workspace @@ -11,7 +11,6 @@ } ], "settings": { - "commentTranslate.hover.enabled": false, - "github.copilot.chat.otel.dbSpanExporter.enabled": true + "commentTranslate.hover.enabled": false } } \ No newline at end of file diff --git a/MaddoShared.Benchmarks/MaddoShared.Benchmarks.csproj b/MaddoShared.Benchmarks/MaddoShared.Benchmarks.csproj index 56b8684..b712ff3 100644 --- a/MaddoShared.Benchmarks/MaddoShared.Benchmarks.csproj +++ b/MaddoShared.Benchmarks/MaddoShared.Benchmarks.csproj @@ -10,8 +10,8 @@ - - + + diff --git a/MaddoShared.ImageSharpTests/MaddoShared.ImageSharpTests.csproj b/MaddoShared.ImageSharpTests/MaddoShared.ImageSharpTests.csproj index 6a7b6fe..52f36f6 100644 --- a/MaddoShared.ImageSharpTests/MaddoShared.ImageSharpTests.csproj +++ b/MaddoShared.ImageSharpTests/MaddoShared.ImageSharpTests.csproj @@ -9,15 +9,15 @@ - - - + + + - - - - + + + + diff --git a/MaddoShared.Tests/DataModelCharacterizationTests.cs b/MaddoShared.Tests/DataModelCharacterizationTests.cs index ac09cb8..0a94ae9 100644 --- a/MaddoShared.Tests/DataModelCharacterizationTests.cs +++ b/MaddoShared.Tests/DataModelCharacterizationTests.cs @@ -96,45 +96,6 @@ public class DataModelCharacterizationTests model.DestinationPath.ShouldBe($"C:{System.IO.Path.DirectorySeparatorChar}output{System.IO.Path.DirectorySeparatorChar}"); } - [TestMethod] - public void DestinationPathChange_UpdatesAiCsvFileNameToMatchFinalFolderSegment() - { - var model = CreateModel(); - model.CsvOutputPath = @"K:\various\catalogtest\aioutput\test2.csv"; - - model.DestinationPath = @"K:\various\catalogtest\Dest\03.KM_8_A\"; - - model.CsvOutputPath.ShouldBe(@"K:\various\catalogtest\aioutput\03.KM_8_A.csv"); - } - - [TestMethod] - public async Task ConfirmAiCsvOverwriteIfNeededAsync_CanCancelWhenCsvAlreadyExists() - { - using var tempDirectory = new TemporaryDirectory(); - var csvPath = Path.Combine(tempDirectory.Path, "existing.csv"); - File.WriteAllText(csvPath, "existing"); - - var model = CreateModel(); - model.CsvOutputPath = csvPath; - - string? requestedTitle = null; - string? requestedMessage = null; - model.ConfirmAiCsvOverwriteAsync = (title, message) => - { - requestedTitle = title; - requestedMessage = message; - return Task.FromResult(false); - }; - - var shouldContinue = await model.ConfirmAiCsvOverwriteIfNeededAsync(); - - shouldContinue.ShouldBeFalse(); - requestedTitle.ShouldBe("File CSV gia esistente"); - requestedMessage.ShouldNotBeNull(); - requestedMessage.ShouldContain("Vuoi sovrascriverlo?"); - requestedMessage.ShouldContain(csvPath); - } - [TestMethod] public void AiChildChange_RaisesDataModelPropertyChanged() { diff --git a/MaddoShared.Tests/MaddoShared.Tests.csproj b/MaddoShared.Tests/MaddoShared.Tests.csproj index 2c8d370..d8936a9 100644 --- a/MaddoShared.Tests/MaddoShared.Tests.csproj +++ b/MaddoShared.Tests/MaddoShared.Tests.csproj @@ -17,7 +17,7 @@ - + diff --git a/MaddoShared.Tests/PickerPreferenceServiceTests.cs b/MaddoShared.Tests/PickerPreferenceServiceTests.cs deleted file mode 100644 index f0fe3d0..0000000 --- a/MaddoShared.Tests/PickerPreferenceServiceTests.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using System.IO; -using ImageCatalog; -using ImageCatalog_2.Services; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using Shouldly; - -namespace MaddoShared.Tests; - -[TestClass] -public class PickerPreferenceServiceTests -{ - [TestMethod] - public void RememberValue_PersistsExactFilePath() - { - using var tempDirectory = new TemporaryDirectory(); - var preferencesFile = Path.Combine(tempDirectory.Path, "userprefs.xml"); - var service = new PickerPreferenceService(new ParametriSetup(preferencesFile)); - var settingsFile = Path.Combine(tempDirectory.Path, "nested", "settings.xml"); - - service.RememberValue(PickerPreferenceKeys.LastSettingsFile, settingsFile); - - service.GetRememberedValue(PickerPreferenceKeys.LastSettingsFile).ShouldBe(settingsFile); - } - - [TestMethod] - public void ForgetValue_RemovesStoredPreference() - { - using var tempDirectory = new TemporaryDirectory(); - var preferencesFile = Path.Combine(tempDirectory.Path, "userprefs.xml"); - var service = new PickerPreferenceService(new ParametriSetup(preferencesFile)); - - service.RememberValue(PickerPreferenceKeys.LastSettingsFile, Path.Combine(tempDirectory.Path, "settings.xml")); - - service.ForgetValue(PickerPreferenceKeys.LastSettingsFile); - - service.GetRememberedValue(PickerPreferenceKeys.LastSettingsFile).ShouldBeNull(); - } - - 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); - } - } - } -} \ No newline at end of file diff --git a/MaddoShared.Tests/Test1.cs b/MaddoShared.Tests/Test1.cs index da8e098..cd00c92 100644 --- a/MaddoShared.Tests/Test1.cs +++ b/MaddoShared.Tests/Test1.cs @@ -1,48 +1,11 @@ -using ImageCatalog_2.Services; -using ImageCatalog_2.Models; -using Shouldly; - -namespace MaddoShared.Tests; - -[TestClass] -public sealed class AiExtractionServiceCsvTests +namespace MaddoShared.Tests { - [TestMethod] - public void WriteCsvOutput_UsesLegacyCompatibleHeaderAndFilenameColumn() + [TestClass] + public sealed class Test1 { - using var tempDir = new TempDirectory(); - var csvPath = Path.Combine(tempDir.Path, "ocr.csv"); - - AiExtractionService.WriteCsvOutput( - csvPath, - [ - new AiResultItem { Path = @"C:\images\IMG_7146.JPG", Text = "43,84,61" }, - new AiResultItem { Path = @"C:\images\IMG_7207.JPG", Text = "a\"b" } - ]); - - var lines = File.ReadAllLines(csvPath); - - lines[0].ShouldBe("filename,text"); - lines[1].ShouldBe("\"IMG_7146.JPG\",\"43,84,61\""); - lines[2].ShouldBe("\"IMG_7207.JPG\",\"a\"\"b\""); - } - - private sealed class TempDirectory : IDisposable - { - public TempDirectory() + [TestMethod] + public void TestMethod1() { - 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/MaddoShared/MaddoShared.csproj b/MaddoShared/MaddoShared.csproj index 7e3998c..b18b80a 100644 --- a/MaddoShared/MaddoShared.csproj +++ b/MaddoShared/MaddoShared.csproj @@ -2,8 +2,6 @@ net10.0 Library - enable - enable false x64 diff --git a/imagecatalog/AvaloniaApp.axaml b/imagecatalog/AvaloniaApp.axaml index fc57b97..97bd721 100644 --- a/imagecatalog/AvaloniaApp.axaml +++ b/imagecatalog/AvaloniaApp.axaml @@ -100,7 +100,7 @@ - + diff --git a/imagecatalog/AvaloniaMainWindow.axaml b/imagecatalog/AvaloniaMainWindow.axaml index c01f37f..ba21b00 100644 --- a/imagecatalog/AvaloniaMainWindow.axaml +++ b/imagecatalog/AvaloniaMainWindow.axaml @@ -5,7 +5,6 @@ xmlns:views="clr-namespace:ImageCatalog_2.AvaloniaViews" xmlns:iconPacks="https://github.com/MahApps/IconPacks.Avalonia" x:Class="ImageCatalog_2.AvaloniaMainWindow" - x:CompileBindings="False" mc:Ignorable="d" Title="Image Catalog - Avalonia" Height="540" Width="800"> @@ -107,7 +106,7 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/imagecatalog/AvaloniaViews/AiTabView.axaml.cs b/imagecatalog/AvaloniaViews/AiTabView.axaml.cs index f56e327..f89ded7 100644 --- a/imagecatalog/AvaloniaViews/AiTabView.axaml.cs +++ b/imagecatalog/AvaloniaViews/AiTabView.axaml.cs @@ -1,4 +1,8 @@ using Avalonia.Controls; +using Avalonia.Interactivity; +using System.Diagnostics; +using System.IO; + namespace ImageCatalog_2.AvaloniaViews; public partial class AiTabView : Avalonia.Controls.UserControl @@ -7,4 +11,56 @@ public partial class AiTabView : Avalonia.Controls.UserControl { InitializeComponent(); } + + private void OpenModelsFolder_Click(object? sender, RoutedEventArgs e) + { + if (DataContext is DataModel model) + { + OpenInExplorer(model.ModelsFolderPath); + } + } + + private void OpenCsvOutputFolder_Click(object? sender, RoutedEventArgs e) + { + if (DataContext is not DataModel model) + { + return; + } + + var directory = Path.GetDirectoryName(model.CsvOutputPath); + OpenInExplorer(string.IsNullOrWhiteSpace(directory) ? model.CsvOutputPath : directory); + } + + private void OpenAiDestinationFolder_Click(object? sender, RoutedEventArgs e) + { + if (DataContext is DataModel model) + { + OpenInExplorer(model.DestinationPath); + } + } + + private static void OpenInExplorer(string? path) + { + if (string.IsNullOrWhiteSpace(path)) + { + return; + } + + var normalizedPath = path.Trim().Trim('"'); + try + { + if (File.Exists(normalizedPath)) + { + Process.Start("explorer.exe", $"/select,\"{normalizedPath}\""); + } + else if (Directory.Exists(normalizedPath)) + { + Process.Start(new ProcessStartInfo { FileName = normalizedPath, UseShellExecute = true }); + } + } + catch + { + // Ignore failures when opening Explorer. + } + } } diff --git a/imagecatalog/AvaloniaViews/FaceAiTabView.axaml b/imagecatalog/AvaloniaViews/FaceAiTabView.axaml index dc1aed0..134621c 100644 --- a/imagecatalog/AvaloniaViews/FaceAiTabView.axaml +++ b/imagecatalog/AvaloniaViews/FaceAiTabView.axaml @@ -1,291 +1,109 @@ - - - + x:Class="ImageCatalog_2.AvaloniaViews.FaceAiTabView"> + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - + + + - - + + + + + + + - - - - - - - - - - - - - - - - + + + + + + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/imagecatalog/AvaloniaViews/GeneralTabView.axaml.cs b/imagecatalog/AvaloniaViews/GeneralTabView.axaml.cs index 9670c2a..cdcbec9 100644 --- a/imagecatalog/AvaloniaViews/GeneralTabView.axaml.cs +++ b/imagecatalog/AvaloniaViews/GeneralTabView.axaml.cs @@ -1,4 +1,9 @@ using Avalonia.Controls; +using Avalonia.Interactivity; +using System; +using System.Diagnostics; +using System.IO; + namespace ImageCatalog_2.AvaloniaViews; public partial class GeneralTabView : Avalonia.Controls.UserControl @@ -7,4 +12,45 @@ public partial class GeneralTabView : Avalonia.Controls.UserControl { InitializeComponent(); } + + private void OpenSourceFolder_Click(object? sender, RoutedEventArgs e) + { + if (DataContext is DataModel model) + { + OpenInExplorer(model.SourcePath); + } + } + + private void OpenDestinationFolder_Click(object? sender, RoutedEventArgs e) + { + if (DataContext is DataModel model) + { + OpenInExplorer(model.DestinationPath); + } + } + + private static void OpenInExplorer(string? path) + { + if (string.IsNullOrWhiteSpace(path)) + { + return; + } + + var normalizedPath = path.Trim().Trim('"'); + try + { + if (File.Exists(normalizedPath)) + { + Process.Start("explorer.exe", $"/select,\"{normalizedPath}\""); + } + else if (Directory.Exists(normalizedPath)) + { + Process.Start(new ProcessStartInfo { FileName = normalizedPath, UseShellExecute = true }); + } + } + catch + { + // Ignore failures when opening Explorer. + } + } } diff --git a/imagecatalog/AvaloniaViews/RaceUploadTabView.axaml b/imagecatalog/AvaloniaViews/RaceUploadTabView.axaml index 1c9a6a8..cc3b507 100644 --- a/imagecatalog/AvaloniaViews/RaceUploadTabView.axaml +++ b/imagecatalog/AvaloniaViews/RaceUploadTabView.axaml @@ -7,7 +7,7 @@ - + @@ -16,7 +16,7 @@ - + @@ -49,7 +49,7 @@ - + diff --git a/imagecatalog/Controls/PathPickerField.axaml b/imagecatalog/Controls/PathPickerField.axaml deleted file mode 100644 index dfa6948..0000000 --- a/imagecatalog/Controls/PathPickerField.axaml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/imagecatalog/Controls/PathPickerField.axaml.cs b/imagecatalog/Controls/PathPickerField.axaml.cs deleted file mode 100644 index 6ec2538..0000000 --- a/imagecatalog/Controls/PathPickerField.axaml.cs +++ /dev/null @@ -1,285 +0,0 @@ -using Avalonia; -using Avalonia.Controls; -using Avalonia.Data; -using Avalonia.Platform.Storage; -using ImageCatalog_2.Services; -using Microsoft.Extensions.DependencyInjection; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -namespace ImageCatalog_2.Controls; - -public partial class PathPickerField : UserControl -{ - private readonly PickerPreferenceService _pickerPreferenceService; - - public static readonly StyledProperty LabelProperty = - AvaloniaProperty.Register(nameof(Label), string.Empty); - - public static readonly StyledProperty TextProperty = - AvaloniaProperty.Register(nameof(Text), string.Empty, defaultBindingMode: BindingMode.TwoWay); - - public static readonly StyledProperty WatermarkProperty = - AvaloniaProperty.Register(nameof(Watermark), string.Empty); - - public static readonly StyledProperty PreferenceKeyProperty = - AvaloniaProperty.Register(nameof(PreferenceKey), string.Empty); - - public static readonly StyledProperty PickerTitleProperty = - AvaloniaProperty.Register(nameof(PickerTitle), string.Empty); - - public static readonly StyledProperty FileTypeNameProperty = - AvaloniaProperty.Register(nameof(FileTypeName), string.Empty); - - public static readonly StyledProperty FilePatternsProperty = - AvaloniaProperty.Register(nameof(FilePatterns), string.Empty); - - public static readonly StyledProperty DefaultExtensionProperty = - AvaloniaProperty.Register(nameof(DefaultExtension), string.Empty); - - public static readonly StyledProperty PickerModeProperty = - AvaloniaProperty.Register(nameof(PickerMode), PathPickerSelectionMode.Folder); - - public static readonly StyledProperty IsTextReadOnlyProperty = - AvaloniaProperty.Register(nameof(IsTextReadOnly), false); - - public static readonly StyledProperty ShowPickerButtonProperty = - AvaloniaProperty.Register(nameof(ShowPickerButton), true); - - public static readonly StyledProperty AppendDirectorySeparatorProperty = - AvaloniaProperty.Register(nameof(AppendDirectorySeparator), false); - - public PathPickerField() - { - _pickerPreferenceService = Program.ServiceProvider.GetRequiredService(); - InitializeComponent(); - } - - public string Label - { - get => GetValue(LabelProperty); - set => SetValue(LabelProperty, value); - } - - public string Text - { - get => GetValue(TextProperty); - set => SetValue(TextProperty, value); - } - - public string Watermark - { - get => GetValue(WatermarkProperty); - set => SetValue(WatermarkProperty, value); - } - - public string PreferenceKey - { - get => GetValue(PreferenceKeyProperty); - set => SetValue(PreferenceKeyProperty, value); - } - - public string PickerTitle - { - get => GetValue(PickerTitleProperty); - set => SetValue(PickerTitleProperty, value); - } - - public string FileTypeName - { - get => GetValue(FileTypeNameProperty); - set => SetValue(FileTypeNameProperty, value); - } - - public string FilePatterns - { - get => GetValue(FilePatternsProperty); - set => SetValue(FilePatternsProperty, value); - } - - public string DefaultExtension - { - get => GetValue(DefaultExtensionProperty); - set => SetValue(DefaultExtensionProperty, value); - } - - public PathPickerSelectionMode PickerMode - { - get => GetValue(PickerModeProperty); - set => SetValue(PickerModeProperty, value); - } - - public bool IsTextReadOnly - { - get => GetValue(IsTextReadOnlyProperty); - set => SetValue(IsTextReadOnlyProperty, value); - } - - public bool ShowPickerButton - { - get => GetValue(ShowPickerButtonProperty); - set => SetValue(ShowPickerButtonProperty, value); - } - - public bool AppendDirectorySeparator - { - get => GetValue(AppendDirectorySeparatorProperty); - set => SetValue(AppendDirectorySeparatorProperty, value); - } - - private async void PickPath_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - var topLevel = TopLevel.GetTopLevel(this); - var storageProvider = topLevel?.StorageProvider; - if (storageProvider is null) - { - return; - } - - var selectedPath = await PickPathAsync(storageProvider); - if (string.IsNullOrWhiteSpace(selectedPath)) - { - return; - } - - Text = selectedPath; - - if (!string.IsNullOrWhiteSpace(PreferenceKey)) - { - _pickerPreferenceService.RememberPath(PreferenceKey, selectedPath); - } - } - - private void OpenPath_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - PathShellService.OpenInExplorer(Text); - } - - private async Task PickPathAsync(IStorageProvider storageProvider) - { - var suggestedStartLocation = await TryGetSuggestedStartLocationAsync(storageProvider); - var pickerTitle = ResolvePickerTitle(); - - switch (PickerMode) - { - case PathPickerSelectionMode.Folder: - { - var folders = await storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions - { - Title = pickerTitle, - SuggestedStartLocation = suggestedStartLocation - }); - - return folders.Count == 0 - ? null - : NormalizeSelectedPath(folders[0].Path.LocalPath); - } - case PathPickerSelectionMode.OpenFile: - { - var files = await storageProvider.OpenFilePickerAsync(new FilePickerOpenOptions - { - Title = pickerTitle, - SuggestedStartLocation = suggestedStartLocation, - FileTypeFilter = BuildFileTypes() - }); - - return files.Count == 0 - ? null - : files[0].Path.LocalPath; - } - case PathPickerSelectionMode.SaveFile: - { - var file = await storageProvider.SaveFilePickerAsync(new FilePickerSaveOptions - { - Title = pickerTitle, - SuggestedStartLocation = suggestedStartLocation, - DefaultExtension = NormalizeDefaultExtension(DefaultExtension), - FileTypeChoices = BuildFileTypes() - }); - - return file?.Path.LocalPath; - } - default: - return null; - } - } - - private async Task TryGetSuggestedStartLocationAsync(IStorageProvider storageProvider) - { - if (string.IsNullOrWhiteSpace(PreferenceKey)) - { - return null; - } - - return await _pickerPreferenceService.TryGetStartFolderAsync(storageProvider, PreferenceKey, Text); - } - - private IReadOnlyList BuildFileTypes() - { - var patterns = (FilePatterns ?? string.Empty) - .Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - - if (patterns.Length == 0) - { - return Array.Empty(); - } - - var fileTypeName = string.IsNullOrWhiteSpace(FileTypeName) - ? "File" - : FileTypeName.Trim(); - - return - [ - new FilePickerFileType(fileTypeName) - { - Patterns = patterns.ToList() - } - ]; - } - - private string ResolvePickerTitle() - { - if (!string.IsNullOrWhiteSpace(PickerTitle)) - { - return PickerTitle; - } - - var cleanedLabel = string.IsNullOrWhiteSpace(Label) - ? "percorso" - : Label.Trim().TrimEnd(':'); - - return PickerMode == PathPickerSelectionMode.SaveFile - ? $"Salva {cleanedLabel.ToLowerInvariant()}" - : $"Seleziona {cleanedLabel.ToLowerInvariant()}"; - } - - private string NormalizeSelectedPath(string selectedPath) - { - if (PickerMode != PathPickerSelectionMode.Folder || !AppendDirectorySeparator) - { - return selectedPath; - } - - if (string.IsNullOrWhiteSpace(selectedPath)) - { - return string.Empty; - } - - return selectedPath.EndsWith(Path.DirectorySeparatorChar) - || selectedPath.EndsWith(Path.AltDirectorySeparatorChar) - ? selectedPath - : selectedPath + Path.DirectorySeparatorChar; - } - - private static string NormalizeDefaultExtension(string extension) - { - if (string.IsNullOrWhiteSpace(extension)) - { - return string.Empty; - } - - return extension.Trim().TrimStart('.'); - } -} \ No newline at end of file diff --git a/imagecatalog/Controls/PathPickerSelectionMode.cs b/imagecatalog/Controls/PathPickerSelectionMode.cs deleted file mode 100644 index 27d456e..0000000 --- a/imagecatalog/Controls/PathPickerSelectionMode.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace ImageCatalog_2.Controls; - -public enum PathPickerSelectionMode -{ - Folder, - OpenFile, - SaveFile -} \ No newline at end of file diff --git a/imagecatalog/Converters/FilePathToBitmapConverter.cs b/imagecatalog/Converters/FilePathToBitmapConverter.cs deleted file mode 100644 index 614897d..0000000 --- a/imagecatalog/Converters/FilePathToBitmapConverter.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Globalization; -using Avalonia.Data.Converters; -using Avalonia.Media.Imaging; - -namespace ImageCatalog_2.Converters; - -public sealed class FilePathToBitmapConverter : IValueConverter -{ - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - if (value is not string path || string.IsNullOrWhiteSpace(path)) - { - return null; - } - - try - { - return new Bitmap(path); - } - catch - { - return null; - } - } - - public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - throw new NotSupportedException(); - } -} diff --git a/imagecatalog/DataModel.cs b/imagecatalog/DataModel.cs index d4a3dc4..66ce8d4 100644 --- a/imagecatalog/DataModel.cs +++ b/imagecatalog/DataModel.cs @@ -13,16 +13,13 @@ using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Text; -using System.Globalization; using System.Threading; using System.Threading.Tasks; using System.Windows.Input; using AIFotoONLUS.Core; -using System.Text.RegularExpressions; using AutoMapper; using MaddoShared; using Microsoft.Extensions.Logging; -using System.Collections.ObjectModel; namespace ImageCatalog_2 { @@ -44,8 +41,6 @@ namespace ImageCatalog_2 public ICommand StartAiCommand { get; } public ICommand StartFaceEncoderCommand { get; } public ICommand StopFaceEncoderCommand { get; } - public ICommand StartFaceMatcherCommand { get; } - public ICommand StopFaceMatcherCommand { get; } private readonly ITestService _service; private readonly ILogger _logger; @@ -62,28 +57,13 @@ namespace ImageCatalog_2 private readonly IMapper _mapper; private readonly AsyncCommand _startFaceEncoderCommand; private readonly AsyncCommand _stopFaceEncoderCommand; - private readonly AsyncCommand _startFaceMatcherCommand; - private readonly AsyncCommand _stopFaceMatcherCommand; private readonly object _faceEncoderProcessLock = new(); - private readonly object _faceMatcherProcessLock = new(); private Process? _faceEncoderProcess; - private Process? _faceMatcherProcess; private CancellationTokenSource? _faceEncoderWatcherTokenSource; private Task? _faceEncoderWatcherTask; private CancellationTokenSource? _faceEncoderLogWatcherTokenSource; private Task? _faceEncoderLogWatcherTask; - private CancellationTokenSource? _faceMatcherWatcherTokenSource; - private Task? _faceMatcherWatcherTask; - private CancellationTokenSource? _faceMatcherLogWatcherTokenSource; - private Task? _faceMatcherLogWatcherTask; private bool _hasStartedFaceEncoderInSession; - private bool _hasStartedFaceMatcherInSession; - private int _numberAiGpuRefreshVersion; - private volatile bool _numberAiGpuValidationPending; - - private sealed record ParsedFaceMatcherRow(string PhotoId, double? Score, string RawRow, string DebugSummary); - - private const string AiCsvOverwriteDialogTitle = "File CSV gia esistente"; // ComboBox collections public List AvailableFonts { get; } @@ -125,12 +105,8 @@ namespace ImageCatalog_2 StartAiCommand = new AsyncCommand(StartAiAsync); _startFaceEncoderCommand = new AsyncCommand(RunFaceEncoderAsync, CanRunFaceEncoder); _stopFaceEncoderCommand = new AsyncCommand(() => StopFaceEncoderAsync("Arresto richiesto dall'utente."), CanStopFaceEncoder); - _startFaceMatcherCommand = new AsyncCommand(RunFaceMatcherAsync, CanRunFaceMatcher); - _stopFaceMatcherCommand = new AsyncCommand(() => StopFaceMatcherAsync("Arresto richiesto dall'utente."), CanStopFaceMatcher); StartFaceEncoderCommand = _startFaceEncoderCommand; StopFaceEncoderCommand = _stopFaceEncoderCommand; - StartFaceMatcherCommand = _startFaceMatcherCommand; - StopFaceMatcherCommand = _stopFaceMatcherCommand; SelectSourceFolderCommand = new RelayCommand(SelectSourceFolder); SelectDestinationFolderCommand = new RelayCommand(SelectDestinationFolder); @@ -142,18 +118,12 @@ namespace ImageCatalog_2 // Load available fonts AvailableFonts = LoadAvailableFonts(); - QueueRefreshNumberAiGpuCapabilities(); + RefreshNumberAiGpuCapabilities(); RefreshFaceExecutableCapabilities(); } private async Task StartAiAsync() { - if (!await ConfirmAiCsvOverwriteIfNeededAsync().ConfigureAwait(false)) - { - await InvokeOnUiThreadAsync(() => NumberAiStatsSummary = "OCR annullato.").ConfigureAwait(false); - return; - } - MainToken = new CancellationTokenSource(); try { @@ -168,7 +138,7 @@ namespace ImageCatalog_2 _logger.LogError(ex, "AI extraction failed"); if (UseNumberAiGpu) { - QueueRefreshNumberAiGpuCapabilities(); + RefreshNumberAiGpuCapabilities(); } await InvokeOnUiThreadAsync(() => NumberAiStatsSummary = $"Errore OCR: {ex.GetBaseException().Message}").ConfigureAwait(false); @@ -266,7 +236,7 @@ namespace ImageCatalog_2 set { _ai.ModelsFolderPath = value; - QueueRefreshNumberAiGpuCapabilities(); + RefreshNumberAiGpuCapabilities(); } } @@ -276,8 +246,6 @@ namespace ImageCatalog_2 set => _ai.CsvOutputPath = value; } - public Func>? ConfirmAiCsvOverwriteAsync { get; set; } - public bool UseNumberAiGpu { get => _ai.UseNumberAiGpu; @@ -391,62 +359,6 @@ namespace ImageCatalog_2 private set => _ai.FaceCommandOutput = value; } - public string FaceMatcherExecutablePath - { - get => _ai.FaceMatcherExecutablePath; - set => _ai.FaceMatcherExecutablePath = value ?? string.Empty; - } - - public string FaceMatcherEncodingsPath - { - get => _ai.FaceMatcherEncodingsPath; - set => _ai.FaceMatcherEncodingsPath = value ?? string.Empty; - } - - public string FaceMatcherOutputPath - { - get => _ai.FaceMatcherOutputPath; - set => _ai.FaceMatcherOutputPath = value ?? string.Empty; - } - - public string FaceMatcherLogPath - { - get => _ai.FaceMatcherLogPath; - set => _ai.FaceMatcherLogPath = value ?? string.Empty; - } - - public double FaceMatcherTolerance - { - get => _ai.FaceMatcherTolerance; - set => _ai.FaceMatcherTolerance = NormalizeFaceMatcherTolerance(value); - } - - public string FaceMatcherSelectedImagePath - { - get => _ai.FaceMatcherSelectedImagePath; - set => _ai.FaceMatcherSelectedImagePath = value ?? string.Empty; - } - - public bool IsFaceMatcherRunning - { - get => _ai.IsFaceMatcherRunning; - private set => _ai.IsFaceMatcherRunning = value; - } - - public string FaceMatcherStatusMessage - { - get => _ai.FaceMatcherStatusMessage; - private set => _ai.FaceMatcherStatusMessage = value; - } - - public string FaceMatcherCommandOutput - { - get => _ai.FaceMatcherCommandOutput; - private set => _ai.FaceMatcherCommandOutput = value; - } - - public System.Collections.ObjectModel.ObservableCollection FaceMatcherResults => _ai.FaceMatcherResults; - // Race upload settings public string ApiLogin { @@ -652,11 +564,6 @@ namespace ImageCatalog_2 return; } - if (string.Equals(e.PropertyName, nameof(PathSettingsViewModel.DestinationPath), StringComparison.Ordinal)) - { - UpdateAiCsvOutputPathForDestination(); - } - NotifyPropertyChanged(e.PropertyName); } @@ -669,7 +576,6 @@ namespace ImageCatalog_2 NotifyPropertyChanged(e.PropertyName); UpdateFaceEncoderCommandStates(); - UpdateFaceMatcherCommandStates(); } private void OnRaceUploadPropertyChanged(object? sender, PropertyChangedEventArgs e) @@ -1622,12 +1528,6 @@ namespace ImageCatalog_2 _stopFaceEncoderCommand?.RaiseCanExecuteChanged(); } - private void UpdateFaceMatcherCommandStates() - { - _startFaceMatcherCommand?.RaiseCanExecuteChanged(); - _stopFaceMatcherCommand?.RaiseCanExecuteChanged(); - } - private async Task RunFaceEncoderAsync() { if (IsFaceEncoderRunning) @@ -1793,252 +1693,6 @@ namespace ImageCatalog_2 } } - private bool CanRunFaceMatcher() - { - return !IsFaceMatcherRunning; - } - - private bool CanStopFaceMatcher() - { - return IsFaceMatcherRunning; - } - - private async Task RunFaceMatcherAsync() - { - if (IsFaceMatcherRunning) - { - FaceMatcherStatusMessage = "Face matcher gia in esecuzione."; - return; - } - - var executablePath = ResolveConfiguredFaceMatcherExecutablePath(NormalizeFilePathArgument(FaceMatcherExecutablePath), NormalizeFilePathArgument(FaceExecutablePath)); - if (string.IsNullOrWhiteSpace(executablePath) || !File.Exists(executablePath)) - { - FaceMatcherStatusMessage = "Percorso face_matcher.exe non valido."; - return; - } - - var searchImagePath = NormalizeFilePathArgument(FaceMatcherSelectedImagePath); - if (string.IsNullOrWhiteSpace(searchImagePath) || !File.Exists(searchImagePath)) - { - FaceMatcherStatusMessage = "Seleziona un'immagine valida per il match."; - return; - } - - var encodingsPath = ResolveConfiguredFaceMatcherEncodingsPath(NormalizeFilePathArgument(FaceMatcherEncodingsPath), NormalizeDirectoryPathArgument(FaceOutputFolderPath)); - if (string.IsNullOrWhiteSpace(encodingsPath) || !File.Exists(encodingsPath)) - { - FaceMatcherStatusMessage = "File encodings .pkl non trovato."; - return; - } - - var fallbackOutputRoot = ResolveFaceMatcherFallbackOutputRoot(NormalizeFilePathArgument(FaceMatcherOutputPath), NormalizeFilePathArgument(FaceMatcherLogPath), NormalizeDirectoryPathArgument(FaceOutputFolderPath), executablePath); - var outputPaths = ResolveFaceMatcherOutputPaths(FaceMatcherOutputPath, FaceMatcherLogPath, fallbackOutputRoot, searchImagePath, DateTime.Now); - - try - { - Directory.CreateDirectory(Path.GetDirectoryName(outputPaths.CsvPath) ?? fallbackOutputRoot); - Directory.CreateDirectory(Path.GetDirectoryName(outputPaths.LogPath) ?? fallbackOutputRoot); - } - catch (Exception ex) - { - _logger.LogError(ex, "Unable to create face matcher output directory."); - FaceMatcherStatusMessage = "Impossibile creare cartelle output/log del matcher."; - return; - } - - var tolerance = NormalizeFaceMatcherTolerance(FaceMatcherTolerance); - FaceMatcherExecutablePath = executablePath; - FaceMatcherEncodingsPath = encodingsPath; - FaceMatcherOutputPath = outputPaths.CsvPath; - FaceMatcherLogPath = outputPaths.LogPath; - - await InvokeOnUiThreadAsync(() => - { - FaceMatcherResults.Clear(); - FaceMatcherCommandOutput = string.Empty; - FaceMatcherStatusMessage = "Esecuzione face matcher in corso..."; - }).ConfigureAwait(false); - - var transcriptLines = new StringBuilder(); - var outputLines = new StringBuilder(); - var errorLines = new StringBuilder(); - - try - { - var processStartInfo = new ProcessStartInfo - { - FileName = executablePath, - WorkingDirectory = Path.GetDirectoryName(executablePath) ?? Environment.CurrentDirectory, - UseShellExecute = false, - RedirectStandardOutput = false, - RedirectStandardError = false, - RedirectStandardInput = false, - CreateNoWindow = true, - }; - - processStartInfo.Environment["PYTHONUTF8"] = "1"; - processStartInfo.Environment["PYTHONIOENCODING"] = "utf-8"; - - processStartInfo.ArgumentList.Add("--image"); - processStartInfo.ArgumentList.Add(searchImagePath); - processStartInfo.ArgumentList.Add("--encodings"); - processStartInfo.ArgumentList.Add(encodingsPath); - processStartInfo.ArgumentList.Add("--out"); - processStartInfo.ArgumentList.Add(outputPaths.CsvPath); - processStartInfo.ArgumentList.Add("--log"); - processStartInfo.ArgumentList.Add(outputPaths.LogPath); - processStartInfo.ArgumentList.Add("--tolerance"); - processStartInfo.ArgumentList.Add(tolerance.ToString("0.##", CultureInfo.InvariantCulture)); - - using var process = new Process { StartInfo = processStartInfo, EnableRaisingEvents = true }; - process.Exited += (_, _) => - { - _ = InvokeOnUiThreadAsync(() => - { - if (!ComputeIsFaceMatcherRunning()) - { - IsFaceMatcherRunning = false; - } - }); - }; - - if (!process.Start()) - { - throw new InvalidOperationException("Avvio face matcher fallito."); - } - - _hasStartedFaceMatcherInSession = true; - EnsureFaceMatcherWatcherStarted(); - TrackFaceMatcherProcess(process); - await InvokeOnUiThreadAsync(() => IsFaceMatcherRunning = true).ConfigureAwait(false); - - StartFaceMatcherLogWatcher(outputPaths.LogPath, outputLines, transcriptLines); - await process.WaitForExitAsync().ConfigureAwait(false); - - var isNoFacesRun = process.ExitCode == 1 && await LogIndicatesNoFacesAsync(outputPaths.LogPath).ConfigureAwait(false); - if (process.ExitCode == 0 || isNoFacesRun) - { - var parsedRows = await ParseFaceMatcherCsvAsync(outputPaths.CsvPath, outputPaths.LogPath).ConfigureAwait(false); - var resolvedPaths = ResolveDestinationImagesByFileName(DestinationPath, parsedRows.Select(row => row.PhotoId)); - await InvokeOnUiThreadAsync(() => - { - FaceMatcherResults.Clear(); - foreach (var row in parsedRows) - { - resolvedPaths.TryGetValue(row.PhotoId, out var candidates); - candidates ??= []; - - FaceMatcherResults.Add(new FaceMatcherResultItem - { - PhotoId = row.PhotoId, - Score = row.Score, - ResolvedImagePath = candidates.FirstOrDefault() ?? string.Empty, - CandidateCount = candidates.Count, - RawRow = row.RawRow, - DebugSummary = row.DebugSummary, - SearchImagePath = searchImagePath, - CsvPath = outputPaths.CsvPath, - LogPath = outputPaths.LogPath - }); - } - }).ConfigureAwait(false); - } - - var summary = BuildFaceMatcherSummary(process.ExitCode, processStartInfo, outputPaths.CsvPath, outputPaths.LogPath, outputLines, errorLines, FaceMatcherResults.Count); - await InvokeOnUiThreadAsync(() => - { - FaceMatcherCommandOutput = string.IsNullOrWhiteSpace(FaceMatcherCommandOutput) - ? summary - : $"{FaceMatcherCommandOutput.TrimEnd()}\n\n{summary}"; - FaceMatcherStatusMessage = process.ExitCode switch - { - 0 when FaceMatcherResults.Count > 0 => $"Face matcher completato: {FaceMatcherResults.Count} match.", - 0 => "Face matcher completato senza match.", - 1 when isNoFacesRun => "Face matcher completato: nessun volto rilevato nell'immagine di ricerca.", - _ => $"Face matcher terminato con errore (code {process.ExitCode})." - }; - }).ConfigureAwait(false); - } - catch (Exception ex) - { - Console.Error.WriteLine(ex); - _logger.LogError(ex, "Face matcher execution failed."); - await InvokeOnUiThreadAsync(() => - { - FaceMatcherCommandOutput = ex.ToString(); - FaceMatcherStatusMessage = "Errore durante esecuzione face matcher."; - }).ConfigureAwait(false); - } - finally - { - await StopFaceMatcherLogWatcherAsync().ConfigureAwait(false); - ClearTrackedFaceMatcherProcess(); - await InvokeOnUiThreadAsync(() => IsFaceMatcherRunning = ComputeIsFaceMatcherRunning()).ConfigureAwait(false); - } - } - - public async Task StopFaceMatcherAsync(string reason, bool waitForExit = true) - { - var trackedProcess = GetTrackedFaceMatcherProcess(); - Process? process = null; - - if (trackedProcess is not null) - { - process = trackedProcess; - } - else if (_hasStartedFaceMatcherInSession) - { - process = FindConfiguredFaceMatcherProcess(); - } - - if (process is null) - { - await StopFaceMatcherLogWatcherAsync().ConfigureAwait(false); - await InvokeOnUiThreadAsync(() => - { - IsFaceMatcherRunning = false; - FaceMatcherStatusMessage = "Face matcher non in esecuzione."; - }).ConfigureAwait(false); - return; - } - - using (process) - { - var gracefulStopRequested = TryRequestFaceMatcherStop(process); - var exited = !IsProcessAlive(process); - - if (!exited && waitForExit) - { - exited = await WaitForProcessExitAsync(process, TimeSpan.FromSeconds(5)).ConfigureAwait(false); - } - - if (!exited) - { - try - { - process.Kill(entireProcessTree: true); - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Unable to terminate face matcher process {ProcessId}", process.Id); - } - } - - await StopFaceMatcherLogWatcherAsync().ConfigureAwait(false); - ClearTrackedFaceMatcherProcess(); - await InvokeOnUiThreadAsync(() => - { - IsFaceMatcherRunning = !exited && IsProcessAlive(process); - FaceMatcherStatusMessage = exited - ? "Face matcher arrestato." - : gracefulStopRequested - ? "Segnale di arresto inviato al face matcher." - : "Arresto forzato del face matcher richiesto."; - }).ConfigureAwait(false); - } - } - private async Task WatchFaceEncoderProcessAsync(CancellationToken token) { using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1)); @@ -2065,32 +1719,6 @@ namespace ImageCatalog_2 } } - private async Task WatchFaceMatcherProcessAsync(CancellationToken token) - { - using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1)); - - try - { - while (await timer.WaitForNextTickAsync(token).ConfigureAwait(false)) - { - if (!_hasStartedFaceMatcherInSession) - { - continue; - } - - var isRunning = ComputeIsFaceMatcherRunning(); - if (isRunning != IsFaceMatcherRunning) - { - await InvokeOnUiThreadAsync(() => IsFaceMatcherRunning = isRunning).ConfigureAwait(false); - } - } - } - catch (OperationCanceledException) - { - // App shutdown. - } - } - private void EnsureFaceEncoderWatcherStarted() { if (_faceEncoderWatcherTask is not null) @@ -2102,17 +1730,6 @@ namespace ImageCatalog_2 _faceEncoderWatcherTask = WatchFaceEncoderProcessAsync(_faceEncoderWatcherTokenSource.Token); } - private void EnsureFaceMatcherWatcherStarted() - { - if (_faceMatcherWatcherTask is not null) - { - return; - } - - _faceMatcherWatcherTokenSource = new CancellationTokenSource(); - _faceMatcherWatcherTask = WatchFaceMatcherProcessAsync(_faceMatcherWatcherTokenSource.Token); - } - private void StartFaceEncoderLogWatcher(string logFilePath, StringBuilder outputLines, StringBuilder transcriptLines) { _faceEncoderLogWatcherTokenSource?.Cancel(); @@ -2122,15 +1739,6 @@ namespace ImageCatalog_2 _faceEncoderLogWatcherTask = WatchFaceEncoderLogFileAsync(logFilePath, outputLines, transcriptLines, _faceEncoderLogWatcherTokenSource.Token); } - private void StartFaceMatcherLogWatcher(string logFilePath, StringBuilder outputLines, StringBuilder transcriptLines) - { - _faceMatcherLogWatcherTokenSource?.Cancel(); - _faceMatcherLogWatcherTokenSource?.Dispose(); - - _faceMatcherLogWatcherTokenSource = new CancellationTokenSource(); - _faceMatcherLogWatcherTask = WatchFaceMatcherLogFileAsync(logFilePath, outputLines, transcriptLines, _faceMatcherLogWatcherTokenSource.Token); - } - private async Task StopFaceEncoderLogWatcherAsync() { var tokenSource = _faceEncoderLogWatcherTokenSource; @@ -2162,37 +1770,6 @@ namespace ImageCatalog_2 } } - private async Task StopFaceMatcherLogWatcherAsync() - { - var tokenSource = _faceMatcherLogWatcherTokenSource; - var task = _faceMatcherLogWatcherTask; - - _faceMatcherLogWatcherTokenSource = null; - _faceMatcherLogWatcherTask = null; - - if (tokenSource is null) - { - return; - } - - try - { - await tokenSource.CancelAsync().ConfigureAwait(false); - if (task is not null) - { - await task.ConfigureAwait(false); - } - } - catch (OperationCanceledException) - { - // Expected when shutting down the watcher. - } - finally - { - tokenSource.Dispose(); - } - } - private async Task WatchFaceEncoderLogFileAsync(string logFilePath, StringBuilder outputLines, StringBuilder transcriptLines, CancellationToken token) { long filePosition = 0; @@ -2237,50 +1814,6 @@ namespace ImageCatalog_2 } } - private async Task WatchFaceMatcherLogFileAsync(string logFilePath, StringBuilder outputLines, StringBuilder transcriptLines, CancellationToken token) - { - long filePosition = 0; - - while (!token.IsCancellationRequested) - { - try - { - if (File.Exists(logFilePath)) - { - using var stream = new FileStream(logFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); - if (filePosition > stream.Length) - { - filePosition = 0; - } - - stream.Seek(filePosition, SeekOrigin.Begin); - using var reader = new StreamReader(stream); - while (!reader.EndOfStream) - { - var line = await reader.ReadLineAsync(token).ConfigureAwait(false); - AppendFaceMatcherProcessOutput(outputLines, transcriptLines, line, isError: false); - } - - filePosition = stream.Position; - } - } - catch (OperationCanceledException) - { - throw; - } - catch (IOException) - { - // Retry while the matcher is still writing. - } - catch (UnauthorizedAccessException) - { - // Retry if the file is transiently locked. - } - - await Task.Delay(TimeSpan.FromMilliseconds(250), token).ConfigureAwait(false); - } - } - private void RefreshFaceExecutableCapabilities() { var executableRoot = NormalizeFilePathArgument(_ai.FaceExecutablePath); @@ -2299,86 +1832,31 @@ namespace ImageCatalog_2 } } - private void QueueRefreshNumberAiGpuCapabilities() + private void RefreshNumberAiGpuCapabilities() { if (!TryBuildNumberAiModelConfiguration(out var configuration)) { - _numberAiGpuValidationPending = false; NumberAiGpuOptionEnabled = false; _ai.UseNumberAiGpu = false; return; } - _numberAiGpuValidationPending = true; - var requestVersion = Interlocked.Increment(ref _numberAiGpuRefreshVersion); - _ = RefreshNumberAiGpuCapabilitiesAsync(configuration, requestVersion); - } - - private async Task RefreshNumberAiGpuCapabilitiesAsync(ModelConfiguration configuration, int requestVersion) - { - try + NumberAiGpuOptionEnabled = NumberRecognitionEngine.TryValidateGpuRuntime(configuration, _logger, out _); + if (!NumberAiGpuOptionEnabled) { - var gpuAvailable = await Task.Run(() => - NumberRecognitionEngine.TryValidateGpuRuntime(configuration, _logger, out _)).ConfigureAwait(false); - - if (requestVersion != Volatile.Read(ref _numberAiGpuRefreshVersion)) - { - return; - } - - await InvokeOnUiThreadAsync(() => - { - if (requestVersion != Volatile.Read(ref _numberAiGpuRefreshVersion)) - { - return; - } - - _numberAiGpuValidationPending = false; - NumberAiGpuOptionEnabled = gpuAvailable; - if (!gpuAvailable) - { - _ai.UseNumberAiGpu = false; - } - }).ConfigureAwait(false); - } - catch (Exception ex) - { - if (requestVersion != Volatile.Read(ref _numberAiGpuRefreshVersion)) - { - return; - } - - _logger.LogWarning(ex, "Failed to refresh OCR GPU capabilities."); - - await InvokeOnUiThreadAsync(() => - { - if (requestVersion != Volatile.Read(ref _numberAiGpuRefreshVersion)) - { - return; - } - - _numberAiGpuValidationPending = false; - NumberAiGpuOptionEnabled = false; - _ai.UseNumberAiGpu = false; - }).ConfigureAwait(false); + _ai.UseNumberAiGpu = false; } } private void SetUseNumberAiGpu(bool value) { - if (!value) + if (!NumberAiGpuOptionEnabled) { _ai.UseNumberAiGpu = false; return; } - if (NumberAiGpuOptionEnabled || _numberAiGpuValidationPending) - { - _ai.UseNumberAiGpu = true; - return; - } - - _ai.UseNumberAiGpu = false; + _ai.UseNumberAiGpu = value; } private bool TryBuildNumberAiModelConfiguration(out ModelConfiguration configuration) @@ -2419,103 +1897,6 @@ namespace ImageCatalog_2 }); } - internal async Task ConfirmAiCsvOverwriteIfNeededAsync() - { - var csvOutputPath = NormalizeFilePathArgument(CsvOutputPath); - if (string.IsNullOrWhiteSpace(csvOutputPath) || !File.Exists(csvOutputPath)) - { - return true; - } - - var confirmOverwrite = ConfirmAiCsvOverwriteAsync; - if (confirmOverwrite is null) - { - return true; - } - - var message = $"Il file CSV esiste gia:\n{csvOutputPath}\n\nVuoi sovrascriverlo? Se scegli Annulla l'operazione OCR non verra avviata."; - return await confirmOverwrite(AiCsvOverwriteDialogTitle, message).ConfigureAwait(false); - } - - internal void UpdateAiCsvOutputPathForDestination() - { - var updatedPath = BuildAiCsvOutputPathForDestination(CsvOutputPath, DestinationPath); - if (string.Equals(updatedPath, CsvOutputPath, StringComparison.Ordinal)) - { - return; - } - - CsvOutputPath = updatedPath; - } - - internal static string BuildAiCsvOutputPathForDestination(string currentCsvOutputPath, string destinationPath) - { - var normalizedCsvPath = NormalizeFilePathArgument(currentCsvOutputPath); - if (string.IsNullOrWhiteSpace(normalizedCsvPath)) - { - return currentCsvOutputPath; - } - - var directory = Path.GetDirectoryName(normalizedCsvPath); - var destinationFolderName = GetDestinationFolderName(destinationPath); - if (string.IsNullOrWhiteSpace(destinationFolderName)) - { - return normalizedCsvPath; - } - - var extension = Path.GetExtension(normalizedCsvPath); - if (string.IsNullOrWhiteSpace(extension)) - { - extension = ".csv"; - } - - var safeFileName = SanitizeFileName(destinationFolderName) + extension; - return string.IsNullOrWhiteSpace(directory) - ? safeFileName - : Path.Combine(directory, safeFileName); - } - - private static string GetDestinationFolderName(string destinationPath) - { - var normalizedDestinationPath = NormalizeDirectoryPathArgument(destinationPath); - if (string.IsNullOrWhiteSpace(normalizedDestinationPath)) - { - return string.Empty; - } - - var trimmedPath = normalizedDestinationPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); - if (string.IsNullOrWhiteSpace(trimmedPath)) - { - return string.Empty; - } - - var rootPath = Path.GetPathRoot(trimmedPath)?.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); - if (!string.IsNullOrWhiteSpace(rootPath) - && string.Equals(trimmedPath, rootPath, StringComparison.OrdinalIgnoreCase)) - { - return string.Empty; - } - - return Path.GetFileName(trimmedPath); - } - - private static string SanitizeFileName(string value) - { - if (string.IsNullOrWhiteSpace(value)) - { - return string.Empty; - } - - var invalidFileNameChars = Path.GetInvalidFileNameChars(); - var builder = new StringBuilder(value.Length); - foreach (var character in value) - { - builder.Append(Array.IndexOf(invalidFileNameChars, character) >= 0 ? '_' : character); - } - - return builder.ToString(); - } - private void SetUseFaceGpu(bool value) { var currentValue = _ai.UseFaceGpu; @@ -2546,14 +1927,6 @@ namespace ImageCatalog_2 } } - private void TrackFaceMatcherProcess(Process process) - { - lock (_faceMatcherProcessLock) - { - _faceMatcherProcess = process; - } - } - private void ClearTrackedFaceEncoderProcess() { lock (_faceEncoderProcessLock) @@ -2568,20 +1941,6 @@ namespace ImageCatalog_2 } } - private void ClearTrackedFaceMatcherProcess() - { - lock (_faceMatcherProcessLock) - { - if (_faceMatcherProcess is not null && _faceMatcherProcess.HasExited) - { - _faceMatcherProcess = null; - return; - } - - _faceMatcherProcess = null; - } - } - private Process? GetTrackedFaceEncoderProcess() { lock (_faceEncoderProcessLock) @@ -2601,25 +1960,6 @@ namespace ImageCatalog_2 } } - private Process? GetTrackedFaceMatcherProcess() - { - lock (_faceMatcherProcessLock) - { - if (_faceMatcherProcess is null) - { - return null; - } - - if (_faceMatcherProcess.HasExited) - { - _faceMatcherProcess = null; - return null; - } - - return _faceMatcherProcess; - } - } - private Process? FindConfiguredFaceEncoderProcess() { var configuredExecutablePath = ResolveConfiguredFaceEncoderExecutablePath(FaceExecutablePath, UseFaceGpu); @@ -2648,34 +1988,6 @@ namespace ImageCatalog_2 return null; } - private Process? FindConfiguredFaceMatcherProcess() - { - var configuredExecutablePath = ResolveConfiguredFaceMatcherExecutablePath(FaceMatcherExecutablePath, FaceExecutablePath); - if (string.IsNullOrWhiteSpace(configuredExecutablePath)) - { - return null; - } - - var processName = Path.GetFileNameWithoutExtension(configuredExecutablePath); - foreach (var process in Process.GetProcessesByName(processName)) - { - if (!IsProcessAlive(process)) - { - process.Dispose(); - continue; - } - - if (IsMatchingProcessPath(process, configuredExecutablePath)) - { - return process; - } - - process.Dispose(); - } - - return null; - } - private bool ComputeIsFaceEncoderRunning() { var trackedProcess = GetTrackedFaceEncoderProcess(); @@ -2693,23 +2005,6 @@ namespace ImageCatalog_2 return discoveredProcess is not null; } - private bool ComputeIsFaceMatcherRunning() - { - var trackedProcess = GetTrackedFaceMatcherProcess(); - if (trackedProcess is not null) - { - return true; - } - - if (!_hasStartedFaceMatcherInSession) - { - return false; - } - - using var discoveredProcess = FindConfiguredFaceMatcherProcess(); - return discoveredProcess is not null; - } - private static bool IsProcessAlive(Process process) { try @@ -2761,31 +2056,6 @@ namespace ImageCatalog_2 } } - private bool TryRequestFaceMatcherStop(Process process) - { - if (!IsProcessAlive(process)) - { - return true; - } - -#if WINDOWS - if (Program.TrySendConsoleInterrupt(process.Id)) - { - return true; - } -#endif - - try - { - return process.CloseMainWindow(); - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Unable to request graceful stop for face matcher process {ProcessId}", process.Id); - return false; - } - } - private static async Task WaitForProcessExitAsync(Process process, TimeSpan timeout) { if (!IsProcessAlive(process)) @@ -2841,88 +2111,6 @@ namespace ImageCatalog_2 _ = InvokeOnUiThreadAsync(() => FaceCommandOutput = transcript); } - private void AppendFaceMatcherProcessOutput(StringBuilder builder, StringBuilder transcriptBuilder, string? line, bool isError) - { - if (string.IsNullOrWhiteSpace(line)) - { - return; - } - - lock (builder) - { - builder.AppendLine(line); - } - - if (isError) - { - Console.Error.WriteLine(line); - } - else - { - Console.WriteLine(line); - } - - string transcript; - lock (transcriptBuilder) - { - if (isError) - { - transcriptBuilder.Append("[stderr] "); - } - - transcriptBuilder.AppendLine(line); - transcript = transcriptBuilder.ToString(); - } - - _ = InvokeOnUiThreadAsync(() => FaceMatcherCommandOutput = transcript); - } - - internal static string? ResolveConfiguredFaceMatcherExecutablePath(string configuredPath, string fallbackEncoderPath) - { - foreach (var candidate in EnumerateFaceMatcherExecutableCandidates(configuredPath, fallbackEncoderPath)) - { - if (File.Exists(candidate)) - { - return candidate; - } - } - - return null; - } - - internal static string? ResolveConfiguredFaceMatcherEncodingsPath(string configuredPath, string fallbackOutputFolderPath) - { - var normalizedPath = NormalizeFilePathArgument(configuredPath); - if (File.Exists(normalizedPath)) - { - return normalizedPath; - } - - if (Directory.Exists(normalizedPath)) - { - var fromDirectory = new DirectoryInfo(normalizedPath) - .EnumerateFiles("*.pkl", SearchOption.TopDirectoryOnly) - .OrderByDescending(file => file.LastWriteTimeUtc) - .FirstOrDefault(); - if (fromDirectory is not null) - { - return fromDirectory.FullName; - } - } - - var fallbackOutputFolder = NormalizeDirectoryPathArgument(fallbackOutputFolderPath); - if (Directory.Exists(fallbackOutputFolder)) - { - var latest = new DirectoryInfo(fallbackOutputFolder) - .EnumerateFiles("*.pkl", SearchOption.TopDirectoryOnly) - .OrderByDescending(file => file.LastWriteTimeUtc) - .FirstOrDefault(); - return latest?.FullName; - } - - return null; - } - internal static string? ResolveConfiguredFaceEncoderExecutablePath(string configuredPath, bool useGpu) { var variant = useGpu ? "gpu" : "cpu"; @@ -2983,37 +2171,6 @@ namespace ImageCatalog_2 }; } - internal static (string CsvPath, string LogPath) ResolveFaceMatcherOutputPaths(string configuredCsvPath, string configuredLogPath, string fallbackRootPath, string imagePath, DateTime timestamp) - { - var baseName = BuildSafeFaceMatcherImageName(imagePath); - var timestampToken = timestamp.ToString("yyyyMMdd_HHmmss"); - var csvPath = ResolveFaceMatcherOutputFilePath(configuredCsvPath, fallbackRootPath, $"result_{timestampToken}_{baseName}.csv"); - var logPath = ResolveFaceMatcherOutputFilePath(configuredLogPath, fallbackRootPath, $"matcher_log_{timestampToken}_{baseName}.txt"); - return (csvPath, logPath); - } - - internal static string BuildSafeFaceMatcherImageName(string imagePath) - { - var fileName = Path.GetFileNameWithoutExtension(imagePath); - if (string.IsNullOrWhiteSpace(fileName)) - { - return "image"; - } - - var invalidChars = Path.GetInvalidFileNameChars(); - var builder = new StringBuilder(fileName.Length); - foreach (var currentChar in fileName) - { - builder.Append(invalidChars.Contains(currentChar) || char.IsWhiteSpace(currentChar) ? '_' : currentChar); - } - - return builder.ToString().Trim('_') switch - { - "" => "image", - var sanitized => sanitized - }; - } - private static IEnumerable EnumerateFaceEncoderExecutableCandidates(string configuredPath, string variant) { var normalizedPath = NormalizeFilePathArgument(configuredPath); @@ -3057,50 +2214,11 @@ namespace ImageCatalog_2 } } - private static IEnumerable EnumerateFaceMatcherExecutableCandidates(string configuredPath, string fallbackEncoderPath) - { - var normalizedPath = NormalizeFilePathArgument(configuredPath); - if (!string.IsNullOrWhiteSpace(normalizedPath)) - { - if (File.Exists(normalizedPath)) - { - yield return normalizedPath; - } - - yield return Path.Combine(normalizedPath, "face_matcher.exe"); - } - - var fallbackPath = NormalizeFilePathArgument(fallbackEncoderPath); - if (!string.IsNullOrWhiteSpace(fallbackPath)) - { - if (File.Exists(fallbackPath)) - { - var fileDirectory = Path.GetDirectoryName(fallbackPath); - if (!string.IsNullOrWhiteSpace(fileDirectory)) - { - yield return Path.Combine(fileDirectory, "face_matcher.exe"); - } - } - - yield return Path.Combine(fallbackPath, "face_matcher.exe"); - } - } - private static int NormalizeFaceParallelism(int value) { return value is >= 1 and <= 5 ? value : 3; } - private static double NormalizeFaceMatcherTolerance(double value) - { - if (double.IsNaN(value) || double.IsInfinity(value)) - { - return 0.5; - } - - return Math.Clamp(Math.Round(value, 2), 0.35, 0.75); - } - private static int NormalizeNumberAiWorkloadLevel(int value) { return value is >= 1 and <= 5 ? value : 3; @@ -3178,299 +2296,6 @@ namespace ImageCatalog_2 return summary.ToString(); } - private static string BuildFaceMatcherSummary( - int exitCode, - ProcessStartInfo processStartInfo, - string csvPath, - string logFilePath, - StringBuilder outputLines, - StringBuilder errorLines, - int matchCount) - { - var summary = new StringBuilder(); - summary.AppendLine($"Exit code: {exitCode}"); - summary.AppendLine($"Command: {processStartInfo.FileName} {string.Join(" ", processStartInfo.ArgumentList)}"); - summary.AppendLine($"Result CSV: {csvPath}"); - summary.AppendLine($"Log file: {logFilePath}"); - summary.AppendLine($"Match count: {matchCount}"); - - lock (outputLines) - { - if (outputLines.Length > 0) - { - summary.AppendLine(); - summary.AppendLine("STDOUT:"); - summary.Append(outputLines); - } - } - - lock (errorLines) - { - if (errorLines.Length > 0) - { - summary.AppendLine(); - summary.AppendLine("STDERR:"); - summary.Append(errorLines); - } - } - - return summary.ToString(); - } - - private static string ResolveFaceMatcherFallbackOutputRoot(string configuredCsvPath, string configuredLogPath, string faceOutputFolderPath, string executablePath) - { - foreach (var candidate in new[] { NormalizeFilePathArgument(configuredCsvPath), NormalizeFilePathArgument(configuredLogPath), NormalizeDirectoryPathArgument(faceOutputFolderPath) }) - { - if (string.IsNullOrWhiteSpace(candidate)) - { - continue; - } - - if (Directory.Exists(candidate)) - { - return candidate; - } - - var directory = Path.GetDirectoryName(candidate); - if (!string.IsNullOrWhiteSpace(directory)) - { - return directory; - } - } - - return Path.Combine(Path.GetDirectoryName(executablePath) ?? Environment.CurrentDirectory, "output"); - } - - private static string ResolveFaceMatcherOutputFilePath(string configuredPath, string fallbackRootPath, string defaultFileName) - { - var normalized = NormalizeFilePathArgument(configuredPath); - if (string.IsNullOrWhiteSpace(normalized)) - { - return Path.Combine(fallbackRootPath, defaultFileName); - } - - if (Directory.Exists(normalized) || string.IsNullOrWhiteSpace(Path.GetExtension(normalized))) - { - return Path.Combine(normalized, defaultFileName); - } - - return normalized; - } - - private async Task LogIndicatesNoFacesAsync(string logPath) - { - try - { - if (!File.Exists(logPath)) - { - return false; - } - - var content = await File.ReadAllTextAsync(logPath).ConfigureAwait(false); - return content.Contains("nessun volt", StringComparison.OrdinalIgnoreCase) - || content.Contains("no face", StringComparison.OrdinalIgnoreCase) - || content.Contains("0 faces", StringComparison.OrdinalIgnoreCase); - } - catch - { - return false; - } - } - - private static async Task> ParseFaceMatcherCsvAsync(string csvPath, string logPath) - { - var parsedRows = new List(); - if (!File.Exists(csvPath)) - { - return parsedRows; - } - - var logScores = await ParseFaceMatcherScoresFromLogAsync(logPath).ConfigureAwait(false); - - var lines = await File.ReadAllLinesAsync(csvPath).ConfigureAwait(false); - var meaningfulLines = lines.Where(line => !string.IsNullOrWhiteSpace(line)).ToArray(); - if (meaningfulLines.Length == 0) - { - return parsedRows; - } - - string[]? headers = null; - var firstCells = ParseCsvLine(meaningfulLines[0]); - var hasHeader = firstCells.Any(cell => cell.Contains("file", StringComparison.OrdinalIgnoreCase) - || cell.Contains("image", StringComparison.OrdinalIgnoreCase) - || cell.Contains("score", StringComparison.OrdinalIgnoreCase) - || cell.Contains("distance", StringComparison.OrdinalIgnoreCase) - || cell.Contains("confidence", StringComparison.OrdinalIgnoreCase)); - - var startIndex = 0; - if (hasHeader) - { - headers = firstCells; - startIndex = 1; - } - - for (var index = startIndex; index < meaningfulLines.Length; index++) - { - var rawLine = meaningfulLines[index].Trim(); - var cells = ParseCsvLine(rawLine); - if (cells.Length == 0 || string.IsNullOrWhiteSpace(cells[0])) - { - continue; - } - - var photoId = Path.GetFileName(cells[0]); - double? score = null; - for (var cellIndex = 1; cellIndex < cells.Length; cellIndex++) - { - if (double.TryParse(cells[cellIndex], NumberStyles.Float, CultureInfo.InvariantCulture, out var parsedScore)) - { - score = parsedScore; - break; - } - } - - if (!score.HasValue && logScores.TryGetValue(photoId, out var logScore)) - { - score = logScore; - } - - var debugParts = new List(); - for (var cellIndex = 1; cellIndex < cells.Length; cellIndex++) - { - var cellValue = cells[cellIndex]; - if (string.IsNullOrWhiteSpace(cellValue)) - { - continue; - } - - var header = headers is not null && cellIndex < headers.Length - ? headers[cellIndex] - : $"col{cellIndex + 1}"; - debugParts.Add($"{header}: {cellValue}"); - } - - if (score.HasValue) - { - debugParts.Add($"score: {score.Value.ToString("0.###", CultureInfo.InvariantCulture)}"); - } - - parsedRows.Add(new ParsedFaceMatcherRow(photoId, score, rawLine, string.Join(" | ", debugParts))); - } - - return parsedRows; - } - - private static async Task> ParseFaceMatcherScoresFromLogAsync(string logPath) - { - var scores = new Dictionary(StringComparer.OrdinalIgnoreCase); - if (!File.Exists(logPath)) - { - return scores; - } - - var lines = await File.ReadAllLinesAsync(logPath).ConfigureAwait(false); - foreach (var line in lines) - { - if (string.IsNullOrWhiteSpace(line)) - { - continue; - } - - var match = Regex.Match( - line, - @"in\s+(?.+?)\s+-\s+\[Somiglianza:\s*(?[0-9]+(?:[\.,][0-9]+)?)%\]", - RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); - - if (!match.Success) - { - continue; - } - - var pathValue = match.Groups["path"].Value.Trim(); - var photoId = Path.GetFileName(pathValue); - if (string.IsNullOrWhiteSpace(photoId)) - { - continue; - } - - var scoreText = match.Groups["score"].Value.Replace(',', '.'); - if (!double.TryParse(scoreText, NumberStyles.Float, CultureInfo.InvariantCulture, out var score)) - { - continue; - } - - if (!scores.TryGetValue(photoId, out var existingScore) || score > existingScore) - { - scores[photoId] = score; - } - } - - return scores; - } - - private static string[] ParseCsvLine(string line) - { - var values = new List(); - var current = new StringBuilder(); - var insideQuotes = false; - - foreach (var currentChar in line) - { - if (currentChar == '"') - { - insideQuotes = !insideQuotes; - continue; - } - - if (currentChar == ',' && !insideQuotes) - { - values.Add(current.ToString().Trim()); - current.Clear(); - continue; - } - - current.Append(currentChar); - } - - values.Add(current.ToString().Trim()); - return values.ToArray(); - } - - private static Dictionary> ResolveDestinationImagesByFileName(string destinationRoot, IEnumerable fileNames) - { - var result = new Dictionary>(StringComparer.OrdinalIgnoreCase); - var normalizedRoot = NormalizeDirectoryPathArgument(destinationRoot); - if (string.IsNullOrWhiteSpace(normalizedRoot) || !Directory.Exists(normalizedRoot)) - { - return result; - } - - var requestedNames = new HashSet(fileNames.Where(name => !string.IsNullOrWhiteSpace(name)).Select(name => Path.GetFileName(name)), StringComparer.OrdinalIgnoreCase); - if (requestedNames.Count == 0) - { - return result; - } - - foreach (var path in Directory.EnumerateFiles(normalizedRoot, "*", SearchOption.AllDirectories)) - { - var fileName = Path.GetFileName(path); - if (!requestedNames.Contains(fileName)) - { - continue; - } - - if (!result.TryGetValue(fileName, out var list)) - { - list = new List(); - result[fileName] = list; - } - - list.Add(path); - } - - return result; - } - private static string NormalizeDirectoryPathArgument(string value) { if (string.IsNullOrWhiteSpace(value)) diff --git a/imagecatalog/ImageCatalog 2.csproj b/imagecatalog/ImageCatalog 2.csproj index 11eda12..734b648 100644 --- a/imagecatalog/ImageCatalog 2.csproj +++ b/imagecatalog/ImageCatalog 2.csproj @@ -3,7 +3,6 @@ enable enable False - false ImageCatalog default @@ -47,16 +46,12 @@ embedded - - - - SettingsSingleFileGenerator @@ -70,16 +65,16 @@ - - - - - + + + + + - - - - + + + + all @@ -164,13 +159,4 @@ - - - - - - - - - \ No newline at end of file diff --git a/imagecatalog/Models/FaceMatcherResultItem.cs b/imagecatalog/Models/FaceMatcherResultItem.cs deleted file mode 100644 index 15914e0..0000000 --- a/imagecatalog/Models/FaceMatcherResultItem.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace ImageCatalog_2.Models; - -public sealed class FaceMatcherResultItem -{ - public string PhotoId { get; init; } = string.Empty; - - public double? Score { get; init; } - - public string ScoreDisplay => Score.HasValue ? Score.Value.ToString("0.###") : string.Empty; - - public string ResolvedImagePath { get; init; } = string.Empty; - - public int CandidateCount { get; init; } - - public string RawRow { get; init; } = string.Empty; - - public string DebugSummary { get; init; } = string.Empty; - - public string SearchImagePath { get; init; } = string.Empty; - - public string CsvPath { get; init; } = string.Empty; - - public string LogPath { get; init; } = string.Empty; -} diff --git a/imagecatalog/Models/SettingsDto.cs b/imagecatalog/Models/SettingsDto.cs index fd68763..e9de47f 100644 --- a/imagecatalog/Models/SettingsDto.cs +++ b/imagecatalog/Models/SettingsDto.cs @@ -314,18 +314,6 @@ namespace ImageCatalog_2.Models [XmlElement("AI_FaceUpsample")] public bool FaceUpsample { get; set; } = true; - [JsonPropertyName("UseFaceGpu")] - [XmlElement("AI_UsaGpuFace")] - public bool UseFaceGpu { get; set; } - - [JsonPropertyName("FaceMatcherExecutablePath")] - [XmlElement("AI_FaceMatcherExecutablePath")] - public string FaceMatcherExecutablePath { get; set; } = string.Empty; - - [JsonPropertyName("FaceMatcherTolerance")] - [XmlElement("AI_FaceMatcherTolerance")] - public double FaceMatcherTolerance { get; set; } = 0.5; - // Race upload settings [JsonPropertyName("ApiLogin")] [XmlElement("RaceUpload_Login")] diff --git a/imagecatalog/Program.cs b/imagecatalog/Program.cs index 798a03d..2a350bd 100644 --- a/imagecatalog/Program.cs +++ b/imagecatalog/Program.cs @@ -154,7 +154,6 @@ static class Program var userPrefsPath = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "ImageCatalog", "userprefs.xml"); services.AddSingleton(new ParametriSetup(userPrefsPath)); - services.AddSingleton(); services.AddSingleton(); services.AddCatalogCommunication(options => diff --git a/imagecatalog/Services/AiExtractionService.cs b/imagecatalog/Services/AiExtractionService.cs index 685a43f..b7ea6cc 100644 --- a/imagecatalog/Services/AiExtractionService.cs +++ b/imagecatalog/Services/AiExtractionService.cs @@ -198,7 +198,20 @@ public class AiExtractionService : IAiExtractionService { try { - WriteCsvOutput(request.CsvOutputPath, extractedResults); + var dir = Path.GetDirectoryName(request.CsvOutputPath) ?? string.Empty; + if (!string.IsNullOrWhiteSpace(dir) && !Directory.Exists(dir)) + { + Directory.CreateDirectory(dir); + } + + using var sw = new StreamWriter(request.CsvOutputPath, false, Encoding.UTF8); + sw.WriteLine("Path,Text"); + foreach (var r in extractedResults) + { + var csvFileName = Path.GetFileName(r.Path ?? string.Empty); + var safeText = (r.Text ?? string.Empty).Replace("\"", "\"\""); + sw.WriteLine($"\"{csvFileName}\",\"{safeText}\""); + } } catch (Exception ex) { @@ -209,24 +222,6 @@ public class AiExtractionService : IAiExtractionService return summary; } - internal static void WriteCsvOutput(string csvOutputPath, IEnumerable extractedResults) - { - var dir = Path.GetDirectoryName(csvOutputPath) ?? string.Empty; - if (!string.IsNullOrWhiteSpace(dir) && !Directory.Exists(dir)) - { - Directory.CreateDirectory(dir); - } - - using var sw = new StreamWriter(csvOutputPath, false, Encoding.UTF8); - sw.WriteLine("filename,text"); - foreach (var result in extractedResults) - { - var csvFileName = Path.GetFileName(result.Path ?? string.Empty); - var safeText = (result.Text ?? string.Empty).Replace("\"", "\"\""); - sw.WriteLine($"\"{csvFileName}\",\"{safeText}\""); - } - } - private static double CalculateAverageImagesPerSecond(int processed, TimeSpan elapsed) { return elapsed.TotalSeconds > 0 ? processed / elapsed.TotalSeconds : 0; diff --git a/imagecatalog/Services/PathShellService.cs b/imagecatalog/Services/PathShellService.cs deleted file mode 100644 index fcba122..0000000 --- a/imagecatalog/Services/PathShellService.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Diagnostics; -using System.IO; - -namespace ImageCatalog_2.Services; - -public static class PathShellService -{ - public static void OpenInExplorer(string? path) - { - if (string.IsNullOrWhiteSpace(path)) - { - return; - } - - var normalizedPath = path.Trim().Trim('"'); - - try - { - if (File.Exists(normalizedPath)) - { - Process.Start("explorer.exe", $"/select,\"{normalizedPath}\""); - return; - } - - if (Directory.Exists(normalizedPath)) - { - Process.Start(new ProcessStartInfo - { - FileName = normalizedPath, - UseShellExecute = true - }); - return; - } - - var containingDirectory = Path.GetDirectoryName(normalizedPath); - if (!string.IsNullOrWhiteSpace(containingDirectory) && Directory.Exists(containingDirectory)) - { - Process.Start(new ProcessStartInfo - { - FileName = containingDirectory, - UseShellExecute = true - }); - } - } - catch - { - // Ignore failures when opening Explorer. - } - } -} \ No newline at end of file diff --git a/imagecatalog/Services/PickerPreferenceService.cs b/imagecatalog/Services/PickerPreferenceService.cs deleted file mode 100644 index 49bdaea..0000000 --- a/imagecatalog/Services/PickerPreferenceService.cs +++ /dev/null @@ -1,122 +0,0 @@ -using Avalonia.Platform.Storage; -using ImageCatalog; -using System; -using System.IO; -using System.Threading.Tasks; - -namespace ImageCatalog_2.Services; - -public static class PickerPreferenceKeys -{ - public const string SourceFolder = "Picker.SourceFolder.LastPath"; - public const string DestinationFolder = "Picker.DestinationFolder.LastPath"; - public const string LogoFile = "Picker.LogoFile.LastPath"; - public const string ModelsFolder = "Picker.ModelsFolder.LastPath"; - public const string CsvOutput = "Picker.CsvOutput.LastPath"; - public const string SettingsFile = "Picker.SettingsFile.LastPath"; - public const string LastSettingsFile = "Settings.LastFilePath"; - public const string FaceExecutableFolder = "Picker.FaceExecutableFolder.LastPath"; - public const string FaceOutputFolder = "Picker.FaceOutputFolder.LastPath"; - public const string FaceMatcherExecutable = "Picker.FaceMatcherExecutable.LastPath"; - public const string FaceMatcherImage = "Picker.FaceMatcherImage.LastPath"; - public const string FaceMatcherEncodings = "Picker.FaceMatcherEncodings.LastPath"; - public const string FaceMatcherOutput = "Picker.FaceMatcherOutput.LastPath"; - public const string FaceMatcherLog = "Picker.FaceMatcherLog.LastPath"; -} - -public sealed class PickerPreferenceService -{ - private readonly ParametriSetup _userPreferences; - - public PickerPreferenceService(ParametriSetup userPreferences) - { - _userPreferences = userPreferences; - } - - public async Task TryGetStartFolderAsync(IStorageProvider storageProvider, string preferenceKey, string? currentPath = null) - { - var startPath = GetPreferredStartDirectory(preferenceKey, currentPath); - if (string.IsNullOrWhiteSpace(startPath)) - { - return null; - } - - try - { - return await storageProvider.TryGetFolderFromPathAsync(new Uri(startPath)).ConfigureAwait(true); - } - catch - { - return null; - } - } - - public void RememberPath(string preferenceKey, string? selectedPath) - { - var directory = TryGetExistingDirectory(selectedPath); - if (string.IsNullOrWhiteSpace(directory)) - { - return; - } - - _userPreferences.AggiornaParametro(preferenceKey, directory); - _userPreferences.SalvaParametriSetup(); - } - - public string? GetRememberedValue(string preferenceKey) - { - var value = _userPreferences.LeggiParametroString(preferenceKey); - return string.IsNullOrWhiteSpace(value) - ? null - : value.Trim().Trim('"'); - } - - public void RememberValue(string preferenceKey, string? value) - { - if (string.IsNullOrWhiteSpace(value)) - { - return; - } - - _userPreferences.AggiornaParametro(preferenceKey, value.Trim().Trim('"')); - _userPreferences.SalvaParametriSetup(); - } - - public void ForgetValue(string preferenceKey) - { - if (_userPreferences.RimuoviParametro(preferenceKey)) - { - _userPreferences.SalvaParametriSetup(); - } - } - - private string? GetPreferredStartDirectory(string preferenceKey, string? currentPath) - { - var storedPath = _userPreferences.LeggiParametroString(preferenceKey); - return TryGetExistingDirectory(storedPath) ?? TryGetExistingDirectory(currentPath); - } - - private static string? TryGetExistingDirectory(string? path) - { - if (string.IsNullOrWhiteSpace(path)) - { - return null; - } - - var normalizedPath = path.Trim().Trim('"'); - if (Directory.Exists(normalizedPath)) - { - return normalizedPath; - } - - if (File.Exists(normalizedPath)) - { - return Path.GetDirectoryName(normalizedPath); - } - - var containingDirectory = Path.GetDirectoryName(normalizedPath); - return !string.IsNullOrWhiteSpace(containingDirectory) && Directory.Exists(containingDirectory) - ? containingDirectory - : null; - } -} \ No newline at end of file diff --git a/imagecatalog/ViewModels/AiSettingsViewModel.cs b/imagecatalog/ViewModels/AiSettingsViewModel.cs index e38213c..501cf5e 100644 --- a/imagecatalog/ViewModels/AiSettingsViewModel.cs +++ b/imagecatalog/ViewModels/AiSettingsViewModel.cs @@ -225,105 +225,6 @@ public class AiSettingsViewModel : ViewModelBase } } - private string _faceMatcherExecutablePath = string.Empty; - public string FaceMatcherExecutablePath - { - get => _faceMatcherExecutablePath; - set - { - _faceMatcherExecutablePath = value; - NotifyPropertyChanged(); - } - } - - private string _faceMatcherEncodingsPath = string.Empty; - public string FaceMatcherEncodingsPath - { - get => _faceMatcherEncodingsPath; - set - { - _faceMatcherEncodingsPath = value; - NotifyPropertyChanged(); - } - } - - private string _faceMatcherOutputPath = string.Empty; - public string FaceMatcherOutputPath - { - get => _faceMatcherOutputPath; - set - { - _faceMatcherOutputPath = value; - NotifyPropertyChanged(); - } - } - - private string _faceMatcherLogPath = string.Empty; - public string FaceMatcherLogPath - { - get => _faceMatcherLogPath; - set - { - _faceMatcherLogPath = value; - NotifyPropertyChanged(); - } - } - - private double _faceMatcherTolerance = 0.5; - public double FaceMatcherTolerance - { - get => _faceMatcherTolerance; - set - { - _faceMatcherTolerance = value; - NotifyPropertyChanged(); - } - } - - private string _faceMatcherSelectedImagePath = string.Empty; - public string FaceMatcherSelectedImagePath - { - get => _faceMatcherSelectedImagePath; - set - { - _faceMatcherSelectedImagePath = value; - NotifyPropertyChanged(); - } - } - - private bool _isFaceMatcherRunning; - public bool IsFaceMatcherRunning - { - get => _isFaceMatcherRunning; - set - { - _isFaceMatcherRunning = value; - NotifyPropertyChanged(); - } - } - - private string _faceMatcherStatusMessage = string.Empty; - public string FaceMatcherStatusMessage - { - get => _faceMatcherStatusMessage; - set - { - _faceMatcherStatusMessage = value; - NotifyPropertyChanged(); - } - } - - private string _faceMatcherCommandOutput = string.Empty; - public string FaceMatcherCommandOutput - { - get => _faceMatcherCommandOutput; - set - { - _faceMatcherCommandOutput = value; - NotifyPropertyChanged(); - } - } - private double _aiProgress; public double AiProgress { @@ -336,6 +237,4 @@ public class AiSettingsViewModel : ViewModelBase } public ObservableCollection PreviewResults { get; } = new(); - - public ObservableCollection FaceMatcherResults { get; } = new(); }