dramaling-vocab-learning/EXAMPLE_IMAGE_GENERATION_BA...

22 KiB
Raw Blame History

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

📋 當前架構評估

已具備的基礎架構

  • 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"
    }
  }
}

🧪 測試策略

單元測試優先級

  1. GeminiImageDescriptionService - 描述生成邏輯
  2. ReplicateImageGenerationService - API 整合
  3. ImageGenerationOrchestrator - 流程編排
  4. ImageGenerationCacheService - 快取邏輯

整合測試

  1. 完整兩階段生成流程
  2. 錯誤處理和重試機制
  3. 成本計算和積分扣款

📦 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" />

🚀 部署檢查清單

開發環境啟動

  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 整合複雜度)


📚 參考文檔


文檔版本: v1.0 建立日期: 2025-09-24 預估完成: 2025-11-19 負責團隊: 後端開發團隊