237 lines
8.9 KiB
C#
237 lines
8.9 KiB
C#
using DramaLing.Api.Models.DTOs;
|
|
using System.Text.Json;
|
|
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 _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"]
|
|
?? throw new InvalidOperationException("Gemini API Key not configured");
|
|
|
|
_logger.LogInformation("GeminiService initialized with API key: {ApiKeyStart}...",
|
|
_apiKey.Length > 10 ? _apiKey.Substring(0, 10) : "[key-not-set]");
|
|
|
|
_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);
|
|
|
|
// 使用簡單的 prompt 直接調用 Gemini API
|
|
var prompt = $"Translate this English sentence to Traditional Chinese and provide grammar analysis: \"{inputText}\"";
|
|
|
|
var aiResponse = await CallGeminiAPI(prompt);
|
|
|
|
_logger.LogInformation("Gemini AI response received: {ResponseLength} characters", aiResponse?.Length ?? 0);
|
|
|
|
if (string.IsNullOrWhiteSpace(aiResponse))
|
|
{
|
|
throw new InvalidOperationException("Gemini API returned empty response");
|
|
}
|
|
|
|
// 直接使用 AI 的回應創建分析數據
|
|
var analysisData = CreateAnalysisFromAIResponse(inputText, userLevel, aiResponse);
|
|
|
|
var processingTime = (DateTime.UtcNow - startTime).TotalSeconds;
|
|
_logger.LogInformation("Sentence analysis completed in {ProcessingTime}s", processingTime);
|
|
|
|
return analysisData;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error analyzing sentence: {Text}", inputText);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
private SentenceAnalysisData CreateAnalysisFromAIResponse(string inputText, string userLevel, string aiResponse)
|
|
{
|
|
_logger.LogInformation("Creating analysis from AI response: {ResponsePreview}...",
|
|
aiResponse.Substring(0, Math.Min(100, aiResponse.Length)));
|
|
|
|
// 直接使用 AI 回應作為分析結果
|
|
var analysisData = new SentenceAnalysisData
|
|
{
|
|
OriginalText = inputText,
|
|
SentenceMeaning = aiResponse, // 直接使用 AI 的完整回應作為分析結果
|
|
VocabularyAnalysis = CreateBasicVocabularyFromText(inputText, aiResponse),
|
|
Metadata = new AnalysisMetadata
|
|
{
|
|
UserLevel = userLevel,
|
|
ProcessingDate = DateTime.UtcNow,
|
|
AnalysisModel = "gemini-1.5-flash",
|
|
AnalysisVersion = "1.0"
|
|
}
|
|
};
|
|
|
|
// 檢查是否有語法錯誤(基於 AI 回應)
|
|
if (aiResponse.ToLower().Contains("error") || aiResponse.ToLower().Contains("incorrect") ||
|
|
aiResponse.ToLower().Contains("should be") || aiResponse.ToLower().Contains("錯誤"))
|
|
{
|
|
analysisData.GrammarCorrection = new GrammarCorrectionDto
|
|
{
|
|
HasErrors = true,
|
|
CorrectedText = inputText, // 保持原文,讓用戶看到 AI 的建議
|
|
Corrections = new List<GrammarErrorDto>
|
|
{
|
|
new GrammarErrorDto
|
|
{
|
|
Error = "AI detected grammar issues",
|
|
Correction = "See AI analysis above",
|
|
Type = "AI Grammar Check",
|
|
Explanation = aiResponse, // 直接使用 AI 的解釋
|
|
Severity = "medium"
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
// 計算統計(使用統一的慣用語術語)
|
|
analysisData.Statistics = CalculateStatistics(analysisData.VocabularyAnalysis, userLevel);
|
|
|
|
return analysisData;
|
|
}
|
|
|
|
private Dictionary<string, VocabularyAnalysisDto> CreateBasicVocabularyFromText(string inputText, string aiResponse)
|
|
{
|
|
var words = inputText.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
|
var result = new Dictionary<string, VocabularyAnalysisDto>();
|
|
|
|
foreach (var word in words.Take(15))
|
|
{
|
|
var cleanWord = word.ToLower().Trim('.', ',', '!', '?', ';', ':', '"', '\'');
|
|
if (string.IsNullOrEmpty(cleanWord) || cleanWord.Length < 2) continue;
|
|
|
|
result[cleanWord] = new VocabularyAnalysisDto
|
|
{
|
|
Word = cleanWord,
|
|
Translation = $"{cleanWord} - AI分析請查看上方詳細說明",
|
|
Definition = $"Definition in AI analysis above",
|
|
PartOfSpeech = "word",
|
|
Pronunciation = $"/{cleanWord}/",
|
|
DifficultyLevel = EstimateBasicDifficulty(cleanWord),
|
|
IsIdiom = false, // 統一使用 IsIdiom
|
|
Frequency = "medium",
|
|
Synonyms = new List<string>(),
|
|
Example = $"See AI analysis for {cleanWord}",
|
|
ExampleTranslation = "詳見上方AI分析",
|
|
Tags = new List<string> { "ai-analyzed" }
|
|
};
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private string EstimateBasicDifficulty(string word)
|
|
{
|
|
var basicWords = new[] { "i", "you", "he", "she", "it", "we", "they", "the", "a", "an", "is", "are", "was", "were" };
|
|
return basicWords.Contains(word.ToLower()) ? "A1" : "A2";
|
|
}
|
|
|
|
private AnalysisStatistics CalculateStatistics(Dictionary<string, VocabularyAnalysisDto> vocabulary, string userLevel)
|
|
{
|
|
var stats = new AnalysisStatistics
|
|
{
|
|
TotalWords = vocabulary.Count,
|
|
UniqueWords = vocabulary.Count,
|
|
SimpleWords = vocabulary.Count(kvp => kvp.Value.DifficultyLevel == "A1"),
|
|
ModerateWords = vocabulary.Count(kvp => kvp.Value.DifficultyLevel == "A2"),
|
|
DifficultWords = vocabulary.Count(kvp => !new[] { "A1", "A2" }.Contains(kvp.Value.DifficultyLevel)),
|
|
Idioms = vocabulary.Count(kvp => kvp.Value.IsIdiom), // 統一使用 Idioms
|
|
AverageDifficulty = userLevel
|
|
};
|
|
|
|
return stats;
|
|
}
|
|
|
|
private async Task<string> CallGeminiAPI(string prompt)
|
|
{
|
|
try
|
|
{
|
|
var requestBody = new
|
|
{
|
|
contents = new[]
|
|
{
|
|
new
|
|
{
|
|
parts = new[]
|
|
{
|
|
new { text = prompt }
|
|
}
|
|
}
|
|
},
|
|
generationConfig = new
|
|
{
|
|
temperature = 0.7,
|
|
topK = 40,
|
|
topP = 0.95,
|
|
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-1.5-flash:generateContent?key={_apiKey}", content);
|
|
response.EnsureSuccessStatusCode();
|
|
|
|
var responseJson = await response.Content.ReadAsStringAsync();
|
|
var geminiResponse = JsonSerializer.Deserialize<GeminiApiResponse>(responseJson);
|
|
|
|
var aiText = geminiResponse?.Candidates?.FirstOrDefault()?.Content?.Parts?.FirstOrDefault()?.Text ?? string.Empty;
|
|
|
|
_logger.LogInformation("Gemini API returned: {ResponseLength} characters", aiText.Length);
|
|
|
|
return aiText;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Gemini API call failed");
|
|
throw;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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; }
|
|
} |