# 例句圖生成功能後端開發計劃 ## 📋 當前架構評估 ### ✅ 已具備的基礎架構 - **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 **負責團隊**: 後端開發團隊