From 179cbc625880157c41e52c8cd9395fa859cf6186 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=84=AD=E6=B2=9B=E8=BB=92?= Date: Wed, 24 Sep 2025 18:51:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E4=BE=8B=E5=8F=A5?= =?UTF-8?q?=E5=9C=96=E7=94=9F=E6=88=90=E5=BE=8C=E7=AB=AF=E9=96=8B=E7=99=BC?= =?UTF-8?q?=E8=A8=88=E5=8A=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 基於當前 ASP.NET Core 架構分析,制定完整的兩階段圖片生成系統開發計劃: **架構分析**: - ✅ 已具備:Gemini 整合、EF Core、JWT 認證、快取服務 - ❌ 需新增:Replicate API、流程編排器、儲存抽象層 **開發規劃** (6-8週): - Phase 1: 資料庫 Schema 擴展和基礎配置 - Phase 2: Gemini 描述生成和 Replicate 圖片生成服務 - Phase 3: API 端點和兩階段流程編排器 - Phase 4: 快取優化和成本控制系統 **技術細節**: - 具體的 C# 程式碼範例和檔案結構 - 完整的環境配置和 NuGet 套件需求 - 測試策略和部署檢查清單 - 與現有架構的整合方案 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- ...AGE_GENERATION_BACKEND_DEVELOPMENT_PLAN.md | 700 ++++++++++++++++++ 1 file changed, 700 insertions(+) create mode 100644 EXAMPLE_IMAGE_GENERATION_BACKEND_DEVELOPMENT_PLAN.md diff --git a/EXAMPLE_IMAGE_GENERATION_BACKEND_DEVELOPMENT_PLAN.md b/EXAMPLE_IMAGE_GENERATION_BACKEND_DEVELOPMENT_PLAN.md new file mode 100644 index 0000000..e0d069a --- /dev/null +++ b/EXAMPLE_IMAGE_GENERATION_BACKEND_DEVELOPMENT_PLAN.md @@ -0,0 +1,700 @@ +# 例句圖生成功能後端開發計劃 + +## 📋 當前架構評估 + +### ✅ 已具備的基礎架構 +- **ASP.NET Core 8.0** + EF Core 8.0 + SQLite +- **Gemini AI 整合** (`GeminiService.cs` 已實現) +- **依賴注入架構** 完整配置 +- **JWT 認證機制** 已建立 +- **錯誤處理中介軟體** 已實現 +- **快取服務** (`HybridCacheService`) 可重用 + +### ❌ 需要新增的組件 +- **Replicate API 整合服務** +- **兩階段流程編排器** +- **圖片儲存抽象層** +- **資料庫 Schema 擴展** +- **新的 API 端點** + +## 🎯 開發目標 + +基於現有架構,實現 **Gemini + Replicate 兩階段例句圖生成系統**,預估開發時間 **6-8 週**。 + +--- + +## 📅 Phase 1: 基礎架構擴展 (Week 1-2) + +### Week 1: 資料庫 Schema 擴展 + +#### 1.1 新增資料表 Migration +```bash +dotnet ef migrations add AddImageGenerationTables +``` + +**需要新增的表格**: +- `example_images` (例句圖片表) +- `flashcard_example_images` (關聯表) +- `image_generation_requests` (生成請求追蹤表) + +#### 1.2 實體模型建立 +**檔案位置**: `/Models/Entities/` + +```csharp +// ExampleImage.cs +public class ExampleImage +{ + public Guid Id { get; set; } + public string RelativePath { get; set; } + public string? AltText { get; set; } + public string? GeminiPrompt { get; set; } + public string? GeminiDescription { get; set; } + public string? ReplicatePrompt { get; set; } + public string ReplicateModel { get; set; } + public decimal? GeminiCost { get; set; } + public decimal? ReplicateCost { get; set; } + public decimal? TotalGenerationCost { get; set; } + // ... 其他欄位參考 PRD +} + +// ImageGenerationRequest.cs +public class ImageGenerationRequest +{ + public Guid Id { get; set; } + public Guid UserId { get; set; } + public Guid FlashcardId { get; set; } + public string OverallStatus { get; set; } // pending/description_generating/image_generating/completed/failed + public string GeminiStatus { get; set; } + public string ReplicateStatus { get; set; } + // ... 兩階段追蹤欄位 +} +``` + +#### 1.3 DbContext 更新 +**檔案**: `/Data/DramaLingDbContext.cs` +```csharp +public DbSet ExampleImages { get; set; } +public DbSet ImageGenerationRequests { get; set; } +public DbSet FlashcardExampleImages { get; set; } + +// 在 OnModelCreating 中配置關聯 +``` + +### Week 2: 配置和基礎服務 + +#### 2.1 Replicate 配置選項 +**檔案**: `/Models/Configuration/ReplicateOptions.cs` +```csharp +public class ReplicateOptions +{ + public const string SectionName = "Replicate"; + + [Required] + public string ApiKey { get; set; } = string.Empty; + + public string BaseUrl { get; set; } = "https://api.replicate.com/v1"; + + [Range(1, 300)] + public int TimeoutSeconds { get; set; } = 180; + + public Dictionary Models { get; set; } = new(); +} + +public class ModelConfig +{ + public string Version { get; set; } + public decimal CostPerGeneration { get; set; } + public int DefaultWidth { get; set; } = 512; + public int DefaultHeight { get; set; } = 512; +} +``` + +#### 2.2 儲存抽象層介面定義 +**檔案**: `/Services/Storage/IImageStorageService.cs` +```csharp +public interface IImageStorageService +{ + Task SaveImageAsync(Stream imageStream, string fileName); + Task GetImageUrlAsync(string imagePath); + Task DeleteImageAsync(string imagePath); + Task GetStorageInfoAsync(); +} + +public class LocalImageStorageService : IImageStorageService +{ + // 開發環境實現 +} +``` + +#### 2.3 Program.cs 服務註冊更新 +```csharp +// 新增 Replicate 配置 +builder.Services.Configure( + builder.Configuration.GetSection(ReplicateOptions.SectionName)); +builder.Services.AddSingleton, ReplicateOptionsValidator>(); + +// 新增圖片生成服務 +builder.Services.AddHttpClient(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +// 新增儲存服務 +builder.Services.AddScoped(provider => +{ + var config = provider.GetRequiredService(); + return ImageStorageFactory.Create(config, provider.GetRequiredService>()); +}); +``` + +--- + +## 📅 Phase 2: 核心服務實現 (Week 3-4) + +### Week 3: Gemini 描述生成服務 + +#### 3.1 擴展現有 GeminiService +**檔案**: `/Services/AI/GeminiImageDescriptionService.cs` + +```csharp +public class GeminiImageDescriptionService : IGeminiImageDescriptionService +{ + private readonly GeminiService _geminiService; // 重用現有服務 + private readonly ILogger _logger; + + public async Task GenerateDescriptionAsync( + Flashcard flashcard, + GenerationOptions options) + { + var prompt = BuildImageDescriptionPrompt(flashcard, options); + + // 重用現有的 GeminiService.CallGeminiAPIAsync() + var response = await _geminiService.CallGeminiAPIAsync(prompt); + + return new ImageDescriptionResult + { + Success = true, + Description = ExtractDescription(response), + OptimizedPrompt = OptimizeForReplicate(response, options), + Cost = CalculateCost(prompt), + ProcessingTimeMs = stopwatch.ElapsedMilliseconds + }; + } + + private string BuildImageDescriptionPrompt(Flashcard flashcard, GenerationOptions options) + { + return $@"Generate a detailed image description for English learning flashcard. + +Word: {flashcard.Word} +Translation: {flashcard.Translation} +Example: {flashcard.Example} +Difficulty: {flashcard.DifficultyLevel} +Style: {options.Style} + +Create a vivid, educational scene description that clearly illustrates the word's meaning. +Return only the image description, no additional text."; + } +} +``` + +#### 3.2 資料模型和 DTOs +**檔案**: `/Models/DTOs/ImageGenerationDto.cs` +```csharp +public class ImageDescriptionResult +{ + public bool Success { get; set; } + public string? Description { get; set; } + public string? OptimizedPrompt { get; set; } + public decimal Cost { get; set; } + public int ProcessingTimeMs { get; set; } + public string? Error { get; set; } +} + +public class GenerationOptions +{ + public string Style { get; set; } = "realistic"; + public int Width { get; set; } = 512; + public int Height { get; set; } = 512; + public string ReplicateModel { get; set; } = "flux-1-dev"; + public bool UseCache { get; set; } = true; + public int TimeoutMinutes { get; set; } = 5; +} +``` + +### Week 4: Replicate 圖片生成服務 + +#### 4.1 Replicate API 整合 +**檔案**: `/Services/AI/ReplicateImageGenerationService.cs` + +```csharp +public class ReplicateImageGenerationService : IReplicateImageGenerationService +{ + private readonly HttpClient _httpClient; + private readonly ReplicateOptions _options; + private readonly ILogger _logger; + + public async Task GenerateImageAsync( + string prompt, + string model, + GenerationOptions options) + { + // 1. 啟動 Replicate 預測 + var prediction = await StartPredictionAsync(prompt, model, options); + + // 2. 輪詢檢查生成狀態 + var result = await WaitForCompletionAsync(prediction.Id, options.TimeoutMinutes); + + return result; + } + + private async Task StartPredictionAsync( + string prompt, + string model, + GenerationOptions options) + { + var requestBody = BuildModelRequest(prompt, model, options); + + var response = await _httpClient.PostAsync( + $"{_options.BaseUrl}/predictions", + new StringContent(JsonSerializer.Serialize(requestBody), Encoding.UTF8, "application/json")); + + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsStringAsync(); + return JsonSerializer.Deserialize(json); + } + + private async Task WaitForCompletionAsync( + string predictionId, + int timeoutMinutes) + { + var timeout = TimeSpan.FromMinutes(timeoutMinutes); + var pollInterval = TimeSpan.FromSeconds(2); + var startTime = DateTime.UtcNow; + + while (DateTime.UtcNow - startTime < timeout) + { + var status = await GetPredictionStatusAsync(predictionId); + + switch (status.Status) + { + case "succeeded": + return new ImageGenerationResult + { + Success = true, + ImageUrl = status.Output?.FirstOrDefault()?.ToString(), + ProcessingTimeMs = (int)(DateTime.UtcNow - startTime).TotalMilliseconds, + Cost = CalculateCost(status) + }; + + case "failed": + return new ImageGenerationResult + { + Success = false, + Error = status.Error?.ToString() ?? "Generation failed" + }; + + case "processing": + await Task.Delay(pollInterval); + continue; + } + } + + return new ImageGenerationResult + { + Success = false, + Error = "Generation timeout" + }; + } +} +``` + +--- + +## 📅 Phase 3: API 端點和流程編排 (Week 5-6) + +### Week 5: 兩階段流程編排器 + +#### 5.1 核心編排器 +**檔案**: `/Services/ImageGenerationOrchestrator.cs` + +```csharp +public class ImageGenerationOrchestrator : IImageGenerationOrchestrator +{ + private readonly IGeminiImageDescriptionService _geminiService; + private readonly IReplicateImageGenerationService _replicateService; + private readonly IImageStorageService _storageService; + private readonly DramaLingDbContext _dbContext; + + public async Task StartGenerationAsync( + Guid flashcardId, + GenerationRequest request) + { + // 1. 建立追蹤記錄 + var generationRequest = new ImageGenerationRequest + { + Id = Guid.NewGuid(), + UserId = request.UserId, + FlashcardId = flashcardId, + OverallStatus = "pending", + GeminiStatus = "pending", + ReplicateStatus = "pending", + OriginalRequest = JsonSerializer.Serialize(request), + CreatedAt = DateTime.UtcNow + }; + + _dbContext.ImageGenerationRequests.Add(generationRequest); + await _dbContext.SaveChangesAsync(); + + // 2. 後台執行兩階段生成 + _ = Task.Run(async () => await ExecuteGenerationPipelineAsync(generationRequest)); + + return new GenerationRequestResult + { + RequestId = generationRequest.Id, + Status = "pending", + EstimatedTimeMinutes = 3 + }; + } + + private async Task ExecuteGenerationPipelineAsync(ImageGenerationRequest request) + { + try + { + // 第一階段:Gemini 描述生成 + await UpdateRequestStatusAsync(request.Id, "description_generating"); + + var flashcard = await _dbContext.Flashcards.FindAsync(request.FlashcardId); + var options = JsonSerializer.Deserialize(request.OriginalRequest); + + var descriptionResult = await _geminiService.GenerateDescriptionAsync(flashcard, options); + + if (!descriptionResult.Success) + { + await MarkRequestAsFailedAsync(request.Id, "gemini", descriptionResult.Error); + return; + } + + // 更新 Gemini 結果 + await UpdateGeminiResultAsync(request.Id, descriptionResult); + + // 第二階段:Replicate 圖片生成 + await UpdateRequestStatusAsync(request.Id, "image_generating"); + + var imageResult = await _replicateService.GenerateImageAsync( + descriptionResult.OptimizedPrompt, + options.ReplicateModel, + options); + + if (!imageResult.Success) + { + await MarkRequestAsFailedAsync(request.Id, "replicate", imageResult.Error); + return; + } + + // 儲存圖片和完成請求 + var savedImage = await SaveGeneratedImageAsync(request, descriptionResult, imageResult); + await CompleteRequestAsync(request.Id, savedImage.Id); + + } + catch (Exception ex) + { + _logger.LogError(ex, "Generation pipeline failed for request {RequestId}", request.Id); + await MarkRequestAsFailedAsync(request.Id, "system", ex.Message); + } + } +} +``` + +### Week 6: API 控制器實現 + +#### 6.1 新增圖片生成控制器 +**檔案**: `/Controllers/ImageGenerationController.cs` + +```csharp +[Route("api/[controller]")] +[ApiController] +[Authorize] +public class ImageGenerationController : ControllerBase +{ + private readonly IImageGenerationOrchestrator _orchestrator; + private readonly DramaLingDbContext _dbContext; + + [HttpPost("flashcards/{flashcardId}/generate")] + public async Task GenerateImage( + Guid flashcardId, + [FromBody] GenerationRequest request) + { + try + { + var userId = GetCurrentUserId(); // 從 JWT 取得 + request.UserId = userId; + + var result = await _orchestrator.StartGenerationAsync(flashcardId, request); + + return Ok(new { success = true, data = result }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to start image generation for flashcard {FlashcardId}", flashcardId); + return BadRequest(new { success = false, error = "Failed to start generation" }); + } + } + + [HttpGet("requests/{requestId}/status")] + public async Task GetGenerationStatus(Guid requestId) + { + try + { + var request = await _dbContext.ImageGenerationRequests + .FirstOrDefaultAsync(r => r.Id == requestId); + + if (request == null) + return NotFound(new { success = false, error = "Request not found" }); + + var response = BuildStatusResponse(request); + return Ok(new { success = true, data = response }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to get status for request {RequestId}", requestId); + return BadRequest(new { success = false, error = "Failed to get status" }); + } + } + + private object BuildStatusResponse(ImageGenerationRequest request) + { + return new + { + requestId = request.Id, + overallStatus = request.OverallStatus, + stages = new + { + gemini = new + { + status = request.GeminiStatus, + startedAt = request.GeminiStartedAt, + completedAt = request.GeminiCompletedAt, + processingTimeMs = request.GeminiProcessingTimeMs, + cost = request.GeminiCost, + generatedDescription = request.GeneratedDescription + }, + replicate = new + { + status = request.ReplicateStatus, + startedAt = request.ReplicateStartedAt, + completedAt = request.ReplicateCompletedAt, + processingTimeMs = request.ReplicateProcessingTimeMs, + cost = request.ReplicateCost + } + }, + totalCost = request.TotalCost, + completedAt = request.CompletedAt + }; + } +} +``` + +--- + +## 📅 Phase 4: 快取和優化 (Week 7-8) + +### Week 7: 兩階段快取實現 + +#### 7.1 擴展現有快取服務 +**檔案**: `/Services/Caching/ImageGenerationCacheService.cs` + +```csharp +public class ImageGenerationCacheService : IImageGenerationCacheService +{ + private readonly ICacheService _cacheService; // 重用現有快取 + private readonly DramaLingDbContext _dbContext; + + public async Task GetCachedDescriptionAsync( + Flashcard flashcard, + GenerationOptions options) + { + // 1. 完全匹配快取 + var cacheKey = $"desc:{flashcard.Id}:{options.GetHashCode()}"; + var cached = await _cacheService.GetAsync(cacheKey); + if (cached != null) return cached; + + // 2. 語意匹配 (資料庫查詢) + var similarDesc = await FindSimilarDescriptionAsync(flashcard, options); + if (similarDesc != null) + { + // 快取相似結果 + await _cacheService.SetAsync(cacheKey, similarDesc, TimeSpan.FromHours(1)); + return similarDesc; + } + + return null; + } + + public async Task GetCachedImageAsync(string optimizedPrompt) + { + var promptHash = ComputeHash(optimizedPrompt); + var cacheKey = $"img:{promptHash}"; + + return await _cacheService.GetAsync(cacheKey); + } + + public async Task CacheDescriptionAsync( + Flashcard flashcard, + GenerationOptions options, + string description) + { + var cacheKey = $"desc:{flashcard.Id}:{options.GetHashCode()}"; + await _cacheService.SetAsync(cacheKey, description, TimeSpan.FromHours(24)); + } +} +``` + +### Week 8: 成本控制和監控 + +#### 8.1 積分系統整合 +**檔案**: `/Services/CreditManagementService.cs` + +```csharp +public class CreditManagementService : ICreditManagementService +{ + public async Task HasSufficientCreditsAsync(Guid userId, decimal requiredCredits) + { + var user = await _dbContext.Users.FindAsync(userId); + return user.Credits >= requiredCredits; + } + + public async Task DeductCreditsAsync(Guid userId, decimal amount, string description) + { + var user = await _dbContext.Users.FindAsync(userId); + if (user.Credits < amount) return false; + + user.Credits -= amount; + + // 記錄積分使用 + _dbContext.CreditTransactions.Add(new CreditTransaction + { + UserId = userId, + Amount = -amount, + Description = description, + CreatedAt = DateTime.UtcNow + }); + + await _dbContext.SaveChangesAsync(); + return true; + } +} +``` + +--- + +## 🔧 環境配置檔案 + +### appsettings.Development.json +```json +{ + "Gemini": { + "ApiKey": "YOUR_GEMINI_API_KEY", + "TimeoutSeconds": 30, + "Model": "gemini-1.5-flash" + }, + "Replicate": { + "ApiKey": "YOUR_REPLICATE_API_KEY", + "TimeoutSeconds": 180, + "Models": { + "flux-1-dev": { + "Version": "dev", + "CostPerGeneration": 0.05, + "DefaultWidth": 512, + "DefaultHeight": 512 + }, + "stable-diffusion-xl": { + "Version": "39ed52f2a78e934b3ba6e2a89f5b1c712de7dfea535525255b1aa35c5565e08b", + "CostPerGeneration": 0.04 + } + } + }, + "ImageStorage": { + "Provider": "Local", + "Local": { + "BasePath": "wwwroot/images/examples", + "BaseUrl": "https://localhost:5008/images/examples" + } + } +} +``` + +--- + +## 🧪 測試策略 + +### 單元測試優先級 +1. **GeminiImageDescriptionService** - 描述生成邏輯 +2. **ReplicateImageGenerationService** - API 整合 +3. **ImageGenerationOrchestrator** - 流程編排 +4. **ImageGenerationCacheService** - 快取邏輯 + +### 整合測試 +1. **完整兩階段生成流程** +2. **錯誤處理和重試機制** +3. **成本計算和積分扣款** + +--- + +## 📦 NuGet 套件需求 + +需要新增到 `DramaLing.Api.csproj`: + +```xml + + + +``` + +--- + +## 🚀 部署檢查清單 + +### 開發環境啟動 +1. ✅ 資料庫 Migration 執行 +2. ✅ Gemini API Key 配置 +3. ✅ Replicate API Key 配置 +4. ✅ 本地圖片存儲目錄建立 +5. ✅ 服務註冊檢查 + +### 測試驗證 +1. ✅ Gemini 描述生成測試 +2. ✅ Replicate 圖片生成測試 +3. ✅ 完整流程端到端測試 +4. ✅ 錯誤處理測試 +5. ✅ 積分扣款測試 + +--- + +## ⏱️ 時程總結 + +| Phase | 時間 | 主要任務 | 可交付成果 | +|-------|------|----------|-----------| +| Phase 1 | Week 1-2 | 基礎架構擴展 | 資料庫 Schema、配置、基礎服務 | +| Phase 2 | Week 3-4 | 核心服務實現 | Gemini 和 Replicate 服務 | +| Phase 3 | Week 5-6 | API 和編排器 | 完整的 API 端點和流程 | +| Phase 4 | Week 7-8 | 優化和監控 | 快取、成本控制、監控 | + +**總時程**: 6-8 週 +**風險緩衝**: +1-2 週 (Replicate API 整合複雜度) + +--- + +## 📚 參考文檔 + +- [例句圖生成功能 PRD](./EXAMPLE_IMAGE_GENERATION_PRD.md) +- [後端架構詳細說明](./docs/04_technical/backend-architecture.md) +- [系統架構總覽](./docs/04_technical/system-architecture.md) +- [Replicate API 文檔](https://replicate.com/docs/reference/http) +- [Gemini API 文檔](https://cloud.google.com/ai-platform/generative-ai/docs) + +--- + +**文檔版本**: v1.0 +**建立日期**: 2025-09-24 +**預估完成**: 2025-11-19 +**負責團隊**: 後端開發團隊 \ No newline at end of file