Catalog/imagecatalog/Services/AiExtractionService.cs
MaddoScientisto 3c722a66df feat: Add AI extraction service and related view models
- Introduced `IAiExtractionService` and its implementation `AiExtractionService` for processing images and extracting text.
- Created `AiResultItem` model to hold results from AI extraction.
- Added `ImageProcessingCoordinator` to manage image processing tasks and provide progress updates.
- Implemented view models for AI settings, path settings, processing state, race upload settings, and visual settings to support UI binding.
- Updated `Program.cs` to register new services and dependencies.
- Modified project file to skip MinVer execution during local builds.
2026-03-12 18:48:13 +01:00

132 lines
4.5 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using ImageCatalog_2.Models;
using Microsoft.Extensions.Logging;
namespace ImageCatalog_2.Services;
public class AiExtractionService : IAiExtractionService
{
private readonly ILogger<AiExtractionService> _logger;
public AiExtractionService(ILogger<AiExtractionService> logger)
{
_logger = logger;
}
public async Task RunAsync(
AiExtractionRequest request,
CancellationToken token,
Func<AiResultItem, Task> onResult,
Func<double, Task> 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))
.ToList();
if (imageFiles.Count == 0)
{
return;
}
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 total = imageFiles.Count;
foreach (var file in imageFiles)
{
token.ThrowIfCancellationRequested();
var extracted = string.Empty;
if (aiProcessorType is not null && aiProcessor is not null)
{
try
{
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)
{
_logger.LogWarning(ex, "Error invoking AI processor for {File}", file);
}
}
if (!string.IsNullOrWhiteSpace(extracted))
{
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 (!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 safeText = (r.Text ?? string.Empty).Replace("\"", "\"\"");
sw.WriteLine($"\"{r.Path}\",\"{safeText}\"");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to write CSV to {CsvOutputPath}", request.CsvOutputPath);
}
}
}
}