feat: Add support for thumbnail inclusion in AI processing and enhance UI bindings
Some checks failed
Build Windows Avalonia / build (push) Failing after 1m48s
Build Windows Avalonia / release (push) Has been skipped

This commit is contained in:
MaddoScientisto 2026-05-09 17:53:15 +02:00
commit 7e105e3738
9 changed files with 235 additions and 27 deletions

View file

@ -1,5 +1,7 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Platform.Storage;
using Avalonia.Styling;
using Avalonia.Threading;
@ -135,6 +137,11 @@ public partial class AvaloniaMainWindow : Window
{
// Color is set by typing hex directly in the TextBox.
};
_model.ShowMessageRequested += async (_, args) =>
{
await ShowMessageDialogAsync(args.Item1, args.Item2);
};
}
private bool _isStoppingFaceEncoderForClose;
@ -182,4 +189,48 @@ public partial class AvaloniaMainWindow : Window
{
_ = this.FindControl<Avalonia.Controls.Button>("ThemeToggleButton");
}
private async Task ShowMessageDialogAsync(string title, string message)
{
var dialog = new Window
{
Title = title,
Width = 480,
CanResize = false,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
SizeToContent = SizeToContent.Height
};
dialog.Content = BuildMessageDialogContent(message, () => dialog.Close());
await dialog.ShowDialog(this);
}
private static Control BuildMessageDialogContent(string message, Action closeDialog)
{
var layout = new StackPanel
{
Margin = new Thickness(16),
Spacing = 12
};
layout.Children.Add(new TextBlock
{
Text = message,
TextWrapping = Avalonia.Media.TextWrapping.Wrap,
MaxWidth = 420
});
var closeButton = new Button
{
Content = "OK",
MinWidth = 88,
HorizontalAlignment = HorizontalAlignment.Right
};
closeButton.Click += (_, _) => closeDialog();
layout.Children.Add(closeButton);
return layout;
}
}

View file

@ -12,8 +12,12 @@
<ScrollViewer Grid.Row="0" VerticalScrollBarVisibility="Auto">
<StackPanel Margin="4">
<TextBlock Text="AI / OCR" FontWeight="Bold" />
<CheckBox Content="Estrai numeri dalle immagini" IsChecked="{Binding ExtractNumbers}" Margin="0,6,0,0" />
<CheckBox Content="Usa GPU" IsChecked="{Binding UseNumberAiGpu, Mode=TwoWay}" Margin="0,2,0,0" />
<StackPanel Orientation="Horizontal" Spacing="12" Margin="0,6,0,0">
<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="6">
<TextBlock Grid.Column="0" Text="Sorgente:" VerticalAlignment="Center" />
@ -47,8 +51,18 @@
</Grid>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" Margin="0,8,0,0" Spacing="8">
<Button Content="Avvia AI" Command="{Binding StartAiCommand}" Width="120" />
<Button Content="Annulla" Command="{Binding AsyncCancelOperationCommand}" Width="120" />
<Button Command="{Binding StartAiCommand}" Width="132">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="PlayCircle" Width="16" Height="16" Foreground="#2E7D32" />
<TextBlock Text="Avvia AI" />
</StackPanel>
</Button>
<Button Command="{Binding AsyncCancelOperationCommand}" Width="132">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
<iconPacks:PackIconMaterial Kind="Cancel" Width="16" Height="16" Foreground="#C62828" />
<TextBlock Text="Annulla" />
</StackPanel>
</Button>
</StackPanel>
<TextBlock Text="Output CSV" FontWeight="Bold" Margin="0,8,0,0" />

View file

@ -112,6 +112,11 @@ internal static class CommandLineOperationRunner
model.UseFaceGpu = options.UseGpu.Value;
}
}
if (options.IncludeThumbnails.HasValue)
{
model.IncludeNumberAiThumbnails = options.IncludeThumbnails.Value;
}
}
private static string NormalizeOperation(string operation)
@ -159,6 +164,14 @@ internal static class CommandLineOperationRunner
case "--cpu":
options.UseGpu = false;
break;
case "--include-thumbnails":
case "--include-tn":
options.IncludeThumbnails = true;
break;
case "--no-thumbnails":
case "--no-tn":
options.IncludeThumbnails = false;
break;
case "--headless":
case "--cli":
break;
@ -213,7 +226,7 @@ internal static class CommandLineOperationRunner
private static void WriteUsage()
{
Console.WriteLine("Usage: ImageCatalog --config <settings.xml> --operation <image-processing|number-ai|face-ai|race-upload> [--models <folder>] [--csv <path>] [--cpu|--gpu]");
Console.WriteLine("Usage: ImageCatalog --config <settings.xml> --operation <image-processing|number-ai|face-ai|race-upload> [--models <folder>] [--csv <path>] [--cpu|--gpu] [--include-thumbnails|--no-thumbnails]");
}
private sealed class CommandLineOptions
@ -224,5 +237,6 @@ internal static class CommandLineOperationRunner
public string ModelsPath { get; set; } = string.Empty;
public string CsvPath { get; set; } = string.Empty;
public bool? UseGpu { get; set; }
public bool? IncludeThumbnails { get; set; }
}
}

