585 lines
23 KiB
C#
585 lines
23 KiB
C#
using DramaLing.Api.Models.DTOs;
|
||
using DramaLing.Api.Models.Entities;
|
||
using DramaLing.Api.Models.Configuration;
|
||
using Microsoft.Extensions.Options;
|
||
using System.Text.Json;
|
||
using System.Text;
|
||
|
||
namespace DramaLing.Api.Services;
|
||
|
||
public interface IGeminiService
|
||
{
|
||
Task<SentenceAnalysisData> AnalyzeSentenceAsync(string inputText, AnalysisOptions options);
|
||
Task<string> GenerateImageDescriptionAsync(Flashcard flashcard, GenerationOptionsDto options);
|
||
}
|
||
|
||
public class GeminiService : IGeminiService
|
||
{
|
||
private readonly HttpClient _httpClient;
|
||
private readonly ILogger<GeminiService> _logger;
|
||
private readonly GeminiOptions _options;
|
||
|
||
public GeminiService(HttpClient httpClient, IOptions<GeminiOptions> options, ILogger<GeminiService> logger)
|
||
{
|
||
_httpClient = httpClient;
|
||
_logger = logger;
|
||
_options = options.Value;
|
||
|
||
_logger.LogInformation("GeminiService initialized with model: {Model}, timeout: {Timeout}s",
|
||
_options.Model, _options.TimeoutSeconds);
|
||
|
||
_httpClient.Timeout = TimeSpan.FromSeconds(_options.TimeoutSeconds);
|
||
_httpClient.DefaultRequestHeaders.Add("User-Agent", "DramaLing/1.0");
|
||
}
|
||
|
||
public async Task<SentenceAnalysisData> AnalyzeSentenceAsync(string inputText, AnalysisOptions options)
|
||
{
|
||
var startTime = DateTime.UtcNow;
|
||
|
||
try
|
||
{
|
||
_logger.LogInformation("Starting sentence analysis for text: {Text}",
|
||
inputText.Substring(0, Math.Min(50, inputText.Length)));
|
||
|
||
// 符合產品需求規格的結構化 prompt
|
||
var prompt = $@"You are an English learning assistant. Analyze this sentence and return ONLY a valid JSON response.
|
||
|
||
**Input Sentence**: ""{inputText}""
|
||
|
||
**Required JSON Structure:**
|
||
{{
|
||
""sentenceTranslation"": ""Traditional Chinese translation of the entire sentence"",
|
||
""hasGrammarErrors"": true/false,
|
||
""grammarCorrections"": [
|
||
{{
|
||
""original"": ""incorrect text"",
|
||
""corrected"": ""correct text"",
|
||
""type"": ""error type (tense/subject-verb/preposition/word-order)"",
|
||
""explanation"": ""brief explanation in Traditional Chinese""
|
||
}}
|
||
],
|
||
""vocabularyAnalysis"": {{
|
||
""word1"": {{
|
||
""word"": ""the word"",
|
||
""translation"": ""Traditional Chinese translation"",
|
||
""definition"": ""English definition"",
|
||
""partOfSpeech"": ""noun/verb/adjective/adverb/pronoun/preposition/conjunction/interjection"",
|
||
""pronunciation"": ""/phonetic/"",
|
||
""difficultyLevel"": ""A1/A2/B1/B2/C1/C2"",
|
||
""frequency"": ""high/medium/low"",
|
||
""synonyms"": [""synonym1"", ""synonym2""],
|
||
""example"": ""example sentence"",
|
||
""exampleTranslation"": ""Traditional Chinese example translation""
|
||
}}
|
||
}},
|
||
""idioms"": [
|
||
{{
|
||
""idiom"": ""idiomatic expression"",
|
||
""translation"": ""Traditional Chinese meaning"",
|
||
""definition"": ""English explanation"",
|
||
""pronunciation"": ""/phonetic notation/"",
|
||
""difficultyLevel"": ""A1/A2/B1/B2/C1/C2"",
|
||
""frequency"": ""high/medium/low"",
|
||
""synonyms"": [""synonym1"", ""synonym2""],
|
||
""example"": ""usage example"",
|
||
""exampleTranslation"": ""Traditional Chinese example""
|
||
}}
|
||
]
|
||
}}
|
||
|
||
**Analysis Guidelines:**
|
||
1. **Grammar Check**: Detect tense errors, subject-verb agreement, preposition usage, word order
|
||
2. **Vocabulary Analysis**: Include ALL significant words (exclude articles: a, an, the)
|
||
3. **CEFR Levels**: Assign accurate A1-C2 levels for each word
|
||
4. **Idioms**: Identify any idiomatic expressions or phrasal verbs
|
||
5. **Translations**: Use Traditional Chinese (Taiwan standard)
|
||
|
||
**IMPORTANT**: Return ONLY the JSON object, no additional text or explanation.";
|
||
|
||
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");
|
||
}
|
||
|
||
// 檢查是否是安全過濾的回退訊息
|
||
if (aiResponse.Contains("temporarily unavailable due to safety filtering"))
|
||
{
|
||
// 這是安全過濾的情況,但我們仍然要處理它而不是拋出異常
|
||
_logger.LogWarning("Using safety filtering fallback response");
|
||
}
|
||
|
||
// 直接使用 AI 的回應創建分析數據
|
||
var analysisData = CreateAnalysisFromAIResponse(inputText, 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 aiResponse)
|
||
{
|
||
_logger.LogInformation("Creating analysis from AI response: {ResponsePreview}...",
|
||
aiResponse.Substring(0, Math.Min(100, aiResponse.Length)));
|
||
|
||
try
|
||
{
|
||
// 清理 AI 回應以確保是純 JSON
|
||
var cleanJson = aiResponse.Trim();
|
||
if (cleanJson.StartsWith("```json"))
|
||
{
|
||
cleanJson = cleanJson.Substring(7);
|
||
}
|
||
if (cleanJson.EndsWith("```"))
|
||
{
|
||
cleanJson = cleanJson.Substring(0, cleanJson.Length - 3);
|
||
}
|
||
|
||
// 解析 AI 回應的 JSON
|
||
var aiAnalysis = JsonSerializer.Deserialize<AiAnalysisResponse>(cleanJson, new JsonSerializerOptions
|
||
{
|
||
PropertyNameCaseInsensitive = true
|
||
});
|
||
|
||
if (aiAnalysis == null)
|
||
{
|
||
throw new InvalidOperationException("Failed to parse AI response JSON");
|
||
}
|
||
|
||
// 轉換為 DTO 結構
|
||
var analysisData = new SentenceAnalysisData
|
||
{
|
||
OriginalText = inputText,
|
||
SentenceMeaning = aiAnalysis.SentenceTranslation ?? "",
|
||
VocabularyAnalysis = ConvertVocabularyAnalysis(aiAnalysis.VocabularyAnalysis ?? new()),
|
||
Idioms = ConvertIdioms(aiAnalysis.Idioms ?? new()),
|
||
GrammarCorrection = ConvertGrammarCorrection(aiAnalysis),
|
||
Metadata = new AnalysisMetadata
|
||
{
|
||
ProcessingDate = DateTime.UtcNow,
|
||
AnalysisModel = "gemini-1.5-flash",
|
||
AnalysisVersion = "2.0"
|
||
}
|
||
};
|
||
|
||
|
||
return analysisData;
|
||
}
|
||
catch (JsonException ex)
|
||
{
|
||
_logger.LogError(ex, "Failed to parse AI response as JSON: {Response}", aiResponse);
|
||
// 回退到舊的處理方式
|
||
return CreateFallbackAnalysis(inputText, aiResponse);
|
||
}
|
||
}
|
||
|
||
private Dictionary<string, VocabularyAnalysisDto> ConvertVocabularyAnalysis(Dictionary<string, AiVocabularyAnalysis> aiVocab)
|
||
{
|
||
var result = new Dictionary<string, VocabularyAnalysisDto>();
|
||
|
||
foreach (var kvp in aiVocab)
|
||
{
|
||
var aiWord = kvp.Value;
|
||
result[kvp.Key] = new VocabularyAnalysisDto
|
||
{
|
||
Word = aiWord.Word ?? kvp.Key,
|
||
Translation = aiWord.Translation ?? "",
|
||
Definition = aiWord.Definition ?? "",
|
||
PartOfSpeech = aiWord.PartOfSpeech ?? "unknown",
|
||
Pronunciation = aiWord.Pronunciation ?? $"/{kvp.Key}/",
|
||
DifficultyLevel = aiWord.DifficultyLevel ?? "A2",
|
||
Frequency = aiWord.Frequency ?? "medium",
|
||
Synonyms = aiWord.Synonyms ?? new List<string>(),
|
||
Example = aiWord.Example,
|
||
ExampleTranslation = aiWord.ExampleTranslation,
|
||
};
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
private List<IdiomDto> ConvertIdioms(List<AiIdiom> aiIdioms)
|
||
{
|
||
var result = new List<IdiomDto>();
|
||
|
||
foreach (var aiIdiom in aiIdioms)
|
||
{
|
||
result.Add(new IdiomDto
|
||
{
|
||
Idiom = aiIdiom.Idiom ?? "",
|
||
Translation = aiIdiom.Translation ?? "",
|
||
Definition = aiIdiom.Definition ?? "",
|
||
Pronunciation = aiIdiom.Pronunciation ?? "",
|
||
DifficultyLevel = aiIdiom.DifficultyLevel ?? "B2",
|
||
Frequency = aiIdiom.Frequency ?? "medium",
|
||
Synonyms = aiIdiom.Synonyms ?? new List<string>(),
|
||
Example = aiIdiom.Example,
|
||
ExampleTranslation = aiIdiom.ExampleTranslation
|
||
});
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
private GrammarCorrectionDto? ConvertGrammarCorrection(AiAnalysisResponse aiAnalysis)
|
||
{
|
||
if (!aiAnalysis.HasGrammarErrors || aiAnalysis.GrammarCorrections == null || !aiAnalysis.GrammarCorrections.Any())
|
||
{
|
||
return null;
|
||
}
|
||
|
||
var corrections = aiAnalysis.GrammarCorrections.Select(gc => new GrammarErrorDto
|
||
{
|
||
Error = gc.Original ?? "",
|
||
Correction = gc.Corrected ?? "",
|
||
Type = gc.Type ?? "grammar",
|
||
Explanation = gc.Explanation ?? "",
|
||
Severity = "medium",
|
||
Position = new ErrorPosition { Start = 0, End = 0 } // 簡化處理
|
||
}).ToList();
|
||
|
||
return new GrammarCorrectionDto
|
||
{
|
||
HasErrors = true,
|
||
CorrectedText = string.Join(" ", corrections.Select(c => c.Correction)),
|
||
Corrections = corrections
|
||
};
|
||
}
|
||
|
||
private SentenceAnalysisData CreateFallbackAnalysis(string inputText, string aiResponse)
|
||
{
|
||
_logger.LogWarning("Using fallback analysis due to JSON parsing failure");
|
||
|
||
return new SentenceAnalysisData
|
||
{
|
||
OriginalText = inputText,
|
||
SentenceMeaning = aiResponse,
|
||
VocabularyAnalysis = CreateBasicVocabularyFromText(inputText, aiResponse),
|
||
Metadata = new AnalysisMetadata
|
||
{
|
||
ProcessingDate = DateTime.UtcNow,
|
||
AnalysisModel = "gemini-1.5-flash-fallback",
|
||
AnalysisVersion = "2.0"
|
||
},
|
||
};
|
||
}
|
||
|
||
private Dictionary<string, VocabularyAnalysisDto> CreateBasicVocabularyFromText(string inputText, string aiResponse)
|
||
{
|
||
// 從 AI 回應中提取真實的詞彙翻譯
|
||
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 = ExtractTranslationFromAI(cleanWord, aiResponse),
|
||
Definition = $"Please refer to the AI analysis above for detailed definition.",
|
||
PartOfSpeech = "unknown",
|
||
Pronunciation = $"/{cleanWord}/",
|
||
DifficultyLevel = EstimateBasicDifficulty(cleanWord),
|
||
Frequency = "medium",
|
||
Synonyms = new List<string>(),
|
||
Example = null,
|
||
ExampleTranslation = null,
|
||
};
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
private string ExtractTranslationFromAI(string word, string aiResponse)
|
||
{
|
||
// 嘗試從 AI 回應中提取該詞的翻譯
|
||
// 這是簡化版本,真正的版本應該解析完整的 JSON
|
||
if (aiResponse.Contains(word, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return $"{word} translation from AI";
|
||
}
|
||
return $"{word} - 請查看完整分析";
|
||
}
|
||
|
||
private string EstimateBasicDifficulty(string word)
|
||
{
|
||
// 基本詞彙列表(這是最小的 fallback 邏輯)
|
||
var a1Words = new[] { "i", "you", "he", "she", "it", "we", "they", "the", "a", "an", "is", "are", "was", "were", "have", "do", "go", "get", "see", "know" };
|
||
var a2Words = new[] { "make", "think", "come", "take", "use", "work", "call", "try", "ask", "need", "feel", "become", "leave", "put", "say", "tell", "turn", "move" };
|
||
|
||
var lowerWord = word.ToLower();
|
||
if (a1Words.Contains(lowerWord)) return "A1";
|
||
if (a2Words.Contains(lowerWord)) return "A2";
|
||
if (word.Length <= 4) return "A2";
|
||
if (word.Length <= 6) return "B1";
|
||
return "B2";
|
||
}
|
||
|
||
|
||
private async Task<string> CallGeminiAPI(string prompt)
|
||
{
|
||
try
|
||
{
|
||
var requestBody = new
|
||
{
|
||
contents = new[]
|
||
{
|
||
new
|
||
{
|
||
parts = new[]
|
||
{
|
||
new { text = prompt }
|
||
}
|
||
}
|
||
},
|
||
generationConfig = new
|
||
{
|
||
temperature = _options.Temperature,
|
||
topK = 40,
|
||
topP = 0.95,
|
||
maxOutputTokens = _options.MaxOutputTokens
|
||
}
|
||
};
|
||
|
||
var json = JsonSerializer.Serialize(requestBody);
|
||
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||
|
||
var response = await _httpClient.PostAsync($"{_options.BaseUrl}/v1beta/models/{_options.Model}:generateContent?key={_options.ApiKey}", content);
|
||
response.EnsureSuccessStatusCode();
|
||
|
||
var responseJson = await response.Content.ReadAsStringAsync();
|
||
_logger.LogInformation("Raw Gemini API response: {Response}", responseJson.Substring(0, Math.Min(500, responseJson.Length)));
|
||
|
||
// 先嘗試使用動態解析來避免反序列化問題
|
||
using var document = JsonDocument.Parse(responseJson);
|
||
var root = document.RootElement;
|
||
|
||
string aiText = string.Empty;
|
||
|
||
// 檢查是否有 candidates 陣列
|
||
if (root.TryGetProperty("candidates", out var candidatesElement) && candidatesElement.ValueKind == JsonValueKind.Array)
|
||
{
|
||
_logger.LogInformation("Found candidates array with {Count} items", candidatesElement.GetArrayLength());
|
||
|
||
var firstCandidate = candidatesElement.EnumerateArray().FirstOrDefault();
|
||
if (firstCandidate.ValueKind != JsonValueKind.Undefined)
|
||
{
|
||
if (firstCandidate.TryGetProperty("content", out var contentElement))
|
||
{
|
||
if (contentElement.TryGetProperty("parts", out var partsElement) && partsElement.ValueKind == JsonValueKind.Array)
|
||
{
|
||
var firstPart = partsElement.EnumerateArray().FirstOrDefault();
|
||
if (firstPart.TryGetProperty("text", out var textElement))
|
||
{
|
||
aiText = textElement.GetString() ?? string.Empty;
|
||
_logger.LogInformation("Successfully extracted text: {Length} characters", aiText.Length);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 檢查是否有安全過濾
|
||
if (root.TryGetProperty("promptFeedback", out var feedbackElement))
|
||
{
|
||
_logger.LogWarning("Gemini prompt feedback received: {Feedback}", feedbackElement.ToString());
|
||
}
|
||
|
||
_logger.LogInformation("Gemini API returned: {ResponseLength} characters", aiText.Length);
|
||
if (!string.IsNullOrEmpty(aiText))
|
||
{
|
||
_logger.LogInformation("AI text preview: {Preview}", aiText.Substring(0, Math.Min(200, aiText.Length)));
|
||
}
|
||
|
||
// 如果沒有獲取到文本且有安全過濾回饋,返回友好訊息
|
||
if (string.IsNullOrWhiteSpace(aiText) && root.TryGetProperty("promptFeedback", out _))
|
||
{
|
||
return "The content analysis is temporarily unavailable due to safety filtering. Please try with different content.";
|
||
}
|
||
|
||
return aiText;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "Gemini API call failed");
|
||
throw;
|
||
}
|
||
}
|
||
|
||
public async Task<string> GenerateImageDescriptionAsync(Flashcard flashcard, GenerationOptionsDto options)
|
||
{
|
||
try
|
||
{
|
||
_logger.LogInformation("Starting image description generation for flashcard {FlashcardId}", flashcard.Id);
|
||
|
||
var prompt = BuildImageDescriptionPrompt(flashcard, options);
|
||
var response = await CallGeminiAPI(prompt);
|
||
|
||
if (string.IsNullOrWhiteSpace(response))
|
||
{
|
||
throw new InvalidOperationException("Gemini API returned empty response");
|
||
}
|
||
|
||
var description = ExtractImageDescription(response);
|
||
var optimizedPrompt = OptimizeForReplicate(description, options);
|
||
|
||
_logger.LogInformation("Image description generated successfully for flashcard {FlashcardId}", flashcard.Id);
|
||
|
||
return optimizedPrompt;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "Image description generation failed for flashcard {FlashcardId}", flashcard.Id);
|
||
throw;
|
||
}
|
||
}
|
||
|
||
private string BuildImageDescriptionPrompt(Flashcard flashcard, GenerationOptionsDto options)
|
||
{
|
||
return $@"# 總覽
|
||
你是一位專業插畫設計師兼職英文老師,專門為英語學習教材製作插畫圖卡,用來幫助學生理解英文例句的意思。
|
||
|
||
# 例句資訊
|
||
例句:{flashcard.Example}
|
||
|
||
# SOP
|
||
1. 根據上述英文例句,請撰寫一段圖像描述提示詞,用於提供圖片生成AI作為生成圖片的提示詞
|
||
2. 請將下方「風格指南」的所有要求加入提示詞中
|
||
3. 並於圖片提示詞最後加上:「Absolutely no visible text, characters, letters, numbers, symbols, handwriting, labels, or any form of writing anywhere in the image — including on signs, books, clothing, screens, or backgrounds.」
|
||
|
||
# 圖片提示詞規範
|
||
|
||
## 情境清楚
|
||
1. 角色描述具體清楚
|
||
2. 動作明確具象
|
||
3. 場景明確具體
|
||
4. 物品明確具體
|
||
5. 語意需與原句一致
|
||
6. 避免過於抽象或象徵性符號
|
||
|
||
## 風格指南
|
||
- 風格類型:扁平插畫(Flat Illustration)
|
||
- 線條特徵:無描邊線條(outline-less)
|
||
- 色調:暖色調、柔和、低飽和
|
||
- 人物樣式:簡化卡通人物,表情自然,不誇張
|
||
- 背景構成:圖形簡化,使用色塊區分層次
|
||
- 整體氛圍:溫馨、平靜、適合教育情境
|
||
- 技術風格:無紋理、無漸層、無光影寫實感
|
||
|
||
請根據以上規範,為這個英文例句生成圖片描述提示詞,並確保完全符合風格指南要求。";
|
||
}
|
||
|
||
private string ExtractImageDescription(string geminiResponse)
|
||
{
|
||
// 從 Gemini 回應中提取圖片描述
|
||
var description = geminiResponse.Trim();
|
||
|
||
// 移除可能的 markdown 標記
|
||
if (description.StartsWith("```"))
|
||
{
|
||
var lines = description.Split('\n');
|
||
description = string.Join('\n', lines.Skip(1).SkipLast(1));
|
||
}
|
||
|
||
return description.Trim();
|
||
}
|
||
|
||
private string OptimizeForReplicate(string description, GenerationOptionsDto options)
|
||
{
|
||
var optimizedPrompt = description;
|
||
|
||
// 確保包含扁平插畫風格要求
|
||
if (!optimizedPrompt.Contains("flat illustration"))
|
||
{
|
||
optimizedPrompt += ". Style guide: flat illustration style, outline-less shapes, warm and soft color tones, low saturation, cartoon-style characters with natural expressions, simplified background with color blocks, cozy and educational atmosphere, no texture, no gradients, no photorealism, no fantasy elements.";
|
||
}
|
||
|
||
// 強制加入禁止文字的規則
|
||
if (!optimizedPrompt.Contains("Absolutely no visible text"))
|
||
{
|
||
optimizedPrompt += " Absolutely no visible text, characters, letters, numbers, symbols, handwriting, labels, or any form of writing anywhere in the image — including on signs, books, clothing, screens, or backgrounds.";
|
||
}
|
||
|
||
return optimizedPrompt;
|
||
}
|
||
}
|
||
|
||
// Gemini API response models
|
||
internal class GeminiApiResponse
|
||
{
|
||
public List<GeminiCandidate>? Candidates { get; set; }
|
||
public object? PromptFeedback { 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; }
|
||
}
|
||
|
||
// AI Response JSON Models
|
||
internal class AiAnalysisResponse
|
||
{
|
||
public string? SentenceTranslation { get; set; }
|
||
public bool HasGrammarErrors { get; set; }
|
||
public List<AiGrammarCorrection>? GrammarCorrections { get; set; }
|
||
public Dictionary<string, AiVocabularyAnalysis>? VocabularyAnalysis { get; set; }
|
||
public List<AiIdiom>? Idioms { get; set; }
|
||
}
|
||
|
||
internal class AiGrammarCorrection
|
||
{
|
||
public string? Original { get; set; }
|
||
public string? Corrected { get; set; }
|
||
public string? Type { get; set; }
|
||
public string? Explanation { get; set; }
|
||
}
|
||
|
||
internal class AiVocabularyAnalysis
|
||
{
|
||
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 string? Frequency { get; set; }
|
||
public List<string>? Synonyms { get; set; }
|
||
public string? Example { get; set; }
|
||
public string? ExampleTranslation { get; set; }
|
||
}
|
||
|
||
internal class AiIdiom
|
||
{
|
||
public string? Idiom { get; set; }
|
||
public string? Translation { get; set; }
|
||
public string? Definition { get; set; }
|
||
public string? Pronunciation { get; set; }
|
||
public string? DifficultyLevel { get; set; }
|
||
public string? Frequency { get; set; }
|
||
public List<string>? Synonyms { get; set; }
|
||
public string? Example { get; set; }
|
||
public string? ExampleTranslation { get; set; }
|
||
} |