Catalog/imagecatalog/CommandLineOperationRunner.cs

228 lines
7.6 KiB
C#
Raw Normal View History

2026-05-09 17:27:05 +02:00
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; }
}
}