2024-10-14 23:25:35 +02:00
using ImageCatalog_2.Commands ;
2026-03-12 18:48:13 +01:00
using ImageCatalog_2.Models ;
2024-10-14 23:25:35 +02:00
using ImageCatalog_2.Services ;
2026-03-12 18:48:13 +01:00
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 ;
2026-05-09 12:09:05 +02:00
using System.IO ;
2024-10-14 22:55:52 +02:00
using System.Linq ;
using System.Text ;
2026-05-09 20:27:44 +02:00
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 ;
2026-05-09 17:53:15 +02:00
using AIFotoONLUS.Core ;
2026-05-09 20:27:44 +02:00
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 ;
2026-05-24 18:45:51 +02:00
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 ; }
2026-02-15 11:13:23 +01:00
public ICommand SelectTransparentColorCommand { get ; }
2026-02-16 18:32:04 +01:00
public ICommand SelectModelsFolderCommand { get ; }
public ICommand SelectCsvOutputCommand { get ; }
2026-03-12 18:48:13 +01:00
public ICommand StartAiCommand { get ; }
2026-05-09 12:09:05 +02:00
public ICommand StartFaceEncoderCommand { get ; }
public ICommand StopFaceEncoderCommand { get ; }
2026-06-06 11:54:21 +02:00
public ICommand UploadFaceEncoderOutputCommand { get ; }
2026-05-09 20:27:44 +02:00
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 ;
2026-03-12 18:48:13 +01:00
private readonly IAiExtractionService _aiExtractionService ;
private readonly IImageProcessingCoordinator _imageProcessingCoordinator ;
2026-06-06 11:54:21 +02:00
private readonly PickerPreferenceService ? _pickerPreferenceService ;
2026-03-12 18:48:13 +01:00
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 ;
2026-05-09 12:09:05 +02:00
private readonly AsyncCommand _startFaceEncoderCommand ;
private readonly AsyncCommand _stopFaceEncoderCommand ;
2026-06-06 11:54:21 +02:00
private readonly AsyncCommand _uploadFaceEncoderOutputCommand ;
2026-05-09 20:27:44 +02:00
private readonly AsyncCommand _startFaceMatcherCommand ;
private readonly AsyncCommand _stopFaceMatcherCommand ;
2026-05-09 12:09:05 +02:00
private readonly object _faceEncoderProcessLock = new ( ) ;
2026-05-09 20:27:44 +02:00
private readonly object _faceMatcherProcessLock = new ( ) ;
2026-05-09 12:09:05 +02:00
private Process ? _faceEncoderProcess ;
2026-05-09 20:27:44 +02:00
private Process ? _faceMatcherProcess ;
2026-05-09 12:09:05 +02:00
private CancellationTokenSource ? _faceEncoderWatcherTokenSource ;
private Task ? _faceEncoderWatcherTask ;
2026-05-09 15:46:41 +02:00
private CancellationTokenSource ? _faceEncoderLogWatcherTokenSource ;
private Task ? _faceEncoderLogWatcherTask ;
2026-05-09 20:27:44 +02:00
private CancellationTokenSource ? _faceMatcherWatcherTokenSource ;
private Task ? _faceMatcherWatcherTask ;
private CancellationTokenSource ? _faceMatcherLogWatcherTokenSource ;
private Task ? _faceMatcherLogWatcherTask ;
2026-05-09 12:09:05 +02:00
private bool _hasStartedFaceEncoderInSession ;
2026-05-09 20:27:44 +02:00
private bool _hasStartedFaceMatcherInSession ;
2026-06-06 11:54:21 +02:00
private bool _isLoadingFaceSshUserPreferences ;
private string _lastFaceEncoderOutputFilePath = string . Empty ;
2026-05-24 17:29:05 +02:00
private int _numberAiGpuRefreshVersion ;
private volatile bool _numberAiGpuValidationPending ;
2026-05-09 20:27:44 +02:00
private sealed record ParsedFaceMatcherRow ( string PhotoId , double? Score , string RawRow , string DebugSummary ) ;
2026-02-04 19:48:03 +01:00
2026-05-24 18:45:51 +02:00
private const string AiCsvOverwriteDialogTitle = "File CSV gia esistente" ;
2026-06-06 11:54:21 +02:00
private static readonly Regex FaceUploadPathRegex = new ( @"^\d{4}/(?:0[1-9]|1[0-2])\.[^/\\]+/[^/\\]+$" , RegexOptions . Compiled | RegexOptions . CultureInvariant ) ;
2026-05-24 18:45:51 +02:00
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
2026-03-12 18:48:13 +01:00
[CLSCompliant(false)]
2026-02-28 21:34:45 +01:00
public DataModel ( ITestService testService , ISettingsService settingsService ,
2026-03-12 18:48:13 +01:00
ImageCreationService imageCreationService , IAiExtractionService aiExtractionService , IImageProcessingCoordinator imageProcessingCoordinator , PicSettings picSettings ,
2026-06-06 11:54:21 +02:00
IMapper mapper , ILogger < DataModel > logger , MaddoShared . IVersionProvider ? versionProvider = null , PickerPreferenceService ? pickerPreferenceService = 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 ;
2026-03-12 18:48:13 +01:00
_aiExtractionService = aiExtractionService ;
_imageProcessingCoordinator = imageProcessingCoordinator ;
2026-06-06 11:54:21 +02:00
_pickerPreferenceService = pickerPreferenceService ;
2026-03-12 18:48:13 +01:00
_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 ;
2026-02-14 22:18:56 +01:00
// 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 ) ;
2026-02-16 18:32:04 +01:00
SelectModelsFolderCommand = new RelayCommand ( SelectModelsFolder ) ;
SelectCsvOutputCommand = new RelayCommand ( SelectCsvOutput ) ;
2026-03-12 18:48:13 +01:00
StartAiCommand = new AsyncCommand ( StartAiAsync ) ;
2026-05-09 12:09:05 +02:00
_startFaceEncoderCommand = new AsyncCommand ( RunFaceEncoderAsync , CanRunFaceEncoder ) ;
_stopFaceEncoderCommand = new AsyncCommand ( ( ) = > StopFaceEncoderAsync ( "Arresto richiesto dall'utente." ) , CanStopFaceEncoder ) ;
2026-06-06 11:54:21 +02:00
_uploadFaceEncoderOutputCommand = new AsyncCommand ( UploadFaceEncoderOutputAsync , CanUploadFaceEncoderOutput ) ;
2026-05-09 20:27:44 +02:00
_startFaceMatcherCommand = new AsyncCommand ( RunFaceMatcherAsync , CanRunFaceMatcher ) ;
_stopFaceMatcherCommand = new AsyncCommand ( ( ) = > StopFaceMatcherAsync ( "Arresto richiesto dall'utente." ) , CanStopFaceMatcher ) ;
2026-05-09 12:09:05 +02:00
StartFaceEncoderCommand = _startFaceEncoderCommand ;
StopFaceEncoderCommand = _stopFaceEncoderCommand ;
2026-06-06 11:54:21 +02:00
UploadFaceEncoderOutputCommand = _uploadFaceEncoderOutputCommand ;
2026-05-09 20:27:44 +02:00
StartFaceMatcherCommand = _startFaceMatcherCommand ;
StopFaceMatcherCommand = _stopFaceMatcherCommand ;
2026-02-28 21:34:45 +01:00
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 ) ;
2026-02-15 11:13:23 +01:00
SelectTransparentColorCommand = new RelayCommand ( SelectTransparentColor ) ;
2026-02-04 19:48:03 +01:00
// Load available fonts
AvailableFonts = LoadAvailableFonts ( ) ;
2026-06-06 11:54:21 +02:00
LoadFaceSshUserPreferences ( ) ;
2026-05-24 17:29:05 +02:00
QueueRefreshNumberAiGpuCapabilities ( ) ;
2026-05-09 12:09:05 +02:00
RefreshFaceExecutableCapabilities ( ) ;
2026-02-04 19:48:03 +01:00
}
2026-03-12 18:48:13 +01:00
private async Task StartAiAsync ( )
2026-02-16 18:32:04 +01:00
{
2026-05-24 18:45:51 +02:00
if ( ! await ConfirmAiCsvOverwriteIfNeededAsync ( ) . ConfigureAwait ( false ) )
{
await InvokeOnUiThreadAsync ( ( ) = > NumberAiStatsSummary = "OCR annullato." ) . ConfigureAwait ( false ) ;
return ;
}
2026-03-12 18:48:13 +01:00
MainToken = new CancellationTokenSource ( ) ;
try
2026-02-16 18:32:04 +01:00
{
2026-03-12 18:48:13 +01:00
await RunAiExtractionCoreAsync ( MainToken . Token , useDestination : true , recursive : true ) . ConfigureAwait ( false ) ;
2026-02-16 18:32:04 +01:00
}
2026-03-12 18:48:13 +01:00
catch ( OperationCanceledException )
2026-02-16 18:32:04 +01:00
{
2026-05-09 18:54:20 +02:00
await InvokeOnUiThreadAsync ( ( ) = > NumberAiStatsSummary = "OCR annullato." ) . ConfigureAwait ( false ) ;
2026-02-16 18:32:04 +01:00
}
2026-05-09 17:53:15 +02:00
catch ( Exception ex )
{
_logger . LogError ( ex , "AI extraction failed" ) ;
if ( UseNumberAiGpu )
{
2026-05-24 17:29:05 +02:00
QueueRefreshNumberAiGpuCapabilities ( ) ;
2026-05-09 17:53:15 +02:00
}
2026-05-09 18:54:20 +02:00
await InvokeOnUiThreadAsync ( ( ) = > NumberAiStatsSummary = $"Errore OCR: {ex.GetBaseException().Message}" ) . ConfigureAwait ( false ) ;
2026-05-09 17:53:15 +02:00
await ShowErrorMessageAsync ( "Errore AI" , ex . GetBaseException ( ) . Message ) . ConfigureAwait ( false ) ;
}
2026-03-12 18:48:13 +01:00
finally
2026-02-16 18:32:04 +01:00
{
2026-03-12 18:48:13 +01:00
MainToken = null ;
2026-02-16 18:32:04 +01:00
}
2026-03-12 18:48:13 +01:00
}
2026-02-16 18:32:04 +01:00
2026-05-09 17:27:05 +02:00
private async Task RunAiExtractionCoreAsync ( CancellationToken token , bool useDestination = false , bool recursive = false , bool failOnInvalidPath = false )
2026-03-12 18:48:13 +01:00
{
var searchRoot = useDestination ? DestinationPath : SourcePath ;
if ( string . IsNullOrWhiteSpace ( searchRoot ) | | ! System . IO . Directory . Exists ( searchRoot ) )
2026-02-16 18:32:04 +01:00
{
2026-03-12 18:48:13 +01:00
_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}" ) ;
}
2026-03-12 18:48:13 +01:00
return ;
2026-02-16 18:32:04 +01:00
}
2026-03-12 18:48:13 +01:00
await InvokeOnUiThreadAsync ( ( ) = >
2026-02-16 18:32:04 +01:00
{
2026-03-12 18:48:13 +01:00
PreviewResults . Clear ( ) ;
AiProgress = 0 ;
2026-05-09 18:54:20 +02:00
NumberAiStatsSummary = BuildNumberAiIdleSummary ( ) ;
2026-03-12 18:48:13 +01:00
} ) . ConfigureAwait ( false ) ;
2026-02-16 18:32:04 +01:00
2026-05-09 18:54:20 +02:00
var summary = await _aiExtractionService . RunAsync (
2026-03-12 18:48:13 +01:00
new AiExtractionRequest
2026-02-16 18:32:04 +01:00
{
2026-03-12 18:48:13 +01:00
SearchRoot = searchRoot ,
Recursive = recursive ,
2026-05-09 17:53:15 +02:00
IncludeThumbnails = IncludeNumberAiThumbnails ,
2026-05-09 17:27:05 +02:00
ModelsFolderPath = ModelsFolderPath ,
UseGpu = UseNumberAiGpu ,
2026-05-09 18:54:20 +02:00
WorkloadLevel = NumberAiWorkloadLevel ,
2026-03-12 18:48:13 +01:00
CsvOutputPath = CsvOutputPath
} ,
token ,
2026-05-09 17:53:15 +02:00
result = > InvokeOnUiThreadAsync ( ( ) = >
{
if ( ! string . IsNullOrWhiteSpace ( result . Text ) )
{
PreviewResults . Add ( result ) ;
}
} ) ,
2026-05-09 18:54:20 +02:00
progress = > InvokeOnUiThreadAsync ( ( ) = >
{
AiProgress = progress . PercentComplete ;
NumberAiStatsSummary = BuildNumberAiProgressSummary ( progress ) ;
} ) ) . ConfigureAwait ( false ) ;
await InvokeOnUiThreadAsync ( ( ) = >
{
AiProgress = summary . TotalFiles > 0 ? 100 : 0 ;
NumberAiStatsSummary = BuildNumberAiCompletionSummary ( summary ) ;
} ) . ConfigureAwait ( false ) ;
2026-02-16 18:32:04 +01:00
}
2026-02-26 18:43:07 +01:00
/// <summary>
2026-05-09 14:04:21 +02:00
/// Optional UI-thread invoker set by the active UI layer.
2026-02-26 18:43:07 +01:00
/// </summary>
public Action < Action > ? UiInvoker { get ; set ; }
2026-02-16 18:32:04 +01:00
private Task InvokeOnUiThreadAsync ( Action action )
{
return Task . Run ( ( ) = >
{
2026-02-26 18:43:07 +01:00
if ( UiInvoker ! = null )
UiInvoker ( action ) ;
else
2026-05-09 14:04:21 +02:00
action ( ) ;
2026-02-16 18:32:04 +01:00
} ) ;
}
2026-03-12 18:48:13 +01:00
public AiSettingsViewModel Ai = > _ai ;
public RaceUploadSettingsViewModel RaceUpload = > _raceUpload ;
2026-02-16 18:32:04 +01:00
// AI properties
public bool ExtractNumbers
{
2026-03-12 18:48:13 +01:00
get = > _ai . ExtractNumbers ;
set = > _ai . ExtractNumbers = value ;
2026-02-16 18:32:04 +01:00
}
public string ModelsFolderPath
{
2026-03-12 18:48:13 +01:00
get = > _ai . ModelsFolderPath ;
2026-05-09 17:53:15 +02:00
set
{
_ai . ModelsFolderPath = value ;
2026-05-24 17:29:05 +02:00
QueueRefreshNumberAiGpuCapabilities ( ) ;
2026-05-09 17:53:15 +02:00
}
2026-02-16 18:32:04 +01:00
}
public string CsvOutputPath
{
2026-03-12 18:48:13 +01:00
get = > _ai . CsvOutputPath ;
set = > _ai . CsvOutputPath = value ;
2026-02-16 18:32:04 +01:00
}
2026-05-24 18:45:51 +02:00
public Func < string , string , Task < bool > > ? ConfirmAiCsvOverwriteAsync { get ; set ; }
2026-05-09 17:27:05 +02:00
public bool UseNumberAiGpu
{
get = > _ai . UseNumberAiGpu ;
2026-05-09 17:53:15 +02:00
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
}
2026-05-09 18:54:20 +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 ;
}
2026-02-28 21:34:45 +01:00
public string FaceExecutablePath
{
2026-03-12 18:48:13 +01:00
get = > _ai . FaceExecutablePath ;
2026-05-09 12:09:05 +02:00
set
{
var normalizedValue = value ? ? string . Empty ;
if ( string . Equals ( _ai . FaceExecutablePath , normalizedValue , StringComparison . Ordinal ) )
{
RefreshFaceExecutableCapabilities ( ) ;
return ;
}
_ai . FaceExecutablePath = normalizedValue ;
RefreshFaceExecutableCapabilities ( ) ;
}
2026-02-28 21:34:45 +01:00
}
public string FaceOutputFolderPath
{
2026-03-12 18:48:13 +01:00
get = > _ai . FaceOutputFolderPath ;
set = > _ai . FaceOutputFolderPath = value ;
2026-02-28 21:34:45 +01:00
}
2026-06-06 11:54:21 +02:00
public string FaceUploadPath
{
get = > _ai . FaceUploadPath ;
set
{
_ai . FaceUploadPath = value ? . Trim ( ) ? ? string . Empty ;
NotifyPropertyChanged ( nameof ( IsFaceUploadPathValid ) ) ;
UpdateFaceUploadCommandStates ( ) ;
}
}
public bool FaceUploadDryRun
{
get = > _ai . FaceUploadDryRun ;
set = > SetFaceSshPreferenceBoolValue ( PickerPreferenceKeys . FaceUploadDryRun , normalizedValue = > _ai . FaceUploadDryRun = normalizedValue , value ) ;
}
public bool IsFaceUploadPathValid = > IsValidFaceUploadPath ( FaceUploadPath ) ;
public string FaceSshUsername
{
get = > _ai . FaceSshUsername ;
set = > SetFaceSshPreferenceValue ( PickerPreferenceKeys . FaceSshUsername , normalizedValue = > _ai . FaceSshUsername = normalizedValue , value ? . Trim ( ) ? ? string . Empty ) ;
}
public string FaceSshPassword
{
get = > _ai . FaceSshPassword ;
set = > SetFaceSshPreferenceValue ( PickerPreferenceKeys . FaceSshPassword , normalizedValue = > _ai . FaceSshPassword = normalizedValue , value ? ? string . Empty ) ;
}
public string FaceSshAddress
{
get = > _ai . FaceSshAddress ;
set = > SetFaceSshPreferenceValue ( PickerPreferenceKeys . FaceSshAddress , normalizedValue = > _ai . FaceSshAddress = normalizedValue , value ? . Trim ( ) ? ? string . Empty ) ;
}
public string FaceSshPort
{
get = > _ai . FaceSshPort ;
set = > SetFaceSshPreferenceValue ( PickerPreferenceKeys . FaceSshPort , normalizedValue = > _ai . FaceSshPort = normalizedValue , value ? . Trim ( ) ? ? string . Empty ) ;
}
public string FaceSshPathA
{
get = > _ai . FaceSshPathA ;
set = > SetFaceSshPreferenceValue ( PickerPreferenceKeys . FaceSshPathA , normalizedValue = > _ai . FaceSshPathA = normalizedValue , value ? . Trim ( ) ? ? string . Empty ) ;
}
public string FaceSshPathB
{
get = > _ai . FaceSshPathB ;
set = > SetFaceSshPreferenceValue ( PickerPreferenceKeys . FaceSshPathB , normalizedValue = > _ai . FaceSshPathB = normalizedValue , value ? . Trim ( ) ? ? string . Empty ) ;
}
public bool IsFaceUploadRunning
{
get = > _ai . IsFaceUploadRunning ;
private set = > _ai . IsFaceUploadRunning = value ;
}
2026-05-09 12:09:05 +02:00
public bool FaceRecursive
{
get = > _ai . FaceRecursive ;
set = > _ai . FaceRecursive = value ;
}
2026-05-09 15:46:41 +02:00
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 ;
}
2026-05-09 12:09:05 +02:00
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 ;
}
2026-05-09 20:27:44 +02:00
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 ;
2026-02-28 21:34:45 +01:00
// Race upload settings
public string ApiLogin
{
2026-03-12 18:48:13 +01:00
get = > _raceUpload . ApiLogin ;
set = > _raceUpload . ApiLogin = value ;
2026-02-28 21:34:45 +01:00
}
public string ApiPassword
{
2026-03-12 18:48:13 +01:00
get = > _raceUpload . ApiPassword ;
set = > _raceUpload . ApiPassword = value ;
2026-02-28 21:34:45 +01:00
}
public string ApiRaceDescription
{
2026-03-12 18:48:13 +01:00
get = > _raceUpload . ApiRaceDescription ;
set = > _raceUpload . ApiRaceDescription = value ;
2026-02-28 21:34:45 +01:00
}
public string ApiRaceTypeId
{
2026-03-12 18:48:13 +01:00
get = > _raceUpload . ApiRaceTypeId ;
set = > _raceUpload . ApiRaceTypeId = value ;
2026-02-28 21:34:45 +01:00
}
public DateTime ApiRaceStartDate
{
2026-03-12 18:48:13 +01:00
get = > _raceUpload . ApiRaceStartDate ;
set = > _raceUpload . ApiRaceStartDate = value ;
2026-02-28 21:34:45 +01:00
}
public DateTime ApiRaceEndDate
{
2026-03-12 18:48:13 +01:00
get = > _raceUpload . ApiRaceEndDate ;
set = > _raceUpload . ApiRaceEndDate = value ;
2026-02-28 21:34:45 +01:00
}
public string ApiPathBase
{
2026-03-12 18:48:13 +01:00
get = > _raceUpload . ApiPathBase ;
set = > _raceUpload . ApiPathBase = value ;
2026-02-28 21:34:45 +01:00
}
public string ApiLocalita
{
2026-03-12 18:48:13 +01:00
get = > _raceUpload . ApiLocalita ;
set = > _raceUpload . ApiLocalita = value ;
2026-02-28 21:34:45 +01:00
}
public int ApiEventoInLineaIndex
{
2026-03-12 18:48:13 +01:00
get = > _raceUpload . ApiEventoInLineaIndex ;
set = > _raceUpload . ApiEventoInLineaIndex = value ;
2026-02-28 21:34:45 +01:00
}
public int ApiTipoIndexValue
{
2026-03-12 18:48:13 +01:00
get = > _raceUpload . ApiTipoIndexValue ;
set = > _raceUpload . ApiTipoIndexValue = value ;
2026-02-28 21:34:45 +01:00
}
public int ApiFreeEventIndex
{
2026-03-12 18:48:13 +01:00
get = > _raceUpload . ApiFreeEventIndex ;
set = > _raceUpload . ApiFreeEventIndex = value ;
2026-02-28 21:34:45 +01:00
}
public string ApiRaceId
{
2026-03-12 18:48:13 +01:00
get = > _raceUpload . ApiRaceId ;
set = > _raceUpload . ApiRaceId = value ;
2026-02-28 21:34:45 +01:00
}
public string ApiRemoteProcessedBasePath
{
2026-03-12 18:48:13 +01:00
get = > _raceUpload . ApiRemoteProcessedBasePath ;
set = > _raceUpload . ApiRemoteProcessedBasePath = value ;
2026-02-28 21:34:45 +01:00
}
2026-02-16 18:32:04 +01:00
// Preview results for DataGrid
2026-03-12 18:48:13 +01:00
public System . Collections . ObjectModel . ObservableCollection < AiResultItem > PreviewResults = > _ai . PreviewResults ;
2026-02-16 18:32:04 +01:00
2026-03-12 18:48:13 +01:00
public double AiProgress
2026-02-16 18:32:04 +01:00
{
2026-03-12 18:48:13 +01:00
get = > _ai . AiProgress ;
set = > _ai . AiProgress = value ;
2026-02-16 18:32:04 +01:00
}
2026-05-09 18:54:20 +02:00
private string BuildNumberAiIdleSummary ( )
{
var workerCount = ResolveNumberAiWorkerCount ( UseNumberAiGpu , NumberAiWorkloadLevel ) ;
2026-05-09 19:31:21 +02:00
var unit = UseNumberAiGpu ? "batch" : "worker" ;
return $"In attesa. Carico {NumberAiWorkloadLevel}/5, {workerCount} {unit}, 0.00 img/s." ;
2026-05-09 18:54:20 +02:00
}
private static string BuildNumberAiProgressSummary ( AiExtractionProgressUpdate progress )
{
2026-05-09 19:31:21 +02:00
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}." ;
2026-05-09 18:54:20 +02:00
}
private static string BuildNumberAiCompletionSummary ( AiExtractionRunSummary summary )
{
if ( summary . TotalFiles = = 0 )
{
return "Nessuna immagine trovata per OCR." ;
}
2026-05-09 19:31:21 +02:00
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-05-09 18:54:20 +02:00
}
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
{
2026-03-12 18:48:13 +01:00
get = > _paths . SourcePath ;
set = > _paths . SourcePath = value ;
2024-10-14 22:55:52 +02:00
}
public string DestinationPath
{
2026-03-12 18:48:13 +01:00
get = > _paths . DestinationPath ;
set = > _paths . DestinationPath = value ;
2024-10-14 22:55:52 +02:00
}
2025-07-28 09:49:55 +02:00
public string HorizontalText
{
2026-03-12 18:48:13 +01:00
get = > _visual . HorizontalText ;
set = > _visual . HorizontalText = value ;
2025-07-28 09:49:55 +02:00
}
2025-07-29 11:10:54 +02:00
2025-07-28 10:34:03 +02:00
public string VerticalText
{
2026-03-12 18:48:13 +01:00
get = > _visual . VerticalText ;
set = > _visual . VerticalText = value ;
2025-07-28 10:34:03 +02:00
}
2025-07-28 09:49:55 +02:00
2025-07-28 14:45:03 +02:00
public bool OverwriteImages
{
2026-03-12 18:48:13 +01:00
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 ;
2026-03-12 18:48:13 +01:00
public ProcessingStateViewModel Processing = > _processing ;
public PathSettingsViewModel Paths = > _paths ;
public VisualSettingsViewModel Visual = > _visual ;
2025-07-29 10:34:23 +02:00
2026-03-12 18:48:13 +01:00
private void OnProcessingPropertyChanged ( object? sender , PropertyChangedEventArgs e )
2025-07-29 10:34:23 +02:00
{
2026-03-12 18:48:13 +01:00
if ( string . IsNullOrWhiteSpace ( e . PropertyName ) )
2025-07-29 10:34:23 +02:00
{
2026-03-12 18:48:13 +01:00
return ;
2025-07-29 10:34:23 +02:00
}
2026-03-12 18:48:13 +01: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 ;
}
2026-05-24 18:45:51 +02:00
if ( string . Equals ( e . PropertyName , nameof ( PathSettingsViewModel . DestinationPath ) , StringComparison . Ordinal ) )
{
UpdateAiCsvOutputPathForDestination ( ) ;
}
2026-03-12 18:48:13 +01:00
NotifyPropertyChanged ( e . PropertyName ) ;
}
private void OnAiPropertyChanged ( object? sender , PropertyChangedEventArgs e )
{
if ( string . IsNullOrWhiteSpace ( e . PropertyName ) )
{
return ;
}
NotifyPropertyChanged ( e . PropertyName ) ;
2026-06-06 11:54:21 +02:00
if ( string . Equals ( e . PropertyName , nameof ( AiSettingsViewModel . FaceUploadPath ) , StringComparison . Ordinal ) )
{
NotifyPropertyChanged ( nameof ( IsFaceUploadPathValid ) ) ;
}
2026-05-09 12:09:05 +02:00
UpdateFaceEncoderCommandStates ( ) ;
2026-06-06 11:54:21 +02:00
UpdateFaceUploadCommandStates ( ) ;
2026-05-09 20:27:44 +02:00
UpdateFaceMatcherCommandStates ( ) ;
2026-03-12 18:48:13 +01:00
}
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
{
2026-03-12 18:48:13 +01:00
get = > _visual . ThumbnailPrefix ;
set = > _visual . ThumbnailPrefix = value ;
2026-02-04 19:48:03 +01:00
}
public int ThumbnailHeight
{
2026-03-12 18:48:13 +01:00
get = > _visual . ThumbnailHeight ;
set = > _visual . ThumbnailHeight = value ;
2026-02-04 19:48:03 +01:00
}
public int ThumbnailWidth
{
2026-03-12 18:48:13 +01:00
get = > _visual . ThumbnailWidth ;
set = > _visual . ThumbnailWidth = value ;
2026-02-04 19:48:03 +01:00
}
// Big photo settings
public int PhotoBigHeight
{
2026-03-12 18:48:13 +01:00
get = > _visual . PhotoBigHeight ;
set = > _visual . PhotoBigHeight = value ;
2026-02-04 19:48:03 +01:00
}
public int PhotoBigWidth
{
2026-03-12 18:48:13 +01:00
get = > _visual . PhotoBigWidth ;
set = > _visual . PhotoBigWidth = value ;
2026-02-04 19:48:03 +01:00
}
// Font settings
public int FontSize
{
2026-03-12 18:48:13 +01:00
get = > _visual . FontSize ;
set = > _visual . FontSize = value ;
2026-02-04 19:48:03 +01:00
}
public int FontSizeThumbnail
{
2026-03-12 18:48:13 +01:00
get = > _visual . FontSizeThumbnail ;
set = > _visual . FontSizeThumbnail = value ;
2026-02-04 19:48:03 +01:00
}
public string FontName
{
2026-03-12 18:48:13 +01:00
get = > _visual . FontName ;
set = > _visual . FontName = value ;
2026-02-04 19:48:03 +01:00
}
public bool FontBold
{
2026-03-12 18:48:13 +01:00
get = > _visual . FontBold ;
set = > _visual . FontBold = value ;
2026-02-04 19:48:03 +01:00
}
// Text settings
public int TextTransparency
{
2026-03-12 18:48:13 +01:00
get = > _visual . TextTransparency ;
set = > _visual . TextTransparency = value ;
2026-02-04 19:48:03 +01:00
}
public int TextMargin
{
2026-03-12 18:48:13 +01:00
get = > _visual . TextMargin ;
set = > _visual . TextMargin = value ;
2026-02-04 19:48:03 +01:00
}
public string TextColorRGB
{
2026-03-12 18:48:13 +01:00
get = > _visual . TextColorRGB ;
set = > _visual . TextColorRGB = value ;
2026-02-04 19:48:03 +01:00
}
2026-02-15 11:13:23 +01:00
public string TransparentColor
{
2026-03-12 18:48:13 +01:00
get = > _visual . TransparentColor ;
set = > _visual . TransparentColor = value ;
2026-02-15 11:13:23 +01:00
}
public bool UseTransparentColor
{
2026-03-12 18:48:13 +01:00
get = > _visual . UseTransparentColor ;
set = > _visual . UseTransparentColor = value ;
2026-02-15 11:13:23 +01:00
}
2026-02-04 19:48:03 +01:00
// Logo/Watermark settings
public string LogoFile
{
2026-03-12 18:48:13 +01:00
get = > _visual . LogoFile ;
set = > _visual . LogoFile = value ;
2026-02-04 19:48:03 +01:00
}
public int LogoHeight
{
2026-03-12 18:48:13 +01:00
get = > _visual . LogoHeight ;
set = > _visual . LogoHeight = value ;
2026-02-04 19:48:03 +01:00
}
public int LogoWidth
{
2026-03-12 18:48:13 +01:00
get = > _visual . LogoWidth ;
set = > _visual . LogoWidth = value ;
2026-02-04 19:48:03 +01:00
}
public int LogoMargin
{
2026-03-12 18:48:13 +01:00
get = > _visual . LogoMargin ;
set = > _visual . LogoMargin = value ;
2026-02-04 19:48:03 +01:00
}
public int LogoTransparency
{
2026-03-12 18:48:13 +01:00
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
{
2026-03-12 18:48:13 +01:00
get = > _visual . VerticalTextSize ;
set = > _visual . VerticalTextSize = value ;
2026-02-04 19:48:03 +01:00
}
public int VerticalTextMargin
{
2026-03-12 18:48:13 +01:00
get = > _visual . VerticalTextMargin ;
set = > _visual . VerticalTextMargin = value ;
2026-02-04 19:48:03 +01:00
}
// JPEG compression settings
public int JpegQuality
{
2026-03-12 18:48:13 +01:00
get = > _visual . JpegQuality ;
set = > _visual . JpegQuality = value ;
2026-02-04 19:48:03 +01:00
}
public int JpegQualityThumbnail
{
2026-03-12 18:48:13 +01:00
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
{
2026-03-12 18:48:13 +01:00
get = > _visual . AddLogo ;
set = > _visual . AddLogo = value ;
2026-02-04 19:48:03 +01:00
}
public bool KeepOriginalDimensions
{
2026-03-12 18:48:13 +01:00
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
{
2026-02-16 18:58:15 +01:00
if ( _showPhotoNumber = = value ) return ;
2026-02-04 19:48:03 +01:00
_showPhotoNumber = value ;
2026-02-16 18:58:15 +01:00
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 ( ) ;
2026-02-16 18:58:15 +01:00
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
{
2026-02-16 19:55:37 +01:00
get = > _thumbnailOption = = ThumbnailOptionEnum . Time ;
2026-02-04 19:48:03 +01:00
set
{
2026-02-16 18:58:15 +01:00
if ( value )
{
2026-02-16 19:55:37 +01:00
ThumbnailOption = ThumbnailOptionEnum . Time ;
}
else if ( _thumbnailOption = = ThumbnailOptionEnum . Time )
{
ThumbnailOption = ThumbnailOptionEnum . None ;
2026-02-16 18:58:15 +01:00
}
2026-02-04 19:48:03 +01:00
NotifyPropertyChanged ( ) ;
}
}
private bool _showFileNameOnThumbnails ;
public bool ShowFileNameOnThumbnails
{
2026-02-16 19:55:37 +01:00
get = > _thumbnailOption = = ThumbnailOptionEnum . FileName ;
2026-02-04 19:48:03 +01:00
set
{
2026-02-16 19:55:37 +01:00
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
{
2026-02-16 19:55:37 +01:00
get = > _thumbnailOption = = ThumbnailOptionEnum . Text ;
2026-02-04 22:10:16 +01:00
set
{
2026-02-16 18:58:15 +01:00
if ( value )
2026-02-16 19:55:37 +01:00
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
{
2026-02-16 19:55:37 +01:00
get = > _thumbnailOption = = ThumbnailOptionEnum . RaceTime ;
2026-02-04 22:10:16 +01:00
set
{
2026-02-16 18:58:15 +01:00
if ( value )
2026-02-16 19:55:37 +01:00
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
{
2026-02-16 19:55:37 +01:00
get = > _thumbnailOption = = ThumbnailOptionEnum . FileNameAndTime ;
2026-02-04 22:10:16 +01:00
set
{
2026-02-16 18:58:15 +01:00
if ( value )
2026-02-16 19:55:37 +01:00
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 ( ) ;
}
}
2026-02-16 18:58:15 +01:00
// Single authoritative thumbnail mode string to avoid conflicting bindings
// Possible values: "None", "Text", "Time", "Number", "NumberAndTime", "RaceTime"
public string ThumbnailMode
{
get
{
2026-02-16 19:55:37 +01:00
return _thumbnailOption switch
{
ThumbnailOptionEnum . Text = > "Text" ,
ThumbnailOptionEnum . Time = > "Time" ,
ThumbnailOptionEnum . FileName = > "Number" ,
ThumbnailOptionEnum . FileNameAndTime = > "NumberAndTime" ,
ThumbnailOptionEnum . RaceTime = > "RaceTime" ,
_ = > "None" ,
} ;
2026-02-16 18:58:15 +01:00
}
set
{
2026-02-16 19:55:37 +01:00
// Map incoming string to enum and set the authoritative property
2026-02-16 18:58:15 +01:00
switch ( ( value ? ? string . Empty ) . ToLowerInvariant ( ) )
{
case "text" :
2026-02-16 19:55:37 +01:00
ThumbnailOption = ThumbnailOptionEnum . Text ;
2026-02-16 18:58:15 +01:00
break ;
case "time" :
2026-02-16 19:55:37 +01:00
ThumbnailOption = ThumbnailOptionEnum . Time ;
2026-02-16 18:58:15 +01:00
break ;
case "number" :
2026-02-16 19:55:37 +01:00
ThumbnailOption = ThumbnailOptionEnum . FileName ;
2026-02-16 18:58:15 +01:00
break ;
case "numberandtime" :
2026-02-16 19:55:37 +01:00
ThumbnailOption = ThumbnailOptionEnum . FileNameAndTime ;
2026-02-16 18:58:15 +01:00
break ;
case "racetime" :
2026-02-16 19:55:37 +01:00
ThumbnailOption = ThumbnailOptionEnum . RaceTime ;
2026-02-16 18:58:15 +01:00
break ;
default :
2026-02-16 19:55:37 +01:00
ThumbnailOption = ThumbnailOptionEnum . None ;
2026-02-16 18:58:15 +01:00
break ;
}
}
}
2026-02-04 23:16:06 +01:00
// Image processing progress and status
public string ProcessingStatus
{
2026-03-12 18:48:13 +01:00
get = > _processing . ProcessingStatus ;
set = > _processing . ProcessingStatus = value ;
2026-02-04 23:16:06 +01:00
}
public int ProcessedImagesCount
{
2026-03-12 18:48:13 +01:00
get = > _processing . ProcessedImagesCount ;
set = > _processing . ProcessedImagesCount = value ;
2026-02-04 23:16:06 +01:00
}
public int TotalImagesCount
{
2026-03-12 18:48:13 +01:00
get = > _processing . TotalImagesCount ;
set = > _processing . TotalImagesCount = value ;
2026-02-04 23:16:06 +01:00
}
public int ProgressBarValue
{
2026-03-12 18:48:13 +01:00
get = > _processing . ProgressBarValue ;
set = > _processing . ProgressBarValue = value ;
2026-02-04 23:16:06 +01:00
}
public int ProgressBarMaximum
{
2026-03-12 18:48:13 +01:00
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 ;
2026-03-12 18:48:13 +01:00
// Normalize paths
_paths . NormalizePaths ( ) ;
2026-02-28 21:34:45 +01:00
2026-02-04 23:16:06 +01:00
// Reset counters
2026-03-12 18:48:13 +01:00
_processing . ResetForRun ( ) ;
2026-02-28 21:34:45 +01:00
2026-02-04 23:16:06 +01:00
// Update PicSettings from DataModel using AutoMapper
_mapper . Map ( this , _picSettings ) ;
2026-02-16 18:58:15 +01:00
// 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-28 21:34:45 +01:00
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
{
2026-03-12 18:48:13 +01:00
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 ) ;
2026-02-16 18:32:04 +01:00
2026-03-12 18:48:13 +01:00
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
{
2026-02-15 11:14:19 +01:00
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 )
{
2026-02-15 11:14:19 +01:00
_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
2026-05-09 12:09:05 +02: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 ( ) ;
}
2026-06-06 11:54:21 +02:00
private void UpdateFaceUploadCommandStates ( )
{
_uploadFaceEncoderOutputCommand ? . RaiseCanExecuteChanged ( ) ;
}
2026-05-09 20:27:44 +02:00
private void UpdateFaceMatcherCommandStates ( )
{
_startFaceMatcherCommand ? . RaiseCanExecuteChanged ( ) ;
_stopFaceMatcherCommand ? . RaiseCanExecuteChanged ( ) ;
}
2026-05-09 12:09:05 +02:00
private async Task RunFaceEncoderAsync ( )
{
if ( IsFaceEncoderRunning )
{
FaceStatusMessage = "Face encoder gia in esecuzione." ;
return ;
}
2026-05-09 15:46:41 +02:00
var executableRootPath = NormalizeFilePathArgument ( FaceExecutablePath ) ;
var outputFolderPath = NormalizeDirectoryPathArgument ( FaceOutputFolderPath ) ;
2026-05-09 12:09:05 +02:00
var imagesFolder = NormalizeDirectoryPathArgument ( DestinationPath ) ;
2026-05-09 15:46:41 +02:00
var executablePath = ResolveConfiguredFaceEncoderExecutablePath ( executableRootPath , UseFaceGpu ) ;
2026-05-09 12:09:05 +02:00
2026-05-09 15:46:41 +02:00
if ( string . IsNullOrWhiteSpace ( executableRootPath ) )
2026-05-09 12:09:05 +02:00
{
2026-05-09 15:46:41 +02:00
FaceStatusMessage = "Percorso cartella face encoder non valido." ;
2026-05-09 12:09:05 +02:00
return ;
}
2026-05-09 15:46:41 +02:00
if ( string . IsNullOrWhiteSpace ( executablePath ) | | ! File . Exists ( executablePath ) )
2026-05-09 12:09:05 +02:00
{
2026-05-09 15:46:41 +02:00
FaceStatusMessage = UseFaceGpu
? "Impossibile trovare face_encoder_gpu.exe nella cartella selezionata."
: "Impossibile trovare face_encoder_cpu.exe nella cartella selezionata." ;
2026-05-09 12:09:05 +02:00
return ;
}
2026-05-09 15:46:41 +02:00
if ( string . IsNullOrWhiteSpace ( imagesFolder ) | | ! Directory . Exists ( imagesFolder ) )
2026-05-09 12:09:05 +02:00
{
2026-05-09 15:46:41 +02:00
FaceStatusMessage = "Cartella Destinazione non valida." ;
2026-05-09 12:09:05 +02:00
return ;
}
2026-05-09 15:46:41 +02:00
if ( string . IsNullOrWhiteSpace ( outputFolderPath ) )
2026-05-09 12:09:05 +02:00
{
2026-05-09 15:46:41 +02:00
FaceStatusMessage = "Inserisci la cartella di output per encodings e log." ;
2026-05-09 12:09:05 +02:00
return ;
}
try
{
2026-05-09 15:46:41 +02:00
Directory . CreateDirectory ( outputFolderPath ) ;
2026-05-09 12:09:05 +02:00
}
catch ( Exception ex )
{
2026-05-09 15:46:41 +02:00
_logger . LogError ( ex , "Unable to create face output directory: {OutputFolderPath}" , outputFolderPath ) ;
FaceStatusMessage = "Impossibile creare la cartella di output." ;
2026-05-09 12:09:05 +02:00
return ;
}
2026-05-09 15:46:41 +02:00
var parallelism = NormalizeFaceParallelism ( FaceParallelism ) ;
var minSize = NormalizeFaceMinSize ( FaceMinSize ) ;
var outputFiles = BuildFaceEncoderOutputPaths ( outputFolderPath , imagesFolder , DateTime . Now ) ;
2026-06-06 11:54:21 +02:00
_lastFaceEncoderOutputFilePath = outputFiles . OutputFilePath ;
2026-05-09 15:46:41 +02:00
FaceExecutablePath = executableRootPath ;
FaceOutputFolderPath = outputFolderPath ;
2026-05-09 12:09:05 +02:00
FaceCommandOutput = string . Empty ;
FaceStatusMessage = "Esecuzione face encoder in corso..." ;
2026-05-09 15:46:41 +02:00
var transcriptLines = new StringBuilder ( ) ;
2026-05-09 12:09:05 +02:00
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 ,
2026-05-09 15:46:41 +02:00
RedirectStandardInput = false ,
2026-05-09 12:09:05 +02:00
CreateNoWindow = false ,
} ;
processStartInfo . ArgumentList . Add ( "--images" ) ;
processStartInfo . ArgumentList . Add ( imagesFolder ) ;
processStartInfo . ArgumentList . Add ( "--out" ) ;
2026-05-09 15:46:41 +02:00
processStartInfo . ArgumentList . Add ( outputFiles . OutputFilePath ) ;
processStartInfo . ArgumentList . Add ( "--log" ) ;
processStartInfo . ArgumentList . Add ( outputFiles . LogFilePath ) ;
2026-05-09 12:09:05 +02:00
if ( FaceRecursive )
{
processStartInfo . ArgumentList . Add ( "--recursive" ) ;
}
2026-05-09 15:46:41 +02:00
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" ) ;
}
2026-05-09 12:09:05 +02:00
using var process = new Process { StartInfo = processStartInfo , EnableRaisingEvents = true } ;
2026-05-09 15:46:41 +02:00
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 ;
}
} ) ;
} ;
2026-05-09 12:09:05 +02:00
if ( ! process . Start ( ) )
{
throw new InvalidOperationException ( "Avvio face encoder fallito." ) ;
}
_hasStartedFaceEncoderInSession = true ;
EnsureFaceEncoderWatcherStarted ( ) ;
TrackFaceEncoderProcess ( process ) ;
await InvokeOnUiThreadAsync ( ( ) = > IsFaceEncoderRunning = true ) . ConfigureAwait ( false ) ;
2026-05-09 15:46:41 +02:00
if ( UseFaceGpu )
{
StartFaceEncoderLogWatcher ( outputFiles . LogFilePath , outputLines , transcriptLines ) ;
}
2026-05-09 12:09:05 +02:00
process . BeginOutputReadLine ( ) ;
process . BeginErrorReadLine ( ) ;
await process . WaitForExitAsync ( ) . ConfigureAwait ( false ) ;
2026-05-09 15:46:41 +02:00
var summary = BuildFaceEncoderSummary ( process . ExitCode , processStartInfo , outputFiles . OutputFilePath , outputFiles . LogFilePath , outputLines , errorLines ) ;
2026-05-09 12:09:05 +02:00
await InvokeOnUiThreadAsync ( ( ) = >
{
2026-05-09 15:46:41 +02:00
FaceCommandOutput = string . IsNullOrWhiteSpace ( FaceCommandOutput )
? summary
: $"{FaceCommandOutput.TrimEnd()}\n\n{summary}" ;
2026-05-09 12:09:05 +02:00
FaceStatusMessage = process . ExitCode = = 0
? "Face encoder completato."
: $"Face encoder terminato con errore (code {process.ExitCode})." ;
} ) . ConfigureAwait ( false ) ;
}
catch ( Exception ex )
{
2026-05-09 15:46:41 +02:00
Console . Error . WriteLine ( ex ) ;
2026-05-09 12:09:05 +02:00
_logger . LogError ( ex , "Face encoder execution failed." ) ;
await InvokeOnUiThreadAsync ( ( ) = >
{
FaceCommandOutput = ex . ToString ( ) ;
FaceStatusMessage = "Errore durante esecuzione face encoder." ;
} ) . ConfigureAwait ( false ) ;
}
finally
{
2026-05-09 15:46:41 +02:00
await StopFaceEncoderLogWatcherAsync ( ) . ConfigureAwait ( false ) ;
2026-05-09 12:09:05 +02:00
ClearTrackedFaceEncoderProcess ( ) ;
await InvokeOnUiThreadAsync ( ( ) = > IsFaceEncoderRunning = ComputeIsFaceEncoderRunning ( ) ) . ConfigureAwait ( false ) ;
}
}
2026-05-09 20:27:44 +02:00
private bool CanRunFaceMatcher ( )
{
return ! IsFaceMatcherRunning ;
}
2026-06-06 11:54:21 +02:00
private bool CanUploadFaceEncoderOutput ( )
{
return IsFaceUploadPathValid & & ! IsFaceUploadRunning ;
}
private async Task UploadFaceEncoderOutputAsync ( )
{
if ( ! IsValidFaceUploadPath ( FaceUploadPath ) )
{
FaceStatusMessage = "Percorso upload non valido." ;
return ;
}
if ( ! TryBuildFaceUploadRequest ( out var request , out var validationMessage ) )
{
FaceStatusMessage = validationMessage ;
await ShowErrorMessageAsync ( "Upload Face AI" , validationMessage ) . ConfigureAwait ( false ) ;
return ;
}
IsFaceUploadRunning = true ;
UpdateFaceUploadCommandStates ( ) ;
FaceStatusMessage = "Upload face encoder in corso..." ;
try
{
if ( FaceUploadDryRun )
{
var preview = BuildFaceUploadDryRunPreview ( request ) ;
await ShowMessageAsync ( "Upload Face AI (dry run)" , preview ) . ConfigureAwait ( false ) ;
await InvokeOnUiThreadAsync ( ( ) = > FaceStatusMessage = "Dry run: comando mostrato nel popup." ) . ConfigureAwait ( false ) ;
return ;
}
var result = await RunFaceUploadPowerShellAsync ( request ) . ConfigureAwait ( false ) ;
await InvokeOnUiThreadAsync ( ( ) = >
{
FaceCommandOutput = string . IsNullOrWhiteSpace ( FaceCommandOutput )
? result
: $"{FaceCommandOutput.TrimEnd()}\n\n{result}" ;
FaceStatusMessage = "Upload completato." ;
} ) . ConfigureAwait ( false ) ;
}
catch ( Exception ex )
{
var message = ex . GetBaseException ( ) . Message ;
_logger . LogError ( ex , "Face encoder upload failed." ) ;
await InvokeOnUiThreadAsync ( ( ) = > FaceStatusMessage = "Errore durante upload face encoder." ) . ConfigureAwait ( false ) ;
await ShowErrorMessageAsync ( "Upload Face AI" , message ) . ConfigureAwait ( false ) ;
}
finally
{
await InvokeOnUiThreadAsync ( ( ) = >
{
IsFaceUploadRunning = false ;
UpdateFaceUploadCommandStates ( ) ;
} ) . ConfigureAwait ( false ) ;
}
}
2026-05-09 20:27:44 +02:00
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 ) ;
}
}
2026-05-09 12:09:05 +02:00
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.
}
}
2026-05-09 20:27:44 +02:00
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.
}
}
2026-05-09 12:09:05 +02:00
private void EnsureFaceEncoderWatcherStarted ( )
{
if ( _faceEncoderWatcherTask is not null )
{
return ;
}
_faceEncoderWatcherTokenSource = new CancellationTokenSource ( ) ;
_faceEncoderWatcherTask = WatchFaceEncoderProcessAsync ( _faceEncoderWatcherTokenSource . Token ) ;
}
2026-05-09 20:27:44 +02:00
private void EnsureFaceMatcherWatcherStarted ( )
{
if ( _faceMatcherWatcherTask is not null )
{
return ;
}
_faceMatcherWatcherTokenSource = new CancellationTokenSource ( ) ;
_faceMatcherWatcherTask = WatchFaceMatcherProcessAsync ( _faceMatcherWatcherTokenSource . Token ) ;
}
2026-05-09 15:46:41 +02:00
private void StartFaceEncoderLogWatcher ( string logFilePath , StringBuilder outputLines , StringBuilder transcriptLines )
2026-05-09 12:09:05 +02:00
{
2026-05-09 15:46:41 +02:00
_faceEncoderLogWatcherTokenSource ? . Cancel ( ) ;
_faceEncoderLogWatcherTokenSource ? . Dispose ( ) ;
_faceEncoderLogWatcherTokenSource = new CancellationTokenSource ( ) ;
_faceEncoderLogWatcherTask = WatchFaceEncoderLogFileAsync ( logFilePath , outputLines , transcriptLines , _faceEncoderLogWatcherTokenSource . Token ) ;
}
2026-05-09 20:27:44 +02:00
private void StartFaceMatcherLogWatcher ( string logFilePath , StringBuilder outputLines , StringBuilder transcriptLines )
{
_faceMatcherLogWatcherTokenSource ? . Cancel ( ) ;
_faceMatcherLogWatcherTokenSource ? . Dispose ( ) ;
_faceMatcherLogWatcherTokenSource = new CancellationTokenSource ( ) ;
_faceMatcherLogWatcherTask = WatchFaceMatcherLogFileAsync ( logFilePath , outputLines , transcriptLines , _faceMatcherLogWatcherTokenSource . Token ) ;
}
2026-05-09 15:46:41 +02:00
private async Task StopFaceEncoderLogWatcherAsync ( )
{
var tokenSource = _faceEncoderLogWatcherTokenSource ;
var task = _faceEncoderLogWatcherTask ;
_faceEncoderLogWatcherTokenSource = null ;
_faceEncoderLogWatcherTask = null ;
if ( tokenSource is null )
2026-05-09 12:09:05 +02:00
{
return ;
}
2026-05-09 15:46:41 +02:00
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 ( ) ;
}
2026-05-09 12:09:05 +02:00
}
2026-05-09 20:27:44 +02:00
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 ( ) ;
}
}
2026-05-09 15:46:41 +02:00
private async Task WatchFaceEncoderLogFileAsync ( string logFilePath , StringBuilder outputLines , StringBuilder transcriptLines , CancellationToken token )
2026-05-09 12:09:05 +02:00
{
2026-05-09 15:46:41 +02:00
long filePosition = 0 ;
while ( ! token . IsCancellationRequested )
2026-05-09 12:09:05 +02:00
{
2026-05-09 15:46:41 +02:00
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 )
2026-05-09 12:09:05 +02:00
{
2026-05-09 15:46:41 +02:00
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.
2026-05-09 12:09:05 +02:00
}
2026-05-09 15:46:41 +02:00
await Task . Delay ( TimeSpan . FromMilliseconds ( 250 ) , token ) . ConfigureAwait ( false ) ;
2026-05-09 12:09:05 +02:00
}
2026-05-09 15:46:41 +02:00
}
2026-05-09 12:09:05 +02:00
2026-05-09 20:27:44 +02:00
private async Task WatchFaceMatcherLogFileAsync ( string logFilePath , StringBuilder outputLines , StringBuilder transcriptLines , CancellationToken token )
2026-05-09 15:46:41 +02:00
{
2026-05-09 20:27:44 +02:00
long filePosition = 0 ;
2026-05-09 12:09:05 +02:00
2026-05-09 20:27:44 +02:00
while ( ! token . IsCancellationRequested )
2026-05-09 12:09:05 +02:00
{
2026-05-09 20:27:44 +02:00
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 ;
}
2026-05-09 12:09:05 +02:00
2026-05-09 20:27:44 +02:00
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 ;
}
}
2026-05-24 17:29:05 +02:00
private void QueueRefreshNumberAiGpuCapabilities ( )
2026-05-09 20:27:44 +02:00
{
if ( ! TryBuildNumberAiModelConfiguration ( out var configuration ) )
{
2026-05-24 17:29:05 +02:00
_numberAiGpuValidationPending = false ;
2026-05-09 20:27:44 +02:00
NumberAiGpuOptionEnabled = false ;
_ai . UseNumberAiGpu = false ;
2026-05-09 17:53:15 +02:00
return ;
}
2026-05-24 17:29:05 +02:00
_numberAiGpuValidationPending = true ;
var requestVersion = Interlocked . Increment ( ref _numberAiGpuRefreshVersion ) ;
_ = RefreshNumberAiGpuCapabilitiesAsync ( configuration , requestVersion ) ;
}
private async Task RefreshNumberAiGpuCapabilitiesAsync ( ModelConfiguration configuration , int requestVersion )
{
try
2026-05-09 17:53:15 +02:00
{
2026-05-24 17:29:05 +02:00
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 ) ;
2026-05-09 17:53:15 +02:00
}
}
private void SetUseNumberAiGpu ( bool value )
{
2026-05-24 17:29:05 +02:00
if ( ! value )
2026-05-09 17:53:15 +02:00
{
_ai . UseNumberAiGpu = false ;
return ;
}
2026-05-24 17:29:05 +02:00
if ( NumberAiGpuOptionEnabled | | _numberAiGpuValidationPending )
{
_ai . UseNumberAiGpu = true ;
return ;
}
_ai . UseNumberAiGpu = false ;
2026-05-09 17:53:15 +02:00
}
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 ) ;
}
2026-06-06 11:54:21 +02:00
private Task ShowMessageAsync ( string title , string message )
2026-05-09 17:53:15 +02:00
{
return InvokeOnUiThreadAsync ( ( ) = >
{
ShowMessageRequested ? . Invoke ( this , Tuple . Create ( title , message , 0 ) ) ;
} ) ;
}
2026-06-06 11:54:21 +02:00
private Task ShowErrorMessageAsync ( string title , string message )
{
return ShowMessageAsync ( title , message ) ;
}
2026-05-24 18:45:51 +02:00
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 ( ) ;
}
2026-05-09 15:46:41 +02:00
private void SetUseFaceGpu ( bool value )
{
var currentValue = _ai . UseFaceGpu ;
if ( ! FaceGpuOptionEnabled )
2026-05-09 12:09:05 +02:00
{
return ;
}
2026-05-09 15:46:41 +02:00
if ( currentValue = = value )
2026-05-09 12:09:05 +02:00
{
2026-05-09 15:46:41 +02:00
return ;
2026-05-09 12:09:05 +02:00
}
2026-05-09 15:46:41 +02:00
_ai . UseFaceGpu = value ;
var previousRecommendedUpsample = GetRecommendedFaceUpsample ( currentValue ) ;
if ( _ai . FaceUpsample = = previousRecommendedUpsample )
2026-05-09 12:09:05 +02:00
{
2026-05-09 15:46:41 +02:00
_ai . FaceUpsample = GetRecommendedFaceUpsample ( value ) ;
2026-05-09 12:09:05 +02:00
}
}
private void TrackFaceEncoderProcess ( Process process )
{
lock ( _faceEncoderProcessLock )
{
_faceEncoderProcess = process ;
}
}
2026-05-09 20:27:44 +02:00
private void TrackFaceMatcherProcess ( Process process )
{
lock ( _faceMatcherProcessLock )
{
_faceMatcherProcess = process ;
}
}
2026-05-09 12:09:05 +02:00
private void ClearTrackedFaceEncoderProcess ( )
{
lock ( _faceEncoderProcessLock )
{
if ( _faceEncoderProcess is not null & & _faceEncoderProcess . HasExited )
{
_faceEncoderProcess = null ;
return ;
}
_faceEncoderProcess = null ;
}
}
2026-05-09 20:27:44 +02:00
private void ClearTrackedFaceMatcherProcess ( )
{
lock ( _faceMatcherProcessLock )
{
if ( _faceMatcherProcess is not null & & _faceMatcherProcess . HasExited )
{
_faceMatcherProcess = null ;
return ;
}
_faceMatcherProcess = null ;
}
}
2026-05-09 12:09:05 +02:00
private Process ? GetTrackedFaceEncoderProcess ( )
{
lock ( _faceEncoderProcessLock )
{
if ( _faceEncoderProcess is null )
{
return null ;
}
if ( _faceEncoderProcess . HasExited )
{
_faceEncoderProcess = null ;
return null ;
}
return _faceEncoderProcess ;
}
}
2026-05-09 20:27:44 +02:00
private Process ? GetTrackedFaceMatcherProcess ( )
{
lock ( _faceMatcherProcessLock )
{
if ( _faceMatcherProcess is null )
{
return null ;
}
if ( _faceMatcherProcess . HasExited )
{
_faceMatcherProcess = null ;
return null ;
}
return _faceMatcherProcess ;
}
}
2026-05-09 12:09:05 +02:00
private Process ? FindConfiguredFaceEncoderProcess ( )
{
2026-05-09 15:46:41 +02:00
var configuredExecutablePath = ResolveConfiguredFaceEncoderExecutablePath ( FaceExecutablePath , UseFaceGpu ) ;
2026-05-09 12:09:05 +02:00
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 ;
}
2026-05-09 20:27:44 +02:00
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 ;
}
2026-05-09 12:09:05 +02:00
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 ;
}
2026-05-09 20:27:44 +02:00
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 ;
}
2026-05-09 12:09:05 +02:00
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 ;
}
}
2026-05-09 20:27:44 +02:00
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 ;
}
}
2026-05-09 12:09:05 +02:00
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 ) ;
}
}
2026-05-09 15:46:41 +02:00
private void AppendFaceProcessOutput ( StringBuilder builder , StringBuilder transcriptBuilder , string? line , bool isError )
2026-05-09 12:09:05 +02:00
{
if ( string . IsNullOrWhiteSpace ( line ) )
{
return ;
}
lock ( builder )
{
builder . AppendLine ( line ) ;
}
2026-05-09 15:46:41 +02:00
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 ) ;
}
2026-05-09 20:27:44 +02:00
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 ;
}
2026-05-09 15:46:41 +02:00
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" ) ) ;
}
2026-06-06 11:54:21 +02:00
internal static bool IsValidFaceUploadPath ( string value )
{
return FaceUploadPathRegex . IsMatch ( ( value ? ? string . Empty ) . Trim ( ) ) ;
}
internal static string CombineRemoteUploadPath ( string basePath , string relativePath )
{
var normalizedBasePath = ( basePath ? ? string . Empty ) . Trim ( ) . Replace ( '\\' , '/' ) . TrimEnd ( '/' ) ;
var normalizedRelativePath = ( relativePath ? ? string . Empty ) . Trim ( ) . Replace ( '\\' , '/' ) . Trim ( '/' ) ;
return string . IsNullOrWhiteSpace ( normalizedBasePath )
? normalizedRelativePath
: string . IsNullOrWhiteSpace ( normalizedRelativePath )
? normalizedBasePath
: $"{normalizedBasePath}/{normalizedRelativePath}" ;
}
private void LoadFaceSshUserPreferences ( )
{
if ( _pickerPreferenceService is null )
{
return ;
}
_isLoadingFaceSshUserPreferences = true ;
try
{
_ai . FaceSshUsername = _pickerPreferenceService . GetRememberedValue ( PickerPreferenceKeys . FaceSshUsername ) ? ? string . Empty ;
_ai . FaceSshPassword = _pickerPreferenceService . GetRememberedRawValue ( PickerPreferenceKeys . FaceSshPassword ) ? ? string . Empty ;
_ai . FaceSshAddress = _pickerPreferenceService . GetRememberedValue ( PickerPreferenceKeys . FaceSshAddress ) ? ? string . Empty ;
_ai . FaceSshPort = _pickerPreferenceService . GetRememberedValue ( PickerPreferenceKeys . FaceSshPort ) ? ? "22" ;
_ai . FaceSshPathA = _pickerPreferenceService . GetRememberedValue ( PickerPreferenceKeys . FaceSshPathA ) ? ? string . Empty ;
_ai . FaceSshPathB = _pickerPreferenceService . GetRememberedValue ( PickerPreferenceKeys . FaceSshPathB ) ? ? string . Empty ;
_ai . FaceUploadDryRun = TryGetRememberedBoolPreference ( PickerPreferenceKeys . FaceUploadDryRun ) ;
}
finally
{
_isLoadingFaceSshUserPreferences = false ;
}
}
private void SetFaceSshPreferenceValue ( string preferenceKey , Action < string > setValue , string value )
{
setValue ( value ) ;
if ( _isLoadingFaceSshUserPreferences )
{
return ;
}
_pickerPreferenceService ? . RememberRawValue ( preferenceKey , value ) ;
}
private void SetFaceSshPreferenceBoolValue ( string preferenceKey , Action < bool > setValue , bool value )
{
setValue ( value ) ;
if ( _isLoadingFaceSshUserPreferences )
{
return ;
}
_pickerPreferenceService ? . RememberRawValue ( preferenceKey , value ? bool . TrueString : bool . FalseString ) ;
}
private bool TryGetRememberedBoolPreference ( string preferenceKey )
{
var rawValue = _pickerPreferenceService ? . GetRememberedRawValue ( preferenceKey ) ;
return bool . TryParse ( rawValue , out var parsed ) & & parsed ;
}
private bool TryBuildFaceUploadRequest ( out FaceUploadRequest request , out string validationMessage )
{
request = null ! ;
validationMessage = string . Empty ;
var sourceFilePath = ResolveFaceEncoderUploadSourceFile ( ) ;
if ( string . IsNullOrWhiteSpace ( sourceFilePath ) | | ! File . Exists ( sourceFilePath ) )
{
validationMessage = "File .pkl face encoder non trovato nella cartella output." ;
return false ;
}
var username = FaceSshUsername . Trim ( ) ;
var password = FaceSshPassword ;
var serverAddress = FaceSshAddress . Trim ( ) ;
if ( string . IsNullOrWhiteSpace ( username ) | | string . IsNullOrWhiteSpace ( serverAddress ) )
{
validationMessage = "Inserisci username e indirizzo SSH." ;
return false ;
}
if ( ! int . TryParse ( FaceSshPort . Trim ( ) , NumberStyles . Integer , CultureInfo . InvariantCulture , out var port ) | | port is < 1 or > 65535 )
{
validationMessage = "Porta SSH non valida." ;
return false ;
}
var remoteBasePaths = new [ ] { FaceSshPathA , FaceSshPathB }
. Select ( path = > path . Trim ( ) )
. Where ( path = > ! string . IsNullOrWhiteSpace ( path ) )
. ToArray ( ) ;
if ( remoteBasePaths . Length ! = 2 )
{
validationMessage = "Inserisci path SSH A e path SSH B." ;
return false ;
}
var relativeUploadPath = FaceUploadPath . Trim ( ) ;
var remoteDirectories = remoteBasePaths
. Select ( path = > CombineRemoteUploadPath ( path , relativeUploadPath ) )
. ToArray ( ) ;
request = new FaceUploadRequest ( sourceFilePath , username , password , serverAddress , port , remoteDirectories ) ;
return true ;
}
private string? ResolveFaceEncoderUploadSourceFile ( )
{
var outputFolderPath = NormalizeDirectoryPathArgument ( FaceOutputFolderPath ) ;
if ( ! Directory . Exists ( outputFolderPath ) )
{
return null ;
}
return ResolveLatestFaceUploadSourceFile ( outputFolderPath , NormalizeDirectoryPathArgument ( DestinationPath ) ) ;
}
internal static string? ResolveLatestFaceUploadSourceFile ( string outputFolderPath , string imagesFolderPath )
{
var normalizedOutputFolderPath = NormalizeDirectoryPathArgument ( outputFolderPath ) ;
if ( ! Directory . Exists ( normalizedOutputFolderPath ) )
{
return null ;
}
var safeRaceName = BuildSafeFaceEncoderRaceName ( NormalizeDirectoryPathArgument ( imagesFolderPath ) ) ;
var racePattern = $"face_encodings_*_{safeRaceName}.pkl" ;
return new DirectoryInfo ( normalizedOutputFolderPath )
. EnumerateFiles ( racePattern , SearchOption . TopDirectoryOnly )
. OrderByDescending ( file = > file . LastWriteTimeUtc )
. ThenByDescending ( file = > file . Name , StringComparer . OrdinalIgnoreCase )
. FirstOrDefault ( )
? . FullName ;
}
private async Task < string > RunFaceUploadPowerShellAsync ( FaceUploadRequest request )
{
var commandLine = BuildFaceUploadPowerShellCommand ( request ) ;
var processStartInfo = CreateFaceUploadProcessStartInfo ( commandLine ) ;
using var process = new Process { StartInfo = processStartInfo } ;
if ( ! process . Start ( ) )
{
throw new InvalidOperationException ( "Avvio PowerShell per upload fallito." ) ;
}
await process . WaitForExitAsync ( ) . ConfigureAwait ( false ) ;
if ( process . ExitCode ! = 0 )
{
throw new InvalidOperationException ( $"Upload fallito (exit code {process.ExitCode}). Verifica autenticazione SSH o inserisci la password nel terminale se richiesta." ) ;
}
var summary = new StringBuilder ( ) ;
summary . AppendLine ( "Upload Face AI completato." ) ;
summary . AppendLine ( $"Command: {commandLine}" ) ;
summary . AppendLine ( $"File: {request.LocalPath}" ) ;
foreach ( var remoteDirectory in request . RemoteDirectories )
{
summary . AppendLine ( $"Destinazione: {remoteDirectory}" ) ;
}
return summary . ToString ( ) ;
}
private static ProcessStartInfo CreateFaceUploadProcessStartInfo ( string commandLine )
{
var processStartInfo = new ProcessStartInfo
{
FileName = ResolvePowerShellExecutable ( ) ,
UseShellExecute = false ,
RedirectStandardOutput = false ,
RedirectStandardError = false ,
RedirectStandardInput = false ,
CreateNoWindow = false
} ;
processStartInfo . ArgumentList . Add ( "-NoProfile" ) ;
processStartInfo . ArgumentList . Add ( "-ExecutionPolicy" ) ;
processStartInfo . ArgumentList . Add ( "Bypass" ) ;
processStartInfo . ArgumentList . Add ( "-Command" ) ;
processStartInfo . ArgumentList . Add ( commandLine ) ;
return processStartInfo ;
}
private static string BuildFaceUploadDryRunPreview ( FaceUploadRequest request )
{
return BuildFaceUploadPowerShellCommand ( request ) ;
}
private static string BuildFaceUploadPowerShellCommand ( FaceUploadRequest request )
{
var fileName = Path . GetFileName ( request . LocalPath ) ;
var copyCommands = request . RemoteDirectories
. Select ( remoteDirectory = > BuildFaceUploadCopyCommand ( request , CombineRemoteUploadPath ( remoteDirectory , fileName ) ) )
. ToArray ( ) ;
return string . Join ( "; " , copyCommands ) ;
}
private static string ResolvePowerShellExecutable ( )
{
return OperatingSystem . IsWindows ( ) ? "powershell.exe" : "pwsh" ;
}
private static string BuildFaceUploadCopyCommand ( FaceUploadRequest request , string remoteFilePath )
{
return BuildScpCommand ( request , remoteFilePath ) ;
}
private static string BuildScpCommand ( FaceUploadRequest request , string remoteFilePath )
{
var remoteTarget = $"{request.UserName}@{request.ServerAddress}:{QuoteScpRemotePath(remoteFilePath)}" ;
return $"scp -P {request.Port.ToString(CultureInfo.InvariantCulture)} {QuotePowerShellString(request.LocalPath)} {QuotePowerShellString(remoteTarget)}" ;
}
private static string QuotePowerShellString ( string value )
{
return $"'{(value ?? string.Empty).Replace(" ' ", " ' ' ", StringComparison.Ordinal)}'" ;
}
private static string QuoteScpRemotePath ( string value )
{
return "'" + ( value ? ? string . Empty ) . Replace ( "'" , "'\\''" , StringComparison . Ordinal ) + "'" ;
}
private sealed record FaceUploadRequest (
string LocalPath ,
string UserName ,
string Password ,
string ServerAddress ,
int Port ,
IReadOnlyList < string > RemoteDirectories ) ;
2026-05-09 15:46:41 +02:00
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
} ;
}
2026-05-09 20:27:44 +02:00
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
} ;
}
2026-05-09 15:46:41 +02:00
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 ) ;
}
}
}
2026-05-09 20:27:44 +02:00
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" ) ;
}
}
2026-05-09 15:46:41 +02:00
private static int NormalizeFaceParallelism ( int value )
{
return value is > = 1 and < = 5 ? value : 3 ;
}
2026-05-09 20:27:44 +02:00
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 ) ;
}
2026-05-09 18:54:20 +02:00
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
{
2026-05-09 19:31:21 +02:00
1 = > 4 ,
2 = > 8 ,
3 = > 16 ,
4 = > 24 ,
_ = > 32
2026-05-09 18:54:20 +02:00
}
: normalized switch
{
1 = > 1 ,
2 = > 2 ,
3 = > 3 ,
4 = > 4 ,
_ = > 5
} ;
2026-05-09 19:31:21 +02:00
return useGpu ? requestedWorkers : Math . Min ( requestedWorkers , maxWorkers ) ;
2026-05-09 18:54:20 +02:00
}
2026-05-09 15:46:41 +02:00
private static int NormalizeFaceMinSize ( int value )
{
return value > 0 ? value : 35 ;
}
private static bool GetRecommendedFaceUpsample ( bool useGpu )
{
return ! useGpu ;
2026-05-09 12:09:05 +02:00
}
2026-05-09 15:46:41 +02:00
private static string BuildFaceEncoderSummary (
int exitCode ,
ProcessStartInfo processStartInfo ,
string outputFilePath ,
string logFilePath ,
StringBuilder outputLines ,
StringBuilder errorLines )
2026-05-09 12:09:05 +02:00
{
var summary = new StringBuilder ( ) ;
summary . AppendLine ( $"Exit code: {exitCode}" ) ;
2026-05-09 15:46:41 +02:00
summary . AppendLine ( $"Command: {processStartInfo.FileName} {string.Join(" ", processStartInfo.ArgumentList)}" ) ;
summary . AppendLine ( $"Output file: {outputFilePath}" ) ;
summary . AppendLine ( $"Log file: {logFilePath}" ) ;
2026-05-09 12:09:05 +02:00
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 ( ) ;
}
2026-05-09 20:27:44 +02:00
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 ;
}
2026-05-09 12:09:05 +02:00
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
2026-03-12 18:48:13 +01:00
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 ;
2026-02-14 19:20:25 +01:00
// Request that the View shows a message to the user (message, caption, icon)
2026-03-12 18:48:13 +01:00
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 ) ;
}
2026-02-16 18:32:04 +01:00
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 )
{
2026-03-12 18:48:13 +01:00
SaveSettingsRequested ? . Invoke ( this , string . Empty ) ;
2026-02-04 19:48:03 +01:00
}
private void LoadSettings ( object parameter )
{
2026-03-12 18:48:13 +01:00
LoadSettingsRequested ? . Invoke ( this , string . Empty ) ;
2026-02-04 19:48:03 +01:00
}
private void SelectColor ( object parameter )
{
SelectColorRequested ? . Invoke ( this , EventArgs . Empty ) ;
}
2026-02-15 11:13:23 +01:00
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 ) ;
}
2026-02-14 22:18:56 +01:00
private string _appVersion = string . Empty ;
public string AppVersion
{
get = > _appVersion ;
set
{
_appVersion = value ;
NotifyPropertyChanged ( ) ;
}
}
2024-10-14 22:55:52 +02:00
}
2026-02-10 21:18:46 +01:00
}