feat: Implement face encoder functionality with GPU support and recursive option
This commit is contained in:
parent
daf3b5ad2c
commit
988a3d94e1
10 changed files with 790 additions and 219 deletions
|
|
@ -14,15 +14,15 @@ env:
|
|||
PROJECT_PATH: imagecatalog/ImageCatalog 2.csproj
|
||||
PUBLISH_DIR: artifacts/publish/win-x64
|
||||
ARTIFACT_NAME: imagecatalog-windows-avalonia
|
||||
NUGET_SOURCE_NAME: Nuget-GitLab-AIFotoONLUS
|
||||
NUGET_SOURCE_URL: https://gitlab.com/api/v4/projects/79509532/packages/nuget/index.json
|
||||
NUGET_SOURCE_NAME: Nuget-Forgejo-AIFotoONLUS
|
||||
NUGET_SOURCE_URL: ${{ vars.AIFOTOONLUS_NUGET_SOURCE_URL || format('{0}/api/packages/{1}/nuget/index.json', github.server_url, vars.AIFOTOONLUS_PACKAGE_OWNER || github.repository_owner) }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: docker
|
||||
env:
|
||||
NUGET_USERNAME: ${{ secrets.NUGET_USERNAME }}
|
||||
NUGET_PASSWORD: ${{ secrets.NUGET_PASSWORD }}
|
||||
FORGEJO_PACKAGE_USERNAME: ${{ secrets.FORGEJO_PACKAGE_USERNAME }}
|
||||
FORGEJO_PACKAGE_TOKEN: ${{ secrets.FORGEJO_PACKAGE_TOKEN }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
|
@ -36,12 +36,12 @@ jobs:
|
|||
- name: Validate NuGet secrets
|
||||
run: |
|
||||
set -eu
|
||||
if [ -z "${NUGET_USERNAME}" ]; then
|
||||
echo "secrets.NUGET_USERNAME is required"
|
||||
if [ -z "${FORGEJO_PACKAGE_USERNAME}" ]; then
|
||||
echo "secrets.FORGEJO_PACKAGE_USERNAME is required"
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "${NUGET_PASSWORD}" ]; then
|
||||
echo "secrets.NUGET_PASSWORD is required"
|
||||
if [ -z "${FORGEJO_PACKAGE_TOKEN}" ]; then
|
||||
echo "secrets.FORGEJO_PACKAGE_TOKEN is required"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
|
@ -53,8 +53,8 @@ jobs:
|
|||
|
||||
dotnet nuget update source "${{ env.NUGET_SOURCE_NAME }}" \
|
||||
--source "${{ env.NUGET_SOURCE_URL }}" \
|
||||
--username "${NUGET_USERNAME}" \
|
||||
--password "${NUGET_PASSWORD}" \
|
||||
--username "${FORGEJO_PACKAGE_USERNAME}" \
|
||||
--password "${FORGEJO_PACKAGE_TOKEN}" \
|
||||
--store-password-in-clear-text \
|
||||
--configfile "${temp_config}"
|
||||
|
||||
|
|
|
|||
13
Catalog.code-workspace
Normal file
13
Catalog.code-workspace
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
},
|
||||
{
|
||||
"path": "../AIFotoONLUS"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"commentTranslate.hover.enabled": false
|
||||
}
|
||||
}
|
||||
14
NuGet.Config
14
NuGet.Config
|
|
@ -1,25 +1,25 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Repository-level NuGet.Config to control which sources are queried for which
|
||||
packages. This prevents the GitLab feed from being queried for all package
|
||||
packages. This prevents the Forgejo feed from being queried for all package
|
||||
IDs (which causes 401 errors for packages hosted on nuget.org).
|
||||
|
||||
Usage:
|
||||
- Keep this file in the repository root so `dotnet restore` picks it up by default.
|
||||
- CI still needs to add the `GitLab` source credentials at runtime (we do this
|
||||
- CI still needs to add the `Forgejo` source credentials at runtime (we do this
|
||||
from the pipeline using `dotnet nuget add source ...`), but the mapping below
|
||||
ensures only the listed package ID patterns are requested from the GitLab feed.
|
||||
ensures only the listed package ID patterns are requested from the Forgejo feed.
|
||||
-->
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||
<add key="Nuget-GitLab-AIFotoONLUS" value="https://gitlab.com/api/v4/projects/79509532/packages/nuget/index.json" />
|
||||
<add key="Nuget-Forgejo-AIFotoONLUS" value="https://forgejo.maddoscientisto.net/api/packages/maddo/nuget/index.json" />
|
||||
</packageSources>
|
||||
|
||||
<!-- Map private package IDs to the GitLab source; everything else uses nuget.org -->
|
||||
<!-- Map private package IDs to the Forgejo source; everything else uses nuget.org -->
|
||||
<packageSourceMapping>
|
||||
<packageSource key="Nuget-GitLab-AIFotoONLUS">
|
||||
<!-- Add patterns for your private packages hosted in GitLab -->
|
||||
<packageSource key="Nuget-Forgejo-AIFotoONLUS">
|
||||
<!-- Add patterns for your private packages hosted in Forgejo -->
|
||||
<package pattern="AIFotoONLUS.*" />
|
||||
<package pattern="AIFotoONLUS.Core" />
|
||||
</packageSource>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using Avalonia.Interactivity;
|
|||
using Avalonia.Platform.Storage;
|
||||
using Avalonia.Styling;
|
||||
using Avalonia.Threading;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
|
||||
namespace ImageCatalog_2;
|
||||
|
|
@ -20,6 +21,7 @@ public partial class AvaloniaMainWindow : Window
|
|||
DataContext = _model;
|
||||
|
||||
Opened += (_, _) => SyncThemeStateFromCurrentTheme();
|
||||
Closing += AvaloniaMainWindow_Closing;
|
||||
|
||||
// Let DataModel marshal callbacks onto Avalonia UI thread.
|
||||
_model.UiInvoker = action => Dispatcher.UIThread.Invoke(action);
|
||||
|
|
@ -135,6 +137,29 @@ public partial class AvaloniaMainWindow : Window
|
|||
};
|
||||
}
|
||||
|
||||
private bool _isStoppingFaceEncoderForClose;
|
||||
|
||||
private async void AvaloniaMainWindow_Closing(object? sender, CancelEventArgs e)
|
||||
{
|
||||
if (_isStoppingFaceEncoderForClose || !_model.IsFaceEncoderRunning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
e.Cancel = true;
|
||||
_isStoppingFaceEncoderForClose = true;
|
||||
|
||||
try
|
||||
{
|
||||
await _model.StopFaceEncoderAsync("Arresto face encoder in chiusura...", waitForExit: true);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isStoppingFaceEncoderForClose = false;
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
private void ToggleTheme_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_isDarkTheme = !_isDarkTheme;
|
||||
|
|
|
|||
|
|
@ -5,13 +5,13 @@
|
|||
<ScrollViewer>
|
||||
<StackPanel Margin="4" Spacing="6">
|
||||
<TextBlock Text="Face Recognition Encoder" FontWeight="Bold" />
|
||||
<TextBlock Text="Esegue face_encoder.exe usando la cartella Destinazione corrente come --images e un file .pkl come --out."
|
||||
<TextBlock Text="Esegue il face encoder usando la cartella Destinazione corrente come --images e un file .pkl come --out."
|
||||
TextWrapping="Wrap" Opacity="0.8" />
|
||||
|
||||
<TextBlock Text="Eseguibile" FontWeight="Bold" Margin="0,4,0,0" />
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="6">
|
||||
<TextBlock Grid.Column="0" Text="face_encoder:" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Column="1" Name="FaceExecutablePathTextBox" Text="{Binding FaceExecutablePath, Mode=TwoWay}" Watermark="C:\\tools\\face_encoder.exe" />
|
||||
<TextBox Grid.Column="1" Name="FaceExecutablePathTextBox" Text="{Binding FaceExecutablePath, Mode=TwoWay}" Watermark="C:\\tools\\face_encoder_cpu.exe" />
|
||||
<Button Grid.Column="2" Name="FaceSelectExecutableButton" Click="SelectFaceExecutable_Click" Width="104" Margin="6,0,0,0">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
|
||||
<iconPacks:PackIconMaterial Kind="FileOutline" Width="14" Height="14" />
|
||||
|
|
@ -26,6 +26,16 @@
|
|||
</Button>
|
||||
</Grid>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="12">
|
||||
<CheckBox Content="Ricorsivo (--recursive)" IsChecked="{Binding FaceRecursive, Mode=TwoWay}" />
|
||||
<CheckBox Content="Usa GPU"
|
||||
IsChecked="{Binding UseFaceGpu, Mode=TwoWay}"
|
||||
IsEnabled="{Binding FaceGpuOptionEnabled}" />
|
||||
</StackPanel>
|
||||
<TextBlock Text="Se l'eseguibile termina con _cpu o _gpu, il checkbox GPU cambia automaticamente il file usato. Se non c'e il suffisso, viene trattato come CPU."
|
||||
TextWrapping="Wrap"
|
||||
Opacity="0.75" />
|
||||
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto" ColumnSpacing="6">
|
||||
<TextBlock Grid.Column="0" Text="Sorgente:" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Column="1" Name="FaceDestinationPathTextBox" Text="{Binding DestinationPath, Mode=OneWay}" IsReadOnly="True" />
|
||||
|
|
@ -56,12 +66,14 @@
|
|||
</Grid>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="6" Margin="0,6,0,0">
|
||||
<Button Name="FaceRunButton" Content="Esegui Face Encoder" Click="RunFaceEncoder_Click" />
|
||||
<TextBlock Name="FaceStatusTextBlock" VerticalAlignment="Center" />
|
||||
<Button Name="FaceRunButton" Content="Esegui Face Encoder" Command="{Binding StartFaceEncoderCommand}" />
|
||||
<Button Content="Stop" Command="{Binding StopFaceEncoderCommand}" />
|
||||
<TextBlock VerticalAlignment="Center" Text="{Binding FaceStatusMessage}" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Text="Output comando" FontWeight="Bold" Margin="0,6,0,0" />
|
||||
<TextBox Name="FaceOutputTextBox"
|
||||
Text="{Binding FaceCommandOutput}"
|
||||
IsReadOnly="True"
|
||||
AcceptsReturn="True"
|
||||
TextWrapping="Wrap"
|
||||
|
|
|
|||
|
|
@ -1,25 +1,17 @@
|
|||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Platform.Storage;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ImageCatalog_2.AvaloniaViews;
|
||||
|
||||
public partial class FaceAiTabView : Avalonia.Controls.UserControl
|
||||
{
|
||||
private readonly ILogger<FaceAiTabView> _logger;
|
||||
|
||||
public FaceAiTabView()
|
||||
{
|
||||
InitializeComponent();
|
||||
_logger = Program.ServiceProvider.GetService(typeof(ILogger<FaceAiTabView>)) as ILogger<FaceAiTabView>
|
||||
?? NullLogger<FaceAiTabView>.Instance;
|
||||
}
|
||||
|
||||
private async void SelectFaceExecutable_Click(object? sender, RoutedEventArgs e)
|
||||
|
|
@ -39,7 +31,7 @@ public partial class FaceAiTabView : Avalonia.Controls.UserControl
|
|||
|
||||
var files = await storageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
||||
{
|
||||
Title = "Seleziona face_encoder.exe",
|
||||
Title = "Seleziona face_encoder_cpu.exe o face_encoder_gpu.exe",
|
||||
FileTypeFilter =
|
||||
[
|
||||
new FilePickerFileType("Eseguibile") { Patterns = ["*.exe"] },
|
||||
|
|
@ -160,167 +152,6 @@ public partial class FaceAiTabView : Avalonia.Controls.UserControl
|
|||
OpenInExplorer(string.IsNullOrWhiteSpace(directory) ? path : directory);
|
||||
}
|
||||
|
||||
private async void RunFaceEncoder_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var executableBox = this.FindControl<Avalonia.Controls.TextBox>("FaceExecutablePathTextBox");
|
||||
var outputFolderBox = this.FindControl<Avalonia.Controls.TextBox>("FaceOutputFolderTextBox");
|
||||
var outputLogBox = this.FindControl<Avalonia.Controls.TextBox>("FaceOutputTextBox");
|
||||
var statusBlock = this.FindControl<TextBlock>("FaceStatusTextBlock");
|
||||
var runButton = this.FindControl<Avalonia.Controls.Button>("FaceRunButton");
|
||||
|
||||
if (executableBox is null || outputFolderBox is null || outputLogBox is null || statusBlock is null || runButton is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (DataContext is not DataModel model)
|
||||
{
|
||||
statusBlock.Text = "DataContext non valido.";
|
||||
return;
|
||||
}
|
||||
|
||||
var executablePath = executableBox.Text?.Trim().Trim('"') ?? string.Empty;
|
||||
var outputFilePath = outputFolderBox.Text?.Trim().Trim('"') ?? string.Empty;
|
||||
var imagesFolder = (model.DestinationPath ?? string.Empty).Trim().Trim('"');
|
||||
|
||||
model.FaceExecutablePath = executablePath;
|
||||
model.FaceOutputFolderPath = outputFilePath;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(executablePath) || !File.Exists(executablePath))
|
||||
{
|
||||
statusBlock.Text = "Percorso eseguibile non valido.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(imagesFolder) || !Directory.Exists(imagesFolder))
|
||||
{
|
||||
statusBlock.Text = "Cartella Destinazione non valida.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(outputFilePath))
|
||||
{
|
||||
statusBlock.Text = "Inserisci il file di output .pkl.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.Equals(Path.GetExtension(outputFilePath), ".pkl", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
statusBlock.Text = "Il file di output deve avere estensione .pkl.";
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var outputDirectory = Path.GetDirectoryName(outputFilePath);
|
||||
if (!string.IsNullOrWhiteSpace(outputDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(outputDirectory);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Unable to create face output directory for file: {OutputFilePath}", outputFilePath);
|
||||
statusBlock.Text = "Impossibile creare la cartella del file di output.";
|
||||
return;
|
||||
}
|
||||
|
||||
runButton.IsEnabled = false;
|
||||
statusBlock.Text = "Esecuzione face encoder in corso...";
|
||||
outputLogBox.Text = string.Empty;
|
||||
|
||||
var outputLines = new StringBuilder();
|
||||
var errorLines = new StringBuilder();
|
||||
|
||||
try
|
||||
{
|
||||
var imagesFolderArg = NormalizeDirectoryPathArgument(imagesFolder);
|
||||
var outputFileArg = NormalizeFilePathArgument(outputFilePath);
|
||||
|
||||
var processStartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = executablePath,
|
||||
WorkingDirectory = Path.GetDirectoryName(executablePath) ?? Environment.CurrentDirectory,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true,
|
||||
};
|
||||
processStartInfo.ArgumentList.Add("--images");
|
||||
processStartInfo.ArgumentList.Add(imagesFolderArg);
|
||||
processStartInfo.ArgumentList.Add("--out");
|
||||
processStartInfo.ArgumentList.Add(outputFileArg);
|
||||
|
||||
using var process = new Process { StartInfo = processStartInfo, EnableRaisingEvents = true };
|
||||
process.OutputDataReceived += (_, args) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(args.Data))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (outputLines)
|
||||
{
|
||||
outputLines.AppendLine(args.Data);
|
||||
}
|
||||
};
|
||||
|
||||
process.ErrorDataReceived += (_, args) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(args.Data))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (errorLines)
|
||||
{
|
||||
errorLines.AppendLine(args.Data);
|
||||
}
|
||||
};
|
||||
|
||||
if (!process.Start())
|
||||
{
|
||||
throw new InvalidOperationException("Avvio face_encoder.exe fallito.");
|
||||
}
|
||||
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
await process.WaitForExitAsync().ConfigureAwait(true);
|
||||
|
||||
var summary = new StringBuilder();
|
||||
summary.AppendLine($"Exit code: {process.ExitCode}");
|
||||
|
||||
if (outputLines.Length > 0)
|
||||
{
|
||||
summary.AppendLine();
|
||||
summary.AppendLine("STDOUT:");
|
||||
summary.Append(outputLines);
|
||||
}
|
||||
|
||||
if (errorLines.Length > 0)
|
||||
{
|
||||
summary.AppendLine();
|
||||
summary.AppendLine("STDERR:");
|
||||
summary.Append(errorLines);
|
||||
}
|
||||
|
||||
outputLogBox.Text = summary.ToString();
|
||||
statusBlock.Text = process.ExitCode == 0
|
||||
? "Face encoder completato."
|
||||
: $"Face encoder terminato con errore (code {process.ExitCode}).";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Face encoder execution failed.");
|
||||
outputLogBox.Text = ex.ToString();
|
||||
statusBlock.Text = "Errore durante esecuzione face encoder.";
|
||||
}
|
||||
finally
|
||||
{
|
||||
runButton.IsEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static void OpenInExplorer(string? path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
|
|
@ -345,31 +176,4 @@ public partial class FaceAiTabView : Avalonia.Controls.UserControl
|
|||
// Ignore failures when opening Explorer.
|
||||
}
|
||||
}
|
||||
|
||||
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('"');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ using System.Diagnostics;
|
|||
#if WINDOWS
|
||||
using System.Drawing.Text;
|
||||
#endif
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
|
@ -40,6 +41,8 @@ namespace ImageCatalog_2
|
|||
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<DataModel> _logger;
|
||||
|
|
@ -54,6 +57,14 @@ namespace ImageCatalog_2
|
|||
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 bool _hasStartedFaceEncoderInSession;
|
||||
private bool _isUpdatingFaceExecutableSelection;
|
||||
|
||||
// ComboBox collections
|
||||
public List<string> AvailableFonts { get; }
|
||||
|
|
@ -93,6 +104,10 @@ namespace ImageCatalog_2
|
|||
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);
|
||||
|
|
@ -104,6 +119,7 @@ namespace ImageCatalog_2
|
|||
|
||||
// Load available fonts
|
||||
AvailableFonts = LoadAvailableFonts();
|
||||
RefreshFaceExecutableCapabilities();
|
||||
}
|
||||
|
||||
private async Task StartAiAsync()
|
||||
|
|
@ -192,7 +208,18 @@ namespace ImageCatalog_2
|
|||
public string FaceExecutablePath
|
||||
{
|
||||
get => _ai.FaceExecutablePath;
|
||||
set => _ai.FaceExecutablePath = value;
|
||||
set
|
||||
{
|
||||
var normalizedValue = value ?? string.Empty;
|
||||
if (string.Equals(_ai.FaceExecutablePath, normalizedValue, StringComparison.Ordinal))
|
||||
{
|
||||
RefreshFaceExecutableCapabilities();
|
||||
return;
|
||||
}
|
||||
|
||||
_ai.FaceExecutablePath = normalizedValue;
|
||||
RefreshFaceExecutableCapabilities();
|
||||
}
|
||||
}
|
||||
|
||||
public string FaceOutputFolderPath
|
||||
|
|
@ -201,6 +228,38 @@ namespace ImageCatalog_2
|
|||
set => _ai.FaceOutputFolderPath = value;
|
||||
}
|
||||
|
||||
public bool FaceRecursive
|
||||
{
|
||||
get => _ai.FaceRecursive;
|
||||
set => _ai.FaceRecursive = 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
|
||||
{
|
||||
|
|
@ -393,6 +452,7 @@ namespace ImageCatalog_2
|
|||
}
|
||||
|
||||
NotifyPropertyChanged(e.PropertyName);
|
||||
UpdateFaceEncoderCommandStates();
|
||||
}
|
||||
|
||||
private void OnRaceUploadPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
|
|
@ -1272,6 +1332,549 @@ namespace ImageCatalog_2
|
|||
}
|
||||
}
|
||||
|
||||
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 executablePath = NormalizeFilePathArgument(FaceExecutablePath);
|
||||
var outputFilePath = NormalizeFilePathArgument(FaceOutputFolderPath);
|
||||
var imagesFolder = NormalizeDirectoryPathArgument(DestinationPath);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(executablePath) || !File.Exists(executablePath))
|
||||
{
|
||||
FaceStatusMessage = "Percorso eseguibile non valido.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(imagesFolder) || !Directory.Exists(imagesFolder))
|
||||
{
|
||||
FaceStatusMessage = "Cartella Destinazione non valida.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(outputFilePath))
|
||||
{
|
||||
FaceStatusMessage = "Inserisci il file di output .pkl.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.Equals(Path.GetExtension(outputFilePath), ".pkl", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
FaceStatusMessage = "Il file di output deve avere estensione .pkl.";
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var outputDirectory = Path.GetDirectoryName(outputFilePath);
|
||||
if (!string.IsNullOrWhiteSpace(outputDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(outputDirectory);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Unable to create face output directory for file: {OutputFilePath}", outputFilePath);
|
||||
FaceStatusMessage = "Impossibile creare la cartella del file di output.";
|
||||
return;
|
||||
}
|
||||
|
||||
FaceExecutablePath = executablePath;
|
||||
FaceOutputFolderPath = outputFilePath;
|
||||
FaceCommandOutput = string.Empty;
|
||||
FaceStatusMessage = "Esecuzione face encoder in corso...";
|
||||
|
||||
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 = true,
|
||||
CreateNoWindow = false,
|
||||
};
|
||||
|
||||
processStartInfo.ArgumentList.Add("--images");
|
||||
processStartInfo.ArgumentList.Add(imagesFolder);
|
||||
processStartInfo.ArgumentList.Add("--out");
|
||||
processStartInfo.ArgumentList.Add(outputFilePath);
|
||||
|
||||
if (FaceRecursive)
|
||||
{
|
||||
processStartInfo.ArgumentList.Add("--recursive");
|
||||
}
|
||||
|
||||
using var process = new Process { StartInfo = processStartInfo, EnableRaisingEvents = true };
|
||||
process.OutputDataReceived += (_, args) => AppendFaceProcessOutput(outputLines, args.Data);
|
||||
process.ErrorDataReceived += (_, args) => AppendFaceProcessOutput(errorLines, args.Data);
|
||||
|
||||
if (!process.Start())
|
||||
{
|
||||
throw new InvalidOperationException("Avvio face encoder fallito.");
|
||||
}
|
||||
|
||||
_hasStartedFaceEncoderInSession = true;
|
||||
EnsureFaceEncoderWatcherStarted();
|
||||
TrackFaceEncoderProcess(process);
|
||||
await InvokeOnUiThreadAsync(() => IsFaceEncoderRunning = true).ConfigureAwait(false);
|
||||
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
await process.WaitForExitAsync().ConfigureAwait(false);
|
||||
|
||||
var summary = BuildFaceEncoderSummary(process.ExitCode, outputLines, errorLines);
|
||||
await InvokeOnUiThreadAsync(() =>
|
||||
{
|
||||
FaceCommandOutput = summary;
|
||||
FaceStatusMessage = process.ExitCode == 0
|
||||
? "Face encoder completato."
|
||||
: $"Face encoder terminato con errore (code {process.ExitCode}).";
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Face encoder execution failed.");
|
||||
await InvokeOnUiThreadAsync(() =>
|
||||
{
|
||||
FaceCommandOutput = ex.ToString();
|
||||
FaceStatusMessage = "Errore durante esecuzione face encoder.";
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
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 RefreshFaceExecutableCapabilities()
|
||||
{
|
||||
if (_isUpdatingFaceExecutableSelection)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var executableName = Path.GetFileNameWithoutExtension(NormalizeFilePathArgument(_ai.FaceExecutablePath));
|
||||
var supportsGpu = executableName.EndsWith("_cpu", StringComparison.OrdinalIgnoreCase)
|
||||
|| executableName.EndsWith("_gpu", StringComparison.OrdinalIgnoreCase);
|
||||
var useGpu = supportsGpu && executableName.EndsWith("_gpu", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
_ai.FaceGpuOptionEnabled = supportsGpu;
|
||||
_ai.UseFaceGpu = supportsGpu && useGpu;
|
||||
}
|
||||
|
||||
private void SetUseFaceGpu(bool value)
|
||||
{
|
||||
if (!FaceGpuOptionEnabled)
|
||||
{
|
||||
if (_ai.UseFaceGpu)
|
||||
{
|
||||
_ai.UseFaceGpu = false;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (_ai.UseFaceGpu == value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_ai.UseFaceGpu = value;
|
||||
|
||||
var currentPath = NormalizeFilePathArgument(_ai.FaceExecutablePath);
|
||||
if (string.IsNullOrWhiteSpace(currentPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var extension = Path.GetExtension(currentPath);
|
||||
var fileName = Path.GetFileNameWithoutExtension(currentPath);
|
||||
var baseName = fileName.EndsWith("_cpu", StringComparison.OrdinalIgnoreCase)
|
||||
? fileName[..^4]
|
||||
: fileName.EndsWith("_gpu", StringComparison.OrdinalIgnoreCase)
|
||||
? fileName[..^4]
|
||||
: fileName;
|
||||
|
||||
if (string.Equals(baseName, fileName, StringComparison.Ordinal))
|
||||
{
|
||||
_ai.UseFaceGpu = false;
|
||||
_ai.FaceGpuOptionEnabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var updatedFileName = $"{baseName}{(value ? "_gpu" : "_cpu")}{extension}";
|
||||
var directory = Path.GetDirectoryName(currentPath);
|
||||
var updatedPath = string.IsNullOrWhiteSpace(directory)
|
||||
? updatedFileName
|
||||
: Path.Combine(directory, updatedFileName);
|
||||
|
||||
if (string.Equals(updatedPath, currentPath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isUpdatingFaceExecutableSelection = true;
|
||||
try
|
||||
{
|
||||
_ai.FaceExecutablePath = updatedPath;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isUpdatingFaceExecutableSelection = false;
|
||||
}
|
||||
}
|
||||
|
||||
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 = NormalizeFilePathArgument(FaceExecutablePath);
|
||||
if (string.IsNullOrWhiteSpace(configuredExecutablePath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var processName = Path.GetFileNameWithoutExtension(configuredExecutablePath);
|
||||
foreach (var process in Process.GetProcessesByName(processName))
|
||||
{
|
||||
if (!IsProcessAlive(process))
|
||||
{
|
||||
process.Dispose();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsMatchingProcessPath(process, configuredExecutablePath))
|
||||
{
|
||||
return process;
|
||||
}
|
||||
|
||||
process.Dispose();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool ComputeIsFaceEncoderRunning()
|
||||
{
|
||||
var trackedProcess = GetTrackedFaceEncoderProcess();
|
||||
if (trackedProcess is not null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!_hasStartedFaceEncoderInSession)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
using var discoveredProcess = FindConfiguredFaceEncoderProcess();
|
||||
return discoveredProcess is not null;
|
||||
}
|
||||
|
||||
private 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<bool> WaitForProcessExitAsync(Process process, TimeSpan timeout)
|
||||
{
|
||||
if (!IsProcessAlive(process))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
using var cancellationTokenSource = new CancellationTokenSource(timeout);
|
||||
try
|
||||
{
|
||||
await process.WaitForExitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return !IsProcessAlive(process);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AppendFaceProcessOutput(StringBuilder builder, string? line)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (builder)
|
||||
{
|
||||
builder.AppendLine(line);
|
||||
}
|
||||
}
|
||||
|
||||
private static string BuildFaceEncoderSummary(int exitCode, StringBuilder outputLines, StringBuilder errorLines)
|
||||
{
|
||||
var summary = new StringBuilder();
|
||||
summary.AppendLine($"Exit code: {exitCode}");
|
||||
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -282,6 +282,10 @@ namespace ImageCatalog_2.Models
|
|||
[XmlElement("AI_FaceOutputFolderPath")]
|
||||
public string FaceOutputFolderPath { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("FaceRecursive")]
|
||||
[XmlElement("AI_FaceRecursive")]
|
||||
public bool FaceRecursive { get; set; }
|
||||
|
||||
// Race upload settings
|
||||
[JsonPropertyName("ApiLogin")]
|
||||
[XmlElement("RaceUpload_Login")]
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@ static class Program
|
|||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern bool AllocConsole();
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern bool FreeConsole();
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
static extern IntPtr GetStdHandle(int nStdHandle);
|
||||
|
||||
|
|
@ -36,6 +39,12 @@ static class Program
|
|||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
static extern bool AttachConsole(int dwProcessId);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern bool GenerateConsoleCtrlEvent(uint dwCtrlEvent, uint dwProcessGroupId);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern bool SetConsoleCtrlHandler(ConsoleCtrlDelegate? handlerRoutine, bool add);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
static extern IntPtr CreateFile(
|
||||
string lpFileName,
|
||||
|
|
@ -48,6 +57,9 @@ static class Program
|
|||
|
||||
private const uint GENERIC_WRITE = 0x40000000;
|
||||
private const uint OPEN_EXISTING = 3;
|
||||
private const uint CTRL_C_EVENT = 0;
|
||||
|
||||
private delegate bool ConsoleCtrlDelegate(uint ctrlType);
|
||||
|
||||
private static void RedirectConsoleOutput()
|
||||
{
|
||||
|
|
@ -58,6 +70,38 @@ static class Program
|
|||
Console.SetOut(standardOutput);
|
||||
Console.SetError(standardOutput);
|
||||
}
|
||||
|
||||
internal static bool TrySendConsoleInterrupt(int processId)
|
||||
{
|
||||
var hadConsole = GetConsoleWindow() != IntPtr.Zero;
|
||||
|
||||
try
|
||||
{
|
||||
if (hadConsole)
|
||||
{
|
||||
FreeConsole();
|
||||
}
|
||||
|
||||
if (!AttachConsole(processId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
SetConsoleCtrlHandler(null, true);
|
||||
return GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0);
|
||||
}
|
||||
finally
|
||||
{
|
||||
FreeConsole();
|
||||
SetConsoleCtrlHandler(null, false);
|
||||
|
||||
if (hadConsole)
|
||||
{
|
||||
AllocConsole();
|
||||
RedirectConsoleOutput();
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
public static IServiceProvider ServiceProvider { get; private set; } = default!;
|
||||
|
|
|
|||
|
|
@ -60,6 +60,72 @@ public class AiSettingsViewModel : ViewModelBase
|
|||
}
|
||||
}
|
||||
|
||||
private bool _faceRecursive;
|
||||
public bool FaceRecursive
|
||||
{
|
||||
get => _faceRecursive;
|
||||
set
|
||||
{
|
||||
_faceRecursive = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private bool _faceGpuOptionEnabled;
|
||||
public bool FaceGpuOptionEnabled
|
||||
{
|
||||
get => _faceGpuOptionEnabled;
|
||||
set
|
||||
{
|
||||
_faceGpuOptionEnabled = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private bool _useFaceGpu;
|
||||
public bool UseFaceGpu
|
||||
{
|
||||
get => _useFaceGpu;
|
||||
set
|
||||
{
|
||||
_useFaceGpu = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private bool _isFaceEncoderRunning;
|
||||
public bool IsFaceEncoderRunning
|
||||
{
|
||||
get => _isFaceEncoderRunning;
|
||||
set
|
||||
{
|
||||
_isFaceEncoderRunning = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private string _faceStatusMessage = string.Empty;
|
||||
public string FaceStatusMessage
|
||||
{
|
||||
get => _faceStatusMessage;
|
||||
set
|
||||
{
|
||||
_faceStatusMessage = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private string _faceCommandOutput = string.Empty;
|
||||
public string FaceCommandOutput
|
||||
{
|
||||
get => _faceCommandOutput;
|
||||
set
|
||||
{
|
||||
_faceCommandOutput = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private double _aiProgress;
|
||||
public double AiProgress
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue