Skip to content

Commit 15b3f0a

Browse files
authored
Merge pull request #2695 from xinnan-tech/py_device_bind
update: 未绑定设备策略优化
2 parents 6eb7aca + 5c26152 commit 15b3f0a

File tree

7 files changed

+139
-70
lines changed

7 files changed

+139
-70
lines changed

main/xiaozhi-server/core/connection.py

Lines changed: 51 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ def __init__(
6969
self.server = server # 保存server实例的引用
7070

7171
self.need_bind = False # 是否需要绑定设备
72+
self.bind_completed_event = asyncio.Event()
7273
self.bind_code = None # 绑定设备的验证码
7374
self.last_bind_prompt_time = 0 # 上次播放绑定提示的时间戳(秒)
7475
self.bind_prompt_interval = 60 # 绑定提示播放间隔(秒)
@@ -266,30 +267,43 @@ def save_memory_task():
266267
f"保存记忆后关闭连接失败: {close_error}"
267268
)
268269

270+
async def _discard_message_with_bind_prompt(self):
271+
"""丢弃消息并检查是否需要播放绑定提示"""
272+
current_time = time.time()
273+
# 检查是否需要播放绑定提示
274+
if current_time - self.last_bind_prompt_time >= self.bind_prompt_interval:
275+
self.last_bind_prompt_time = current_time
276+
# 复用现有的绑定提示逻辑
277+
from core.handle.receiveAudioHandle import check_bind_device
278+
279+
asyncio.create_task(check_bind_device(self))
280+
269281
async def _route_message(self, message):
270282
"""消息路由"""
283+
# 检查是否已经获取到真实的绑定状态
284+
if not self.bind_completed_event.is_set():
285+
# 还没有获取到真实状态,等待直到获取到真实状态或超时
286+
try:
287+
await asyncio.wait_for(self.bind_completed_event.wait(), timeout=1)
288+
except asyncio.TimeoutError:
289+
# 超时仍未获取到真实状态,丢弃消息
290+
await self._discard_message_with_bind_prompt()
291+
return
292+
293+
# 已经获取到真实状态,检查是否需要绑定
294+
if self.need_bind:
295+
# 需要绑定,丢弃消息
296+
await self._discard_message_with_bind_prompt()
297+
return
298+
299+
# 不需要绑定,继续处理消息
300+
271301
if isinstance(message, str):
272302
await handleTextMessage(self, message)
273303
elif isinstance(message, bytes):
274304
if self.vad is None or self.asr is None:
275305
return
276306

277-
# 未绑定设备直接丢弃所有音频,不进行ASR处理
278-
if self.need_bind:
279-
current_time = time.time()
280-
# 检查是否需要播放绑定提示
281-
if (
282-
current_time - self.last_bind_prompt_time
283-
>= self.bind_prompt_interval
284-
):
285-
self.last_bind_prompt_time = current_time
286-
# 复用现有的绑定提示逻辑
287-
from core.handle.receiveAudioHandle import check_bind_device
288-
289-
asyncio.create_task(check_bind_device(self))
290-
# 直接丢弃音频,不进行ASR处理
291-
return
292-
293307
# 处理来自MQTT网关的音频包
294308
if self.conn_from_mqtt_gateway and len(message) >= 16:
295309
handled = await self._process_mqtt_audio_message(message)
@@ -413,6 +427,14 @@ def restart_server():
413427

414428
def _initialize_components(self):
415429
try:
430+
if self.tts is None:
431+
self.tts = self._initialize_tts()
432+
# 打开语音合成通道
433+
asyncio.run_coroutine_threadsafe(
434+
self.tts.open_audio_channels(self), self.loop
435+
)
436+
if self.need_bind:
437+
return
416438
self.selected_module_str = build_module_string(
417439
self.config.get("selected_module", {})
418440
)
@@ -436,17 +458,10 @@ def _initialize_components(self):
436458

437459
# 初始化声纹识别
438460
self._initialize_voiceprint()
439-
440461
# 打开语音识别通道
441462
asyncio.run_coroutine_threadsafe(
442463
self.asr.open_audio_channels(self), self.loop
443464
)
444-
if self.tts is None:
445-
self.tts = self._initialize_tts()
446-
# 打开语音合成通道
447-
asyncio.run_coroutine_threadsafe(
448-
self.tts.open_audio_channels(self), self.loop
449-
)
450465

451466
"""加载记忆"""
452467
self._initialize_memory()
@@ -461,6 +476,7 @@ def _initialize_components(self):
461476
self.logger.bind(tag=TAG).error(f"实例化组件失败: {e}")
462477

463478
def _init_prompt_enhancement(self):
479+
464480
# 更新上下文信息
465481
self.prompt_manager.update_context_info(self, self.client_ip)
466482
enhanced_prompt = self.prompt_manager.build_enhanced_prompt(
@@ -496,7 +512,11 @@ def _initialize_tts(self):
496512

497513
def _initialize_asr(self):
498514
"""初始化ASR"""
499-
if self._asr is not None and hasattr(self._asr, "interface_type") and self._asr.interface_type == InterfaceType.LOCAL:
515+
if (
516+
self._asr is not None
517+
and hasattr(self._asr, "interface_type")
518+
and self._asr.interface_type == InterfaceType.LOCAL
519+
):
500520
# 如果公共ASR是本地服务,则直接返回
501521
# 因为本地一个实例ASR,可以被多个连接共享
502522
asr = self._asr
@@ -536,6 +556,8 @@ async def _background_initialize(self):
536556
async def _initialize_private_config_async(self):
537557
"""从接口异步获取差异化配置(异步版本,不阻塞主循环)"""
538558
if not self.read_config_from_api:
559+
self.need_bind = False
560+
self.bind_completed_event.set()
539561
return
540562
try:
541563
begin_time = time.time()
@@ -548,15 +570,20 @@ async def _initialize_private_config_async(self):
548570
self.logger.bind(tag=TAG).info(
549571
f"{time.time() - begin_time} 秒,异步获取差异化配置成功: {json.dumps(filter_sensitive_info(private_config), ensure_ascii=False)}"
550572
)
573+
self.need_bind = False
574+
self.bind_completed_event.set()
551575
except DeviceNotFoundException as e:
552576
self.need_bind = True
577+
self.bind_completed_event.set() # 状态已确定,设置事件
553578
private_config = {}
554579
except DeviceBindException as e:
555580
self.need_bind = True
556581
self.bind_code = e.bind_code
582+
self.bind_completed_event.set() # 状态已确定,设置事件
557583
private_config = {}
558584
except Exception as e:
559585
self.need_bind = True
586+
self.bind_completed_event.set() # 状态已确定,设置事件
560587
self.logger.bind(tag=TAG).error(f"异步获取差异化配置失败: {e}")
561588
private_config = {}
562589

main/xiaozhi-server/core/handle/helloHandle.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ async def checkWakeupWords(conn, text):
101101
}
102102

103103
# 获取音频数据
104-
opus_packets = audio_to_data(response.get("file_path"))
104+
opus_packets = await audio_to_data(response.get("file_path"), use_cache=False)
105105
# 播放唤醒词回复
106106
conn.client_abort = False
107107

main/xiaozhi-server/core/handle/receiveAudioHandle.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ async def max_out_size(conn):
123123
text = "不好意思,我现在有点事情要忙,明天这个时候我们再聊,约好了哦!明天不见不散,拜拜!"
124124
await send_stt_message(conn, text)
125125
file_path = "config/assets/max_output_size.wav"
126-
opus_packets = audio_to_data(file_path)
126+
opus_packets = await audio_to_data(file_path)
127127
conn.tts.tts_audio_queue.put((SentenceType.LAST, opus_packets, text))
128128
conn.close_after_chat = True
129129

@@ -142,15 +142,15 @@ async def check_bind_device(conn):
142142

143143
# 播放提示音
144144
music_path = "config/assets/bind_code.wav"
145-
opus_packets = audio_to_data(music_path)
145+
opus_packets = await audio_to_data(music_path)
146146
conn.tts.tts_audio_queue.put((SentenceType.FIRST, opus_packets, text))
147147

148148
# 逐个播放数字
149149
for i in range(6): # 确保只播放6位数字
150150
try:
151151
digit = conn.bind_code[i]
152152
num_path = f"config/assets/bind_code/{digit}.wav"
153-
num_packets = audio_to_data(num_path)
153+
num_packets = await audio_to_data(num_path)
154154
conn.tts.tts_audio_queue.put((SentenceType.MIDDLE, num_packets, None))
155155
except Exception as e:
156156
conn.logger.bind(tag=TAG).error(f"播放数字音频失败: {e}")
@@ -162,5 +162,5 @@ async def check_bind_device(conn):
162162
text = f"没有找到该设备的版本信息,请正确配置 OTA地址,然后重新编译固件。"
163163
await send_stt_message(conn, text)
164164
music_path = "config/assets/bind_not_found.wav"
165-
opus_packets = audio_to_data(music_path)
165+
opus_packets = await audio_to_data(music_path)
166166
conn.tts.tts_audio_queue.put((SentenceType.LAST, opus_packets, text))

