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

449 lines
16 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<SentenceAnalysisResponse> AnalyzeSentenceAsync(string inputText, string userLevel = "A2");
}
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, string userLevel = "A2")
{
try
{
if (string.IsNullOrEmpty(_apiKey) || _apiKey == "your-gemini-api-key-here")
{
throw new InvalidOperationException("Gemini API key not configured");
}
var targetRange = CEFRLevelService.GetTargetLevelRange(userLevel);
var prompt = $@"
請分析以下英文句子,提供翻譯和個人化詞彙分析:
句子:{inputText}
學習者程度:{userLevel}
請按照以下JSON格式回應不要包含任何其他文字
{{
""translation"": ""自然流暢的繁體中文翻譯"",
""grammarCorrection"": {{
""hasErrors"": false,
""originalText"": ""{inputText}"",
""correctedText"": null,
""corrections"": []
}},
""highValueWords"": [""重要詞彙1"", ""重要詞彙2""],
""wordAnalysis"": {{
""單字"": {{
""translation"": ""中文翻譯"",
""definition"": ""英文定義"",
""partOfSpeech"": ""詞性"",
""pronunciation"": ""音標"",
""isHighValue"": true,
""difficultyLevel"": ""CEFR等級"",
""example"": ""實用的例句展示該詞彙的真實用法"",
""exampleTranslation"": ""例句的自然中文翻譯""
}}
}}
}}
要求:
1. 翻譯要自然流暢,符合中文語法
2. **基於學習者程度({userLevel}),標記 {targetRange} 等級的詞彙為重點學習**
3. **為每個詞彙提供實用的例句,展示真實語境和用法**
4. **例句要有學習價值,避免簡單重複的句型**
5. 如有語法錯誤請指出並修正
6. 確保JSON格式正確
例句要求:
- 使用真實場景(工作、學習、日常生活)
- 展示詞彙的實際搭配和用法
- 適合學習者程度,不要太簡單或太複雜
- 中文翻譯要自然流暢
重點學習判定邏輯:
- 學習者程度: {userLevel}
- 重點學習範圍: {targetRange}
- 太簡單的詞彙(≤{userLevel})不要標記為重點學習
- 太難的詞彙謹慎標記
- 重點關注適合學習者程度的詞彙
";
var response = await CallGeminiApiAsync(prompt);
return ParseSentenceAnalysisResponse(response);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error analyzing sentence 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 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}");
}
}
/// <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"),
Example = GetStringProperty(analysis, "example"),
ExampleTranslation = GetStringProperty(analysis, "exampleTranslation")
};
}
}
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 格式回應...";
}
// 支援類型
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 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 string? Example { get; set; }
public string? ExampleTranslation { get; set; }
}
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;
}