# 例句圖生成功能後端開發計劃 ## 📋 當前架構評估 ### ✅ 已具備的基礎架構 - **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 $@"# 總覽 你是一位專業插畫設計師兼職英文老師,專門為英語學習教材製作插畫圖卡,用來幫助學生理解英文例句的意思。 # 詞卡資訊 - 詞彙:{flashcard.Word} - 中文翻譯:{flashcard.Translation} - 詞性:{flashcard.PartOfSpeech} - 例句:{flashcard.Example} - 難度等級:{flashcard.DifficultyLevel} # SOP 1. 根據上述英文例句,請撰寫一段圖像描述提示詞,用於提供圖片生成AI作為生成圖片的提示詞 2. 請將下方「風格指南」的所有要求加入提示詞中 3. 並於圖片提示詞最後加上:「Absolutely no visible text, characters, letters, numbers, symbols, handwriting, labels, or any form of writing anywhere in the image — including on signs, books, clothing, screens, or backgrounds.」 # 圖片提示詞規範 ## 情境清楚 1. 角色描述具體清楚 2. 動作明確具象 3. 場景明確具體 4. 物品明確具體 5. 語意需與原句一致 6. 避免過於抽象或象徵性符號 ## 風格指南 - 風格類型:扁平插畫(Flat Illustration) - 線條特徵:無描邊線條(outline-less) - 色調:暖色調、柔和、低飽和 - 人物樣式:簡化卡通人物,表情自然,不誇張 - 背景構成:圖形簡化,使用色塊區分層次 - 整體氛圍:溫馨、平靜、適合教育情境 - 技術風格:無紋理、無漸層、無光影寫實感 請根據以上規範生成圖片描述提示詞。"; } } ``` #### 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); // 使用 Ideogram V2 Turbo 的專用端點 var apiUrl = model.ToLower() switch { "ideogram-v2a-turbo" => "https://api.replicate.com/v1/models/ideogram-ai/ideogram-v2a-turbo/predictions", _ => $"{_options.BaseUrl}/predictions" }; var response = await _httpClient.PostAsync( apiUrl, new StringContent(JsonSerializer.Serialize(requestBody), Encoding.UTF8, "application/json")); response.EnsureSuccessStatusCode(); var json = await response.Content.ReadAsStringAsync(); return JsonSerializer.Deserialize(json); } private object BuildModelRequest(string prompt, string model, GenerationOptions options) { return model.ToLower() switch { "ideogram-v2a-turbo" => new { input = new { prompt = prompt, width = options.Width ?? 512, height = options.Height ?? 512, magic_prompt_option = "Auto", // 自動優化提示詞 style_type = "General", // 適合教育內容的一般風格 aspect_ratio = "ASPECT_1_1", // 1:1 比例適合詞卡 model = "V_2_TURBO", // 使用 Turbo 版本 seed = options.Seed ?? Random.Shared.Next() } }, "flux-1-dev" => new { input = new { prompt = prompt, width = options.Width ?? 512, height = options.Height ?? 512, num_outputs = 1, guidance_scale = 3.5, num_inference_steps = 28, seed = options.Seed ?? Random.Shared.Next() } }, _ => throw new NotSupportedException($"Model {model} not supported") }; } 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", "BaseUrl": "https://api.replicate.com/v1", "TimeoutSeconds": 300, "DefaultModel": "ideogram-v2a-turbo", "Models": { "ideogram-v2a-turbo": { "Version": "c169dbd9a03b7bd35c3b05aa91e83bc4ad23ee2a4b8f93f2b6cbdda4f466de4a", "CostPerGeneration": 0.025, "DefaultWidth": 512, "DefaultHeight": 512, "StyleType": "General", "AspectRatio": "ASPECT_1_1", "Model": "V_2_TURBO" }, "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", "MaxFileSize": 10485760, "AllowedFormats": ["png", "jpg", "jpeg", "webp"] } }, "ImageGeneration": { "DefaultCreditsPerGeneration": 2.6, "GeminiCreditsPerRequest": 0.1, "EnableCaching": true, "CacheExpirationHours": 24, "MaxRetries": 3, "DefaultTimeout": 300 } } ``` --- ## 🧪 測試策略 ### 單元測試優先級 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 **負責團隊**: 後端開發團隊