diff --git a/Catalog.Communication/Catalog.Communication.csproj b/Catalog.Communication/Catalog.Communication.csproj
index cb8dc94..8b3c8df 100644
--- a/Catalog.Communication/Catalog.Communication.csproj
+++ b/Catalog.Communication/Catalog.Communication.csproj
@@ -8,10 +8,10 @@
-
-
-
-
+
+
+
+
diff --git a/MaddoShared.Benchmarks/MaddoShared.Benchmarks.csproj b/MaddoShared.Benchmarks/MaddoShared.Benchmarks.csproj
index b712ff3..56b8684 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 52f36f6..6a7b6fe 100644
--- a/MaddoShared.ImageSharpTests/MaddoShared.ImageSharpTests.csproj
+++ b/MaddoShared.ImageSharpTests/MaddoShared.ImageSharpTests.csproj
@@ -9,15 +9,15 @@
-
-
-
+
+
+
-
-
-
-
+
+
+
+
diff --git a/MaddoShared.Tests/PickerPreferenceServiceTests.cs b/MaddoShared.Tests/PickerPreferenceServiceTests.cs
new file mode 100644
index 0000000..f0fe3d0
--- /dev/null
+++ b/MaddoShared.Tests/PickerPreferenceServiceTests.cs
@@ -0,0 +1,58 @@
+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 cd00c92..da8e098 100644
--- a/MaddoShared.Tests/Test1.cs
+++ b/MaddoShared.Tests/Test1.cs
@@ -1,11 +1,48 @@
-namespace MaddoShared.Tests
+using ImageCatalog_2.Services;
+using ImageCatalog_2.Models;
+using Shouldly;
+
+namespace MaddoShared.Tests;
+
+[TestClass]
+public sealed class AiExtractionServiceCsvTests
{
- [TestClass]
- public sealed class Test1
+ [TestMethod]
+ public void WriteCsvOutput_UsesLegacyCompatibleHeaderAndFilenameColumn()
{
- [TestMethod]
- public void TestMethod1()
+ 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()
{
+ 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/AvaloniaMainWindow.axaml.cs b/imagecatalog/AvaloniaMainWindow.axaml.cs
index 08ee289..ec1c454 100644
--- a/imagecatalog/AvaloniaMainWindow.axaml.cs
+++ b/imagecatalog/AvaloniaMainWindow.axaml.cs
@@ -17,6 +17,7 @@ public partial class AvaloniaMainWindow : Window
private readonly DataModel _model;
private readonly PickerPreferenceService _pickerPreferenceService;
private bool _isDarkTheme;
+ private bool _startupSettingsRestoreAttempted;
public AvaloniaMainWindow(DataModel model)
{
@@ -26,7 +27,11 @@ public partial class AvaloniaMainWindow : Window
_pickerPreferenceService = Program.ServiceProvider.GetRequiredService();
DataContext = _model;
- Opened += (_, _) => SyncThemeStateFromCurrentTheme();
+ Opened += async (_, _) =>
+ {
+ SyncThemeStateFromCurrentTheme();
+ await TryLoadLastSettingsOnStartupAsync();
+ };
Closing += AvaloniaMainWindow_Closing;
// Let DataModel marshal callbacks onto Avalonia UI thread.
@@ -120,30 +125,38 @@ public partial class AvaloniaMainWindow : Window
_model.SaveSettingsRequested += async (_, _) =>
{
+ var suggestedStartLocation = await _pickerPreferenceService.TryGetStartFolderAsync(StorageProvider, PickerPreferenceKeys.SettingsFile);
var file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
{
Title = "Salva impostazioni",
DefaultExtension = "xml",
- FileTypeChoices = [new FilePickerFileType("Setup") { Patterns = ["*.xml"] }]
+ FileTypeChoices = [new FilePickerFileType("Setup") { Patterns = ["*.xml"] }],
+ SuggestedStartLocation = suggestedStartLocation
});
if (file is not null)
{
await _model.SaveSettingsToFileAsync(file.Path.LocalPath);
+ _pickerPreferenceService.RememberPath(PickerPreferenceKeys.SettingsFile, file.Path.LocalPath);
+ _pickerPreferenceService.RememberValue(PickerPreferenceKeys.LastSettingsFile, file.Path.LocalPath);
}
};
_model.LoadSettingsRequested += async (_, _) =>
{
+ var suggestedStartLocation = await _pickerPreferenceService.TryGetStartFolderAsync(StorageProvider, PickerPreferenceKeys.SettingsFile);
var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = "Carica impostazioni",
- FileTypeFilter = [new FilePickerFileType("Setup") { Patterns = ["*.xml"] }]
+ FileTypeFilter = [new FilePickerFileType("Setup") { Patterns = ["*.xml"] }],
+ SuggestedStartLocation = suggestedStartLocation
});
if (files.Count > 0)
{
await _model.LoadSettingsFromFileAsync(files[0].Path.LocalPath);
+ _pickerPreferenceService.RememberPath(PickerPreferenceKeys.SettingsFile, files[0].Path.LocalPath);
+ _pickerPreferenceService.RememberValue(PickerPreferenceKeys.LastSettingsFile, files[0].Path.LocalPath);
}
};
@@ -165,6 +178,38 @@ public partial class AvaloniaMainWindow : Window
private bool _isStoppingFaceEncoderForClose;
+ private async Task TryLoadLastSettingsOnStartupAsync()
+ {
+ if (_startupSettingsRestoreAttempted)
+ {
+ return;
+ }
+
+ _startupSettingsRestoreAttempted = true;
+
+ var lastSettingsFile = _pickerPreferenceService.GetRememberedValue(PickerPreferenceKeys.LastSettingsFile);
+ if (string.IsNullOrWhiteSpace(lastSettingsFile))
+ {
+ return;
+ }
+
+ if (!File.Exists(lastSettingsFile))
+ {
+ _pickerPreferenceService.ForgetValue(PickerPreferenceKeys.LastSettingsFile);
+ return;
+ }
+
+ try
+ {
+ await _model.LoadSettingsFromFileAsync(lastSettingsFile);
+ _pickerPreferenceService.RememberPath(PickerPreferenceKeys.SettingsFile, lastSettingsFile);
+ }
+ catch (Exception ex)
+ {
+ await ShowMessageDialogAsync("Impostazioni", $"Impossibile caricare il file impostazioni automatico:\n{ex.GetBaseException().Message}");
+ }
+ }
+
private async void AvaloniaMainWindow_Closing(object? sender, CancelEventArgs e)
{
if (_isStoppingFaceEncoderForClose || (!_model.IsFaceEncoderRunning && !_model.IsFaceMatcherRunning))
diff --git a/imagecatalog/AvaloniaViews/AiTabView.axaml b/imagecatalog/AvaloniaViews/AiTabView.axaml
index c731f4e..0a2d395 100644
--- a/imagecatalog/AvaloniaViews/AiTabView.axaml
+++ b/imagecatalog/AvaloniaViews/AiTabView.axaml
@@ -3,112 +3,128 @@
xmlns:avaloniaDataGrid="clr-namespace:Avalonia.Controls;assembly=Avalonia.Controls.DataGrid"
xmlns:iconPacks="https://github.com/MahApps/IconPacks.Avalonia"
x:Class="ImageCatalog_2.AvaloniaViews.AiTabView">
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
diff --git a/imagecatalog/AvaloniaViews/AiTabView.axaml.cs b/imagecatalog/AvaloniaViews/AiTabView.axaml.cs
index f89ded7..852cc63 100644
--- a/imagecatalog/AvaloniaViews/AiTabView.axaml.cs
+++ b/imagecatalog/AvaloniaViews/AiTabView.axaml.cs
@@ -31,7 +31,7 @@ public partial class AiTabView : Avalonia.Controls.UserControl
OpenInExplorer(string.IsNullOrWhiteSpace(directory) ? model.CsvOutputPath : directory);
}
- private void OpenAiDestinationFolder_Click(object? sender, RoutedEventArgs e)
+ private void OpenAiSourceFolder_Click(object? sender, RoutedEventArgs e)
{
if (DataContext is DataModel model)
{
diff --git a/imagecatalog/DataModel.cs b/imagecatalog/DataModel.cs
index cea5dce..c89cb2c 100644
--- a/imagecatalog/DataModel.cs
+++ b/imagecatalog/DataModel.cs
@@ -77,6 +77,8 @@ namespace ImageCatalog_2
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);
@@ -137,7 +139,7 @@ namespace ImageCatalog_2
// Load available fonts
AvailableFonts = LoadAvailableFonts();
- RefreshNumberAiGpuCapabilities();
+ QueueRefreshNumberAiGpuCapabilities();
RefreshFaceExecutableCapabilities();
}
@@ -157,7 +159,7 @@ namespace ImageCatalog_2
_logger.LogError(ex, "AI extraction failed");
if (UseNumberAiGpu)
{
- RefreshNumberAiGpuCapabilities();
+ QueueRefreshNumberAiGpuCapabilities();
}
await InvokeOnUiThreadAsync(() => NumberAiStatsSummary = $"Errore OCR: {ex.GetBaseException().Message}").ConfigureAwait(false);
@@ -255,7 +257,7 @@ namespace ImageCatalog_2
set
{
_ai.ModelsFolderPath = value;
- RefreshNumberAiGpuCapabilities();
+ QueueRefreshNumberAiGpuCapabilities();
}
}
@@ -2281,31 +2283,86 @@ namespace ImageCatalog_2
}
}
- private void RefreshNumberAiGpuCapabilities()
+ private void QueueRefreshNumberAiGpuCapabilities()
{
if (!TryBuildNumberAiModelConfiguration(out var configuration))
{
+ _numberAiGpuValidationPending = false;
NumberAiGpuOptionEnabled = false;
_ai.UseNumberAiGpu = false;
return;
}
- NumberAiGpuOptionEnabled = NumberRecognitionEngine.TryValidateGpuRuntime(configuration, _logger, out _);
- if (!NumberAiGpuOptionEnabled)
+ _numberAiGpuValidationPending = true;
+ var requestVersion = Interlocked.Increment(ref _numberAiGpuRefreshVersion);
+ _ = RefreshNumberAiGpuCapabilitiesAsync(configuration, requestVersion);
+ }
+
+ private async Task RefreshNumberAiGpuCapabilitiesAsync(ModelConfiguration configuration, int requestVersion)
+ {
+ try
{
- _ai.UseNumberAiGpu = false;
+ 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);
}
}
private void SetUseNumberAiGpu(bool value)
{
- if (!NumberAiGpuOptionEnabled)
+ if (!value)
{
_ai.UseNumberAiGpu = false;
return;
}
- _ai.UseNumberAiGpu = value;
+ if (NumberAiGpuOptionEnabled || _numberAiGpuValidationPending)
+ {
+ _ai.UseNumberAiGpu = true;
+ return;
+ }
+
+ _ai.UseNumberAiGpu = false;
}
private bool TryBuildNumberAiModelConfiguration(out ModelConfiguration configuration)
diff --git a/imagecatalog/ImageCatalog 2.csproj b/imagecatalog/ImageCatalog 2.csproj
index 5affa11..7446b3a 100644
--- a/imagecatalog/ImageCatalog 2.csproj
+++ b/imagecatalog/ImageCatalog 2.csproj
@@ -65,16 +65,16 @@
-
-
-
-
-
+
+
+
+
+
-
-
-
-
+
+
+
+
all
diff --git a/imagecatalog/Services/AiExtractionService.cs b/imagecatalog/Services/AiExtractionService.cs
index b7ea6cc..685a43f 100644
--- a/imagecatalog/Services/AiExtractionService.cs
+++ b/imagecatalog/Services/AiExtractionService.cs
@@ -198,20 +198,7 @@ public class AiExtractionService : IAiExtractionService
{
try
{
- 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}\"");
- }
+ WriteCsvOutput(request.CsvOutputPath, extractedResults);
}
catch (Exception ex)
{
@@ -222,6 +209,24 @@ 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/PickerPreferenceService.cs b/imagecatalog/Services/PickerPreferenceService.cs
index 26f7033..49bdaea 100644
--- a/imagecatalog/Services/PickerPreferenceService.cs
+++ b/imagecatalog/Services/PickerPreferenceService.cs
@@ -13,6 +13,8 @@ public static class PickerPreferenceKeys
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";
@@ -61,6 +63,33 @@ public sealed class PickerPreferenceService
_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);