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
{