diff --git a/.gitignore b/.gitignore index 7e95a94..9a1e321 100644 --- a/.gitignore +++ b/.gitignore @@ -256,4 +256,3 @@ paket-files/ .idea/ *.sln.iml .vscode/settings.json -tmp/** \ No newline at end of file diff --git a/MaddoShared.Tests/DataModelCharacterizationTests.cs b/MaddoShared.Tests/DataModelCharacterizationTests.cs index 0a94ae9..423acb5 100644 --- a/MaddoShared.Tests/DataModelCharacterizationTests.cs +++ b/MaddoShared.Tests/DataModelCharacterizationTests.cs @@ -137,24 +137,6 @@ public class DataModelCharacterizationTests model.IncludeNumberAiThumbnails.ShouldBeTrue(); } - [TestMethod] - public void NumberAiWorkload_DefaultsToThreeAndClampsToFive() - { - var model = CreateModel(); - model.NumberAiWorkloadLevel.ShouldBe(3); - - string? changed = null; - model.PropertyChanged += (_, args) => changed = args.PropertyName; - - model.NumberAiWorkloadLevel = 99; - - changed.ShouldBe(nameof(DataModel.NumberAiWorkloadLevel)); - model.NumberAiWorkloadLevel.ShouldBe(3); - - model.Ai.NumberAiWorkloadLevel = 5; - model.NumberAiWorkloadLevel.ShouldBe(5); - } - [TestMethod] public void CommandLineOperationRunner_DetectsHeadlessRequest() { diff --git a/imagecatalog/AvaloniaViews/AiTabView.axaml b/imagecatalog/AvaloniaViews/AiTabView.axaml index c731f4e..9436e1f 100644 --- a/imagecatalog/AvaloniaViews/AiTabView.axaml +++ b/imagecatalog/AvaloniaViews/AiTabView.axaml @@ -19,19 +19,6 @@ - - - - - - diff --git a/imagecatalog/CommandLineOperationRunner.cs b/imagecatalog/CommandLineOperationRunner.cs index 5aafa69..deed5bd 100644 --- a/imagecatalog/CommandLineOperationRunner.cs +++ b/imagecatalog/CommandLineOperationRunner.cs @@ -117,11 +117,6 @@ internal static class CommandLineOperationRunner { model.IncludeNumberAiThumbnails = options.IncludeThumbnails.Value; } - - if (options.NumberAiWorkloadLevel.HasValue) - { - model.NumberAiWorkloadLevel = options.NumberAiWorkloadLevel.Value; - } } private static string NormalizeOperation(string operation) @@ -177,10 +172,6 @@ internal static class CommandLineOperationRunner case "--no-tn": options.IncludeThumbnails = false; break; - case "--workload": - case "--ai-workload": - options.NumberAiWorkloadLevel = int.Parse(ReadValue(args, ref i, arg)); - break; case "--headless": case "--cli": break; @@ -218,10 +209,6 @@ internal static class CommandLineOperationRunner case "--csv": options.CsvPath = value; break; - case "--workload": - case "--ai-workload": - options.NumberAiWorkloadLevel = int.Parse(value); - break; default: throw new ArgumentException($"Unknown argument: {arg}"); } @@ -239,7 +226,7 @@ internal static class CommandLineOperationRunner private static void WriteUsage() { - Console.WriteLine("Usage: ImageCatalog --config --operation [--models ] [--csv ] [--cpu|--gpu] [--include-thumbnails|--no-thumbnails] [--workload <1-5>]"); + Console.WriteLine("Usage: ImageCatalog --config --operation [--models ] [--csv ] [--cpu|--gpu] [--include-thumbnails|--no-thumbnails]"); } private sealed class CommandLineOptions @@ -251,6 +238,5 @@ internal static class CommandLineOperationRunner public string CsvPath { get; set; } = string.Empty; public bool? UseGpu { get; set; } public bool? IncludeThumbnails { get; set; } - public int? NumberAiWorkloadLevel { get; set; } } } \ No newline at end of file diff --git a/imagecatalog/DataModel.cs b/imagecatalog/DataModel.cs index 66ce8d4..88e509e 100644 --- a/imagecatalog/DataModel.cs +++ b/imagecatalog/DataModel.cs @@ -131,7 +131,7 @@ namespace ImageCatalog_2 } catch (OperationCanceledException) { - await InvokeOnUiThreadAsync(() => NumberAiStatsSummary = "OCR annullato.").ConfigureAwait(false); + // user cancelled } catch (Exception ex) { @@ -141,8 +141,6 @@ namespace ImageCatalog_2 RefreshNumberAiGpuCapabilities(); } - await InvokeOnUiThreadAsync(() => NumberAiStatsSummary = $"Errore OCR: {ex.GetBaseException().Message}").ConfigureAwait(false); - await ShowErrorMessageAsync("Errore AI", ex.GetBaseException().Message).ConfigureAwait(false); } finally @@ -169,10 +167,9 @@ namespace ImageCatalog_2 { PreviewResults.Clear(); AiProgress = 0; - NumberAiStatsSummary = BuildNumberAiIdleSummary(); }).ConfigureAwait(false); - var summary = await _aiExtractionService.RunAsync( + await _aiExtractionService.RunAsync( new AiExtractionRequest { SearchRoot = searchRoot, @@ -180,7 +177,6 @@ namespace ImageCatalog_2 IncludeThumbnails = IncludeNumberAiThumbnails, ModelsFolderPath = ModelsFolderPath, UseGpu = UseNumberAiGpu, - WorkloadLevel = NumberAiWorkloadLevel, CsvOutputPath = CsvOutputPath }, token, @@ -191,17 +187,7 @@ namespace ImageCatalog_2 PreviewResults.Add(result); } }), - progress => InvokeOnUiThreadAsync(() => - { - AiProgress = progress.PercentComplete; - NumberAiStatsSummary = BuildNumberAiProgressSummary(progress); - })).ConfigureAwait(false); - - await InvokeOnUiThreadAsync(() => - { - AiProgress = summary.TotalFiles > 0 ? 100 : 0; - NumberAiStatsSummary = BuildNumberAiCompletionSummary(summary); - }).ConfigureAwait(false); + progress => InvokeOnUiThreadAsync(() => AiProgress = progress)).ConfigureAwait(false); } /// @@ -264,20 +250,6 @@ namespace ImageCatalog_2 set => _ai.IncludeNumberAiThumbnails = value; } - public IReadOnlyList NumberAiWorkloadOptions { get; } = [1, 2, 3, 4, 5]; - - public int NumberAiWorkloadLevel - { - get => _ai.NumberAiWorkloadLevel; - set => _ai.NumberAiWorkloadLevel = NormalizeNumberAiWorkloadLevel(value); - } - - public string NumberAiStatsSummary - { - get => _ai.NumberAiStatsSummary; - private set => _ai.NumberAiStatsSummary = value; - } - public string FaceExecutablePath { get => _ai.FaceExecutablePath; @@ -447,30 +419,6 @@ namespace ImageCatalog_2 set => _ai.AiProgress = value; } - private string BuildNumberAiIdleSummary() - { - var workerCount = ResolveNumberAiWorkerCount(UseNumberAiGpu, NumberAiWorkloadLevel); - var unit = UseNumberAiGpu ? "batch" : "worker"; - return $"In attesa. Carico {NumberAiWorkloadLevel}/5, {workerCount} {unit}, 0.00 img/s."; - } - - private static string BuildNumberAiProgressSummary(AiExtractionProgressUpdate progress) - { - var unit = progress.UseGpu ? "batch" : "worker"; - return $"{progress.ProcessedFiles}/{progress.TotalFiles} immagini, media {progress.AverageImagesPerSecond:F2} img/s, carico {progress.WorkloadLevel}/5, {progress.WorkerCount} {unit}."; - } - - private static string BuildNumberAiCompletionSummary(AiExtractionRunSummary summary) - { - if (summary.TotalFiles == 0) - { - return "Nessuna immagine trovata per OCR."; - } - - var unit = summary.UseGpu ? "batch" : "worker"; - return $"Completato: {summary.ProcessedFiles}/{summary.TotalFiles} immagini, media finale {summary.AverageImagesPerSecond:F2} img/s, errori {summary.FailedFiles}, carico {summary.WorkloadLevel}/5, {summary.WorkerCount} {unit}."; - } - private List LoadAvailableFonts() { #if WINDOWS @@ -2219,36 +2167,6 @@ namespace ImageCatalog_2 return value is >= 1 and <= 5 ? value : 3; } - private static int NormalizeNumberAiWorkloadLevel(int value) - { - return value is >= 1 and <= 5 ? value : 3; - } - - private static int ResolveNumberAiWorkerCount(bool useGpu, int workloadLevel) - { - var normalized = NormalizeNumberAiWorkloadLevel(workloadLevel); - var maxWorkers = Math.Max(1, Environment.ProcessorCount); - var requestedWorkers = useGpu - ? normalized switch - { - 1 => 4, - 2 => 8, - 3 => 16, - 4 => 24, - _ => 32 - } - : normalized switch - { - 1 => 1, - 2 => 2, - 3 => 3, - 4 => 4, - _ => 5 - }; - - return useGpu ? requestedWorkers : Math.Min(requestedWorkers, maxWorkers); - } - private static int NormalizeFaceMinSize(int value) { return value > 0 ? value : 35; diff --git a/imagecatalog/ImageCatalog 2.csproj b/imagecatalog/ImageCatalog 2.csproj index 734b648..7c33f76 100644 --- a/imagecatalog/ImageCatalog 2.csproj +++ b/imagecatalog/ImageCatalog 2.csproj @@ -36,9 +36,6 @@ true - - $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\..\AIFotoONLUS\src\AIFotoONLUS.Core\bin\$(Configuration)\net10.0')) - embedded @@ -150,13 +147,4 @@ - - - - - - - - - \ No newline at end of file diff --git a/imagecatalog/Models/SettingsDto.cs b/imagecatalog/Models/SettingsDto.cs index e9de47f..f5e7111 100644 --- a/imagecatalog/Models/SettingsDto.cs +++ b/imagecatalog/Models/SettingsDto.cs @@ -282,10 +282,6 @@ namespace ImageCatalog_2.Models [XmlElement("AI_IncludiThumbnailNumeri")] public bool IncludeNumberAiThumbnails { get; set; } - [JsonPropertyName("NumberAiWorkloadLevel")] - [XmlElement("AI_LivelloCaricoNumeri")] - public int NumberAiWorkloadLevel { get; set; } = 3; - [JsonPropertyName("FaceExecutablePath")] [XmlElement("AI_FaceExecutablePath")] public string FaceExecutablePath { get; set; } = string.Empty; diff --git a/imagecatalog/Services/AiExtractionService.cs b/imagecatalog/Services/AiExtractionService.cs index b7ea6cc..0f1a987 100644 --- a/imagecatalog/Services/AiExtractionService.cs +++ b/imagecatalog/Services/AiExtractionService.cs @@ -4,7 +4,6 @@ using System.IO; using System.Linq; using System.Text; using System.Threading; -using System.Threading.Channels; using System.Threading.Tasks; using AIFotoONLUS.Core; using ImageCatalog_2.Models; @@ -21,11 +20,11 @@ public class AiExtractionService : IAiExtractionService _logger = logger; } - public async Task RunAsync( + public async Task RunAsync( AiExtractionRequest request, CancellationToken token, Func onResult, - Func onProgress) + Func onProgress) { var searchOption = request.Recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; @@ -40,134 +39,38 @@ public class AiExtractionService : IAiExtractionService var extractedResults = new List(); var modelConfiguration = BuildModelConfiguration(request.ModelsFolderPath, request.UseGpu); - var workloadLevel = NormalizeWorkloadLevel(request.WorkloadLevel); - var workerCount = ResolveWorkerCount(request.UseGpu, workloadLevel); - var total = imageFiles.Count; - if (total == 0) - { - var emptySummary = new AiExtractionRunSummary(0, 0, 0, 0, workloadLevel, workerCount, request.UseGpu); - await onProgress(new AiExtractionProgressUpdate(0, 0, 100, 0, workloadLevel, workerCount, request.UseGpu)).ConfigureAwait(false); - return emptySummary; - } + + using var engine = new NumberRecognitionEngine(modelConfiguration, _logger); var processed = 0; + var total = imageFiles.Count; var failed = 0; Exception? firstFailure = null; - var stopwatch = System.Diagnostics.Stopwatch.StartNew(); - var resultChannel = Channel.CreateUnbounded(new UnboundedChannelOptions - { - SingleReader = true, - SingleWriter = false - }); - var fileChannel = Channel.CreateBounded(new BoundedChannelOptions(Math.Max(workerCount * 2, 1)) - { - SingleReader = false, - SingleWriter = true, - FullMode = BoundedChannelFullMode.Wait - }); - var failureLock = new object(); - var logLock = new object(); - var lastLoggedElapsed = TimeSpan.Zero; - var reporterTask = Task.Run(async () => + foreach (var file in imageFiles) { - await foreach (var result in resultChannel.Reader.ReadAllAsync(token).ConfigureAwait(false)) + token.ThrowIfCancellationRequested(); + + var extracted = string.Empty; + + try { - extractedResults.Add(result); - await onResult(result).ConfigureAwait(false); - - var currentProcessed = Interlocked.Increment(ref processed); - var averageImagesPerSecond = CalculateAverageImagesPerSecond(currentProcessed, stopwatch.Elapsed); - var percent = currentProcessed * 100.0 / total; - await onProgress(new AiExtractionProgressUpdate(total, currentProcessed, percent, averageImagesPerSecond, workloadLevel, workerCount, request.UseGpu)).ConfigureAwait(false); - - var shouldLog = false; - lock (logLock) - { - if (currentProcessed == total || stopwatch.Elapsed - lastLoggedElapsed >= TimeSpan.FromSeconds(2)) - { - lastLoggedElapsed = stopwatch.Elapsed; - shouldLog = true; - } - } - - if (shouldLog) - { - _logger.LogInformation( - "Number AI progress: {Processed}/{Total} ({Percent:F1}%), {ImagesPerSecond:F2} img/s avg, workload {WorkloadLevel} ({WorkerCount} {ExecutionUnit})", - currentProcessed, - total, - percent, - averageImagesPerSecond, - workloadLevel, - workerCount, - request.UseGpu ? "batch" : "workers"); - } + extracted = engine.ProcessImage(file).Text; } - }, token); - - try - { - if (request.UseGpu) + catch (Exception ex) { - using var engine = new NumberRecognitionEngine(modelConfiguration, _logger); - var resultProgress = new SynchronousProgress(result => - { - resultChannel.Writer.TryWrite(new AiResultItem { Path = result.FilePath, Text = result.Text }); - }); - - await engine.ProcessFilesAsync( - imageFiles, - skipTextNegative: false, - maxDegreeOfParallelism: workerCount, - progress: null, - resultProgress: resultProgress, - cancellationToken: token).ConfigureAwait(false); + failed++; + firstFailure ??= ex; + _logger.LogWarning(ex, "Error processing AI OCR for {File}", file); } - else - { - var workerTasks = Enumerable.Range(0, workerCount) - .Select(_ => Task.Run(async () => - { - using var engine = new NumberRecognitionEngine(modelConfiguration, _logger); - await foreach (var file in fileChannel.Reader.ReadAllAsync(token).ConfigureAwait(false)) - { - var extracted = string.Empty; - try - { - extracted = engine.ProcessImage(file).Text; - } - catch (Exception ex) - { - lock (failureLock) - { - failed++; - firstFailure ??= ex; - } + var result = new AiResultItem { Path = file, Text = extracted }; + extractedResults.Add(result); + await onResult(result).ConfigureAwait(false); - _logger.LogWarning(ex, "Error processing AI OCR for {File}", file); - } - - await resultChannel.Writer.WriteAsync(new AiResultItem { Path = file, Text = extracted }, token).ConfigureAwait(false); - } - }, token)) - .ToArray(); - - foreach (var file in imageFiles) - { - await fileChannel.Writer.WriteAsync(file, token).ConfigureAwait(false); - } - - fileChannel.Writer.TryComplete(); - await Task.WhenAll(workerTasks).ConfigureAwait(false); - } - } - finally - { - fileChannel.Writer.TryComplete(); - resultChannel.Writer.TryComplete(); - await reporterTask.ConfigureAwait(false); + processed++; + var percent = total > 0 ? (processed * 100.0 / total) : 100.0; + await onProgress(percent).ConfigureAwait(false); } if (imageFiles.Count > 0 && failed == imageFiles.Count) @@ -175,25 +78,6 @@ public class AiExtractionService : IAiExtractionService throw new InvalidOperationException($"AI OCR failed for all {imageFiles.Count} image(s). See previous log entries for details.", firstFailure); } - var summary = new AiExtractionRunSummary( - total, - processed, - failed, - CalculateAverageImagesPerSecond(processed, stopwatch.Elapsed), - workloadLevel, - workerCount, - request.UseGpu); - - _logger.LogInformation( - "Number AI completed: {Processed}/{Total} processed, {Failed} failures, {ImagesPerSecond:F2} img/s avg, workload {WorkloadLevel} ({WorkerCount} {ExecutionUnit})", - summary.ProcessedFiles, - summary.TotalFiles, - summary.FailedFiles, - summary.AverageImagesPerSecond, - summary.WorkloadLevel, - summary.WorkerCount, - request.UseGpu ? "batch" : "workers"); - if (!string.IsNullOrWhiteSpace(request.CsvOutputPath)) { try @@ -218,55 +102,6 @@ public class AiExtractionService : IAiExtractionService _logger.LogError(ex, "Failed to write CSV to {CsvOutputPath}", request.CsvOutputPath); } } - - return summary; - } - - private static double CalculateAverageImagesPerSecond(int processed, TimeSpan elapsed) - { - return elapsed.TotalSeconds > 0 ? processed / elapsed.TotalSeconds : 0; - } - - private static int NormalizeWorkloadLevel(int workloadLevel) - { - return Math.Clamp(workloadLevel, 1, 5); - } - - private static int ResolveWorkerCount(bool useGpu, int workloadLevel) - { - var normalized = NormalizeWorkloadLevel(workloadLevel); - var maxWorkers = Math.Max(1, Environment.ProcessorCount); - var requestedWorkers = useGpu - ? normalized switch - { - 1 => 4, - 2 => 8, - 3 => 16, - 4 => 24, - _ => 32 - } - : normalized switch - { - 1 => 1, - 2 => 2, - 3 => 3, - 4 => 4, - _ => 5 - }; - - return useGpu ? requestedWorkers : Math.Min(requestedWorkers, maxWorkers); - } - - private sealed class SynchronousProgress : IProgress - { - private readonly Action _handler; - - public SynchronousProgress(Action handler) - { - _handler = handler; - } - - public void Report(T value) => _handler(value); } private static ModelConfiguration BuildModelConfiguration(string modelsFolderPath, bool useGpu) diff --git a/imagecatalog/Services/IAiExtractionService.cs b/imagecatalog/Services/IAiExtractionService.cs index 3fd517c..60de781 100644 --- a/imagecatalog/Services/IAiExtractionService.cs +++ b/imagecatalog/Services/IAiExtractionService.cs @@ -12,33 +12,14 @@ public sealed class AiExtractionRequest public bool IncludeThumbnails { get; init; } public required string ModelsFolderPath { get; init; } public bool UseGpu { get; init; } - public int WorkloadLevel { get; init; } = 3; public string CsvOutputPath { get; init; } = string.Empty; } -public sealed record AiExtractionProgressUpdate( - int TotalFiles, - int ProcessedFiles, - double PercentComplete, - double AverageImagesPerSecond, - int WorkloadLevel, - int WorkerCount, - bool UseGpu); - -public sealed record AiExtractionRunSummary( - int TotalFiles, - int ProcessedFiles, - int FailedFiles, - double AverageImagesPerSecond, - int WorkloadLevel, - int WorkerCount, - bool UseGpu); - public interface IAiExtractionService { - Task RunAsync( + Task RunAsync( AiExtractionRequest request, CancellationToken token, Func onResult, - Func onProgress); + Func onProgress); } diff --git a/imagecatalog/ViewModels/AiSettingsViewModel.cs b/imagecatalog/ViewModels/AiSettingsViewModel.cs index 501cf5e..9f0fecb 100644 --- a/imagecatalog/ViewModels/AiSettingsViewModel.cs +++ b/imagecatalog/ViewModels/AiSettingsViewModel.cs @@ -71,28 +71,6 @@ public class AiSettingsViewModel : ViewModelBase } } - private int _numberAiWorkloadLevel = 3; - public int NumberAiWorkloadLevel - { - get => _numberAiWorkloadLevel; - set - { - _numberAiWorkloadLevel = value; - NotifyPropertyChanged(); - } - } - - private string _numberAiStatsSummary = string.Empty; - public string NumberAiStatsSummary - { - get => _numberAiStatsSummary; - set - { - _numberAiStatsSummary = value; - NotifyPropertyChanged(); - } - } - private string _faceExecutablePath = string.Empty; public string FaceExecutablePath {