Catalog/imagecatalog/DataModel.cs

3533 lines
120 KiB
C#
Raw Normal View History

2024-10-14 23:25:35 +02:00
using ImageCatalog_2.Commands;
using ImageCatalog_2.Models;
2024-10-14 23:25:35 +02:00
using ImageCatalog_2.Services;
using ImageCatalog_2.ViewModels;
2024-10-14 23:05:18 +02:00
using System;
2024-10-14 22:55:52 +02:00
using System.Collections.Generic;
using System.ComponentModel;
2024-10-14 23:25:35 +02:00
using System.Diagnostics;
using System.IO;
2024-10-14 22:55:52 +02:00
using System.Linq;
using System.Text;
using System.Globalization;
2026-02-04 23:16:06 +01:00
using System.Threading;
2024-10-14 22:55:52 +02:00
using System.Threading.Tasks;
2024-10-14 23:25:35 +02:00
using System.Windows.Input;
using AIFotoONLUS.Core;
using System.Text.RegularExpressions;
2026-02-04 23:16:06 +01:00
using AutoMapper;
using MaddoShared;
2025-07-29 11:07:49 +02:00
using Microsoft.Extensions.Logging;
using System.Collections.ObjectModel;
2026-05-28 20:27:05 +02:00
using SixLabors.Fonts;
2024-10-14 22:55:52 +02:00
namespace ImageCatalog_2
{
public class DataModel : ViewModelBase
{
2024-10-14 23:25:35 +02:00
public ICommand TestCommand { get; }
public ICommand AsyncTestCommand { get; }
2025-07-29 11:07:49 +02:00
public ICommand AsyncCancelOperationCommand { get; }
2025-07-23 17:16:06 +02:00
public ICommand ProcessImagesCommand { get; }
2026-02-04 19:48:03 +01:00
public ICommand SelectSourceFolderCommand { get; }
public ICommand SelectDestinationFolderCommand { get; }
public ICommand SelectLogoFileCommand { get; }
public ICommand SaveSettingsCommand { get; }
public ICommand LoadSettingsCommand { get; }
public ICommand SelectColorCommand { get; }
public ICommand SelectTransparentColorCommand { get; }
public ICommand SelectModelsFolderCommand { get; }
public ICommand SelectCsvOutputCommand { get; }
public ICommand StartAiCommand { get; }
public ICommand StartFaceEncoderCommand { get; }
public ICommand StopFaceEncoderCommand { get; }
public ICommand StartFaceMatcherCommand { get; }
public ICommand StopFaceMatcherCommand { get; }
2024-10-14 23:25:35 +02:00
2024-10-14 23:05:18 +02:00
private readonly ITestService _service;
2025-07-29 11:10:54 +02:00
private readonly ILogger<DataModel> _logger;
2026-02-04 19:48:03 +01:00
private readonly ISettingsService _settingsService;
2026-02-21 15:53:52 +01:00
private readonly ImageCreationService _imageCreationService;
private readonly IAiExtractionService _aiExtractionService;
private readonly IImageProcessingCoordinator _imageProcessingCoordinator;
private readonly ProcessingStateViewModel _processing;
private readonly PathSettingsViewModel _paths;
private readonly AiSettingsViewModel _ai;
private readonly RaceUploadSettingsViewModel _raceUpload;
private readonly VisualSettingsViewModel _visual;
2026-02-04 23:16:06 +01:00
private readonly PicSettings _picSettings;
private readonly IMapper _mapper;
private readonly AsyncCommand _startFaceEncoderCommand;
private readonly AsyncCommand _stopFaceEncoderCommand;
private readonly AsyncCommand _startFaceMatcherCommand;
private readonly AsyncCommand _stopFaceMatcherCommand;
private readonly object _faceEncoderProcessLock = new();
private readonly object _faceMatcherProcessLock = new();
private Process? _faceEncoderProcess;
private Process? _faceMatcherProcess;
private CancellationTokenSource? _faceEncoderWatcherTokenSource;
private Task? _faceEncoderWatcherTask;
private CancellationTokenSource? _faceEncoderLogWatcherTokenSource;
private Task? _faceEncoderLogWatcherTask;
private CancellationTokenSource? _faceMatcherWatcherTokenSource;
private Task? _faceMatcherWatcherTask;
private CancellationTokenSource? _faceMatcherLogWatcherTokenSource;
private Task? _faceMatcherLogWatcherTask;
private bool _hasStartedFaceEncoderInSession;
private bool _hasStartedFaceMatcherInSession;
private int _numberAiGpuRefreshVersion;
private volatile bool _numberAiGpuValidationPending;
private sealed record ParsedFaceMatcherRow(string PhotoId, double? Score, string RawRow, string DebugSummary);
2026-02-04 19:48:03 +01:00
private const string AiCsvOverwriteDialogTitle = "File CSV gia esistente";
2026-02-04 19:48:03 +01:00
// ComboBox collections
public List<string> AvailableFonts { get; }
public List<string> VerticalPositions { get; } = new() { "Alto", "Centro", "Basso" };
public List<string> HorizontalAlignments { get; } = new() { "Sinistra", "Centro", "Destra" };
2025-07-29 11:10:54 +02:00
[CLSCompliant(false)]
public DataModel(ITestService testService, ISettingsService settingsService,
ImageCreationService imageCreationService, IAiExtractionService aiExtractionService, IImageProcessingCoordinator imageProcessingCoordinator, PicSettings picSettings,
IMapper mapper, ILogger<DataModel> logger, MaddoShared.IVersionProvider? versionProvider = null)
2024-10-14 23:05:18 +02:00
{
_service = testService;
2025-07-29 11:07:49 +02:00
_logger = logger;
2026-02-04 19:48:03 +01:00
_settingsService = settingsService;
2026-02-04 23:16:06 +01:00
_imageCreationService = imageCreationService;
_aiExtractionService = aiExtractionService;
_imageProcessingCoordinator = imageProcessingCoordinator;
_processing = new ProcessingStateViewModel();
_processing.PropertyChanged += OnProcessingPropertyChanged;
_paths = new PathSettingsViewModel();
_paths.PropertyChanged += OnPathsPropertyChanged;
_ai = new AiSettingsViewModel();
_ai.PropertyChanged += OnAiPropertyChanged;
_raceUpload = new RaceUploadSettingsViewModel();
_raceUpload.PropertyChanged += OnRaceUploadPropertyChanged;
_visual = new VisualSettingsViewModel();
_visual.PropertyChanged += OnVisualPropertyChanged;
2026-02-04 23:16:06 +01:00
_picSettings = picSettings;
_mapper = mapper;
// Populate AppVersion from version provider when available
AppVersion = versionProvider?.GetVersionString() ?? string.Empty;
2024-10-14 23:25:35 +02:00
TestCommand = new RelayCommand(Test);
AsyncTestCommand = new AsyncCommand(TestAsync);
2025-07-29 11:07:49 +02:00
AsyncCancelOperationCommand = new AsyncCommand(CancelOperation);
2025-07-23 17:16:06 +02:00
ProcessImagesCommand = new AsyncCommand(ProcessImages);
SelectModelsFolderCommand = new RelayCommand(SelectModelsFolder);
SelectCsvOutputCommand = new RelayCommand(SelectCsvOutput);
StartAiCommand = new AsyncCommand(StartAiAsync);
_startFaceEncoderCommand = new AsyncCommand(RunFaceEncoderAsync, CanRunFaceEncoder);
_stopFaceEncoderCommand = new AsyncCommand(() => StopFaceEncoderAsync("Arresto richiesto dall'utente."), CanStopFaceEncoder);
_startFaceMatcherCommand = new AsyncCommand(RunFaceMatcherAsync, CanRunFaceMatcher);
_stopFaceMatcherCommand = new AsyncCommand(() => StopFaceMatcherAsync("Arresto richiesto dall'utente."), CanStopFaceMatcher);
StartFaceEncoderCommand = _startFaceEncoderCommand;
StopFaceEncoderCommand = _stopFaceEncoderCommand;
StartFaceMatcherCommand = _startFaceMatcherCommand;
StopFaceMatcherCommand = _stopFaceMatcherCommand;
2026-02-04 19:48:03 +01:00
SelectSourceFolderCommand = new RelayCommand(SelectSourceFolder);
SelectDestinationFolderCommand = new RelayCommand(SelectDestinationFolder);
SelectLogoFileCommand = new RelayCommand(SelectLogoFile);
SaveSettingsCommand = new RelayCommand(SaveSettings);
LoadSettingsCommand = new RelayCommand(LoadSettings);
SelectColorCommand = new RelayCommand(SelectColor);
SelectTransparentColorCommand = new RelayCommand(SelectTransparentColor);
2026-02-04 19:48:03 +01:00
// Load available fonts
AvailableFonts = LoadAvailableFonts();
QueueRefreshNumberAiGpuCapabilities();
RefreshFaceExecutableCapabilities();
2026-02-04 19:48:03 +01:00
}
private async Task StartAiAsync()
{
if (!await ConfirmAiCsvOverwriteIfNeededAsync().ConfigureAwait(false))
{
await InvokeOnUiThreadAsync(() => NumberAiStatsSummary = "OCR annullato.").ConfigureAwait(false);
return;
}
MainToken = new CancellationTokenSource();
try
{
await RunAiExtractionCoreAsync(MainToken.Token, useDestination: true, recursive: true).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
await InvokeOnUiThreadAsync(() => NumberAiStatsSummary = "OCR annullato.").ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(ex, "AI extraction failed");
if (UseNumberAiGpu)
{
QueueRefreshNumberAiGpuCapabilities();
}
await InvokeOnUiThreadAsync(() => NumberAiStatsSummary = $"Errore OCR: {ex.GetBaseException().Message}").ConfigureAwait(false);
await ShowErrorMessageAsync("Errore AI", ex.GetBaseException().Message).ConfigureAwait(false);
}
finally
{
MainToken = null;
}
}
2026-05-09 17:27:05 +02:00
private async Task RunAiExtractionCoreAsync(CancellationToken token, bool useDestination = false, bool recursive = false, bool failOnInvalidPath = false)
{
var searchRoot = useDestination ? DestinationPath : SourcePath;
if (string.IsNullOrWhiteSpace(searchRoot) || !System.IO.Directory.Exists(searchRoot))
{
_logger.LogWarning("AI extraction path invalid: {Path}", searchRoot);
2026-05-09 17:27:05 +02:00
if (failOnInvalidPath)
{
throw new DirectoryNotFoundException($"AI extraction path invalid: {searchRoot}");
}
return;
}
await InvokeOnUiThreadAsync(() =>
{
PreviewResults.Clear();
AiProgress = 0;
NumberAiStatsSummary = BuildNumberAiIdleSummary();
}).ConfigureAwait(false);
var summary = await _aiExtractionService.RunAsync(
new AiExtractionRequest
{
SearchRoot = searchRoot,
Recursive = recursive,
IncludeThumbnails = IncludeNumberAiThumbnails,
2026-05-09 17:27:05 +02:00
ModelsFolderPath = ModelsFolderPath,
UseGpu = UseNumberAiGpu,
WorkloadLevel = NumberAiWorkloadLevel,
CsvOutputPath = CsvOutputPath
},
token,
result => InvokeOnUiThreadAsync(() =>
{
if (!string.IsNullOrWhiteSpace(result.Text))
{
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);
}
/// <summary>
/// Optional UI-thread invoker set by the active UI layer.
/// </summary>
public Action<Action>? UiInvoker { get; set; }
private Task InvokeOnUiThreadAsync(Action action)
{
return Task.Run(() =>
{
if (UiInvoker != null)
UiInvoker(action);
else
action();
});
}
public AiSettingsViewModel Ai => _ai;
public RaceUploadSettingsViewModel RaceUpload => _raceUpload;
// AI properties
public bool ExtractNumbers
{
get => _ai.ExtractNumbers;
set => _ai.ExtractNumbers = value;
}
public string ModelsFolderPath
{
get => _ai.ModelsFolderPath;
set
{
_ai.ModelsFolderPath = value;
QueueRefreshNumberAiGpuCapabilities();
}
}
public string CsvOutputPath
{
get => _ai.CsvOutputPath;
set => _ai.CsvOutputPath = value;
}
public Func<string, string, Task<bool>>? ConfirmAiCsvOverwriteAsync { get; set; }
2026-05-09 17:27:05 +02:00
public bool UseNumberAiGpu
{
get => _ai.UseNumberAiGpu;
set => SetUseNumberAiGpu(value);
}
public bool NumberAiGpuOptionEnabled
{
get => _ai.NumberAiGpuOptionEnabled;
private set => _ai.NumberAiGpuOptionEnabled = value;
}
public bool IncludeNumberAiThumbnails
{
get => _ai.IncludeNumberAiThumbnails;
set => _ai.IncludeNumberAiThumbnails = value;
2026-05-09 17:27:05 +02:00
}
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
{
get => _ai.FaceExecutablePath;
set
{
var normalizedValue = value ?? string.Empty;
if (string.Equals(_ai.FaceExecutablePath, normalizedValue, StringComparison.Ordinal))
{
RefreshFaceExecutableCapabilities();
return;
}
_ai.FaceExecutablePath = normalizedValue;
RefreshFaceExecutableCapabilities();
}
}
public string FaceOutputFolderPath
{
get => _ai.FaceOutputFolderPath;
set => _ai.FaceOutputFolderPath = value;
}
public bool FaceRecursive
{
get => _ai.FaceRecursive;
set => _ai.FaceRecursive = value;
}
public bool FaceIncludeThumbnails
{
get => _ai.FaceIncludeThumbnails;
set => _ai.FaceIncludeThumbnails = value;
}
public IReadOnlyList<int> FaceParallelismOptions { get; } = [1, 2, 3, 4, 5];
public int FaceParallelism
{
get => _ai.FaceParallelism;
set => _ai.FaceParallelism = value;
}
public int FaceMinSize
{
get => _ai.FaceMinSize;
set => _ai.FaceMinSize = value;
}
public bool FaceUpsample
{
get => _ai.FaceUpsample;
set => _ai.FaceUpsample = value;
}
public bool FaceGpuOptionEnabled => _ai.FaceGpuOptionEnabled;
public bool UseFaceGpu
{
get => _ai.UseFaceGpu;
set => SetUseFaceGpu(value);
}
public bool IsFaceEncoderRunning
{
get => _ai.IsFaceEncoderRunning;
private set => _ai.IsFaceEncoderRunning = value;
}
public string FaceStatusMessage
{
get => _ai.FaceStatusMessage;
private set => _ai.FaceStatusMessage = value;
}
public string FaceCommandOutput
{
get => _ai.FaceCommandOutput;
private set => _ai.FaceCommandOutput = value;
}
public string FaceMatcherExecutablePath
{
get => _ai.FaceMatcherExecutablePath;
set => _ai.FaceMatcherExecutablePath = value ?? string.Empty;
}
public string FaceMatcherEncodingsPath
{
get => _ai.FaceMatcherEncodingsPath;
set => _ai.FaceMatcherEncodingsPath = value ?? string.Empty;
}
public string FaceMatcherOutputPath
{
get => _ai.FaceMatcherOutputPath;
set => _ai.FaceMatcherOutputPath = value ?? string.Empty;
}
public string FaceMatcherLogPath
{
get => _ai.FaceMatcherLogPath;
set => _ai.FaceMatcherLogPath = value ?? string.Empty;
}
public double FaceMatcherTolerance
{
get => _ai.FaceMatcherTolerance;
set => _ai.FaceMatcherTolerance = NormalizeFaceMatcherTolerance(value);
}
public string FaceMatcherSelectedImagePath
{
get => _ai.FaceMatcherSelectedImagePath;
set => _ai.FaceMatcherSelectedImagePath = value ?? string.Empty;
}
public bool IsFaceMatcherRunning
{
get => _ai.IsFaceMatcherRunning;
private set => _ai.IsFaceMatcherRunning = value;
}
public string FaceMatcherStatusMessage
{
get => _ai.FaceMatcherStatusMessage;
private set => _ai.FaceMatcherStatusMessage = value;
}
public string FaceMatcherCommandOutput
{
get => _ai.FaceMatcherCommandOutput;
private set => _ai.FaceMatcherCommandOutput = value;
}
public System.Collections.ObjectModel.ObservableCollection<FaceMatcherResultItem> FaceMatcherResults => _ai.FaceMatcherResults;
// Race upload settings
public string ApiLogin
{
get => _raceUpload.ApiLogin;
set => _raceUpload.ApiLogin = value;
}
public string ApiPassword
{
get => _raceUpload.ApiPassword;
set => _raceUpload.ApiPassword = value;
}
public string ApiRaceDescription
{
get => _raceUpload.ApiRaceDescription;
set => _raceUpload.ApiRaceDescription = value;
}
public string ApiRaceTypeId
{
get => _raceUpload.ApiRaceTypeId;
set => _raceUpload.ApiRaceTypeId = value;
}
public DateTime ApiRaceStartDate
{
get => _raceUpload.ApiRaceStartDate;
set => _raceUpload.ApiRaceStartDate = value;
}
public DateTime ApiRaceEndDate
{
get => _raceUpload.ApiRaceEndDate;
set => _raceUpload.ApiRaceEndDate = value;
}
public string ApiPathBase
{
get => _raceUpload.ApiPathBase;
set => _raceUpload.ApiPathBase = value;
}
public string ApiLocalita
{
get => _raceUpload.ApiLocalita;
set => _raceUpload.ApiLocalita = value;
}
public int ApiEventoInLineaIndex
{
get => _raceUpload.ApiEventoInLineaIndex;
set => _raceUpload.ApiEventoInLineaIndex = value;
}
public int ApiTipoIndexValue
{
get => _raceUpload.ApiTipoIndexValue;
set => _raceUpload.ApiTipoIndexValue = value;
}
public int ApiFreeEventIndex
{
get => _raceUpload.ApiFreeEventIndex;
set => _raceUpload.ApiFreeEventIndex = value;
}
public string ApiRaceId
{
get => _raceUpload.ApiRaceId;
set => _raceUpload.ApiRaceId = value;
}
public string ApiRemoteProcessedBasePath
{
get => _raceUpload.ApiRemoteProcessedBasePath;
set => _raceUpload.ApiRemoteProcessedBasePath = value;
}
// Preview results for DataGrid
public System.Collections.ObjectModel.ObservableCollection<AiResultItem> PreviewResults => _ai.PreviewResults;
public double AiProgress
{
get => _ai.AiProgress;
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}.";
}
2026-02-04 19:48:03 +01:00
private List<string> LoadAvailableFonts()
{
2026-05-28 20:27:05 +02:00
try
{
return SystemFonts.Collection.Families
.Select(f => f.Name)
.Where(name => !string.IsNullOrWhiteSpace(name))
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(name => name, StringComparer.CurrentCultureIgnoreCase)
.ToList();
}
catch
2026-02-04 19:48:03 +01:00
{
2026-05-28 20:27:05 +02:00
return new List<string>();
2026-02-04 19:48:03 +01:00
}
2024-10-14 23:05:18 +02:00
}
2024-10-14 22:55:52 +02:00
2025-07-29 11:07:49 +02:00
private CancellationTokenSource? _mainToken;
public CancellationTokenSource? MainToken
{
get => _mainToken;
set
{
_mainToken = value;
NotifyPropertyChanged();
}
}
2024-10-14 22:55:52 +02:00
public string SourcePath
{
get => _paths.SourcePath;
set => _paths.SourcePath = value;
2024-10-14 22:55:52 +02:00
}
public string DestinationPath
{
get => _paths.DestinationPath;
set => _paths.DestinationPath = value;
2024-10-14 22:55:52 +02:00
}
public string HorizontalText
{
get => _visual.HorizontalText;
set => _visual.HorizontalText = value;
}
2025-07-29 11:10:54 +02:00
2025-07-28 10:34:03 +02:00
public string VerticalText
{
get => _visual.VerticalText;
set => _visual.VerticalText = value;
2025-07-28 10:34:03 +02:00
}
2025-07-28 14:45:03 +02:00
public bool OverwriteImages
{
get => _visual.OverwriteImages;
set => _visual.OverwriteImages = value;
2025-07-28 14:45:03 +02:00
}
2024-10-14 23:48:21 +02:00
private bool _uiEnabled = true;
public bool UiEnabled
{
2025-07-28 10:34:03 +02:00
get => _uiEnabled;
2024-10-14 23:48:21 +02:00
set
{
_uiEnabled = value;
NotifyPropertyChanged();
2026-02-04 21:12:27 +01:00
NotifyPropertyChanged(nameof(UiDisabled));
2024-10-14 23:48:21 +02:00
}
}
2025-07-29 11:10:54 +02:00
2025-07-29 10:34:23 +02:00
public bool UiDisabled => !_uiEnabled;
public ProcessingStateViewModel Processing => _processing;
public PathSettingsViewModel Paths => _paths;
public VisualSettingsViewModel Visual => _visual;
2025-07-29 10:34:23 +02:00
private void OnProcessingPropertyChanged(object? sender, PropertyChangedEventArgs e)
2025-07-29 10:34:23 +02:00
{
if (string.IsNullOrWhiteSpace(e.PropertyName))
2025-07-29 10:34:23 +02:00
{
return;
2025-07-29 10:34:23 +02:00
}
// Keep existing DataModel bindings working while state lives in child viewmodel.
NotifyPropertyChanged(e.PropertyName);
}
private void OnPathsPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (string.IsNullOrWhiteSpace(e.PropertyName))
{
return;
}
if (string.Equals(e.PropertyName, nameof(PathSettingsViewModel.DestinationPath), StringComparison.Ordinal))
{
UpdateAiCsvOutputPathForDestination();
}
NotifyPropertyChanged(e.PropertyName);
}
private void OnAiPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (string.IsNullOrWhiteSpace(e.PropertyName))
{
return;
}
NotifyPropertyChanged(e.PropertyName);
UpdateFaceEncoderCommandStates();
UpdateFaceMatcherCommandStates();
}
private void OnRaceUploadPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (string.IsNullOrWhiteSpace(e.PropertyName))
{
return;
}
NotifyPropertyChanged(e.PropertyName);
}
private void OnVisualPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (string.IsNullOrWhiteSpace(e.PropertyName))
{
return;
}
NotifyPropertyChanged(e.PropertyName);
}
public string SpeedCounter
{
get => _processing.SpeedCounter;
set => _processing.SpeedCounter = value;
2025-07-29 10:34:23 +02:00
}
2024-10-14 23:48:21 +02:00
2025-09-19 09:53:31 +02:00
private int _chunkSize;
public int ChunkSize
{
get => _chunkSize;
set
{
_chunkSize = value;
NotifyPropertyChanged();
}
}
private int _threadsCount;
public int ThreadsCount
{
get => _threadsCount;
set
{
_threadsCount = value;
NotifyPropertyChanged();
}
}
2026-02-04 19:48:03 +01:00
// Thumbnail settings
public string ThumbnailPrefix
{
get => _visual.ThumbnailPrefix;
set => _visual.ThumbnailPrefix = value;
2026-02-04 19:48:03 +01:00
}
public int ThumbnailHeight
{
get => _visual.ThumbnailHeight;
set => _visual.ThumbnailHeight = value;
2026-02-04 19:48:03 +01:00
}
public int ThumbnailWidth
{
get => _visual.ThumbnailWidth;
set => _visual.ThumbnailWidth = value;
2026-02-04 19:48:03 +01:00
}
// Big photo settings
public int PhotoBigHeight
{
get => _visual.PhotoBigHeight;
set => _visual.PhotoBigHeight = value;
2026-02-04 19:48:03 +01:00
}
public int PhotoBigWidth
{
get => _visual.PhotoBigWidth;
set => _visual.PhotoBigWidth = value;
2026-02-04 19:48:03 +01:00
}
// Font settings
public int FontSize
{
get => _visual.FontSize;
set => _visual.FontSize = value;
2026-02-04 19:48:03 +01:00
}
public int FontSizeThumbnail
{
get => _visual.FontSizeThumbnail;
set => _visual.FontSizeThumbnail = value;
2026-02-04 19:48:03 +01:00
}
public string FontName
{
get => _visual.FontName;
set => _visual.FontName = value;
2026-02-04 19:48:03 +01:00
}
public bool FontBold
{
get => _visual.FontBold;
set => _visual.FontBold = value;
2026-02-04 19:48:03 +01:00
}
// Text settings
public int TextTransparency
{
get => _visual.TextTransparency;
set => _visual.TextTransparency = value;
2026-02-04 19:48:03 +01:00
}
public int TextMargin
{
get => _visual.TextMargin;
set => _visual.TextMargin = value;
2026-02-04 19:48:03 +01:00
}
public string TextColorRGB
{
get => _visual.TextColorRGB;
set => _visual.TextColorRGB = value;
2026-02-04 19:48:03 +01:00
}
public string TransparentColor
{
get => _visual.TransparentColor;
set => _visual.TransparentColor = value;
}
public bool UseTransparentColor
{
get => _visual.UseTransparentColor;
set => _visual.UseTransparentColor = value;
}
2026-02-04 19:48:03 +01:00
// Logo/Watermark settings
public string LogoFile
{
get => _visual.LogoFile;
set => _visual.LogoFile = value;
2026-02-04 19:48:03 +01:00
}
public int LogoHeight
{
get => _visual.LogoHeight;
set => _visual.LogoHeight = value;
2026-02-04 19:48:03 +01:00
}
public int LogoWidth
{
get => _visual.LogoWidth;
set => _visual.LogoWidth = value;
2026-02-04 19:48:03 +01:00
}
public int LogoMargin
{
get => _visual.LogoMargin;
set => _visual.LogoMargin = value;
2026-02-04 19:48:03 +01:00
}
public int LogoTransparency
{
get => _visual.LogoTransparency;
set => _visual.LogoTransparency = value;
2026-02-04 19:48:03 +01:00
}
// Folder division settings
private int _filesPerFolder = 99;
public int FilesPerFolder
{
get => _filesPerFolder;
set
{
_filesPerFolder = value;
NotifyPropertyChanged();
}
}
private string _folderSuffix = "";
public string FolderSuffix
{
get => _folderSuffix;
set
{
_folderSuffix = value;
NotifyPropertyChanged();
}
}
private int _counterDigits = 2;
public int CounterDigits
{
get => _counterDigits;
set
{
_counterDigits = value;
NotifyPropertyChanged();
}
}
// Vertical text settings
public int VerticalTextSize
{
get => _visual.VerticalTextSize;
set => _visual.VerticalTextSize = value;
2026-02-04 19:48:03 +01:00
}
public int VerticalTextMargin
{
get => _visual.VerticalTextMargin;
set => _visual.VerticalTextMargin = value;
2026-02-04 19:48:03 +01:00
}
// JPEG compression settings
public int JpegQuality
{
get => _visual.JpegQuality;
set => _visual.JpegQuality = value;
2026-02-04 19:48:03 +01:00
}
public int JpegQualityThumbnail
{
get => _visual.JpegQualityThumbnail;
set => _visual.JpegQualityThumbnail = value;
2026-02-04 19:48:03 +01:00
}
// CheckBox settings
private bool _createThumbnails = true;
public bool CreateThumbnails
{
get => _createThumbnails;
set
{
_createThumbnails = value;
NotifyPropertyChanged();
}
}
private bool _automaticRotation;
public bool AutomaticRotation
{
get => _automaticRotation;
set
{
_automaticRotation = value;
NotifyPropertyChanged();
}
}
private bool _forceJpeg;
public bool ForceJpeg
{
get => _forceJpeg;
set
{
_forceJpeg = value;
NotifyPropertyChanged();
}
}
private bool _updateSubdirectories;
public bool UpdateSubdirectories
{
get => _updateSubdirectories;
set
{
_updateSubdirectories = value;
NotifyPropertyChanged();
}
}
private bool _createSubfolders;
public bool CreateSubfolders
{
get => _createSubfolders;
set
{
_createSubfolders = value;
NotifyPropertyChanged();
}
}
private bool _addTime;
public bool AddTime
{
get => _addTime;
set
{
_addTime = value;
NotifyPropertyChanged();
}
}
private bool _addRaceTime;
public bool AddRaceTime
{
get => _addRaceTime;
set
{
_addRaceTime = value;
NotifyPropertyChanged();
}
}
public bool AddLogo
{
get => _visual.AddLogo;
set => _visual.AddLogo = value;
2026-02-04 19:48:03 +01:00
}
public bool KeepOriginalDimensions
{
get => _visual.KeepOriginalDimensions;
set => _visual.KeepOriginalDimensions = value;
2026-02-04 19:48:03 +01:00
}
private bool _showDate;
public bool ShowDate
{
get => _showDate;
set
{
_showDate = value;
NotifyPropertyChanged();
}
}
private bool _showPhotoNumber;
public bool ShowPhotoNumber
{
get => _showPhotoNumber;
set
{
if (_showPhotoNumber == value) return;
2026-02-04 19:48:03 +01:00
_showPhotoNumber = value;
if (value)
{
// ensure mutually exclusive choices
_addTimeToThumbnails = false;
_addTextToThumbnails = false;
_addNumberAndTimeToThumbnails = false;
_addRaceTimeToThumbnails = false;
NotifyPropertyChanged(nameof(AddTimeToThumbnails));
NotifyPropertyChanged(nameof(AddTextToThumbnails));
NotifyPropertyChanged(nameof(AddNumberAndTimeToThumbnails));
NotifyPropertyChanged(nameof(AddRaceTimeToThumbnails));
}
2026-02-04 19:48:03 +01:00
NotifyPropertyChanged();
NotifyPropertyChanged(nameof(ThumbnailMode));
2026-02-04 19:48:03 +01:00
}
}
private bool _shutdownSystem;
public bool ShutdownSystem
{
get => _shutdownSystem;
set
{
_shutdownSystem = value;
NotifyPropertyChanged();
}
}
// ComboBox position/alignment settings
private string _verticalPosition = "Basso";
public string VerticalPosition
{
get => _verticalPosition;
set
{
_verticalPosition = value;
NotifyPropertyChanged();
}
}
private string _horizontalAlignment = "Centro";
public string HorizontalAlignment
{
get => _horizontalAlignment;
set
{
_horizontalAlignment = value;
NotifyPropertyChanged();
}
}
private string _logoHorizontalPosition = "Destra";
public string LogoHorizontalPosition
{
get => _logoHorizontalPosition;
set
{
_logoHorizontalPosition = value;
NotifyPropertyChanged();
}
}
private string _logoVerticalPosition = "Basso";
public string LogoVerticalPosition
{
get => _logoVerticalPosition;
set
{
_logoVerticalPosition = value;
NotifyPropertyChanged();
}
}
// RadioButton settings
private bool _useProgressiveNumbering = true;
public bool UseProgressiveNumbering
{
get => _useProgressiveNumbering;
set
{
_useProgressiveNumbering = value;
NotifyPropertyChanged();
}
}
private bool _useFileNumbering;
public bool UseFileNumbering
{
get => _useFileNumbering;
set
{
_useFileNumbering = value;
NotifyPropertyChanged();
}
}
private bool _useParallelProcessing = true;
public bool UseParallelProcessing
{
get => _useParallelProcessing;
set
{
_useParallelProcessing = value;
NotifyPropertyChanged();
}
}
private bool _useSequentialProcessing;
public bool UseSequentialProcessing
{
get => _useSequentialProcessing;
set
{
_useSequentialProcessing = value;
NotifyPropertyChanged();
}
}
// Additional settings that were missing
private bool _addTimeToThumbnails;
public bool AddTimeToThumbnails
{
get => _thumbnailOption == ThumbnailOptionEnum.Time;
2026-02-04 19:48:03 +01:00
set
{
if (value)
{
ThumbnailOption = ThumbnailOptionEnum.Time;
}
else if (_thumbnailOption == ThumbnailOptionEnum.Time)
{
ThumbnailOption = ThumbnailOptionEnum.None;
}
2026-02-04 19:48:03 +01:00
NotifyPropertyChanged();
}
}
private bool _showFileNameOnThumbnails;
public bool ShowFileNameOnThumbnails
{
get => _thumbnailOption == ThumbnailOptionEnum.FileName;
2026-02-04 19:48:03 +01:00
set
{
if (value)
ThumbnailOption = ThumbnailOptionEnum.FileName;
else if (_thumbnailOption == ThumbnailOptionEnum.FileName)
ThumbnailOption = ThumbnailOptionEnum.None;
2026-02-04 19:48:03 +01:00
NotifyPropertyChanged();
}
}
private DateTime _raceStartDate = DateTime.Now;
public DateTime RaceStartDate
{
get => _raceStartDate;
set
{
_raceStartDate = value;
NotifyPropertyChanged();
}
}
private string _timeLabel = "";
public string TimeLabel
{
get => _timeLabel;
set
{
_timeLabel = value;
NotifyPropertyChanged();
}
}
2026-02-04 22:10:16 +01:00
private string _bigPhotoSuffix = "";
public string BigPhotoSuffix
{
get => _bigPhotoSuffix;
set
{
_bigPhotoSuffix = value;
NotifyPropertyChanged();
}
}
private bool _addTextToThumbnails;
public bool AddTextToThumbnails
{
get => _thumbnailOption == ThumbnailOptionEnum.Text;
2026-02-04 22:10:16 +01:00
set
{
if (value)
ThumbnailOption = ThumbnailOptionEnum.Text;
else if (_thumbnailOption == ThumbnailOptionEnum.Text)
ThumbnailOption = ThumbnailOptionEnum.None;
2026-02-04 22:10:16 +01:00
NotifyPropertyChanged();
}
}
private bool _addRaceTimeToThumbnails;
public bool AddRaceTimeToThumbnails
{
get => _thumbnailOption == ThumbnailOptionEnum.RaceTime;
2026-02-04 22:10:16 +01:00
set
{
if (value)
ThumbnailOption = ThumbnailOptionEnum.RaceTime;
else if (_thumbnailOption == ThumbnailOptionEnum.RaceTime)
ThumbnailOption = ThumbnailOptionEnum.None;
2026-02-04 22:10:16 +01:00
NotifyPropertyChanged();
}
}
private bool _addNumberAndTimeToThumbnails;
public bool AddNumberAndTimeToThumbnails
{
get => _thumbnailOption == ThumbnailOptionEnum.FileNameAndTime;
2026-02-04 22:10:16 +01:00
set
{
if (value)
ThumbnailOption = ThumbnailOptionEnum.FileNameAndTime;
else if (_thumbnailOption == ThumbnailOptionEnum.FileNameAndTime)
ThumbnailOption = ThumbnailOptionEnum.None;
NotifyPropertyChanged();
}
}
// New enum and authoritative property for thumbnail selection
public enum ThumbnailOptionEnum
{
None = 0,
Text = 1,
FileName = 2,
Time = 3,
FileNameAndTime = 4,
RaceTime = 5
}
private ThumbnailOptionEnum _thumbnailOption = ThumbnailOptionEnum.None;
// Name matches DTO property so SettingsService mapping works
public ThumbnailOptionEnum ThumbnailOption
{
get => _thumbnailOption;
set
{
if (_thumbnailOption == value) return;
_thumbnailOption = value;
// Notify all dependent properties so UI updates
NotifyPropertyChanged();
NotifyPropertyChanged(nameof(AddTextToThumbnails));
NotifyPropertyChanged(nameof(AddTimeToThumbnails));
NotifyPropertyChanged(nameof(ShowPhotoNumber));
NotifyPropertyChanged(nameof(AddNumberAndTimeToThumbnails));
NotifyPropertyChanged(nameof(AddRaceTimeToThumbnails));
NotifyPropertyChanged(nameof(ShowFileNameOnThumbnails));
NotifyPropertyChanged(nameof(ThumbnailMode));
}
}
// Helper int property to bind ComboBox SelectedIndex in the WinForms designer
public int ThumbnailOptionIndex
{
get => (int)ThumbnailOption;
set
{
var opt = (ThumbnailOptionEnum)value;
if (opt == ThumbnailOption) return;
ThumbnailOption = opt;
2026-02-04 22:10:16 +01:00
NotifyPropertyChanged();
}
}
// Single authoritative thumbnail mode string to avoid conflicting bindings
// Possible values: "None", "Text", "Time", "Number", "NumberAndTime", "RaceTime"
public string ThumbnailMode
{
get
{
return _thumbnailOption switch
{
ThumbnailOptionEnum.Text => "Text",
ThumbnailOptionEnum.Time => "Time",
ThumbnailOptionEnum.FileName => "Number",
ThumbnailOptionEnum.FileNameAndTime => "NumberAndTime",
ThumbnailOptionEnum.RaceTime => "RaceTime",
_ => "None",
};
}
set
{
// Map incoming string to enum and set the authoritative property
switch ((value ?? string.Empty).ToLowerInvariant())
{
case "text":
ThumbnailOption = ThumbnailOptionEnum.Text;
break;
case "time":
ThumbnailOption = ThumbnailOptionEnum.Time;
break;
case "number":
ThumbnailOption = ThumbnailOptionEnum.FileName;
break;
case "numberandtime":
ThumbnailOption = ThumbnailOptionEnum.FileNameAndTime;
break;
case "racetime":
ThumbnailOption = ThumbnailOptionEnum.RaceTime;
break;
default:
ThumbnailOption = ThumbnailOptionEnum.None;
break;
}
}
}
2026-02-04 23:16:06 +01:00
// Image processing progress and status
public string ProcessingStatus
{
get => _processing.ProcessingStatus;
set => _processing.ProcessingStatus = value;
2026-02-04 23:16:06 +01:00
}
public int ProcessedImagesCount
{
get => _processing.ProcessedImagesCount;
set => _processing.ProcessedImagesCount = value;
2026-02-04 23:16:06 +01:00
}
public int TotalImagesCount
{
get => _processing.TotalImagesCount;
set => _processing.TotalImagesCount = value;
2026-02-04 23:16:06 +01:00
}
public int ProgressBarValue
{
get => _processing.ProgressBarValue;
set => _processing.ProgressBarValue = value;
2026-02-04 23:16:06 +01:00
}
public int ProgressBarMaximum
{
get => _processing.ProgressBarMaximum;
set => _processing.ProgressBarMaximum = value;
2026-02-04 23:16:06 +01:00
}
2024-10-14 23:25:35 +02:00
private void Test(object parameter)
{
Debug.WriteLine("Yep");
2024-10-14 23:48:21 +02:00
this.UiEnabled = !this.UiEnabled;
2024-10-14 23:25:35 +02:00
}
private async Task TestAsync()
{
Debug.WriteLine("Yep c");
}
2026-05-09 17:27:05 +02:00
public async Task ProcessImages()
2025-07-23 17:16:06 +02:00
{
2026-02-04 23:16:06 +01:00
_logger.LogInformation("Avvio elaborazione...");
UiEnabled = false;
MainToken?.Dispose();
MainToken = new CancellationTokenSource();
var token = MainToken.Token;
// Normalize paths
_paths.NormalizePaths();
2026-02-04 23:16:06 +01:00
// Reset counters
_processing.ResetForRun();
2026-02-04 23:16:06 +01:00
// Update PicSettings from DataModel using AutoMapper
_mapper.Map(this, _picSettings);
// Explicitly ensure thumbnail-related flags are applied to PicSettings
// because AutoMapper may not map differently-named properties.
try
{
_picSettings.AggiungiScritteMiniature = this.AddTextToThumbnails;
_picSettings.UsaOrarioMiniatura = this.AddTimeToThumbnails;
_picSettings.AggNumTempMin = this.AddNumberAndTimeToThumbnails;
_picSettings.AggTempoGaraMin = this.AddRaceTimeToThumbnails;
_picSettings.CreaMiniature = this.CreateThumbnails;
_picSettings.LarghezzaSmall = this.ThumbnailWidth;
_picSettings.AltezzaSmall = this.ThumbnailHeight;
_picSettings.DimMin = this.FontSizeThumbnail;
_picSettings.JpegQualityMin = this.JpegQualityThumbnail;
}
catch
{
// Best-effort; do not fail processing on mapping issues
}
2026-02-21 15:53:52 +01:00
var imageCreationOptions = new ImageCreationService.Options
2026-02-04 23:16:06 +01:00
{
AggiornaSottodirectory = UpdateSubdirectories,
CreaSottocartelle = CreateSubfolders,
FilePerCartella = FilesPerFolder,
SuffissoCartelle = FolderSuffix,
CifreContatore = CounterDigits,
NumerazioneType = UseProgressiveNumbering ? NumerazioneType.Progressiva : NumerazioneType.Files,
SourcePath = SourcePath,
DestinationPath = DestinationPath,
MaxThreads = ThreadsCount,
ChunksSize = ChunkSize,
LinearExecution = UseSequentialProcessing
};
try
{
var runResult = await _imageProcessingCoordinator.RunAsync(
new ImageProcessingRunRequest { Options = imageCreationOptions },
token,
update =>
{
ProcessedImagesCount = update.Processed;
TotalImagesCount = update.Total;
ProgressBarMaximum = update.Total;
ProgressBarValue = update.Processed;
ProcessingStatus = update.Status;
},
speed => { SpeedCounter = speed; }).ConfigureAwait(false);
SpeedCounter = runResult.FinalSpeedCounter;
2026-02-04 23:16:06 +01:00
}
catch (OperationCanceledException)
{
_logger.LogInformation("Operazione Cancellata");
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore durante l'elaborazione delle immagini");
ProcessingStatus = $"Errore: {ex.Message}";
}
finally
{
MainToken?.Dispose();
MainToken = null;
}
ProcessingStatus = "Finito";
UiEnabled = true;
}
2026-05-09 17:27:05 +02:00
public async Task RunNumberAiAsync(CancellationToken token)
{
await RunAiExtractionCoreAsync(token, useDestination: true, recursive: true, failOnInvalidPath: true).ConfigureAwait(false);
}
public async Task RunFaceAiAsync(CancellationToken token)
{
using var registration = token.Register(() => _ = StopFaceEncoderAsync("Arresto face encoder richiesto dalla CLI.", waitForExit: true));
await RunFaceEncoderAsync().ConfigureAwait(false);
}
2025-07-29 11:07:49 +02:00
private async Task CancelOperation()
{
try
{
var tokenSource = MainToken;
if (tokenSource is not null)
{
// Cancel synchronously and return to caller. Some CTSource implementations
// may provide async helpers but cancelling is immediate.
try
{
tokenSource.Cancel();
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Exception while cancelling token");
}
}
2025-07-29 11:07:49 +02:00
UiEnabled = true;
}
catch (Exception e)
{
_logger.LogError(e, "Error canceling the token");
2025-07-29 11:07:49 +02:00
_logger.LogInformation("Ignora questo errore");
}
}
2026-02-04 19:48:03 +01:00
public async Task StopFaceEncoderAsync(string reason, bool waitForExit = true)
{
var trackedProcess = GetTrackedFaceEncoderProcess();
var ownsProcess = false;
var process = trackedProcess;
if (process is null)
{
process = FindConfiguredFaceEncoderProcess();
ownsProcess = process is not null;
}
if (process is null)
{
await InvokeOnUiThreadAsync(() =>
{
IsFaceEncoderRunning = false;
FaceStatusMessage = "Face encoder non in esecuzione.";
}).ConfigureAwait(false);
return;
}
try
{
await InvokeOnUiThreadAsync(() => FaceStatusMessage = reason).ConfigureAwait(false);
var gracefulStopRequested = TryRequestFaceEncoderStop(process);
if (waitForExit)
{
var exited = await WaitForProcessExitAsync(process, TimeSpan.FromSeconds(5)).ConfigureAwait(false);
if (!exited)
{
try
{
process.Kill(entireProcessTree: true);
exited = await WaitForProcessExitAsync(process, TimeSpan.FromSeconds(5)).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Unable to terminate face encoder process {ProcessId}", process.Id);
}
}
await InvokeOnUiThreadAsync(() =>
{
IsFaceEncoderRunning = !exited && IsProcessAlive(process);
FaceStatusMessage = exited
? "Face encoder arrestato."
: gracefulStopRequested
? "Segnale di arresto inviato al face encoder."
: "Arresto forzato del face encoder richiesto.";
}).ConfigureAwait(false);
}
}
finally
{
if (ownsProcess)
{
process.Dispose();
}
}
}
private bool CanRunFaceEncoder()
{
return !IsFaceEncoderRunning;
}
private bool CanStopFaceEncoder()
{
return IsFaceEncoderRunning;
}
private void UpdateFaceEncoderCommandStates()
{
_startFaceEncoderCommand?.RaiseCanExecuteChanged();
_stopFaceEncoderCommand?.RaiseCanExecuteChanged();
}
private void UpdateFaceMatcherCommandStates()
{
_startFaceMatcherCommand?.RaiseCanExecuteChanged();
_stopFaceMatcherCommand?.RaiseCanExecuteChanged();
}
private async Task RunFaceEncoderAsync()
{
if (IsFaceEncoderRunning)
{
FaceStatusMessage = "Face encoder gia in esecuzione.";
return;
}
var executableRootPath = NormalizeFilePathArgument(FaceExecutablePath);
var outputFolderPath = NormalizeDirectoryPathArgument(FaceOutputFolderPath);
var imagesFolder = NormalizeDirectoryPathArgument(DestinationPath);
var executablePath = ResolveConfiguredFaceEncoderExecutablePath(executableRootPath, UseFaceGpu);
if (string.IsNullOrWhiteSpace(executableRootPath))
{
FaceStatusMessage = "Percorso cartella face encoder non valido.";
return;
}
if (string.IsNullOrWhiteSpace(executablePath) || !File.Exists(executablePath))
{
FaceStatusMessage = UseFaceGpu
? "Impossibile trovare face_encoder_gpu.exe nella cartella selezionata."
: "Impossibile trovare face_encoder_cpu.exe nella cartella selezionata.";
return;
}
if (string.IsNullOrWhiteSpace(imagesFolder) || !Directory.Exists(imagesFolder))
{
FaceStatusMessage = "Cartella Destinazione non valida.";
return;
}
if (string.IsNullOrWhiteSpace(outputFolderPath))
{
FaceStatusMessage = "Inserisci la cartella di output per encodings e log.";
return;
}
try
{
Directory.CreateDirectory(outputFolderPath);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unable to create face output directory: {OutputFolderPath}", outputFolderPath);
FaceStatusMessage = "Impossibile creare la cartella di output.";
return;
}
var parallelism = NormalizeFaceParallelism(FaceParallelism);
var minSize = NormalizeFaceMinSize(FaceMinSize);
var outputFiles = BuildFaceEncoderOutputPaths(outputFolderPath, imagesFolder, DateTime.Now);
FaceExecutablePath = executableRootPath;
FaceOutputFolderPath = outputFolderPath;
FaceCommandOutput = string.Empty;
FaceStatusMessage = "Esecuzione face encoder in corso...";
var transcriptLines = new StringBuilder();
var outputLines = new StringBuilder();
var errorLines = new StringBuilder();
try
{
var processStartInfo = new ProcessStartInfo
{
FileName = executablePath,
WorkingDirectory = Path.GetDirectoryName(executablePath) ?? Environment.CurrentDirectory,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
RedirectStandardInput = false,
CreateNoWindow = false,
};
processStartInfo.ArgumentList.Add("--images");
processStartInfo.ArgumentList.Add(imagesFolder);
processStartInfo.ArgumentList.Add("--out");
processStartInfo.ArgumentList.Add(outputFiles.OutputFilePath);
processStartInfo.ArgumentList.Add("--log");
processStartInfo.ArgumentList.Add(outputFiles.LogFilePath);
if (FaceRecursive)
{
processStartInfo.ArgumentList.Add("--recursive");
}
if (FaceIncludeThumbnails)
{
processStartInfo.ArgumentList.Add("--include-tn");
}
processStartInfo.ArgumentList.Add(UseFaceGpu ? "--multiprocess" : "--multicore");
processStartInfo.ArgumentList.Add(parallelism.ToString());
processStartInfo.ArgumentList.Add("--min-size");
processStartInfo.ArgumentList.Add(minSize.ToString());
if (FaceUpsample)
{
processStartInfo.ArgumentList.Add("--upsample");
}
using var process = new Process { StartInfo = processStartInfo, EnableRaisingEvents = true };
process.OutputDataReceived += (_, args) => AppendFaceProcessOutput(outputLines, transcriptLines, args.Data, isError: false);
process.ErrorDataReceived += (_, args) => AppendFaceProcessOutput(errorLines, transcriptLines, args.Data, isError: true);
process.Exited += (_, _) =>
{
_ = InvokeOnUiThreadAsync(() =>
{
if (!ComputeIsFaceEncoderRunning())
{
IsFaceEncoderRunning = false;
}
});
};
if (!process.Start())
{
throw new InvalidOperationException("Avvio face encoder fallito.");
}
_hasStartedFaceEncoderInSession = true;
EnsureFaceEncoderWatcherStarted();
TrackFaceEncoderProcess(process);
await InvokeOnUiThreadAsync(() => IsFaceEncoderRunning = true).ConfigureAwait(false);
if (UseFaceGpu)
{
StartFaceEncoderLogWatcher(outputFiles.LogFilePath, outputLines, transcriptLines);
}
process.BeginOutputReadLine();
process.BeginErrorReadLine();
await process.WaitForExitAsync().ConfigureAwait(false);
var summary = BuildFaceEncoderSummary(process.ExitCode, processStartInfo, outputFiles.OutputFilePath, outputFiles.LogFilePath, outputLines, errorLines);
await InvokeOnUiThreadAsync(() =>
{
FaceCommandOutput = string.IsNullOrWhiteSpace(FaceCommandOutput)
? summary
: $"{FaceCommandOutput.TrimEnd()}\n\n{summary}";
FaceStatusMessage = process.ExitCode == 0
? "Face encoder completato."
: $"Face encoder terminato con errore (code {process.ExitCode}).";
}).ConfigureAwait(false);
}
catch (Exception ex)
{
Console.Error.WriteLine(ex);
_logger.LogError(ex, "Face encoder execution failed.");
await InvokeOnUiThreadAsync(() =>
{
FaceCommandOutput = ex.ToString();
FaceStatusMessage = "Errore durante esecuzione face encoder.";
}).ConfigureAwait(false);
}
finally
{
await StopFaceEncoderLogWatcherAsync().ConfigureAwait(false);
ClearTrackedFaceEncoderProcess();
await InvokeOnUiThreadAsync(() => IsFaceEncoderRunning = ComputeIsFaceEncoderRunning()).ConfigureAwait(false);
}
}
private bool CanRunFaceMatcher()
{
return !IsFaceMatcherRunning;
}
private bool CanStopFaceMatcher()
{
return IsFaceMatcherRunning;
}
private async Task RunFaceMatcherAsync()
{
if (IsFaceMatcherRunning)
{
FaceMatcherStatusMessage = "Face matcher gia in esecuzione.";
return;
}
var executablePath = ResolveConfiguredFaceMatcherExecutablePath(NormalizeFilePathArgument(FaceMatcherExecutablePath), NormalizeFilePathArgument(FaceExecutablePath));
if (string.IsNullOrWhiteSpace(executablePath) || !File.Exists(executablePath))
{
FaceMatcherStatusMessage = "Percorso face_matcher.exe non valido.";
return;
}
var searchImagePath = NormalizeFilePathArgument(FaceMatcherSelectedImagePath);
if (string.IsNullOrWhiteSpace(searchImagePath) || !File.Exists(searchImagePath))
{
FaceMatcherStatusMessage = "Seleziona un'immagine valida per il match.";
return;
}
var encodingsPath = ResolveConfiguredFaceMatcherEncodingsPath(NormalizeFilePathArgument(FaceMatcherEncodingsPath), NormalizeDirectoryPathArgument(FaceOutputFolderPath));
if (string.IsNullOrWhiteSpace(encodingsPath) || !File.Exists(encodingsPath))
{
FaceMatcherStatusMessage = "File encodings .pkl non trovato.";
return;
}
var fallbackOutputRoot = ResolveFaceMatcherFallbackOutputRoot(NormalizeFilePathArgument(FaceMatcherOutputPath), NormalizeFilePathArgument(FaceMatcherLogPath), NormalizeDirectoryPathArgument(FaceOutputFolderPath), executablePath);
var outputPaths = ResolveFaceMatcherOutputPaths(FaceMatcherOutputPath, FaceMatcherLogPath, fallbackOutputRoot, searchImagePath, DateTime.Now);
try
{
Directory.CreateDirectory(Path.GetDirectoryName(outputPaths.CsvPath) ?? fallbackOutputRoot);
Directory.CreateDirectory(Path.GetDirectoryName(outputPaths.LogPath) ?? fallbackOutputRoot);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unable to create face matcher output directory.");
FaceMatcherStatusMessage = "Impossibile creare cartelle output/log del matcher.";
return;
}
var tolerance = NormalizeFaceMatcherTolerance(FaceMatcherTolerance);
FaceMatcherExecutablePath = executablePath;
FaceMatcherEncodingsPath = encodingsPath;
FaceMatcherOutputPath = outputPaths.CsvPath;
FaceMatcherLogPath = outputPaths.LogPath;
await InvokeOnUiThreadAsync(() =>
{
FaceMatcherResults.Clear();
FaceMatcherCommandOutput = string.Empty;
FaceMatcherStatusMessage = "Esecuzione face matcher in corso...";
}).ConfigureAwait(false);
var transcriptLines = new StringBuilder();
var outputLines = new StringBuilder();
var errorLines = new StringBuilder();
try
{
var processStartInfo = new ProcessStartInfo
{
FileName = executablePath,
WorkingDirectory = Path.GetDirectoryName(executablePath) ?? Environment.CurrentDirectory,
UseShellExecute = false,
RedirectStandardOutput = false,
RedirectStandardError = false,
RedirectStandardInput = false,
CreateNoWindow = true,
};
processStartInfo.Environment["PYTHONUTF8"] = "1";
processStartInfo.Environment["PYTHONIOENCODING"] = "utf-8";
processStartInfo.ArgumentList.Add("--image");
processStartInfo.ArgumentList.Add(searchImagePath);
processStartInfo.ArgumentList.Add("--encodings");
processStartInfo.ArgumentList.Add(encodingsPath);
processStartInfo.ArgumentList.Add("--out");
processStartInfo.ArgumentList.Add(outputPaths.CsvPath);
processStartInfo.ArgumentList.Add("--log");
processStartInfo.ArgumentList.Add(outputPaths.LogPath);
processStartInfo.ArgumentList.Add("--tolerance");
processStartInfo.ArgumentList.Add(tolerance.ToString("0.##", CultureInfo.InvariantCulture));
using var process = new Process { StartInfo = processStartInfo, EnableRaisingEvents = true };
process.Exited += (_, _) =>
{
_ = InvokeOnUiThreadAsync(() =>
{
if (!ComputeIsFaceMatcherRunning())
{
IsFaceMatcherRunning = false;
}
});
};
if (!process.Start())
{
throw new InvalidOperationException("Avvio face matcher fallito.");
}
_hasStartedFaceMatcherInSession = true;
EnsureFaceMatcherWatcherStarted();
TrackFaceMatcherProcess(process);
await InvokeOnUiThreadAsync(() => IsFaceMatcherRunning = true).ConfigureAwait(false);
StartFaceMatcherLogWatcher(outputPaths.LogPath, outputLines, transcriptLines);
await process.WaitForExitAsync().ConfigureAwait(false);
var isNoFacesRun = process.ExitCode == 1 && await LogIndicatesNoFacesAsync(outputPaths.LogPath).ConfigureAwait(false);
if (process.ExitCode == 0 || isNoFacesRun)
{
var parsedRows = await ParseFaceMatcherCsvAsync(outputPaths.CsvPath, outputPaths.LogPath).ConfigureAwait(false);
var resolvedPaths = ResolveDestinationImagesByFileName(DestinationPath, parsedRows.Select(row => row.PhotoId));
await InvokeOnUiThreadAsync(() =>
{
FaceMatcherResults.Clear();
foreach (var row in parsedRows)
{
resolvedPaths.TryGetValue(row.PhotoId, out var candidates);
candidates ??= [];
FaceMatcherResults.Add(new FaceMatcherResultItem
{
PhotoId = row.PhotoId,
Score = row.Score,
ResolvedImagePath = candidates.FirstOrDefault() ?? string.Empty,
CandidateCount = candidates.Count,
RawRow = row.RawRow,
DebugSummary = row.DebugSummary,
SearchImagePath = searchImagePath,
CsvPath = outputPaths.CsvPath,
LogPath = outputPaths.LogPath
});
}
}).ConfigureAwait(false);
}
var summary = BuildFaceMatcherSummary(process.ExitCode, processStartInfo, outputPaths.CsvPath, outputPaths.LogPath, outputLines, errorLines, FaceMatcherResults.Count);
await InvokeOnUiThreadAsync(() =>
{
FaceMatcherCommandOutput = string.IsNullOrWhiteSpace(FaceMatcherCommandOutput)
? summary
: $"{FaceMatcherCommandOutput.TrimEnd()}\n\n{summary}";
FaceMatcherStatusMessage = process.ExitCode switch
{
0 when FaceMatcherResults.Count > 0 => $"Face matcher completato: {FaceMatcherResults.Count} match.",
0 => "Face matcher completato senza match.",
1 when isNoFacesRun => "Face matcher completato: nessun volto rilevato nell'immagine di ricerca.",
_ => $"Face matcher terminato con errore (code {process.ExitCode})."
};
}).ConfigureAwait(false);
}
catch (Exception ex)
{
Console.Error.WriteLine(ex);
_logger.LogError(ex, "Face matcher execution failed.");
await InvokeOnUiThreadAsync(() =>
{
FaceMatcherCommandOutput = ex.ToString();
FaceMatcherStatusMessage = "Errore durante esecuzione face matcher.";
}).ConfigureAwait(false);
}
finally
{
await StopFaceMatcherLogWatcherAsync().ConfigureAwait(false);
ClearTrackedFaceMatcherProcess();
await InvokeOnUiThreadAsync(() => IsFaceMatcherRunning = ComputeIsFaceMatcherRunning()).ConfigureAwait(false);
}
}
public async Task StopFaceMatcherAsync(string reason, bool waitForExit = true)
{
var trackedProcess = GetTrackedFaceMatcherProcess();
Process? process = null;
if (trackedProcess is not null)
{
process = trackedProcess;
}
else if (_hasStartedFaceMatcherInSession)
{
process = FindConfiguredFaceMatcherProcess();
}
if (process is null)
{
await StopFaceMatcherLogWatcherAsync().ConfigureAwait(false);
await InvokeOnUiThreadAsync(() =>
{
IsFaceMatcherRunning = false;
FaceMatcherStatusMessage = "Face matcher non in esecuzione.";
}).ConfigureAwait(false);
return;
}
using (process)
{
var gracefulStopRequested = TryRequestFaceMatcherStop(process);
var exited = !IsProcessAlive(process);
if (!exited && waitForExit)
{
exited = await WaitForProcessExitAsync(process, TimeSpan.FromSeconds(5)).ConfigureAwait(false);
}
if (!exited)
{
try
{
process.Kill(entireProcessTree: true);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Unable to terminate face matcher process {ProcessId}", process.Id);
}
}
await StopFaceMatcherLogWatcherAsync().ConfigureAwait(false);
ClearTrackedFaceMatcherProcess();
await InvokeOnUiThreadAsync(() =>
{
IsFaceMatcherRunning = !exited && IsProcessAlive(process);
FaceMatcherStatusMessage = exited
? "Face matcher arrestato."
: gracefulStopRequested
? "Segnale di arresto inviato al face matcher."
: "Arresto forzato del face matcher richiesto.";
}).ConfigureAwait(false);
}
}
private async Task WatchFaceEncoderProcessAsync(CancellationToken token)
{
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
try
{
while (await timer.WaitForNextTickAsync(token).ConfigureAwait(false))
{
if (!_hasStartedFaceEncoderInSession)
{
continue;
}
var isRunning = ComputeIsFaceEncoderRunning();
if (isRunning != IsFaceEncoderRunning)
{
await InvokeOnUiThreadAsync(() => IsFaceEncoderRunning = isRunning).ConfigureAwait(false);
}
}
}
catch (OperationCanceledException)
{
// App shutdown.
}
}
private async Task WatchFaceMatcherProcessAsync(CancellationToken token)
{
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
try
{
while (await timer.WaitForNextTickAsync(token).ConfigureAwait(false))
{
if (!_hasStartedFaceMatcherInSession)
{
continue;
}
var isRunning = ComputeIsFaceMatcherRunning();
if (isRunning != IsFaceMatcherRunning)
{
await InvokeOnUiThreadAsync(() => IsFaceMatcherRunning = isRunning).ConfigureAwait(false);
}
}
}
catch (OperationCanceledException)
{
// App shutdown.
}
}
private void EnsureFaceEncoderWatcherStarted()
{
if (_faceEncoderWatcherTask is not null)
{
return;
}
_faceEncoderWatcherTokenSource = new CancellationTokenSource();
_faceEncoderWatcherTask = WatchFaceEncoderProcessAsync(_faceEncoderWatcherTokenSource.Token);
}
private void EnsureFaceMatcherWatcherStarted()
{
if (_faceMatcherWatcherTask is not null)
{
return;
}
_faceMatcherWatcherTokenSource = new CancellationTokenSource();
_faceMatcherWatcherTask = WatchFaceMatcherProcessAsync(_faceMatcherWatcherTokenSource.Token);
}
private void StartFaceEncoderLogWatcher(string logFilePath, StringBuilder outputLines, StringBuilder transcriptLines)
{
_faceEncoderLogWatcherTokenSource?.Cancel();
_faceEncoderLogWatcherTokenSource?.Dispose();
_faceEncoderLogWatcherTokenSource = new CancellationTokenSource();
_faceEncoderLogWatcherTask = WatchFaceEncoderLogFileAsync(logFilePath, outputLines, transcriptLines, _faceEncoderLogWatcherTokenSource.Token);
}
private void StartFaceMatcherLogWatcher(string logFilePath, StringBuilder outputLines, StringBuilder transcriptLines)
{
_faceMatcherLogWatcherTokenSource?.Cancel();
_faceMatcherLogWatcherTokenSource?.Dispose();
_faceMatcherLogWatcherTokenSource = new CancellationTokenSource();
_faceMatcherLogWatcherTask = WatchFaceMatcherLogFileAsync(logFilePath, outputLines, transcriptLines, _faceMatcherLogWatcherTokenSource.Token);
}
private async Task StopFaceEncoderLogWatcherAsync()
{
var tokenSource = _faceEncoderLogWatcherTokenSource;
var task = _faceEncoderLogWatcherTask;
_faceEncoderLogWatcherTokenSource = null;
_faceEncoderLogWatcherTask = null;
if (tokenSource is null)
{
return;
}
try
{
await tokenSource.CancelAsync().ConfigureAwait(false);
if (task is not null)
{
await task.ConfigureAwait(false);
}
}
catch (OperationCanceledException)
{
// Expected when shutting down the watcher.
}
finally
{
tokenSource.Dispose();
}
}
private async Task StopFaceMatcherLogWatcherAsync()
{
var tokenSource = _faceMatcherLogWatcherTokenSource;
var task = _faceMatcherLogWatcherTask;
_faceMatcherLogWatcherTokenSource = null;
_faceMatcherLogWatcherTask = null;
if (tokenSource is null)
{
return;
}
try
{
await tokenSource.CancelAsync().ConfigureAwait(false);
if (task is not null)
{
await task.ConfigureAwait(false);
}
}
catch (OperationCanceledException)
{
// Expected when shutting down the watcher.
}
finally
{
tokenSource.Dispose();
}
}
private async Task WatchFaceEncoderLogFileAsync(string logFilePath, StringBuilder outputLines, StringBuilder transcriptLines, CancellationToken token)
{
long filePosition = 0;
while (!token.IsCancellationRequested)
{
try
{
if (File.Exists(logFilePath))
{
using var stream = new FileStream(logFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
if (filePosition > stream.Length)
{
filePosition = 0;
}
stream.Seek(filePosition, SeekOrigin.Begin);
using var reader = new StreamReader(stream);
while (!reader.EndOfStream)
{
var line = await reader.ReadLineAsync(token).ConfigureAwait(false);
AppendFaceProcessOutput(outputLines, transcriptLines, line, isError: false);
}
filePosition = stream.Position;
}
}
catch (OperationCanceledException)
{
throw;
}
catch (IOException)
{
// Retry on the next polling interval while the encoder is still writing.
}
catch (UnauthorizedAccessException)
{
// Retry on the next polling interval if the log file is transiently locked.
}
await Task.Delay(TimeSpan.FromMilliseconds(250), token).ConfigureAwait(false);
}
}
private async Task WatchFaceMatcherLogFileAsync(string logFilePath, StringBuilder outputLines, StringBuilder transcriptLines, CancellationToken token)
{
long filePosition = 0;
while (!token.IsCancellationRequested)
{
try
{
if (File.Exists(logFilePath))
{
using var stream = new FileStream(logFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
if (filePosition > stream.Length)
{
filePosition = 0;
}
stream.Seek(filePosition, SeekOrigin.Begin);
using var reader = new StreamReader(stream);
while (!reader.EndOfStream)
{
var line = await reader.ReadLineAsync(token).ConfigureAwait(false);
AppendFaceMatcherProcessOutput(outputLines, transcriptLines, line, isError: false);
}
filePosition = stream.Position;
}
}
catch (OperationCanceledException)
{
throw;
}
catch (IOException)
{
// Retry while the matcher is still writing.
}
catch (UnauthorizedAccessException)
{
// Retry if the file is transiently locked.
}
await Task.Delay(TimeSpan.FromMilliseconds(250), token).ConfigureAwait(false);
}
}
private void RefreshFaceExecutableCapabilities()
{
var executableRoot = NormalizeFilePathArgument(_ai.FaceExecutablePath);
var hasCpu = !string.IsNullOrWhiteSpace(ResolveConfiguredFaceEncoderExecutablePath(executableRoot, useGpu: false));
var hasGpu = !string.IsNullOrWhiteSpace(ResolveConfiguredFaceEncoderExecutablePath(executableRoot, useGpu: true));
_ai.FaceGpuOptionEnabled = hasCpu && hasGpu;
if (hasGpu && !hasCpu)
{
_ai.UseFaceGpu = true;
}
else if (!hasGpu)
{
_ai.UseFaceGpu = false;
}
}
private void QueueRefreshNumberAiGpuCapabilities()
{
if (!TryBuildNumberAiModelConfiguration(out var configuration))
{
_numberAiGpuValidationPending = false;
NumberAiGpuOptionEnabled = false;
_ai.UseNumberAiGpu = false;
return;
}
_numberAiGpuValidationPending = true;
var requestVersion = Interlocked.Increment(ref _numberAiGpuRefreshVersion);
_ = RefreshNumberAiGpuCapabilitiesAsync(configuration, requestVersion);
}
private async Task RefreshNumberAiGpuCapabilitiesAsync(ModelConfiguration configuration, int requestVersion)
{
try
{
var gpuAvailable = await Task.Run(() =>
NumberRecognitionEngine.TryValidateGpuRuntime(configuration, _logger, out _)).ConfigureAwait(false);
if (requestVersion != Volatile.Read(ref _numberAiGpuRefreshVersion))
{
return;
}
await InvokeOnUiThreadAsync(() =>
{
if (requestVersion != Volatile.Read(ref _numberAiGpuRefreshVersion))
{
return;
}
_numberAiGpuValidationPending = false;
NumberAiGpuOptionEnabled = gpuAvailable;
if (!gpuAvailable)
{
_ai.UseNumberAiGpu = false;
}
}).ConfigureAwait(false);
}
catch (Exception ex)
{
if (requestVersion != Volatile.Read(ref _numberAiGpuRefreshVersion))
{
return;
}
_logger.LogWarning(ex, "Failed to refresh OCR GPU capabilities.");
await InvokeOnUiThreadAsync(() =>
{
if (requestVersion != Volatile.Read(ref _numberAiGpuRefreshVersion))
{
return;
}
_numberAiGpuValidationPending = false;
NumberAiGpuOptionEnabled = false;
_ai.UseNumberAiGpu = false;
}).ConfigureAwait(false);
}
}
private void SetUseNumberAiGpu(bool value)
{
if (!value)
{
_ai.UseNumberAiGpu = false;
return;
}
if (NumberAiGpuOptionEnabled || _numberAiGpuValidationPending)
{
_ai.UseNumberAiGpu = true;
return;
}
_ai.UseNumberAiGpu = false;
}
private bool TryBuildNumberAiModelConfiguration(out ModelConfiguration configuration)
{
configuration = null!;
if (string.IsNullOrWhiteSpace(ModelsFolderPath))
{
return false;
}
var modelsRoot = Path.GetFullPath(ModelsFolderPath.Trim().Trim('"'));
if (!Directory.Exists(modelsRoot))
{
return false;
}
configuration = new ModelConfiguration
{
DetectionCfg = Path.Combine(modelsRoot, "detection.cfg"),
DetectionWeights = Path.Combine(modelsRoot, "detection.weights"),
RecognitionCfg = Path.Combine(modelsRoot, "recognition.cfg"),
RecognitionWeights = Path.Combine(modelsRoot, "recognition.weights"),
UseGpu = true
};
return File.Exists(configuration.DetectionCfg)
&& File.Exists(configuration.DetectionWeights)
&& File.Exists(configuration.RecognitionCfg)
&& File.Exists(configuration.RecognitionWeights);
}
private Task ShowErrorMessageAsync(string title, string message)
{
return InvokeOnUiThreadAsync(() =>
{
ShowMessageRequested?.Invoke(this, Tuple.Create(title, message, 0));
});
}
internal async Task<bool> ConfirmAiCsvOverwriteIfNeededAsync()
{
var csvOutputPath = NormalizeFilePathArgument(CsvOutputPath);
if (string.IsNullOrWhiteSpace(csvOutputPath) || !File.Exists(csvOutputPath))
{
return true;
}
var confirmOverwrite = ConfirmAiCsvOverwriteAsync;
if (confirmOverwrite is null)
{
return true;
}
var message = $"Il file CSV esiste gia:\n{csvOutputPath}\n\nVuoi sovrascriverlo? Se scegli Annulla l'operazione OCR non verra avviata.";
return await confirmOverwrite(AiCsvOverwriteDialogTitle, message).ConfigureAwait(false);
}
internal void UpdateAiCsvOutputPathForDestination()
{
var updatedPath = BuildAiCsvOutputPathForDestination(CsvOutputPath, DestinationPath);
if (string.Equals(updatedPath, CsvOutputPath, StringComparison.Ordinal))
{
return;
}
CsvOutputPath = updatedPath;
}
internal static string BuildAiCsvOutputPathForDestination(string currentCsvOutputPath, string destinationPath)
{
var normalizedCsvPath = NormalizeFilePathArgument(currentCsvOutputPath);
if (string.IsNullOrWhiteSpace(normalizedCsvPath))
{
return currentCsvOutputPath;
}
var directory = Path.GetDirectoryName(normalizedCsvPath);
var destinationFolderName = GetDestinationFolderName(destinationPath);
if (string.IsNullOrWhiteSpace(destinationFolderName))
{
return normalizedCsvPath;
}
var extension = Path.GetExtension(normalizedCsvPath);
if (string.IsNullOrWhiteSpace(extension))
{
extension = ".csv";
}
var safeFileName = SanitizeFileName(destinationFolderName) + extension;
return string.IsNullOrWhiteSpace(directory)
? safeFileName
: Path.Combine(directory, safeFileName);
}
private static string GetDestinationFolderName(string destinationPath)
{
var normalizedDestinationPath = NormalizeDirectoryPathArgument(destinationPath);
if (string.IsNullOrWhiteSpace(normalizedDestinationPath))
{
return string.Empty;
}
var trimmedPath = normalizedDestinationPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
if (string.IsNullOrWhiteSpace(trimmedPath))
{
return string.Empty;
}
var rootPath = Path.GetPathRoot(trimmedPath)?.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
if (!string.IsNullOrWhiteSpace(rootPath)
&& string.Equals(trimmedPath, rootPath, StringComparison.OrdinalIgnoreCase))
{
return string.Empty;
}
return Path.GetFileName(trimmedPath);
}
private static string SanitizeFileName(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return string.Empty;
}
var invalidFileNameChars = Path.GetInvalidFileNameChars();
var builder = new StringBuilder(value.Length);
foreach (var character in value)
{
builder.Append(Array.IndexOf(invalidFileNameChars, character) >= 0 ? '_' : character);
}
return builder.ToString();
}
private void SetUseFaceGpu(bool value)
{
var currentValue = _ai.UseFaceGpu;
if (!FaceGpuOptionEnabled)
{
return;
}
if (currentValue == value)
{
return;
}
_ai.UseFaceGpu = value;
var previousRecommendedUpsample = GetRecommendedFaceUpsample(currentValue);
if (_ai.FaceUpsample == previousRecommendedUpsample)
{
_ai.FaceUpsample = GetRecommendedFaceUpsample(value);
}
}
private void TrackFaceEncoderProcess(Process process)
{
lock (_faceEncoderProcessLock)
{
_faceEncoderProcess = process;
}
}
private void TrackFaceMatcherProcess(Process process)
{
lock (_faceMatcherProcessLock)
{
_faceMatcherProcess = process;
}
}
private void ClearTrackedFaceEncoderProcess()
{
lock (_faceEncoderProcessLock)
{
if (_faceEncoderProcess is not null && _faceEncoderProcess.HasExited)
{
_faceEncoderProcess = null;
return;
}
_faceEncoderProcess = null;
}
}
private void ClearTrackedFaceMatcherProcess()
{
lock (_faceMatcherProcessLock)
{
if (_faceMatcherProcess is not null && _faceMatcherProcess.HasExited)
{
_faceMatcherProcess = null;
return;
}
_faceMatcherProcess = null;
}
}
private Process? GetTrackedFaceEncoderProcess()
{
lock (_faceEncoderProcessLock)
{
if (_faceEncoderProcess is null)
{
return null;
}
if (_faceEncoderProcess.HasExited)
{
_faceEncoderProcess = null;
return null;
}
return _faceEncoderProcess;
}
}
private Process? GetTrackedFaceMatcherProcess()
{
lock (_faceMatcherProcessLock)
{
if (_faceMatcherProcess is null)
{
return null;
}
if (_faceMatcherProcess.HasExited)
{
_faceMatcherProcess = null;
return null;
}
return _faceMatcherProcess;
}
}
private Process? FindConfiguredFaceEncoderProcess()
{
var configuredExecutablePath = ResolveConfiguredFaceEncoderExecutablePath(FaceExecutablePath, UseFaceGpu);
if (string.IsNullOrWhiteSpace(configuredExecutablePath))
{
return null;
}
var processName = Path.GetFileNameWithoutExtension(configuredExecutablePath);
foreach (var process in Process.GetProcessesByName(processName))
{
if (!IsProcessAlive(process))
{
process.Dispose();
continue;
}
if (IsMatchingProcessPath(process, configuredExecutablePath))
{
return process;
}
process.Dispose();
}
return null;
}
private Process? FindConfiguredFaceMatcherProcess()
{
var configuredExecutablePath = ResolveConfiguredFaceMatcherExecutablePath(FaceMatcherExecutablePath, FaceExecutablePath);
if (string.IsNullOrWhiteSpace(configuredExecutablePath))
{
return null;
}
var processName = Path.GetFileNameWithoutExtension(configuredExecutablePath);
foreach (var process in Process.GetProcessesByName(processName))
{
if (!IsProcessAlive(process))
{
process.Dispose();
continue;
}
if (IsMatchingProcessPath(process, configuredExecutablePath))
{
return process;
}
process.Dispose();
}
return null;
}
private bool ComputeIsFaceEncoderRunning()
{
var trackedProcess = GetTrackedFaceEncoderProcess();
if (trackedProcess is not null)
{
return true;
}
if (!_hasStartedFaceEncoderInSession)
{
return false;
}
using var discoveredProcess = FindConfiguredFaceEncoderProcess();
return discoveredProcess is not null;
}
private bool ComputeIsFaceMatcherRunning()
{
var trackedProcess = GetTrackedFaceMatcherProcess();
if (trackedProcess is not null)
{
return true;
}
if (!_hasStartedFaceMatcherInSession)
{
return false;
}
using var discoveredProcess = FindConfiguredFaceMatcherProcess();
return discoveredProcess is not null;
}
private static bool IsProcessAlive(Process process)
{
try
{
return !process.HasExited;
}
catch
{
return false;
}
}
private static bool IsMatchingProcessPath(Process process, string configuredExecutablePath)
{
try
{
var processPath = process.MainModule?.FileName;
return !string.IsNullOrWhiteSpace(processPath)
&& string.Equals(Path.GetFullPath(processPath), Path.GetFullPath(configuredExecutablePath), StringComparison.OrdinalIgnoreCase);
}
catch
{
return false;
}
}
private bool TryRequestFaceEncoderStop(Process process)
{
if (!IsProcessAlive(process))
{
return true;
}
#if WINDOWS
if (Program.TrySendConsoleInterrupt(process.Id))
{
return true;
}
#endif
try
{
return process.CloseMainWindow();
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Unable to request graceful stop for face encoder process {ProcessId}", process.Id);
return false;
}
}
private bool TryRequestFaceMatcherStop(Process process)
{
if (!IsProcessAlive(process))
{
return true;
}
#if WINDOWS
if (Program.TrySendConsoleInterrupt(process.Id))
{
return true;
}
#endif
try
{
return process.CloseMainWindow();
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Unable to request graceful stop for face matcher process {ProcessId}", process.Id);
return false;
}
}
private static async Task<bool> WaitForProcessExitAsync(Process process, TimeSpan timeout)
{
if (!IsProcessAlive(process))
{
return true;
}
using var cancellationTokenSource = new CancellationTokenSource(timeout);
try
{
await process.WaitForExitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
return true;
}
catch (OperationCanceledException)
{
return !IsProcessAlive(process);
}
}
private void AppendFaceProcessOutput(StringBuilder builder, StringBuilder transcriptBuilder, string? line, bool isError)
{
if (string.IsNullOrWhiteSpace(line))
{
return;
}
lock (builder)
{
builder.AppendLine(line);
}
if (isError)
{
Console.Error.WriteLine(line);
}
else
{
Console.WriteLine(line);
}
string transcript;
lock (transcriptBuilder)
{
if (isError)
{
transcriptBuilder.Append("[stderr] ");
}
transcriptBuilder.AppendLine(line);
transcript = transcriptBuilder.ToString();
}
_ = InvokeOnUiThreadAsync(() => FaceCommandOutput = transcript);
}
private void AppendFaceMatcherProcessOutput(StringBuilder builder, StringBuilder transcriptBuilder, string? line, bool isError)
{
if (string.IsNullOrWhiteSpace(line))
{
return;
}
lock (builder)
{
builder.AppendLine(line);
}
if (isError)
{
Console.Error.WriteLine(line);
}
else
{
Console.WriteLine(line);
}
string transcript;
lock (transcriptBuilder)
{
if (isError)
{
transcriptBuilder.Append("[stderr] ");
}
transcriptBuilder.AppendLine(line);
transcript = transcriptBuilder.ToString();
}
_ = InvokeOnUiThreadAsync(() => FaceMatcherCommandOutput = transcript);
}
internal static string? ResolveConfiguredFaceMatcherExecutablePath(string configuredPath, string fallbackEncoderPath)
{
foreach (var candidate in EnumerateFaceMatcherExecutableCandidates(configuredPath, fallbackEncoderPath))
{
if (File.Exists(candidate))
{
return candidate;
}
}
return null;
}
internal static string? ResolveConfiguredFaceMatcherEncodingsPath(string configuredPath, string fallbackOutputFolderPath)
{
var normalizedPath = NormalizeFilePathArgument(configuredPath);
if (File.Exists(normalizedPath))
{
return normalizedPath;
}
if (Directory.Exists(normalizedPath))
{
var fromDirectory = new DirectoryInfo(normalizedPath)
.EnumerateFiles("*.pkl", SearchOption.TopDirectoryOnly)
.OrderByDescending(file => file.LastWriteTimeUtc)
.FirstOrDefault();
if (fromDirectory is not null)
{
return fromDirectory.FullName;
}
}
var fallbackOutputFolder = NormalizeDirectoryPathArgument(fallbackOutputFolderPath);
if (Directory.Exists(fallbackOutputFolder))
{
var latest = new DirectoryInfo(fallbackOutputFolder)
.EnumerateFiles("*.pkl", SearchOption.TopDirectoryOnly)
.OrderByDescending(file => file.LastWriteTimeUtc)
.FirstOrDefault();
return latest?.FullName;
}
return null;
}
internal static string? ResolveConfiguredFaceEncoderExecutablePath(string configuredPath, bool useGpu)
{
var variant = useGpu ? "gpu" : "cpu";
foreach (var candidate in EnumerateFaceEncoderExecutableCandidates(configuredPath, variant))
{
if (File.Exists(candidate))
{
return candidate;
}
}
return null;
}
internal static (string OutputFilePath, string LogFilePath) BuildFaceEncoderOutputPaths(string outputFolderPath, string imagesFolderPath, DateTime timestamp)
{
var safeRaceName = BuildSafeFaceEncoderRaceName(imagesFolderPath);
var timestampToken = timestamp.ToString("yyyyMMdd_HHmmss");
return (
Path.Combine(outputFolderPath, $"face_encodings_{timestampToken}_{safeRaceName}.pkl"),
Path.Combine(outputFolderPath, $"encoder_log_{timestampToken}_{safeRaceName}.txt"));
}
internal static string BuildSafeFaceEncoderRaceName(string imagesFolderPath)
{
var raceName = new DirectoryInfo(imagesFolderPath).Name;
if (string.IsNullOrWhiteSpace(raceName))
{
return "race";
}
var invalidChars = Path.GetInvalidFileNameChars();
var builder = new StringBuilder(raceName.Length);
var previousWasSeparator = false;
foreach (var currentChar in raceName)
{
if (char.IsWhiteSpace(currentChar) || invalidChars.Contains(currentChar))
{
if (!previousWasSeparator)
{
builder.Append('_');
previousWasSeparator = true;
}
continue;
}
builder.Append(currentChar);
previousWasSeparator = false;
}
return builder.ToString().Trim('_') switch
{
"" => "race",
var sanitized => sanitized
};
}
internal static (string CsvPath, string LogPath) ResolveFaceMatcherOutputPaths(string configuredCsvPath, string configuredLogPath, string fallbackRootPath, string imagePath, DateTime timestamp)
{
var baseName = BuildSafeFaceMatcherImageName(imagePath);
var timestampToken = timestamp.ToString("yyyyMMdd_HHmmss");
var csvPath = ResolveFaceMatcherOutputFilePath(configuredCsvPath, fallbackRootPath, $"result_{timestampToken}_{baseName}.csv");
var logPath = ResolveFaceMatcherOutputFilePath(configuredLogPath, fallbackRootPath, $"matcher_log_{timestampToken}_{baseName}.txt");
return (csvPath, logPath);
}
internal static string BuildSafeFaceMatcherImageName(string imagePath)
{
var fileName = Path.GetFileNameWithoutExtension(imagePath);
if (string.IsNullOrWhiteSpace(fileName))
{
return "image";
}
var invalidChars = Path.GetInvalidFileNameChars();
var builder = new StringBuilder(fileName.Length);
foreach (var currentChar in fileName)
{
builder.Append(invalidChars.Contains(currentChar) || char.IsWhiteSpace(currentChar) ? '_' : currentChar);
}
return builder.ToString().Trim('_') switch
{
"" => "image",
var sanitized => sanitized
};
}
private static IEnumerable<string> EnumerateFaceEncoderExecutableCandidates(string configuredPath, string variant)
{
var normalizedPath = NormalizeFilePathArgument(configuredPath);
if (string.IsNullOrWhiteSpace(normalizedPath))
{
yield break;
}
var executableName = $"face_encoder_{variant}.exe";
if (File.Exists(normalizedPath))
{
var fileDirectory = Path.GetDirectoryName(normalizedPath);
if (!string.IsNullOrWhiteSpace(fileDirectory))
{
yield return Path.Combine(fileDirectory, executableName);
var parentDirectory = Directory.GetParent(fileDirectory)?.FullName;
if (!string.IsNullOrWhiteSpace(parentDirectory))
{
yield return Path.Combine(parentDirectory, $"face_encoder_{variant}", executableName);
}
}
yield return normalizedPath;
yield break;
}
yield return Path.Combine(normalizedPath, executableName);
yield return Path.Combine(normalizedPath, $"face_encoder_{variant}", executableName);
var leafName = Path.GetFileName(normalizedPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
if (leafName.Equals("face_encoder_cpu", StringComparison.OrdinalIgnoreCase)
|| leafName.Equals("face_encoder_gpu", StringComparison.OrdinalIgnoreCase))
{
var parentDirectory = Directory.GetParent(normalizedPath)?.FullName;
if (!string.IsNullOrWhiteSpace(parentDirectory))
{
yield return Path.Combine(parentDirectory, $"face_encoder_{variant}", executableName);
}
}
}
private static IEnumerable<string> EnumerateFaceMatcherExecutableCandidates(string configuredPath, string fallbackEncoderPath)
{
var normalizedPath = NormalizeFilePathArgument(configuredPath);
if (!string.IsNullOrWhiteSpace(normalizedPath))
{
if (File.Exists(normalizedPath))
{
yield return normalizedPath;
}
yield return Path.Combine(normalizedPath, "face_matcher.exe");
}
var fallbackPath = NormalizeFilePathArgument(fallbackEncoderPath);
if (!string.IsNullOrWhiteSpace(fallbackPath))
{
if (File.Exists(fallbackPath))
{
var fileDirectory = Path.GetDirectoryName(fallbackPath);
if (!string.IsNullOrWhiteSpace(fileDirectory))
{
yield return Path.Combine(fileDirectory, "face_matcher.exe");
}
}
yield return Path.Combine(fallbackPath, "face_matcher.exe");
}
}
private static int NormalizeFaceParallelism(int value)
{
return value is >= 1 and <= 5 ? value : 3;
}
private static double NormalizeFaceMatcherTolerance(double value)
{
if (double.IsNaN(value) || double.IsInfinity(value))
{
return 0.5;
}
return Math.Clamp(Math.Round(value, 2), 0.35, 0.75);
}
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;
}
private static bool GetRecommendedFaceUpsample(bool useGpu)
{
return !useGpu;
}
private static string BuildFaceEncoderSummary(
int exitCode,
ProcessStartInfo processStartInfo,
string outputFilePath,
string logFilePath,
StringBuilder outputLines,
StringBuilder errorLines)
{
var summary = new StringBuilder();
summary.AppendLine($"Exit code: {exitCode}");
summary.AppendLine($"Command: {processStartInfo.FileName} {string.Join(" ", processStartInfo.ArgumentList)}");
summary.AppendLine($"Output file: {outputFilePath}");
summary.AppendLine($"Log file: {logFilePath}");
lock (outputLines)
{
if (outputLines.Length > 0)
{
summary.AppendLine();
summary.AppendLine("STDOUT:");
summary.Append(outputLines);
}
}
lock (errorLines)
{
if (errorLines.Length > 0)
{
summary.AppendLine();
summary.AppendLine("STDERR:");
summary.Append(errorLines);
}
}
return summary.ToString();
}
private static string BuildFaceMatcherSummary(
int exitCode,
ProcessStartInfo processStartInfo,
string csvPath,
string logFilePath,
StringBuilder outputLines,
StringBuilder errorLines,
int matchCount)
{
var summary = new StringBuilder();
summary.AppendLine($"Exit code: {exitCode}");
summary.AppendLine($"Command: {processStartInfo.FileName} {string.Join(" ", processStartInfo.ArgumentList)}");
summary.AppendLine($"Result CSV: {csvPath}");
summary.AppendLine($"Log file: {logFilePath}");
summary.AppendLine($"Match count: {matchCount}");
lock (outputLines)
{
if (outputLines.Length > 0)
{
summary.AppendLine();
summary.AppendLine("STDOUT:");
summary.Append(outputLines);
}
}
lock (errorLines)
{
if (errorLines.Length > 0)
{
summary.AppendLine();
summary.AppendLine("STDERR:");
summary.Append(errorLines);
}
}
return summary.ToString();
}
private static string ResolveFaceMatcherFallbackOutputRoot(string configuredCsvPath, string configuredLogPath, string faceOutputFolderPath, string executablePath)
{
foreach (var candidate in new[] { NormalizeFilePathArgument(configuredCsvPath), NormalizeFilePathArgument(configuredLogPath), NormalizeDirectoryPathArgument(faceOutputFolderPath) })
{
if (string.IsNullOrWhiteSpace(candidate))
{
continue;
}
if (Directory.Exists(candidate))
{
return candidate;
}
var directory = Path.GetDirectoryName(candidate);
if (!string.IsNullOrWhiteSpace(directory))
{
return directory;
}
}
return Path.Combine(Path.GetDirectoryName(executablePath) ?? Environment.CurrentDirectory, "output");
}
private static string ResolveFaceMatcherOutputFilePath(string configuredPath, string fallbackRootPath, string defaultFileName)
{
var normalized = NormalizeFilePathArgument(configuredPath);
if (string.IsNullOrWhiteSpace(normalized))
{
return Path.Combine(fallbackRootPath, defaultFileName);
}
if (Directory.Exists(normalized) || string.IsNullOrWhiteSpace(Path.GetExtension(normalized)))
{
return Path.Combine(normalized, defaultFileName);
}
return normalized;
}
private async Task<bool> LogIndicatesNoFacesAsync(string logPath)
{
try
{
if (!File.Exists(logPath))
{
return false;
}
var content = await File.ReadAllTextAsync(logPath).ConfigureAwait(false);
return content.Contains("nessun volt", StringComparison.OrdinalIgnoreCase)
|| content.Contains("no face", StringComparison.OrdinalIgnoreCase)
|| content.Contains("0 faces", StringComparison.OrdinalIgnoreCase);
}
catch
{
return false;
}
}
private static async Task<List<ParsedFaceMatcherRow>> ParseFaceMatcherCsvAsync(string csvPath, string logPath)
{
var parsedRows = new List<ParsedFaceMatcherRow>();
if (!File.Exists(csvPath))
{
return parsedRows;
}
var logScores = await ParseFaceMatcherScoresFromLogAsync(logPath).ConfigureAwait(false);
var lines = await File.ReadAllLinesAsync(csvPath).ConfigureAwait(false);
var meaningfulLines = lines.Where(line => !string.IsNullOrWhiteSpace(line)).ToArray();
if (meaningfulLines.Length == 0)
{
return parsedRows;
}
string[]? headers = null;
var firstCells = ParseCsvLine(meaningfulLines[0]);
var hasHeader = firstCells.Any(cell => cell.Contains("file", StringComparison.OrdinalIgnoreCase)
|| cell.Contains("image", StringComparison.OrdinalIgnoreCase)
|| cell.Contains("score", StringComparison.OrdinalIgnoreCase)
|| cell.Contains("distance", StringComparison.OrdinalIgnoreCase)
|| cell.Contains("confidence", StringComparison.OrdinalIgnoreCase));
var startIndex = 0;
if (hasHeader)
{
headers = firstCells;
startIndex = 1;
}
for (var index = startIndex; index < meaningfulLines.Length; index++)
{
var rawLine = meaningfulLines[index].Trim();
var cells = ParseCsvLine(rawLine);
if (cells.Length == 0 || string.IsNullOrWhiteSpace(cells[0]))
{
continue;
}
var photoId = Path.GetFileName(cells[0]);
double? score = null;
for (var cellIndex = 1; cellIndex < cells.Length; cellIndex++)
{
if (double.TryParse(cells[cellIndex], NumberStyles.Float, CultureInfo.InvariantCulture, out var parsedScore))
{
score = parsedScore;
break;
}
}
if (!score.HasValue && logScores.TryGetValue(photoId, out var logScore))
{
score = logScore;
}
var debugParts = new List<string>();
for (var cellIndex = 1; cellIndex < cells.Length; cellIndex++)
{
var cellValue = cells[cellIndex];
if (string.IsNullOrWhiteSpace(cellValue))
{
continue;
}
var header = headers is not null && cellIndex < headers.Length
? headers[cellIndex]
: $"col{cellIndex + 1}";
debugParts.Add($"{header}: {cellValue}");
}
if (score.HasValue)
{
debugParts.Add($"score: {score.Value.ToString("0.###", CultureInfo.InvariantCulture)}");
}
parsedRows.Add(new ParsedFaceMatcherRow(photoId, score, rawLine, string.Join(" | ", debugParts)));
}
return parsedRows;
}
private static async Task<Dictionary<string, double>> ParseFaceMatcherScoresFromLogAsync(string logPath)
{
var scores = new Dictionary<string, double>(StringComparer.OrdinalIgnoreCase);
if (!File.Exists(logPath))
{
return scores;
}
var lines = await File.ReadAllLinesAsync(logPath).ConfigureAwait(false);
foreach (var line in lines)
{
if (string.IsNullOrWhiteSpace(line))
{
continue;
}
var match = Regex.Match(
line,
@"in\s+(?<path>.+?)\s+-\s+\[Somiglianza:\s*(?<score>[0-9]+(?:[\.,][0-9]+)?)%\]",
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
if (!match.Success)
{
continue;
}
var pathValue = match.Groups["path"].Value.Trim();
var photoId = Path.GetFileName(pathValue);
if (string.IsNullOrWhiteSpace(photoId))
{
continue;
}
var scoreText = match.Groups["score"].Value.Replace(',', '.');
if (!double.TryParse(scoreText, NumberStyles.Float, CultureInfo.InvariantCulture, out var score))
{
continue;
}
if (!scores.TryGetValue(photoId, out var existingScore) || score > existingScore)
{
scores[photoId] = score;
}
}
return scores;
}
private static string[] ParseCsvLine(string line)
{
var values = new List<string>();
var current = new StringBuilder();
var insideQuotes = false;
foreach (var currentChar in line)
{
if (currentChar == '"')
{
insideQuotes = !insideQuotes;
continue;
}
if (currentChar == ',' && !insideQuotes)
{
values.Add(current.ToString().Trim());
current.Clear();
continue;
}
current.Append(currentChar);
}
values.Add(current.ToString().Trim());
return values.ToArray();
}
private static Dictionary<string, List<string>> ResolveDestinationImagesByFileName(string destinationRoot, IEnumerable<string> fileNames)
{
var result = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
var normalizedRoot = NormalizeDirectoryPathArgument(destinationRoot);
if (string.IsNullOrWhiteSpace(normalizedRoot) || !Directory.Exists(normalizedRoot))
{
return result;
}
var requestedNames = new HashSet<string>(fileNames.Where(name => !string.IsNullOrWhiteSpace(name)).Select(name => Path.GetFileName(name)), StringComparer.OrdinalIgnoreCase);
if (requestedNames.Count == 0)
{
return result;
}
foreach (var path in Directory.EnumerateFiles(normalizedRoot, "*", SearchOption.AllDirectories))
{
var fileName = Path.GetFileName(path);
if (!requestedNames.Contains(fileName))
{
continue;
}
if (!result.TryGetValue(fileName, out var list))
{
list = new List<string>();
result[fileName] = list;
}
list.Add(path);
}
return result;
}
private static string NormalizeDirectoryPathArgument(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return string.Empty;
}
var normalized = value.Trim().Trim('"');
var root = Path.GetPathRoot(normalized);
if (!string.IsNullOrEmpty(root) && normalized.Length > root.Length)
{
normalized = normalized.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
}
return normalized;
}
private static string NormalizeFilePathArgument(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return string.Empty;
}
return value.Trim().Trim('"');
}
2026-02-04 19:48:03 +01:00
// Note: These commands will trigger events that the View will handle to show dialogs
// since dialogs require UI context
public event EventHandler? SelectSourceFolderRequested;
public event EventHandler? SelectDestinationFolderRequested;
public event EventHandler? SelectLogoFileRequested;
public event EventHandler? SelectModelsFolderRequested;
public event EventHandler? SelectCsvOutputRequested;
public event EventHandler<string?>? SaveSettingsRequested;
public event EventHandler<string?>? LoadSettingsRequested;
public event EventHandler? SelectColorRequested;
// Request that the View shows a message to the user (message, caption, icon)
public event EventHandler<Tuple<string, string, int>>? ShowMessageRequested;
public event EventHandler? SelectTransparentColorRequested;
2026-02-04 19:48:03 +01:00
private void SelectSourceFolder(object parameter)
{
SelectSourceFolderRequested?.Invoke(this, EventArgs.Empty);
}
private void SelectDestinationFolder(object parameter)
{
SelectDestinationFolderRequested?.Invoke(this, EventArgs.Empty);
}
private void SelectLogoFile(object parameter)
{
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);
}
2026-02-04 19:48:03 +01:00
private void SaveSettings(object parameter)
{
SaveSettingsRequested?.Invoke(this, string.Empty);
2026-02-04 19:48:03 +01:00
}
private void LoadSettings(object parameter)
{
LoadSettingsRequested?.Invoke(this, string.Empty);
2026-02-04 19:48:03 +01:00
}
private void SelectColor(object parameter)
{
SelectColorRequested?.Invoke(this, EventArgs.Empty);
}
private void SelectTransparentColor(object parameter)
{
SelectTransparentColorRequested?.Invoke(this, EventArgs.Empty);
}
2026-02-04 19:48:03 +01:00
public async Task SaveSettingsToFileAsync(string filePath)
{
await _settingsService.SaveSettingsAsync(filePath, this);
}
public async Task LoadSettingsFromFileAsync(string filePath)
{
await _settingsService.LoadSettingsAsync(filePath, this);
}
private string _appVersion = string.Empty;
public string AppVersion
{
get => _appVersion;
set
{
_appVersion = value;
NotifyPropertyChanged();
}
}
2024-10-14 22:55:52 +02:00
}
}