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

536 lines
19 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 System.Text.Json;
using System.Text;
using DramaLing.Api.Models.Entities;
namespace DramaLing.Api.Services;
public interface IGeminiService
{
Task<List<GeneratedCard>> GenerateCardsAsync(string inputText, string extractionType, int cardCount);
Task<ValidationResult> ValidateCardAsync(Flashcard card);
Task<SentenceAnalysisResponse> AnalyzeSentenceAsync(string inputText);
}
public class GeminiService : IGeminiService
{
private readonly HttpClient _httpClient;
private readonly IConfiguration _configuration;
private readonly ILogger<GeminiService> _logger;
private readonly string _apiKey;
public GeminiService(HttpClient httpClient, IConfiguration configuration, ILogger<GeminiService> logger)
{
_httpClient = httpClient;
_configuration = configuration;
_logger = logger;
_apiKey = Environment.GetEnvironmentVariable("DRAMALING_GEMINI_API_KEY")
?? _configuration["AI:GeminiApiKey"] ?? "";
}
public async Task<List<GeneratedCard>> GenerateCardsAsync(string inputText, string extractionType, int cardCount)
{
try
{
if (string.IsNullOrEmpty(_apiKey) || _apiKey == "your-gemini-api-key-here")
{
throw new InvalidOperationException("Gemini API key not configured");
}
var prompt = BuildPrompt(inputText, extractionType, cardCount);
var response = await CallGeminiApiAsync(prompt);
return ParseGeneratedCards(response);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error generating cards with Gemini API");
throw;
}
}
/// <summary>
/// 真正的句子分析和翻譯 - 調用 Gemini AI
/// </summary>
public async Task<SentenceAnalysisResponse> AnalyzeSentenceAsync(string inputText)
{
try
{
if (string.IsNullOrEmpty(_apiKey) || _apiKey == "your-gemini-api-key-here")
{
throw new InvalidOperationException("Gemini API key not configured");
}
var prompt = $@"
請分析以下英文句子,提供完整的分析:
句子:{inputText}
請按照以下JSON格式回應不要包含任何其他文字
{{
""translation"": ""自然流暢的繁體中文翻譯"",
""explanation"": ""詳細解釋句子的語法結構、詞彙特點、使用場景和學習要點"",
""grammarCorrection"": {{
""hasErrors"": false,
""originalText"": ""{inputText}"",
""correctedText"": null,
""corrections"": []
}},
""highValueWords"": [""重要詞彙1"", ""重要詞彙2""],
""wordAnalysis"": {{
""單字"": {{
""translation"": ""中文翻譯"",
""definition"": ""英文定義"",
""partOfSpeech"": ""詞性"",
""pronunciation"": ""音標"",
""isHighValue"": true,
""difficultyLevel"": ""CEFR等級""
}}
}}
}}
要求:
1. 翻譯要自然流暢,符合中文語法
2. 解釋要具體有用,不要空泛
3. 標記B1以上詞彙為高價值
4. 如有語法錯誤請指出並修正
5. 確保JSON格式正確
";
var response = await CallGeminiApiAsync(prompt);
return ParseSentenceAnalysisResponse(response);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error analyzing sentence with Gemini API");
throw;
}
}
public async Task<ValidationResult> ValidateCardAsync(Flashcard card)
{
try
{
if (string.IsNullOrEmpty(_apiKey) || _apiKey == "your-gemini-api-key-here")
{
throw new InvalidOperationException("Gemini API key not configured");
}
var prompt = BuildValidationPrompt(card);
var response = await CallGeminiApiAsync(prompt);
return ParseValidationResult(response);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error validating card with Gemini API");
throw;
}
}
private string BuildPrompt(string inputText, string extractionType, int cardCount)
{
var template = extractionType == "vocabulary" ? VocabularyExtractionPrompt : SmartExtractionPrompt;
return template
.Replace("{cardCount}", cardCount.ToString())
.Replace("{inputText}", inputText);
}
private string BuildValidationPrompt(Flashcard card)
{
return CardValidationPrompt
.Replace("{word}", card.Word)
.Replace("{translation}", card.Translation)
.Replace("{definition}", card.Definition)
.Replace("{partOfSpeech}", card.PartOfSpeech ?? "")
.Replace("{pronunciation}", card.Pronunciation ?? "")
.Replace("{example}", card.Example ?? "");
}
private async Task<string> CallGeminiApiAsync(string prompt)
{
var requestBody = new
{
contents = new[]
{
new
{
parts = new[]
{
new { text = prompt }
}
}
}
};
var json = JsonSerializer.Serialize(requestBody);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync(
$"https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent?key={_apiKey}",
content);
if (!response.IsSuccessStatusCode)
{
var errorContent = await response.Content.ReadAsStringAsync();
_logger.LogError("Gemini API error: {StatusCode} - {Content}", response.StatusCode, errorContent);
throw new HttpRequestException($"Gemini API request failed: {response.StatusCode}");
}
var responseContent = await response.Content.ReadAsStringAsync();
var geminiResponse = JsonSerializer.Deserialize<JsonElement>(responseContent);
if (geminiResponse.TryGetProperty("candidates", out var candidates) &&
candidates.GetArrayLength() > 0 &&
candidates[0].TryGetProperty("content", out var contentElement) &&
contentElement.TryGetProperty("parts", out var parts) &&
parts.GetArrayLength() > 0 &&
parts[0].TryGetProperty("text", out var textElement))
{
return textElement.GetString() ?? "";
}
throw new InvalidOperationException("Invalid response format from Gemini API");
}
private List<GeneratedCard> ParseGeneratedCards(string response)
{
try
{
// 清理回應文本
var cleanText = response.Trim();
cleanText = cleanText.Replace("```json", "").Replace("```", "").Trim();
// 如果不是以 { 開始,嘗試找到 JSON 部分
if (!cleanText.StartsWith("{"))
{
var jsonStart = cleanText.IndexOf("{");
if (jsonStart >= 0)
{
cleanText = cleanText[jsonStart..];
}
}
var jsonResponse = JsonSerializer.Deserialize<JsonElement>(cleanText);
if (!jsonResponse.TryGetProperty("cards", out var cardsElement) || cardsElement.ValueKind != JsonValueKind.Array)
{
throw new InvalidOperationException("Response does not contain cards array");
}
var cards = new List<GeneratedCard>();
foreach (var cardElement in cardsElement.EnumerateArray())
{
var card = new GeneratedCard
{
Word = GetStringProperty(cardElement, "word"),
PartOfSpeech = GetStringProperty(cardElement, "part_of_speech"),
Pronunciation = GetStringProperty(cardElement, "pronunciation"),
Translation = GetStringProperty(cardElement, "translation"),
Definition = GetStringProperty(cardElement, "definition"),
Synonyms = GetArrayProperty(cardElement, "synonyms"),
Example = GetStringProperty(cardElement, "example"),
ExampleTranslation = GetStringProperty(cardElement, "example_translation"),
DifficultyLevel = GetStringProperty(cardElement, "difficulty_level")
};
if (!string.IsNullOrEmpty(card.Word) && !string.IsNullOrEmpty(card.Translation))
{
cards.Add(card);
}
}
return cards;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error parsing generated cards response: {Response}", response);
throw new InvalidOperationException($"Failed to parse AI response: {ex.Message}");
}
}
private ValidationResult ParseValidationResult(string response)
{
try
{
var cleanText = response.Trim().Replace("```json", "").Replace("```", "").Trim();
var jsonResponse = JsonSerializer.Deserialize<JsonElement>(cleanText);
var issues = new List<ValidationIssue>();
if (jsonResponse.TryGetProperty("issues", out var issuesElement))
{
foreach (var issueElement in issuesElement.EnumerateArray())
{
issues.Add(new ValidationIssue
{
Field = GetStringProperty(issueElement, "field"),
Original = GetStringProperty(issueElement, "original"),
Corrected = GetStringProperty(issueElement, "corrected"),
Reason = GetStringProperty(issueElement, "reason"),
Severity = GetStringProperty(issueElement, "severity")
});
}
}
var suggestions = new List<string>();
if (jsonResponse.TryGetProperty("suggestions", out var suggestionsElement))
{
foreach (var suggestion in suggestionsElement.EnumerateArray())
{
suggestions.Add(suggestion.GetString() ?? "");
}
}
return new ValidationResult
{
Issues = issues,
Suggestions = suggestions,
OverallScore = jsonResponse.TryGetProperty("overall_score", out var scoreElement)
? scoreElement.GetInt32() : 85,
Confidence = jsonResponse.TryGetProperty("confidence", out var confidenceElement)
? confidenceElement.GetDouble() : 0.9
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Error parsing validation result: {Response}", response);
throw new InvalidOperationException($"Failed to parse validation response: {ex.Message}");
}
}
/// <summary>
/// 解析 Gemini AI 句子分析響應
/// </summary>
private SentenceAnalysisResponse ParseSentenceAnalysisResponse(string response)
{
try
{
var cleanText = response.Trim().Replace("```json", "").Replace("```", "").Trim();
if (!cleanText.StartsWith("{"))
{
var jsonStart = cleanText.IndexOf("{");
if (jsonStart >= 0)
{
cleanText = cleanText[jsonStart..];
}
}
var jsonResponse = JsonSerializer.Deserialize<JsonElement>(cleanText);
return new SentenceAnalysisResponse
{
Translation = GetStringProperty(jsonResponse, "translation"),
Explanation = GetStringProperty(jsonResponse, "explanation"),
HighValueWords = GetArrayProperty(jsonResponse, "highValueWords"),
WordAnalysis = ParseWordAnalysisFromJson(jsonResponse),
GrammarCorrection = ParseGrammarCorrectionFromJson(jsonResponse)
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Error parsing sentence analysis response: {Response}", response);
throw new InvalidOperationException($"Failed to parse AI sentence analysis: {ex.Message}");
}
}
private Dictionary<string, WordAnalysisResult> ParseWordAnalysisFromJson(JsonElement jsonResponse)
{
var result = new Dictionary<string, WordAnalysisResult>();
if (jsonResponse.TryGetProperty("wordAnalysis", out var wordAnalysisElement))
{
foreach (var property in wordAnalysisElement.EnumerateObject())
{
var word = property.Name;
var analysis = property.Value;
result[word] = new WordAnalysisResult
{
Word = word,
Translation = GetStringProperty(analysis, "translation"),
Definition = GetStringProperty(analysis, "definition"),
PartOfSpeech = GetStringProperty(analysis, "partOfSpeech"),
Pronunciation = GetStringProperty(analysis, "pronunciation"),
IsHighValue = analysis.TryGetProperty("isHighValue", out var isHighValueElement) && isHighValueElement.GetBoolean(),
DifficultyLevel = GetStringProperty(analysis, "difficultyLevel")
};
}
}
return result;
}
private GrammarCorrectionResult ParseGrammarCorrectionFromJson(JsonElement jsonResponse)
{
if (jsonResponse.TryGetProperty("grammarCorrection", out var grammarElement))
{
return new GrammarCorrectionResult
{
HasErrors = grammarElement.TryGetProperty("hasErrors", out var hasErrorsElement) && hasErrorsElement.GetBoolean(),
OriginalText = GetStringProperty(grammarElement, "originalText"),
CorrectedText = GetStringProperty(grammarElement, "correctedText"),
Corrections = new List<GrammarCorrection>() // 簡化
};
}
return new GrammarCorrectionResult
{
HasErrors = false,
OriginalText = "",
CorrectedText = null,
Corrections = new List<GrammarCorrection>()
};
}
private static string GetStringProperty(JsonElement element, string propertyName)
{
return element.TryGetProperty(propertyName, out var prop) ? prop.GetString() ?? "" : "";
}
private static List<string> GetArrayProperty(JsonElement element, string propertyName)
{
var result = new List<string>();
if (element.TryGetProperty(propertyName, out var arrayElement) && arrayElement.ValueKind == JsonValueKind.Array)
{
foreach (var item in arrayElement.EnumerateArray())
{
result.Add(item.GetString() ?? "");
}
}
return result;
}
// Prompt 模板
private const string VocabularyExtractionPrompt = @"
從以下英文文本中萃取 {cardCount} 個最重要的詞彙,為每個詞彙生成詞卡資料。
輸入文本:
{inputText}
請按照以下 JSON 格式回應,不要包含任何其他文字或代碼塊標記:
{
""cards"": [
{
""word"": ""單字原型"",
""part_of_speech"": ""詞性(n./v./adj./adv.等)"",
""pronunciation"": ""IPA音標"",
""translation"": ""繁體中文翻譯"",
""definition"": ""英文定義(保持A1-A2程度)"",
""synonyms"": [""同義詞1"", ""同義詞2""],
""example"": ""例句(使用原文中的句子或生成新句子)"",
""example_translation"": ""例句中文翻譯"",
""difficulty_level"": ""CEFR等級(A1/A2/B1/B2/C1/C2)""
}
]
}
要求:
1. 選擇最有學習價值的詞彙
2. 定義要簡單易懂,適合英語學習者
3. 例句要實用且符合語境
4. 確保 JSON 格式正確
5. 同義詞最多2個選擇常用的";
private const string SmartExtractionPrompt = @"
分析以下英文文本,識別片語、俚語和常用表達,生成 {cardCount} 個學習卡片:
輸入文本:
{inputText}
重點關注:
1. 片語和俚語
2. 文化相關表達
3. 語境特定用法
4. 慣用語和搭配
請按照相同的 JSON 格式回應...";
private const string CardValidationPrompt = @"
請檢查以下詞卡內容的準確性:
單字: {word}
翻譯: {translation}
定義: {definition}
詞性: {partOfSpeech}
發音: {pronunciation}
例句: {example}
請按照以下 JSON 格式回應:
{
""issues"": [],
""suggestions"": [],
""overall_score"": 85,
""confidence"": 0.9
}";
}
// 支援類型
public class GeneratedCard
{
public string Word { get; set; } = string.Empty;
public string PartOfSpeech { get; set; } = string.Empty;
public string Pronunciation { get; set; } = string.Empty;
public string Translation { get; set; } = string.Empty;
public string Definition { get; set; } = string.Empty;
public List<string> Synonyms { get; set; } = new();
public string Example { get; set; } = string.Empty;
public string ExampleTranslation { get; set; } = string.Empty;
public string DifficultyLevel { get; set; } = string.Empty;
}
public class ValidationResult
{
public List<ValidationIssue> Issues { get; set; } = new();
public List<string> Suggestions { get; set; } = new();
public int OverallScore { get; set; }
public double Confidence { get; set; }
}
public class ValidationIssue
{
public string Field { get; set; } = string.Empty;
public string Original { get; set; } = string.Empty;
public string Corrected { get; set; } = string.Empty;
public string Reason { get; set; } = string.Empty;
public string Severity { get; set; } = string.Empty;
}
// 新增句子分析相關類型
public class SentenceAnalysisResponse
{
public string Translation { get; set; } = string.Empty;
public string Explanation { get; set; } = string.Empty;
public List<string> HighValueWords { get; set; } = new();
public Dictionary<string, WordAnalysisResult> WordAnalysis { get; set; } = new();
public GrammarCorrectionResult GrammarCorrection { get; set; } = new();
}
public class WordAnalysisResult
{
public string Word { get; set; } = string.Empty;
public string Translation { get; set; } = string.Empty;
public string Definition { get; set; } = string.Empty;
public string PartOfSpeech { get; set; } = string.Empty;
public string Pronunciation { get; set; } = string.Empty;
public bool IsHighValue { get; set; }
public string DifficultyLevel { get; set; } = string.Empty;
}
public class GrammarCorrectionResult
{
public bool HasErrors { get; set; }
public string OriginalText { get; set; } = string.Empty;
public string? CorrectedText { get; set; }
public List<GrammarCorrection> Corrections { get; set; } = new();
}
public class GrammarCorrection
{
public string ErrorType { get; set; } = string.Empty;
public string Original { get; set; } = string.Empty;
public string Corrected { get; set; } = string.Empty;
public string Reason { get; set; } = string.Empty;
}