diff --git a/MaddoShared.Benchmarks/MaddoShared.Benchmarks.csproj b/MaddoShared.Benchmarks/MaddoShared.Benchmarks.csproj index 7ce2437..b712ff3 100644 --- a/MaddoShared.Benchmarks/MaddoShared.Benchmarks.csproj +++ b/MaddoShared.Benchmarks/MaddoShared.Benchmarks.csproj @@ -1,4 +1,4 @@ - + Exe @@ -9,7 +9,7 @@ - + diff --git a/MaddoShared.Tests/MaddoShared.Tests.csproj b/MaddoShared.Tests/MaddoShared.Tests.csproj index c4a7299..b2f98b7 100644 --- a/MaddoShared.Tests/MaddoShared.Tests.csproj +++ b/MaddoShared.Tests/MaddoShared.Tests.csproj @@ -9,13 +9,13 @@ - - - + + + - + diff --git a/imagecatalog/DataModel.cs b/imagecatalog/DataModel.cs index 8091907..683d296 100644 --- a/imagecatalog/DataModel.cs +++ b/imagecatalog/DataModel.cs @@ -31,6 +31,8 @@ namespace ImageCatalog_2 public ICommand LoadSettingsCommand { get; } public ICommand SelectColorCommand { get; } public ICommand SelectTransparentColorCommand { get; } + public ICommand SelectModelsFolderCommand { get; } + public ICommand SelectCsvOutputCommand { get; } private readonly ITestService _service; private readonly ILogger _logger; @@ -61,6 +63,8 @@ namespace ImageCatalog_2 AsyncTestCommand = new AsyncCommand(TestAsync); AsyncCancelOperationCommand = new AsyncCommand(CancelOperation); ProcessImagesCommand = new AsyncCommand(ProcessImages); + SelectModelsFolderCommand = new RelayCommand(SelectModelsFolder); + SelectCsvOutputCommand = new RelayCommand(SelectCsvOutput); SelectSourceFolderCommand = new RelayCommand(SelectSourceFolder); SelectDestinationFolderCommand = new RelayCommand(SelectDestinationFolder); @@ -74,6 +78,176 @@ namespace ImageCatalog_2 AvailableFonts = LoadAvailableFonts(); } + private async Task RunAiExtractionAsync(CancellationToken token) + { + // Simple stub: scan source folder for supported images and either call AIFotoONLUS.Core + // or simulate results. Write CSV output and populate PreviewResults. + if (string.IsNullOrWhiteSpace(SourcePath) || !System.IO.Directory.Exists(SourcePath)) + { + _logger.LogWarning("Source path invalid for AI extraction: {SourcePath}", SourcePath); + return; + } + + var imageFiles = System.IO.Directory.EnumerateFiles(SourcePath, "*.*", System.IO.SearchOption.TopDirectoryOnly) + .Where(f => f.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase) + || f.EndsWith(".jpeg", StringComparison.OrdinalIgnoreCase) + || f.EndsWith(".png", StringComparison.OrdinalIgnoreCase) + || f.EndsWith(".bmp", StringComparison.OrdinalIgnoreCase) + || f.EndsWith(".gif", StringComparison.OrdinalIgnoreCase)) + .ToList(); + + if (imageFiles.Count == 0) + { + _logger.LogInformation("No image files found for AI extraction in {SourcePath}", SourcePath); + return; + } + + // Clear preview + await InvokeOnUiThreadAsync(() => { PreviewResults.Clear(); }); + + // Try to locate AIFotoONLUS.Core types via reflection to avoid hard reference at compile time + Type? aiProcessorType = null; + object? aiProcessor = null; + + try + { + var assembly = AppDomain.CurrentDomain.GetAssemblies() + .FirstOrDefault(a => a.GetName().Name?.Equals("AIFotoONLUS.Core", StringComparison.OrdinalIgnoreCase) == true); + if (assembly != null) + { + aiProcessorType = assembly.GetType("AIFotoONLUS.Core.AiProcessor"); + if (aiProcessorType != null) + { + // Create instance assuming parameterless ctor + aiProcessor = Activator.CreateInstance(aiProcessorType); + } + } + } + catch (Exception ex) + { + _logger.LogDebug(ex, "AIFotoONLUS.Core not available or failed to load via reflection"); + } + + var results = new List(); + + foreach (var file in imageFiles) + { + token.ThrowIfCancellationRequested(); + + string extracted = string.Empty; + + if (aiProcessorType is not null && aiProcessor is not null) + { + try + { + // Preferred method name: ExtractNumbersFromImage(string imagePath) + var method = aiProcessorType.GetMethod("ExtractNumbersFromImage") ?? aiProcessorType.GetMethod("ExtractTextFromImage"); + if (method is not null) + { + var value = method.Invoke(aiProcessor, new object[] { file }); + if (value != null) + extracted = value.ToString() ?? string.Empty; + } + else + { + // No expected method found, fallback to simulated result + extracted = SimulateExtraction(file); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error invoking AI processor for {File}", file); + extracted = SimulateExtraction(file); + } + } + else + { + // Simulate extraction when library not available + extracted = SimulateExtraction(file); + } + + var res = new AiResult { Path = file, Text = extracted }; + results.Add(res); + + await InvokeOnUiThreadAsync(() => PreviewResults.Add(res)); + } + + // Write CSV if requested + if (!string.IsNullOrWhiteSpace(CsvOutputPath)) + { + try + { + var dir = System.IO.Path.GetDirectoryName(CsvOutputPath) ?? string.Empty; + if (!string.IsNullOrWhiteSpace(dir) && !System.IO.Directory.Exists(dir)) + { + System.IO.Directory.CreateDirectory(dir); + } + + using var sw = new System.IO.StreamWriter(CsvOutputPath, false, System.Text.Encoding.UTF8); + sw.WriteLine("Path,Text"); + foreach (var r in results) + { + var safeText = (r.Text ?? string.Empty).Replace("\"", "\"\""); + sw.WriteLine($"\"{r.Path}\",\"{safeText}\""); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to write CSV to {CsvOutputPath}", CsvOutputPath); + } + } + } + + private string SimulateExtraction(string file) + { + // Cheap heuristic: return filename digits + var name = System.IO.Path.GetFileNameWithoutExtension(file); + var digits = new string(name.Where(char.IsDigit).ToArray()); + if (string.IsNullOrEmpty(digits)) return ""; + return digits; + } + + private Task InvokeOnUiThreadAsync(Action action) + { + // Use SynchronizationContext via Task to ensure UI thread update + return Task.Run(() => + { + System.Windows.Application.Current?.Dispatcher.Invoke(action); + }); + } + + // AI properties + private bool _extractNumbers; + public bool ExtractNumbers + { + get => _extractNumbers; + set { _extractNumbers = value; NotifyPropertyChanged(); } + } + + private string _modelsFolderPath = string.Empty; + public string ModelsFolderPath + { + get => _modelsFolderPath; + set { _modelsFolderPath = value; NotifyPropertyChanged(); } + } + + private string _csvOutputPath = string.Empty; + public string CsvOutputPath + { + get => _csvOutputPath; + set { _csvOutputPath = value; NotifyPropertyChanged(); } + } + + // Preview results for DataGrid + private System.Collections.ObjectModel.ObservableCollection _previewResults = new(); + public System.Collections.ObjectModel.ObservableCollection PreviewResults => _previewResults; + + public class AiResult + { + public string Path { get; set; } = string.Empty; + public string Text { get; set; } = string.Empty; + } + private List LoadAvailableFonts() { var fonts = new List(); @@ -991,6 +1165,23 @@ namespace ImageCatalog_2 _results, OnImageProcessed, token); + + // AI integration stub: if ExtractNumbers is enabled, simulate or invoke OCR processing + if (ExtractNumbers) + { + try + { + await RunAiExtractionAsync(token); + } + catch (OperationCanceledException) + { + _logger.LogInformation("AI extraction canceled"); + } + catch (Exception ex) + { + _logger.LogError(ex, "AI extraction failed"); + } + } // Compute final averages and show only averages (do not show raw seconds) var finalProcessed = System.Threading.Volatile.Read(ref _processedAtomic); @@ -1161,6 +1352,8 @@ namespace ImageCatalog_2 public event EventHandler SelectSourceFolderRequested; public event EventHandler SelectDestinationFolderRequested; public event EventHandler SelectLogoFileRequested; + public event EventHandler SelectModelsFolderRequested; + public event EventHandler SelectCsvOutputRequested; public event EventHandler SaveSettingsRequested; public event EventHandler LoadSettingsRequested; public event EventHandler SelectColorRequested; @@ -1183,6 +1376,16 @@ namespace ImageCatalog_2 SelectLogoFileRequested?.Invoke(this, EventArgs.Empty); } + private void SelectModelsFolder(object parameter) + { + SelectModelsFolderRequested?.Invoke(this, EventArgs.Empty); + } + + private void SelectCsvOutput(object parameter) + { + SelectCsvOutputRequested?.Invoke(this, EventArgs.Empty); + } + private void SaveSettings(object parameter) { SaveSettingsRequested?.Invoke(this, null); diff --git a/imagecatalog/ImageCatalog 2.csproj b/imagecatalog/ImageCatalog 2.csproj index 4563b31..af7d536 100644 --- a/imagecatalog/ImageCatalog 2.csproj +++ b/imagecatalog/ImageCatalog 2.csproj @@ -41,6 +41,7 @@ + diff --git a/imagecatalog/MainWindow.xaml b/imagecatalog/MainWindow.xaml index b2bd652..7634adf 100644 --- a/imagecatalog/MainWindow.xaml +++ b/imagecatalog/MainWindow.xaml @@ -4,7 +4,33 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" - Title="Image Catalog - WPF" Height="490" Width="800"> + Title="Image Catalog - WPF" Height="490" Width="800" + Background="{DynamicResource WindowBackgroundBrush}" Foreground="{DynamicResource ControlForegroundBrush}"> + + + + + + + + + + + + + + + + + + + + + + + + + @@ -210,6 +236,51 @@ + + + + + + + + + + + + + + + + + +