dramaling-vocab-learning/backend/DramaLing.Api/Services/AI/Gemini/GeminiService.cs

585 lines
23 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using DramaLing.Api.Models.DTOs;
using DramaLing.Api.Models.Entities;
using DramaLing.Api.Models.Configuration;
using Microsoft.Extensions.Options;
using System.Text.Json;
using System.Text;
namespace DramaLing.Api.Services;
public interface IGeminiService
{
Task<SentenceAnalysisData> AnalyzeSentenceAsync(string inputText, AnalysisOptions options);
Task<string> GenerateImageDescriptionAsync(Flashcard flashcard, GenerationOptionsDto options);
}
public class GeminiService : IGeminiService
{
private readonly HttpClient _httpClient;
private readonly ILogger<GeminiService> _logger;
private readonly GeminiOptions _options;
public GeminiService(HttpClient httpClient, IOptions<GeminiOptions> options, ILogger<GeminiService> logger)
{
_httpClient = httpClient;
_logger = logger;
_options = options.Value;
_logger.LogInformation("GeminiService initialized with model: {Model}, timeout: {Timeout}s",
_options.Model, _options.TimeoutSeconds);
_httpClient.Timeout = TimeSpan.FromSeconds(_options.TimeoutSeconds);
_httpClient.DefaultRequestHeaders.Add("User-Agent", "DramaLing/1.0");
}
public async Task<SentenceAnalysisData> AnalyzeSentenceAsync(string inputText, AnalysisOptions options)
{
var startTime = DateTime.UtcNow;
try
{
_logger.LogInformation("Starting sentence analysis for text: {Text}",
inputText.Substring(0, Math.Min(50, inputText.Length)));
// 符合產品需求規格的結構化 prompt
var prompt = $@"You are an English learning assistant. Analyze this sentence and return ONLY a valid JSON response.
**Input Sentence**: ""{inputText}""
**Required JSON Structure:**
{{
""sentenceTranslation"": ""Traditional Chinese translation of the entire sentence"",
""hasGrammarErrors"": true/false,
""grammarCorrections"": [
{{
""original"": ""incorrect text"",
""corrected"": ""correct text"",
""type"": ""error type (tense/subject-verb/preposition/word-order)"",
""explanation"": ""brief explanation in Traditional Chinese""
}}
],
""vocabularyAnalysis"": {{
""word1"": {{
""word"": ""the word"",
""translation"": ""Traditional Chinese translation"",
""definition"": ""English definition"",
""partOfSpeech"": ""noun/verb/adjective/adverb/pronoun/preposition/conjunction/interjection"",
""pronunciation"": ""/phonetic/"",
""difficultyLevel"": ""A1/A2/B1/B2/C1/C2"",
""frequency"": ""high/medium/low"",
""synonyms"": [""synonym1"", ""synonym2""],
""example"": ""example sentence"",
""exampleTranslation"": ""Traditional Chinese example translation""
}}
}},
""idioms"": [
{{
""idiom"": ""idiomatic expression"",
""translation"": ""Traditional Chinese meaning"",
""definition"": ""English explanation"",
""pronunciation"": ""/phonetic notation/"",
""difficultyLevel"": ""A1/A2/B1/B2/C1/C2"",
""frequency"": ""high/medium/low"",
""synonyms"": [""synonym1"", ""synonym2""],
""example"": ""usage example"",
""exampleTranslation"": ""Traditional Chinese example""
}}
]
}}
**Analysis Guidelines:**
1. **Grammar Check**: Detect tense errors, subject-verb agreement, preposition usage, word order
2. **Vocabulary Analysis**: Include ALL significant words (exclude articles: a, an, the)
3. **CEFR Levels**: Assign accurate A1-C2 levels for each word
4. **Idioms**: Identify any idiomatic expressions or phrasal verbs
5. **Translations**: Use Traditional Chinese (Taiwan standard)
**IMPORTANT**: Return ONLY the JSON object, no additional text or explanation.";
var aiResponse = await CallGeminiAPI(prompt);
_logger.LogInformation("Gemini AI response received: {ResponseLength} characters", aiResponse?.Length ?? 0);
if (string.IsNullOrWhiteSpace(aiResponse))
{
throw new InvalidOperationException("Gemini API returned empty response");
}
// 檢查是否是安全過濾的回退訊息
if (aiResponse.Contains("temporarily unavailable due to safety filtering"))
{
// 這是安全過濾的情況,但我們仍然要處理它而不是拋出異常
_logger.LogWarning("Using safety filtering fallback response");
}
// 直接使用 AI 的回應創建分析數據
var analysisData = CreateAnalysisFromAIResponse(inputText, aiResponse);
var processingTime = (DateTime.UtcNow - startTime).TotalSeconds;
_logger.LogInformation("Sentence analysis completed in {ProcessingTime}s", processingTime);
return analysisData;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error analyzing sentence: {Text}", inputText);
throw;
}
}
private SentenceAnalysisData CreateAnalysisFromAIResponse(string inputText, string aiResponse)
{
_logger.LogInformation("Creating analysis from AI response: {ResponsePreview}...",
aiResponse.Substring(0, Math.Min(100, aiResponse.Length)));
try
{
// 清理 AI 回應以確保是純 JSON
var cleanJson = aiResponse.Trim();
if (cleanJson.StartsWith("```json"))
{
cleanJson = cleanJson.Substring(7);
}
if (cleanJson.EndsWith("```"))
{
cleanJson = cleanJson.Substring(0, cleanJson.Length - 3);
}
// 解析 AI 回應的 JSON
var aiAnalysis = JsonSerializer.Deserialize<AiAnalysisResponse>(cleanJson, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
if (aiAnalysis == null)
{
throw new InvalidOperationException("Failed to parse AI response JSON");
}
// 轉換為 DTO 結構
var analysisData = new SentenceAnalysisData
{
OriginalText = inputText,
SentenceMeaning = aiAnalysis.SentenceTranslation ?? "",
VocabularyAnalysis = ConvertVocabularyAnalysis(aiAnalysis.VocabularyAnalysis ?? new()),
Idioms = ConvertIdioms(aiAnalysis.Idioms ?? new()),
GrammarCorrection = ConvertGrammarCorrection(aiAnalysis),
Metadata = new AnalysisMetadata
{
ProcessingDate = DateTime.UtcNow,
AnalysisModel = "gemini-1.5-flash",
AnalysisVersion = "2.0"
}
};
return analysisData;
}
catch (JsonException ex)
{
_logger.LogError(ex, "Failed to parse AI response as JSON: {Response}", aiResponse);
// 回退到舊的處理方式
return CreateFallbackAnalysis(inputText, aiResponse);
}
}
private Dictionary<string, VocabularyAnalysisDto> ConvertVocabularyAnalysis(Dictionary<string, AiVocabularyAnalysis> aiVocab)
{
var result = new Dictionary<string, VocabularyAnalysisDto>();
foreach (var kvp in aiVocab)
{
var aiWord = kvp.Value;
result[kvp.Key] = new VocabularyAnalysisDto
{
Word = aiWord.Word ?? kvp.Key,
Translation = aiWord.Translation ?? "",
Definition = aiWord.Definition ?? "",
PartOfSpeech = aiWord.PartOfSpeech ?? "unknown",
Pronunciation = aiWord.Pronunciation ?? $"/{kvp.Key}/",
DifficultyLevel = aiWord.DifficultyLevel ?? "A2",
Frequency = aiWord.Frequency ?? "medium",
Synonyms = aiWord.Synonyms ?? new List<string>(),
Example = aiWord.Example,
ExampleTranslation = aiWord.ExampleTranslation,
};
}
return result;
}
private List<IdiomDto> ConvertIdioms(List<AiIdiom> aiIdioms)
{
var result = new List<IdiomDto>();
foreach (var aiIdiom in aiIdioms)
{
result.Add(new IdiomDto
{
Idiom = aiIdiom.Idiom ?? "",
Translation = aiIdiom.Translation ?? "",
Definition = aiIdiom.Definition ?? "",
Pronunciation = aiIdiom.Pronunciation ?? "",
DifficultyLevel = aiIdiom.DifficultyLevel ?? "B2",
Frequency = aiIdiom.Frequency ?? "medium",
Synonyms = aiIdiom.Synonyms ?? new List<string>(),
Example = aiIdiom.Example,
ExampleTranslation = aiIdiom.ExampleTranslation
});
}
return result;
}
private GrammarCorrectionDto? ConvertGrammarCorrection(AiAnalysisResponse aiAnalysis)
{
if (!aiAnalysis.HasGrammarErrors || aiAnalysis.GrammarCorrections == null || !aiAnalysis.GrammarCorrections.Any())
{
return null;
}
var corrections = aiAnalysis.GrammarCorrections.Select(gc => new GrammarErrorDto
{
Error = gc.Original ?? "",
Correction = gc.Corrected ?? "",
Type = gc.Type ?? "grammar",
Explanation = gc.Explanation ?? "",
Severity = "medium",
Position = new ErrorPosition { Start = 0, End = 0 } // 簡化處理
}).ToList();
return new GrammarCorrectionDto
{
HasErrors = true,
CorrectedText = string.Join(" ", corrections.Select(c => c.Correction)),
Corrections = corrections
};
}
private SentenceAnalysisData CreateFallbackAnalysis(string inputText, string aiResponse)
{
_logger.LogWarning("Using fallback analysis due to JSON parsing failure");
return new SentenceAnalysisData
{
OriginalText = inputText,
SentenceMeaning = aiResponse,
VocabularyAnalysis = CreateBasicVocabularyFromText(inputText, aiResponse),
Metadata = new AnalysisMetadata
{
ProcessingDate = DateTime.UtcNow,
AnalysisModel = "gemini-1.5-flash-fallback",
AnalysisVersion = "2.0"
},
};
}
private Dictionary<string, VocabularyAnalysisDto> CreateBasicVocabularyFromText(string inputText, string aiResponse)
{
// 從 AI 回應中提取真實的詞彙翻譯
var words = inputText.Split(' ', StringSplitOptions.RemoveEmptyEntries);
var result = new Dictionary<string, VocabularyAnalysisDto>();
foreach (var word in words.Take(15))
{
var cleanWord = word.ToLower().Trim('.', ',', '!', '?', ';', ':', '"', '\'');
if (string.IsNullOrEmpty(cleanWord) || cleanWord.Length < 2) continue;
result[cleanWord] = new VocabularyAnalysisDto
{
Word = cleanWord,
Translation = ExtractTranslationFromAI(cleanWord, aiResponse),
Definition = $"Please refer to the AI analysis above for detailed definition.",
PartOfSpeech = "unknown",
Pronunciation = $"/{cleanWord}/",
DifficultyLevel = EstimateBasicDifficulty(cleanWord),
Frequency = "medium",
Synonyms = new List<string>(),
Example = null,
ExampleTranslation = null,
};
}
return result;
}
private string ExtractTranslationFromAI(string word, string aiResponse)
{
// 嘗試從 AI 回應中提取該詞的翻譯
// 這是簡化版本,真正的版本應該解析完整的 JSON
if (aiResponse.Contains(word, StringComparison.OrdinalIgnoreCase))
{
return $"{word} translation from AI";
}
return $"{word} - 請查看完整分析";
}
private string EstimateBasicDifficulty(string word)
{
// 基本詞彙列表(這是最小的 fallback 邏輯)
var a1Words = new[] { "i", "you", "he", "she", "it", "we", "they", "the", "a", "an", "is", "are", "was", "were", "have", "do", "go", "get", "see", "know" };
var a2Words = new[] { "make", "think", "come", "take", "use", "work", "call", "try", "ask", "need", "feel", "become", "leave", "put", "say", "tell", "turn", "move" };
var lowerWord = word.ToLower();
if (a1Words.Contains(lowerWord)) return "A1";
if (a2Words.Contains(lowerWord)) return "A2";
if (word.Length <= 4) return "A2";
if (word.Length <= 6) return "B1";
return "B2";
}
private async Task<string> CallGeminiAPI(string prompt)
{
try
{
var requestBody = new
{
contents = new[]
{
new
{
parts = new[]
{
new { text = prompt }
}
}
},
generationConfig = new
{
temperature = _options.Temperature,
topK = 40,
topP = 0.95,
maxOutputTokens = _options.MaxOutputTokens
}
};
var json = JsonSerializer.Serialize(requestBody);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync($"{_options.BaseUrl}/v1beta/models/{_options.Model}:generateContent?key={_options.ApiKey}", content);
response.EnsureSuccessStatusCode();
var responseJson = await response.Content.ReadAsStringAsync();
_logger.LogInformation("Raw Gemini API response: {Response}", responseJson.Substring(0, Math.Min(500, responseJson.Length)));
// 先嘗試使用動態解析來避免反序列化問題
using var document = JsonDocument.Parse(responseJson);
var root = document.RootElement;
string aiText = string.Empty;
// 檢查是否有 candidates 陣列
if (root.TryGetProperty("candidates", out var candidatesElement) && candidatesElement.ValueKind == JsonValueKind.Array)
{
_logger.LogInformation("Found candidates array with {Count} items", candidatesElement.GetArrayLength());
var firstCandidate = candidatesElement.EnumerateArray().FirstOrDefault();
if (firstCandidate.ValueKind != JsonValueKind.Undefined)
{
if (firstCandidate.TryGetProperty("content", out var contentElement))
{
if (contentElement.TryGetProperty("parts", out var partsElement) && partsElement.ValueKind == JsonValueKind.Array)
{
var firstPart = partsElement.EnumerateArray().FirstOrDefault();
if (firstPart.TryGetProperty("text", out var textElement))
{
aiText = textElement.GetString() ?? string.Empty;
_logger.LogInformation("Successfully extracted text: {Length} characters", aiText.Length);
}
}
}
}
}
// 檢查是否有安全過濾
if (root.TryGetProperty("promptFeedback", out var feedbackElement))
{
_logger.LogWarning("Gemini prompt feedback received: {Feedback}", feedbackElement.ToString());
}
_logger.LogInformation("Gemini API returned: {ResponseLength} characters", aiText.Length);
if (!string.IsNullOrEmpty(aiText))
{
_logger.LogInformation("AI text preview: {Preview}", aiText.Substring(0, Math.Min(200, aiText.Length)));
}
// 如果沒有獲取到文本且有安全過濾回饋,返回友好訊息
if (string.IsNullOrWhiteSpace(aiText) && root.TryGetProperty("promptFeedback", out _))
{
return "The content analysis is temporarily unavailable due to safety filtering. Please try with different content.";
}
return aiText;
}
catch (Exception ex)
{
_logger.LogError(ex, "Gemini API call failed");
throw;
}
}
public async Task<string> GenerateImageDescriptionAsync(Flashcard flashcard, GenerationOptionsDto options)
{
try
{
_logger.LogInformation("Starting image description generation for flashcard {FlashcardId}", flashcard.Id);
var prompt = BuildImageDescriptionPrompt(flashcard, options);
var response = await CallGeminiAPI(prompt);
if (string.IsNullOrWhiteSpace(response))
{
throw new InvalidOperationException("Gemini API returned empty response");
}
var description = ExtractImageDescription(response);
var optimizedPrompt = OptimizeForReplicate(description, options);
_logger.LogInformation("Image description generated successfully for flashcard {FlashcardId}", flashcard.Id);
return optimizedPrompt;
}
catch (Exception ex)
{
_logger.LogError(ex, "Image description generation failed for flashcard {FlashcardId}", flashcard.Id);
throw;
}
}
private string BuildImageDescriptionPrompt(Flashcard flashcard, GenerationOptionsDto options)
{
return $@"# 總覽
你是一位專業插畫設計師兼職英文老師,專門為英語學習教材製作插畫圖卡,用來幫助學生理解英文例句的意思。
# 例句資訊
例句:{flashcard.Example}
# 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
- 色調:暖色調、柔和、低飽和
- 人物樣式:簡化卡通人物,表情自然,不誇張
- 背景構成:圖形簡化,使用色塊區分層次
- 整體氛圍:溫馨、平靜、適合教育情境
- 技術風格:無紋理、無漸層、無光影寫實感
請根據以上規範,為這個英文例句生成圖片描述提示詞,並確保完全符合風格指南要求。";
}
private string ExtractImageDescription(string geminiResponse)
{
// 從 Gemini 回應中提取圖片描述
var description = geminiResponse.Trim();
// 移除可能的 markdown 標記
if (description.StartsWith("```"))
{
var lines = description.Split('\n');
description = string.Join('\n', lines.Skip(1).SkipLast(1));
}
return description.Trim();
}
private string OptimizeForReplicate(string description, GenerationOptionsDto options)
{
var optimizedPrompt = description;
// 確保包含扁平插畫風格要求
if (!optimizedPrompt.Contains("flat illustration"))
{
optimizedPrompt += ". Style guide: flat illustration style, outline-less shapes, warm and soft color tones, low saturation, cartoon-style characters with natural expressions, simplified background with color blocks, cozy and educational atmosphere, no texture, no gradients, no photorealism, no fantasy elements.";
}
// 強制加入禁止文字的規則
if (!optimizedPrompt.Contains("Absolutely no visible text"))
{
optimizedPrompt += " 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.";
}
return optimizedPrompt;
}
}
// Gemini API response models
internal class GeminiApiResponse
{
public List<GeminiCandidate>? Candidates { get; set; }
public object? PromptFeedback { get; set; }
}
internal class GeminiCandidate
{
public GeminiContent? Content { get; set; }
}
internal class GeminiContent
{
public List<GeminiPart>? Parts { get; set; }
}
internal class GeminiPart
{
public string? Text { get; set; }
}
// AI Response JSON Models
internal class AiAnalysisResponse
{
public string? SentenceTranslation { get; set; }
public bool HasGrammarErrors { get; set; }
public List<AiGrammarCorrection>? GrammarCorrections { get; set; }
public Dictionary<string, AiVocabularyAnalysis>? VocabularyAnalysis { get; set; }
public List<AiIdiom>? Idioms { get; set; }
}
internal class AiGrammarCorrection
{
public string? Original { get; set; }
public string? Corrected { get; set; }
public string? Type { get; set; }
public string? Explanation { get; set; }
}
internal class AiVocabularyAnalysis
{
public string? Word { get; set; }
public string? Translation { get; set; }
public string? Definition { get; set; }
public string? PartOfSpeech { get; set; }
public string? Pronunciation { get; set; }
public string? DifficultyLevel { get; set; }
public string? Frequency { get; set; }
public List<string>? Synonyms { get; set; }
public string? Example { get; set; }
public string? ExampleTranslation { get; set; }
}
internal class AiIdiom
{
public string? Idiom { get; set; }
public string? Translation { get; set; }
public string? Definition { get; set; }
public string? Pronunciation { get; set; }
public string? DifficultyLevel { get; set; }
public string? Frequency { get; set; }
public List<string>? Synonyms { get; set; }
public string? Example { get; set; }
public string? ExampleTranslation { get; set; }
}