536 lines
19 KiB
C#
536 lines
19 KiB
C#
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;
|
||
} |