Files
Foxel/Services/Background/Processors/VisualRecognitionTaskProcessor.cs

288 lines
14 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Foxel.Models.DataBase;
using Foxel.Services.AI;
using Foxel.Services.Storage;
using Foxel.Utils;
using Microsoft.EntityFrameworkCore;
using System.Text.Json;
using Foxel.Services.VectorDb;
// using Foxel.Services.Attributes; // StorageType enum might not be directly needed here anymore
using Microsoft.Extensions.DependencyInjection; // For CreateScope
using Microsoft.AspNetCore.Hosting; // For IWebHostEnvironment
using Microsoft.Extensions.Logging; // For ILogger
namespace Foxel.Services.Background.Processors
{
public class VisualRecognitionPayload
{
public int PictureId { get; set; }
public int? UserIdForPicture { get; set; }
}
public class VisualRecognitionTaskProcessor : ITaskProcessor
{
private readonly IDbContextFactory<MyDbContext> _contextFactory;
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<VisualRecognitionTaskProcessor> _logger;
private readonly IWebHostEnvironment _environment;
public VisualRecognitionTaskProcessor(
IDbContextFactory<MyDbContext> contextFactory,
IServiceProvider serviceProvider,
ILogger<VisualRecognitionTaskProcessor> logger,
IWebHostEnvironment environment)
{
_contextFactory = contextFactory;
_serviceProvider = serviceProvider;
_logger = logger;
_environment = environment;
}
public async Task ProcessAsync(BackgroundTask backgroundTask)
{
// ... (payload deserialization and validation logic remains the same) ...
if (backgroundTask.Payload == null)
{
await UpdateTaskStatusInDb(backgroundTask.Id, TaskExecutionStatus.Failed, 0, "任务 Payload 为空。");
_logger.LogError("视觉识别任务 Payload 为空: TaskId={TaskId}", backgroundTask.Id);
return;
}
VisualRecognitionPayload? payload;
try
{
payload = JsonSerializer.Deserialize<VisualRecognitionPayload>(backgroundTask.Payload);
}
catch (JsonException ex)
{
_logger.LogError(ex, "无法解析视觉识别任务的 Payload: TaskId={TaskId}", backgroundTask.Id);
await UpdateTaskStatusInDb(backgroundTask.Id, TaskExecutionStatus.Failed, 0, "Payload 解析失败。");
return;
}
if (payload == null || payload.PictureId == 0)
{
_logger.LogError("视觉识别任务的 Payload 无效或缺少 PictureId: TaskId={TaskId}", backgroundTask.Id);
await UpdateTaskStatusInDb(backgroundTask.Id, TaskExecutionStatus.Failed, 0,
"Payload 无效或缺少 PictureId。");
return;
}
var pictureId = payload.PictureId;
string thumbnailForAiDownloadPath = string.Empty;
bool isTempThumbnailFile = false;
await using var dbContext = await _contextFactory.CreateDbContextAsync();
var currentBackgroundTaskState = await dbContext.BackgroundTasks.FindAsync(backgroundTask.Id);
if (currentBackgroundTaskState == null)
{
_logger.LogError("在 VisualRecognitionTaskProcessor 中找不到后台任务: TaskId={TaskId}", backgroundTask.Id);
return;
}
var picture = await dbContext.Pictures
.Include(p => p.User)
.ThenInclude(u => u!.Tags) // Ensure Tags on User is loaded if needed
.Include(p => p.StorageMode) // Include StorageMode
.Include(p => p.Tags) // Include picture's own tags
.FirstOrDefaultAsync(p => p.Id == pictureId);
try
{
await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 10,
currentBackgroundTaskState: currentBackgroundTaskState);
if (picture == null)
{
throw new Exception($"找不到ID为{pictureId}的图片。");
}
if (picture.StorageMode == null || picture.StorageModeId < 0)
{
throw new Exception($"图片ID {pictureId} 缺少有效的 StorageMode 配置。");
}
if (string.IsNullOrEmpty(picture.ThumbnailPath))
{
// It's possible the thumbnail is generated by PictureTaskProcessor but this task runs before it completes.
// Or thumbnail generation failed.
_logger.LogWarning("图片ID {PictureId} 的缩略图路径为空。AI分析将无法进行或可能失败。", pictureId);
// Depending on requirements, you might throw, or try to generate it here, or skip AI.
// For now, let's assume it should exist.
throw new Exception($"图片ID {pictureId} 的缩略图路径为空无法进行AI分析。");
}
using var scope = _serviceProvider.CreateScope();
var aiService = scope.ServiceProvider.GetRequiredService<IAiService>();
var storageService = scope.ServiceProvider.GetRequiredService<IStorageService>();
string contentRootPath = _environment.ContentRootPath;
string actualThumbnailPathForAI;
// Check the StorageType of the associated StorageMode
if (picture.StorageMode.StorageType == StorageType.Local)
{
// As with PictureTaskProcessor, safer to use DownloadFileAsync for consistency
_logger.LogInformation("Picture {PictureId} thumbnail is Local. Attempting to download via StorageService for AI.", pictureId);
// Fall-through
}
// else // Remote storage or consistent handling for Local
// {
await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 15,
currentBackgroundTaskState: currentBackgroundTaskState);
// Use picture.StorageModeId to download the thumbnail
thumbnailForAiDownloadPath = await storageService.ExecuteAsync(picture.StorageModeId,
provider => provider.DownloadFileAsync(picture.ThumbnailPath));
actualThumbnailPathForAI = thumbnailForAiDownloadPath;
isTempThumbnailFile = true;
// }
if (string.IsNullOrEmpty(actualThumbnailPathForAI) || !File.Exists(actualThumbnailPathForAI))
{
throw new Exception($"找不到用于AI分析的缩略图文件: {actualThumbnailPathForAI} (源存储路径: {picture.ThumbnailPath})");
}
await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 20,
currentBackgroundTaskState: currentBackgroundTaskState);
string base64Image = await ImageHelper.ConvertImageToBase64(actualThumbnailPathForAI);
await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 40,
currentBackgroundTaskState: currentBackgroundTaskState);
var (title, description) = await aiService.AnalyzeImageAsync(base64Image);
string finalTitle = !string.IsNullOrWhiteSpace(title) && title != "AI生成的标题"
? title
: Path.GetFileNameWithoutExtension(picture.Name); // Fallback to existing name
string finalDescription = !string.IsNullOrWhiteSpace(description) && description != "AI生成的描述"
? description
: picture.Description; // Fallback to existing description
picture.Name = finalTitle;
picture.Description = finalDescription;
await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 60,
currentBackgroundTaskState: currentBackgroundTaskState);
var combinedText = $"{finalTitle}. {finalDescription}";
var embedding = await aiService.GetEmbeddingAsync(combinedText);
picture.Embedding = embedding;
if (picture.UserId.HasValue && embedding != null && embedding.Length > 0)
{
var vectorDbService = scope.ServiceProvider.GetRequiredService<IVectorDbService>();
await vectorDbService.AddPictureToUserCollectionAsync(picture.UserId.Value,
new Models.Vector.PictureVector
{
Id = (ulong)picture.Id, // Ensure Picture.Id can be cast to ulong or adjust type
Name = picture.Name,
Embedding = embedding
});
}
await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 70,
currentBackgroundTaskState: currentBackgroundTaskState);
var availableTagNames = await dbContext.Tags.Select(t => t.Name).ToListAsync();
await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 80,
currentBackgroundTaskState: currentBackgroundTaskState);
var matchedTagNames = await aiService.GenerateTagsFromImageAsync(base64Image, availableTagNames, true);
await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 90,
currentBackgroundTaskState: currentBackgroundTaskState);
if (matchedTagNames.Any()) // Apply tags even if user is null, if that's desired
{
picture.Tags ??= new List<Tag>();
foreach (var tagName in matchedTagNames)
{
var existingTag =
await dbContext.Tags.FirstOrDefaultAsync(t => t.Name.ToLower() == tagName.ToLower());
if (existingTag == null)
{
existingTag = new Tag { Name = tagName.Trim(), Description = tagName.Trim() };
dbContext.Tags.Add(existingTag);
// await dbContext.SaveChangesAsync(); // Save tag immediately or batch
}
if (picture.Tags.All(t => t.Id != existingTag.Id)) picture.Tags.Add(existingTag);
// Add to user's tags if user exists
if (picture.User != null)
{
picture.User.Tags ??= new List<Tag>();
if (picture.User.Tags.All(t => t.Id != existingTag.Id)) picture.User.Tags.Add(existingTag);
}
}
}
await dbContext.SaveChangesAsync();
await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Completed, 100,
completedAt: DateTime.UtcNow, currentBackgroundTaskState: currentBackgroundTaskState);
}
catch (Exception ex)
{
_logger.LogError(ex, "视觉识别任务失败: TaskId={TaskId}, PictureId={PictureId}", currentBackgroundTaskState.Id,
pictureId);
await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Failed,
currentBackgroundTaskState.Progress, ex.Message,
currentBackgroundTaskState: currentBackgroundTaskState);
}
finally
{
if (isTempThumbnailFile && File.Exists(thumbnailForAiDownloadPath))
{
try
{
File.Delete(thumbnailForAiDownloadPath);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "删除临时AI缩略图文件失败: {FilePath}", thumbnailForAiDownloadPath);
}
}
}
}
// ... (UpdateTaskStatusInDb remains the same) ...
private async Task UpdateTaskStatusInDb(Guid taskId, TaskExecutionStatus status, int progress,
string? error = null, DateTime? startedAt = null, DateTime? completedAt = null,
BackgroundTask? currentBackgroundTaskState = null)
{
await using var dbContext = await _contextFactory.CreateDbContextAsync();
var taskToUpdate = currentBackgroundTaskState ?? await dbContext.BackgroundTasks.FindAsync(taskId);
if (taskToUpdate != null)
{
if (currentBackgroundTaskState != null &&
dbContext.Entry(currentBackgroundTaskState).State == EntityState.Detached)
{
dbContext.BackgroundTasks.Attach(currentBackgroundTaskState);
}
taskToUpdate.Status = status;
taskToUpdate.Progress = progress;
taskToUpdate.ErrorMessage =
string.IsNullOrEmpty(error)
? taskToUpdate.ErrorMessage
: error;
if (startedAt.HasValue) taskToUpdate.StartedAt = startedAt;
if (completedAt.HasValue) taskToUpdate.CompletedAt = completedAt;
if ((status == TaskExecutionStatus.Completed || status == TaskExecutionStatus.Failed) &&
!taskToUpdate.StartedAt.HasValue)
{
taskToUpdate.StartedAt = taskToUpdate.CreatedAt;
}
if (status == TaskExecutionStatus.Completed || status == TaskExecutionStatus.Failed)
{
taskToUpdate.CompletedAt ??= DateTime.UtcNow;
}
await dbContext.SaveChangesAsync();
_logger.LogInformation(
"任务状态更新 (VisualRecognitionProcessor): TaskId={TaskId}, Status={Status}, Progress={Progress}%",
taskId, status, progress);
}
else
{
_logger.LogWarning("尝试在 VisualRecognitionProcessor 中更新不存在的任务状态: TaskId={TaskId}", taskId);
}
}
}
}