refactor: 清理未使用的後端服務並建立審計報告
- 移除4個未使用的服務檔案: • IGeminiAnalyzer.cs - 未實作的介面 • AudioCacheService.cs - 未使用的音頻快取服務 • AzureSpeechService.cs - 未使用的語音服務 • UsageTrackingService.cs - 未使用的使用量追蹤服務 - 移除相關的 DI 容器註冊 - 移除空的 Services/Media/Audio/ 目錄 - 新增完整的後端服務審計報告文件 - 保留核心功能服務的所有依賴關係 編譯測試通過,功能完整保留,程式碼減少約500+行 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
ad63b8fed8
commit
da78d04b8b
|
|
@ -136,9 +136,6 @@ public static class ServiceCollectionExtensions
|
|||
public static IServiceCollection AddBusinessServices(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<IAuthService, AuthService>();
|
||||
services.AddScoped<IUsageTrackingService, UsageTrackingService>();
|
||||
services.AddScoped<IAzureSpeechService, AzureSpeechService>();
|
||||
services.AddScoped<IAudioCacheService, AudioCacheService>();
|
||||
|
||||
// 媒體服務
|
||||
services.AddScoped<IImageProcessingService, ImageProcessingService>();
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
using DramaLing.Api.Models.DTOs;
|
||||
|
||||
namespace DramaLing.Api.Services.AI.Analysis;
|
||||
|
||||
public interface IGeminiAnalyzer
|
||||
{
|
||||
Task<SentenceAnalysisData> AnalyzeSentenceAsync(string inputText, AnalysisOptions options);
|
||||
}
|
||||
|
|
@ -1,255 +0,0 @@
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using DramaLing.Api.Data;
|
||||
using DramaLing.Api.Models.Entities;
|
||||
|
||||
namespace DramaLing.Api.Services;
|
||||
|
||||
public interface IUsageTrackingService
|
||||
{
|
||||
Task<bool> CheckUsageLimitAsync(Guid userId, bool isPremium = false);
|
||||
Task RecordSentenceAnalysisAsync(Guid userId);
|
||||
Task RecordWordQueryAsync(Guid userId, bool wasHighValue);
|
||||
Task<UserUsageStats> GetUsageStatsAsync(Guid userId);
|
||||
}
|
||||
|
||||
public class UsageTrackingService : IUsageTrackingService
|
||||
{
|
||||
private readonly DramaLingDbContext _context;
|
||||
private readonly ILogger<UsageTrackingService> _logger;
|
||||
|
||||
// 免費用戶限制
|
||||
private const int FREE_USER_ANALYSIS_LIMIT = 5;
|
||||
private const int FREE_USER_RESET_HOURS = 3;
|
||||
|
||||
public UsageTrackingService(DramaLingDbContext context, ILogger<UsageTrackingService> logger)
|
||||
{
|
||||
_context = context;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 檢查用戶使用限制
|
||||
/// </summary>
|
||||
public async Task<bool> 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; // 出錯時拒絕使用
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 記錄句子分析使用
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 記錄單字查詢使用
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 獲取用戶使用統計
|
||||
/// </summary>
|
||||
public async Task<UserUsageStats> 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 };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 獲取或創建今日統計記錄
|
||||
/// </summary>
|
||||
private async Task<WordQueryUsageStats> 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; }
|
||||
}
|
||||
|
|
@ -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<TTSResponse> GetOrCreateAudioAsync(TTSRequest request);
|
||||
Task<string> 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<AudioCacheService> _logger;
|
||||
|
||||
public AudioCacheService(
|
||||
DramaLingDbContext context,
|
||||
IAzureSpeechService speechService,
|
||||
ILogger<AudioCacheService> logger)
|
||||
{
|
||||
_context = context;
|
||||
_speechService = speechService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<TTSResponse> 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<string> 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,191 +0,0 @@
|
|||
using DramaLing.Api.Models.Dtos;
|
||||
using System.Text;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace DramaLing.Api.Services;
|
||||
|
||||
public interface IAzureSpeechService
|
||||
{
|
||||
Task<TTSResponse> GenerateAudioAsync(TTSRequest request);
|
||||
Task<PronunciationResponse> EvaluatePronunciationAsync(Stream audioStream, PronunciationRequest request);
|
||||
}
|
||||
|
||||
public class AzureSpeechService : IAzureSpeechService
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly ILogger<AzureSpeechService> _logger;
|
||||
private readonly bool _isConfigured;
|
||||
|
||||
public AzureSpeechService(IConfiguration configuration, ILogger<AzureSpeechService> 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<TTSResponse> 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<PronunciationResponse> 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<PhonemeScore> GenerateMockPhonemeScores(string text)
|
||||
{
|
||||
var phonemes = new List<PhonemeScore>();
|
||||
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<string> GenerateMockSuggestions(int overallScore)
|
||||
{
|
||||
var suggestions = new List<string>();
|
||||
|
||||
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 $@"
|
||||
<speak version='1.0' xmlns='http://www.w3.org/2001/10/synthesis' xml:lang='en-US'>
|
||||
<voice name='{voice}'>
|
||||
<prosody rate='{rate}'>
|
||||
{text}
|
||||
</prosody>
|
||||
</voice>
|
||||
</speak>";
|
||||
}
|
||||
|
||||
private float CalculateAudioDuration(int textLength)
|
||||
{
|
||||
// 根據文字長度估算音頻時長:平均每個字符 0.1 秒
|
||||
return Math.Max(1.0f, textLength * 0.1f);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue