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

648 lines
26 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 System.Text.Json;
using System.Text.RegularExpressions;
using System.Text;
namespace DramaLing.Api.Services;
public interface IGeminiService
{
Task<SentenceAnalysisData> AnalyzeSentenceAsync(string inputText, string userLevel, AnalysisOptions options);
}
public class GeminiService : IGeminiService
{
private readonly HttpClient _httpClient;
private readonly ILogger<GeminiService> _logger;
private readonly string[] _cefrLevels = { "A1", "A2", "B1", "B2", "C1", "C2" };
private readonly string _apiKey;
public GeminiService(HttpClient httpClient, IConfiguration configuration, ILogger<GeminiService> logger)
{
_httpClient = httpClient;
_logger = logger;
_apiKey = Environment.GetEnvironmentVariable("GEMINI_API_KEY")
?? configuration["AI:GeminiApiKey"]
?? configuration["Gemini:ApiKey"]
?? "mock-api-key"; // For development without Gemini
_logger.LogInformation("GeminiService initialized with API key: {ApiKeyStart}...",
_apiKey.Length > 10 ? _apiKey.Substring(0, 10) : "mock");
_httpClient.DefaultRequestHeaders.Add("User-Agent", "DramaLing/1.0");
}
public async Task<SentenceAnalysisData> AnalyzeSentenceAsync(string inputText, string userLevel, AnalysisOptions options)
{
var startTime = DateTime.UtcNow;
try
{
_logger.LogInformation("Starting sentence analysis for text: {Text}, UserLevel: {UserLevel}",
inputText.Substring(0, Math.Min(50, inputText.Length)), userLevel);
var prompt = BuildAnalysisPrompt(inputText, userLevel, options);
var response = await CallGeminiAPI(prompt);
var analysisData = ParseGeminiResponse(response, inputText, userLevel);
var processingTime = (DateTime.UtcNow - startTime).TotalSeconds;
analysisData.Metadata.ProcessingDate = DateTime.UtcNow;
_logger.LogInformation("Sentence analysis completed in {ProcessingTime}s", processingTime);
return analysisData;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error analyzing sentence: {Text}", inputText);
throw;
}
}
private string BuildAnalysisPrompt(string inputText, string userLevel, AnalysisOptions options)
{
var userIndex = Array.IndexOf(_cefrLevels, userLevel);
var targetLevels = GetTargetLevels(userIndex);
return $@"
請分析以下英文句子並以JSON格式回應
句子: ""{inputText}""
學習者程度: {userLevel}
請提供完整的分析,包含:
1. 語法檢查:檢查是否有語法錯誤,如有則提供修正建議
2. 詞彙分析:分析句子中每個有意義的詞彙
3. 中文翻譯:提供自然流暢的繁體中文翻譯
4. 慣用語識別:識別句子中的慣用語和片語
詞彙分析要求:
- 為每個詞彙標註CEFR等級 (A1-C2)
- 如果是慣用語,設置 isPhrase: true
- 提供IPA發音標記
- 包含同義詞
- 提供適當的例句和翻譯
回應格式要求:
{{
""grammarCorrection"": {{
""hasErrors"": boolean,
""correctedText"": ""修正後的句子"",
""corrections"": [
{{
""error"": ""錯誤詞彙"",
""correction"": ""正確詞彙"",
""type"": ""錯誤類型"",
""explanation"": ""解釋""
}}
]
}},
""sentenceMeaning"": ""繁體中文翻譯"",
""vocabularyAnalysis"": {{
""詞彙"": {{
""word"": ""詞彙"",
""translation"": ""中文翻譯"",
""definition"": ""英文定義"",
""partOfSpeech"": ""詞性"",
""pronunciation"": ""IPA發音"",
""difficultyLevel"": ""CEFR等級"",
""isPhrase"": false,
""frequency"": ""使用頻率"",
""synonyms"": [""同義詞""],
""example"": ""例句"",
""exampleTranslation"": ""例句翻譯"",
""tags"": [""標籤""]
}}
}}
}}
重要回應必須是有效的JSON格式不要包含任何其他文字。";
}
private string[] GetTargetLevels(int userIndex)
{
var targets = new List<string>();
if (userIndex + 1 < _cefrLevels.Length)
targets.Add(_cefrLevels[userIndex + 1]);
if (userIndex + 2 < _cefrLevels.Length)
targets.Add(_cefrLevels[userIndex + 2]);
return targets.ToArray();
}
private async Task<string> CallGeminiAPI(string prompt)
{
// 暫時使用模擬數據稍後可替換為真實Gemini調用
if (_apiKey == "mock-api-key")
{
_logger.LogInformation("Using mock AI response for development");
await Task.Delay(1000); // 模擬API延遲
return GetMockResponse();
}
try
{
var requestBody = new
{
contents = new[]
{
new
{
parts = new[]
{
new { text = prompt }
}
}
},
generationConfig = new
{
temperature = 0.3,
topK = 1,
topP = 1,
maxOutputTokens = 2000
}
};
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-pro:generateContent?key={_apiKey}", content);
response.EnsureSuccessStatusCode();
var responseJson = await response.Content.ReadAsStringAsync();
var geminiResponse = JsonSerializer.Deserialize<GeminiApiResponse>(responseJson);
return geminiResponse?.Candidates?.FirstOrDefault()?.Content?.Parts?.FirstOrDefault()?.Text ?? string.Empty;
}
catch (Exception ex)
{
_logger.LogError(ex, "Gemini API call failed, falling back to mock response");
return GetMockResponse();
}
}
private string GetMockResponse()
{
return @"{
""grammarCorrection"": {
""hasErrors"": true,
""correctedText"": ""She just joined the team, so let's cut her some slack until she gets used to the workflow."",
""corrections"": [
{
""error"": ""join"",
""correction"": ""joined"",
""type"": ""時態錯誤"",
""explanation"": ""第三人稱單數過去式應使用 'joined'""
},
{
""error"": ""get"",
""correction"": ""gets"",
""type"": ""時態錯誤"",
""explanation"": ""第三人稱單數現在式應使用 'gets'""
}
]
},
""sentenceMeaning"": ""她剛加入團隊,所以讓我們對她寬容一點,直到她習慣工作流程。"",
""vocabularyAnalysis"": {
""she"": {
""word"": ""she"",
""translation"": ""她"",
""definition"": ""female person pronoun"",
""partOfSpeech"": ""pronoun"",
""pronunciation"": ""/ʃiː/"",
""difficultyLevel"": ""A1"",
""isPhrase"": false,
""frequency"": ""very_high"",
""synonyms"": [""her""],
""example"": ""She is a teacher."",
""exampleTranslation"": ""她是一名老師。"",
""tags"": [""basic"", ""pronoun""]
},
""just"": {
""word"": ""just"",
""translation"": ""剛剛;僅僅"",
""definition"": ""recently; only"",
""partOfSpeech"": ""adverb"",
""pronunciation"": ""/dʒʌst/"",
""difficultyLevel"": ""A2"",
""isPhrase"": false,
""frequency"": ""high"",
""synonyms"": [""recently"", ""only"", ""merely""],
""example"": ""I just arrived."",
""exampleTranslation"": ""我剛到。"",
""tags"": [""time"", ""adverb""]
},
""joined"": {
""word"": ""joined"",
""translation"": ""加入"",
""definition"": ""became a member of (past tense of join)"",
""partOfSpeech"": ""verb"",
""pronunciation"": ""/dʒɔɪnd/"",
""difficultyLevel"": ""B1"",
""isPhrase"": false,
""frequency"": ""medium"",
""synonyms"": [""entered"", ""became part of""],
""example"": ""He joined the company last year."",
""exampleTranslation"": ""他去年加入了這家公司。"",
""tags"": [""work"", ""action""]
},
""the"": {
""word"": ""the"",
""translation"": ""定冠詞"",
""definition"": ""definite article"",
""partOfSpeech"": ""article"",
""pronunciation"": ""/ðə/"",
""difficultyLevel"": ""A1"",
""isPhrase"": false,
""frequency"": ""very_high"",
""synonyms"": [],
""example"": ""The cat is sleeping."",
""exampleTranslation"": ""貓在睡覺。"",
""tags"": [""basic""]
},
""team"": {
""word"": ""team"",
""translation"": ""團隊"",
""definition"": ""a group of people working together"",
""partOfSpeech"": ""noun"",
""pronunciation"": ""/tiːm/"",
""difficultyLevel"": ""A2"",
""isPhrase"": false,
""frequency"": ""high"",
""synonyms"": [""group"", ""crew""],
""example"": ""Our team works well together."",
""exampleTranslation"": ""我們的團隊合作得很好。"",
""tags"": [""work"", ""group""]
},
""so"": {
""word"": ""so"",
""translation"": ""所以;如此"",
""definition"": ""therefore; to such a degree"",
""partOfSpeech"": ""adverb"",
""pronunciation"": ""/soʊ/"",
""difficultyLevel"": ""A1"",
""isPhrase"": false,
""frequency"": ""very_high"",
""synonyms"": [""therefore"", ""thus""],
""example"": ""It was raining, so I stayed home."",
""exampleTranslation"": ""下雨了,所以我待在家裡。"",
""tags"": [""basic""]
},
""let's"": {
""word"": ""let's"",
""translation"": ""讓我們"",
""definition"": ""let us (contraction)"",
""partOfSpeech"": ""contraction"",
""pronunciation"": ""/lets/"",
""difficultyLevel"": ""A1"",
""isPhrase"": false,
""frequency"": ""high"",
""synonyms"": [""let us""],
""example"": ""Let's go to the park."",
""exampleTranslation"": ""我們去公園吧。"",
""tags"": [""basic""]
},
""cut"": {
""word"": ""cut"",
""translation"": ""切;削減"",
""definition"": ""to use a knife or other sharp tool to divide something"",
""partOfSpeech"": ""verb"",
""pronunciation"": ""/kʌt/"",
""difficultyLevel"": ""A2"",
""isPhrase"": false,
""frequency"": ""high"",
""synonyms"": [""slice"", ""chop"", ""reduce""],
""example"": ""Please cut the apple."",
""exampleTranslation"": ""請切蘋果。"",
""tags"": [""action""]
},
""her"": {
""word"": ""her"",
""translation"": ""她的;她"",
""definition"": ""belonging to or associated with a female"",
""partOfSpeech"": ""pronoun"",
""pronunciation"": ""/hər/"",
""difficultyLevel"": ""A1"",
""isPhrase"": false,
""frequency"": ""very_high"",
""synonyms"": [""hers""],
""example"": ""This is her book."",
""exampleTranslation"": ""這是她的書。"",
""tags"": [""basic"", ""pronoun""]
},
""some"": {
""word"": ""some"",
""translation"": ""一些"",
""definition"": ""an unspecified amount or number of"",
""partOfSpeech"": ""determiner"",
""pronunciation"": ""/sʌm/"",
""difficultyLevel"": ""A1"",
""isPhrase"": false,
""frequency"": ""very_high"",
""synonyms"": [""several"", ""a few""],
""example"": ""I need some help."",
""exampleTranslation"": ""我需要一些幫助。"",
""tags"": [""basic""]
},
""slack"": {
""word"": ""slack"",
""translation"": ""寬鬆;懈怠"",
""definition"": ""looseness; lack of tension"",
""partOfSpeech"": ""noun"",
""pronunciation"": ""/slæk/"",
""difficultyLevel"": ""B1"",
""isPhrase"": false,
""frequency"": ""medium"",
""synonyms"": [""looseness"", ""leeway""],
""example"": ""There's too much slack in this rope."",
""exampleTranslation"": ""這條繩子太鬆了。"",
""tags"": [""physical""]
},
""until"": {
""word"": ""until"",
""translation"": ""直到"",
""definition"": ""up to a particular time"",
""partOfSpeech"": ""preposition"",
""pronunciation"": ""/ʌnˈtɪl/"",
""difficultyLevel"": ""A2"",
""isPhrase"": false,
""frequency"": ""high"",
""synonyms"": [""till"", ""up to""],
""example"": ""Wait until tomorrow."",
""exampleTranslation"": ""等到明天。"",
""tags"": [""time""]
},
""gets"": {
""word"": ""gets"",
""translation"": ""變得;獲得"",
""definition"": ""becomes or obtains (third person singular)"",
""partOfSpeech"": ""verb"",
""pronunciation"": ""/ɡets/"",
""difficultyLevel"": ""A1"",
""isPhrase"": false,
""frequency"": ""very_high"",
""synonyms"": [""becomes"", ""obtains""],
""example"": ""It gets cold at night."",
""exampleTranslation"": ""晚上會變冷。"",
""tags"": [""basic""]
},
""used"": {
""word"": ""used"",
""translation"": ""習慣的"",
""definition"": ""familiar with something (used to)"",
""partOfSpeech"": ""adjective"",
""pronunciation"": ""/juːzd/"",
""difficultyLevel"": ""A2"",
""isPhrase"": false,
""frequency"": ""high"",
""synonyms"": [""accustomed"", ""familiar""],
""example"": ""I'm not used to this weather."",
""exampleTranslation"": ""我不習慣這種天氣。"",
""tags"": [""state""]
},
""to"": {
""word"": ""to"",
""translation"": ""到;向"",
""definition"": ""preposition expressing direction"",
""partOfSpeech"": ""preposition"",
""pronunciation"": ""/tu/"",
""difficultyLevel"": ""A1"",
""isPhrase"": false,
""frequency"": ""very_high"",
""synonyms"": [],
""example"": ""I'm going to school."",
""exampleTranslation"": ""我要去學校。"",
""tags"": [""basic""]
},
""workflow"": {
""word"": ""workflow"",
""translation"": ""工作流程"",
""definition"": ""the sequence of processes through which work passes"",
""partOfSpeech"": ""noun"",
""pronunciation"": ""/ˈːrkfloʊ/"",
""difficultyLevel"": ""B2"",
""isPhrase"": false,
""frequency"": ""medium"",
""synonyms"": [""process"", ""procedure"", ""system""],
""example"": ""We need to improve our workflow."",
""exampleTranslation"": ""我們需要改善工作流程。"",
""tags"": [""work"", ""process""]
},
""cut someone some slack"": {
""word"": ""cut someone some slack"",
""translation"": ""對某人寬容一點"",
""definition"": ""to be more lenient or forgiving with someone"",
""partOfSpeech"": ""idiom"",
""pronunciation"": ""/kʌt ˈsʌmwʌn sʌm slæk/"",
""difficultyLevel"": ""B2"",
""isPhrase"": true,
""frequency"": ""medium"",
""synonyms"": [""be lenient"", ""be forgiving"", ""give leeway""],
""example"": ""Cut him some slack, he's new here."",
""exampleTranslation"": ""對他寬容一點,他是新來的。"",
""tags"": [""idiom"", ""workplace"", ""tolerance""]
}
}
}";
}
private SentenceAnalysisData ParseGeminiResponse(string response, string originalText, string userLevel)
{
try
{
// Clean the response to extract JSON
var jsonMatch = Regex.Match(response, @"\{.*\}", RegexOptions.Singleline);
if (!jsonMatch.Success)
{
throw new InvalidOperationException("Invalid JSON response from Gemini");
}
var jsonResponse = jsonMatch.Value;
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
var parsedResponse = JsonSerializer.Deserialize<GeminiAnalysisResponse>(jsonResponse, options)
?? throw new InvalidOperationException("Failed to parse Gemini response");
return ConvertToAnalysisData(parsedResponse, originalText, userLevel);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error parsing Gemini response: {Response}", response);
return CreateFallbackResponse(originalText, userLevel);
}
}
private SentenceAnalysisData ConvertToAnalysisData(GeminiAnalysisResponse response, string originalText, string userLevel)
{
var analysisData = new SentenceAnalysisData
{
OriginalText = originalText,
SentenceMeaning = response.SentenceMeaning ?? string.Empty,
GrammarCorrection = response.GrammarCorrection != null ? new GrammarCorrectionDto
{
HasErrors = response.GrammarCorrection.HasErrors,
CorrectedText = response.GrammarCorrection.CorrectedText ?? originalText,
Corrections = response.GrammarCorrection.Corrections?.Select(c => new GrammarErrorDto
{
Error = c.Error ?? string.Empty,
Correction = c.Correction ?? string.Empty,
Type = c.Type ?? string.Empty,
Explanation = c.Explanation ?? string.Empty,
Severity = "medium"
}).ToList() ?? new()
} : null,
Metadata = new AnalysisMetadata
{
UserLevel = userLevel,
ProcessingDate = DateTime.UtcNow,
AnalysisModel = "gemini-pro"
}
};
// Process vocabulary analysis
if (response.VocabularyAnalysis != null)
{
foreach (var (word, analysis) in response.VocabularyAnalysis)
{
analysisData.VocabularyAnalysis[word] = new VocabularyAnalysisDto
{
Word = analysis.Word ?? word,
Translation = analysis.Translation ?? string.Empty,
Definition = analysis.Definition ?? string.Empty,
PartOfSpeech = analysis.PartOfSpeech ?? string.Empty,
Pronunciation = analysis.Pronunciation ?? $"/{word}/",
DifficultyLevel = analysis.DifficultyLevel ?? "A1",
IsPhrase = analysis.IsPhrase,
Frequency = analysis.Frequency ?? "medium",
Synonyms = analysis.Synonyms ?? new List<string>(),
Example = analysis.Example,
ExampleTranslation = analysis.ExampleTranslation,
Tags = analysis.Tags ?? new List<string>()
};
}
}
// Calculate statistics
analysisData.Statistics = CalculateStatistics(analysisData.VocabularyAnalysis, userLevel);
return analysisData;
}
private AnalysisStatistics CalculateStatistics(Dictionary<string, VocabularyAnalysisDto> vocabulary, string userLevel)
{
var userIndex = Array.IndexOf(_cefrLevels, userLevel);
var stats = new AnalysisStatistics();
foreach (var word in vocabulary.Values)
{
var wordIndex = Array.IndexOf(_cefrLevels, word.DifficultyLevel);
if (word.IsPhrase)
{
stats.Phrases++;
}
else if (wordIndex < userIndex)
{
stats.SimpleWords++;
}
else if (wordIndex == userIndex)
{
stats.ModerateWords++;
}
else
{
stats.DifficultWords++;
}
}
stats.TotalWords = vocabulary.Count;
stats.UniqueWords = vocabulary.Count;
stats.AverageDifficulty = userLevel; // Simplified calculation
return stats;
}
private SentenceAnalysisData CreateFallbackResponse(string originalText, string userLevel)
{
_logger.LogWarning("Using fallback response for text: {Text}", originalText);
return new SentenceAnalysisData
{
OriginalText = originalText,
SentenceMeaning = "分析過程中發生錯誤,請稍後再試。",
Metadata = new AnalysisMetadata
{
UserLevel = userLevel,
ProcessingDate = DateTime.UtcNow,
AnalysisModel = "fallback"
}
};
}
}
// Gemini API response models
internal class GeminiApiResponse
{
public List<GeminiCandidate>? Candidates { 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; }
}
// Internal models for Gemini response parsing
internal class GeminiAnalysisResponse
{
public GeminiGrammarCorrection? GrammarCorrection { get; set; }
public string? SentenceMeaning { get; set; }
public Dictionary<string, GeminiVocabularyAnalysis>? VocabularyAnalysis { get; set; }
}
internal class GeminiGrammarCorrection
{
public bool HasErrors { get; set; }
public string? CorrectedText { get; set; }
public List<GeminiGrammarError>? Corrections { get; set; }
}
internal class GeminiGrammarError
{
public string? Error { get; set; }
public string? Correction { get; set; }
public string? Type { get; set; }
public string? Explanation { get; set; }
}
internal class GeminiVocabularyAnalysis
{
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 bool IsPhrase { get; set; }
public string? Frequency { get; set; }
public List<string>? Synonyms { get; set; }
public string? Example { get; set; }
public string? ExampleTranslation { get; set; }
public List<string>? Tags { get; set; }
}