feat: Update package references and enhance AI extraction service with CSV output functionality

This commit is contained in:
Maddo 2026-05-24 17:29:05 +02:00
commit af74c90ce7
12 changed files with 400 additions and 153 deletions

View file

@ -8,10 +8,10 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.8" />
</ItemGroup>
</Project>

View file

@ -10,8 +10,8 @@
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.8" />
</ItemGroup>
<ItemGroup>

View file

@ -9,15 +9,15 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageReference Include="MSTest.TestAdapter" Version="4.1.0" />
<PackageReference Include="MSTest.TestFramework" Version="4.1.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
<PackageReference Include="MSTest.TestAdapter" Version="4.2.3" />
<PackageReference Include="MSTest.TestFramework" Version="4.2.3" />
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="Shouldly" Version="4.3.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.3" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.7" />
<PackageReference Include="SixLabors.Fonts" Version="2.1.3" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.8" />
<PackageReference Include="SixLabors.ImageSharp" Version="4.0.0" />
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="3.0.0" />
<PackageReference Include="SixLabors.Fonts" Version="3.0.0" />
</ItemGroup>
<ItemGroup>

View file

@ -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);
}
}
}
}

View file

@ -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 TestMethod1()
public void WriteCsvOutput_UsesLegacyCompatibleHeaderAndFilenameColumn()
{
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);
}
}
}
}

View file

@ -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<PickerPreferenceService>();
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))

View file