main/xiaozhi-server/core/handle/sendAudioHandle.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,12 @@ async def sendAudioMessage(conn, sentenceType, audios, text):
1717

1818
if sentenceType == SentenceType.FIRST:
1919
# 同一句子的后续消息加入流控队列,其他情况立即发送
20-
if hasattr(conn, "audio_rate_controller") and conn.audio_rate_controller and getattr(conn, "audio_flow_control", {}).get("sentence_id") == conn.sentence_id:
20+
if (
21+
hasattr(conn, "audio_rate_controller")
22+
and conn.audio_rate_controller
23+
and getattr(conn, "audio_flow_control", {}).get("sentence_id")
24+
== conn.sentence_id
25+
):
2126
conn.audio_rate_controller.add_message(
2227
lambda: send_tts_message(conn, "sentence_start", text)
2328
)
@@ -120,7 +125,8 @@ def _get_or_create_rate_controller(conn, frame_duration, is_single_packet):
120125
# 判断是否需要重置:单包模式且 sentence_id 变化,或者控制器不存在
121126
need_reset = (
122127
is_single_packet
123-
and getattr(conn, "audio_flow_control", {}).get("sentence_id") != conn.sentence_id
128+
and getattr(conn, "audio_flow_control", {}).get("sentence_id")
129+
!= conn.sentence_id
124130
) or not hasattr(conn, "audio_rate_controller")
125131

126132
if need_reset:
@@ -138,7 +144,9 @@ def _get_or_create_rate_controller(conn, frame_duration, is_single_packet):
138144
}
139145

140146
# 启动后台发送循环
141-
_start_background_sender(conn, conn.audio_rate_controller, conn.audio_flow_control)
147+
_start_background_sender(
148+
conn, conn.audio_rate_controller, conn.audio_flow_control
149+
)
142150

143151
return conn.audio_rate_controller, conn.audio_flow_control
144152

@@ -152,6 +160,7 @@ def _start_background_sender(conn, rate_controller, flow_control):
152160
rate_controller: 速率控制器
153161
flow_control: 流控状态
154162
"""
163+
155164
async def send_callback(packet):
156165
# 检查是否应该中止
157166
if conn.client_abort:
@@ -165,7 +174,9 @@ async def send_callback(packet):
165174
rate_controller.start_sending(send_callback)
166175

167176

168-
async def _send_audio_with_rate_control(conn, audio_list, rate_controller, flow_control, send_delay):
177+
async def _send_audio_with_rate_control(
178+
conn, audio_list, rate_controller, flow_control, send_delay
179+
):
169180
"""
170181
使用 rate_controller 发送音频包
171182
@@ -235,7 +246,7 @@ async def send_tts_message(conn, state, text=None):
235246
stop_tts_notify_voice = conn.config.get(
236247
"stop_tts_notify_voice", "config/assets/tts_notify.mp3"
237248
)
238-
audios = audio_to_data(stop_tts_notify_voice, is_opus=True)
249+
audios = await audio_to_data(stop_tts_notify_voice, is_opus=True)
239250
await sendAudio(conn, audios)
240251
# 等待所有音频包发送完成
241252
await _wait_for_audio_completion(conn)

main/xiaozhi-server/core/utils/audioRateController.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,6 @@ async def check_queue(self, send_audio_callback):
118118

119119
self.queue_empty_event.set()
120120

121-
122121
def start_sending(self, send_audio_callback):
123122
"""
124123
启动异步发送任务
@@ -129,6 +128,7 @@ def start_sending(self, send_audio_callback):
129128
Returns:
130129
asyncio.Task: 发送任务
131130
"""
131+
132132
async def _send_loop():
133133
try:
134134
while True:

main/xiaozhi-server/core/utils/cache/config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class CacheType(Enum):
1919
CONFIG = "config"
2020
DEVICE_PROMPT = "device_prompt"
2121
VOICEPRINT_HEALTH = "voiceprint_health" # 声纹识别健康检查
22+
AUDIO_DATA = "audio_data" # 音频数据缓存
2223

2324

2425
@dataclass
@@ -58,5 +59,8 @@ def for_type(cls, cache_type: CacheType) -> "CacheConfig":
5859
CacheType.VOICEPRINT_HEALTH: cls(
5960
strategy=CacheStrategy.TTL, ttl=600, max_size=100 # 10分钟过期
6061
),
62+
CacheType.AUDIO_DATA: cls(
63+
strategy=CacheStrategy.TTL, ttl=600, max_size=100 # 10分钟过期
64+
),
6165
}
6266
return configs.get(cache_type, cls())

0 commit comments

Comments
 (0)