mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-06-02 22:20:01 +08:00
feat(background): add visual recognition task and refactor picture processing
This commit is contained in:
@@ -6,9 +6,23 @@ using Foxel.Utils;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Text.Json;
|
||||
using Foxel.Services.Attributes;
|
||||
using Foxel.Services.Background; // Added for IBackgroundTaskQueue
|
||||
|
||||
namespace Foxel.Services.Background.Processors
|
||||
{
|
||||
public class PictureProcessingPayload // Ensure this is defined or imported
|
||||
{
|
||||
public int PictureId { get; set; }
|
||||
public string OriginalFilePath { get; set; } = string.Empty;
|
||||
public int? UserIdForPicture { get; set; }
|
||||
}
|
||||
|
||||
public class VisualRecognitionPayload // Define new payload
|
||||
{
|
||||
public int PictureId { get; set; }
|
||||
public int? UserIdForPicture { get; set; }
|
||||
}
|
||||
|
||||
public class PictureTaskProcessor : ITaskProcessor
|
||||
{
|
||||
private readonly IDbContextFactory<MyDbContext> _contextFactory;
|
||||
@@ -60,18 +74,17 @@ namespace Foxel.Services.Background.Processors
|
||||
var originalFilePathFromPayload = payload.OriginalFilePath;
|
||||
string localFilePath = "";
|
||||
bool isTempFile = false;
|
||||
string thumbnailForAI = string.Empty;
|
||||
// string thumbnailForAI = string.Empty; // No longer directly used for AI here
|
||||
|
||||
await using var dbContext = await _contextFactory.CreateDbContextAsync();
|
||||
var currentBackgroundTaskState = await dbContext.BackgroundTasks.FindAsync(backgroundTask.Id);
|
||||
if (currentBackgroundTaskState == null)
|
||||
{
|
||||
_logger.LogError("在 PictureTaskProcessor 中找不到后台任务: TaskId={TaskId}", backgroundTask.Id);
|
||||
return;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
var picture = await dbContext.Pictures.Include(p => p.User).ThenInclude(u => u.Tags).FirstOrDefaultAsync(p => p.Id == pictureId);
|
||||
var picture = await dbContext.Pictures.Include(p => p.User).FirstOrDefaultAsync(p => p.Id == pictureId);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -83,10 +96,9 @@ namespace Foxel.Services.Background.Processors
|
||||
}
|
||||
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
var aiService = scope.ServiceProvider.GetRequiredService<IAiService>();
|
||||
var storageService = scope.ServiceProvider.GetRequiredService<IStorageService>();
|
||||
|
||||
string contentRootPath = _environment.ContentRootPath;
|
||||
string contentRootPath = _environment.ContentRootPath;
|
||||
|
||||
if (picture.StorageType == StorageType.Local)
|
||||
{
|
||||
@@ -94,7 +106,7 @@ namespace Foxel.Services.Background.Processors
|
||||
}
|
||||
else
|
||||
{
|
||||
await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 15, currentBackgroundTaskState: currentBackgroundTaskState);
|
||||
await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 25, currentBackgroundTaskState: currentBackgroundTaskState); // Adjusted progress
|
||||
localFilePath = await storageService.ExecuteAsync(picture.StorageType,
|
||||
provider => provider.DownloadFileAsync(originalFilePathFromPayload));
|
||||
isTempFile = true;
|
||||
@@ -105,22 +117,28 @@ namespace Foxel.Services.Background.Processors
|
||||
throw new Exception($"找不到图片文件: {localFilePath} (源路径: {originalFilePathFromPayload})");
|
||||
}
|
||||
|
||||
await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 20, currentBackgroundTaskState: currentBackgroundTaskState);
|
||||
thumbnailForAI = localFilePath;
|
||||
await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 50, currentBackgroundTaskState: currentBackgroundTaskState); // Adjusted progress
|
||||
|
||||
if (string.IsNullOrEmpty(picture.ThumbnailPath))
|
||||
{
|
||||
var tempThumbContainer = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(tempThumbContainer);
|
||||
var thumbnailDiskPath = Path.Combine(tempThumbContainer, Path.GetFileNameWithoutExtension(Path.GetFileName(localFilePath)) + "_thumb.webp");
|
||||
|
||||
// Derive baseName from OriginalPath (which is in originalFilePathFromPayload)
|
||||
// originalFilePathFromPayload is the stored path/key, not a local path.
|
||||
// We need the base name (UUID part) from the picture's OriginalPath.
|
||||
string baseNameFromOriginalPath = Path.GetFileNameWithoutExtension(picture.OriginalPath);
|
||||
|
||||
var thumbnailDiskPath = Path.Combine(tempThumbContainer, $"{baseNameFromOriginalPath}-thumbnail-temp.webp");
|
||||
|
||||
await ImageHelper.CreateThumbnailAsync(localFilePath, thumbnailDiskPath, 500);
|
||||
thumbnailForAI = thumbnailDiskPath;
|
||||
// thumbnailForAI = thumbnailDiskPath; // This temp path is for AI, but AI is in next step
|
||||
|
||||
await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 25, currentBackgroundTaskState: currentBackgroundTaskState);
|
||||
await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 65, currentBackgroundTaskState: currentBackgroundTaskState); // Adjusted progress
|
||||
|
||||
await using var thumbnailFileStream = new FileStream(thumbnailDiskPath, FileMode.Open, FileAccess.Read);
|
||||
var thumbnailStorageFileName = Path.GetFileNameWithoutExtension(picture.Path.Split('/').LastOrDefault() ?? picture.Name) + "_thumb.webp";
|
||||
// Use the new naming convention for storage
|
||||
var thumbnailStorageFileName = $"{baseNameFromOriginalPath}-thumbnail.webp";
|
||||
|
||||
string storedThumbnailPath = await storageService.ExecuteAsync(
|
||||
picture.StorageType,
|
||||
@@ -129,104 +147,18 @@ namespace Foxel.Services.Background.Processors
|
||||
|
||||
if (Directory.Exists(tempThumbContainer)) Directory.Delete(tempThumbContainer, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (picture.StorageType != StorageType.Local && !string.IsNullOrEmpty(picture.ThumbnailPath))
|
||||
{
|
||||
thumbnailForAI = await storageService.ExecuteAsync(picture.StorageType,
|
||||
provider => provider.DownloadFileAsync(picture.ThumbnailPath));
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(picture.ThumbnailPath))
|
||||
{
|
||||
// 对于本地存储的缩略图,也基于 ContentRootPath 构建路径
|
||||
thumbnailForAI = Path.Combine(contentRootPath, picture.ThumbnailPath.TrimStart('/'));
|
||||
}
|
||||
}
|
||||
|
||||
if (!File.Exists(thumbnailForAI) && isTempFile) // If thumbnailForAI was meant to be a temp file but doesn't exist, re-download or handle
|
||||
{
|
||||
_logger.LogWarning("AI分析所需的缩略图文件不存在: {ThumbnailPath}", thumbnailForAI);
|
||||
// Fallback or error handling if thumbnailForAI is critical
|
||||
if (string.IsNullOrEmpty(picture.ThumbnailPath) || picture.StorageType == StorageType.Local)
|
||||
{
|
||||
thumbnailForAI = localFilePath; // Fallback to original if thumbnail is missing and was supposed to be local or not generated
|
||||
}
|
||||
else
|
||||
{
|
||||
// Attempt to re-download if it was from remote storage
|
||||
thumbnailForAI = await storageService.ExecuteAsync(picture.StorageType, provider => provider.DownloadFileAsync(picture.ThumbnailPath!));
|
||||
if (!File.Exists(thumbnailForAI)) throw new Exception($"无法获取用于AI分析的缩略图: {picture.ThumbnailPath}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 30, currentBackgroundTaskState: currentBackgroundTaskState);
|
||||
await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 80, currentBackgroundTaskState: currentBackgroundTaskState); // Adjusted progress
|
||||
var exifInfo = await ImageHelper.ExtractExifInfoAsync(localFilePath);
|
||||
picture.ExifInfo = exifInfo;
|
||||
picture.TakenAt = ImageHelper.ParseExifDateTime(exifInfo.DateTimeOriginal);
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Processing, 50, currentBackgroundTaskState: currentBackgroundTaskState);
|
||||
string base64Image = await ImageHelper.ConvertImageToBase64(thumbnailForAI);
|
||||
var (title, description) = await aiService.AnalyzeImageAsync(base64Image);
|
||||
|
||||
string finalTitle = !string.IsNullOrWhiteSpace(title) && title != "AI生成的标题" ? title : Path.GetFileNameWithoutExtension(picture.Name);
|
||||
string finalDescription = !string.IsNullOrWhiteSpace(description) && description != "AI生成的描述" ? description : picture.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,
|
||||
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 (picture.User != null && matchedTagNames.Any())
|
||||
{
|
||||
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);
|
||||
}
|
||||
if (!picture.Tags.Any(t => t.Id == existingTag.Id)) picture.Tags.Add(existingTag);
|
||||
|
||||
picture.User.Tags ??= new List<Tag>();
|
||||
if (!picture.User.Tags.Any(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);
|
||||
_logger.LogError(ex, "图片元数据处理任务失败: TaskId={TaskId}, PictureId={PictureId}", currentBackgroundTaskState.Id, pictureId);
|
||||
await UpdateTaskStatusInDb(currentBackgroundTaskState.Id, TaskExecutionStatus.Failed, currentBackgroundTaskState.Progress, ex.Message, currentBackgroundTaskState: currentBackgroundTaskState);
|
||||
if (picture != null)
|
||||
{
|
||||
// Potentially update picture entity itself if needed, though task failure is primary
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -234,11 +166,6 @@ namespace Foxel.Services.Background.Processors
|
||||
{
|
||||
try { File.Delete(localFilePath); } catch (Exception ex) { _logger.LogWarning(ex, "删除临时主图片文件失败: {FilePath}", localFilePath); }
|
||||
}
|
||||
bool thumbnailIsTemp = thumbnailForAI.StartsWith(Path.GetTempPath());
|
||||
if (thumbnailIsTemp && thumbnailForAI != localFilePath && File.Exists(thumbnailForAI))
|
||||
{
|
||||
try { File.Delete(thumbnailForAI); } catch (Exception ex) { _logger.LogWarning(ex, "删除临时缩略图文件失败: {FilePath}", thumbnailForAI); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user