# 例句圖生成功能後端開發計劃

## 📋 當前架構評估

### ✅ 已具備的基礎架構
- **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 $@"# 總覽
你是一位專業插畫設計師兼職英文老師，專門為英語學習教材製作插畫圖卡，用來幫助學生理解英文例句的意思。

# 詞卡資訊
- 詞彙：{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<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);

        // 使用 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<ReplicatePrediction>(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<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",
    "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
<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
**負責團隊**: 後端開發團隊