using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace ImageCatalog_2; internal static class CommandLineOperationRunner { private static readonly HashSet 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 RunAsync(IServiceProvider services, string[] args) { var logger = services.GetRequiredService().CreateLogger("CommandLine"); try { var options = Parse(args); if (options.ShowHelp) { WriteUsage(); return 0; } if (string.IsNullOrWhiteSpace(options.ConfigPath)) { throw new ArgumentException("Missing required --config argument."); } if (string.IsNullOrWhiteSpace(options.Operation)) { throw new ArgumentException("Missing required --operation argument."); } if (!File.Exists(options.ConfigPath)) { throw new FileNotFoundException("Configuration file not found.", options.ConfigPath); } var model = services.GetRequiredService(); 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; } if (options.NumberAiWorkloadLevel.HasValue) { model.NumberAiWorkloadLevel = options.NumberAiWorkloadLevel.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 "--workload": case "--ai-workload": options.NumberAiWorkloadLevel = int.Parse(ReadValue(args, ref i, arg)); 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; case "--workload": case "--ai-workload": options.NumberAiWorkloadLevel = int.Parse(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 --operation [--models ] [--csv ] [--cpu|--gpu] [--include-thumbnails|--no-thumbnails] [--workload <1-5>]"); } 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; } public int? NumberAiWorkloadLevel { get; set; } } }