22 KiB
22 KiB
例句圖生成功能後端開發計劃
📋 當前架構評估
✅ 已具備的基礎架構
- 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
dotnet ef migrations add AddImageGenerationTables
需要新增的表格:
example_images(例句圖片表)flashcard_example_images(關聯表)image_generation_requests(生成請求追蹤表)
1.2 實體模型建立
檔案位置: /Models/Entities/
// 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
public DbSet<ExampleImage> ExampleImages { get; set; }
public DbSet<ImageGenerationRequest> ImageGenerationRequests { get; set; }
public DbSet<FlashcardExampleImage> FlashcardExampleImages { get; set; }
// 在 OnModelCreating 中配置關聯
Week 2: 配置和基礎服務
2.1 Replicate 配置選項
檔案: /Models/Configuration/ReplicateOptions.cs
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<string, ModelConfig> 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
public interface IImageStorageService
{
Task<string> SaveImageAsync(Stream imageStream, string fileName);
Task<string> GetImageUrlAsync(string imagePath);
Task<bool> DeleteImageAsync(string imagePath);
Task<StorageInfo> GetStorageInfoAsync();
}
public class LocalImageStorageService : IImageStorageService
{
// 開發環境實現
}
2.3 Program.cs 服務註冊更新
// 新增 Replicate 配置
builder.Services.Configure<ReplicateOptions>(
builder.Configuration.GetSection(ReplicateOptions.SectionName));
builder.Services.AddSingleton<IValidateOptions<ReplicateOptions>, ReplicateOptionsValidator>();
// 新增圖片生成服務
builder.Services.AddHttpClient<IReplicateImageGenerationService, ReplicateImageGenerationService>();
builder.Services.AddScoped<IGeminiImageDescriptionService, GeminiImageDescriptionService>();
builder.Services.AddScoped<IImageGenerationOrchestrator, ImageGenerationOrchestrator>();
// 新增儲存服務
builder.Services.AddScoped<IImageStorageService>(provider =>
{
var config = provider.GetRequiredService<IConfiguration>();
return ImageStorageFactory.Create(config, provider.GetRequiredService<ILogger<IImageStorageService>>());
});
📅 Phase 2: 核心服務實現 (Week 3-4)
Week 3: Gemini 描述生成服務
3.1 擴展現有 GeminiService
檔案: /Services/AI/GeminiImageDescriptionService.cs
public class GeminiImageDescriptionService : IGeminiImageDescriptionService
{
private readonly GeminiService _geminiService; // 重用現有服務
private readonly ILogger<GeminiImageDescriptionService> _logger;
public async Task<ImageDescriptionResult> 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
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
public class ReplicateImageGenerationService : IReplicateImageGenerationService
{
private readonly HttpClient _httpClient;
private readonly ReplicateOptions _options;
private readonly ILogger<ReplicateImageGenerationService> _logger;
public async Task<ImageGenerationResult> 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<ReplicatePrediction> 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<ReplicatePrediction>(json);
}
private async Task<ImageGenerationResult> 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
public class ImageGenerationOrchestrator : IImageGenerationOrchestrator
{
private readonly IGeminiImageDescriptionService _geminiService;
private readonly IReplicateImageGenerationService _replicateService;
private readonly IImageStorageService _storageService;
private readonly DramaLingDbContext _dbContext;
public async Task<GenerationRequestResult> 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<GenerationOptions>(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
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class ImageGenerationController : ControllerBase
{
private readonly IImageGenerationOrchestrator _orchestrator;
private readonly DramaLingDbContext _dbContext;
[HttpPost("flashcards/{flashcardId}/generate")]
public async Task<IActionResult> 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<IActionResult> 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
public class ImageGenerationCacheService : IImageGenerationCacheService
{
private readonly ICacheService _cacheService; // 重用現有快取
private readonly DramaLingDbContext _dbContext;
public async Task<string?> GetCachedDescriptionAsync(
Flashcard flashcard,
GenerationOptions options)
{
// 1. 完全匹配快取
var cacheKey = $"desc:{flashcard.Id}:{options.GetHashCode()}";
var cached = await _cacheService.GetAsync<string>(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<string?> GetCachedImageAsync(string optimizedPrompt)
{
var promptHash = ComputeHash(optimizedPrompt);
var cacheKey = $"img:{promptHash}";
return await _cacheService.GetAsync<string>(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
public class CreditManagementService : ICreditManagementService
{
public async Task<bool> HasSufficientCreditsAsync(Guid userId, decimal requiredCredits)
{
var user = await _dbContext.Users.FindAsync(userId);
return user.Credits >= requiredCredits;
}
public async Task<bool> 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
{
"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"
}
}
}
🧪 測試策略
單元測試優先級
- GeminiImageDescriptionService - 描述生成邏輯
- ReplicateImageGenerationService - API 整合
- ImageGenerationOrchestrator - 流程編排
- ImageGenerationCacheService - 快取邏輯
整合測試
- 完整兩階段生成流程
- 錯誤處理和重試機制
- 成本計算和積分扣款
📦 NuGet 套件需求
需要新增到 DramaLing.Api.csproj:
<PackageReference Include="System.Text.Json" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="8.0.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.0.0" />
🚀 部署檢查清單
開發環境啟動
- ✅ 資料庫 Migration 執行
- ✅ Gemini API Key 配置
- ✅ Replicate API Key 配置
- ✅ 本地圖片存儲目錄建立
- ✅ 服務註冊檢查
測試驗證
- ✅ Gemini 描述生成測試
- ✅ Replicate 圖片生成測試
- ✅ 完整流程端到端測試
- ✅ 錯誤處理測試
- ✅ 積分扣款測試
⏱️ 時程總結
| 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 整合複雜度)
📚 參考文檔
文檔版本: v1.0 建立日期: 2025-09-24 預估完成: 2025-11-19 負責團隊: 後端開發團隊