diff --git a/backend/DramaLing.Api/Extensions/ServiceCollectionExtensions.cs b/backend/DramaLing.Api/Extensions/ServiceCollectionExtensions.cs index d8cce98..38e2c31 100644 --- a/backend/DramaLing.Api/Extensions/ServiceCollectionExtensions.cs +++ b/backend/DramaLing.Api/Extensions/ServiceCollectionExtensions.cs @@ -136,9 +136,6 @@ public static class ServiceCollectionExtensions public static IServiceCollection AddBusinessServices(this IServiceCollection services) { services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); // 媒體服務 services.AddScoped(); diff --git a/backend/DramaLing.Api/Services/AI/Analysis/IGeminiAnalyzer.cs b/backend/DramaLing.Api/Services/AI/Analysis/IGeminiAnalyzer.cs deleted file mode 100644 index a06648c..0000000 --- a/backend/DramaLing.Api/Services/AI/Analysis/IGeminiAnalyzer.cs +++ /dev/null @@ -1,8 +0,0 @@ -using DramaLing.Api.Models.DTOs; - -namespace DramaLing.Api.Services.AI.Analysis; - -public interface IGeminiAnalyzer -{ - Task AnalyzeSentenceAsync(string inputText, AnalysisOptions options); -} \ No newline at end of file diff --git a/backend/DramaLing.Api/Services/Infrastructure/Monitoring/UsageTrackingService.cs b/backend/DramaLing.Api/Services/Infrastructure/Monitoring/UsageTrackingService.cs deleted file mode 100644 index 85247d7..0000000 --- a/backend/DramaLing.Api/Services/Infrastructure/Monitoring/UsageTrackingService.cs +++ /dev/null @@ -1,255 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using DramaLing.Api.Data; -using DramaLing.Api.Models.Entities; - -namespace DramaLing.Api.Services; - -public interface IUsageTrackingService -{ - Task CheckUsageLimitAsync(Guid userId, bool isPremium = false); - Task RecordSentenceAnalysisAsync(Guid userId); - Task RecordWordQueryAsync(Guid userId, bool wasHighValue); - Task GetUsageStatsAsync(Guid userId); -} - -public class UsageTrackingService : IUsageTrackingService -{ - private readonly DramaLingDbContext _context; - private readonly ILogger _logger; - - // 免費用戶限制 - private const int FREE_USER_ANALYSIS_LIMIT = 5; - private const int FREE_USER_RESET_HOURS = 3; - - public UsageTrackingService(DramaLingDbContext context, ILogger logger) - { - _context = context; - _logger = logger; - } - - /// - /// 檢查用戶使用限制 - /// - public async Task CheckUsageLimitAsync(Guid userId, bool isPremium = false) - { - try - { - if (isPremium) - { - return true; // 付費用戶無限制 - } - - var resetTime = DateTime.UtcNow.AddHours(-FREE_USER_RESET_HOURS); - var recentUsage = await _context.WordQueryUsageStats - .Where(stats => stats.UserId == userId && stats.CreatedAt >= resetTime) - .SumAsync(stats => stats.SentenceAnalysisCount + stats.LowValueWordClicks); - - var canUse = recentUsage < FREE_USER_ANALYSIS_LIMIT; - - _logger.LogInformation("Usage check for user {UserId}: {RecentUsage}/{Limit}, Can use: {CanUse}", - userId, recentUsage, FREE_USER_ANALYSIS_LIMIT, canUse); - - return canUse; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error checking usage limit for user {UserId}", userId); - return false; // 出錯時拒絕使用 - } - } - - /// - /// 記錄句子分析使用 - /// - public async Task RecordSentenceAnalysisAsync(Guid userId) - { - try - { - var today = DateOnly.FromDateTime(DateTime.Today); - var stats = await GetOrCreateTodayStatsAsync(userId, today); - - stats.SentenceAnalysisCount++; - stats.TotalApiCalls++; - stats.UpdatedAt = DateTime.UtcNow; - - await _context.SaveChangesAsync(); - - _logger.LogInformation("Recorded sentence analysis for user {UserId}, total today: {Count}", - userId, stats.SentenceAnalysisCount); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error recording sentence analysis for user {UserId}", userId); - } - } - - /// - /// 記錄單字查詢使用 - /// - public async Task RecordWordQueryAsync(Guid userId, bool wasHighValue) - { - try - { - var today = DateOnly.FromDateTime(DateTime.Today); - var stats = await GetOrCreateTodayStatsAsync(userId, today); - - if (wasHighValue) - { - stats.HighValueWordClicks++; - } - else - { - stats.LowValueWordClicks++; - stats.TotalApiCalls++; // 低價值詞彙需要API調用 - } - - stats.UniqueWordsQueried++; // 簡化:每次查詢都算一個獨特詞彙 - stats.UpdatedAt = DateTime.UtcNow; - - await _context.SaveChangesAsync(); - - _logger.LogInformation("Recorded word query for user {UserId}, high value: {IsHighValue}", - userId, wasHighValue); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error recording word query for user {UserId}", userId); - } - } - - /// - /// 獲取用戶使用統計 - /// - public async Task GetUsageStatsAsync(Guid userId) - { - try - { - var today = DateOnly.FromDateTime(DateTime.Today); - var resetTime = DateTime.UtcNow.AddHours(-FREE_USER_RESET_HOURS); - - // 今日統計 - var todayStats = await _context.WordQueryUsageStats - .FirstOrDefaultAsync(stats => stats.UserId == userId && stats.Date == today) - ?? new WordQueryUsageStats { UserId = userId, Date = today }; - - // 最近3小時使用量(用於限制檢查) - var recentUsage = await _context.WordQueryUsageStats - .Where(stats => stats.UserId == userId && stats.CreatedAt >= resetTime) - .SumAsync(stats => stats.SentenceAnalysisCount + stats.LowValueWordClicks); - - // 本週統計 - var weekStart = DateTime.Today.AddDays(-(int)DateTime.Today.DayOfWeek); - var weekStats = await _context.WordQueryUsageStats - .Where(stats => stats.UserId == userId && stats.Date >= DateOnly.FromDateTime(weekStart)) - .GroupBy(stats => 1) - .Select(g => new - { - TotalAnalysis = g.Sum(s => s.SentenceAnalysisCount), - TotalWordClicks = g.Sum(s => s.HighValueWordClicks + s.LowValueWordClicks), - TotalApiCalls = g.Sum(s => s.TotalApiCalls), - UniqueWords = g.Sum(s => s.UniqueWordsQueried) - }) - .FirstOrDefaultAsync(); - - return new UserUsageStats - { - UserId = userId, - Today = new DailyUsageStats - { - Date = today, - SentenceAnalysisCount = todayStats.SentenceAnalysisCount, - HighValueWordClicks = todayStats.HighValueWordClicks, - LowValueWordClicks = todayStats.LowValueWordClicks, - TotalApiCalls = todayStats.TotalApiCalls, - UniqueWordsQueried = todayStats.UniqueWordsQueried - }, - RecentUsage = new UsageLimitInfo - { - UsedInWindow = recentUsage, - WindowLimit = FREE_USER_ANALYSIS_LIMIT, - WindowHours = FREE_USER_RESET_HOURS, - ResetTime = DateTime.UtcNow.AddHours(FREE_USER_RESET_HOURS - - ((DateTime.UtcNow - resetTime).TotalHours % FREE_USER_RESET_HOURS)) - }, - ThisWeek = weekStats != null ? new WeeklyUsageStats - { - StartDate = DateOnly.FromDateTime(weekStart), - EndDate = DateOnly.FromDateTime(weekStart.AddDays(6)), - TotalSentenceAnalysis = weekStats.TotalAnalysis, - TotalWordClicks = weekStats.TotalWordClicks, - TotalApiCalls = weekStats.TotalApiCalls, - UniqueWordsQueried = weekStats.UniqueWords - } : new WeeklyUsageStats() - }; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting usage stats for user {UserId}", userId); - return new UserUsageStats { UserId = userId }; - } - } - - /// - /// 獲取或創建今日統計記錄 - /// - private async Task GetOrCreateTodayStatsAsync(Guid userId, DateOnly date) - { - var stats = await _context.WordQueryUsageStats - .FirstOrDefaultAsync(s => s.UserId == userId && s.Date == date); - - if (stats == null) - { - stats = new WordQueryUsageStats - { - Id = Guid.NewGuid(), - UserId = userId, - Date = date, - CreatedAt = DateTime.UtcNow, - UpdatedAt = DateTime.UtcNow - }; - - _context.WordQueryUsageStats.Add(stats); - } - - return stats; - } -} - -// 回應用的 DTO 類別 -public class UserUsageStats -{ - public Guid UserId { get; set; } - public DailyUsageStats Today { get; set; } = new(); - public UsageLimitInfo RecentUsage { get; set; } = new(); - public WeeklyUsageStats ThisWeek { get; set; } = new(); -} - -public class DailyUsageStats -{ - public DateOnly Date { get; set; } - public int SentenceAnalysisCount { get; set; } - public int HighValueWordClicks { get; set; } - public int LowValueWordClicks { get; set; } - public int TotalApiCalls { get; set; } - public int UniqueWordsQueried { get; set; } -} - -public class UsageLimitInfo -{ - public int UsedInWindow { get; set; } - public int WindowLimit { get; set; } - public int WindowHours { get; set; } - public DateTime ResetTime { get; set; } - public bool CanUse => UsedInWindow < WindowLimit; - public int Remaining => Math.Max(0, WindowLimit - UsedInWindow); -} - -public class WeeklyUsageStats -{ - public DateOnly StartDate { get; set; } - public DateOnly EndDate { get; set; } - public int TotalSentenceAnalysis { get; set; } - public int TotalWordClicks { get; set; } - public int TotalApiCalls { get; set; } - public int UniqueWordsQueried { get; set; } -} \ No newline at end of file diff --git a/backend/DramaLing.Api/Services/Media/Audio/AudioCacheService.cs b/backend/DramaLing.Api/Services/Media/Audio/AudioCacheService.cs deleted file mode 100644 index 6e7ceee..0000000 --- a/backend/DramaLing.Api/Services/Media/Audio/AudioCacheService.cs +++ /dev/null @@ -1,147 +0,0 @@ -using System.Security.Cryptography; -using System.Text; -using Microsoft.EntityFrameworkCore; -using DramaLing.Api.Data; -using DramaLing.Api.Models.Entities; -using DramaLing.Api.Models.Dtos; - -namespace DramaLing.Api.Services; - -public interface IAudioCacheService -{ - Task GetOrCreateAudioAsync(TTSRequest request); - Task GenerateCacheKeyAsync(string text, string accent, string voice); - Task UpdateAccessTimeAsync(string cacheKey); - Task CleanupOldCacheAsync(); -} - -public class AudioCacheService : IAudioCacheService -{ - private readonly DramaLingDbContext _context; - private readonly IAzureSpeechService _speechService; - private readonly ILogger _logger; - - public AudioCacheService( - DramaLingDbContext context, - IAzureSpeechService speechService, - ILogger logger) - { - _context = context; - _speechService = speechService; - _logger = logger; - } - - public async Task GetOrCreateAudioAsync(TTSRequest request) - { - try - { - var cacheKey = await GenerateCacheKeyAsync(request.Text, request.Accent, request.Voice); - - // 檢查快取 - var cachedAudio = await _context.AudioCaches - .FirstOrDefaultAsync(a => a.TextHash == cacheKey); - - if (cachedAudio != null) - { - // 更新訪問時間 - await UpdateAccessTimeAsync(cacheKey); - - return new TTSResponse - { - AudioUrl = cachedAudio.AudioUrl, - Duration = cachedAudio.DurationMs.HasValue ? cachedAudio.DurationMs.Value / 1000.0f : 0, - CacheHit = true - }; - } - - // 生成新音頻 - var response = await _speechService.GenerateAudioAsync(request); - - if (!string.IsNullOrEmpty(response.Error)) - { - return response; - } - - // 存入快取 - var audioCache = new AudioCache - { - TextHash = cacheKey, - TextContent = request.Text, - Accent = request.Accent, - VoiceId = request.Voice, - AudioUrl = response.AudioUrl, - DurationMs = (int)(response.Duration * 1000), - CreatedAt = DateTime.UtcNow, - LastAccessed = DateTime.UtcNow, - AccessCount = 1 - }; - - _context.AudioCaches.Add(audioCache); - await _context.SaveChangesAsync(); - - _logger.LogInformation("Created new audio cache entry for text: {Text}", request.Text); - - return response; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error in GetOrCreateAudioAsync for text: {Text}", request.Text); - return new TTSResponse - { - Error = "Internal error processing audio request" - }; - } - } - - public async Task GenerateCacheKeyAsync(string text, string accent, string voice) - { - var combined = $"{text}|{accent}|{voice}"; - using var sha256 = SHA256.Create(); - var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(combined)); - return Convert.ToHexString(hash).ToLowerInvariant(); - } - - public async Task UpdateAccessTimeAsync(string cacheKey) - { - try - { - var audioCache = await _context.AudioCaches - .FirstOrDefaultAsync(a => a.TextHash == cacheKey); - - if (audioCache != null) - { - audioCache.LastAccessed = DateTime.UtcNow; - audioCache.AccessCount++; - await _context.SaveChangesAsync(); - } - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to update access time for cache key: {CacheKey}", cacheKey); - } - } - - public async Task CleanupOldCacheAsync() - { - try - { - var cutoffDate = DateTime.UtcNow.AddDays(-30); - - var oldEntries = await _context.AudioCaches - .Where(a => a.LastAccessed < cutoffDate) - .ToListAsync(); - - if (oldEntries.Any()) - { - _context.AudioCaches.RemoveRange(oldEntries); - await _context.SaveChangesAsync(); - - _logger.LogInformation("Cleaned up {Count} old audio cache entries", oldEntries.Count); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error during audio cache cleanup"); - } - } -} \ No newline at end of file diff --git a/backend/DramaLing.Api/Services/Media/Audio/AzureSpeechService.cs b/backend/DramaLing.Api/Services/Media/Audio/AzureSpeechService.cs deleted file mode 100644 index bf85b24..0000000 --- a/backend/DramaLing.Api/Services/Media/Audio/AzureSpeechService.cs +++ /dev/null @@ -1,191 +0,0 @@ -using DramaLing.Api.Models.Dtos; -using System.Text; -using System.Security.Cryptography; - -namespace DramaLing.Api.Services; - -public interface IAzureSpeechService -{ - Task GenerateAudioAsync(TTSRequest request); - Task EvaluatePronunciationAsync(Stream audioStream, PronunciationRequest request); -} - -public class AzureSpeechService : IAzureSpeechService -{ - private readonly IConfiguration _configuration; - private readonly ILogger _logger; - private readonly bool _isConfigured; - - public AzureSpeechService(IConfiguration configuration, ILogger logger) - { - _configuration = configuration; - _logger = logger; - - var subscriptionKey = _configuration["Azure:Speech:SubscriptionKey"]; - var region = _configuration["Azure:Speech:Region"]; - - if (string.IsNullOrEmpty(subscriptionKey) || string.IsNullOrEmpty(region)) - { - _logger.LogWarning("Azure Speech configuration is missing. TTS functionality will be disabled."); - _isConfigured = false; - return; - } - - _isConfigured = true; - _logger.LogInformation("Azure Speech service configured for region: {Region}", region); - } - - public async Task GenerateAudioAsync(TTSRequest request) - { - try - { - if (!_isConfigured) - { - return new TTSResponse - { - Error = "Azure Speech service is not configured" - }; - } - - // 模擬 TTS 處理,返回模擬數據 - await Task.Delay(500); // 模擬 API 延遲 - - // 生成模擬的 base64 音頻數據 (實際上是空的 MP3 標頭) - var mockAudioData = Convert.ToBase64String(new byte[] { - 0xFF, 0xFB, 0x90, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 - }); - var audioUrl = $"data:audio/mp3;base64,{mockAudioData}"; - - return new TTSResponse - { - AudioUrl = audioUrl, - Duration = CalculateAudioDuration(request.Text.Length), - CacheHit = false - }; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error generating audio for text: {Text}", request.Text); - return new TTSResponse - { - Error = "Internal error generating audio" - }; - } - } - - public async Task EvaluatePronunciationAsync(Stream audioStream, PronunciationRequest request) - { - try - { - if (!_isConfigured) - { - return new PronunciationResponse - { - Error = "Azure Speech service is not configured" - }; - } - - // 模擬語音評估處理 - await Task.Delay(2000); // 模擬 API 調用延遲 - - // 生成模擬的評分數據 - var random = new Random(); - var overallScore = random.Next(75, 95); - - return new PronunciationResponse - { - OverallScore = overallScore, - Accuracy = (float)(random.NextDouble() * 20 + 75), - Fluency = (float)(random.NextDouble() * 20 + 75), - Completeness = (float)(random.NextDouble() * 20 + 75), - Prosody = (float)(random.NextDouble() * 20 + 75), - PhonemeScores = GenerateMockPhonemeScores(request.TargetText), - Suggestions = GenerateMockSuggestions(overallScore) - }; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error evaluating pronunciation for text: {Text}", request.TargetText); - return new PronunciationResponse - { - Error = "Internal error evaluating pronunciation" - }; - } - } - - private List GenerateMockPhonemeScores(string text) - { - var phonemes = new List(); - var words = text.Split(' ', StringSplitOptions.RemoveEmptyEntries); - - foreach (var word in words.Take(3)) // 只處理前3個詞 - { - phonemes.Add(new PhonemeScore - { - Phoneme = $"/{word[0]}/", - Score = Random.Shared.Next(70, 95), - Suggestion = Random.Shared.Next(0, 3) == 0 ? $"注意 {word} 的發音" : null - }); - } - - return phonemes; - } - - private List GenerateMockSuggestions(int overallScore) - { - var suggestions = new List(); - - if (overallScore < 85) - { - suggestions.Add("注意單詞的重音位置"); - } - - if (overallScore < 80) - { - suggestions.Add("發音可以更清晰一些"); - suggestions.Add("嘗試放慢語速,確保每個音都發準"); - } - - if (overallScore >= 90) - { - suggestions.Add("發音很棒!繼續保持"); - } - - return suggestions; - } - - private string GetVoiceName(string accent, string voicePreference) - { - return accent.ToLower() switch - { - "uk" => "en-GB-SoniaNeural", - "us" => "en-US-AriaNeural", - _ => "en-US-AriaNeural" - }; - } - - private string CreateSSML(string text, string voice, float speed) - { - var rate = speed switch - { - < 0.8f => "slow", - > 1.2f => "fast", - _ => "medium" - }; - - return $@" - - - - {text} - - - "; - } - - private float CalculateAudioDuration(int textLength) - { - // 根據文字長度估算音頻時長:平均每個字符 0.1 秒 - return Math.Max(1.0f, textLength * 0.1f); - } -} \ No newline at end of file