using ImageCatalog_2.Commands; using ImageCatalog_2.Models; using ImageCatalog_2.Services; using ImageCatalog_2.ViewModels; using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; #if WINDOWS using System.Drawing.Text; #endif using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Input; using AIFotoONLUS.Core; using AutoMapper; using MaddoShared; using Microsoft.Extensions.Logging; namespace ImageCatalog_2 { public class DataModel : ViewModelBase { public ICommand TestCommand { get; } public ICommand AsyncTestCommand { get; } public ICommand AsyncCancelOperationCommand { get; } public ICommand ProcessImagesCommand { get; } public ICommand SelectSourceFolderCommand { get; } public ICommand SelectDestinationFolderCommand { get; } public ICommand SelectLogoFileCommand { get; } public ICommand SaveSettingsCommand { get; } public ICommand LoadSettingsCommand { get; } public ICommand SelectColorCommand { get; } public ICommand SelectTransparentColorCommand { get; } public ICommand SelectModelsFolderCommand { get; } public ICommand SelectCsvOutputCommand { get; } public ICommand StartAiCommand { get; } public ICommand StartFaceEncoderCommand { get; } public ICommand StopFaceEncoderCommand { get; } private readonly ITestService _service; private readonly ILogger _logger; private readonly ISettingsService _settingsService; private readonly ImageCreationService _imageCreationService; private readonly IAiExtractionService _aiExtractionService; private readonly IImageProcessingCoordinator _imageProcessingCoordinator; private readonly ProcessingStateViewModel _processing; private readonly PathSettingsViewModel _paths; private readonly AiSettingsViewModel _ai; private readonly RaceUploadSettingsViewModel _raceUpload; private readonly VisualSettingsViewModel _visual; private readonly PicSettings _picSettings; private readonly IMapper _mapper; private readonly AsyncCommand _startFaceEncoderCommand; private readonly AsyncCommand _stopFaceEncoderCommand; private readonly object _faceEncoderProcessLock = new(); private Process? _faceEncoderProcess; private CancellationTokenSource? _faceEncoderWatcherTokenSource; private Task? _faceEncoderWatcherTask; private CancellationTokenSource? _faceEncoderLogWatcherTokenSource; private Task? _faceEncoderLogWatcherTask; private bool _hasStartedFaceEncoderInSession; // ComboBox collections public List AvailableFonts { get; } public List VerticalPositions { get; } = new() { "Alto", "Centro", "Basso" }; public List HorizontalAlignments { get; } = new() { "Sinistra", "Centro", "Destra" }; [CLSCompliant(false)] public DataModel(ITestService testService, ISettingsService settingsService, ImageCreationService imageCreationService, IAiExtractionService aiExtractionService, IImageProcessingCoordinator imageProcessingCoordinator, PicSettings picSettings, IMapper mapper, ILogger logger, MaddoShared.IVersionProvider? versionProvider = null) { _service = testService; _logger = logger; _settingsService = settingsService; _imageCreationService = imageCreationService; _aiExtractionService = aiExtractionService; _imageProcessingCoordinator = imageProcessingCoordinator; _processing = new ProcessingStateViewModel(); _processing.PropertyChanged += OnProcessingPropertyChanged; _paths = new PathSettingsViewModel(); _paths.PropertyChanged += OnPathsPropertyChanged; _ai = new AiSettingsViewModel(); _ai.PropertyChanged += OnAiPropertyChanged; _raceUpload = new RaceUploadSettingsViewModel(); _raceUpload.PropertyChanged += OnRaceUploadPropertyChanged; _visual = new VisualSettingsViewModel(); _visual.PropertyChanged += OnVisualPropertyChanged; _picSettings = picSettings; _mapper = mapper; // Populate AppVersion from version provider when available AppVersion = versionProvider?.GetVersionString() ?? string.Empty; TestCommand = new RelayCommand(Test); AsyncTestCommand = new AsyncCommand(TestAsync); AsyncCancelOperationCommand = new AsyncCommand(CancelOperation); ProcessImagesCommand = new AsyncCommand(ProcessImages); SelectModelsFolderCommand = new RelayCommand(SelectModelsFolder); SelectCsvOutputCommand = new RelayCommand(SelectCsvOutput); StartAiCommand = new AsyncCommand(StartAiAsync); _startFaceEncoderCommand = new AsyncCommand(RunFaceEncoderAsync, CanRunFaceEncoder); _stopFaceEncoderCommand = new AsyncCommand(() => StopFaceEncoderAsync("Arresto richiesto dall'utente."), CanStopFaceEncoder); StartFaceEncoderCommand = _startFaceEncoderCommand; StopFaceEncoderCommand = _stopFaceEncoderCommand; SelectSourceFolderCommand = new RelayCommand(SelectSourceFolder); SelectDestinationFolderCommand = new RelayCommand(SelectDestinationFolder); SelectLogoFileCommand = new RelayCommand(SelectLogoFile); SaveSettingsCommand = new RelayCommand(SaveSettings); LoadSettingsCommand = new RelayCommand(LoadSettings); SelectColorCommand = new RelayCommand(SelectColor); SelectTransparentColorCommand = new RelayCommand(SelectTransparentColor); // Load available fonts AvailableFonts = LoadAvailableFonts(); RefreshNumberAiGpuCapabilities(); RefreshFaceExecutableCapabilities(); } private async Task StartAiAsync() { MainToken = new CancellationTokenSource(); try { await RunAiExtractionCoreAsync(MainToken.Token, useDestination: true, recursive: true).ConfigureAwait(false); } catch (OperationCanceledException) { await InvokeOnUiThreadAsync(() => NumberAiStatsSummary = "OCR annullato.").ConfigureAwait(false); } catch (Exception ex) { _logger.LogError(ex, "AI extraction failed"); if (UseNumberAiGpu) { RefreshNumberAiGpuCapabilities(); } await InvokeOnUiThreadAsync(() => NumberAiStatsSummary = $"Errore OCR: {ex.GetBaseException().Message}").ConfigureAwait(false); await ShowErrorMessageAsync("Errore AI", ex.GetBaseException().Message).ConfigureAwait(false); } finally { MainToken = null; } } private async Task RunAiExtractionCoreAsync(CancellationToken token, bool useDestination = false, bool recursive = false, bool failOnInvalidPath = false) { var searchRoot = useDestination ? DestinationPath : SourcePath; if (string.IsNullOrWhiteSpace(searchRoot) || !System.IO.Directory.Exists(searchRoot)) { _logger.LogWarning("AI extraction path invalid: {Path}", searchRoot); if (failOnInvalidPath) { throw new DirectoryNotFoundException($"AI extraction path invalid: {searchRoot}"); } return; } await InvokeOnUiThreadAsync(() => { PreviewResults.Clear(); AiProgress = 0; NumberAiStatsSummary = BuildNumberAiIdleSummary(); }).ConfigureAwait(false); var summary = await _aiExtractionService.RunAsync( new AiExtractionRequest { SearchRoot = searchRoot, Recursive = recursive, IncludeThumbnails = IncludeNumberAiThumbnails, ModelsFolderPath = ModelsFolderPath, UseGpu = UseNumberAiGpu, WorkloadLevel = NumberAiWorkloadLevel, CsvOutputPath = CsvOutputPath }, token, result => InvokeOnUiThreadAsync(() => { if (!string.IsNullOrWhiteSpace(result.Text)) { PreviewResults.Add(result); } }), progress => InvokeOnUiThreadAsync(() => { AiProgress = progress.PercentComplete; NumberAiStatsSummary = BuildNumberAiProgressSummary(progress); })).ConfigureAwait(false); await InvokeOnUiThreadAsync(() => { AiProgress = summary.TotalFiles > 0 ? 100 : 0; NumberAiStatsSummary = BuildNumberAiCompletionSummary(summary); }).ConfigureAwait(false); } /// /// Optional UI-thread invoker set by the active UI layer. /// public Action? UiInvoker { get; set; } private Task InvokeOnUiThreadAsync(Action action) { return Task.Run(() => { if (UiInvoker != null) UiInvoker(action); else action(); }); } public AiSettingsViewModel Ai => _ai; public RaceUploadSettingsViewModel RaceUpload => _raceUpload; // AI properties public bool ExtractNumbers { get => _ai.ExtractNumbers; set => _ai.ExtractNumbers = value; } public string ModelsFolderPath { get => _ai.ModelsFolderPath; set { _ai.ModelsFolderPath = value; RefreshNumberAiGpuCapabilities(); } } public string CsvOutputPath { get => _ai.CsvOutputPath; set => _ai.CsvOutputPath = value; } public bool UseNumberAiGpu { get => _ai.UseNumberAiGpu; set => SetUseNumberAiGpu(value); } public bool NumberAiGpuOptionEnabled { get => _ai.NumberAiGpuOptionEnabled; private set => _ai.NumberAiGpuOptionEnabled = value; } public bool IncludeNumberAiThumbnails { get => _ai.IncludeNumberAiThumbnails; set => _ai.IncludeNumberAiThumbnails = value; } public IReadOnlyList NumberAiWorkloadOptions { get; } = [1, 2, 3, 4, 5]; public int NumberAiWorkloadLevel { get => _ai.NumberAiWorkloadLevel; set => _ai.NumberAiWorkloadLevel = NormalizeNumberAiWorkloadLevel(value); } public string NumberAiStatsSummary { get => _ai.NumberAiStatsSummary; private set => _ai.NumberAiStatsSummary = value; } public string FaceExecutablePath { get => _ai.FaceExecutablePath; set { var normalizedValue = value ?? string.Empty; if (string.Equals(_ai.FaceExecutablePath, normalizedValue, StringComparison.Ordinal)) { RefreshFaceExecutableCapabilities(); return; } _ai.FaceExecutablePath = normalizedValue; RefreshFaceExecutableCapabilities(); } } public string FaceOutputFolderPath { get => _ai.FaceOutputFolderPath; set => _ai.FaceOutputFolderPath = value; } public bool FaceRecursive { get => _ai.FaceRecursive; set => _ai.FaceRecursive = value; } public bool FaceIncludeThumbnails { get => _ai.FaceIncludeThumbnails; set => _ai.FaceIncludeThumbnails = value; } public IReadOnlyList FaceParallelismOptions { get; } = [1, 2, 3, 4, 5]; public int FaceParallelism { get => _ai.FaceParallelism; set => _ai.FaceParallelism = value; } public int FaceMinSize { get => _ai.FaceMinSize; set => _ai.FaceMinSize = value; } public bool FaceUpsample { get => _ai.FaceUpsample; set => _ai.FaceUpsample = value; } public bool FaceGpuOptionEnabled => _ai.FaceGpuOptionEnabled; public bool UseFaceGpu { get => _ai.UseFaceGpu; set => SetUseFaceGpu(value); } public bool IsFaceEncoderRunning { get => _ai.IsFaceEncoderRunning; private set => _ai.IsFaceEncoderRunning = value; } public string FaceStatusMessage { get => _ai.FaceStatusMessage; private set => _ai.FaceStatusMessage = value; } public string FaceCommandOutput { get => _ai.FaceCommandOutput; private set => _ai.FaceCommandOutput = value; } // Race upload settings public string ApiLogin { get => _raceUpload.ApiLogin; set => _raceUpload.ApiLogin = value; } public string ApiPassword { get => _raceUpload.ApiPassword; set => _raceUpload.ApiPassword = value; } public string ApiRaceDescription { get => _raceUpload.ApiRaceDescription; set => _raceUpload.ApiRaceDescription = value; } public string ApiRaceTypeId { get => _raceUpload.ApiRaceTypeId; set => _raceUpload.ApiRaceTypeId = value; } public DateTime ApiRaceStartDate { get => _raceUpload.ApiRaceStartDate; set => _raceUpload.ApiRaceStartDate = value; } public DateTime ApiRaceEndDate { get => _raceUpload.ApiRaceEndDate; set => _raceUpload.ApiRaceEndDate = value; } public string ApiPathBase { get => _raceUpload.ApiPathBase; set => _raceUpload.ApiPathBase = value; } public string ApiLocalita { get => _raceUpload.ApiLocalita; set => _raceUpload.ApiLocalita = value; } public int ApiEventoInLineaIndex { get => _raceUpload.ApiEventoInLineaIndex; set => _raceUpload.ApiEventoInLineaIndex = value; } public int ApiTipoIndexValue { get => _raceUpload.ApiTipoIndexValue; set => _raceUpload.ApiTipoIndexValue = value; } public int ApiFreeEventIndex { get => _raceUpload.ApiFreeEventIndex; set => _raceUpload.ApiFreeEventIndex = value; } public string ApiRaceId { get => _raceUpload.ApiRaceId; set => _raceUpload.ApiRaceId = value; } public string ApiRemoteProcessedBasePath { get => _raceUpload.ApiRemoteProcessedBasePath; set => _raceUpload.ApiRemoteProcessedBasePath = value; } // Preview results for DataGrid public System.Collections.ObjectModel.ObservableCollection PreviewResults => _ai.PreviewResults; public double AiProgress { get => _ai.AiProgress; set => _ai.AiProgress = value; } private string BuildNumberAiIdleSummary() { var workerCount = ResolveNumberAiWorkerCount(UseNumberAiGpu, NumberAiWorkloadLevel); return $"In attesa. Carico {NumberAiWorkloadLevel}/5, {workerCount} worker, 0.00 img/s."; } private static string BuildNumberAiProgressSummary(AiExtractionProgressUpdate progress) { return $"{progress.ProcessedFiles}/{progress.TotalFiles} immagini, media {progress.AverageImagesPerSecond:F2} img/s, carico {progress.WorkloadLevel}/5, {progress.WorkerCount} worker."; } private static string BuildNumberAiCompletionSummary(AiExtractionRunSummary summary) { if (summary.TotalFiles == 0) { return "Nessuna immagine trovata per OCR."; } return $"Completato: {summary.ProcessedFiles}/{summary.TotalFiles} immagini, media finale {summary.AverageImagesPerSecond:F2} img/s, errori {summary.FailedFiles}, carico {summary.WorkloadLevel}/5, {summary.WorkerCount} worker."; } private List LoadAvailableFonts() { #if WINDOWS var fonts = new List(); using (var installedFonts = new InstalledFontCollection()) { fonts.AddRange(installedFonts.Families.Select(f => f.Name)); } return fonts; #else return new List(); #endif } private CancellationTokenSource? _mainToken; public CancellationTokenSource? MainToken { get => _mainToken; set { _mainToken = value; NotifyPropertyChanged(); } } public string SourcePath { get => _paths.SourcePath; set => _paths.SourcePath = value; } public string DestinationPath { get => _paths.DestinationPath; set => _paths.DestinationPath = value; } public string HorizontalText { get => _visual.HorizontalText; set => _visual.HorizontalText = value; } public string VerticalText { get => _visual.VerticalText; set => _visual.VerticalText = value; } public bool OverwriteImages { get => _visual.OverwriteImages; set => _visual.OverwriteImages = value; } private bool _uiEnabled = true; public bool UiEnabled { get => _uiEnabled; set { _uiEnabled = value; NotifyPropertyChanged(); NotifyPropertyChanged(nameof(UiDisabled)); } } public bool UiDisabled => !_uiEnabled; public ProcessingStateViewModel Processing => _processing; public PathSettingsViewModel Paths => _paths; public VisualSettingsViewModel Visual => _visual; private void OnProcessingPropertyChanged(object? sender, PropertyChangedEventArgs e) { if (string.IsNullOrWhiteSpace(e.PropertyName)) { return; } // 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; } NotifyPropertyChanged(e.PropertyName); } private void OnAiPropertyChanged(object? sender, PropertyChangedEventArgs e) { if (string.IsNullOrWhiteSpace(e.PropertyName)) { return; } NotifyPropertyChanged(e.PropertyName); UpdateFaceEncoderCommandStates(); } 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; } private int _chunkSize; public int ChunkSize { get => _chunkSize; set { _chunkSize = value; NotifyPropertyChanged(); } } private int _threadsCount; public int ThreadsCount { get => _threadsCount; set { _threadsCount = value; NotifyPropertyChanged(); } } // Thumbnail settings public string ThumbnailPrefix { get => _visual.ThumbnailPrefix; set => _visual.ThumbnailPrefix = value; } public int ThumbnailHeight { get => _visual.ThumbnailHeight; set => _visual.ThumbnailHeight = value; } public int ThumbnailWidth { get => _visual.ThumbnailWidth; set => _visual.ThumbnailWidth = value; } // Big photo settings public int PhotoBigHeight { get => _visual.PhotoBigHeight; set => _visual.PhotoBigHeight = value; } public int PhotoBigWidth { get => _visual.PhotoBigWidth; set => _visual.PhotoBigWidth = value; } // Font settings public int FontSize { get => _visual.FontSize; set => _visual.FontSize = value; } public int FontSizeThumbnail { get => _visual.FontSizeThumbnail; set => _visual.FontSizeThumbnail = value; } public string FontName { get => _visual.FontName; set => _visual.FontName = value; } public bool FontBold { get => _visual.FontBold; set => _visual.FontBold = value; } // Text settings public int TextTransparency { get => _visual.TextTransparency; set => _visual.TextTransparency = value; } public int TextMargin { get => _visual.TextMargin; set => _visual.TextMargin = value; } public string TextColorRGB { get => _visual.TextColorRGB; set => _visual.TextColorRGB = value; } public string TransparentColor { get => _visual.TransparentColor; set => _visual.TransparentColor = value; } public bool UseTransparentColor { get => _visual.UseTransparentColor; set => _visual.UseTransparentColor = value; } // Logo/Watermark settings public string LogoFile { get => _visual.LogoFile; set => _visual.LogoFile = value; } public int LogoHeight { get => _visual.LogoHeight; set => _visual.LogoHeight = value; } public int LogoWidth { get => _visual.LogoWidth; set => _visual.LogoWidth = value; } public int LogoMargin { get => _visual.LogoMargin; set => _visual.LogoMargin = value; } public int LogoTransparency { get => _visual.LogoTransparency; set => _visual.LogoTransparency = value; } // Image library selection (UI radio buttons bind to the boolean helpers) private string _imageLibrary = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "System.Graphics" : "ImageSharp"; /// /// Whether the application is running on Windows. Used by cross-platform UIs to show/hide Windows-only options. /// public bool IsRunningOnWindows => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); /// /// The selected image processing library. Possible values: "System.Graphics" or "ImageSharp". /// This value is mirrored into PicSettings.ImageCreatorProvider so the runtime mapper picks the implementation. /// public string ImageLibrary { get => _imageLibrary; set { if (_imageLibrary == value) return; _imageLibrary = value; // Reflect selection into PicSettings so mapper can resolve at runtime _picSettings.ImageCreatorProvider = string.Equals(value, "ImageSharp", StringComparison.OrdinalIgnoreCase) ? "ALTERNATE" : "Sharp"; NotifyPropertyChanged(); NotifyPropertyChanged(nameof(UseSystemGraphics)); NotifyPropertyChanged(nameof(UseImageSharp)); NotifyPropertyChanged(nameof(IsRunningOnWindows)); } } public bool UseSystemGraphics { get => string.Equals(ImageLibrary, "System.Graphics", StringComparison.OrdinalIgnoreCase); set { if (value) ImageLibrary = "System.Graphics"; NotifyPropertyChanged(); } } public bool UseImageSharp { get => string.Equals(ImageLibrary, "ImageSharp", StringComparison.OrdinalIgnoreCase); set { if (value) ImageLibrary = "ImageSharp"; NotifyPropertyChanged(); } } // Folder division settings private int _filesPerFolder = 99; public int FilesPerFolder { get => _filesPerFolder; set { _filesPerFolder = value; NotifyPropertyChanged(); } } private string _folderSuffix = ""; public string FolderSuffix { get => _folderSuffix; set { _folderSuffix = value; NotifyPropertyChanged(); } } private int _counterDigits = 2; public int CounterDigits { get => _counterDigits; set { _counterDigits = value; NotifyPropertyChanged(); } } // Vertical text settings public int VerticalTextSize { get => _visual.VerticalTextSize; set => _visual.VerticalTextSize = value; } public int VerticalTextMargin { get => _visual.VerticalTextMargin; set => _visual.VerticalTextMargin = value; } // JPEG compression settings public int JpegQuality { get => _visual.JpegQuality; set => _visual.JpegQuality = value; } public int JpegQualityThumbnail { get => _visual.JpegQualityThumbnail; set => _visual.JpegQualityThumbnail = value; } // CheckBox settings private bool _createThumbnails = true; public bool CreateThumbnails { get => _createThumbnails; set { _createThumbnails = value; NotifyPropertyChanged(); } } private bool _automaticRotation; public bool AutomaticRotation { get => _automaticRotation; set { _automaticRotation = value; NotifyPropertyChanged(); } } private bool _forceJpeg; public bool ForceJpeg { get => _forceJpeg; set { _forceJpeg = value; NotifyPropertyChanged(); } } private bool _updateSubdirectories; public bool UpdateSubdirectories { get => _updateSubdirectories; set { _updateSubdirectories = value; NotifyPropertyChanged(); } } private bool _createSubfolders; public bool CreateSubfolders { get => _createSubfolders; set { _createSubfolders = value; NotifyPropertyChanged(); } } private bool _addTime; public bool AddTime { get => _addTime; set { _addTime = value; NotifyPropertyChanged(); } } private bool _addRaceTime; public bool AddRaceTime { get => _addRaceTime; set { _addRaceTime = value; NotifyPropertyChanged(); } } public bool AddLogo { get => _visual.AddLogo; set => _visual.AddLogo = value; } public bool KeepOriginalDimensions { get => _visual.KeepOriginalDimensions; set => _visual.KeepOriginalDimensions = value; } private bool _showDate; public bool ShowDate { get => _showDate; set { _showDate = value; NotifyPropertyChanged(); } } private bool _showPhotoNumber; public bool ShowPhotoNumber { get => _showPhotoNumber; set { if (_showPhotoNumber == value) return; _showPhotoNumber = value; if (value) { // ensure mutually exclusive choices _addTimeToThumbnails = false; _addTextToThumbnails = false; _addNumberAndTimeToThumbnails = false; _addRaceTimeToThumbnails = false; NotifyPropertyChanged(nameof(AddTimeToThumbnails)); NotifyPropertyChanged(nameof(AddTextToThumbnails)); NotifyPropertyChanged(nameof(AddNumberAndTimeToThumbnails)); NotifyPropertyChanged(nameof(AddRaceTimeToThumbnails)); } NotifyPropertyChanged(); NotifyPropertyChanged(nameof(ThumbnailMode)); } } private bool _shutdownSystem; public bool ShutdownSystem { get => _shutdownSystem; set { _shutdownSystem = value; NotifyPropertyChanged(); } } // ComboBox position/alignment settings private string _verticalPosition = "Basso"; public string VerticalPosition { get => _verticalPosition; set { _verticalPosition = value; NotifyPropertyChanged(); } } private string _horizontalAlignment = "Centro"; public string HorizontalAlignment { get => _horizontalAlignment; set { _horizontalAlignment = value; NotifyPropertyChanged(); } } private string _logoHorizontalPosition = "Destra"; public string LogoHorizontalPosition { get => _logoHorizontalPosition; set { _logoHorizontalPosition = value; NotifyPropertyChanged(); } } private string _logoVerticalPosition = "Basso"; public string LogoVerticalPosition { get => _logoVerticalPosition; set { _logoVerticalPosition = value; NotifyPropertyChanged(); } } // RadioButton settings private bool _useProgressiveNumbering = true; public bool UseProgressiveNumbering { get => _useProgressiveNumbering; set { _useProgressiveNumbering = value; NotifyPropertyChanged(); } } private bool _useFileNumbering; public bool UseFileNumbering { get => _useFileNumbering; set { _useFileNumbering = value; NotifyPropertyChanged(); } } private bool _useParallelProcessing = true; public bool UseParallelProcessing { get => _useParallelProcessing; set { _useParallelProcessing = value; NotifyPropertyChanged(); } } private bool _useSequentialProcessing; public bool UseSequentialProcessing { get => _useSequentialProcessing; set { _useSequentialProcessing = value; NotifyPropertyChanged(); } } // Additional settings that were missing private bool _addTimeToThumbnails; public bool AddTimeToThumbnails { get => _thumbnailOption == ThumbnailOptionEnum.Time; set { if (value) { ThumbnailOption = ThumbnailOptionEnum.Time; } else if (_thumbnailOption == ThumbnailOptionEnum.Time) { ThumbnailOption = ThumbnailOptionEnum.None; } NotifyPropertyChanged(); } } private bool _showFileNameOnThumbnails; public bool ShowFileNameOnThumbnails { get => _thumbnailOption == ThumbnailOptionEnum.FileName; set { if (value) ThumbnailOption = ThumbnailOptionEnum.FileName; else if (_thumbnailOption == ThumbnailOptionEnum.FileName) ThumbnailOption = ThumbnailOptionEnum.None; 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(); } } private string _bigPhotoSuffix = ""; public string BigPhotoSuffix { get => _bigPhotoSuffix; set { _bigPhotoSuffix = value; NotifyPropertyChanged(); } } private bool _addTextToThumbnails; public bool AddTextToThumbnails { get => _thumbnailOption == ThumbnailOptionEnum.Text; set { if (value) ThumbnailOption = ThumbnailOptionEnum.Text; else if (_thumbnailOption == ThumbnailOptionEnum.Text) ThumbnailOption = ThumbnailOptionEnum.None; NotifyPropertyChanged(); } } private bool _addRaceTimeToThumbnails; public bool AddRaceTimeToThumbnails { get => _thumbnailOption == ThumbnailOptionEnum.RaceTime; set { if (value) ThumbnailOption = ThumbnailOptionEnum.RaceTime; else if (_thumbnailOption == ThumbnailOptionEnum.RaceTime) ThumbnailOption = ThumbnailOptionEnum.None; NotifyPropertyChanged(); } } private bool _addNumberAndTimeToThumbnails; public bool AddNumberAndTimeToThumbnails { get => _thumbnailOption == ThumbnailOptionEnum.FileNameAndTime; set { if (value) ThumbnailOption = ThumbnailOptionEnum.FileNameAndTime; else if (_thumbnailOption == ThumbnailOptionEnum.FileNameAndTime) ThumbnailOption = ThumbnailOptionEnum.None; NotifyPropertyChanged(); } } // New enum and authoritative property for thumbnail selection public enum ThumbnailOptionEnum { None = 0, Text = 1, FileName = 2, Time = 3, FileNameAndTime = 4, RaceTime = 5 } private ThumbnailOptionEnum _thumbnailOption = ThumbnailOptionEnum.None; // Name matches DTO property so SettingsService mapping works public ThumbnailOptionEnum ThumbnailOption { get => _thumbnailOption; set { if (_thumbnailOption == value) return; _thumbnailOption = value; // Notify all dependent properties so UI updates NotifyPropertyChanged(); NotifyPropertyChanged(nameof(AddTextToThumbnails)); NotifyPropertyChanged(nameof(AddTimeToThumbnails)); NotifyPropertyChanged(nameof(ShowPhotoNumber)); NotifyPropertyChanged(nameof(AddNumberAndTimeToThumbnails)); NotifyPropertyChanged(nameof(AddRaceTimeToThumbnails)); NotifyPropertyChanged(nameof(ShowFileNameOnThumbnails)); NotifyPropertyChanged(nameof(ThumbnailMode)); } } // Helper int property to bind ComboBox SelectedIndex in the WinForms designer public int ThumbnailOptionIndex { get => (int)ThumbnailOption; set { var opt = (ThumbnailOptionEnum)value; if (opt == ThumbnailOption) return; ThumbnailOption = opt; NotifyPropertyChanged(); } } // Single authoritative thumbnail mode string to avoid conflicting bindings // Possible values: "None", "Text", "Time", "Number", "NumberAndTime", "RaceTime" public string ThumbnailMode { get { return _thumbnailOption switch { ThumbnailOptionEnum.Text => "Text", ThumbnailOptionEnum.Time => "Time", ThumbnailOptionEnum.FileName => "Number", ThumbnailOptionEnum.FileNameAndTime => "NumberAndTime", ThumbnailOptionEnum.RaceTime => "RaceTime", _ => "None", }; } set { // Map incoming string to enum and set the authoritative property switch ((value ?? string.Empty).ToLowerInvariant()) { case "text": ThumbnailOption = ThumbnailOptionEnum.Text; break; case "time": ThumbnailOption = ThumbnailOptionEnum.Time; break; case "number": ThumbnailOption = ThumbnailOptionEnum.FileName; break; case "numberandtime": ThumbnailOption = ThumbnailOptionEnum.FileNameAndTime; break; case "racetime": ThumbnailOption = ThumbnailOptionEnum.RaceTime; break; default: ThumbnailOption = ThumbnailOptionEnum.None; break; } } } // Image processing progress and status public string ProcessingStatus { get => _processing.ProcessingStatus; set => _processing.ProcessingStatus = value; } public int ProcessedImagesCount { get => _processing.ProcessedImagesCount; set => _processing.ProcessedImagesCount = value; } public int TotalImagesCount { get => _processing.TotalImagesCount; set => _processing.TotalImagesCount = value; } public int ProgressBarValue { get => _processing.ProgressBarValue; set => _processing.ProgressBarValue = value; } public int ProgressBarMaximum { get => _processing.ProgressBarMaximum; set => _processing.ProgressBarMaximum = value; } private void Test(object parameter) { Debug.WriteLine("Yep"); this.UiEnabled = !this.UiEnabled; } private async Task TestAsync() { Debug.WriteLine("Yep c"); } public async Task ProcessImages() { _logger.LogInformation("Avvio elaborazione..."); UiEnabled = false; MainToken?.Dispose(); MainToken = new CancellationTokenSource(); var token = MainToken.Token; // Normalize paths _paths.NormalizePaths(); // Reset counters _processing.ResetForRun(); // Update PicSettings from DataModel using AutoMapper _mapper.Map(this, _picSettings); // Explicitly ensure thumbnail-related flags are applied to PicSettings // because AutoMapper may not map differently-named properties. try { _picSettings.AggiungiScritteMiniature = this.AddTextToThumbnails; _picSettings.UsaOrarioMiniatura = this.AddTimeToThumbnails; _picSettings.AggNumTempMin = this.AddNumberAndTimeToThumbnails; _picSettings.AggTempoGaraMin = this.AddRaceTimeToThumbnails; _picSettings.CreaMiniature = this.CreateThumbnails; _picSettings.LarghezzaSmall = this.ThumbnailWidth; _picSettings.AltezzaSmall = this.ThumbnailHeight; _picSettings.DimMin = this.FontSizeThumbnail; _picSettings.JpegQualityMin = this.JpegQualityThumbnail; } catch { // Best-effort; do not fail processing on mapping issues } var imageCreationOptions = new ImageCreationService.Options { AggiornaSottodirectory = UpdateSubdirectories, CreaSottocartelle = CreateSubfolders, FilePerCartella = FilesPerFolder, SuffissoCartelle = FolderSuffix, CifreContatore = CounterDigits, NumerazioneType = UseProgressiveNumbering ? NumerazioneType.Progressiva : NumerazioneType.Files, SourcePath = SourcePath, DestinationPath = DestinationPath, MaxThreads = ThreadsCount, ChunksSize = ChunkSize, LinearExecution = UseSequentialProcessing }; try { var runResult = await _imageProcessingCoordinator.RunAsync( new ImageProcessingRunRequest { Options = imageCreationOptions }, token, update => { ProcessedImagesCount = update.Processed; TotalImagesCount = update.Total; ProgressBarMaximum = update.Total; ProgressBarValue = update.Processed; ProcessingStatus = update.Status; }, speed => { SpeedCounter = speed; }).ConfigureAwait(false); SpeedCounter = runResult.FinalSpeedCounter; } 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; } 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); } private async Task CancelOperation() { try { var tokenSource = MainToken; if (tokenSource is not null) { // Cancel synchronously and return to caller. Some CTSource implementations // may provide async helpers but cancelling is immediate. try { tokenSource.Cancel(); } catch (Exception ex) { _logger.LogWarning(ex, "Exception while cancelling token"); } } UiEnabled = true; } catch (Exception e) { _logger.LogError(e, "Error canceling the token"); _logger.LogInformation("Ignora questo errore"); } } public async Task StopFaceEncoderAsync(string reason, bool waitForExit = true) { var trackedProcess = GetTrackedFaceEncoderProcess(); var ownsProcess = false; var process = trackedProcess; if (process is null) { process = FindConfiguredFaceEncoderProcess(); ownsProcess = process is not null; } if (process is null) { await InvokeOnUiThreadAsync(() => { IsFaceEncoderRunning = false; FaceStatusMessage = "Face encoder non in esecuzione."; }).ConfigureAwait(false); return; } try { await InvokeOnUiThreadAsync(() => FaceStatusMessage = reason).ConfigureAwait(false); var gracefulStopRequested = TryRequestFaceEncoderStop(process); if (waitForExit) { var exited = await WaitForProcessExitAsync(process, TimeSpan.FromSeconds(5)).ConfigureAwait(false); if (!exited) { try { process.Kill(entireProcessTree: true); exited = await WaitForProcessExitAsync(process, TimeSpan.FromSeconds(5)).ConfigureAwait(false); } catch (Exception ex) { _logger.LogWarning(ex, "Unable to terminate face encoder process {ProcessId}", process.Id); } } await InvokeOnUiThreadAsync(() => { IsFaceEncoderRunning = !exited && IsProcessAlive(process); FaceStatusMessage = exited ? "Face encoder arrestato." : gracefulStopRequested ? "Segnale di arresto inviato al face encoder." : "Arresto forzato del face encoder richiesto."; }).ConfigureAwait(false); } } finally { if (ownsProcess) { process.Dispose(); } } } private bool CanRunFaceEncoder() { return !IsFaceEncoderRunning; } private bool CanStopFaceEncoder() { return IsFaceEncoderRunning; } private void UpdateFaceEncoderCommandStates() { _startFaceEncoderCommand?.RaiseCanExecuteChanged(); _stopFaceEncoderCommand?.RaiseCanExecuteChanged(); } private async Task RunFaceEncoderAsync() { if (IsFaceEncoderRunning) { FaceStatusMessage = "Face encoder gia in esecuzione."; return; } var executableRootPath = NormalizeFilePathArgument(FaceExecutablePath); var outputFolderPath = NormalizeDirectoryPathArgument(FaceOutputFolderPath); var imagesFolder = NormalizeDirectoryPathArgument(DestinationPath); var executablePath = ResolveConfiguredFaceEncoderExecutablePath(executableRootPath, UseFaceGpu); if (string.IsNullOrWhiteSpace(executableRootPath)) { FaceStatusMessage = "Percorso cartella face encoder non valido."; return; } if (string.IsNullOrWhiteSpace(executablePath) || !File.Exists(executablePath)) { FaceStatusMessage = UseFaceGpu ? "Impossibile trovare face_encoder_gpu.exe nella cartella selezionata." : "Impossibile trovare face_encoder_cpu.exe nella cartella selezionata."; return; } if (string.IsNullOrWhiteSpace(imagesFolder) || !Directory.Exists(imagesFolder)) { FaceStatusMessage = "Cartella Destinazione non valida."; return; } if (string.IsNullOrWhiteSpace(outputFolderPath)) { FaceStatusMessage = "Inserisci la cartella di output per encodings e log."; return; } try { Directory.CreateDirectory(outputFolderPath); } catch (Exception ex) { _logger.LogError(ex, "Unable to create face output directory: {OutputFolderPath}", outputFolderPath); FaceStatusMessage = "Impossibile creare la cartella di output."; return; } var parallelism = NormalizeFaceParallelism(FaceParallelism); var minSize = NormalizeFaceMinSize(FaceMinSize); var outputFiles = BuildFaceEncoderOutputPaths(outputFolderPath, imagesFolder, DateTime.Now); FaceExecutablePath = executableRootPath; FaceOutputFolderPath = outputFolderPath; FaceCommandOutput = string.Empty; FaceStatusMessage = "Esecuzione face encoder in corso..."; var transcriptLines = new StringBuilder(); var outputLines = new StringBuilder(); var errorLines = new StringBuilder(); try { var processStartInfo = new ProcessStartInfo { FileName = executablePath, WorkingDirectory = Path.GetDirectoryName(executablePath) ?? Environment.CurrentDirectory, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, RedirectStandardInput = false, CreateNoWindow = false, }; processStartInfo.ArgumentList.Add("--images"); processStartInfo.ArgumentList.Add(imagesFolder); processStartInfo.ArgumentList.Add("--out"); processStartInfo.ArgumentList.Add(outputFiles.OutputFilePath); processStartInfo.ArgumentList.Add("--log"); processStartInfo.ArgumentList.Add(outputFiles.LogFilePath); if (FaceRecursive) { processStartInfo.ArgumentList.Add("--recursive"); } if (FaceIncludeThumbnails) { processStartInfo.ArgumentList.Add("--include-tn"); } processStartInfo.ArgumentList.Add(UseFaceGpu ? "--multiprocess" : "--multicore"); processStartInfo.ArgumentList.Add(parallelism.ToString()); processStartInfo.ArgumentList.Add("--min-size"); processStartInfo.ArgumentList.Add(minSize.ToString()); if (FaceUpsample) { processStartInfo.ArgumentList.Add("--upsample"); } using var process = new Process { StartInfo = processStartInfo, EnableRaisingEvents = true }; process.OutputDataReceived += (_, args) => AppendFaceProcessOutput(outputLines, transcriptLines, args.Data, isError: false); process.ErrorDataReceived += (_, args) => AppendFaceProcessOutput(errorLines, transcriptLines, args.Data, isError: true); process.Exited += (_, _) => { _ = InvokeOnUiThreadAsync(() => { if (!ComputeIsFaceEncoderRunning()) { IsFaceEncoderRunning = false; } }); }; if (!process.Start()) { throw new InvalidOperationException("Avvio face encoder fallito."); } _hasStartedFaceEncoderInSession = true; EnsureFaceEncoderWatcherStarted(); TrackFaceEncoderProcess(process); await InvokeOnUiThreadAsync(() => IsFaceEncoderRunning = true).ConfigureAwait(false); if (UseFaceGpu) { StartFaceEncoderLogWatcher(outputFiles.LogFilePath, outputLines, transcriptLines); } process.BeginOutputReadLine(); process.BeginErrorReadLine(); await process.WaitForExitAsync().ConfigureAwait(false); var summary = BuildFaceEncoderSummary(process.ExitCode, processStartInfo, outputFiles.OutputFilePath, outputFiles.LogFilePath, outputLines, errorLines); await InvokeOnUiThreadAsync(() => { FaceCommandOutput = string.IsNullOrWhiteSpace(FaceCommandOutput) ? summary : $"{FaceCommandOutput.TrimEnd()}\n\n{summary}"; FaceStatusMessage = process.ExitCode == 0 ? "Face encoder completato." : $"Face encoder terminato con errore (code {process.ExitCode})."; }).ConfigureAwait(false); } catch (Exception ex) { Console.Error.WriteLine(ex); _logger.LogError(ex, "Face encoder execution failed."); await InvokeOnUiThreadAsync(() => { FaceCommandOutput = ex.ToString(); FaceStatusMessage = "Errore durante esecuzione face encoder."; }).ConfigureAwait(false); } finally { await StopFaceEncoderLogWatcherAsync().ConfigureAwait(false); ClearTrackedFaceEncoderProcess(); await InvokeOnUiThreadAsync(() => IsFaceEncoderRunning = ComputeIsFaceEncoderRunning()).ConfigureAwait(false); } } private async Task WatchFaceEncoderProcessAsync(CancellationToken token) { using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1)); try { while (await timer.WaitForNextTickAsync(token).ConfigureAwait(false)) { if (!_hasStartedFaceEncoderInSession) { continue; } var isRunning = ComputeIsFaceEncoderRunning(); if (isRunning != IsFaceEncoderRunning) { await InvokeOnUiThreadAsync(() => IsFaceEncoderRunning = isRunning).ConfigureAwait(false); } } } catch (OperationCanceledException) { // App shutdown. } } private void EnsureFaceEncoderWatcherStarted() { if (_faceEncoderWatcherTask is not null) { return; } _faceEncoderWatcherTokenSource = new CancellationTokenSource(); _faceEncoderWatcherTask = WatchFaceEncoderProcessAsync(_faceEncoderWatcherTokenSource.Token); } private void StartFaceEncoderLogWatcher(string logFilePath, StringBuilder outputLines, StringBuilder transcriptLines) { _faceEncoderLogWatcherTokenSource?.Cancel(); _faceEncoderLogWatcherTokenSource?.Dispose(); _faceEncoderLogWatcherTokenSource = new CancellationTokenSource(); _faceEncoderLogWatcherTask = WatchFaceEncoderLogFileAsync(logFilePath, outputLines, transcriptLines, _faceEncoderLogWatcherTokenSource.Token); } private async Task StopFaceEncoderLogWatcherAsync() { var tokenSource = _faceEncoderLogWatcherTokenSource; var task = _faceEncoderLogWatcherTask; _faceEncoderLogWatcherTokenSource = null; _faceEncoderLogWatcherTask = null; if (tokenSource is null) { return; } try { await tokenSource.CancelAsync().ConfigureAwait(false); if (task is not null) { await task.ConfigureAwait(false); } } catch (OperationCanceledException) { // Expected when shutting down the watcher. } finally { tokenSource.Dispose(); } } private async Task WatchFaceEncoderLogFileAsync(string logFilePath, StringBuilder outputLines, StringBuilder transcriptLines, CancellationToken token) { long filePosition = 0; while (!token.IsCancellationRequested) { try { if (File.Exists(logFilePath)) { using var stream = new FileStream(logFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); if (filePosition > stream.Length) { filePosition = 0; } stream.Seek(filePosition, SeekOrigin.Begin); using var reader = new StreamReader(stream); while (!reader.EndOfStream) { var line = await reader.ReadLineAsync(token).ConfigureAwait(false); AppendFaceProcessOutput(outputLines, transcriptLines, line, isError: false); } filePosition = stream.Position; } } catch (OperationCanceledException) { throw; } catch (IOException) { // Retry on the next polling interval while the encoder is still writing. } catch (UnauthorizedAccessException) { // Retry on the next polling interval if the log file is transiently locked. } await Task.Delay(TimeSpan.FromMilliseconds(250), token).ConfigureAwait(false); } } private void RefreshFaceExecutableCapabilities() { var executableRoot = NormalizeFilePathArgument(_ai.FaceExecutablePath); var hasCpu = !string.IsNullOrWhiteSpace(ResolveConfiguredFaceEncoderExecutablePath(executableRoot, useGpu: false)); var hasGpu = !string.IsNullOrWhiteSpace(ResolveConfiguredFaceEncoderExecutablePath(executableRoot, useGpu: true)); _ai.FaceGpuOptionEnabled = hasCpu && hasGpu; if (hasGpu && !hasCpu) { _ai.UseFaceGpu = true; } else if (!hasGpu) { _ai.UseFaceGpu = false; } } private void RefreshNumberAiGpuCapabilities() { if (!TryBuildNumberAiModelConfiguration(out var configuration)) { NumberAiGpuOptionEnabled = false; _ai.UseNumberAiGpu = false; return; } NumberAiGpuOptionEnabled = NumberRecognitionEngine.TryValidateGpuRuntime(configuration, _logger, out _); if (!NumberAiGpuOptionEnabled) { _ai.UseNumberAiGpu = false; } } private void SetUseNumberAiGpu(bool value) { if (!NumberAiGpuOptionEnabled) { _ai.UseNumberAiGpu = false; return; } _ai.UseNumberAiGpu = value; } private bool TryBuildNumberAiModelConfiguration(out ModelConfiguration configuration) { configuration = null!; if (string.IsNullOrWhiteSpace(ModelsFolderPath)) { return false; } var modelsRoot = Path.GetFullPath(ModelsFolderPath.Trim().Trim('"')); if (!Directory.Exists(modelsRoot)) { return false; } configuration = new ModelConfiguration { DetectionCfg = Path.Combine(modelsRoot, "detection.cfg"), DetectionWeights = Path.Combine(modelsRoot, "detection.weights"), RecognitionCfg = Path.Combine(modelsRoot, "recognition.cfg"), RecognitionWeights = Path.Combine(modelsRoot, "recognition.weights"), UseGpu = true }; return File.Exists(configuration.DetectionCfg) && File.Exists(configuration.DetectionWeights) && File.Exists(configuration.RecognitionCfg) && File.Exists(configuration.RecognitionWeights); } private Task ShowErrorMessageAsync(string title, string message) { return InvokeOnUiThreadAsync(() => { ShowMessageRequested?.Invoke(this, Tuple.Create(title, message, 0)); }); } private void SetUseFaceGpu(bool value) { var currentValue = _ai.UseFaceGpu; if (!FaceGpuOptionEnabled) { return; } if (currentValue == value) { return; } _ai.UseFaceGpu = value; var previousRecommendedUpsample = GetRecommendedFaceUpsample(currentValue); if (_ai.FaceUpsample == previousRecommendedUpsample) { _ai.FaceUpsample = GetRecommendedFaceUpsample(value); } } private void TrackFaceEncoderProcess(Process process) { lock (_faceEncoderProcessLock) { _faceEncoderProcess = process; } } private void ClearTrackedFaceEncoderProcess() { lock (_faceEncoderProcessLock) { if (_faceEncoderProcess is not null && _faceEncoderProcess.HasExited) { _faceEncoderProcess = null; return; } _faceEncoderProcess = null; } } private Process? GetTrackedFaceEncoderProcess() { lock (_faceEncoderProcessLock) { if (_faceEncoderProcess is null) { return null; } if (_faceEncoderProcess.HasExited) { _faceEncoderProcess = null; return null; } return _faceEncoderProcess; } } private Process? FindConfiguredFaceEncoderProcess() { var configuredExecutablePath = ResolveConfiguredFaceEncoderExecutablePath(FaceExecutablePath, UseFaceGpu); if (string.IsNullOrWhiteSpace(configuredExecutablePath)) { return null; } var processName = Path.GetFileNameWithoutExtension(configuredExecutablePath); foreach (var process in Process.GetProcessesByName(processName)) { if (!IsProcessAlive(process)) { process.Dispose(); continue; } if (IsMatchingProcessPath(process, configuredExecutablePath)) { return process; } process.Dispose(); } return null; } private bool ComputeIsFaceEncoderRunning() { var trackedProcess = GetTrackedFaceEncoderProcess(); if (trackedProcess is not null) { return true; } if (!_hasStartedFaceEncoderInSession) { return false; } using var discoveredProcess = FindConfiguredFaceEncoderProcess(); return discoveredProcess is not null; } private static bool IsProcessAlive(Process process) { try { return !process.HasExited; } catch { return false; } } private static bool IsMatchingProcessPath(Process process, string configuredExecutablePath) { try { var processPath = process.MainModule?.FileName; return !string.IsNullOrWhiteSpace(processPath) && string.Equals(Path.GetFullPath(processPath), Path.GetFullPath(configuredExecutablePath), StringComparison.OrdinalIgnoreCase); } catch { return false; } } private bool TryRequestFaceEncoderStop(Process process) { if (!IsProcessAlive(process)) { return true; } #if WINDOWS if (Program.TrySendConsoleInterrupt(process.Id)) { return true; } #endif try { return process.CloseMainWindow(); } catch (Exception ex) { _logger.LogWarning(ex, "Unable to request graceful stop for face encoder process {ProcessId}", process.Id); return false; } } private static async Task WaitForProcessExitAsync(Process process, TimeSpan timeout) { if (!IsProcessAlive(process)) { return true; } using var cancellationTokenSource = new CancellationTokenSource(timeout); try { await process.WaitForExitAsync(cancellationTokenSource.Token).ConfigureAwait(false); return true; } catch (OperationCanceledException) { return !IsProcessAlive(process); } } private void AppendFaceProcessOutput(StringBuilder builder, StringBuilder transcriptBuilder, string? line, bool isError) { if (string.IsNullOrWhiteSpace(line)) { return; } lock (builder) { builder.AppendLine(line); } if (isError) { Console.Error.WriteLine(line); } else { Console.WriteLine(line); } string transcript; lock (transcriptBuilder) { if (isError) { transcriptBuilder.Append("[stderr] "); } transcriptBuilder.AppendLine(line); transcript = transcriptBuilder.ToString(); } _ = InvokeOnUiThreadAsync(() => FaceCommandOutput = transcript); } internal static string? ResolveConfiguredFaceEncoderExecutablePath(string configuredPath, bool useGpu) { var variant = useGpu ? "gpu" : "cpu"; foreach (var candidate in EnumerateFaceEncoderExecutableCandidates(configuredPath, variant)) { if (File.Exists(candidate)) { return candidate; } } return null; } internal static (string OutputFilePath, string LogFilePath) BuildFaceEncoderOutputPaths(string outputFolderPath, string imagesFolderPath, DateTime timestamp) { var safeRaceName = BuildSafeFaceEncoderRaceName(imagesFolderPath); var timestampToken = timestamp.ToString("yyyyMMdd_HHmmss"); return ( Path.Combine(outputFolderPath, $"face_encodings_{timestampToken}_{safeRaceName}.pkl"), Path.Combine(outputFolderPath, $"encoder_log_{timestampToken}_{safeRaceName}.txt")); } internal static string BuildSafeFaceEncoderRaceName(string imagesFolderPath) { var raceName = new DirectoryInfo(imagesFolderPath).Name; if (string.IsNullOrWhiteSpace(raceName)) { return "race"; } var invalidChars = Path.GetInvalidFileNameChars(); var builder = new StringBuilder(raceName.Length); var previousWasSeparator = false; foreach (var currentChar in raceName) { if (char.IsWhiteSpace(currentChar) || invalidChars.Contains(currentChar)) { if (!previousWasSeparator) { builder.Append('_'); previousWasSeparator = true; } continue; } builder.Append(currentChar); previousWasSeparator = false; } return builder.ToString().Trim('_') switch { "" => "race", var sanitized => sanitized }; } private static IEnumerable EnumerateFaceEncoderExecutableCandidates(string configuredPath, string variant) { var normalizedPath = NormalizeFilePathArgument(configuredPath); if (string.IsNullOrWhiteSpace(normalizedPath)) { yield break; } var executableName = $"face_encoder_{variant}.exe"; if (File.Exists(normalizedPath)) { var fileDirectory = Path.GetDirectoryName(normalizedPath); if (!string.IsNullOrWhiteSpace(fileDirectory)) { yield return Path.Combine(fileDirectory, executableName); var parentDirectory = Directory.GetParent(fileDirectory)?.FullName; if (!string.IsNullOrWhiteSpace(parentDirectory)) { yield return Path.Combine(parentDirectory, $"face_encoder_{variant}", executableName); } } yield return normalizedPath; yield break; } yield return Path.Combine(normalizedPath, executableName); yield return Path.Combine(normalizedPath, $"face_encoder_{variant}", executableName); var leafName = Path.GetFileName(normalizedPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)); if (leafName.Equals("face_encoder_cpu", StringComparison.OrdinalIgnoreCase) || leafName.Equals("face_encoder_gpu", StringComparison.OrdinalIgnoreCase)) { var parentDirectory = Directory.GetParent(normalizedPath)?.FullName; if (!string.IsNullOrWhiteSpace(parentDirectory)) { yield return Path.Combine(parentDirectory, $"face_encoder_{variant}", executableName); } } } private static int NormalizeFaceParallelism(int value) { return value is >= 1 and <= 5 ? value : 3; } private static int NormalizeNumberAiWorkloadLevel(int value) { return value is >= 1 and <= 5 ? value : 3; } private static int ResolveNumberAiWorkerCount(bool useGpu, int workloadLevel) { var normalized = NormalizeNumberAiWorkloadLevel(workloadLevel); var maxWorkers = Math.Max(1, Environment.ProcessorCount); var requestedWorkers = useGpu ? normalized switch { 1 => 1, 2 => 2, 3 => 4, 4 => 6, _ => 8 } : normalized switch { 1 => 1, 2 => 2, 3 => 3, 4 => 4, _ => 5 }; return Math.Min(requestedWorkers, maxWorkers); } private static int NormalizeFaceMinSize(int value) { return value > 0 ? value : 35; } private static bool GetRecommendedFaceUpsample(bool useGpu) { return !useGpu; } private static string BuildFaceEncoderSummary( int exitCode, ProcessStartInfo processStartInfo, string outputFilePath, string logFilePath, StringBuilder outputLines, StringBuilder errorLines) { var summary = new StringBuilder(); summary.AppendLine($"Exit code: {exitCode}"); summary.AppendLine($"Command: {processStartInfo.FileName} {string.Join(" ", processStartInfo.ArgumentList)}"); summary.AppendLine($"Output file: {outputFilePath}"); summary.AppendLine($"Log file: {logFilePath}"); lock (outputLines) { if (outputLines.Length > 0) { summary.AppendLine(); summary.AppendLine("STDOUT:"); summary.Append(outputLines); } } lock (errorLines) { if (errorLines.Length > 0) { summary.AppendLine(); summary.AppendLine("STDERR:"); summary.Append(errorLines); } } return summary.ToString(); } private static string 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('"'); } // Note: These commands will trigger events that the View will handle to show dialogs // since dialogs require UI context public event EventHandler? SelectSourceFolderRequested; public event EventHandler? SelectDestinationFolderRequested; public event EventHandler? SelectLogoFileRequested; public event EventHandler? SelectModelsFolderRequested; public event EventHandler? SelectCsvOutputRequested; public event EventHandler? SaveSettingsRequested; public event EventHandler? LoadSettingsRequested; public event EventHandler? SelectColorRequested; // Request that the View shows a message to the user (message, caption, icon) public event EventHandler>? ShowMessageRequested; public event EventHandler? SelectTransparentColorRequested; private void SelectSourceFolder(object parameter) { SelectSourceFolderRequested?.Invoke(this, EventArgs.Empty); } private void SelectDestinationFolder(object parameter) { SelectDestinationFolderRequested?.Invoke(this, EventArgs.Empty); } private void SelectLogoFile(object parameter) { SelectLogoFileRequested?.Invoke(this, EventArgs.Empty); } private void SelectModelsFolder(object parameter) { SelectModelsFolderRequested?.Invoke(this, EventArgs.Empty); } private void SelectCsvOutput(object parameter) { SelectCsvOutputRequested?.Invoke(this, EventArgs.Empty); } private void SaveSettings(object parameter) { SaveSettingsRequested?.Invoke(this, string.Empty); } private void LoadSettings(object parameter) { LoadSettingsRequested?.Invoke(this, string.Empty); } private void SelectColor(object parameter) { SelectColorRequested?.Invoke(this, EventArgs.Empty); } private void SelectTransparentColor(object parameter) { SelectTransparentColorRequested?.Invoke(this, EventArgs.Empty); } public async Task SaveSettingsToFileAsync(string filePath) { await _settingsService.SaveSettingsAsync(filePath, this); } public async Task LoadSettingsFromFileAsync(string filePath) { await _settingsService.LoadSettingsAsync(filePath, this); } private string _appVersion = string.Empty; public string AppVersion { get => _appVersion; set { _appVersion = value; NotifyPropertyChanged(); } } } }