using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using AIFotoONLUS.Core; using ImageCatalog_2.Models; using Microsoft.Extensions.Logging; namespace ImageCatalog_2.Services; public class AiExtractionService : IAiExtractionService { private readonly ILogger _logger; public AiExtractionService(ILogger logger) { _logger = logger; } public async Task RunAsync( AiExtractionRequest request, CancellationToken token, Func onResult, Func onProgress) { var searchOption = request.Recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; var imageFiles = Directory.EnumerateFiles(request.SearchRoot, "*.*", searchOption) .Where(f => f.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase) || f.EndsWith(".jpeg", StringComparison.OrdinalIgnoreCase) || f.EndsWith(".png", StringComparison.OrdinalIgnoreCase) || f.EndsWith(".bmp", StringComparison.OrdinalIgnoreCase) || f.EndsWith(".gif", StringComparison.OrdinalIgnoreCase)) .Where(f => request.IncludeThumbnails || !Path.GetFileName(f).StartsWith("tn_", StringComparison.OrdinalIgnoreCase)) .ToList(); var extractedResults = new List(); var modelConfiguration = BuildModelConfiguration(request.ModelsFolderPath, request.UseGpu); using var engine = new NumberRecognitionEngine(modelConfiguration, _logger); var processed = 0; var total = imageFiles.Count; var failed = 0; Exception? firstFailure = null; foreach (var file in imageFiles) { token.ThrowIfCancellationRequested(); var extracted = string.Empty; try { extracted = engine.ProcessImage(file).Text; } catch (Exception ex) { failed++; firstFailure ??= ex; _logger.LogWarning(ex, "Error processing AI OCR for {File}", file); } var result = new AiResultItem { Path = file, Text = extracted }; extractedResults.Add(result); await onResult(result).ConfigureAwait(false); processed++; var percent = total > 0 ? (processed * 100.0 / total) : 100.0; 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)) { try { var dir = Path.GetDirectoryName(request.CsvOutputPath) ?? string.Empty; if (!string.IsNullOrWhiteSpace(dir) && !Directory.Exists(dir)) { Directory.CreateDirectory(dir); } using var sw = new StreamWriter(request.CsvOutputPath, false, Encoding.UTF8); sw.WriteLine("Path,Text"); foreach (var r in extractedResults) { var csvFileName = Path.GetFileName(r.Path ?? string.Empty); var safeText = (r.Text ?? string.Empty).Replace("\"", "\"\""); sw.WriteLine($"\"{csvFileName}\",\"{safeText}\""); } } catch (Exception ex) { _logger.LogError(ex, "Failed to write CSV to {CsvOutputPath}", request.CsvOutputPath); } } } 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 }; } }