Compare commits
No commits in common. "7e105e3738c045d02674f7422b8112cbb076289e" and "25fdb82d2f0c91c2261403a168eb5d668f764bb8" have entirely different histories.
7e105e3738
...
25fdb82d2f
12 changed files with 76 additions and 608 deletions
|
|
@ -109,42 +109,6 @@ public class DataModelCharacterizationTests
|
||||||
model.ModelsFolderPath.ShouldBe("K:/models");
|
model.ModelsFolderPath.ShouldBe("K:/models");
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
|
||||||
public void NumberAiGpuChildChange_RaisesDataModelPropertyChanged()
|
|
||||||
{
|
|
||||||
var model = CreateModel();
|
|
||||||
string? changed = null;
|
|
||||||
model.PropertyChanged += (_, args) => changed = args.PropertyName;
|
|
||||||
|
|
||||||
model.Ai.UseNumberAiGpu = true;
|
|
||||||
|
|
||||||
changed.ShouldBe(nameof(DataModel.UseNumberAiGpu));
|
|
||||||
model.UseNumberAiGpu.ShouldBeTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestMethod]
|
|
||||||
public void NumberAiThumbnails_DefaultsOffAndRaisesDataModelPropertyChanged()
|
|
||||||
{
|
|
||||||
var model = CreateModel();
|
|
||||||
model.IncludeNumberAiThumbnails.ShouldBeFalse();
|
|
||||||
|
|
||||||
string? changed = null;
|
|
||||||
model.PropertyChanged += (_, args) => changed = args.PropertyName;
|
|
||||||
|
|
||||||
model.Ai.IncludeNumberAiThumbnails = true;
|
|
||||||
|
|
||||||
changed.ShouldBe(nameof(DataModel.IncludeNumberAiThumbnails));
|
|
||||||
model.IncludeNumberAiThumbnails.ShouldBeTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestMethod]
|
|
||||||
public void CommandLineOperationRunner_DetectsHeadlessRequest()
|
|
||||||
{
|
|
||||||
CommandLineOperationRunner.IsHeadlessRequest(["--config", "settings.xml", "--operation", "number-ai"]).ShouldBeTrue();
|
|
||||||
CommandLineOperationRunner.IsHeadlessRequest(["--config=settings.xml", "--operation=number-ai"]).ShouldBeTrue();
|
|
||||||
CommandLineOperationRunner.IsHeadlessRequest([]).ShouldBeFalse();
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void RaceUploadChildChange_RaisesDataModelPropertyChanged()
|
public void RaceUploadChildChange_RaisesDataModelPropertyChanged()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
using Avalonia;
|
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Layout;
|
|
||||||
using Avalonia.Platform.Storage;
|
using Avalonia.Platform.Storage;
|
||||||
using Avalonia.Styling;
|
using Avalonia.Styling;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
|
|
@ -137,11 +135,6 @@ public partial class AvaloniaMainWindow : Window
|
||||||
{
|
{
|
||||||
// Color is set by typing hex directly in the TextBox.
|
// Color is set by typing hex directly in the TextBox.
|
||||||
};
|
};
|
||||||
|
|
||||||
_model.ShowMessageRequested += async (_, args) =>
|
|
||||||
{
|
|
||||||
await ShowMessageDialogAsync(args.Item1, args.Item2);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool _isStoppingFaceEncoderForClose;
|
private bool _isStoppingFaceEncoderForClose;
|
||||||
|
|
@ -189,48 +182,4 @@ public partial class AvaloniaMainWindow : Window
|
||||||
{
|
{
|
||||||
_ = this.FindControl<Avalonia.Controls.Button>("ThemeToggleButton");
|
_ = this.FindControl<Avalonia.Controls.Button>("ThemeToggleButton");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ShowMessageDialogAsync(string title, string message)
|
|
||||||
{
|
|
||||||
var dialog = new Window
|
|
||||||
{
|
|
||||||
Title = title,
|
|
||||||
Width = 480,
|
|
||||||
CanResize = false,
|
|
||||||
WindowStartupLocation = WindowStartupLocation.CenterOwner,
|
|
||||||
SizeToContent = SizeToContent.Height
|
|
||||||
};
|
|
||||||
|
|
||||||
dialog.Content = BuildMessageDialogContent(message, () => dialog.Close());
|
|
||||||
|
|
||||||
await dialog.ShowDialog(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Control BuildMessageDialogContent(string message, Action closeDialog)
|
|
||||||
{
|
|
||||||
var layout = new StackPanel
|
|
||||||
{
|
|
||||||
Margin = new Thickness(16),
|
|
||||||
Spacing = 12
|
|
||||||
};
|
|
||||||
|
|
||||||
layout.Children.Add(new TextBlock
|
|
||||||
{
|
|
||||||
Text = message,
|
|
||||||
TextWrapping = Avalonia.Media.TextWrapping.Wrap,
|
|
||||||
MaxWidth = 420
|
|
||||||
});
|
|
||||||
|
|
||||||
var closeButton = new Button
|
|
||||||
{
|
|
||||||
Content = "OK",
|
|
||||||
MinWidth = 88,
|
|
||||||
HorizontalAlignment = HorizontalAlignment.Right
|
|
||||||
};
|
|
||||||
|
|
||||||
closeButton.Click += (_, _) => closeDialog();
|
|
||||||
|
|
||||||
layout.Children.Add(closeButton);
|
|
||||||
return layout;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,23 +12,7 @@
|
||||||
<ScrollViewer Grid.Row="0" VerticalScrollBarVisibility="Auto">
|
<ScrollViewer Grid.Row="0" VerticalScrollBarVisibility="Auto">
|
||||||
<StackPanel Margin="4">
|
<StackPanel Margin="4">
|
||||||
<TextBlock Text="AI / OCR" FontWeight="Bold" />
|
<TextBlock Text="AI / OCR" FontWeight="Bold" />
|
||||||
<StackPanel Orientation="Horizontal" Spacing="12" Margin="0,6,0,0">
|
<CheckBox Content="Estrai numeri dalle immagini" IsChecked="{Binding ExtractNumbers}" Margin="0,6,0,0" />
|
||||||
<CheckBox Content="Usa GPU"
|
|
||||||
IsChecked="{Binding UseNumberAiGpu, Mode=TwoWay}"
|
|
||||||
IsEnabled="{Binding NumberAiGpuOptionEnabled}" />
|
|
||||||
<CheckBox Content="Includi thumbnail" IsChecked="{Binding IncludeNumberAiThumbnails, Mode=TwoWay}" />
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<Grid Margin="0,8,0,0" ColumnDefinitions="Auto,*,Auto" ColumnSpacing="6">
|
|
||||||
<TextBlock Grid.Column="0" Text="Sorgente:" VerticalAlignment="Center" />
|
|
||||||
<TextBox Grid.Column="1" Text="{Binding DestinationPath, Mode=OneWay}" IsReadOnly="True" VerticalAlignment="Center" />
|
|
||||||
<Button Grid.Column="2" Width="72" Click="OpenAiDestinationFolder_Click">
|
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
|
|
||||||
<iconPacks:PackIconMaterial Kind="Folder" Width="14" Height="14" />
|
|
||||||
<TextBlock Text="Apri" />
|
|
||||||
</StackPanel>
|
|
||||||
</Button>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<TextBlock Text="Modelli" FontWeight="Bold" Margin="0,8,0,0" />
|
<TextBlock Text="Modelli" FontWeight="Bold" Margin="0,8,0,0" />
|
||||||
<Grid Margin="0,4,0,0" ColumnDefinitions="Auto,*,Auto,Auto">
|
<Grid Margin="0,4,0,0" ColumnDefinitions="Auto,*,Auto,Auto">
|
||||||
|
|
@ -51,18 +35,8 @@
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" Margin="0,8,0,0" Spacing="8">
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" Margin="0,8,0,0" Spacing="8">
|
||||||
<Button Command="{Binding StartAiCommand}" Width="132">
|
<Button Content="Avvia AI" Command="{Binding StartAiCommand}" Width="120" />
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
|
<Button Content="Annulla" Command="{Binding AsyncCancelOperationCommand}" Width="120" />
|
||||||
<iconPacks:PackIconMaterial Kind="PlayCircle" Width="16" Height="16" Foreground="#2E7D32" />
|
|
||||||
<TextBlock Text="Avvia AI" />
|
|
||||||
</StackPanel>
|
|
||||||
</Button>
|
|
||||||
<Button Command="{Binding AsyncCancelOperationCommand}" Width="132">
|
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="6">
|
|
||||||
<iconPacks:PackIconMaterial Kind="Cancel" Width="16" Height="16" Foreground="#C62828" />
|
|
||||||
<TextBlock Text="Annulla" />
|
|
||||||
</StackPanel>
|
|
||||||
</Button>
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<TextBlock Text="Output CSV" FontWeight="Bold" Margin="0,8,0,0" />
|
<TextBlock Text="Output CSV" FontWeight="Bold" Margin="0,8,0,0" />
|
||||||
|
|
|
||||||
|
|
@ -31,14 +31,6 @@ public partial class AiTabView : Avalonia.Controls.UserControl
|
||||||
OpenInExplorer(string.IsNullOrWhiteSpace(directory) ? model.CsvOutputPath : directory);
|
OpenInExplorer(string.IsNullOrWhiteSpace(directory) ? model.CsvOutputPath : directory);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OpenAiDestinationFolder_Click(object? sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (DataContext is DataModel model)
|
|
||||||
{
|
|
||||||
OpenInExplorer(model.DestinationPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void OpenInExplorer(string? path)
|
private static void OpenInExplorer(string? path)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(path))
|
if (string.IsNullOrWhiteSpace(path))
|
||||||
|
|
|
||||||
|
|
@ -1,242 +0,0 @@
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace ImageCatalog_2;
|
|
||||||
|
|
||||||
internal static class CommandLineOperationRunner
|
|
||||||
{
|
|
||||||
private static readonly HashSet<string> HeadlessMarkers = new(StringComparer.OrdinalIgnoreCase)
|
|
||||||
{
|
|
||||||
"--operation",
|
|
||||||
"--op",
|
|
||||||
"--config",
|
|
||||||
"--headless",
|
|
||||||
"--cli"
|
|
||||||
};
|
|
||||||
|
|
||||||
public static bool IsHeadlessRequest(string[]? args)
|
|
||||||
{
|
|
||||||
return args?.Any(arg => HeadlessMarkers.Contains(arg) || arg.StartsWith("--operation=", StringComparison.OrdinalIgnoreCase) || arg.StartsWith("--config=", StringComparison.OrdinalIgnoreCase)) == true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task<int> RunAsync(IServiceProvider services, string[] args)
|
|
||||||
{
|
|
||||||
var logger = services.GetRequiredService<ILoggerFactory>().CreateLogger("CommandLine");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var options = Parse(args);
|
|
||||||
if (options.ShowHelp)
|
|
||||||
{
|
|
||||||
WriteUsage();
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(options.ConfigPath))
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Missing required --config <path> argument.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(options.Operation))
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Missing required --operation <image-processing|number-ai|face-ai|race-upload> argument.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!File.Exists(options.ConfigPath))
|
|
||||||
{
|
|
||||||
throw new FileNotFoundException("Configuration file not found.", options.ConfigPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
var model = services.GetRequiredService<DataModel>();
|
|
||||||
await model.LoadSettingsFromFileAsync(options.ConfigPath).ConfigureAwait(false);
|
|
||||||
ApplyOverrides(model, options);
|
|
||||||
|
|
||||||
using var cancellationTokenSource = new CancellationTokenSource();
|
|
||||||
Console.CancelKeyPress += (_, eventArgs) =>
|
|
||||||
{
|
|
||||||
eventArgs.Cancel = true;
|
|
||||||
cancellationTokenSource.Cancel();
|
|
||||||
};
|
|
||||||
|
|
||||||
logger.LogInformation("Running ImageCatalog operation {Operation} with config {ConfigPath}", options.Operation, options.ConfigPath);
|
|
||||||
|
|
||||||
switch (NormalizeOperation(options.Operation))
|
|
||||||
{
|
|
||||||
case "image-processing":
|
|
||||||
await model.ProcessImages().ConfigureAwait(false);
|
|
||||||
break;
|
|
||||||
case "number-ai":
|
|
||||||
await model.RunNumberAiAsync(cancellationTokenSource.Token).ConfigureAwait(false);
|
|
||||||
break;
|
|
||||||
case "face-ai":
|
|
||||||
await model.RunFaceAiAsync(cancellationTokenSource.Token).ConfigureAwait(false);
|
|
||||||
break;
|
|
||||||
case "race-upload":
|
|
||||||
throw new NotSupportedException("race-upload is not available in headless mode yet because the upload workflow still lives in the Avalonia view layer.");
|
|
||||||
default:
|
|
||||||
throw new ArgumentException($"Unknown operation: {options.Operation}");
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.LogInformation("ImageCatalog operation {Operation} completed", options.Operation);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
logger.LogWarning("Command-line operation canceled.");
|
|
||||||
return 130;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
logger.LogError(ex, "Command-line operation failed.");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ApplyOverrides(DataModel model, CommandLineOptions options)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(options.ModelsPath))
|
|
||||||
{
|
|
||||||
model.ModelsFolderPath = options.ModelsPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(options.CsvPath))
|
|
||||||
{
|
|
||||||
model.CsvOutputPath = options.CsvPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.UseGpu.HasValue)
|
|
||||||
{
|
|
||||||
model.UseNumberAiGpu = options.UseGpu.Value;
|
|
||||||
if (model.FaceGpuOptionEnabled || !options.UseGpu.Value)
|
|
||||||
{
|
|
||||||
model.UseFaceGpu = options.UseGpu.Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.IncludeThumbnails.HasValue)
|
|
||||||
{
|
|
||||||
model.IncludeNumberAiThumbnails = options.IncludeThumbnails.Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string NormalizeOperation(string operation)
|
|
||||||
{
|
|
||||||
return operation.Trim().ToLowerInvariant() switch
|
|
||||||
{
|
|
||||||
"images" or "image" or "process-images" or "image-processing" => "image-processing",
|
|
||||||
"ai" or "ocr" or "number" or "number-ai" => "number-ai",
|
|
||||||
"face" or "face-ai" => "face-ai",
|
|
||||||
"race" or "race-upload" => "race-upload",
|
|
||||||
var normalized => normalized
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static CommandLineOptions Parse(string[] args)
|
|
||||||
{
|
|
||||||
var options = new CommandLineOptions();
|
|
||||||
|
|
||||||
for (var i = 0; i < args.Length; i++)
|
|
||||||
{
|
|
||||||
var arg = args[i];
|
|
||||||
switch (arg.ToLowerInvariant())
|
|
||||||
{
|
|
||||||
case "--help":
|
|
||||||
case "-h":
|
|
||||||
case "/?":
|
|
||||||
options.ShowHelp = true;
|
|
||||||
break;
|
|
||||||
case "--operation":
|
|
||||||
case "--op":
|
|
||||||
options.Operation = ReadValue(args, ref i, arg);
|
|
||||||
break;
|
|
||||||
case "--config":
|
|
||||||
options.ConfigPath = ReadValue(args, ref i, arg);
|
|
||||||
break;
|
|
||||||
case "--models":
|
|
||||||
options.ModelsPath = ReadValue(args, ref i, arg);
|
|
||||||
break;
|
|
||||||
case "--csv":
|
|
||||||
options.CsvPath = ReadValue(args, ref i, arg);
|
|
||||||
break;
|
|
||||||
case "--gpu":
|
|
||||||
options.UseGpu = true;
|
|
||||||
break;
|
|
||||||
case "--cpu":
|
|
||||||
options.UseGpu = false;
|
|
||||||
break;
|
|
||||||
case "--include-thumbnails":
|
|
||||||
case "--include-tn":
|
|
||||||
options.IncludeThumbnails = true;
|
|
||||||
break;
|
|
||||||
case "--no-thumbnails":
|
|
||||||
case "--no-tn":
|
|
||||||
options.IncludeThumbnails = false;
|
|
||||||
break;
|
|
||||||
case "--headless":
|
|
||||||
case "--cli":
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
ApplyInlineArgument(options, arg);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ApplyInlineArgument(CommandLineOptions options, string arg)
|
|
||||||
{
|
|
||||||
var separatorIndex = arg.IndexOf('=');
|
|
||||||
if (separatorIndex < 0)
|
|
||||||
{
|
|
||||||
throw new ArgumentException($"Unknown argument: {arg}");
|
|
||||||
}
|
|
||||||
|
|
||||||
var name = arg[..separatorIndex].ToLowerInvariant();
|
|
||||||
var value = arg[(separatorIndex + 1)..];
|
|
||||||
switch (name)
|
|
||||||
{
|
|
||||||
case "--operation":
|
|
||||||
case "--op":
|
|
||||||
options.Operation = value;
|
|
||||||
break;
|
|
||||||
case "--config":
|
|
||||||
options.ConfigPath = value;
|
|
||||||
break;
|
|
||||||
case "--models":
|
|
||||||
options.ModelsPath = value;
|
|
||||||
break;
|
|
||||||
case "--csv":
|
|
||||||
options.CsvPath = value;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new ArgumentException($"Unknown argument: {arg}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string ReadValue(string[] args, ref int index, string name)
|
|
||||||
{
|
|
||||||
if (index + 1 >= args.Length)
|
|
||||||
{
|
|
||||||
throw new ArgumentException($"Missing value for {name}.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return args[++index];
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void WriteUsage()
|
|
||||||
{
|
|
||||||
Console.WriteLine("Usage: ImageCatalog --config <settings.xml> --operation <image-processing|number-ai|face-ai|race-upload> [--models <folder>] [--csv <path>] [--cpu|--gpu] [--include-thumbnails|--no-thumbnails]");
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class CommandLineOptions
|
|
||||||
{
|
|
||||||
public bool ShowHelp { get; set; }
|
|
||||||
public string Operation { get; set; } = string.Empty;
|
|
||||||
public string ConfigPath { get; set; } = string.Empty;
|
|
||||||
public string ModelsPath { get; set; } = string.Empty;
|
|
||||||
public string CsvPath { get; set; } = string.Empty;
|
|
||||||
public bool? UseGpu { get; set; }
|
|
||||||
public bool? IncludeThumbnails { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -16,7 +16,6 @@ using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using AIFotoONLUS.Core;
|
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using MaddoShared;
|
using MaddoShared;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
@ -118,7 +117,6 @@ namespace ImageCatalog_2
|
||||||
|
|
||||||
// Load available fonts
|
// Load available fonts
|
||||||
AvailableFonts = LoadAvailableFonts();
|
AvailableFonts = LoadAvailableFonts();
|
||||||
RefreshNumberAiGpuCapabilities();
|
|
||||||
RefreshFaceExecutableCapabilities();
|
RefreshFaceExecutableCapabilities();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -133,33 +131,18 @@ namespace ImageCatalog_2
|
||||||
{
|
{
|
||||||
// user cancelled
|
// user cancelled
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "AI extraction failed");
|
|
||||||
if (UseNumberAiGpu)
|
|
||||||
{
|
|
||||||
RefreshNumberAiGpuCapabilities();
|
|
||||||
}
|
|
||||||
|
|
||||||
await ShowErrorMessageAsync("Errore AI", ex.GetBaseException().Message).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
MainToken = null;
|
MainToken = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task RunAiExtractionCoreAsync(CancellationToken token, bool useDestination = false, bool recursive = false, bool failOnInvalidPath = false)
|
private async Task RunAiExtractionCoreAsync(CancellationToken token, bool useDestination = false, bool recursive = false)
|
||||||
{
|
{
|
||||||
var searchRoot = useDestination ? DestinationPath : SourcePath;
|
var searchRoot = useDestination ? DestinationPath : SourcePath;
|
||||||
if (string.IsNullOrWhiteSpace(searchRoot) || !System.IO.Directory.Exists(searchRoot))
|
if (string.IsNullOrWhiteSpace(searchRoot) || !System.IO.Directory.Exists(searchRoot))
|
||||||
{
|
{
|
||||||
_logger.LogWarning("AI extraction path invalid: {Path}", searchRoot);
|
_logger.LogWarning("AI extraction path invalid: {Path}", searchRoot);
|
||||||
if (failOnInvalidPath)
|
|
||||||
{
|
|
||||||
throw new DirectoryNotFoundException($"AI extraction path invalid: {searchRoot}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -174,19 +157,10 @@ namespace ImageCatalog_2
|
||||||
{
|
{
|
||||||
SearchRoot = searchRoot,
|
SearchRoot = searchRoot,
|
||||||
Recursive = recursive,
|
Recursive = recursive,
|
||||||
IncludeThumbnails = IncludeNumberAiThumbnails,
|
|
||||||
ModelsFolderPath = ModelsFolderPath,
|
|
||||||
UseGpu = UseNumberAiGpu,
|
|
||||||
CsvOutputPath = CsvOutputPath
|
CsvOutputPath = CsvOutputPath
|
||||||
},
|
},
|
||||||
token,
|
token,
|
||||||
result => InvokeOnUiThreadAsync(() =>
|
result => InvokeOnUiThreadAsync(() => PreviewResults.Add(result)),
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(result.Text))
|
|
||||||
{
|
|
||||||
PreviewResults.Add(result);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
progress => InvokeOnUiThreadAsync(() => AiProgress = progress)).ConfigureAwait(false);
|
progress => InvokeOnUiThreadAsync(() => AiProgress = progress)).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -219,11 +193,7 @@ namespace ImageCatalog_2
|
||||||
public string ModelsFolderPath
|
public string ModelsFolderPath
|
||||||
{
|
{
|
||||||
get => _ai.ModelsFolderPath;
|
get => _ai.ModelsFolderPath;
|
||||||
set
|
set => _ai.ModelsFolderPath = value;
|
||||||
{
|
|
||||||
_ai.ModelsFolderPath = value;
|
|
||||||
RefreshNumberAiGpuCapabilities();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public string CsvOutputPath
|
public string CsvOutputPath
|
||||||
|
|
@ -232,24 +202,6 @@ namespace ImageCatalog_2
|
||||||
set => _ai.CsvOutputPath = value;
|
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 string FaceExecutablePath
|
public string FaceExecutablePath
|
||||||
{
|
{
|
||||||
get => _ai.FaceExecutablePath;
|
get => _ai.FaceExecutablePath;
|
||||||
|
|
@ -1271,7 +1223,7 @@ namespace ImageCatalog_2
|
||||||
Debug.WriteLine("Yep c");
|
Debug.WriteLine("Yep c");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ProcessImages()
|
private async Task ProcessImages()
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Avvio elaborazione...");
|
_logger.LogInformation("Avvio elaborazione...");
|
||||||
UiEnabled = false;
|
UiEnabled = false;
|
||||||
|
|
@ -1337,6 +1289,23 @@ namespace ImageCatalog_2
|
||||||
},
|
},
|
||||||
speed => { SpeedCounter = speed; }).ConfigureAwait(false);
|
speed => { SpeedCounter = speed; }).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// AI integration stub: if ExtractNumbers is enabled, simulate or invoke OCR processing
|
||||||
|
if (ExtractNumbers)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await RunAiExtractionCoreAsync(token);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("AI extraction canceled");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "AI extraction failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SpeedCounter = runResult.FinalSpeedCounter;
|
SpeedCounter = runResult.FinalSpeedCounter;
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
|
|
@ -1358,17 +1327,6 @@ namespace ImageCatalog_2
|
||||||
UiEnabled = true;
|
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()
|
private async Task CancelOperation()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
@ -1780,71 +1738,6 @@ namespace ImageCatalog_2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
private void SetUseFaceGpu(bool value)
|
||||||
{
|
{
|
||||||
var currentValue = _ai.UseFaceGpu;
|
var currentValue = _ai.UseFaceGpu;
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,6 @@
|
||||||
<AssemblyName>ImageCatalog</AssemblyName>
|
<AssemblyName>ImageCatalog</AssemblyName>
|
||||||
<LangVersion>default</LangVersion>
|
<LangVersion>default</LangVersion>
|
||||||
<AvaloniaWindowsCrossPublish Condition="'$(AvaloniaWindowsCrossPublish)' == ''">false</AvaloniaWindowsCrossPublish>
|
<AvaloniaWindowsCrossPublish Condition="'$(AvaloniaWindowsCrossPublish)' == ''">false</AvaloniaWindowsCrossPublish>
|
||||||
<UseLocalAIFotoONLUS Condition="'$(UseLocalAIFotoONLUS)' == '' and Exists('..\..\AIFotoONLUS\src\AIFotoONLUS.Core\AIFotoONLUS.Core.csproj') and '$(GITHUB_ACTIONS)' != 'true' and '$(CI)' != 'true'">true</UseLocalAIFotoONLUS>
|
|
||||||
<UseLocalAIFotoONLUS Condition="'$(UseLocalAIFotoONLUS)' == ''">false</UseLocalAIFotoONLUS>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<!-- Windows: net10.0-windows TFM auto-defines WINDOWS preprocessor symbol -->
|
<!-- Windows: net10.0-windows TFM auto-defines WINDOWS preprocessor symbol -->
|
||||||
<PropertyGroup Condition="$([MSBuild]::IsOsPlatform('Windows'))">
|
<PropertyGroup Condition="$([MSBuild]::IsOsPlatform('Windows'))">
|
||||||
|
|
@ -58,10 +56,9 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Catalog.Communication\Catalog.Communication.csproj" />
|
<ProjectReference Include="..\Catalog.Communication\Catalog.Communication.csproj" />
|
||||||
<ProjectReference Include="..\MaddoShared\MaddoShared.csproj" />
|
<ProjectReference Include="..\MaddoShared\MaddoShared.csproj" />
|
||||||
<ProjectReference Include="..\..\AIFotoONLUS\src\AIFotoONLUS.Core\AIFotoONLUS.Core.csproj" Condition="'$(UseLocalAIFotoONLUS)' == 'true'" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AIFotoONLUS.Core" Version="0.1.2" Condition="'$(UseLocalAIFotoONLUS)' != 'true'" />
|
<PackageReference Include="AIFotoONLUS.Core" Version="0.1.1" />
|
||||||
<PackageReference Include="AutoMapper" Version="16.1.0" />
|
<PackageReference Include="AutoMapper" Version="16.1.0" />
|
||||||
<PackageReference Include="IconPacks.Avalonia" Version="1.3.1" />
|
<PackageReference Include="IconPacks.Avalonia" Version="1.3.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.5" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.5" />
|
||||||
|
|
|
||||||
|
|
@ -274,14 +274,6 @@ namespace ImageCatalog_2.Models
|
||||||
[XmlElement("AI_PercorsoCsv")]
|
[XmlElement("AI_PercorsoCsv")]
|
||||||
public string CsvOutputPath { get; set; }
|
public string CsvOutputPath { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("UseNumberAiGpu")]
|
|
||||||
[XmlElement("AI_UsaGpuNumeri")]
|
|
||||||
public bool UseNumberAiGpu { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("IncludeNumberAiThumbnails")]
|
|
||||||
[XmlElement("AI_IncludiThumbnailNumeri")]
|
|
||||||
public bool IncludeNumberAiThumbnails { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("FaceExecutablePath")]
|
[JsonPropertyName("FaceExecutablePath")]
|
||||||
[XmlElement("AI_FaceExecutablePath")]
|
[XmlElement("AI_FaceExecutablePath")]
|
||||||
public string FaceExecutablePath { get; set; } = string.Empty;
|
public string FaceExecutablePath { get; set; } = string.Empty;
|
||||||
|
|
|
||||||
|
|
@ -38,8 +38,6 @@ static class Program
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
static extern bool AttachConsole(int dwProcessId);
|
static extern bool AttachConsole(int dwProcessId);
|
||||||
|
|
||||||
private const int ATTACH_PARENT_PROCESS = -1;
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
private static extern bool GenerateConsoleCtrlEvent(uint dwCtrlEvent, uint dwProcessGroupId);
|
private static extern bool GenerateConsoleCtrlEvent(uint dwCtrlEvent, uint dwProcessGroupId);
|
||||||
|
|
||||||
|
|
@ -87,18 +85,10 @@ static class Program
|
||||||
.LogToTrace();
|
.LogToTrace();
|
||||||
|
|
||||||
[STAThread]
|
[STAThread]
|
||||||
static int Main(string[] args)
|
static void Main(string[] args)
|
||||||
{
|
{
|
||||||
#if WINDOWS
|
#if WINDOWS
|
||||||
if (CommandLineOperationRunner.IsHeadlessRequest(args))
|
|
||||||
{
|
|
||||||
AttachConsole(ATTACH_PARENT_PROCESS);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
AllocConsole();
|
AllocConsole();
|
||||||
}
|
|
||||||
|
|
||||||
RedirectConsoleOutput();
|
RedirectConsoleOutput();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
@ -107,15 +97,7 @@ static class Program
|
||||||
|
|
||||||
ServiceProvider = serviceCollection.BuildServiceProvider();
|
ServiceProvider = serviceCollection.BuildServiceProvider();
|
||||||
|
|
||||||
if (CommandLineOperationRunner.IsHeadlessRequest(args))
|
|
||||||
{
|
|
||||||
return CommandLineOperationRunner.RunAsync(ServiceProvider, args ?? Array.Empty<string>())
|
|
||||||
.GetAwaiter()
|
|
||||||
.GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args ?? Array.Empty<string>());
|
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args ?? Array.Empty<string>());
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ConfigureServices(ServiceCollection services)
|
private static void ConfigureServices(ServiceCollection services)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using AIFotoONLUS.Core;
|
|
||||||
using ImageCatalog_2.Models;
|
using ImageCatalog_2.Models;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
|
@ -34,18 +33,38 @@ public class AiExtractionService : IAiExtractionService
|
||||||
|| f.EndsWith(".png", StringComparison.OrdinalIgnoreCase)
|
|| f.EndsWith(".png", StringComparison.OrdinalIgnoreCase)
|
||||||
|| f.EndsWith(".bmp", StringComparison.OrdinalIgnoreCase)
|
|| f.EndsWith(".bmp", StringComparison.OrdinalIgnoreCase)
|
||||||
|| f.EndsWith(".gif", StringComparison.OrdinalIgnoreCase))
|
|| f.EndsWith(".gif", StringComparison.OrdinalIgnoreCase))
|
||||||
.Where(f => request.IncludeThumbnails || !Path.GetFileName(f).StartsWith("tn_", StringComparison.OrdinalIgnoreCase))
|
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var extractedResults = new List<AiResultItem>();
|
if (imageFiles.Count == 0)
|
||||||
var modelConfiguration = BuildModelConfiguration(request.ModelsFolderPath, request.UseGpu);
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
using var engine = new NumberRecognitionEngine(modelConfiguration, _logger);
|
var extractedResults = new List<AiResultItem>();
|
||||||
|
|
||||||
|
Type? aiProcessorType = null;
|
||||||
|
object? aiProcessor = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var assembly = AppDomain.CurrentDomain.GetAssemblies()
|
||||||
|
.FirstOrDefault(a => a.GetName().Name?.Equals("AIFotoONLUS.Core", StringComparison.OrdinalIgnoreCase) == true);
|
||||||
|
if (assembly != null)
|
||||||
|
{
|
||||||
|
aiProcessorType = assembly.GetType("AIFotoONLUS.Core.AiProcessor");
|
||||||
|
if (aiProcessorType != null)
|
||||||
|
{
|
||||||
|
aiProcessor = Activator.CreateInstance(aiProcessorType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "AIFotoONLUS.Core not available or failed to load via reflection");
|
||||||
|
}
|
||||||
|
|
||||||
var processed = 0;
|
var processed = 0;
|
||||||
var total = imageFiles.Count;
|
var total = imageFiles.Count;
|
||||||
var failed = 0;
|
|
||||||
Exception? firstFailure = null;
|
|
||||||
|
|
||||||
foreach (var file in imageFiles)
|
foreach (var file in imageFiles)
|
||||||
{
|
{
|
||||||
|
|
@ -53,31 +72,39 @@ public class AiExtractionService : IAiExtractionService
|
||||||
|
|
||||||
var extracted = string.Empty;
|
var extracted = string.Empty;
|
||||||
|
|
||||||
|
if (aiProcessorType is not null && aiProcessor is not null)
|
||||||
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
extracted = engine.ProcessImage(file).Text;
|
var method = aiProcessorType.GetMethod("ExtractNumbersFromImage")
|
||||||
|
?? aiProcessorType.GetMethod("ExtractTextFromImage");
|
||||||
|
if (method is not null)
|
||||||
|
{
|
||||||
|
var value = method.Invoke(aiProcessor, new object[] { file });
|
||||||
|
if (value != null)
|
||||||
|
{
|
||||||
|
extracted = value.ToString() ?? string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
failed++;
|
_logger.LogWarning(ex, "Error invoking AI processor for {File}", file);
|
||||||
firstFailure ??= ex;
|
}
|
||||||
_logger.LogWarning(ex, "Error processing AI OCR for {File}", file);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(extracted))
|
||||||
|
{
|
||||||
var result = new AiResultItem { Path = file, Text = extracted };
|
var result = new AiResultItem { Path = file, Text = extracted };
|
||||||
extractedResults.Add(result);
|
extractedResults.Add(result);
|
||||||
await onResult(result).ConfigureAwait(false);
|
await onResult(result).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
processed++;
|
processed++;
|
||||||
var percent = total > 0 ? (processed * 100.0 / total) : 100.0;
|
var percent = total > 0 ? (processed * 100.0 / total) : 100.0;
|
||||||
await onProgress(percent).ConfigureAwait(false);
|
await onProgress(percent).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (imageFiles.Count > 0 && failed == imageFiles.Count)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"AI OCR failed for all {imageFiles.Count} image(s). See previous log entries for details.", firstFailure);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(request.CsvOutputPath))
|
if (!string.IsNullOrWhiteSpace(request.CsvOutputPath))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
@ -92,9 +119,8 @@ public class AiExtractionService : IAiExtractionService
|
||||||
sw.WriteLine("Path,Text");
|
sw.WriteLine("Path,Text");
|
||||||
foreach (var r in extractedResults)
|
foreach (var r in extractedResults)
|
||||||
{
|
{
|
||||||
var csvFileName = Path.GetFileName(r.Path ?? string.Empty);
|
|
||||||
var safeText = (r.Text ?? string.Empty).Replace("\"", "\"\"");
|
var safeText = (r.Text ?? string.Empty).Replace("\"", "\"\"");
|
||||||
sw.WriteLine($"\"{csvFileName}\",\"{safeText}\"");
|
sw.WriteLine($"\"{r.Path}\",\"{safeText}\"");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|
@ -103,27 +129,4 @@ public class AiExtractionService : IAiExtractionService
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ModelConfiguration BuildModelConfiguration(string modelsFolderPath, bool useGpu)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(modelsFolderPath))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("AI models folder is not configured.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var modelsRoot = Path.GetFullPath(modelsFolderPath.Trim().Trim('"'));
|
|
||||||
if (!Directory.Exists(modelsRoot))
|
|
||||||
{
|
|
||||||
throw new DirectoryNotFoundException($"AI models folder not found: {modelsRoot}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return 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 = useGpu
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,6 @@ public sealed class AiExtractionRequest
|
||||||
{
|
{
|
||||||
public required string SearchRoot { get; init; }
|
public required string SearchRoot { get; init; }
|
||||||
public required bool Recursive { get; init; }
|
public required bool Recursive { get; init; }
|
||||||
public bool IncludeThumbnails { get; init; }
|
|
||||||
public required string ModelsFolderPath { get; init; }
|
|
||||||
public bool UseGpu { get; init; }
|
|
||||||
public string CsvOutputPath { get; init; } = string.Empty;
|
public string CsvOutputPath { get; init; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,39 +38,6 @@ public class AiSettingsViewModel : ViewModelBase
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool _useNumberAiGpu;
|
|
||||||
public bool UseNumberAiGpu
|
|
||||||
{
|
|
||||||
get => _useNumberAiGpu;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
_useNumberAiGpu = value;
|
|
||||||
NotifyPropertyChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool _numberAiGpuOptionEnabled;
|
|
||||||
public bool NumberAiGpuOptionEnabled
|
|
||||||
{
|
|
||||||
get => _numberAiGpuOptionEnabled;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
_numberAiGpuOptionEnabled = value;
|
|
||||||
NotifyPropertyChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool _includeNumberAiThumbnails;
|
|
||||||
public bool IncludeNumberAiThumbnails
|
|
||||||
{
|
|
||||||
get => _includeNumberAiThumbnails;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
_includeNumberAiThumbnails = value;
|
|
||||||
NotifyPropertyChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private string _faceExecutablePath = string.Empty;
|
private string _faceExecutablePath = string.Empty;
|
||||||
public string FaceExecutablePath
|
public string FaceExecutablePath
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue