import os
|
import re
|
import yaml
|
import time
|
import hashlib
|
import portalocker
|
from typing import Dict
|
|
|
class FileLock:
|
def __init__(self, file, timeout=5):
|
self.file = file
|
self.timeout = timeout
|
self.start_time = None
|
|
def __enter__(self):
|
self.start_time = time.time()
|
while True:
|
try:
|
portalocker.lock(self.file, portalocker.LOCK_EX | portalocker.LOCK_NB)
|
return self.file
|
except portalocker.LockException:
|
if time.time() - self.start_time > self.timeout:
|
raise TimeoutError("获取文件锁超时")
|
time.sleep(0.1)
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
portalocker.unlock(self.file)
|
|
|
class WakeupWordsConfig:
|
def __init__(self):
|
self.config_file = "data/.wakeup_words.yaml"
|
self.assets_dir = "config/assets/wakeup_words"
|
self._ensure_directories()
|
self._config_cache = None
|
self._last_load_time = 0
|
self._cache_ttl = 1 # 缓存有效期(秒)
|
self._lock_timeout = 5 # 文件锁超时时间(秒)
|
|
def _ensure_directories(self):
|
"""确保必要的目录存在"""
|
os.makedirs(os.path.dirname(self.config_file), exist_ok=True)
|
os.makedirs(self.assets_dir, exist_ok=True)
|
|
def _load_config(self) -> Dict:
|
"""加载配置文件,使用缓存机制"""
|
current_time = time.time()
|
|
# 如果缓存有效,直接返回缓存
|
if (
|
self._config_cache is not None
|
and current_time - self._last_load_time < self._cache_ttl
|
):
|
return self._config_cache
|
|
try:
|
with open(self.config_file, "a+", encoding="utf-8") as f:
|
with FileLock(f, timeout=self._lock_timeout):
|
f.seek(0)
|
content = f.read()
|
config = yaml.safe_load(content) if content else {}
|
self._config_cache = config
|
self._last_load_time = current_time
|
return config
|
except (TimeoutError, IOError) as e:
|
print(f"加载配置文件失败: {e}")
|
return {}
|
except Exception as e:
|
print(f"加载配置文件时发生未知错误: {e}")
|
return {}
|
|
def _save_config(self, config: Dict):
|
"""保存配置到文件,使用文件锁保护"""
|
try:
|
with open(self.config_file, "w", encoding="utf-8") as f:
|
with FileLock(f, timeout=self._lock_timeout):
|
yaml.dump(config, f, allow_unicode=True)
|
self._config_cache = config
|
self._last_load_time = time.time()
|
except (TimeoutError, IOError) as e:
|
print(f"保存配置文件失败: {e}")
|
raise
|
except Exception as e:
|
print(f"保存配置文件时发生未知错误: {e}")
|
raise
|
|
def get_wakeup_response(self, voice: str) -> Dict:
|
voice = hashlib.md5(voice.encode()).hexdigest()
|
"""获取唤醒词回复配置"""
|
config = self._load_config()
|
|
if not config or voice not in config:
|
return None
|
|
# 检查文件大小
|
file_path = config[voice]["file_path"]
|
if not os.path.exists(file_path) or os.stat(file_path).st_size < (15 * 1024):
|
return None
|
|
return config[voice]
|
|
def update_wakeup_response(self, voice: str, file_path: str, text: str):
|
"""更新唤醒词回复配置"""
|
try:
|
# 过滤表情符号
|
filtered_text = re.sub(r'[\U0001F600-\U0001F64F\U0001F900-\U0001F9FF]', '', text)
|
|
config = self._load_config()
|
voice_hash = hashlib.md5(voice.encode()).hexdigest()
|
config[voice_hash] = {
|
"voice": voice,
|
"file_path": file_path,
|
"time": time.time(),
|
"text": filtered_text,
|
}
|
self._save_config(config)
|
except Exception as e:
|
print(f"更新唤醒词回复配置失败: {e}")
|
raise
|
|
def generate_file_path(self, voice: str) -> str:
|
"""生成音频文件路径,使用voice的哈希值作为文件名"""
|
try:
|
# 生成voice的哈希值
|
voice_hash = hashlib.md5(voice.encode()).hexdigest()
|
file_path = os.path.join(self.assets_dir, f"{voice_hash}.wav")
|
|
# 如果文件已存在,先删除
|
if os.path.exists(file_path):
|
try:
|
os.remove(file_path)
|
except Exception as e:
|
print(f"删除已存在的音频文件失败: {e}")
|
raise
|
|
return file_path
|
except Exception as e:
|
print(f"生成音频文件路径失败: {e}")
|
raise
|