View file

@ -16,6 +16,7 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
using AIFotoONLUS.Core;
using AutoMapper;
using MaddoShared;
using Microsoft.Extensions.Logging;
@ -117,6 +118,7 @@ namespace ImageCatalog_2
// Load available fonts
AvailableFonts = LoadAvailableFonts();
RefreshNumberAiGpuCapabilities();
RefreshFaceExecutableCapabilities();
}
@ -131,6 +133,16 @@ namespace ImageCatalog_2
{
// user cancelled
}
catch (Exception ex)
{
_logger.LogError(ex, "AI extraction failed");
if (UseNumberAiGpu)
{
RefreshNumberAiGpuCapabilities();
}
await ShowErrorMessageAsync("Errore AI", ex.GetBaseException().Message).ConfigureAwait(false);
}
finally
{
MainToken = null;
@ -162,12 +174,19 @@ namespace ImageCatalog_2
{
SearchRoot = searchRoot,
Recursive = recursive,
IncludeThumbnails = IncludeNumberAiThumbnails,
ModelsFolderPath = ModelsFolderPath,
UseGpu = UseNumberAiGpu,
CsvOutputPath = CsvOutputPath
},
token,
result => InvokeOnUiThreadAsync(() => PreviewResults.Add(result)),
result => InvokeOnUiThreadAsync(() =>
{
if (!string.IsNullOrWhiteSpace(result.Text))
{
PreviewResults.Add(result);
}
}),
progress => InvokeOnUiThreadAsync(() => AiProgress = progress)).ConfigureAwait(false);
}
@ -200,7 +219,11 @@ namespace ImageCatalog_2
public string ModelsFolderPath
{
get => _ai.ModelsFolderPath;
set => _ai.ModelsFolderPath = value;
set
{
_ai.ModelsFolderPath = value;
RefreshNumberAiGpuCapabilities();
}
}
public string CsvOutputPath
@ -212,7 +235,19 @@ namespace ImageCatalog_2
public bool UseNumberAiGpu
{
get => _ai.UseNumberAiGpu;
set => _ai.UseNumberAiGpu = value;
set => SetUseNumberAiGpu(value);
}
public bool NumberAiGpuOptionEnabled
{
get => _ai.NumberAiGpuOptionEnabled;
private set => _ai.NumberAiGpuOptionEnabled = value;
}
public bool IncludeNumberAiThumbnails
{
get => _ai.IncludeNumberAiThumbnails;
set => _ai.IncludeNumberAiThumbnails = value;
}
public string FaceExecutablePath
@ -1302,23 +1337,6 @@ namespace ImageCatalog_2
},
speed => { SpeedCounter = speed; }).ConfigureAwait(false);
// AI integration: OCR runs over processed output so it matches the face AI input folder.
if (ExtractNumbers)
{
try
{
await RunAiExtractionCoreAsync(token, useDestination: true, recursive: true);
}
catch (OperationCanceledException)
{
_logger.LogInformation("AI extraction canceled");
}
catch (Exception ex)
{
_logger.LogError(ex, "AI extraction failed");
}
}
SpeedCounter = runResult.FinalSpeedCounter;
}
catch (OperationCanceledException)
@ -1762,6 +1780,71 @@ namespace ImageCatalog_2
}
}
private void RefreshNumberAiGpuCapabilities()
{
if (!TryBuildNumberAiModelConfiguration(out var configuration))
{
NumberAiGpuOptionEnabled = false;
_ai.UseNumberAiGpu = false;
return;
}
NumberAiGpuOptionEnabled = NumberRecognitionEngine.TryValidateGpuRuntime(configuration, _logger, out _);
if (!NumberAiGpuOptionEnabled)
{
_ai.UseNumberAiGpu = false;
}
}
private void SetUseNumberAiGpu(bool value)
{
if (!NumberAiGpuOptionEnabled)
{
_ai.UseNumberAiGpu = false;
return;
}
_ai.UseNumberAiGpu = value;
}
private bool TryBuildNumberAiModelConfiguration(out ModelConfiguration configuration)
{
configuration = null!;
if (string.IsNullOrWhiteSpace(ModelsFolderPath))
{
return false;
}
var modelsRoot = Path.GetFullPath(ModelsFolderPath.Trim().Trim('"'));
if (!Directory.Exists(modelsRoot))
{
return false;
}
configuration = new ModelConfiguration
{
DetectionCfg = Path.Combine(modelsRoot, "detection.cfg"),
DetectionWeights = Path.Combine(modelsRoot, "detection.weights"),
RecognitionCfg = Path.Combine(modelsRoot, "recognition.cfg"),
RecognitionWeights = Path.Combine(modelsRoot, "recognition.weights"),
UseGpu = true
};
return File.Exists(configuration.DetectionCfg)
&& File.Exists(configuration.DetectionWeights)
&& File.Exists(configuration.RecognitionCfg)
&& File.Exists(configuration.RecognitionWeights);
}
private Task ShowErrorMessageAsync(string title, string message)
{
return InvokeOnUiThreadAsync(() =>
{
ShowMessageRequested?.Invoke(this, Tuple.Create(title, message, 0));
});
}
private void SetUseFaceGpu(bool value)
{
var currentValue = _ai.UseFaceGpu;

View file

@ -278,6 +278,10 @@ namespace ImageCatalog_2.Models
[XmlElement("AI_UsaGpuNumeri")]
public bool UseNumberAiGpu { get; set; }
[JsonPropertyName("IncludeNumberAiThumbnails")]
[XmlElement("AI_IncludiThumbnailNumeri")]
public bool IncludeNumberAiThumbnails { get; set; }
[JsonPropertyName("FaceExecutablePath")]
[XmlElement("AI_FaceExecutablePath")]
public string FaceExecutablePath { get; set; } = string.Empty;

View file

@ -34,6 +34,7 @@ public class AiExtractionService : IAiExtractionService
|| f.EndsWith(".png", StringComparison.OrdinalIgnoreCase)
|| f.EndsWith(".bmp", StringComparison.OrdinalIgnoreCase)
|| f.EndsWith(".gif", StringComparison.OrdinalIgnoreCase))
.Where(f => request.IncludeThumbnails || !Path.GetFileName(f).StartsWith("tn_", StringComparison.OrdinalIgnoreCase))
.ToList();
var extractedResults = new List<AiResultItem>();
@ -44,6 +45,7 @@ public class AiExtractionService : IAiExtractionService
var processed = 0;
var total = imageFiles.Count;
var failed = 0;
Exception? firstFailure = null;
foreach (var file in imageFiles)
{
@ -58,6 +60,7 @@ public class AiExtractionService : IAiExtractionService
catch (Exception ex)
{
failed++;
firstFailure ??= ex;
_logger.LogWarning(ex, "Error processing AI OCR for {File}", file);
}
@ -72,7 +75,7 @@ public class AiExtractionService : IAiExtractionService
if (imageFiles.Count > 0 && failed == imageFiles.Count)
{
throw new InvalidOperationException($"AI OCR failed for all {imageFiles.Count} image(s). See previous log entries for details.");
throw new InvalidOperationException($"AI OCR failed for all {imageFiles.Count} image(s). See previous log entries for details.", firstFailure);
}
if (!string.IsNullOrWhiteSpace(request.CsvOutputPath))
@ -89,8 +92,9 @@ public class AiExtractionService : IAiExtractionService
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($"\"{r.Path}\",\"{safeText}\"");
sw.WriteLine($"\"{csvFileName}\",\"{safeText}\"");
}
}
catch (Exception ex)

View file

@ -9,6 +9,7 @@ public sealed class AiExtractionRequest
{
public required string SearchRoot { get; init; }
public required bool Recursive { get; init; }
public bool IncludeThumbnails { get; init; }
public required string ModelsFolderPath { get; init; }
public bool UseGpu { get; init; }
public string CsvOutputPath { get; init; } = string.Empty;

View file

@ -49,6 +49,28 @@ public class AiSettingsViewModel : ViewModelBase
}
}
private bool _numberAiGpuOptionEnabled;
public bool NumberAiGpuOptionEnabled
{
get => _numberAiGpuOptionEnabled;
set
{
_numberAiGpuOptionEnabled = value;
NotifyPropertyChanged();
}
}
private bool _includeNumberAiThumbnails;
public bool IncludeNumberAiThumbnails
{
get => _includeNumberAiThumbnails;
set
{
_includeNumberAiThumbnails = value;
NotifyPropertyChanged();
}
}
private string _faceExecutablePath = string.Empty;
public string FaceExecutablePath
{