@ -3,23 +3,21 @@
xmlns:avaloniaDataGrid="clr-namespace:Avalonia.Controls;assembly=Avalonia.Controls.DataGrid"
xmlns:iconPacks="https://github.com/MahApps/IconPacks.Avalonia"
x:Class="ImageCatalog_2.AvaloniaViews.AiTabView">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TabControl Margin="4">
<TabItem Header="Esecuzione">
<Grid RowDefinitions="Auto,*">
<ScrollViewer Grid.Row="0" VerticalScrollBarVisibility="Auto">
<StackPanel Margin="4">
<StackPanel Margin="4" Spacing="8">
<TextBlock Text="AI / OCR" FontWeight="Bold" />
<StackPanel Orientation="Horizontal" Spacing="12" Margin="0,6,0,0">
<StackPanel Orientation="Horizontal" Spacing="12">
<CheckBox Content="Usa GPU"
IsChecked="{Binding UseNumberAiGpu, Mode=TwoWay}"
IsEnabled="{Binding NumberAiGpuOptionEnabled}" />
<CheckBox Content="Includi thumbnail" IsChecked="{Binding IncludeNumberAiThumbnails, Mode=TwoWay}" />
</StackPanel>
<Grid Margin="0,8,0,0" ColumnDefinitions="Auto,Auto,*" ColumnSpacing="8">
<Grid ColumnDefinitions="Auto,Auto,*" ColumnSpacing="8">
<TextBlock Grid.Column="0" Text="Carico OCR:" VerticalAlignment="Center" />
<ComboBox Grid.Column="1"
Width="84"
@ -32,30 +30,19 @@
FontWeight="SemiBold" />
</Grid>
<Grid Margin="0,8,0,0" ColumnDefinitions="Auto,*,Auto" ColumnSpacing="6">
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="6">
<TextBlock Grid.Column="0" Text="Sorgente:" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Text="{Binding DestinationPath, Mode=OneWay}" IsReadOnly="True" VerticalAlignment="Center" />
<Button Grid.Column="2" Width="72" Click="OpenAiDestinationFolder_Click">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="Folder" Width="14" Height="14" />
<TextBlock Text="Apri" />
</StackPanel>
</Button>
</Grid>
<TextBlock Text="Modelli" FontWeight="Bold" Margin="0,8,0,0" />
<Grid Margin="0,4,0,0" ColumnDefinitions="Auto,*,Auto,Auto">
<TextBlock Text="Cartella modelli:" VerticalAlignment="Center" Margin="0,0,8,0" Grid.Column="0" />
<TextBox Text="{Binding ModelsFolderPath, Mode=TwoWay}" Grid.Column="1" VerticalAlignment="Center" />
<Button Width="104" Margin="6,0,0,0" Command="{Binding SelectModelsFolderCommand}"
Grid.Column="2">
<TextBox Grid.Column="1"
Text="{Binding DestinationPath, Mode=OneWay}"
IsReadOnly="True"
VerticalAlignment="Center" />
<Button Grid.Column="2" Width="104" Command="{Binding SelectDestinationFolderCommand}">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="FolderOpenOutline" Width="14" Height="14" />
<TextBlock Text="Scegli..." />
</StackPanel>
</Button>
<Button Width="72" Margin="6,0,0,0" Grid.Column="3"
Click="OpenModelsFolder_Click">
<Button Grid.Column="3" Width="72" Click="OpenAiSourceFolder_Click">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="Folder" Width="14" Height="14" />
<TextBlock Text="Apri" />
@ -63,7 +50,7 @@
</Button>
</Grid>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" Margin="0,8,0,0" Spacing="8">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" Spacing="8">
<Button Command="{Binding StartAiCommand}" Width="132">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="PlayCircle" Width="16" Height="16" Foreground="#2E7D32" />
@ -78,19 +65,17 @@
</Button>
</StackPanel>
<TextBlock Text="Output CSV" FontWeight="Bold" Margin="0,8,0,0" />
<Grid Margin="0,4,0,0" ColumnDefinitions="Auto,*,Auto,Auto">
<TextBlock Text="Output CSV" FontWeight="Bold" />
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="6">
<TextBlock Text="Percorso CSV:" VerticalAlignment="Center" Margin="0,0,8,0" Grid.Column="0" />
<TextBox Text="{Binding CsvOutputPath, Mode=TwoWay}" Grid.Column="1" VerticalAlignment="Center" />
<Button Width="104" Margin="6,0,0,0" Command="{Binding SelectCsvOutputCommand}"
Grid.Column="2">
<Button Width="104" Command="{Binding SelectCsvOutputCommand}" Grid.Column="2">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="FileOutline" Width="14" Height="14" />
<TextBlock Text="Scegli..." />
</StackPanel>
</Button>
<Button Width="72" Margin="6,0,0,0" Grid.Column="3"
Click="OpenCsvOutputFolder_Click">
<Button Width="72" Grid.Column="3" Click="OpenCsvOutputFolder_Click">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="Folder" Width="14" Height="14" />
<TextBlock Text="Apri" />
@ -98,17 +83,48 @@
</Button>
</Grid>
<TextBlock Text="Anteprima risultati" FontWeight="Bold" Margin="0,8,0,0" />
<ProgressBar Minimum="0" Maximum="100" Value="{Binding AiProgress}" Height="16" Margin="0,4,0,4" />
<TextBlock Text="Anteprima risultati" FontWeight="Bold" />
<ProgressBar Minimum="0" Maximum="100" Value="{Binding AiProgress}" Height="16" Margin="0,0,0,4" />
</StackPanel>
</ScrollViewer>
<avaloniaDataGrid:DataGrid Grid.Row="1" ItemsSource="{Binding PreviewResults}" IsReadOnly="True"
AutoGenerateColumns="False" Margin="4,4,4,4" CanUserResizeColumns="True" VerticalAlignment="Stretch">
<avaloniaDataGrid:DataGrid Grid.Row="1"
ItemsSource="{Binding PreviewResults}"
IsReadOnly="True"
AutoGenerateColumns="False"
Margin="4,4,4,4"
CanUserResizeColumns="True"
VerticalAlignment="Stretch">
<avaloniaDataGrid:DataGrid.Columns>
<avaloniaDataGrid:DataGridTextColumn Header="Path" Binding="{Binding Path}" Width="*" />
<avaloniaDataGrid:DataGridTextColumn Header="Text" Binding="{Binding Text}" Width="2*" />
</avaloniaDataGrid:DataGrid.Columns>
</avaloniaDataGrid:DataGrid>
</Grid>
</TabItem>
<TabItem Header="Impostazioni">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="8" Spacing="8">
<TextBlock Text="Modelli" FontWeight="Bold" />
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="6">
<TextBlock Text="Cartella modelli:" VerticalAlignment="Center" Margin="0,0,8,0" Grid.Column="0" />
<TextBox Text="{Binding ModelsFolderPath, Mode=TwoWay}" Grid.Column="1" VerticalAlignment="Center" />
<Button Width="104" Command="{Binding SelectModelsFolderCommand}" Grid.Column="2">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="FolderOpenOutline" Width="14" Height="14" />
<TextBlock Text="Scegli..." />
</StackPanel>
</Button>
<Button Width="72" Grid.Column="3" Click="OpenModelsFolder_Click">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="Folder" Width="14" Height="14" />
<TextBlock Text="Apri" />
</StackPanel>
</Button>
</Grid>
</StackPanel>
</ScrollViewer>
</TabItem>
</TabControl>
</UserControl>

View file

@ -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)
{

View file

@ -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
{
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)

View file

@ -65,16 +65,16 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="AIFotoONLUS.Core" Version="0.1.2" Condition="'$(UseLocalAIFotoONLUS)' != 'true'" />
<PackageReference Include="AutoMapper" Version="16.1.0" />
<PackageReference Include="IconPacks.Avalonia" Version="1.3.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.5" />
<PackageReference Include="AutoMapper" Version="16.1.1" />
<PackageReference Include="IconPacks.Avalonia" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.8" />
<PackageReference Include="MinVer" Version="7.0.0" PrivateAssets="all" />
<PackageReference Include="Avalonia" Version="11.3.12" />
<PackageReference Include="Avalonia.Desktop" Version="11.3.12" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.12" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.3.12" />
<PackageReference Include="Avalonia" Version="12.0.3" />
<PackageReference Include="Avalonia.Desktop" Version="12.0.3" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="12.0.3" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="12.0.0" />
<PackageReference Include="Microsoft.DotNet.UpgradeAssistant.Extensions.Default.Analyzers" Version="0.4.421302">
<PrivateAssets>all</PrivateAssets>
</PackageReference>

View file

@ -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<AiResultItem> 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;

View file

@ -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);