dramaling-vocab-learning/backend/DramaLing.Api/Services/Media/Audio/AudioCacheService.cs

147 lines
4.7 KiB
C#

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");
}
}
}