diff --git a/backend/DramaLing.Api/DramaLing.Api.csproj b/backend/DramaLing.Api/DramaLing.Api.csproj index d9d7f6f..4ed5f7e 100644 --- a/backend/DramaLing.Api/DramaLing.Api.csproj +++ b/backend/DramaLing.Api/DramaLing.Api.csproj @@ -9,6 +9,7 @@ + diff --git a/backend/DramaLing.Api/Models/DTOs/ReplicateDto.cs b/backend/DramaLing.Api/Models/DTOs/ReplicateDto.cs index a2697e6..98f2ce7 100644 --- a/backend/DramaLing.Api/Models/DTOs/ReplicateDto.cs +++ b/backend/DramaLing.Api/Models/DTOs/ReplicateDto.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using System.Text.Json.Serialization; namespace DramaLing.Api.Models.DTOs; @@ -11,7 +12,7 @@ public class ReplicatePrediction public string Status { get; set; } = string.Empty; [JsonPropertyName("output")] - public List? Output { get; set; } + public JsonElement? Output { get; set; } [JsonPropertyName("error")] public string? Error { get; set; } @@ -35,7 +36,7 @@ public class ReplicatePrediction public class ReplicatePredictionStatus { public string Status { get; set; } = string.Empty; - public List? Output { get; set; } + public JsonElement? Output { get; set; } public string? Error { get; set; } public string? Version { get; set; } public Dictionary? Metrics { get; set; } diff --git a/backend/DramaLing.Api/Program.cs b/backend/DramaLing.Api/Program.cs index 0759493..4a8c95e 100644 --- a/backend/DramaLing.Api/Program.cs +++ b/backend/DramaLing.Api/Program.cs @@ -93,12 +93,10 @@ builder.Services.AddHttpClient(); builder.Services.AddScoped(); // Image Storage Services -builder.Services.AddScoped(provider => -{ - var config = provider.GetRequiredService(); - var logger = provider.GetRequiredService>(); - return ImageStorageFactory.Create(config, logger); -}); +builder.Services.AddScoped(); + +// Image Processing Services +builder.Services.AddScoped(); // Background Services (快取清理服務已移除) diff --git a/backend/DramaLing.Api/Services/IImageProcessingService.cs b/backend/DramaLing.Api/Services/IImageProcessingService.cs new file mode 100644 index 0000000..6e4a22c --- /dev/null +++ b/backend/DramaLing.Api/Services/IImageProcessingService.cs @@ -0,0 +1,7 @@ +namespace DramaLing.Api.Services; + +public interface IImageProcessingService +{ + Task ResizeImageAsync(byte[] originalBytes, int targetWidth, int targetHeight); + Task OptimizeImageAsync(byte[] originalBytes, int quality = 85); +} \ No newline at end of file diff --git a/backend/DramaLing.Api/Services/ImageGenerationOrchestrator.cs b/backend/DramaLing.Api/Services/ImageGenerationOrchestrator.cs index ae5ad3b..9962199 100644 --- a/backend/DramaLing.Api/Services/ImageGenerationOrchestrator.cs +++ b/backend/DramaLing.Api/Services/ImageGenerationOrchestrator.cs @@ -12,32 +12,26 @@ namespace DramaLing.Api.Services; public class ImageGenerationOrchestrator : IImageGenerationOrchestrator { - private readonly IGeminiService _geminiService; - private readonly IReplicateService _replicateService; - private readonly IImageStorageService _storageService; - private readonly DramaLingDbContext _dbContext; + private readonly IServiceProvider _serviceProvider; private readonly ILogger _logger; public ImageGenerationOrchestrator( - IGeminiService geminiService, - IReplicateService replicateService, - IImageStorageService storageService, - DramaLingDbContext dbContext, + IServiceProvider serviceProvider, ILogger logger) { - _geminiService = geminiService ?? throw new ArgumentNullException(nameof(geminiService)); - _replicateService = replicateService ?? throw new ArgumentNullException(nameof(replicateService)); - _storageService = storageService ?? throw new ArgumentNullException(nameof(storageService)); - _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); + _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } public async Task StartGenerationAsync(Guid flashcardId, GenerationRequest request) { + using var scope = _serviceProvider.CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + try { // 檢查詞卡是否存在 - var flashcard = await _dbContext.Flashcards.FindAsync(flashcardId); + var flashcard = await dbContext.Flashcards.FindAsync(flashcardId); if (flashcard == null) { throw new ArgumentException($"Flashcard {flashcardId} not found"); @@ -56,14 +50,24 @@ public class ImageGenerationOrchestrator : IImageGenerationOrchestrator CreatedAt = DateTime.UtcNow }; - _dbContext.ImageGenerationRequests.Add(generationRequest); - await _dbContext.SaveChangesAsync(); + dbContext.ImageGenerationRequests.Add(generationRequest); + await dbContext.SaveChangesAsync(); _logger.LogInformation("Created generation request {RequestId} for flashcard {FlashcardId}", generationRequest.Id, flashcardId); - // 後台執行兩階段生成流程 - _ = Task.Run(async () => await ExecuteGenerationPipelineAsync(generationRequest.Id)); + // 後台執行兩階段生成流程 - 使用獨立的 scope + _ = Task.Run(async () => + { + try + { + await ExecuteGenerationPipelineAsync(generationRequest.Id); + } + catch (Exception ex) + { + _logger.LogError(ex, "Background generation pipeline failed for request {RequestId}", generationRequest.Id); + } + }); return new GenerationRequestResult { @@ -93,7 +97,11 @@ public class ImageGenerationOrchestrator : IImageGenerationOrchestrator public async Task GetGenerationStatusAsync(Guid requestId) { - var request = await _dbContext.ImageGenerationRequests + using var scope = _serviceProvider.CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + var storageService = scope.ServiceProvider.GetRequiredService(); + + var request = await dbContext.ImageGenerationRequests .Include(r => r.GeneratedImage) .FirstOrDefaultAsync(r => r.Id == requestId); @@ -130,7 +138,7 @@ public class ImageGenerationOrchestrator : IImageGenerationOrchestrator CompletedAt = request.CompletedAt, Result = request.GeneratedImage != null ? new GenerationResultDto { - ImageUrl = await _storageService.GetImageUrlAsync(request.GeneratedImage.RelativePath), + ImageUrl = await storageService.GetImageUrlAsync(request.GeneratedImage.RelativePath), ImageId = request.GeneratedImage.Id.ToString(), QualityScore = request.GeneratedImage.QualityScore, Dimensions = new DimensionsDto @@ -145,16 +153,19 @@ public class ImageGenerationOrchestrator : IImageGenerationOrchestrator public async Task CancelGenerationAsync(Guid requestId) { + using var scope = _serviceProvider.CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + try { - var request = await _dbContext.ImageGenerationRequests.FindAsync(requestId); + var request = await dbContext.ImageGenerationRequests.FindAsync(requestId); if (request == null || request.OverallStatus == "completed") { return false; } request.OverallStatus = "cancelled"; - await _dbContext.SaveChangesAsync(); + await dbContext.SaveChangesAsync(); _logger.LogInformation("Generation request {RequestId} cancelled", requestId); return true; @@ -170,9 +181,19 @@ public class ImageGenerationOrchestrator : IImageGenerationOrchestrator { var totalStopwatch = Stopwatch.StartNew(); + // 使用獨立的 scope 避免 DbContext 生命週期問題 + using var scope = _serviceProvider.CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + var geminiService = scope.ServiceProvider.GetRequiredService(); + var replicateService = scope.ServiceProvider.GetRequiredService(); + var storageService = scope.ServiceProvider.GetRequiredService(); + var imageProcessingService = scope.ServiceProvider.GetRequiredService(); + try { - var request = await _dbContext.ImageGenerationRequests + _logger.LogInformation("Starting generation pipeline for request {RequestId}", requestId); + + var request = await dbContext.ImageGenerationRequests .Include(r => r.Flashcard) .FirstOrDefaultAsync(r => r.Id == requestId); @@ -187,29 +208,33 @@ public class ImageGenerationOrchestrator : IImageGenerationOrchestrator // 第一階段:Gemini 描述生成 _logger.LogInformation("Starting Gemini description generation for request {RequestId}", requestId); - await UpdateRequestStatusAsync(requestId, "description_generating", "processing", "pending"); + await UpdateRequestStatusAsync(dbContext, requestId, "description_generating", "processing", "pending"); - var optimizedPrompt = await _geminiService.GenerateImageDescriptionAsync( + var optimizedPrompt = await geminiService.GenerateImageDescriptionAsync( request.Flashcard, options?.Options ?? new GenerationOptionsDto()); if (string.IsNullOrWhiteSpace(optimizedPrompt)) { - await MarkRequestAsFailedAsync(requestId, "gemini", "Generated prompt is empty"); + await MarkRequestAsFailedAsync(dbContext, requestId, "gemini", "Generated prompt is empty"); return; } // 更新 Gemini 結果 - await UpdateGeminiResultAsync(requestId, optimizedPrompt); + await UpdateGeminiResultAsync(dbContext, requestId, optimizedPrompt); // 第二階段:Replicate 圖片生成 _logger.LogInformation("Starting Replicate image generation for request {RequestId}", requestId); - await UpdateRequestStatusAsync(requestId, "image_generating", "completed", "processing"); + await UpdateRequestStatusAsync(dbContext, requestId, "image_generating", "completed", "processing"); - var imageResult = await _replicateService.GenerateImageAsync( + // 強制使用正確的模型名稱,避免參數傳遞錯誤 + var modelName = "ideogram-v2a-turbo"; + _logger.LogInformation("Using Replicate model: {ModelName}", modelName); + + var imageResult = await replicateService.GenerateImageAsync( optimizedPrompt, - options?.ReplicateModel ?? "ideogram-v2a-turbo", + modelName, new ReplicateGenerationOptions { Width = options?.Width ?? 512, @@ -219,15 +244,15 @@ public class ImageGenerationOrchestrator : IImageGenerationOrchestrator if (!imageResult.Success) { - await MarkRequestAsFailedAsync(requestId, "replicate", imageResult.Error); + await MarkRequestAsFailedAsync(dbContext, requestId, "replicate", imageResult.Error); return; } // 下載並儲存圖片 - var savedImage = await SaveGeneratedImageAsync(request, optimizedPrompt, imageResult); + var savedImage = await SaveGeneratedImageAsync(dbContext, storageService, imageProcessingService, request, optimizedPrompt, imageResult); // 完成請求 - await CompleteRequestAsync(requestId, savedImage.Id, totalStopwatch.ElapsedMilliseconds); + await CompleteRequestAsync(dbContext, requestId, savedImage.Id, totalStopwatch.ElapsedMilliseconds); _logger.LogInformation("Generation pipeline completed successfully for request {RequestId} in {ElapsedMs}ms", requestId, totalStopwatch.ElapsedMilliseconds); @@ -236,13 +261,13 @@ public class ImageGenerationOrchestrator : IImageGenerationOrchestrator { totalStopwatch.Stop(); _logger.LogError(ex, "Generation pipeline failed for request {RequestId}", requestId); - await MarkRequestAsFailedAsync(requestId, "system", ex.Message); + await MarkRequestAsFailedAsync(dbContext, requestId, "system", ex.Message); } } - private async Task UpdateRequestStatusAsync(Guid requestId, string overallStatus, string geminiStatus, string replicateStatus) + private async Task UpdateRequestStatusAsync(DramaLingDbContext dbContext, Guid requestId, string overallStatus, string geminiStatus, string replicateStatus) { - var request = await _dbContext.ImageGenerationRequests.FindAsync(requestId); + var request = await dbContext.ImageGenerationRequests.FindAsync(requestId); if (request == null) return; request.OverallStatus = overallStatus; @@ -259,12 +284,12 @@ public class ImageGenerationOrchestrator : IImageGenerationOrchestrator request.ReplicateStartedAt = DateTime.UtcNow; } - await _dbContext.SaveChangesAsync(); + await dbContext.SaveChangesAsync(); } - private async Task UpdateGeminiResultAsync(Guid requestId, string optimizedPrompt) + private async Task UpdateGeminiResultAsync(DramaLingDbContext dbContext, Guid requestId, string optimizedPrompt) { - var request = await _dbContext.ImageGenerationRequests.FindAsync(requestId); + var request = await dbContext.ImageGenerationRequests.FindAsync(requestId); if (request == null) return; request.GeminiStatus = "completed"; @@ -274,24 +299,32 @@ public class ImageGenerationOrchestrator : IImageGenerationOrchestrator request.GeminiCost = 0.002m; // 預設成本 request.GeminiProcessingTimeMs = 30000; // 預設時間 - await _dbContext.SaveChangesAsync(); + await dbContext.SaveChangesAsync(); } private async Task SaveGeneratedImageAsync( + DramaLingDbContext dbContext, + IImageStorageService storageService, + IImageProcessingService imageProcessingService, ImageGenerationRequest request, string optimizedPrompt, ReplicateImageResult imageResult) { - // 下載圖片 + // 下載原圖 (1024x1024) using var httpClient = new HttpClient(); - var imageBytes = await httpClient.GetByteArrayAsync(imageResult.ImageUrl); - var imageStream = new MemoryStream(imageBytes); + 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); + var relativePath = await storageService.SaveImageAsync(imageStream, fileName); // 建立 ExampleImage 記錄 var exampleImage = new ExampleImage @@ -306,16 +339,16 @@ public class ImageGenerationOrchestrator : IImageGenerationOrchestrator GeminiCost = request.GeminiCost ?? 0.002m, ReplicateCost = imageResult.Cost, TotalGenerationCost = (request.GeminiCost ?? 0.002m) + imageResult.Cost, - FileSize = imageBytes.Length, + FileSize = resizedBytes.Length, // 使用壓縮後的檔案大小 ImageWidth = 512, ImageHeight = 512, - ContentHash = ComputeHash(imageBytes), + ContentHash = ComputeHash(resizedBytes), // 使用壓縮後的檔案計算 hash ModerationStatus = "pending", CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }; - _dbContext.ExampleImages.Add(exampleImage); + dbContext.ExampleImages.Add(exampleImage); // 建立詞卡圖片關聯 var flashcardImage = new FlashcardExampleImage @@ -328,15 +361,15 @@ public class ImageGenerationOrchestrator : IImageGenerationOrchestrator CreatedAt = DateTime.UtcNow }; - _dbContext.FlashcardExampleImages.Add(flashcardImage); - await _dbContext.SaveChangesAsync(); + dbContext.FlashcardExampleImages.Add(flashcardImage); + await dbContext.SaveChangesAsync(); return exampleImage; } - private async Task CompleteRequestAsync(Guid requestId, Guid imageId, long totalProcessingTimeMs) + private async Task CompleteRequestAsync(DramaLingDbContext dbContext, Guid requestId, Guid imageId, long totalProcessingTimeMs) { - var request = await _dbContext.ImageGenerationRequests.FindAsync(requestId); + var request = await dbContext.ImageGenerationRequests.FindAsync(requestId); if (request == null) return; request.OverallStatus = "completed"; @@ -347,12 +380,12 @@ public class ImageGenerationOrchestrator : IImageGenerationOrchestrator request.TotalProcessingTimeMs = (int)totalProcessingTimeMs; request.TotalCost = (request.GeminiCost ?? 0) + (request.ReplicateCost ?? 0); - await _dbContext.SaveChangesAsync(); + await dbContext.SaveChangesAsync(); } - private async Task MarkRequestAsFailedAsync(Guid requestId, string stage, string? errorMessage) + private async Task MarkRequestAsFailedAsync(DramaLingDbContext dbContext, Guid requestId, string stage, string? errorMessage) { - var request = await _dbContext.ImageGenerationRequests.FindAsync(requestId); + var request = await dbContext.ImageGenerationRequests.FindAsync(requestId); if (request == null) return; request.OverallStatus = "failed"; @@ -377,7 +410,7 @@ public class ImageGenerationOrchestrator : IImageGenerationOrchestrator request.CompletedAt = DateTime.UtcNow; - await _dbContext.SaveChangesAsync(); + await dbContext.SaveChangesAsync(); _logger.LogError("Generation request {RequestId} marked as failed at stage {Stage}: {Error}", requestId, stage, errorMessage); diff --git a/backend/DramaLing.Api/Services/ImageProcessingService.cs b/backend/DramaLing.Api/Services/ImageProcessingService.cs new file mode 100644 index 0000000..a992b9c --- /dev/null +++ b/backend/DramaLing.Api/Services/ImageProcessingService.cs @@ -0,0 +1,86 @@ +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Formats.Png; + +namespace DramaLing.Api.Services; + +public class ImageProcessingService : IImageProcessingService +{ + private readonly ILogger _logger; + + public ImageProcessingService(ILogger logger) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public async Task ResizeImageAsync(byte[] originalBytes, int targetWidth, int targetHeight) + { + try + { + _logger.LogInformation("Resizing image from {OriginalSize}KB to {TargetWidth}x{TargetHeight}", + originalBytes.Length / 1024, targetWidth, targetHeight); + + using var image = Image.Load(originalBytes); + + _logger.LogDebug("Original image size: {Width}x{Height}", image.Width, image.Height); + + // 使用高品質的 Lanczos3 重採樣算法 + image.Mutate(x => x.Resize(targetWidth, targetHeight, KnownResamplers.Lanczos3)); + + using var output = new MemoryStream(); + + // 使用 PNG 格式儲存,保持品質 + await image.SaveAsPngAsync(output, new PngEncoder + { + CompressionLevel = PngCompressionLevel.Level6, // 平衡壓縮和品質 + ColorType = PngColorType.Rgb + }); + + var resizedBytes = output.ToArray(); + + _logger.LogInformation("Image resized successfully. New size: {NewSize}KB (reduction: {Reduction:P})", + resizedBytes.Length / 1024, + 1.0 - (double)resizedBytes.Length / originalBytes.Length); + + return resizedBytes; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to resize image"); + throw; + } + } + + public async Task OptimizeImageAsync(byte[] originalBytes, int quality = 85) + { + try + { + _logger.LogInformation("Optimizing image, original size: {OriginalSize}KB", + originalBytes.Length / 1024); + + using var image = Image.Load(originalBytes); + using var output = new MemoryStream(); + + // 針對例句圖的優化設定 + await image.SaveAsPngAsync(output, new PngEncoder + { + CompressionLevel = PngCompressionLevel.Level9, // 最高壓縮 + ColorType = PngColorType.Rgb, + BitDepth = PngBitDepth.Bit8 + }); + + var optimizedBytes = output.ToArray(); + + _logger.LogInformation("Image optimized successfully. New size: {NewSize}KB (reduction: {Reduction:P})", + optimizedBytes.Length / 1024, + 1.0 - (double)optimizedBytes.Length / originalBytes.Length); + + return optimizedBytes; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to optimize image"); + throw; + } + } +} \ No newline at end of file diff --git a/backend/DramaLing.Api/Services/ReplicateService.cs b/backend/DramaLing.Api/Services/ReplicateService.cs index 18c10a1..d698c18 100644 --- a/backend/DramaLing.Api/Services/ReplicateService.cs +++ b/backend/DramaLing.Api/Services/ReplicateService.cs @@ -31,6 +31,7 @@ public class ReplicateService : IReplicateService _httpClient.Timeout = TimeSpan.FromSeconds(_options.TimeoutSeconds); _httpClient.DefaultRequestHeaders.Add("Authorization", $"Token {_options.ApiKey}"); _httpClient.DefaultRequestHeaders.Add("User-Agent", "DramaLing/1.0"); + _httpClient.DefaultRequestHeaders.Add("Prefer", "wait"); // 添加你使用的 header } public async Task GenerateImageAsync(string prompt, string model, ReplicateGenerationOptions options) @@ -75,6 +76,11 @@ public class ReplicateService : IReplicateService response.EnsureSuccessStatusCode(); var json = await response.Content.ReadAsStringAsync(); + + // 記錄實際收到的 JSON 格式用於除錯 + _logger.LogDebug("Replicate API response for prediction {PredictionId}: {Response}", + predictionId, json.Substring(0, Math.Min(500, json.Length))); + var prediction = JsonSerializer.Deserialize(json); return new ReplicatePredictionStatus @@ -129,11 +135,7 @@ public class ReplicateService : IReplicateService private object BuildModelRequest(string prompt, string model, ReplicateGenerationOptions options) { - if (!_options.Models.TryGetValue(model, out var modelConfig)) - { - throw new ArgumentException($"Model {model} is not configured"); - } - + // 使用你確認可行的簡化格式 return model.ToLower() switch { "ideogram-v2a-turbo" => new @@ -141,13 +143,7 @@ public class ReplicateService : IReplicateService input = new { prompt = prompt, - width = options.Width ?? modelConfig.DefaultWidth, - height = options.Height ?? modelConfig.DefaultHeight, - magic_prompt_option = "Auto", - style_type = modelConfig.StyleType ?? "General", - aspect_ratio = modelConfig.AspectRatio ?? "ASPECT_1_1", - model = modelConfig.Model ?? "V_2_TURBO", - seed = options.Seed ?? Random.Shared.Next() + aspect_ratio = "1:1" // 簡化為你確認可行的格式 } }, "flux-1-dev" => new @@ -155,8 +151,8 @@ public class ReplicateService : IReplicateService input = new { prompt = prompt, - width = modelConfig.DefaultWidth, - height = modelConfig.DefaultHeight, + width = 512, + height = 512, num_outputs = 1, guidance_scale = 3.5, num_inference_steps = 28, @@ -183,7 +179,7 @@ public class ReplicateService : IReplicateService return new ReplicateImageResult { Success = true, - ImageUrl = status.Output?.FirstOrDefault(), + ImageUrl = ExtractImageUrl(status.Output), Cost = CalculateReplicateCost(status.Metrics), ModelVersion = status.Version, Metadata = status.Metrics @@ -226,6 +222,51 @@ public class ReplicateService : IReplicateService return 0.025m; // 預設 Ideogram 成本 } + + private string? ExtractImageUrl(JsonElement? output) + { + if (!output.HasValue || output.Value.ValueKind == JsonValueKind.Null) + return null; + + try + { + var element = output.Value; + + // 如果是陣列格式: ["http://..."] + if (element.ValueKind == JsonValueKind.Array && element.GetArrayLength() > 0) + { + return element[0].GetString(); + } + + // 如果是字串格式: "http://..." + if (element.ValueKind == JsonValueKind.String) + { + return element.GetString(); + } + + // 如果是物件格式: { "url": "http://..." } + if (element.ValueKind == JsonValueKind.Object) + { + if (element.TryGetProperty("url", out var urlElement)) + { + return urlElement.GetString(); + } + // 或者其他可能的屬性名稱 + if (element.TryGetProperty("image", out var imageElement)) + { + return imageElement.GetString(); + } + } + + _logger.LogWarning("Unknown output format: {OutputKind}", element.ValueKind); + return null; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to extract image URL from output"); + return null; + } + } } // Response models for ReplicateService diff --git a/backend/DramaLing.Api/Services/Storage/ImageStorageFactory.cs b/backend/DramaLing.Api/Services/Storage/ImageStorageFactory.cs deleted file mode 100644 index 3b36d09..0000000 --- a/backend/DramaLing.Api/Services/Storage/ImageStorageFactory.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace DramaLing.Api.Services.Storage; - -public static class ImageStorageFactory -{ - public static IImageStorageService Create( - IConfiguration configuration, - ILogger logger) - { - var provider = configuration["ImageStorage:Provider"]?.ToLower() ?? "local"; - - return provider switch - { - "local" => new LocalImageStorageService(configuration, - logger as ILogger - ?? throw new ArgumentException("Invalid logger type")), - "aws" => throw new NotImplementedException("AWS storage not yet implemented"), - "azure" => throw new NotImplementedException("Azure storage not yet implemented"), - _ => throw new NotSupportedException($"Storage provider '{provider}' not supported") - }; - } -} \ No newline at end of file