226 lines
7.9 KiB
Python
226 lines
7.9 KiB
Python
"""
|
||
翻译缓存管理模块 - 简化版
|
||
基于全局ID和chunk的缓存系统
|
||
"""
|
||
|
||
import json
|
||
import hashlib
|
||
from pathlib import Path
|
||
from datetime import datetime, timedelta
|
||
from typing import Dict, Optional, List
|
||
from loguru import logger
|
||
|
||
|
||
class TranslationCache:
|
||
"""翻译缓存管理器 - 简化版"""
|
||
|
||
def __init__(self, config: Dict):
|
||
"""初始化缓存管理器"""
|
||
self.config = config
|
||
cache_config = config.get('cache', {})
|
||
|
||
self.enabled = cache_config.get('enabled', True)
|
||
self.cache_dir = Path(cache_config.get('directory', 'cache'))
|
||
self.max_age_days = cache_config.get('max_age_days', 30)
|
||
|
||
if self.enabled:
|
||
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
||
self.translations_dir = self.cache_dir / 'translations'
|
||
self.translations_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
logger.info(f"翻译缓存已启用: {self.cache_dir}")
|
||
|
||
def get_chunk_translation(self, chunk: List[Dict], model: str) -> Optional[Dict[str, str]]:
|
||
"""
|
||
获取chunk的缓存翻译
|
||
|
||
Args:
|
||
chunk: 段落列表(带global_id)
|
||
model: 模型名称
|
||
|
||
Returns:
|
||
{global_id: translation} 映射,如果不存在返回 None
|
||
"""
|
||
if not self.enabled:
|
||
return None
|
||
|
||
try:
|
||
cache_key = self._get_chunk_cache_key(chunk, model)
|
||
cache_file = self._get_cache_file_path(cache_key)
|
||
|
||
if not cache_file.exists():
|
||
return None
|
||
|
||
# 检查是否过期
|
||
file_age = datetime.now() - datetime.fromtimestamp(cache_file.stat().st_mtime)
|
||
if file_age > timedelta(days=self.max_age_days):
|
||
logger.debug(f"缓存已过期: {cache_key[:8]}...")
|
||
cache_file.unlink()
|
||
return None
|
||
|
||
# 读取缓存
|
||
with open(cache_file, 'r', encoding='utf-8') as f:
|
||
cache_data = json.load(f)
|
||
|
||
# 验证缓存
|
||
if (cache_data.get('success') and
|
||
cache_data.get('model') == model and
|
||
self._validate_cache_data(cache_data, chunk)):
|
||
|
||
logger.debug(f"缓存命中: {cache_key[:8]}... ({len(chunk)} 段落)")
|
||
return cache_data.get('translations', {})
|
||
|
||
return None
|
||
|
||
except Exception as e:
|
||
logger.warning(f"读取缓存失败: {e}")
|
||
return None
|
||
|
||
def save_chunk_translation(self, chunk: List[Dict], translations: Dict[str, str],
|
||
model: str, success: bool = True) -> None:
|
||
"""
|
||
保存chunk翻译到缓存
|
||
|
||
Args:
|
||
chunk: 段落列表(带global_id)
|
||
translations: {global_id: translation} 映射
|
||
model: 模型名称
|
||
success: 是否翻译成功
|
||
"""
|
||
if not self.enabled:
|
||
return
|
||
|
||
try:
|
||
cache_key = self._get_chunk_cache_key(chunk, model)
|
||
cache_file = self._get_cache_file_path(cache_key)
|
||
|
||
# 构建缓存数据
|
||
cache_data = {
|
||
'global_ids': [p['global_id'] for p in chunk],
|
||
'translations': translations,
|
||
'model': model,
|
||
'timestamp': datetime.now().isoformat(),
|
||
'success': success,
|
||
'paragraph_count': len(chunk),
|
||
'cache_version': '3.0'
|
||
}
|
||
|
||
with open(cache_file, 'w', encoding='utf-8') as f:
|
||
json.dump(cache_data, f, ensure_ascii=False, indent=2)
|
||
|
||
logger.debug(f"缓存已保存: {cache_key[:8]}... ({len(chunk)} 段落)")
|
||
|
||
except Exception as e:
|
||
logger.warning(f"保存缓存失败: {e}")
|
||
|
||
def _get_chunk_cache_key(self, chunk: List[Dict], model: str) -> str:
|
||
"""
|
||
生成chunk缓存键(基于全局ID序列)
|
||
|
||
Args:
|
||
chunk: 段落列表
|
||
model: 模型名称
|
||
|
||
Returns:
|
||
缓存键
|
||
"""
|
||
# 使用全局ID序列作为缓存键的一部分
|
||
id_sequence = ",".join(p['global_id'] for p in chunk)
|
||
combined = f"{id_sequence}|{model}"
|
||
return hashlib.md5(combined.encode('utf-8')).hexdigest()
|
||
|
||
def _get_cache_file_path(self, cache_key: str) -> Path:
|
||
"""获取缓存文件路径"""
|
||
today = datetime.now().strftime('%Y-%m-%d')
|
||
cache_date_dir = self.translations_dir / today
|
||
cache_date_dir.mkdir(parents=True, exist_ok=True)
|
||
return cache_date_dir / f"{cache_key}.json"
|
||
|
||
def _validate_cache_data(self, cache_data: Dict, chunk: List[Dict]) -> bool:
|
||
"""验证缓存数据的有效性"""
|
||
# 检查ID序列是否匹配
|
||
cached_ids = cache_data.get('global_ids', [])
|
||
chunk_ids = [p['global_id'] for p in chunk]
|
||
|
||
if cached_ids != chunk_ids:
|
||
logger.debug("缓存ID序列不匹配")
|
||
return False
|
||
|
||
# 检查翻译数量
|
||
translations = cache_data.get('translations', {})
|
||
if len(translations) != len(chunk):
|
||
logger.debug("缓存翻译数量不匹配")
|
||
return False
|
||
|
||
return True
|
||
|
||
def clear_cache(self, older_than_days: Optional[int] = None) -> int:
|
||
"""清理缓存"""
|
||
if not self.enabled or not self.translations_dir.exists():
|
||
return 0
|
||
|
||
cleared_count = 0
|
||
cutoff_time = None
|
||
|
||
if older_than_days is not None:
|
||
cutoff_time = datetime.now() - timedelta(days=older_than_days)
|
||
|
||
try:
|
||
for cache_file in self.translations_dir.rglob('*.json'):
|
||
should_delete = False
|
||
|
||
if cutoff_time is None:
|
||
should_delete = True
|
||
else:
|
||
file_time = datetime.fromtimestamp(cache_file.stat().st_mtime)
|
||
should_delete = file_time < cutoff_time
|
||
|
||
if should_delete:
|
||
cache_file.unlink()
|
||
cleared_count += 1
|
||
|
||
# 清理空目录
|
||
for date_dir in self.translations_dir.iterdir():
|
||
if date_dir.is_dir() and not any(date_dir.iterdir()):
|
||
date_dir.rmdir()
|
||
|
||
logger.info(f"清理了 {cleared_count} 个缓存文件")
|
||
return cleared_count
|
||
|
||
except Exception as e:
|
||
logger.error(f"清理缓存失败: {e}")
|
||
return 0
|
||
|
||
def get_cache_stats(self) -> Dict:
|
||
"""获取缓存统计信息"""
|
||
if not self.enabled or not self.translations_dir.exists():
|
||
return {'enabled': False}
|
||
|
||
try:
|
||
cache_files = list(self.translations_dir.rglob('*.json'))
|
||
total_files = len(cache_files)
|
||
total_size = sum(f.stat().st_size for f in cache_files)
|
||
|
||
# 统计段落数
|
||
total_paragraphs = 0
|
||
for cache_file in cache_files:
|
||
try:
|
||
with open(cache_file, 'r', encoding='utf-8') as f:
|
||
data = json.load(f)
|
||
total_paragraphs += data.get('paragraph_count', 0)
|
||
except:
|
||
continue
|
||
|
||
return {
|
||
'enabled': True,
|
||
'total_files': total_files,
|
||
'total_paragraphs': total_paragraphs,
|
||
'total_size_mb': round(total_size / 1024 / 1024, 2),
|
||
'cache_directory': str(self.cache_dir),
|
||
'max_age_days': self.max_age_days
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"获取缓存统计失败: {e}")
|
||
return {'enabled': True, 'error': str(e)}
|