feat: Enhance AI extraction summaries and worker allocation for GPU support
Some checks failed
Build Windows Avalonia / build (push) Failing after 1m38s
Build Windows Avalonia / release (push) Has been skipped

This commit is contained in:
MaddoScientisto 2026-05-09 19:31:21 +02:00
commit f57dc1edba
3 changed files with 90 additions and 51 deletions

View file

@ -450,12 +450,14 @@ namespace ImageCatalog_2
private string BuildNumberAiIdleSummary() private string BuildNumberAiIdleSummary()
{ {
var workerCount = ResolveNumberAiWorkerCount(UseNumberAiGpu, NumberAiWorkloadLevel); var workerCount = ResolveNumberAiWorkerCount(UseNumberAiGpu, NumberAiWorkloadLevel);
return $"In attesa. Carico {NumberAiWorkloadLevel}/5, {workerCount} worker, 0.00 img/s."; var unit = UseNumberAiGpu ? "batch" : "worker";
return $"In attesa. Carico {NumberAiWorkloadLevel}/5, {workerCount} {unit}, 0.00 img/s.";
} }
private static string BuildNumberAiProgressSummary(AiExtractionProgressUpdate progress) private static string BuildNumberAiProgressSummary(AiExtractionProgressUpdate progress)
{ {
return $"{progress.ProcessedFiles}/{progress.TotalFiles} immagini, media {progress.AverageImagesPerSecond:F2} img/s, carico {progress.WorkloadLevel}/5, {progress.WorkerCount} worker."; 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) private static string BuildNumberAiCompletionSummary(AiExtractionRunSummary summary)
@ -465,7 +467,8 @@ namespace ImageCatalog_2
return "Nessuna immagine trovata per OCR."; return "Nessuna immagine trovata per OCR.";
} }
return $"Completato: {summary.ProcessedFiles}/{summary.TotalFiles} immagini, media finale {summary.AverageImagesPerSecond:F2} img/s, errori {summary.FailedFiles}, carico {summary.WorkloadLevel}/5, {summary.WorkerCount} worker."; 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<string> LoadAvailableFonts() private List<string> LoadAvailableFonts()
@ -2228,11 +2231,11 @@ namespace ImageCatalog_2
var requestedWorkers = useGpu var requestedWorkers = useGpu
? normalized switch ? normalized switch
{ {
1 => 2, 1 => 4,
2 => 4, 2 => 8,
3 => 8, 3 => 16,
4 => 12, 4 => 24,
_ => 16 _ => 32
} }
: normalized switch : normalized switch
{ {
@ -2243,7 +2246,7 @@ namespace ImageCatalog_2
_ => 5 _ => 5
}; };
return Math.Min(requestedWorkers, maxWorkers); return useGpu ? requestedWorkers : Math.Min(requestedWorkers, maxWorkers);
} }
private static int NormalizeFaceMinSize(int value) private static int NormalizeFaceMinSize(int value)

View file

