Compare commits

...

4 commits

Author SHA1 Message Date
f57dc1edba 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
2026-05-09 19:31:21 +02:00
48d6af13da feat: Update GPU worker allocation for improved performance in AI extraction 2026-05-09 19:15:00 +02:00
88c193549f feat: Implement AI workload settings and enhance AI processing summaries 2026-05-09 18:54:20 +02:00
4230300518 CUDA build 2026-05-09 18:37:31 +02:00
10 changed files with 377 additions and 27 deletions

1
.gitignore vendored
View file

@ -256,3 +256,4 @@ paket-files/
.idea/ .idea/
*.sln.iml *.sln.iml
.vscode/settings.json .vscode/settings.json
tmp/**

View file

@ -137,6 +137,24 @@ public class DataModelCharacterizationTests
model.IncludeNumberAiThumbnails.ShouldBeTrue(); 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] [TestMethod]
public void CommandLineOperationRunner_DetectsHeadlessRequest() public void CommandLineOperationRunner_DetectsHeadlessRequest()
{ {

View file

@ -19,6 +19,19 @@
<CheckBox Content="Includi thumbnail" IsChecked="{Binding IncludeNumberAiThumbnails, Mode=TwoWay}" /> <CheckBox Content="Includi thumbnail" IsChecked="{Binding IncludeNumberAiThumbnails, Mode=TwoWay}" />
</StackPanel> </StackPanel>
<Grid Margin="0,8,0,0" ColumnDefinitions="Auto,Auto,*" ColumnSpacing="8">
<TextBlock Grid.Column="0" Text="Carico OCR:" VerticalAlignment="Center" />
<ComboBox Grid.Column="1"
Width="84"
ItemsSource="{Binding NumberAiWorkloadOptions}"
SelectedItem="{Binding NumberAiWorkloadLevel, Mode=TwoWay}" />
<TextBlock Grid.Column="2"
Text="{Binding NumberAiStatsSummary}"
VerticalAlignment="Center"
TextWrapping="Wrap"
FontWeight="SemiBold" />
</Grid>
<Grid Margin="0,8,0,0" ColumnDefinitions="Auto,*,Auto" ColumnSpacing="6"> <Grid Margin="0,8,0,0" ColumnDefinitions="Auto,*,Auto" ColumnSpacing="6">
<TextBlock Grid.Column="0" Text="Sorgente:" VerticalAlignment="Center" /> <TextBlock Grid.Column="0" Text="Sorgente:" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Text="{Binding DestinationPath, Mode=OneWay}" IsReadOnly="True" VerticalAlignment="Center" /> <TextBox Grid.Column="1" Text="{Binding DestinationPath, Mode=OneWay}" IsReadOnly="True" VerticalAlignment="Center" />

View file

@ -117,6 +117,11 @@ internal static class CommandLineOperationRunner
{ {
model.IncludeNumberAiThumbnails = options.IncludeThumbnails.Value; model.IncludeNumberAiThumbnails = options.IncludeThumbnails.Value;
} }
if (options.NumberAiWorkloadLevel.HasValue)
{
model.NumberAiWorkloadLevel = options.NumberAiWorkloadLevel.Value;
}
} }
private static string NormalizeOperation(string operation) private static string NormalizeOperation(string operation)
@ -172,6 +177,10 @@ internal static class CommandLineOperationRunner
case "--no-tn": case "--no-tn":
options.IncludeThumbnails = false; options.IncludeThumbnails = false;
break; break;
case "--workload":
case "--ai-workload":
options.NumberAiWorkloadLevel = int.Parse(ReadValue(args, ref i, arg));
break;
case "--headless": case "--headless":
case "--cli": case "--cli":
break; break;
@ -209,6 +218,10 @@ internal static class CommandLineOperationRunner
case "--csv": case "--csv":
options.CsvPath = value; options.CsvPath = value;
break; break;
case "--workload":
case "--ai-workload":
options.NumberAiWorkloadLevel = int.Parse(value);
break;
default: default:
throw new ArgumentException($"Unknown argument: {arg}"); throw new ArgumentException($"Unknown argument: {arg}");
} }
@ -226,7 +239,7 @@ internal static class CommandLineOperationRunner
private static void WriteUsage() 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] [--include-thumbnails|--no-thumbnails]"); 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] [--workload <1-5>]");
} }
private sealed class CommandLineOptions private sealed class CommandLineOptions
@ -238,5 +251,6 @@ internal static class CommandLineOperationRunner
public string CsvPath { get; set; } = string.Empty; public string CsvPath { get; set; } = string.Empty;
public bool? UseGpu { get; set; } public bool? UseGpu { get; set; }
public bool? IncludeThumbnails { get; set; } public bool? IncludeThumbnails { get; set; }
public int? NumberAiWorkloadLevel { get; set; }
} }
} }

View file

@ -131,7 +131,7 @@ namespace ImageCatalog_2
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
// user cancelled await InvokeOnUiThreadAsync(() => NumberAiStatsSummary = "OCR annullato.").ConfigureAwait(false);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -141,6 +141,8 @@ namespace ImageCatalog_2
RefreshNumberAiGpuCapabilities(); RefreshNumberAiGpuCapabilities();
} }
await InvokeOnUiThreadAsync(() => NumberAiStatsSummary = $"Errore OCR: {ex.GetBaseException().Message}").ConfigureAwait(false);
await ShowErrorMessageAsync("Errore AI", ex.GetBaseException().Message).ConfigureAwait(false); await ShowErrorMessageAsync("Errore AI", ex.GetBaseException().Message).ConfigureAwait(false);
} }
finally finally
@ -167,9 +169,10 @@ namespace ImageCatalog_2
{ {
PreviewResults.Clear(); PreviewResults.Clear();
AiProgress = 0; AiProgress = 0;
NumberAiStatsSummary = BuildNumberAiIdleSummary();
}).ConfigureAwait(false); }).ConfigureAwait(false);
await _aiExtractionService.RunAsync( var summary = await _aiExtractionService.RunAsync(
new AiExtractionRequest new AiExtractionRequest
{ {
SearchRoot = searchRoot, SearchRoot = searchRoot,
@ -177,6 +180,7 @@ namespace ImageCatalog_2
IncludeThumbnails = IncludeNumberAiThumbnails, IncludeThumbnails = IncludeNumberAiThumbnails,
ModelsFolderPath = ModelsFolderPath, ModelsFolderPath = ModelsFolderPath,
UseGpu = UseNumberAiGpu, UseGpu = UseNumberAiGpu,
WorkloadLevel = NumberAiWorkloadLevel,
CsvOutputPath = CsvOutputPath CsvOutputPath = CsvOutputPath
}, },
token, token,
@ -187,7 +191,17 @@ namespace ImageCatalog_2
PreviewResults.Add(result); PreviewResults.Add(result);
} }
}), }),
progress => InvokeOnUiThreadAsync(() => AiProgress = progress)).ConfigureAwait(false); progress => InvokeOnUiThreadAsync(() =>
{
AiProgress = progress.PercentComplete;
NumberAiStatsSummary = BuildNumberAiProgressSummary(progress);
})).ConfigureAwait(false);
await InvokeOnUiThreadAsync(() =>
{
AiProgress = summary.TotalFiles > 0 ? 100 : 0;
NumberAiStatsSummary = BuildNumberAiCompletionSummary(summary);
}).ConfigureAwait(false);
} }
/// <summary> /// <summary>
@ -250,6 +264,20 @@ namespace ImageCatalog_2
set => _ai.IncludeNumberAiThumbnails = value; set => _ai.IncludeNumberAiThumbnails = value;
} }
public IReadOnlyList<int> 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 public string FaceExecutablePath
{ {
get => _ai.FaceExecutablePath; get => _ai.FaceExecutablePath;
@ -419,6 +447,30 @@ namespace ImageCatalog_2
set => _ai.AiProgress = value; 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<string> LoadAvailableFonts() private List<string> LoadAvailableFonts()
{ {
#if WINDOWS #if WINDOWS
@ -2167,6 +2219,36 @@ namespace ImageCatalog_2
return value is >= 1 and <= 5 ? value : 3; 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) private static int NormalizeFaceMinSize(int value)
{ {
return value > 0 ? value : 35; return value > 0 ? value : 35;

View file

@ -36,6 +36,9 @@
<!-- Skip MinVer execution during local builds to avoid environment/runtime-specific failures. --> <!-- Skip MinVer execution during local builds to avoid environment/runtime-specific failures. -->
<MinVerSkip>true</MinVerSkip> <MinVerSkip>true</MinVerSkip>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="$([MSBuild]::IsOsPlatform('Windows')) and '$(UseLocalAIFotoONLUS)' == 'true'">
<LocalAIFotoOutputDir>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\..\AIFotoONLUS\src\AIFotoONLUS.Core\bin\$(Configuration)\net10.0'))</LocalAIFotoOutputDir>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugType>embedded</DebugType> <DebugType>embedded</DebugType>
</PropertyGroup> </PropertyGroup>
@ -147,4 +150,13 @@
<Copy SourceFiles="$(_CandidateFound)" DestinationFiles="$(_DestExe)" SkipUnchangedFiles="false" Condition="'$(_CandidateFound)' != ''" /> <Copy SourceFiles="$(_CandidateFound)" DestinationFiles="$(_DestExe)" SkipUnchangedFiles="false" Condition="'$(_CandidateFound)' != ''" />
<Delete Files="$(_CandidateFound)" Condition="'$(_CandidateFound)' != ''" /> <Delete Files="$(_CandidateFound)" Condition="'$(_CandidateFound)' != ''" />
</Target> </Target>
<Target Name="CopyLocalCudaInferenceLibraries" AfterTargets="Build" Condition="$([MSBuild]::IsOsPlatform('Windows')) and '$(UseLocalAIFotoONLUS)' == 'true'">
<ItemGroup>
<LocalCudaInferenceLibrary Include="$(LocalAIFotoOutputDir)\cublasLt64_11.dll" Condition="Exists('$(LocalAIFotoOutputDir)\cublasLt64_11.dll')" />
<LocalCudaInferenceLibrary Include="$(LocalAIFotoOutputDir)\cudnn_cnn_infer64_8.dll" Condition="Exists('$(LocalAIFotoOutputDir)\cudnn_cnn_infer64_8.dll')" />
</ItemGroup>
<Copy SourceFiles="@(LocalCudaInferenceLibrary)" DestinationFolder="$(TargetDir)" SkipUnchangedFiles="true" Condition="'@(LocalCudaInferenceLibrary)' != ''" />
</Target>
</Project> </Project>

View file

@ -282,6 +282,10 @@ namespace ImageCatalog_2.Models
[XmlElement("AI_IncludiThumbnailNumeri")] [XmlElement("AI_IncludiThumbnailNumeri")]
public bool IncludeNumberAiThumbnails { get; set; } public bool IncludeNumberAiThumbnails { get; set; }
[JsonPropertyName("NumberAiWorkloadLevel")]
[XmlElement("AI_LivelloCaricoNumeri")]
public int NumberAiWorkloadLevel { get; set; } = 3;
[JsonPropertyName("FaceExecutablePath")] [JsonPropertyName("FaceExecutablePath")]
[XmlElement("AI_FaceExecutablePath")] [XmlElement("AI_FaceExecutablePath")]
public string FaceExecutablePath { get; set; } = string.Empty; public string FaceExecutablePath { get; set; } = string.Empty;

View file

@ -4,6 +4,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks; using System.Threading.Tasks;
using AIFotoONLUS.Core; using AIFotoONLUS.Core;
using ImageCatalog_2.Models; using ImageCatalog_2.Models;
@ -20,11 +21,11 @@ public class AiExtractionService : IAiExtractionService
_logger = logger; _logger = logger;
} }
public async Task RunAsync( public async Task<AiExtractionRunSummary> RunAsync(
AiExtractionRequest request, AiExtractionRequest request,
CancellationToken token, CancellationToken token,
Func<AiResultItem, Task> onResult, Func<AiResultItem, Task> onResult,
Func<double, Task> onProgress) Func<AiExtractionProgressUpdate, Task> onProgress)
{ {
var searchOption = request.Recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; var searchOption = request.Recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
@ -39,38 +40,134 @@ public class AiExtractionService : IAiExtractionService
var extractedResults = new List<AiResultItem>(); var extractedResults = new List<AiResultItem>();
var modelConfiguration = BuildModelConfiguration(request.ModelsFolderPath, request.UseGpu); var modelConfiguration = BuildModelConfiguration(request.ModelsFolderPath, request.UseGpu);
var workloadLevel = NormalizeWorkloadLevel(request.WorkloadLevel);
using var engine = new NumberRecognitionEngine(modelConfiguration, _logger); 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;
}
var processed = 0; var processed = 0;
var total = imageFiles.Count;
var failed = 0; var failed = 0;
Exception? firstFailure = null; Exception? firstFailure = null;
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
foreach (var file in imageFiles) var resultChannel = Channel.CreateUnbounded<AiResultItem>(new UnboundedChannelOptions
{ {
token.ThrowIfCancellationRequested(); SingleReader = true,
SingleWriter = false
});
var fileChannel = Channel.CreateBounded<string>(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 extracted = string.Empty; var reporterTask = Task.Run(async () =>
{
try await foreach (var result in resultChannel.Reader.ReadAllAsync(token).ConfigureAwait(false))
{ {
extracted = engine.ProcessImage(file).Text; 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");
}
} }
catch (Exception ex) }, token);
try
{
if (request.UseGpu)
{ {
failed++; using var engine = new NumberRecognitionEngine(modelConfiguration, _logger);
firstFailure ??= ex; var resultProgress = new SynchronousProgress<ImageResult>(result =>
_logger.LogWarning(ex, "Error processing AI OCR for {File}", file); {
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);
} }
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;
var result = new AiResultItem { Path = file, Text = extracted }; try
extractedResults.Add(result); {
await onResult(result).ConfigureAwait(false); extracted = engine.ProcessImage(file).Text;
}
catch (Exception ex)
{
lock (failureLock)
{
failed++;
firstFailure ??= ex;
}
processed++; _logger.LogWarning(ex, "Error processing AI OCR for {File}", file);
var percent = total > 0 ? (processed * 100.0 / total) : 100.0; }
await onProgress(percent).ConfigureAwait(false);
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);
} }
if (imageFiles.Count > 0 && failed == imageFiles.Count) if (imageFiles.Count > 0 && failed == imageFiles.Count)
@ -78,6 +175,25 @@ public class AiExtractionService : IAiExtractionService
throw new InvalidOperationException($"AI OCR failed for all {imageFiles.Count} image(s). See previous log entries for details.", firstFailure); 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)) if (!string.IsNullOrWhiteSpace(request.CsvOutputPath))
{ {
try try
@ -102,6 +218,55 @@ public class AiExtractionService : IAiExtractionService
_logger.LogError(ex, "Failed to write CSV to {CsvOutputPath}", request.CsvOutputPath); _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<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

@ -12,14 +12,33 @@ public sealed class AiExtractionRequest
public bool IncludeThumbnails { get; init; } public bool IncludeThumbnails { get; init; }
public required string ModelsFolderPath { get; init; } public required string ModelsFolderPath { get; init; }
public bool UseGpu { get; init; } public bool UseGpu { get; init; }
public int WorkloadLevel { get; init; } = 3;
public string CsvOutputPath { get; init; } = string.Empty; 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 public interface IAiExtractionService
{ {
Task RunAsync( Task<AiExtractionRunSummary> RunAsync(
AiExtractionRequest request, AiExtractionRequest request,
CancellationToken token, CancellationToken token,
Func<AiResultItem, Task> onResult, Func<AiResultItem, Task> onResult,
Func<double, Task> onProgress); Func<AiExtractionProgressUpdate, Task> onProgress);
} }

View file

@ -71,6 +71,28 @@ 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; private string _faceExecutablePath = string.Empty;
public string FaceExecutablePath public string FaceExecutablePath
{ {