449 lines
16 KiB
C#
449 lines
16 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<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;
|
||
} |