@ -45,8 +45,8 @@ public class AiExtractionService : IAiExtractionService
var total = imageFiles.Count; var total = imageFiles.Count;
if (total == 0) if (total == 0)
{ {
var emptySummary = new AiExtractionRunSummary(0, 0, 0, 0, workloadLevel, workerCount); var emptySummary = new AiExtractionRunSummary(0, 0, 0, 0, workloadLevel, workerCount, request.UseGpu);
await onProgress(new AiExtractionProgressUpdate(0, 0, 100, 0, workloadLevel, workerCount)).ConfigureAwait(false); await onProgress(new AiExtractionProgressUpdate(0, 0, 100, 0, workloadLevel, workerCount, request.UseGpu)).ConfigureAwait(false);
return emptySummary; return emptySummary;
} }
@ -79,7 +79,7 @@ public class AiExtractionService : IAiExtractionService
var currentProcessed = Interlocked.Increment(ref processed); var currentProcessed = Interlocked.Increment(ref processed);
var averageImagesPerSecond = CalculateAverageImagesPerSecond(currentProcessed, stopwatch.Elapsed); var averageImagesPerSecond = CalculateAverageImagesPerSecond(currentProcessed, stopwatch.Elapsed);
var percent = currentProcessed * 100.0 / total; var percent = currentProcessed * 100.0 / total;
await onProgress(new AiExtractionProgressUpdate(total, currentProcessed, percent, averageImagesPerSecond, workloadLevel, workerCount)).ConfigureAwait(false); await onProgress(new AiExtractionProgressUpdate(total, currentProcessed, percent, averageImagesPerSecond, workloadLevel, workerCount, request.UseGpu)).ConfigureAwait(false);
var shouldLog = false; var shouldLog = false;
lock (logLock) lock (logLock)
@ -94,54 +94,74 @@ public class AiExtractionService : IAiExtractionService
if (shouldLog) if (shouldLog)
{ {
_logger.LogInformation( _logger.LogInformation(
"Number AI progress: {Processed}/{Total} ({Percent:F1}%), {ImagesPerSecond:F2} img/s avg, workload {WorkloadLevel} ({WorkerCount} workers)", "Number AI progress: {Processed}/{Total} ({Percent:F1}%), {ImagesPerSecond:F2} img/s avg, workload {WorkloadLevel} ({WorkerCount} {ExecutionUnit})",
currentProcessed, currentProcessed,
total, total,
percent, percent,
averageImagesPerSecond, averageImagesPerSecond,
workloadLevel, workloadLevel,
workerCount); workerCount,
request.UseGpu ? "batch" : "workers");
} }
} }
}, token); }, token);
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;
}
_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();
try try
{ {
foreach (var file in imageFiles) if (request.UseGpu)
{ {
await fileChannel.Writer.WriteAsync(file, token).ConfigureAwait(false); using var engine = new NumberRecognitionEngine(modelConfiguration, _logger);
} var resultProgress = new SynchronousProgress<ImageResult>(result =>
{
resultChannel.Writer.TryWrite(new AiResultItem { Path = result.FilePath, Text = result.Text });
});
fileChannel.Writer.TryComplete(); await engine.ProcessFilesAsync(
await Task.WhenAll(workerTasks).ConfigureAwait(false); imageFiles,
skipTextNegative: false,
maxDegreeOfParallelism: workerCount,
progress: null,
resultProgress: resultProgress,
cancellationToken: token).ConfigureAwait(false);
}
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;
}
_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 finally
{ {
@ -161,16 +181,18 @@ public class AiExtractionService : IAiExtractionService
failed, failed,
CalculateAverageImagesPerSecond(processed, stopwatch.Elapsed), CalculateAverageImagesPerSecond(processed, stopwatch.Elapsed),
workloadLevel, workloadLevel,
workerCount); workerCount,
request.UseGpu);
_logger.LogInformation( _logger.LogInformation(
"Number AI completed: {Processed}/{Total} processed, {Failed} failures, {ImagesPerSecond:F2} img/s avg, workload {WorkloadLevel} ({WorkerCount} workers)", "Number AI completed: {Processed}/{Total} processed, {Failed} failures, {ImagesPerSecond:F2} img/s avg, workload {WorkloadLevel} ({WorkerCount} {ExecutionUnit})",
summary.ProcessedFiles, summary.ProcessedFiles,
summary.TotalFiles, summary.TotalFiles,
summary.FailedFiles, summary.FailedFiles,
summary.AverageImagesPerSecond, summary.AverageImagesPerSecond,
summary.WorkloadLevel, summary.WorkloadLevel,
summary.WorkerCount); summary.WorkerCount,
request.UseGpu ? "batch" : "workers");
if (!string.IsNullOrWhiteSpace(request.CsvOutputPath)) if (!string.IsNullOrWhiteSpace(request.CsvOutputPath))
{ {
@ -217,11 +239,11 @@ public class AiExtractionService : IAiExtractionService
var requestedWorkers = useGpu var requestedWorkers = useGpu
? normalized switch ? normalized switch
{ {
1 => 2, 1 => 4,
2 => 4, 2 => 8,
3 => 8, 3 => 16,
4 => 12, 4 => 24,
_ => 16 _ => 32
} }
: normalized switch : normalized switch
{ {
@ -232,7 +254,19 @@ public class AiExtractionService : IAiExtractionService
_ => 5 _ => 5
}; };
return Math.Min(requestedWorkers, maxWorkers); return useGpu ? requestedWorkers : Math.Min(requestedWorkers, maxWorkers);
}
private sealed class SynchronousProgress<T> : IProgress<T>
{
private readonly Action<T> _handler;
public SynchronousProgress(Action<T> handler)
{
_handler = handler;
}
public void Report(T value) => _handler(value);
} }
private static ModelConfiguration BuildModelConfiguration(string modelsFolderPath, bool useGpu) private static ModelConfiguration BuildModelConfiguration(string modelsFolderPath, bool useGpu)

View file

@ -22,7 +22,8 @@ public sealed record AiExtractionProgressUpdate(
double PercentComplete, double PercentComplete,
double AverageImagesPerSecond, double AverageImagesPerSecond,
int WorkloadLevel, int WorkloadLevel,
int WorkerCount); int WorkerCount,
bool UseGpu);
public sealed record AiExtractionRunSummary( public sealed record AiExtractionRunSummary(
int TotalFiles, int TotalFiles,
@ -30,7 +31,8 @@ public sealed record AiExtractionRunSummary(
int FailedFiles, int FailedFiles,
double AverageImagesPerSecond, double AverageImagesPerSecond,
int WorkloadLevel, int WorkloadLevel,
int WorkerCount); int WorkerCount,
bool UseGpu);
public interface IAiExtractionService public interface IAiExtractionService
{ {