228 lines
7.6 KiB
C#
228 lines
7.6 KiB
C#
|
|
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;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
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 "--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]");
|
||
|
|
}
|
||
|
|
|
||
|
|
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; }
|
||
|
|
}
|
||
|
|
}
|