Merge remote-tracking branch 'upstream/main' into add_local_tts_change_voice

This commit is contained in:
shane0411
2025-06-11 14:29:35 +08:00
8 changed files with 41 additions and 29 deletions
+2 -2
View File
@@ -269,7 +269,7 @@ LLM:
如果你能看到,类似以下日志,则是本项目服务启动成功的标志。 如果你能看到,类似以下日志,则是本项目服务启动成功的标志。
``` ```
250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-OTA接口是 http://192.168.4.123:8002/xiaozhi/ota/ 250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-OTA接口是 http://192.168.4.123:8003/xiaozhi/ota/
250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-Websocket地址是 ws://192.168.4.123:8000/xiaozhi/v1/ 250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-Websocket地址是 ws://192.168.4.123:8000/xiaozhi/v1/
250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-=======上面的地址是websocket协议地址,请勿用浏览器访问======= 250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-=======上面的地址是websocket协议地址,请勿用浏览器访问=======
250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-如想测试websocket请用谷歌浏览器打开test目录下的test_page.html 250427 13:04:20[0.3.11_SiFuChTTnofu][__main__]-INFO-如想测试websocket请用谷歌浏览器打开test目录下的test_page.html
@@ -280,7 +280,7 @@ LLM:
但是如果你用docker部署,那么你的日志里给出的接口地址信息就不是真实的接口地址。 但是如果你用docker部署,那么你的日志里给出的接口地址信息就不是真实的接口地址。
最正确的方法,是根据电脑的局域网IP来确定你的接口地址。 最正确的方法,是根据电脑的局域网IP来确定你的接口地址。
如果你的电脑的局域网IP比如是`192.168.1.25`,那么你的接口地址就是:`ws://192.168.1.25:8000/xiaozhi/v1/`,对应的OTA地址就是:`http://192.168.1.25:8002/xiaozhi/ota/` 如果你的电脑的局域网IP比如是`192.168.1.25`,那么你的接口地址就是:`ws://192.168.1.25:8000/xiaozhi/v1/`,对应的OTA地址就是:`http://192.168.1.25:8003/xiaozhi/ota/`
这个信息很有用的,后面`编译esp32固件`需要用到。 这个信息很有用的,后面`编译esp32固件`需要用到。
+1 -1
View File
@@ -9,7 +9,7 @@
### 如果你用的是简单Server部署 ### 如果你用的是简单Server部署
此刻,请你用浏览器打开你的ota地址,例如我的ota地址 此刻,请你用浏览器打开你的ota地址,例如我的ota地址
``` ```
http://192.168.1.25:8002/xiaozhi/ota/ http://192.168.1.25:8003/xiaozhi/ota/
``` ```
如果显示“OTA接口运行正常,向设备发送的websocket地址是:ws://xxx:8000/xiaozhi/v1/ 如果显示“OTA接口运行正常,向设备发送的websocket地址是:ws://xxx:8000/xiaozhi/v1/
@@ -106,6 +106,7 @@ public class AgentController {
entity.setAsrModelId(template.getAsrModelId()); entity.setAsrModelId(template.getAsrModelId());
entity.setVadModelId(template.getVadModelId()); entity.setVadModelId(template.getVadModelId());
entity.setLlmModelId(template.getLlmModelId()); entity.setLlmModelId(template.getLlmModelId());
entity.setVllmModelId(template.getVllmModelId());
entity.setTtsModelId(template.getTtsModelId()); entity.setTtsModelId(template.getTtsModelId());
entity.setTtsVoiceId(template.getTtsVoiceId()); entity.setTtsVoiceId(template.getTtsVoiceId());
entity.setMemModelId(template.getMemModelId()); entity.setMemModelId(template.getMemModelId());
@@ -538,7 +538,7 @@ export default {
.data-table { .data-table {
border-radius: 6px; border-radius: 6px;
overflow-y: auto; overflow-y: hidden;
background-color: transparent !important; background-color: transparent !important;
--table-max-height: calc(100vh - 40vh); --table-max-height: calc(100vh - 40vh);
max-height: var(--table-max-height); max-height: var(--table-max-height);
+15 -6
View File
@@ -68,23 +68,32 @@ async def checkWakeupWords(conn, text):
# 获取当前音色 # 获取当前音色
voice = getattr(conn.tts, "voice", "default") voice = getattr(conn.tts, "voice", "default")
if not voice:
voice = "default"
# 获取唤醒词回复配置 # 获取唤醒词回复配置
response = wakeup_words_config.get_wakeup_response(voice) response = wakeup_words_config.get_wakeup_response(voice)
if not response or not response.get("file_path"):
response = {
"voice": "default",
"file_path": "config/assets/wakeup_words.wav",
"time": 0,
"text": "哈啰啊,我是小智啦,声音好听的台湾女孩一枚,超开心认识你耶,最近在忙啥,别忘了给我来点有趣的料哦,我超爱听八卦的啦",
}
# 播放唤醒词回复 # 播放唤醒词回复
conn.client_abort = False conn.client_abort = False
opus_packets, _ = audio_to_data(response["file_path"]) opus_packets, _ = audio_to_data(response.get("file_path"))
conn.logger.bind(tag=TAG).info(f"播放唤醒词回复: {response['text']}") conn.logger.bind(tag=TAG).info(f"播放唤醒词回复: {response.get('text')}")
await sendAudioMessage(conn, SentenceType.FIRST, opus_packets, response["text"]) await sendAudioMessage(conn, SentenceType.FIRST, opus_packets, response.get("text"))
await sendAudioMessage(conn, SentenceType.LAST, [], None) await sendAudioMessage(conn, SentenceType.LAST, [], None)
# 补充对话 # 补充对话
conn.dialogue.put(Message(role="assistant", content=response["text"])) conn.dialogue.put(Message(role="assistant", content=response.get("text")))
# 检查是否需要更新唤醒词回复 # 检查是否需要更新唤醒词回复
if time.time() - response["time"] > WAKEUP_CONFIG["refresh_time"]: if time.time() - response.get("time", 0) > WAKEUP_CONFIG["refresh_time"]:
if not _wakeup_response_lock.locked(): if not _wakeup_response_lock.locked():
asyncio.create_task(wakeupWordsResponse(conn)) asyncio.create_task(wakeupWordsResponse(conn))
return True return True
@@ -104,7 +113,7 @@ async def wakeupWordsResponse(conn):
question = ( question = (
"此刻用户正在和你说```" "此刻用户正在和你说```"
+ wakeup_word + wakeup_word
+ "```。\n请你根据以上用户的内容进行简短回复。要像一个人正常人一样说话,不要像机器人一样说话。\n" + "```。\n请你根据以上用户的内容进行20-30字回复。要符合系统设置的角色情感和态度,不要像机器人一样说话。\n"
+ "请勿对这条内容本身进行任何解释和回应,请勿返回表情符号,仅返回对用户的内容的回复。" + "请勿对这条内容本身进行任何解释和回应,请勿返回表情符号,仅返回对用户的内容的回复。"
) )
+12 -8
View File
@@ -1,4 +1,5 @@
import os import os
import re
import queue import queue
import uuid import uuid
import asyncio import asyncio
@@ -169,15 +170,18 @@ class TTSProviderBase(ABC):
content_type=ContentType.ACTION, content_type=ContentType.ACTION,
) )
) )
self.tts_text_queue.put( # 对于单句的文本,进行分段处理
TTSMessageDTO( segments = re.split(r'([。!?!?;\n])', content_detail)
sentence_id=sentence_id, for seg in segments:
sentence_type=SentenceType.MIDDLE, self.tts_text_queue.put(
content_type=content_type, TTSMessageDTO(
content_detail=content_detail, sentence_id=sentence_id,
content_file=content_file, sentence_type=SentenceType.MIDDLE,
content_type=content_type,
content_detail=seg,
content_file=content_file,
)
) )
)
self.tts_text_queue.put( self.tts_text_queue.put(
TTSMessageDTO( TTSMessageDTO(
sentence_id=sentence_id, sentence_id=sentence_id,
@@ -11,8 +11,8 @@ class TTSProvider(TTSProviderBase):
self.voice = config.get("private_voice") self.voice = config.get("private_voice")
else: else:
self.voice = config.get("voice") self.voice = config.get("voice")
self.response_format = config.get("response_format", "mp3") self.response_format = config.get("response_format", "wav")
self.audio_file_type = config.get("response_format", "mp3") self.audio_file_type = config.get("response_format", "wav")
self.host = "api.coze.cn" self.host = "api.coze.cn"
self.api_url = f"https://{self.host}/v1/audio/speech" self.api_url = f"https://{self.host}/v1/audio/speech"
@@ -1,4 +1,5 @@
import os import os
import re
import yaml import yaml
import time import time
import hashlib import hashlib
@@ -88,33 +89,30 @@ class WakeupWordsConfig:
voice = hashlib.md5(voice.encode()).hexdigest() voice = hashlib.md5(voice.encode()).hexdigest()
"""获取唤醒词回复配置""" """获取唤醒词回复配置"""
config = self._load_config() 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: if not config or voice not in config:
return default_response return None
# 检查文件大小 # 检查文件大小
file_path = config[voice]["file_path"] file_path = config[voice]["file_path"]
if not os.path.exists(file_path) or os.stat(file_path).st_size < (15 * 1024): if not os.path.exists(file_path) or os.stat(file_path).st_size < (15 * 1024):
return default_response return None
return config[voice] return config[voice]
def update_wakeup_response(self, voice: str, file_path: str, text: str): def update_wakeup_response(self, voice: str, file_path: str, text: str):
"""更新唤醒词回复配置""" """更新唤醒词回复配置"""
try: try:
# 过滤表情符号
filtered_text = re.sub(r'[\U0001F600-\U0001F64F\U0001F900-\U0001F9FF]', '', text)
config = self._load_config() config = self._load_config()
voice_hash = hashlib.md5(voice.encode()).hexdigest() voice_hash = hashlib.md5(voice.encode()).hexdigest()
config[voice_hash] = { config[voice_hash] = {
"voice": voice, "voice": voice,
"file_path": file_path, "file_path": file_path,
"time": time.time(), "time": time.time(),
"text": text, "text": filtered_text,
} }
self._save_config(config) self._save_config(config)
except Exception as e: except Exception as e: