using DramaLing.Api.Data; using DramaLing.Api.Models.Entities; using DramaLing.Api.Services.Storage; using DramaLing.Api.Services; namespace DramaLing.Api.Services.AI.Generation; public class ImageSaveManager : IImageSaveManager { private readonly ILogger _logger; public ImageSaveManager(ILogger logger) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } public async Task SaveGeneratedImageAsync( DramaLingDbContext dbContext, IImageStorageService storageService, IImageProcessingService imageProcessingService, ImageGenerationRequest request, string optimizedPrompt, ReplicateImageResult imageResult) { // 下載原圖 (1024x1024) using var httpClient = new HttpClient(); var originalBytes = await httpClient.GetByteArrayAsync(imageResult.ImageUrl); _logger.LogInformation("Downloaded original image: {OriginalSize}KB", originalBytes.Length / 1024); // 壓縮為 512x512 var resizedBytes = await imageProcessingService.ResizeImageAsync(originalBytes, 512, 512); var imageStream = new MemoryStream(resizedBytes); // 生成檔案名稱 var fileName = $"{request.FlashcardId}_{Guid.NewGuid()}.png"; // 儲存到本地/雲端 var relativePath = await storageService.SaveImageAsync(imageStream, fileName); // 建立 ExampleImage 記錄 var exampleImage = new ExampleImage { Id = Guid.NewGuid(), RelativePath = relativePath, AltText = $"Example image for {request.Flashcard?.Word}", GeminiPrompt = request.GeminiPrompt, GeminiDescription = request.GeneratedDescription, ReplicatePrompt = optimizedPrompt, ReplicateModel = "ideogram-v2a-turbo", GeminiCost = request.GeminiCost ?? 0.002m, ReplicateCost = imageResult.Cost, TotalGenerationCost = (request.GeminiCost ?? 0.002m) + imageResult.Cost, FileSize = resizedBytes.Length, ImageWidth = 512, ImageHeight = 512, ContentHash = ComputeHash(resizedBytes), ModerationStatus = "pending", CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }; dbContext.ExampleImages.Add(exampleImage); // 建立詞卡圖片關聯 var flashcardImage = new FlashcardExampleImage { FlashcardId = request.FlashcardId, ExampleImageId = exampleImage.Id, DisplayOrder = 1, IsPrimary = true, ContextRelevance = 1.0m, CreatedAt = DateTime.UtcNow }; dbContext.FlashcardExampleImages.Add(flashcardImage); await dbContext.SaveChangesAsync(); return exampleImage; } private static string ComputeHash(byte[] bytes) { using var sha256 = System.Security.Cryptography.SHA256.Create(); var hashBytes = sha256.ComputeHash(bytes); return Convert.ToHexString(hashBytes); } }