mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-22 15:13:55 +08:00
update:优化唤醒词答复
This commit is contained in:
@@ -12,19 +12,21 @@ from core.handle.mcpHandle import (
|
||||
send_mcp_initialize_message,
|
||||
send_mcp_tools_list_request,
|
||||
)
|
||||
|
||||
from core.utils.wakeup_word import WakeupWordsConfig
|
||||
|
||||
TAG = __name__
|
||||
|
||||
WAKEUP_CONFIG = {
|
||||
"dir": "config/assets/",
|
||||
"file_name": "wakeup_words",
|
||||
"create_time": time.time(),
|
||||
"refresh_time": 10,
|
||||
"refresh_time": 5,
|
||||
"words": ["你好小智", "你好啊小智", "小智你好", "小智"],
|
||||
"text": "",
|
||||
}
|
||||
|
||||
# 创建全局的唤醒词配置管理器
|
||||
wakeup_words_config = WakeupWordsConfig()
|
||||
|
||||
# 用于防止并发调用wakeupWordsResponse的锁
|
||||
_wakeup_response_lock = asyncio.Lock()
|
||||
|
||||
|
||||
async def handleHelloMessage(conn, msg_json):
|
||||
"""处理hello消息"""
|
||||
@@ -55,96 +57,78 @@ async def checkWakeupWords(conn, text):
|
||||
enable_wakeup_words_response_cache = conn.config[
|
||||
"enable_wakeup_words_response_cache"
|
||||
]
|
||||
"""是否用的是非流式tts"""
|
||||
if conn.tts and conn.tts.interface_type != InterfaceType.NON_STREAM:
|
||||
|
||||
if not enable_wakeup_words_response_cache or not conn.tts:
|
||||
return False
|
||||
|
||||
"""是否开启唤醒词加速"""
|
||||
if not enable_wakeup_words_response_cache:
|
||||
if conn.tts.interface_type != InterfaceType.NON_STREAM:
|
||||
return False
|
||||
"""检查是否是唤醒词"""
|
||||
|
||||
_, filtered_text = remove_punctuation_and_length(text)
|
||||
if filtered_text in conn.config.get("wakeup_words"):
|
||||
await send_stt_message(conn, text)
|
||||
if filtered_text not in conn.config.get("wakeup_words"):
|
||||
return False
|
||||
|
||||
file = getWakeupWordFile(WAKEUP_CONFIG["file_name"])
|
||||
if file is None:
|
||||
await send_stt_message(conn, text)
|
||||
|
||||
# 获取当前音色
|
||||
voice = getattr(conn.tts, "voice", "default")
|
||||
|
||||
# 获取唤醒词回复配置
|
||||
response = wakeup_words_config.get_wakeup_response(voice)
|
||||
|
||||
# 播放唤醒词回复
|
||||
conn.client_abort = False
|
||||
conn.tts.tts_one_sentence(
|
||||
conn,
|
||||
ContentType.FILE,
|
||||
content_file=response["file_path"],
|
||||
content_detail=response["text"],
|
||||
)
|
||||
|
||||
# 检查是否需要更新唤醒词回复
|
||||
if time.time() - response["time"] > WAKEUP_CONFIG["refresh_time"]:
|
||||
if not _wakeup_response_lock.locked():
|
||||
asyncio.create_task(wakeupWordsResponse(conn))
|
||||
return False
|
||||
text_hello = WAKEUP_CONFIG["text"]
|
||||
if not text_hello:
|
||||
text_hello = text
|
||||
if conn.tts is None:
|
||||
return False
|
||||
conn.tts.tts_one_sentence(
|
||||
conn, ContentType.FILE, content_file=file, content_detail=text_hello
|
||||
)
|
||||
if time.time() - WAKEUP_CONFIG["create_time"] > WAKEUP_CONFIG["refresh_time"]:
|
||||
asyncio.create_task(wakeupWordsResponse(conn))
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def getWakeupWordFile(file_name):
|
||||
for file in os.listdir(WAKEUP_CONFIG["dir"]):
|
||||
if file.startswith("my_" + file_name):
|
||||
"""避免缓存文件是一个空文件"""
|
||||
if os.stat(f"config/assets/{file}").st_size > (15 * 1024):
|
||||
return f"config/assets/{file}"
|
||||
|
||||
"""查找config/assets/目录下名称为wakeup_words的文件"""
|
||||
for file in os.listdir(WAKEUP_CONFIG["dir"]):
|
||||
if file.startswith(file_name):
|
||||
return f"config/assets/{file}"
|
||||
return None
|
||||
return True
|
||||
|
||||
|
||||
async def wakeupWordsResponse(conn):
|
||||
wait_max_time = 5
|
||||
while conn.llm is None or not conn.llm.response_no_stream:
|
||||
await asyncio.sleep(1)
|
||||
wait_max_time -= 1
|
||||
if wait_max_time <= 0:
|
||||
conn.logger.bind(tag=TAG).error("连接对象没有llm")
|
||||
return
|
||||
|
||||
"""唤醒词响应"""
|
||||
wakeup_word = random.choice(WAKEUP_CONFIG["words"])
|
||||
question = (
|
||||
"此刻用户正在和你说```"
|
||||
+ wakeup_word
|
||||
+ "```。\n请你根据以上用户的内容,进行简短回复,文字内容控制在15个字以内。\n"
|
||||
+ "请勿对这条内容本身进行任何解释和回应,仅返回对用户的内容的回复。"
|
||||
)
|
||||
result = conn.llm.response_no_stream(conn.config["prompt"], question)
|
||||
if result is None or result == "":
|
||||
if not conn.tts or not conn.llm or not conn.llm.response_no_stream:
|
||||
return
|
||||
|
||||
tts_result = await asyncio.to_thread(conn.tts.to_tts, result)
|
||||
if conn.tts.delete_audio_file:
|
||||
# 返回的是opus数据流
|
||||
try:
|
||||
# 尝试获取锁,如果获取不到就返回
|
||||
if not await _wakeup_response_lock.acquire():
|
||||
return
|
||||
|
||||
# 生成唤醒词回复
|
||||
wakeup_word = random.choice(WAKEUP_CONFIG["words"])
|
||||
question = (
|
||||
"此刻用户正在和你说```"
|
||||
+ wakeup_word
|
||||
+ "```。\n请你根据以上用户的内容进行简短回复。要像一个人正常人一样说话,不要像机器人一样说话。\n"
|
||||
+ "请勿对这条内容本身进行任何解释和回应,请勿返回表情符号,仅返回对用户的内容的回复。"
|
||||
)
|
||||
|
||||
result = conn.llm.response_no_stream(conn.config["prompt"], question)
|
||||
if not result or len(result) == 0:
|
||||
return
|
||||
|
||||
# 生成TTS音频
|
||||
tts_result = await asyncio.to_thread(conn.tts.to_tts, result)
|
||||
if not tts_result:
|
||||
return
|
||||
|
||||
# 获取当前音色
|
||||
voice = getattr(conn.tts, "voice", "default")
|
||||
|
||||
wav_bytes = opus_datas_to_wav_bytes(tts_result, sample_rate=16000)
|
||||
file_path = os.path.join(
|
||||
WAKEUP_CONFIG["dir"], "my_" + WAKEUP_CONFIG["file_name"] + ".wav"
|
||||
)
|
||||
file_path = wakeup_words_config.generate_file_path(voice)
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(wav_bytes)
|
||||
|
||||
else:
|
||||
# 返回为文件
|
||||
if tts_result is not None and os.path.exists(tts_result):
|
||||
file_type = os.path.splitext(tts_result)[1]
|
||||
if file_type:
|
||||
file_type = file_type.lstrip(".")
|
||||
old_file = getWakeupWordFile("my_" + WAKEUP_CONFIG["file_name"])
|
||||
if old_file is not None:
|
||||
os.remove(old_file)
|
||||
"""将文件挪到wakeup_words目录下"""
|
||||
shutil.move(
|
||||
tts_result,
|
||||
WAKEUP_CONFIG["dir"] + "my_" + WAKEUP_CONFIG["file_name"] + '.' + file_type,
|
||||
)
|
||||
WAKEUP_CONFIG["create_time"] = time.time()
|
||||
WAKEUP_CONFIG["text"] = result
|
||||
# 更新配置
|
||||
wakeup_words_config.update_wakeup_response(voice, file_path, result)
|
||||
finally:
|
||||
# 确保在任何情况下都释放锁
|
||||
if _wakeup_response_lock.locked():
|
||||
_wakeup_response_lock.release()
|
||||
|
||||
@@ -145,10 +145,9 @@ class TTSProvider(TTSProviderBase):
|
||||
self.cluster = config.get("cluster")
|
||||
self.resource_id = config.get("resource_id")
|
||||
if config.get("private_voice"):
|
||||
self.speaker = config.get("private_voice")
|
||||
self.voice = config.get("private_voice")
|
||||
else:
|
||||
self.speaker = config.get("speaker")
|
||||
self.voice = config.get("voice")
|
||||
self.voice = config.get("speaker")
|
||||
self.ws_url = config.get("ws_url")
|
||||
self.authorization = config.get("authorization")
|
||||
self.header = {"Authorization": f"{self.authorization}{self.access_token}"}
|
||||
@@ -272,7 +271,7 @@ class TTSProvider(TTSProviderBase):
|
||||
logger.bind(tag=TAG).error(f"WebSocket连接不存在,终止发送文本")
|
||||
return
|
||||
# 发送文本
|
||||
await self.send_text(self.speaker, text, self.conn.sentence_id)
|
||||
await self.send_text(self.voice, text, self.conn.sentence_id)
|
||||
return
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"发送TTS文本失败: {str(e)}")
|
||||
@@ -302,7 +301,7 @@ class TTSProvider(TTSProviderBase):
|
||||
event=EVENT_StartSession, sessionId=session_id
|
||||
).as_bytes()
|
||||
payload = self.get_payload_bytes(
|
||||
event=EVENT_StartSession, speaker=self.speaker
|
||||
event=EVENT_StartSession, speaker=self.voice
|
||||
)
|
||||
await self.send_event(header, optional, payload)
|
||||
logger.bind(tag=TAG).info("会话启动请求已发送")
|
||||
|
||||
@@ -14,9 +14,9 @@ class TTSProvider(TTSProviderBase):
|
||||
self.api_key = config.get("api_key")
|
||||
self.model = config.get("model")
|
||||
if config.get("private_voice"):
|
||||
self.voice_id = config.get("private_voice")
|
||||
self.voice = config.get("private_voice")
|
||||
else:
|
||||
self.voice_id = config.get("voice_id")
|
||||
self.voice = config.get("voice_id")
|
||||
|
||||
default_voice_setting = {
|
||||
"voice_id": "female-shaonv",
|
||||
@@ -43,8 +43,8 @@ class TTSProvider(TTSProviderBase):
|
||||
self.audio_setting = {**defult_audio_setting, **config.get("audio_setting", {})}
|
||||
self.timber_weights = parse_string_to_list(config.get("timber_weights"))
|
||||
|
||||
if self.voice_id:
|
||||
self.voice_setting["voice_id"] = self.voice_id
|
||||
if self.voice:
|
||||
self.voice_setting["voice_id"] = self.voice
|
||||
|
||||
self.host = "api.minimax.chat"
|
||||
self.api_url = f"https://{self.host}/v1/t2a_v2?GroupId={self.group_id}"
|
||||
|
||||
@@ -19,9 +19,9 @@ class TTSProvider(TTSProviderBase):
|
||||
"https://u95167-bd74-2aef8085.westx.seetacloud.com:8443/flashsummary/tts?token=",
|
||||
)
|
||||
if config.get("private_voice"):
|
||||
self.voice_id = int(config.get("private_voice"))
|
||||
self.voice = int(config.get("private_voice"))
|
||||
else:
|
||||
self.voice_id = int(config.get("voice_id", 1695))
|
||||
self.voice = int(config.get("voice_id", 1695))
|
||||
self.token = config.get("token")
|
||||
self.to_lang = config.get("to_lang")
|
||||
self.volume_change_dB = int(config.get("volume_change_dB", 0))
|
||||
@@ -50,7 +50,7 @@ class TTSProvider(TTSProviderBase):
|
||||
"emotion": self.emotion,
|
||||
"format": self.format,
|
||||
"volume_change_dB": self.volume_change_dB,
|
||||
"voice_id": self.voice_id,
|
||||
"voice_id": self.voice,
|
||||
"pitch_factor": self.pitch_factor,
|
||||
"speed_factor": self.speed_factor,
|
||||
"token": self.token,
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
import os
|
||||
import yaml
|
||||
import time
|
||||
import hashlib
|
||||
import fcntl
|
||||
from typing import Dict
|
||||
from contextlib import contextmanager
|
||||
|
||||
|
||||
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:
|
||||
fcntl.flock(self.file, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
return self.file
|
||||
except IOError:
|
||||
if time.time() - self.start_time > self.timeout:
|
||||
raise TimeoutError("获取文件锁超时")
|
||||
time.sleep(0.1)
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
fcntl.flock(self.file, fcntl.LOCK_UN)
|
||||
|
||||
|
||||
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+") 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") 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()
|
||||
default_response = {
|
||||
"voice": "default",
|
||||
"file_path": "config/assets/wakeup_words.wav",
|
||||
"time": 0,
|
||||
"text": "哈啰啊,我是小智啦,声音好听的台湾女孩一枚,超开心认识你耶,最近在忙啥,别忘了给我来点有趣的料哦,我超爱听八卦的啦",
|
||||
}
|
||||
|
||||
if not config or voice not in config:
|
||||
return default_response
|
||||
|
||||
# 检查文件大小
|
||||
file_path = config[voice]["file_path"]
|
||||
if not os.path.exists(file_path) or os.stat(file_path).st_size < (15 * 1024):
|
||||
return default_response
|
||||
|
||||
return config[voice]
|
||||
|
||||
def update_wakeup_response(self, voice: str, file_path: str, text: str):
|
||||
"""更新唤醒词回复配置"""
|
||||
try:
|
||||
config = self._load_config()
|
||||
voice_hash = hashlib.md5(voice.encode()).hexdigest()
|
||||
config[voice_hash] = {
|
||||
"voice": voice,
|
||||
"file_path": file_path,
|
||||
"time": time.time(),
|
||||
"text": 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
|
||||
Reference in New Issue
Block a user