feat: 新增例句圖生成後端開發計劃
基於當前 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 <noreply@anthropic.com>
This commit is contained in:
parent
502e7f920b
commit
179cbc6258
|
|
@ -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<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`
|
||||||
|
```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<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`
|
||||||
|
```csharp
|
||||||
|
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 服務註冊更新
|
||||||
|
```csharp
|
||||||
|
// 新增 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`
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
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`
|
||||||
|
```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<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`
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
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`
|
||||||
|
|
||||||
|
```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<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`
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
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`
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
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
|
||||||
|
```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
|
||||||
|
<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" />
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 部署檢查清單
|
||||||
|
|
||||||
|
### 開發環境啟動
|
||||||
|
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
|
||||||
|
**負責團隊**: 後端開發團隊
|
||||||
Loading…
Reference in New Issue