From bec1c7b88f4e5ebd53c3f2988c466ea71ba05542 Mon Sep 17 00:00:00 2001 From: Sakura-RanChen <1908198662@qq.com> Date: Fri, 19 Sep 2025 17:29:09 +0800 Subject: [PATCH 01/27] =?UTF-8?q?update:=20=E5=A2=9E=E5=8A=A0=E8=AE=AF?= =?UTF-8?q?=E9=A3=9E=E5=8F=8C=E6=B5=81=E5=BC=8FTTS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/db/changelog/202509191545.sql | 41 ++ .../db/changelog/db.changelog-master.yaml | 7 + main/xiaozhi-server/config.yaml | 24 +- .../core/providers/tts/xunfei_stream.py | 527 ++++++++++++++++++ 4 files changed, 598 insertions(+), 1 deletion(-) create mode 100644 main/manager-api/src/main/resources/db/changelog/202509191545.sql create mode 100644 main/xiaozhi-server/core/providers/tts/xunfei_stream.py diff --git a/main/manager-api/src/main/resources/db/changelog/202509191545.sql b/main/manager-api/src/main/resources/db/changelog/202509191545.sql new file mode 100644 index 00000000..a1992977 --- /dev/null +++ b/main/manager-api/src/main/resources/db/changelog/202509191545.sql @@ -0,0 +1,41 @@ +-- 添加讯飞流式TTS供应器 +delete from `ai_model_provider` where id = 'SYSTEM_TTS_XunFeiStreamTTS'; +INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES +('SYSTEM_TTS_XunFeiStreamTTS', 'TTS', 'xunfei_stream', '讯飞流式语音合成', '[{"key":"app_id","label":"APP_ID","type":"string"},{"key":"api_secret","label":"API_Secret","type":"string"},{"key":"api_key","label":"API密钥","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"},{"key":"voice","label":"音色","type":"string"},{"key":"format","label":"音频格式","type":"string"},{"key":"sample_rate","label":"采样率","type":"number"},{"key": "volume", "type": "number", "label": "音量"},{"key": "speed", "type": "number", "label": "语速"},{"key": "pitch", "type": "number", "label": "音调"},{"key": "oral_level", "type": "number", "label": "口语化等级"},{"key": "spark_assist", "type": "number", "label": "是否口语化"},{"key": "stop_split", "type": "number", "label": "服务端拆句"},{"key": "remain", "type": "number", "label": "保留书面语"}]', 20, 1, NOW(), 1, NOW()); + +-- 添加讯飞流式TTS模型配置 +delete from `ai_model_config` where id = 'TTS_XunFeiStreamTTS'; +INSERT INTO `ai_model_config` VALUES ('TTS_XunFeiStreamTTS', 'TTS', 'XunFeiStreamTTS', '讯飞流式语音合成', 0, 1, '{\"type\": \"xunfei_stream\", \"app_id\": \"\", \"api_secret\": \"\", \"api_key\": \"\", \"output_dir\": \"tmp/\", \"voice\": \"x5_lingxiaoxuan_flow\", \"format\": \"raw\", \"sample_rate\": 24000, \"volume\": 50, \"speed\": 50, \"pitch\": 50, \"oral_level\": \"mid\", \"spark_assist\": 1, \"stop_split\": 0, \"remain\": 0}', NULL, NULL, 23, NULL, NULL, NULL, NULL); + +-- 更新讯飞流式TTS配置说明 +UPDATE `ai_model_config` SET +`doc_link` = 'https://console.xfyun.cn/app/myapp', +`remark` = '讯飞流式TTS说明: +1. 登录讯飞语音技术平台 https://console.xfyun.cn/app/myapp 创建相关应用 +2. 选择需要的服务获取api相关配置 https://console.xfyun.cn/services/uts +3. 为需要使用的应用(APPID)购买相关服务 例如:超拟人合成 https://console.xfyun.cn/services/uts +5. 支持实时双流式通信,具有较低的延迟 +6. 支持口语化设置和音频参数调整 注意:V5音色不支持相关口语化配置 +7. 支持实时调节音量、语速、音调等参数 +' WHERE `id` = 'TTS_XunFeiStreamTTS'; + +-- 添加讯飞流式TTS音色 +delete from `ai_tts_voice` where tts_model_id = 'TTS_XunFeiStreamTTS'; + +-- 基础角色 +INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0001', 'TTS_XunFeiStreamTTS', '聆小璇', 'x5_lingxiaoxuan_flow', '中文', NULL, NULL, NULL, NULL, 1, NULL, NULL, NULL, NULL); +INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0002', 'TTS_XunFeiStreamTTS', '聆飞逸', 'x5_lingfeiyi_flow', '中文', NULL, NULL, NULL, NULL, 2, NULL, NULL, NULL, NULL); +INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0003', 'TTS_XunFeiStreamTTS', '聆小玥', 'x5_lingxiaoyue_flow', '中文', NULL, NULL, NULL, NULL, 3, NULL, NULL, NULL, NULL); +INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0004', 'TTS_XunFeiStreamTTS', '聆玉昭', 'x5_lingyuzhao_flow', '中文', NULL, NULL, NULL, NULL, 4, NULL, NULL, NULL, NULL); +INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0005', 'TTS_XunFeiStreamTTS', '聆玉言', 'x5_lingyuyan_flow', '中文', NULL, NULL, NULL, NULL, 5, NULL, NULL, NULL, NULL); + +-- 需要添加对应的角色音色 +INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0006', 'TTS_XunFeiStreamTTS', '聆飞哲', 'x4_lingfeizhe_oral', '中文', NULL, NULL, NULL, NULL, 6, NULL, NULL, NULL, NULL); +INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0007', 'TTS_XunFeiStreamTTS', '聆小璃', 'x4_lingxiaoli_oral', '中文', NULL, NULL, NULL, NULL, 7, NULL, NULL, NULL, NULL); +INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0008', 'TTS_XunFeiStreamTTS', '聆小糖', 'x5_lingxiaotang_flow', '中文', NULL, NULL, NULL, NULL, 8, NULL, NULL, NULL, NULL); +INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0009', 'TTS_XunFeiStreamTTS', '聆小琪', 'x4_lingxiaoqi_oral', '中文', NULL, NULL, NULL, NULL, 9, NULL, NULL, NULL, NULL); +INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0010', 'TTS_XunFeiStreamTTS', '聆佑佑-童年女声', 'x4_lingyouyou_oral', '中文', NULL, NULL, NULL, NULL, 10, NULL, NULL, NULL, NULL); +INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0011', 'TTS_XunFeiStreamTTS', '子津', 'x4_zijin_oral', '天津话', NULL, NULL, NULL, NULL, 11, NULL, NULL, NULL, NULL); +INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0012', 'TTS_XunFeiStreamTTS', '子阳', 'x4_ziyang_oral', '东北话', NULL, NULL, NULL, NULL, 12, NULL, NULL, NULL, NULL); +INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0013', 'TTS_XunFeiStreamTTS', 'Grant', 'x5_EnUs_Grant_flow', '英文', NULL, NULL, NULL, NULL, 13, NULL, NULL, NULL, NULL); +INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0014', 'TTS_XunFeiStreamTTS', 'Lila', 'x5_EnUs_Lila_flow', '英文', NULL, NULL, NULL, NULL, 14, NULL, NULL, NULL, NULL); diff --git a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml index 934f7314..c82ec415 100755 --- a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml @@ -359,3 +359,10 @@ databaseChangeLog: - sqlFile: encoding: utf8 path: classpath:db/changelog/202509161701.sql + - changeSet: + id: 202509191545 + author: RanChen + changes: + - sqlFile: + encoding: utf8 + path: classpath:db/changelog/202509191545.sql diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index a02de435..832ca13e 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -936,4 +936,26 @@ TTS: # sample_rate: 24000 # 采样率:16000, 24000, 48000 # volume: 50 # 音量:0-100 # rate: 1 # 语速:0.5~2 - # pitch: 1 # 语调:0.5~2 \ No newline at end of file + # pitch: 1 # 语调:0.5~2 + XunFeiTTS: + # 讯飞TTS服务 官方网站:https://www.xfyun.cn/ + # 登录讯飞语音技术平台 https://console.xfyun.cn/app/myapp 创建相关应用 + # 选择需要的服务获取api相关配置 https://console.xfyun.cn/services/uts + # 为需要使用的应用(APPID)购买相关服务 例如:超拟人合成 https://console.xfyun.cn/services/uts + type: xunfei_stream + api_url: wss://cbm01.cn-huabei-1.xf-yun.com/v1/private/mcd9m97e6 + app_id: 你的app_id + api_secret: 你的api_secret + api_key: 你的api_key + voice: x5_lingxiaoxuan_flow + output_dir: tmp/ + # 以下可不用设置,使用默认设置,注意V5音色不支持口语化配置 + # oral_level: mid # 口语化等级:high, mid, low + # spark_assist: 1 # 是否通过大模型进行口语化 开启:1, 关闭:0 + # stop_split: 0 # 关闭服务端拆句 不关闭:0,关闭:1 + # remain: 0 # 是否保留原书面语的样子 保留:1, 不保留:0 + # format: raw # 音频格式:raw(PCM), lame(MP3), speex, opus, opus-wb, opus-swb, speex-wb + # sample_rate: 24000 # 采样率:16000, 8000, 24000 + # volume: 50 # 音量:0-100 + # speed: 50 # 语速:0-100 + # pitch: 50 # 语调:0-100 \ No newline at end of file diff --git a/main/xiaozhi-server/core/providers/tts/xunfei_stream.py b/main/xiaozhi-server/core/providers/tts/xunfei_stream.py new file mode 100644 index 00000000..d06c9330 --- /dev/null +++ b/main/xiaozhi-server/core/providers/tts/xunfei_stream.py @@ -0,0 +1,527 @@ +import os +import time +import uuid +import json +import hmac +import queue +import base64 +import hashlib +import asyncio +import traceback +import websockets +from asyncio import Task +from config.logger import setup_logging +from core.utils import opus_encoder_utils +from core.utils.tts import MarkdownCleaner +from urllib.parse import urlencode, urlparse +from core.providers.tts.base import TTSProviderBase +from core.providers.tts.dto.dto import SentenceType, ContentType, InterfaceType + +TAG = __name__ +logger = setup_logging() + + +class XunfeiWSAuth: + @staticmethod + def create_auth_url(api_key, api_secret, api_url): + """生成讯飞WebSocket认证URL""" + parsed_url = urlparse(api_url) + host = parsed_url.netloc + path = parsed_url.path + + # 获取UTC时间,讯飞要求使用RFC1123格式 + now = time.gmtime() + date = time.strftime('%a, %d %b %Y %H:%M:%S GMT', now) + + # 构造签名字符串 + signature_origin = f"host: {host}\ndate: {date}\nGET {path} HTTP/1.1" + + # 计算签名 + signature_sha = hmac.new( + api_secret.encode('utf-8'), + signature_origin.encode('utf-8'), + digestmod=hashlib.sha256 + ).digest() + signature_sha_base64 = base64.b64encode(signature_sha).decode(encoding='utf-8') + + # 构造authorization + authorization_origin = f'api_key="{api_key}", algorithm="hmac-sha256", headers="host date request-line", signature="{signature_sha_base64}"' + authorization = base64.b64encode(authorization_origin.encode('utf-8')).decode(encoding='utf-8') + + # 构造最终的WebSocket URL + v = { + "authorization": authorization, + "date": date, + "host": host + } + url = api_url + '?' + urlencode(v) + return url + + +class TTSProvider(TTSProviderBase): + def __init__(self, config, delete_audio_file): + super().__init__(config, delete_audio_file) + + # 设置为流式接口类型 + self.interface_type = InterfaceType.DUAL_STREAM + + # 基础配置 + self.app_id = config.get("app_id") + self.api_key = config.get("api_key") + self.api_secret = config.get("api_secret") + + # 接口地址 + self.api_url = config.get("api_url", "wss://cbm01.cn-huabei-1.xf-yun.com/v1/private/mcd9m97e6") + + # 音色配置 + self.voice = config.get("voice", "x5_lingxiaoxuan_flow") + if config.get("private_voice"): + self.voice = config.get("private_voice") + + # 音频参数配置 + speed = config.get("speed", "50") + self.speed = int(speed) if speed else 50 + + volume = config.get("volume", "50") + self.volume = int(volume) if volume else 50 + + pitch = config.get("pitch", "50") + self.pitch = int(pitch) if pitch else 50 + + # 音频编码配置 + self.format = config.get("format", "raw") + + sample_rate = config.get("sample_rate", "24000") + self.sample_rate = int(sample_rate) if sample_rate else 24000 + + # 口语化配置 + self.oral_level = config.get("oral_level", "mid") + + spark_assist = config.get("spark_assist", "1") + self.spark_assist = int(spark_assist) if spark_assist else 1 + + stop_split = config.get("stop_split", "0") + self.stop_split = int(stop_split) if stop_split else 0 + + remain = config.get("remain", "0") + self.remain = int(remain) if remain else 0 + + # WebSocket配置 + self.ws = None + self._monitor_task = None + + # 序列号管理 + self.text_seq = 0 + + # 创建Opus编码器 + self.opus_encoder = opus_encoder_utils.OpusEncoderUtils( + sample_rate=self.sample_rate, channels=1, frame_size_ms=60 + ) + + # 验证必需参数 + if not all([self.app_id, self.api_key, self.api_secret]): + raise ValueError("讯飞TTS需要配置app_id、api_key和api_secret") + + async def _ensure_connection(self): + """确保WebSocket连接可用""" + try: + logger.bind(tag=TAG).info("开始建立新连接...") + + # 生成认证URL + auth_url = XunfeiWSAuth.create_auth_url( + self.api_key, self.api_secret, self.api_url + ) + + self.ws = await websockets.connect( + auth_url, + ping_interval=30, + ping_timeout=10, + close_timeout=10, + ) + logger.bind(tag=TAG).info("WebSocket连接建立成功") + return self.ws + except Exception as e: + logger.bind(tag=TAG).error(f"建立连接失败: {str(e)}") + self.ws = None + raise + + def tts_text_priority_thread(self): + """流式文本处理线程""" + while not self.conn.stop_event.is_set(): + try: + message = self.tts_text_queue.get(timeout=1) + logger.bind(tag=TAG).debug( + f"收到TTS任务|{message.sentence_type.name} | {message.content_type.name} | 会话ID: {self.conn.sentence_id}" + ) + + if message.sentence_type == SentenceType.FIRST: + # 重置序列号 + self.text_seq = 0 + self.conn.client_abort = False + # 增加序列号 + self.text_seq += 1 + if self.conn.client_abort: + logger.bind(tag=TAG).info("收到打断信息,终止TTS文本处理线程") + continue + + if message.sentence_type == SentenceType.FIRST: + # 初始化参数 + try: + if not getattr(self.conn, "sentence_id", None): + self.conn.sentence_id = uuid.uuid4().hex + logger.bind(tag=TAG).info(f"自动生成新的 会话ID: {self.conn.sentence_id}") + + logger.bind(tag=TAG).info("开始启动TTS会话...") + future = asyncio.run_coroutine_threadsafe( + self.start_session(self.conn.sentence_id), + loop=self.conn.loop, + ) + future.result() + self.before_stop_play_files.clear() + logger.bind(tag=TAG).info("TTS会话启动成功") + + except Exception as e: + logger.bind(tag=TAG).error(f"启动TTS会话失败: {str(e)}") + continue + + # 处理文本内容 + if ContentType.TEXT == message.content_type: + if message.content_detail: + try: + logger.bind(tag=TAG).debug( + f"开始发送TTS文本: {message.content_detail}" + ) + future = asyncio.run_coroutine_threadsafe( + self.text_to_speak(message.content_detail, None), + loop=self.conn.loop, + ) + future.result() + logger.bind(tag=TAG).debug("TTS文本发送成功") + except Exception as e: + logger.bind(tag=TAG).error(f"发送TTS文本失败: {str(e)}") + # 不使用continue,确保后续处理不被中断 + + # 处理文件内容 + if ContentType.FILE == message.content_type: + logger.bind(tag=TAG).info( + f"添加音频文件到待播放列表: {message.content_file}" + ) + if message.content_file and os.path.exists(message.content_file): + # 先处理文件音频数据 + self._process_audio_file_stream(message.content_file, callback=lambda audio_data: self.handle_audio_file(audio_data, message.content_detail)) + + # 处理会话结束 + if message.sentence_type == SentenceType.LAST: + try: + logger.bind(tag=TAG).info("开始结束TTS会话...") + asyncio.run_coroutine_threadsafe( + self.finish_session(self.conn.sentence_id), + loop=self.conn.loop, + ) + except Exception as e: + logger.bind(tag=TAG).error(f"结束TTS会话失败: {str(e)}") + continue + + except queue.Empty: + continue + except Exception as e: + logger.bind(tag=TAG).error( + f"处理TTS文本失败: {str(e)}, 类型: {type(e).__name__}, 堆栈: {traceback.format_exc()}" + ) + + async def text_to_speak(self, text, _): + """发送文本到TTS服务进行合成""" + try: + if self.ws is None: + logger.bind(tag=TAG).warning(f"WebSocket连接不存在,终止发送文本") + return + + filtered_text = MarkdownCleaner.clean_markdown(text) + + # 发送文本合成请求 + run_request = self._build_base_request(status=1,text=filtered_text) + await self.ws.send(json.dumps(run_request)) + return + + except Exception as e: + logger.bind(tag=TAG).error(f"发送TTS文本失败: {str(e)}") + if self.ws: + try: + await self.ws.close() + except: + pass + self.ws = None + raise + + async def start_session(self, session_id): + logger.bind(tag=TAG).info(f"开始会话~~{session_id}") + try: + # 会话开始时检测上个会话的监听状态 + if ( + self._monitor_task is not None + and isinstance(self._monitor_task, Task) + and not self._monitor_task.done() + ): + logger.bind(tag=TAG).info( + "检测到未完成的上个会话,关闭监听任务和连接..." + ) + await self.close() + + # 建立新连接 + await self._ensure_connection() + + # 启动监听任务 + self._monitor_task = asyncio.create_task(self._start_monitor_tts_response()) + + # 发送会话启动请求 + start_request = self._build_base_request(status=0) + + await self.ws.send(json.dumps(start_request)) + logger.bind(tag=TAG).info("会话启动请求已发送") + except Exception as e: + logger.bind(tag=TAG).error(f"启动会话失败: {str(e)}") + # 确保清理资源 + await self.close() + raise + + async def finish_session(self, session_id): + logger.bind(tag=TAG).info(f"关闭会话~~{session_id}") + try: + if self.ws: + # 发送会话结束请求 + stop_request = self._build_base_request(status=2) + await self.ws.send(json.dumps(stop_request)) + logger.bind(tag=TAG).info("会话结束请求已发送") + + if self._monitor_task: + try: + await self._monitor_task + except Exception as e: + logger.bind(tag=TAG).error(f"等待监听任务完成时发生错误: {str(e)}") + finally: + self._monitor_task = None + except Exception as e: + logger.bind(tag=TAG).error(f"关闭会话失败: {str(e)}") + await self.close() + raise + + async def close(self): + """资源清理""" + if self._monitor_task: + try: + self._monitor_task.cancel() + await self._monitor_task + except asyncio.CancelledError: + pass + except Exception as e: + logger.bind(tag=TAG).warning(f"关闭时取消监听任务错误: {e}") + self._monitor_task = None + + if self.ws: + try: + await self.ws.close() + except: + pass + self.ws = None + + async def _start_monitor_tts_response(self): + """监听TTS响应""" + try: + while not self.conn.stop_event.is_set(): + try: + msg = await self.ws.recv() + + # 检查客户端是否中止 + if self.conn.client_abort: + logger.bind(tag=TAG).info("收到打断信息,终止监听TTS响应") + break + + try: + data = json.loads(msg) + header = data.get("header", {}) + code = header.get("code") + + if code == 0: + payload = data.get("payload", {}) + audio_payload = payload.get("audio", {}) + + if audio_payload: + status = audio_payload.get("status", 0) + audio_data = audio_payload.get("audio", "") + if status == 0: + logger.bind(tag=TAG).debug("TTS合成已启动") + self.tts_audio_queue.put( + (SentenceType.FIRST, [], None) + ) + elif status == 2: + logger.bind(tag=TAG).debug("收到结束状态的音频数据,TTS合成完成") + self._process_before_stop_play_files() + break + else: + if self.conn.tts_MessageText: + logger.bind(tag=TAG).info( + f"句子语音生成成功: {self.conn.tts_MessageText}" + ) + self.tts_audio_queue.put( + (SentenceType.FIRST, [], self.conn.tts_MessageText) + ) + self.conn.tts_MessageText = None + try: + audio_bytes = base64.b64decode(audio_data) + self.opus_encoder.encode_pcm_to_opus_stream( + audio_bytes, False, self.handle_opus + ) + + except Exception as e: + logger.bind(tag=TAG).error(f"处理音频数据失败: {e}") + + else: + message = header.get("message", "未知错误") + logger.bind(tag=TAG).error(f"TTS合成错误: {code} - {message}") + break + + except json.JSONDecodeError: + logger.bind(tag=TAG).warning("收到无效的JSON消息") + + except websockets.ConnectionClosed: + logger.bind(tag=TAG).warning("WebSocket连接已关闭") + break + + except Exception as e: + logger.bind(tag=TAG).error( + f"处理TTS响应时出错: {e}\n{traceback.format_exc()}" + ) + break + + # 链接不可复用 + if self.ws: + try: + await self.ws.close() + except: + pass + self.ws = None + # 监听任务退出时清理引用 + finally: + self._monitor_task = None + + def to_tts(self, text: str) -> list: + """非流式TTS处理,用于测试及保存音频文件的场景""" + try: + # 创建新的事件循环 + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + # 存储音频数据 + audio_data = [] + + async def _generate_audio(): + # 生成认证URL + auth_url = XunfeiWSAuth.create_auth_url( + self.api_key, self.api_secret, self.api_url + ) + + # 建立WebSocket连接 + ws = await websockets.connect( + auth_url, + ping_interval=30, + ping_timeout=10, + close_timeout=10, + ) + + try: + filtered_text = MarkdownCleaner.clean_markdown(text) + + text_request = self._build_base_request(status=2,text=filtered_text) + + await ws.send(json.dumps(text_request)) + + task_finished = False + while not task_finished: + msg = await ws.recv() + + data = json.loads(msg) + header = data.get("header", {}) + code = header.get("code") + + if code == 0: + payload = data.get("payload", {}) + audio_payload = payload.get("audio", {}) + if audio_payload: + status = audio_payload.get("status", 0) + audio_base64 = audio_payload.get("audio", "") + if status == 1: + try: + audio_bytes = base64.b64decode(audio_base64) + self.opus_encoder.encode_pcm_to_opus_stream( + audio_bytes, + end_of_stream=False, + callback=lambda opus: audio_data.append(opus) + ) + except Exception as e: + logger.bind(tag=TAG).error(f"处理音频数据失败: {e}") + elif status == 2: + task_finished = True + logger.bind(tag=TAG).debug("TTS任务完成") + + else: + message = header.get("message", "未知错误") + raise Exception(f"合成失败: {code} - {message}") + + finally: + # 清理资源 + try: + await ws.close() + except: + pass + + loop.run_until_complete(_generate_audio()) + loop.close() + + return audio_data + except Exception as e: + logger.bind(tag=TAG).error(f"生成音频数据失败: {str(e)}") + return [] + + def _build_base_request(self, status,text=" "): + """构建基础请求结构""" + return { + "header": { + "app_id": self.app_id, + "status": status, + }, + "parameter": { + "oral": { + "oral_level": self.oral_level, + "spark_assist": self.spark_assist, + "stop_split": self.stop_split, + "remain": self.remain + }, + "tts": { + "vcn": self.voice, + "speed": self.speed, + "volume": self.volume, + "pitch": self.pitch, + "bgs": 0, + "reg": 0, + "rdn": 0, + "rhy": 0, + "audio": { + "encoding": self.format, + "sample_rate": self.sample_rate, + "channels": 1, + "bit_depth": 16, + "frame_size": 0 + } + } + }, + "payload": { + "text": { + "encoding": "utf8", + "compress": "raw", + "format": "plain", + "status": status, + "seq": self.text_seq, + "text": base64.b64encode(text.encode('utf-8')).decode('utf-8') + } + } + } From 966d8a8a8414a306a2b3ab13d999dcbbc585bca6 Mon Sep 17 00:00:00 2001 From: FAN-yeB <1442100690@qq.com> Date: Fri, 19 Sep 2025 17:54:52 +0800 Subject: [PATCH 02/27] update:xunfei asr --- .../resources/db/changelog/202509191732.sql | 26 ++ .../resources/db/changelog/202509191739.sql | 20 + .../db/changelog/db.changelog-master.yaml | 15 + main/manager-web/package-lock.json | 32 +- main/xiaozhi-server/config.yaml | 21 +- .../core/providers/asr/xunfei_stream.py | 442 ++++++++++++++++++ 6 files changed, 550 insertions(+), 6 deletions(-) create mode 100644 main/manager-api/src/main/resources/db/changelog/202509191732.sql create mode 100644 main/manager-api/src/main/resources/db/changelog/202509191739.sql create mode 100644 main/xiaozhi-server/core/providers/asr/xunfei_stream.py diff --git a/main/manager-api/src/main/resources/db/changelog/202509191732.sql b/main/manager-api/src/main/resources/db/changelog/202509191732.sql new file mode 100644 index 00000000..92e0ef0a --- /dev/null +++ b/main/manager-api/src/main/resources/db/changelog/202509191732.sql @@ -0,0 +1,26 @@ +-- 添加讯飞流式语音识别服务配置 +delete from `ai_model_provider` where id = 'SYSTEM_ASR_XunfeiStream'; +INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES +('SYSTEM_ASR_XunfeiStream', 'ASR', 'xunfei_stream', '讯飞流式语音识别', '[{"key":"app_id","label":"应用ID","type":"string"},{"key":"api_key","label":"API密钥","type":"password"},{"key":"api_secret","label":"API密钥","type":"password"},{"key":"domain","label":"识别领域","type":"string"},{"key":"language","label":"识别语言","type":"string"},{"key":"accent","label":"方言","type":"string"},{"key":"dwa","label":"动态修正","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"}]', 18, 1, NOW(), 1, NOW()); + +delete from `ai_model_config` where id = 'ASR_XunfeiStream'; +INSERT INTO `ai_model_config` VALUES ('ASR_XunfeiStream', 'ASR', '讯飞流式语音识别', '讯飞流式语音识别服务', 0, 1, '{"type": "xunfei_stream", "app_id": "", "api_key": "", "api_secret": "", "domain": "slm", "language": "zh_cn", "accent": "mandarin", "dwa": "wpgs", "output_dir": "tmp/"}', 'https://www.xfyun.cn/doc/spark/spark_zh_iat.html', '支持实时流式语音识别,适用于中文普通话及多种方言识别', 21, NULL, NULL, NULL, NULL); + +-- 更新讯飞流式语音识别模型配置的说明文档 +UPDATE `ai_model_config` SET +`doc_link` = 'https://www.xfyun.cn/doc/spark/spark_zh_iat.html', +`remark` = '讯飞流式语音识别配置说明: +1. 登录讯飞开放平台 https://www.xfyun.cn/ +2. 创建语音识别应用获取APPID、APISecret、APIKey +3. 参数说明: + - app_id: 应用ID,在讯飞开放平台创建应用后获得 + - api_key: API密钥,用于接口鉴权 + - api_secret: API密钥,用于生成签名 + - domain: 识别领域,默认slm(智能化语音转写) + - language: 识别语言,默认zh_cn(中文) + - accent: 方言类型,默认mandarin(普通话),支持cantonese(粤语)等 + - dwa: 动态修正,默认wpgs(开启动态修正) + - output_dir: 音频文件输出目录,默认tmp/ +4. 支持实时流式识别,适用于实时语音交互场景 +5. 支持多种方言和语言识别 +' WHERE `id` = 'ASR_XunfeiStream'; \ No newline at end of file diff --git a/main/manager-api/src/main/resources/db/changelog/202509191739.sql b/main/manager-api/src/main/resources/db/changelog/202509191739.sql new file mode 100644 index 00000000..94d267ea --- /dev/null +++ b/main/manager-api/src/main/resources/db/changelog/202509191739.sql @@ -0,0 +1,20 @@ +delete from `ai_model_config` where id = 'LLM_XunfeiSparkLLM'; +INSERT INTO `ai_model_config` VALUES ('LLM_XunfeiSparkLLM', 'LLM', '讯飞星火认知大模型', '讯飞星火认知大模型', 0, 1, '{"type": "openai", "model_name": "generalv3.5", "base_url": "https://spark-api-open.xf-yun.com/v1", "api_key": "你的api_key", "temperature": 0.5, "max_tokens": 2048, "top_p": 1.0, "frequency_penalty": 0.0}', 'https://www.xfyun.cn/doc/spark/HTTP%E8%B0%83%E7%94%A8%E6%96%87%E6%A1%A3.html', '讯飞星火认知大模型,支持多轮对话、文本生成等功能', 14, NULL, NULL, NULL, NULL); + +-- 更新讯飞星火认知大模型配置的说明文档 +UPDATE `ai_model_config` SET +`doc_link` = 'https://www.xfyun.cn/doc/spark/HTTP%E8%B0%83%E7%94%A8%E6%96%87%E6%A1%A3.html', +`remark` = '讯飞星火认知大模型配置说明: +1. 登录讯飞开放平台 https://www.xfyun.cn/ +2. 创建星火认知大模型应用获取API Key +3. 参数说明: + - api_key: API密钥,在讯飞开放平台创建应用后获得 + - model_name: 模型名称,支持generalv3.5、generalv3等版本 + - base_url: API地址,默认https://spark-api-open.xf-yun.com/v1 + - temperature: 温度参数,控制生成随机性,范围0-1,默认0.5 + - max_tokens: 最大输出token数,默认2048 + - top_p: 核心采样参数,控制词汇多样性,默认1.0 + - frequency_penalty: 频率惩罚,降低重复内容,默认0.0 +4. 支持多轮对话、文本生成、知识问答等功能 +5. 兼容OpenAI接口格式,可直接使用现有的OpenAI Provider +' WHERE `id` = 'LLM_XunfeiSparkLLM'; \ No newline at end of file diff --git a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml index c82ec415..b183980b 100755 --- a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml @@ -366,3 +366,18 @@ databaseChangeLog: - sqlFile: encoding: utf8 path: classpath:db/changelog/202509191545.sql + - changeSet: + id: 202509191732 + author: fyb + changes: + - sqlFile: + encoding: utf8 + path: classpath:db/changelog/202509191732.sql + + - changeSet: + id: 202509191739 + author: fyb + changes: + - sqlFile: + encoding: utf8 + path: classpath:db/changelog/202509191739.sql diff --git a/main/manager-web/package-lock.json b/main/manager-web/package-lock.json index c104b60c..afd543b1 100644 --- a/main/manager-web/package-lock.json +++ b/main/manager-web/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "core-js": "^3.41.0", "cross-env": "^7.0.3", + "dotenv": "^16.5.0", "element-ui": "^2.15.14", "flyio": "^0.6.14", "normalize.css": "^8.0.1", @@ -17,6 +18,7 @@ "opus-recorder": "^8.0.5", "vue": "^2.6.14", "vue-axios": "^3.5.2", + "vue-i18n": "^8.28.2", "vue-router": "^3.6.5", "vuex": "^3.6.2", "xiaozhi": "file:" @@ -2540,6 +2542,16 @@ } } }, + "node_modules/@vue/cli-service/node_modules/dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=10" + } + }, "node_modules/@vue/cli-shared-utils": { "version": "5.0.8", "resolved": "https://registry.npmmirror.com/@vue/cli-shared-utils/-/cli-shared-utils-5.0.8.tgz", @@ -4776,12 +4788,15 @@ } }, "node_modules/dotenv": { - "version": "10.0.0", - "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-10.0.0.tgz", - "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", - "dev": true, + "version": "16.6.1", + "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", "engines": { - "node": ">=10" + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" } }, "node_modules/dotenv-expand": { @@ -11131,6 +11146,13 @@ "integrity": "sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==", "dev": true }, + "node_modules/vue-i18n": { + "version": "8.28.2", + "resolved": "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-8.28.2.tgz", + "integrity": "sha512-C5GZjs1tYlAqjwymaaCPDjCyGo10ajUphiwA922jKt9n7KPpqR7oM1PCwYzhB/E7+nT3wfdG3oRre5raIT1rKA==", + "deprecated": "Vue I18n v8.x has reached EOL and is no longer actively maintained. About maintenance status, see https://vue-i18n.intlify.dev/guide/maintenance.html", + "license": "MIT" + }, "node_modules/vue-loader": { "version": "17.4.2", "resolved": "https://registry.npmmirror.com/vue-loader/-/vue-loader-17.4.2.tgz", diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index 832ca13e..836fc673 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -427,7 +427,26 @@ ASR: enable_itn: true # 逆文本归一化 #language: "zh" # 语种,支持zh、en、ja、ko等 context: "" # 上下文信息,用于提高识别准确率,不超过10000 Token - + XunfeiStreamASR: + # 讯飞流式语音识别服务 + # 需要先在讯飞开放平台创建应用,获取以下认证信息 + # 讯飞开放平台地址:https://www.xfyun.cn/ + # 创建应用后,在"我的应用"中获取: + # - APPID + # - APISecret + # - APIKey + type: xunfei_stream + # 必填参数 - 讯飞开放平台应用信息 + app_id: 你的APPID + api_key: 你的APIKey + api_secret: 你的APISecret + # 识别参数配置 + domain: slm # 识别领域,iat:日常用语,medical:医疗,finance:金融等 + language: zh_cn # 语言,zh_cn:中文,en_us:英文 + accent: mandarin # 方言,mandarin:普通话 + dwa: wpgs # 动态修正,wpgs:实时返回中间结果 + # 调整音频处理参数以提高长语音识别质量 + output_dir: tmp/ VAD: SileroVAD: diff --git a/main/xiaozhi-server/core/providers/asr/xunfei_stream.py b/main/xiaozhi-server/core/providers/asr/xunfei_stream.py new file mode 100644 index 00000000..7f696428 --- /dev/null +++ b/main/xiaozhi-server/core/providers/asr/xunfei_stream.py @@ -0,0 +1,442 @@ +import json +import hmac +import base64 +import hashlib +import asyncio +import websockets +import opuslib_next +from time import mktime +from datetime import datetime +from urllib.parse import urlencode +from typing import List +from config.logger import setup_logging +from wsgiref.handlers import format_date_time +from core.providers.asr.base import ASRProviderBase +from core.providers.asr.dto.dto import InterfaceType + +TAG = __name__ +logger = setup_logging() + +# 帧状态常量 +STATUS_FIRST_FRAME = 0 # 第一帧的标识 +STATUS_CONTINUE_FRAME = 1 # 中间帧标识 +STATUS_LAST_FRAME = 2 # 最后一帧的标识 + +class ASRProvider(ASRProviderBase): + def __init__(self, config, delete_audio_file): + super().__init__() + self.interface_type = InterfaceType.STREAM + self.config = config + self.text = "" + self.decoder = opuslib_next.Decoder(16000, 1) + self.asr_ws = None + self.forward_task = None + self.is_processing = False + self.server_ready = False + self.last_frame_sent = False # 标记是否已发送最终帧 + self.best_text = "" # 保存最佳识别结果 + + # 讯飞配置 + self.app_id = config.get("app_id") + self.api_key = config.get("api_key") + self.api_secret = config.get("api_secret") + + if not all([self.app_id, self.api_key, self.api_secret]): + raise ValueError("必须提供app_id、api_key和api_secret") + + # 识别参数 + self.iat_params = { + "domain": config.get("domain", "slm"), + "language": config.get("language", "zh_cn"), + "accent": config.get("accent", "mandarin"), + "dwa": config.get("dwa", "wpgs"), + "result": { + "encoding": "utf8", + "compress": "raw", + "format": "plain" + } + } + + self.output_dir = config.get("output_dir", "tmp/") + self.delete_audio_file = delete_audio_file + + def create_url(self) -> str: + """生成认证URL""" + url = 'ws://iat.cn-huabei-1.xf-yun.com/v1' + # 生成RFC1123格式的时间戳 + now = datetime.now() + date = format_date_time(mktime(now.timetuple())) + + # 拼接字符串 + signature_origin = "host: " + "iat.cn-huabei-1.xf-yun.com" + "\n" + signature_origin += "date: " + date + "\n" + signature_origin += "GET " + "/v1 " + "HTTP/1.1" + + # 进行hmac-sha256进行加密 + signature_sha = hmac.new(self.api_secret.encode('utf-8'), signature_origin.encode('utf-8'), + digestmod=hashlib.sha256).digest() + signature_sha = base64.b64encode(signature_sha).decode(encoding='utf-8') + + authorization_origin = "api_key=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"" % ( + self.api_key, "hmac-sha256", "host date request-line", signature_sha) + authorization = base64.b64encode(authorization_origin.encode('utf-8')).decode(encoding='utf-8') + + # 将请求的鉴权参数组合为字典 + v = { + "authorization": authorization, + "date": date, + "host": "iat.cn-huabei-1.xf-yun.com" + } + + # 拼接鉴权参数,生成url + url = url + '?' + urlencode(v) + return url + + async def open_audio_channels(self, conn): + await super().open_audio_channels(conn) + + async def receive_audio(self, conn, audio, audio_have_voice): + # 先调用父类方法处理基础逻辑 + await super().receive_audio(conn, audio, audio_have_voice) + + # 存储音频数据用于声纹识别 + if not hasattr(conn, 'asr_audio_for_voiceprint'): + conn.asr_audio_for_voiceprint = [] + conn.asr_audio_for_voiceprint.append(audio) + + # 如果本次有声音,且之前没有建立连接 + if audio_have_voice and self.asr_ws is None and not self.is_processing: + try: + await self._start_recognition(conn) + except Exception as e: + logger.bind(tag=TAG).error(f"建立ASR连接失败: {str(e)}") + await self._cleanup(conn) + return + + # 发送当前音频数据 + if self.asr_ws and self.is_processing and self.server_ready: + try: + pcm_frame = self.decoder.decode(audio, 960) + await self._send_audio_frame(pcm_frame, STATUS_CONTINUE_FRAME) + except Exception as e: + logger.bind(tag=TAG).warning(f"发送音频数据时发生错误: {e}") + await self._cleanup(conn) + + async def _start_recognition(self, conn): + """开始识别会话""" + try: + self.is_processing = True + # 建立WebSocket连接 + ws_url = self.create_url() + logger.bind(tag=TAG).info(f"正在连接ASR服务: {ws_url[:50]}...") + + self.asr_ws = await websockets.connect( + ws_url, + max_size=1000000000, + ping_interval=None, + ping_timeout=None, + close_timeout=10, + ) + + logger.bind(tag=TAG).info("ASR WebSocket连接已建立") + self.server_ready = False + self.last_frame_sent = False + self.best_text = "" + self.forward_task = asyncio.create_task(self._forward_results(conn)) + + # 发送首帧音频 + if conn.asr_audio and len(conn.asr_audio) > 0: + first_audio = conn.asr_audio[-1] if conn.asr_audio else b'' + pcm_frame = self.decoder.decode(first_audio, 960) if first_audio else b'' + await self._send_audio_frame(pcm_frame, STATUS_FIRST_FRAME) + self.server_ready = True + logger.bind(tag=TAG).info("已发送首帧,开始识别") + + # 发送缓存的音频数据 + for cached_audio in conn.asr_audio[-10:]: + try: + pcm_frame = self.decoder.decode(cached_audio, 960) + await self._send_audio_frame(pcm_frame, STATUS_CONTINUE_FRAME) + except Exception as e: + logger.bind(tag=TAG).info(f"发送缓存音频数据时发生错误: {e}") + break + + except Exception as e: + logger.bind(tag=TAG).error(f"建立ASR连接失败: {str(e)}") + if hasattr(e, "__cause__") and e.__cause__: + logger.bind(tag=TAG).error(f"错误原因: {str(e.__cause__)}") + if self.asr_ws: + await self.asr_ws.close() + self.asr_ws = None + self.is_processing = False + raise + + async def _send_audio_frame(self, audio_data: bytes, status: int): + """发送音频帧""" + if not self.asr_ws: + return + + audio_b64 = base64.b64encode(audio_data).decode('utf-8') + + frame_data = { + "header": { + "status": status, + "app_id": self.app_id + }, + "parameter": { + "iat": self.iat_params + }, + "payload": { + "audio": { + "audio": audio_b64, + "sample_rate": 16000, + "encoding": "raw" + } + } + } + + await self.asr_ws.send(json.dumps(frame_data, ensure_ascii=False)) + + # 标记是否发送了最终帧 + if status == STATUS_LAST_FRAME: + self.last_frame_sent = True + logger.bind(tag=TAG).info("标记最终帧已发送") + + async def _forward_results(self, conn): + """转发识别结果""" + try: + while self.asr_ws and not conn.stop_event.is_set(): + # 获取当前连接的音频数据 + audio_data = getattr(conn, 'asr_audio_for_voiceprint', []) + try: + # 如果已发送最终帧,增加超时时间等待完整结果 + timeout = 3.0 if self.last_frame_sent else 30.0 + response = await asyncio.wait_for(self.asr_ws.recv(), timeout=timeout) + result = json.loads(response) + logger.bind(tag=TAG).debug(f"收到ASR结果: {result}") + + header = result.get("header", {}) + payload = result.get("payload", {}) + code = header.get("code", 0) + status = header.get("status", 0) + + if code != 0: + logger.bind(tag=TAG).error(f"识别错误,错误码: {code}, 消息: {header.get('message', '')}") + if code in [10114, 10160]: # 连接问题 + break + continue + + # 处理识别结果 + if payload and "result" in payload: + text_data = payload["result"]["text"] + if text_data: + # 解码base64文本 + decoded_text = base64.b64decode(text_data).decode('utf-8') + text_json = json.loads(decoded_text) + + # 提取文本内容 + text_ws = text_json.get('ws', []) + result_text = '' + for i in text_ws: + for j in i.get("cw", []): + w = j.get("w", "") + result_text += w + + # 更新识别文本 - 实时更新策略 + if result_text and result_text.strip() not in ['', '。', '.', ',', ',']: + # 实时更新:正常情况下都更新,提高响应速度 + should_update = True + + # 保存最佳文本(最长的有意义文本) + if (len(result_text) > len(self.best_text) and + result_text.strip() not in ['?', '?', '。', '.']): + self.best_text = result_text + logger.bind(tag=TAG).debug(f"保存最佳文本: {self.best_text}") + + # 如果已发送最终帧,只过滤明显的无效结果 + if self.last_frame_sent: + # 最终帧后拒绝问号等明显错误的结果 + if result_text.strip() in ['?', '?', '。', '.']: + should_update = False + logger.bind(tag=TAG).warning(f"最终帧后拒绝无效文本: {result_text}") + + if should_update: + self.text = result_text + logger.bind(tag=TAG).info(f"实时更新识别文本: {self.text} (最终帧已发送: {self.last_frame_sent})") + + # 识别完成,但如果还没发送最终帧,继续等待 + if status == 2: + logger.bind(tag=TAG).info(f"识别完成状态已到达,当前识别文本: {self.text}") + + # 如果还没发送最终帧,继续等待 + if not self.last_frame_sent: + logger.bind(tag=TAG).info("识别完成但最终帧未发送,继续等待...") + continue + + # 已发送最终帧且收到完成状态,使用最佳文本作为最终结果 + if self.best_text and len(self.best_text) > len(self.text): + logger.bind(tag=TAG).info(f"使用最佳文本作为最终结果: {self.text} -> {self.best_text}") + self.text = self.best_text + + logger.bind(tag=TAG).info(f"获取到最终完整文本: {self.text}") + conn.reset_vad_states() + if len(audio_data) > 15: # 确保有足够音频数据 + # 准备处理结果 + pass + break + + except asyncio.TimeoutError: + if self.last_frame_sent: + # 超时时也使用最佳文本 + if self.best_text and len(self.best_text) > len(self.text): + logger.bind(tag=TAG).info(f"超时,使用最佳文本: {self.text} -> {self.best_text}") + self.text = self.best_text + logger.bind(tag=TAG).info(f"最终帧后超时,使用结果: {self.text}") + break + # 如果还没发送最终帧,继续等待 + continue + except websockets.ConnectionClosed: + logger.bind(tag=TAG).info("ASR服务连接已关闭") + self.is_processing = False + break + except Exception as e: + logger.bind(tag=TAG).error(f"处理ASR结果时发生错误: {str(e)}") + if hasattr(e, "__cause__") and e.__cause__: + logger.bind(tag=TAG).error(f"错误原因: {str(e.__cause__)}") + self.is_processing = False + break + + except Exception as e: + logger.bind(tag=TAG).error(f"ASR结果转发任务发生错误: {str(e)}") + if hasattr(e, "__cause__") and e.__cause__: + logger.bind(tag=TAG).error(f"错误原因: {str(e.__cause__)}") + finally: + if self.asr_ws: + await self.asr_ws.close() + self.asr_ws = None + self.is_processing = False + if conn: + if hasattr(conn, 'asr_audio_for_voiceprint'): + conn.asr_audio_for_voiceprint = [] + if hasattr(conn, 'asr_audio'): + conn.asr_audio = [] + if hasattr(conn, 'has_valid_voice'): + conn.has_valid_voice = False + + async def handle_voice_stop(self, conn, asr_audio_task: List[bytes]): + """处理语音停止,发送最后一帧并处理识别结果""" + try: + # 先发送最后一帧表示音频结束 + if self.asr_ws and self.is_processing: + try: + # 取最后一个有效的音频帧作为最后一帧数据 + last_frame = b'' + if asr_audio_task: + last_audio = asr_audio_task[-1] + last_frame = self.decoder.decode(last_audio, 960) + await self._send_audio_frame(last_frame, STATUS_LAST_FRAME) + logger.bind(tag=TAG).info("已发送最后一帧") + + # 发送最终帧后,给_forward_results适当时间处理最终结果 + # 减少等待时间,提高响应速度 + await asyncio.sleep(0.1) # 从3秒减少到1秒 + + logger.bind(tag=TAG).info(f"准备处理最终识别结果: {self.text}") + except Exception as e: + logger.bind(tag=TAG).error(f"发送最后一帧失败: {e}") + + # 调用父类的handle_voice_stop方法处理识别结果 + await super().handle_voice_stop(conn, asr_audio_task) + except Exception as e: + logger.bind(tag=TAG).error(f"处理语音停止失败: {e}") + import traceback + logger.bind(tag=TAG).debug(f"异常详情: {traceback.format_exc()}") + + def stop_ws_connection(self): + if self.asr_ws: + asyncio.create_task(self.asr_ws.close()) + self.asr_ws = None + self.is_processing = False + + async def _cleanup(self, conn): + """清理资源""" + logger.bind(tag=TAG).info(f"开始ASR会话清理 | 当前状态: processing={self.is_processing}, server_ready={self.server_ready}") + + # 发送最后一帧 + if self.asr_ws and self.is_processing: + try: + await self._send_audio_frame(b'', STATUS_LAST_FRAME) + await asyncio.sleep(0.1) + logger.bind(tag=TAG).info("已发送最后一帧") + except Exception as e: + logger.bind(tag=TAG).error(f"发送最后一帧失败: {e}") + + # 状态重置 + self.is_processing = False + self.server_ready = False + self.last_frame_sent = False + self.best_text = "" + logger.bind(tag=TAG).info("ASR状态已重置") + + # 清理任务 + if self.forward_task and not self.forward_task.done(): + self.forward_task.cancel() + try: + await asyncio.wait_for(self.forward_task, timeout=1.0) + except asyncio.CancelledError: + pass + except Exception as e: + logger.bind(tag=TAG).debug(f"forward_task取消异常: {e}") + finally: + self.forward_task = None + + # 关闭连接 + if self.asr_ws: + try: + logger.bind(tag=TAG).debug("正在关闭WebSocket连接") + await asyncio.wait_for(self.asr_ws.close(), timeout=2.0) + logger.bind(tag=TAG).debug("WebSocket连接已关闭") + except Exception as e: + logger.bind(tag=TAG).error(f"关闭WebSocket连接失败: {e}") + finally: + self.asr_ws = None + + # 清理连接的音频缓存 + if conn: + if hasattr(conn, 'asr_audio_for_voiceprint'): + conn.asr_audio_for_voiceprint = [] + if hasattr(conn, 'asr_audio'): + conn.asr_audio = [] + if hasattr(conn, 'has_valid_voice'): + conn.has_valid_voice = False + + logger.bind(tag=TAG).info("ASR会话清理完成") + + async def speech_to_text(self, opus_data, session_id, audio_format): + """获取识别结果""" + result = self.text + self.text = "" + return result, None + + async def close(self): + """资源清理方法""" + if self.asr_ws: + await self.asr_ws.close() + self.asr_ws = None + if self.forward_task: + self.forward_task.cancel() + try: + await self.forward_task + except asyncio.CancelledError: + pass + self.forward_task = None + self.is_processing = False + # 清理所有连接的音频缓冲区 + if hasattr(self, '_connections'): + for conn in self._connections.values(): + if hasattr(conn, 'asr_audio_for_voiceprint'): + conn.asr_audio_for_voiceprint = [] + if hasattr(conn, 'asr_audio'): + conn.asr_audio = [] + if hasattr(conn, 'has_valid_voice'): + conn.has_valid_voice = False \ No newline at end of file From 38e1eb0c181f3d9dec87a3c108b4c2d731371342 Mon Sep 17 00:00:00 2001 From: FAN-yeB <1442100690@qq.com> Date: Mon, 22 Sep 2025 09:25:43 +0800 Subject: [PATCH 03/27] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20202509191732.sql?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/db/changelog/202509191732.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/manager-api/src/main/resources/db/changelog/202509191732.sql b/main/manager-api/src/main/resources/db/changelog/202509191732.sql index 92e0ef0a..060fa4d9 100644 --- a/main/manager-api/src/main/resources/db/changelog/202509191732.sql +++ b/main/manager-api/src/main/resources/db/changelog/202509191732.sql @@ -1,7 +1,7 @@ -- 添加讯飞流式语音识别服务配置 delete from `ai_model_provider` where id = 'SYSTEM_ASR_XunfeiStream'; INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES -('SYSTEM_ASR_XunfeiStream', 'ASR', 'xunfei_stream', '讯飞流式语音识别', '[{"key":"app_id","label":"应用ID","type":"string"},{"key":"api_key","label":"API密钥","type":"password"},{"key":"api_secret","label":"API密钥","type":"password"},{"key":"domain","label":"识别领域","type":"string"},{"key":"language","label":"识别语言","type":"string"},{"key":"accent","label":"方言","type":"string"},{"key":"dwa","label":"动态修正","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"}]', 18, 1, NOW(), 1, NOW()); +('SYSTEM_ASR_XunfeiStream', 'ASR', 'xunfei_stream', '讯飞流式语音识别', '[{"key":"app_id","label":"应用ID","type":"string"},{"key":"api_key","label":"API_KEY","type":"password"},{"key":"api_secret","label":"API_SECRET","type":"password"},{"key":"domain","label":"识别领域","type":"string"},{"key":"language","label":"识别语言","type":"string"},{"key":"accent","label":"方言","type":"string"},{"key":"dwa","label":"动态修正","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"}]', 18, 1, NOW(), 1, NOW()); delete from `ai_model_config` where id = 'ASR_XunfeiStream'; INSERT INTO `ai_model_config` VALUES ('ASR_XunfeiStream', 'ASR', '讯飞流式语音识别', '讯飞流式语音识别服务', 0, 1, '{"type": "xunfei_stream", "app_id": "", "api_key": "", "api_secret": "", "domain": "slm", "language": "zh_cn", "accent": "mandarin", "dwa": "wpgs", "output_dir": "tmp/"}', 'https://www.xfyun.cn/doc/spark/spark_zh_iat.html', '支持实时流式语音识别,适用于中文普通话及多种方言识别', 21, NULL, NULL, NULL, NULL); From ec2b52ccac250c5c0d818e986cb67381cbeb90b7 Mon Sep 17 00:00:00 2001 From: FAN-yeB <1442100690@qq.com> Date: Mon, 22 Sep 2025 09:34:05 +0800 Subject: [PATCH 04/27] =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=AE=AF=E9=A3=9Easr?= =?UTF-8?q?=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{202509191732.sql => 202509220926.sql} | 0 .../db/changelog/db.changelog-master.yaml | 15 ++++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) rename main/manager-api/src/main/resources/db/changelog/{202509191732.sql => 202509220926.sql} (100%) diff --git a/main/manager-api/src/main/resources/db/changelog/202509191732.sql b/main/manager-api/src/main/resources/db/changelog/202509220926.sql similarity index 100% rename from main/manager-api/src/main/resources/db/changelog/202509191732.sql rename to main/manager-api/src/main/resources/db/changelog/202509220926.sql diff --git a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml index b183980b..e9f0243c 100755 --- a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml @@ -366,13 +366,6 @@ databaseChangeLog: - sqlFile: encoding: utf8 path: classpath:db/changelog/202509191545.sql - - changeSet: - id: 202509191732 - author: fyb - changes: - - sqlFile: - encoding: utf8 - path: classpath:db/changelog/202509191732.sql - changeSet: id: 202509191739 @@ -381,3 +374,11 @@ databaseChangeLog: - sqlFile: encoding: utf8 path: classpath:db/changelog/202509191739.sql + + - changeSet: + id: 202509220926 + author: fyb + changes: + - sqlFile: + encoding: utf8 + path: classpath:db/changelog/202509220926.sql From 646f4cbfe9652b70bdf41574e80ccf2ebc321433 Mon Sep 17 00:00:00 2001 From: FAN-yeB <1442100690@qq.com> Date: Mon, 22 Sep 2025 10:00:29 +0800 Subject: [PATCH 05/27] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E8=AE=AF=E9=A3=9E?= =?UTF-8?q?=E6=98=9F=E7=81=AB=E8=AE=A4=E7=9F=A5=E5=A4=A7=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E7=9A=84=E8=AF=B4=E6=98=8E=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{202509191739.sql => 202509220958.sql} | 9 ++++----- .../db/changelog/db.changelog-master.yaml | 15 ++++++++------- 2 files changed, 12 insertions(+), 12 deletions(-) rename main/manager-api/src/main/resources/db/changelog/{202509191739.sql => 202509220958.sql} (63%) diff --git a/main/manager-api/src/main/resources/db/changelog/202509191739.sql b/main/manager-api/src/main/resources/db/changelog/202509220958.sql similarity index 63% rename from main/manager-api/src/main/resources/db/changelog/202509191739.sql rename to main/manager-api/src/main/resources/db/changelog/202509220958.sql index 94d267ea..7b79dfd4 100644 --- a/main/manager-api/src/main/resources/db/changelog/202509191739.sql +++ b/main/manager-api/src/main/resources/db/changelog/202509220958.sql @@ -1,20 +1,19 @@ delete from `ai_model_config` where id = 'LLM_XunfeiSparkLLM'; -INSERT INTO `ai_model_config` VALUES ('LLM_XunfeiSparkLLM', 'LLM', '讯飞星火认知大模型', '讯飞星火认知大模型', 0, 1, '{"type": "openai", "model_name": "generalv3.5", "base_url": "https://spark-api-open.xf-yun.com/v1", "api_key": "你的api_key", "temperature": 0.5, "max_tokens": 2048, "top_p": 1.0, "frequency_penalty": 0.0}', 'https://www.xfyun.cn/doc/spark/HTTP%E8%B0%83%E7%94%A8%E6%96%87%E6%A1%A3.html', '讯飞星火认知大模型,支持多轮对话、文本生成等功能', 14, NULL, NULL, NULL, NULL); +INSERT INTO `ai_model_config` VALUES ('LLM_XunfeiSparkLLM', 'LLM', '讯飞星火认知大模型', '讯飞星火认知大模型', 0, 1, '{"type": "openai", "model_name": "generalv3.5", "base_url": "https://spark-api-open.xf-yun.com/v1", "api_password": "你的api_password", "temperature": 0.5, "max_tokens": 2048, "top_p": 1.0, "frequency_penalty": 0.0}', 'https://www.xfyun.cn/doc/spark/HTTP%E8%B0%83%E7%94%A8%E6%96%87%E6%A1%A3.html', '讯飞星火认知大模型,支持多轮对话、文本生成等功能', 14, NULL, NULL, NULL, NULL); -- 更新讯飞星火认知大模型配置的说明文档 UPDATE `ai_model_config` SET `doc_link` = 'https://www.xfyun.cn/doc/spark/HTTP%E8%B0%83%E7%94%A8%E6%96%87%E6%A1%A3.html', `remark` = '讯飞星火认知大模型配置说明: 1. 登录讯飞开放平台 https://www.xfyun.cn/ -2. 创建星火认知大模型应用获取API Key +2. 创建星火认知大模型应用获取API Password 3. 参数说明: - - api_key: API密钥,在讯飞开放平台创建应用后获得 + - api_password: API Password,在讯飞开放平台创建应用后获得 - model_name: 模型名称,支持generalv3.5、generalv3等版本 - base_url: API地址,默认https://spark-api-open.xf-yun.com/v1 - temperature: 温度参数,控制生成随机性,范围0-1,默认0.5 - max_tokens: 最大输出token数,默认2048 - top_p: 核心采样参数,控制词汇多样性,默认1.0 - frequency_penalty: 频率惩罚,降低重复内容,默认0.0 -4. 支持多轮对话、文本生成、知识问答等功能 -5. 兼容OpenAI接口格式,可直接使用现有的OpenAI Provider +4. 每一个模型对应每一个api_password,更改模型时需要查看对应模型的api_password。 ' WHERE `id` = 'LLM_XunfeiSparkLLM'; \ No newline at end of file diff --git a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml index e9f0243c..b5163d92 100755 --- a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml @@ -367,13 +367,6 @@ databaseChangeLog: encoding: utf8 path: classpath:db/changelog/202509191545.sql - - changeSet: - id: 202509191739 - author: fyb - changes: - - sqlFile: - encoding: utf8 - path: classpath:db/changelog/202509191739.sql - changeSet: id: 202509220926 @@ -382,3 +375,11 @@ databaseChangeLog: - sqlFile: encoding: utf8 path: classpath:db/changelog/202509220926.sql + + - changeSet: + id: 202509220958 + author: fyb + changes: + - sqlFile: + encoding: utf8 + path: classpath:db/changelog/202509220958.sql \ No newline at end of file From 9a6348c71a615ab8ff981489bf3a3b482e2149cd Mon Sep 17 00:00:00 2001 From: FAN-yeB <1442100690@qq.com> Date: Mon, 22 Sep 2025 10:01:30 +0800 Subject: [PATCH 06/27] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20202509220958.sql?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/db/changelog/202509220958.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/manager-api/src/main/resources/db/changelog/202509220958.sql b/main/manager-api/src/main/resources/db/changelog/202509220958.sql index 7b79dfd4..b05faa1c 100644 --- a/main/manager-api/src/main/resources/db/changelog/202509220958.sql +++ b/main/manager-api/src/main/resources/db/changelog/202509220958.sql @@ -5,7 +5,7 @@ INSERT INTO `ai_model_config` VALUES ('LLM_XunfeiSparkLLM', 'LLM', '讯飞星火 UPDATE `ai_model_config` SET `doc_link` = 'https://www.xfyun.cn/doc/spark/HTTP%E8%B0%83%E7%94%A8%E6%96%87%E6%A1%A3.html', `remark` = '讯飞星火认知大模型配置说明: -1. 登录讯飞开放平台 https://www.xfyun.cn/ +1. 登录讯飞开放平台 https://www.xfyun.cn/,每一个模型对应每一个api_password,更改模型时需要查看对应模型的api_password 2. 创建星火认知大模型应用获取API Password 3. 参数说明: - api_password: API Password,在讯飞开放平台创建应用后获得 From bf2b6d7554efebced93f3f6de6f55ec7be522a8d Mon Sep 17 00:00:00 2001 From: FAN-yeB <1442100690@qq.com> Date: Mon, 22 Sep 2025 10:10:46 +0800 Subject: [PATCH 07/27] =?UTF-8?q?=E8=BF=98=E5=8E=9F=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E6=9B=B4=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/manager-web/package-lock.json | 32 +++++------------------------- 1 file changed, 5 insertions(+), 27 deletions(-) diff --git a/main/manager-web/package-lock.json b/main/manager-web/package-lock.json index afd543b1..c104b60c 100644 --- a/main/manager-web/package-lock.json +++ b/main/manager-web/package-lock.json @@ -10,7 +10,6 @@ "dependencies": { "core-js": "^3.41.0", "cross-env": "^7.0.3", - "dotenv": "^16.5.0", "element-ui": "^2.15.14", "flyio": "^0.6.14", "normalize.css": "^8.0.1", @@ -18,7 +17,6 @@ "opus-recorder": "^8.0.5", "vue": "^2.6.14", "vue-axios": "^3.5.2", - "vue-i18n": "^8.28.2", "vue-router": "^3.6.5", "vuex": "^3.6.2", "xiaozhi": "file:" @@ -2542,16 +2540,6 @@ } } }, - "node_modules/@vue/cli-service/node_modules/dotenv": { - "version": "10.0.0", - "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-10.0.0.tgz", - "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=10" - } - }, "node_modules/@vue/cli-shared-utils": { "version": "5.0.8", "resolved": "https://registry.npmmirror.com/@vue/cli-shared-utils/-/cli-shared-utils-5.0.8.tgz", @@ -4788,15 +4776,12 @@ } }, "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "license": "BSD-2-Clause", + "version": "10.0.0", + "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", + "dev": true, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" + "node": ">=10" } }, "node_modules/dotenv-expand": { @@ -11146,13 +11131,6 @@ "integrity": "sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==", "dev": true }, - "node_modules/vue-i18n": { - "version": "8.28.2", - "resolved": "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-8.28.2.tgz", - "integrity": "sha512-C5GZjs1tYlAqjwymaaCPDjCyGo10ajUphiwA922jKt9n7KPpqR7oM1PCwYzhB/E7+nT3wfdG3oRre5raIT1rKA==", - "deprecated": "Vue I18n v8.x has reached EOL and is no longer actively maintained. About maintenance status, see https://vue-i18n.intlify.dev/guide/maintenance.html", - "license": "MIT" - }, "node_modules/vue-loader": { "version": "17.4.2", "resolved": "https://registry.npmmirror.com/vue-loader/-/vue-loader-17.4.2.tgz", From 559699ec1921f56941e6d143ca2c871e571dabb8 Mon Sep 17 00:00:00 2001 From: FAN-yeB <1442100690@qq.com> Date: Mon, 22 Sep 2025 14:18:03 +0800 Subject: [PATCH 08/27] =?UTF-8?q?update:=E5=A2=9E=E5=8A=A0=E5=8D=95?= =?UTF-8?q?=E6=A8=A1=E5=9D=97mqtt-gateway=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/config.yaml | 18 ++++ main/xiaozhi-server/core/api/ota_handler.py | 108 +++++++++++++++++++- 2 files changed, 123 insertions(+), 3 deletions(-) diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index a02de435..a946579c 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -41,6 +41,24 @@ server: # 可选:设备白名单,如果设置了白名单,那么白名单的机器无论是什么token都可以连接。 #allowed_devices: # - "24:0A:C4:1D:3B:F0" # MAC地址列表 + # MQTT网关配置,用于通过OTA下发到设备 + mqtt_gateway: + # MQTT服务器地址以及端口,根据mqtt_gateway的.env文件配置,为null或者不填写时使用websocket协议 + host: null + port: null + # group_id会从设备型号动态生成,格式为GID_设备型号 + # 此配置仅作为当获取设备型号失败时可以填写的回退值 + # group_id: + + # MQTT签名密钥,用于生成MQTT连接密码,根据mqtt_gateway的.env文件配置 + mqtt_signature_key: null + + # UDP网关配置,根据mqtt_gateway的.env文件配置 + udp_gateway: + # UDP服务器地址 + host: null + # UDP服务器端口 + port: null log: # 设置控制台输出的日志格式,时间、日志级别、标签、消息 log_format: "{time:YYMMDD HH:mm:ss}[{version}_{selected_module}][{extra[tag]}]-{level}-{message}" diff --git a/main/xiaozhi-server/core/api/ota_handler.py b/main/xiaozhi-server/core/api/ota_handler.py index 763c9c49..f24c3f8f 100644 --- a/main/xiaozhi-server/core/api/ota_handler.py +++ b/main/xiaozhi-server/core/api/ota_handler.py @@ -1,5 +1,8 @@ import json import time +import base64 +import hashlib +import hmac from aiohttp import web from core.utils.util import get_local_ip from core.api.base_handler import BaseHandler @@ -10,6 +13,24 @@ TAG = __name__ class OTAHandler(BaseHandler): def __init__(self, config: dict): super().__init__(config) + + def generate_password_signature(self, content: str, secret_key: str) -> str: + """生成MQTT密码签名 + + Args: + content: 签名内容 (clientId + '|' + username) + secret_key: 密钥 + + Returns: + str: Base64编码的HMAC-SHA256签名 + """ + try: + hmac_obj = hmac.new(secret_key.encode('utf-8'), content.encode('utf-8'), hashlib.sha256) + signature = hmac_obj.digest() + return base64.b64encode(signature).decode('utf-8') + except Exception as e: + self.logger.bind(tag=TAG).error(f"生成MQTT密码签名失败: {e}") + return "" def _get_websocket_url(self, local_ip: str, port: int) -> str: """获取websocket地址 @@ -58,10 +79,91 @@ class OTAHandler(BaseHandler): "version": data_json["application"].get("version", "1.0.0"), "url": "", }, - "websocket": { - "url": self._get_websocket_url(local_ip, port), - }, } + + mqtt_gateway_config = server_config.get("mqtt_gateway", {}) + mqtt_gateway_host = mqtt_gateway_config.get("host", "") + mqtt_gateway_port = mqtt_gateway_config.get("port", "") + + + if mqtt_gateway_host: # 配置了mqtt_gateway,使用MQTT和UDP协议传输 + # 从设备型号获取group_id,与manager-api保持一致 + # 客户端ID格式:groupId@@@macAddress@@@macAddress + # 尝试从请求数据中获取设备型号 + device_model = "default" + try: + # 假设设备型号在request数据的某个字段中 + if "device" in data_json and isinstance(data_json["device"], dict): + device_model = data_json["device"].get("model", "default") + elif "model" in data_json: + device_model = data_json["model"] + # 为了保证格式一致性,进行与manager-api相同的处理 + group_id = f"GID_{device_model}".replace(":", "_").replace(" ", "_") + except Exception as e: + self.logger.bind(tag=TAG).error(f"获取设备型号失败: {e}") + # 如果获取失败,使用配置文件中的默认值 + group_id = mqtt_gateway_config.get("group_id", "GID_default").replace(":", "_") + + mac_address_safe = device_id.replace(":", "_") + mqtt_client_id = f"{group_id}@@@{mac_address_safe}@@@{mac_address_safe}" + + # 构建用户数据(包含IP等信息) + user_data = { + "ip": "unknown" + } + + # 将用户数据编码为Base64 JSON + try: + user_data_json = json.dumps(user_data) + username = base64.b64encode(user_data_json.encode('utf-8')).decode('utf-8') + except Exception as e: + self.logger.bind(tag=TAG).error(f"生成用户名失败: {e}") + username = "" + + # 获取MQTT签名密钥 + password = "" + signature_key = server_config.get("mqtt_signature_key", "") + if signature_key: + # 使用签名密钥生成密码 + password = self.generate_password_signature(mqtt_client_id + "|" + username, signature_key) + if not password: + # 如果签名生成失败,使用配置文件中的密码 + password = mqtt_gateway_config.get("password", "") + else: + # 如果没有签名密钥,使用配置文件中的密码 + password = mqtt_gateway_config.get("password", "") + self.logger.bind(tag=TAG).warning("缺少MQTT签名密钥,使用配置文件中的密码") + + # 构建MQTT配置 + endpoint = f"{mqtt_gateway_host}:{mqtt_gateway_port}" + return_json["mqtt_gateway"] = { + "endpoint": endpoint, + "client_id": mqtt_client_id, + "username": username, + "password": password, + "publish_topic": "device-server", + "subscribe_topic": f"devices/p2p/{mac_address_safe}" + } + self.logger.bind(tag=TAG).info(f"为设备 {device_id} 下发MQTT网关配置") + + # 添加UDP网关配置 + if "udp_gateway" in server_config: + udp_config = server_config["udp_gateway"] + udp_host = udp_config.get("host", "") + udp_port = udp_config.get("port", "") + if udp_host: + return_json["udp_gateway"] = { + "host": udp_host, + "port": udp_port + } + self.logger.bind(tag=TAG).info(f"为设备 {device_id} 下发UDP网关配置") + + else: # 未配置mqtt_gateway,使用WebSocket协议传输 + return_json["websocket"] = { + "url": self._get_websocket_url(local_ip, port), + } + self.logger.bind(tag=TAG).info(f"未配置MQTT网关,为设备 {device_id} 下发WebSocket配置") + response = web.Response( text=json.dumps(return_json, separators=(",", ":")), content_type="application/json", From 41679f4e1d7aa81b9071cb523fd86156ba4b40fb Mon Sep 17 00:00:00 2001 From: rainv123 <2148537152@qq.com> Date: Mon, 22 Sep 2025 14:55:22 +0800 Subject: [PATCH 09/27] =?UTF-8?q?updata:=E8=B0=83=E6=95=B4=E6=96=87?= =?UTF-8?q?=E6=9C=AC=E9=A1=BA=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/manager-web/src/i18n/en.js | 94 ++++++------- main/manager-web/src/i18n/zh_CN.js | 93 ++++++------- main/manager-web/src/i18n/zh_TW.js | 210 ++++++++++++++--------------- 3 files changed, 199 insertions(+), 198 deletions(-) diff --git a/main/manager-web/src/i18n/en.js b/main/manager-web/src/i18n/en.js index 14f4d5a6..5e00c9b0 100644 --- a/main/manager-web/src/i18n/en.js +++ b/main/manager-web/src/i18n/en.js @@ -7,7 +7,6 @@ export default { 'login.loginSuccess': 'Login successful!', // HeaderBar组件文本 - // 在header相关翻译中添加 'header.smartManagement': 'Agents', 'header.modelConfig': 'Models', 'header.userManagement': 'Users', @@ -588,43 +587,6 @@ export default { 'user.info': 'User Information', 'user.username': 'Username', 'user.mobile': 'Mobile Phone', - // Provider Management Page Text - 'providerManagement.categoryFilter': 'Category Filter', - 'providerManagement.searchPlaceholder': 'Please enter provider name to search', - 'providerManagement.category': 'Category', - 'providerManagement.providerCode': 'Provider Code', - 'providerManagement.fieldConfig': 'Field Configuration', - 'providerManagement.selectToDelete': 'Please select providers to delete first', - 'providerManagement.confirmDelete': 'Are you sure to delete the selected {count} providers?', - 'providerManagement.viewFields': 'View Fields', - // Common Text - 'common.all': 'All', - 'common.search': 'Search', - 'common.name': 'Name', - 'common.sort': 'Sort', - 'common.action': 'Action', - 'common.edit': 'Edit', - 'common.delete': 'Delete', - 'common.selectAll': 'Select All', - 'common.deselectAll': 'Deselect All', - 'common.add': 'Add', - 'common.perPage': '{number}/page', - 'common.firstPage': 'First Page', - 'common.prevPage': 'Previous Page', - 'common.nextPage': 'Next Page', - 'common.totalRecords': 'Total {number} records', - 'common.addProvider': 'Add Provider', - 'common.editProvider': 'Edit Provider', - 'common.updateSuccess': 'Update Success', - 'common.addSuccess': 'Add Success', - 'common.deleteSuccess': 'Delete Success', - 'common.deleteFailure': 'Delete Failed, Please Try Again', - 'common.deleteCancelled': 'Delete Cancelled', - 'common.warning': 'Warning', - 'common.tip': 'Tip', - 'common.confirm': 'Confirm', - 'common.cancel': 'Cancel', - 'common.sensitive': 'Sensitive', 'user.userid': 'User ID', 'user.deviceCount': 'Device Count', 'user.createDate': 'Registration Time', @@ -659,6 +621,45 @@ export default { 'user.searchPhone': 'Please enter mobile phone number to search', 'user.search': 'Search', + // Provider Management Page Text + 'providerManagement.categoryFilter': 'Category Filter', + 'providerManagement.searchPlaceholder': 'Please enter provider name to search', + 'providerManagement.category': 'Category', + 'providerManagement.providerCode': 'Provider Code', + 'providerManagement.fieldConfig': 'Field Configuration', + 'providerManagement.selectToDelete': 'Please select providers to delete first', + 'providerManagement.confirmDelete': 'Are you sure to delete the selected {count} providers?', + 'providerManagement.viewFields': 'View Fields', + + // Common Text + 'common.all': 'All', + 'common.search': 'Search', + 'common.name': 'Name', + 'common.sort': 'Sort', + 'common.action': 'Action', + 'common.edit': 'Edit', + 'common.delete': 'Delete', + 'common.selectAll': 'Select All', + 'common.deselectAll': 'Deselect All', + 'common.add': 'Add', + 'common.perPage': '{number}/page', + 'common.firstPage': 'First Page', + 'common.prevPage': 'Previous Page', + 'common.nextPage': 'Next Page', + 'common.totalRecords': 'Total {number} records', + 'common.addProvider': 'Add Provider', + 'common.editProvider': 'Edit Provider', + 'common.updateSuccess': 'Update Success', + 'common.addSuccess': 'Add Success', + 'common.deleteSuccess': 'Delete Success', + 'common.deleteFailure': 'Delete Failed, Please Try Again', + 'common.deleteCancelled': 'Delete Cancelled', + 'common.warning': 'Warning', + 'common.tip': 'Tip', + 'common.confirm': 'Confirm', + 'common.cancel': 'Cancel', + 'common.sensitive': 'Sensitive', + // Language switch 'language.zhCN': '中文简体', 'language.zhTW': '中文繁體', @@ -675,7 +676,14 @@ export default { 'home.deviceManagement': 'Devices', 'home.chatHistory': 'Chat History', 'home.lastConversation': 'Last Conversation', - 'home.noConversation': 'No conversation yet', + 'home.noConversation': 'No conversation', + 'home.justNow': 'Just now', + 'home.minutesAgo': '{minutes} minutes ago', + 'home.hoursAgo': '{hours} hours {minutes} minutes ago', + 'home.confirmDeleteAgent': 'Are you sure you want to delete this agent?', + 'home.deleteSuccess': 'Delete successful', + 'home.deleteFailed': 'Delete failed', + 'home.enableMemory': 'Please enable memory in the "Config Role" page first', // Parameter management page text 'paramManagement.pageTitle': 'Parameter Management', @@ -711,14 +719,6 @@ export default { 'paramManagement.addSuccess': 'Add successful', 'paramManagement.updateFailed': 'Update failed', 'paramManagement.addFailed': 'Add failed', - 'home.noConversation': 'No conversation', - 'home.justNow': 'Just now', - 'home.minutesAgo': '{minutes} minutes ago', - 'home.hoursAgo': '{hours} hours {minutes} minutes ago', - 'home.confirmDeleteAgent': 'Are you sure you want to delete this agent?', - 'home.deleteSuccess': 'Delete successful', - 'home.deleteFailed': 'Delete failed', - 'home.enableMemory': 'Please enable memory in the "Config Role" page first', // Server side management page text 'serverSideManager.pageTitle': 'Server Management', diff --git a/main/manager-web/src/i18n/zh_CN.js b/main/manager-web/src/i18n/zh_CN.js index 27b0b9e0..d71d51b1 100644 --- a/main/manager-web/src/i18n/zh_CN.js +++ b/main/manager-web/src/i18n/zh_CN.js @@ -444,7 +444,6 @@ export default { 'device.input6DigitCode': '请输入6位数字验证码', 'device.bindSuccess': '设备绑定成功', 'device.bindFailed': '绑定失败', - // DeviceManagement页面扩展翻译 'device.searchPlaceholder': '请输入设备型号或Mac地址查询', 'device.model': '设备型号', 'device.macAddress': 'Mac地址', @@ -559,7 +558,7 @@ export default { 'dictManagement.dictValue': '字典值', 'dictManagement.sort': '排序', 'dictManagement.delete': '删除', - 'dictManagement.selectAll': '默认角色管理', + 'dictManagement.selectAll': '全选', 'dictManagement.deselectAll': '取消全选', 'dictManagement.addDictType': '新增字典类型', 'dictManagement.batchDeleteDictType': '批量删除字典类型', @@ -588,43 +587,6 @@ export default { 'user.info': '用户信息', 'user.username': '用户名', 'user.mobile': '手机号', - // 字段管理页面文本 - 'providerManagement.categoryFilter': '类别筛选', - 'providerManagement.searchPlaceholder': '请输入供应器名称查询', - 'providerManagement.category': '类别', - 'providerManagement.providerCode': '供应器编码', - 'providerManagement.fieldConfig': '字段配置', - 'providerManagement.selectToDelete': '请先选择需要删除的供应器', - 'providerManagement.confirmDelete': '确定要删除选中的{count}个供应器吗?', - 'providerManagement.viewFields': '查看字段', - // 公共文本 - 'common.all': '全部', - 'common.search': '搜索', - 'common.name': '名称', - 'common.sort': '排序', - 'common.action': '操作', - 'common.edit': '编辑', - 'common.delete': '删除', - 'common.selectAll': '全选', - 'common.deselectAll': '取消全选', - 'common.add': '新增', - 'common.perPage': '{number}条/页', - 'common.firstPage': '首页', - 'common.prevPage': '上一页', - 'common.nextPage': '下一页', - 'common.totalRecords': '共{number}条记录', - 'common.addProvider': '新增供应器', - 'common.editProvider': '编辑供应器', - 'common.updateSuccess': '修改成功', - 'common.addSuccess': '新增成功', - 'common.deleteSuccess': '删除成功', - 'common.deleteFailure': '删除失败,请重试', - 'common.deleteCancelled': '已取消删除', - 'common.warning': '警告', - 'common.tip': '提示', - 'common.confirm': '确定', - 'common.cancel': '取消', - 'common.sensitive': '敏感', 'user.userid': '用户Id', 'user.deviceCount': '设备数量', 'user.createDate': '注册时间', @@ -659,6 +621,45 @@ export default { 'user.searchPhone': '请输入手机号码查询', 'user.search': '搜索', + // 字段管理页面文本 + 'providerManagement.categoryFilter': '类别筛选', + 'providerManagement.searchPlaceholder': '请输入供应器名称查询', + 'providerManagement.category': '类别', + 'providerManagement.providerCode': '供应器编码', + 'providerManagement.fieldConfig': '字段配置', + 'providerManagement.selectToDelete': '请先选择需要删除的供应器', + 'providerManagement.confirmDelete': '确定要删除选中的{count}个供应器吗?', + 'providerManagement.viewFields': '查看字段', + + // 公共文本 + 'common.all': '全部', + 'common.search': '搜索', + 'common.name': '名称', + 'common.sort': '排序', + 'common.action': '操作', + 'common.edit': '编辑', + 'common.delete': '删除', + 'common.selectAll': '全选', + 'common.deselectAll': '取消全选', + 'common.add': '新增', + 'common.perPage': '{number}条/页', + 'common.firstPage': '首页', + 'common.prevPage': '上一页', + 'common.nextPage': '下一页', + 'common.totalRecords': '共{number}条记录', + 'common.addProvider': '新增供应器', + 'common.editProvider': '编辑供应器', + 'common.updateSuccess': '修改成功', + 'common.addSuccess': '新增成功', + 'common.deleteSuccess': '删除成功', + 'common.deleteFailure': '删除失败,请重试', + 'common.deleteCancelled': '已取消删除', + 'common.warning': '警告', + 'common.tip': '提示', + 'common.confirm': '确定', + 'common.cancel': '取消', + 'common.sensitive': '敏感', + // 语言切换 'language.zhCN': '中文简体', 'language.zhTW': '中文繁體', @@ -676,6 +677,13 @@ export default { 'home.chatHistory': '聊天记录', 'home.lastConversation': '最近对话', 'home.noConversation': '暂未对话', + 'home.justNow': '刚刚', + 'home.minutesAgo': '{minutes}分钟前', + 'home.hoursAgo': '{hours}小时{minutes}分钟前', + 'home.confirmDeleteAgent': '确定要删除该智能体吗?', + 'home.deleteSuccess': '删除成功', + 'home.deleteFailed': '删除失败', + 'home.enableMemory': '请先在“配置角色”界面开启记忆', // 参数管理页面文本 'paramManagement.pageTitle': '参数管理', @@ -711,13 +719,6 @@ export default { 'paramManagement.addSuccess': '新增成功', 'paramManagement.updateFailed': '更新失败', 'paramManagement.addFailed': '新增失败', - 'home.justNow': '刚刚', - 'home.minutesAgo': '{minutes}分钟前', - 'home.hoursAgo': '{hours}小时{minutes}分钟前', - 'home.confirmDeleteAgent': '确定要删除该智能体吗?', - 'home.deleteSuccess': '删除成功', - 'home.deleteFailed': '删除失败', - 'home.enableMemory': '请先在“配置角色”界面开启记忆', // 服务端管理页面文本 'serverSideManager.pageTitle': '服务端管理', diff --git a/main/manager-web/src/i18n/zh_TW.js b/main/manager-web/src/i18n/zh_TW.js index a9f224f1..173ebbd8 100644 --- a/main/manager-web/src/i18n/zh_TW.js +++ b/main/manager-web/src/i18n/zh_TW.js @@ -7,7 +7,6 @@ export default { 'login.loginSuccess': '登錄成功!', // HeaderBar组件文本 - // 在header相关翻译中添加 'header.smartManagement': '智能體管理', 'header.modelConfig': '模型配置', 'header.userManagement': '用戶管理', @@ -548,6 +547,42 @@ export default { 'voiceprint.add': '添加聲紋', 'voiceprint.delete': '刪除聲紋', + // 字典管理頁面文本 + 'dictManagement.pageTitle': '字典管理', + 'dictManagement.searchPlaceholder': '請輸入字典值標籤查詢', + 'dictManagement.search': '搜索', + 'dictManagement.dictTypeName': '字典類型名稱', + 'dictManagement.operation': '操作', + 'dictManagement.edit': '編輯', + 'dictManagement.dictLabel': '字典標籤', + 'dictManagement.dictValue': '字典值', + 'dictManagement.sort': '排序', + 'dictManagement.delete': '刪除', + 'dictManagement.selectAll': '全選', + 'dictManagement.deselectAll': '取消全選', + 'dictManagement.addDictType': '新增字典類型', + 'dictManagement.batchDeleteDictType': '批量刪除字典類型', + 'dictManagement.addDictData': '新增字典數據', + 'dictManagement.batchDeleteDictData': '批量刪除字典數據', + 'dictManagement.itemsPerPage': '{items}條/頁', + 'dictManagement.firstPage': '首頁', + 'dictManagement.prevPage': '上一頁', + 'dictManagement.nextPage': '下一頁', + 'dictManagement.totalRecords': '共{total}條記錄', + 'dictManagement.editDictType': '編輯字典類型', + 'dictManagement.editDictData': '編輯字典數據', + 'dictManagement.saveSuccess': '保存成功', + 'dictManagement.deleteSuccess': '刪除成功', + 'dictManagement.selectDictTypeToDelete': '請選擇要刪除的字典類型', + 'dictManagement.confirmDeleteDictType': '確定要刪除選中的字典類型嗎?', + 'dictManagement.confirm': '確定', + 'dictManagement.cancel': '取消', + 'dictManagement.selectDictTypeFirst': '請先選擇字典類型', + 'dictManagement.confirmDeleteDictData': '確定要刪除該字典數據嗎?', + 'dictManagement.selectDictDataToDelete': '請選擇要刪除的字典數據', + 'dictManagement.confirmBatchDeleteDictData': '確定要刪除選中的{count}個字典數據嗎?', + 'dictManagement.getDictDataFailed': '獲取字典數據失敗', + // 用户信息 'user.info': '用戶信息', 'user.username': '用戶名', @@ -586,44 +621,6 @@ export default { 'user.searchPhone': '請輸入手機號碼查詢', 'user.search': '搜索', - // 語言切換 - 'language.zhCN': '中文简体', - 'language.zhTW': '中文繁體', - 'language.en': 'English', - - // 首頁文本 - 'home.addAgent': '添加智能體', - 'home.greeting': '你好小智', - 'home.wish': '讓我們度過美好的一天!', - 'home.languageModel': '語言模型', - 'home.voiceModel': '音色模型', - 'home.configureRole': '配置角色', - 'home.voiceprintRecognition': '聲紋識別', - 'home.deviceManagement': '設備管理', - 'home.chatHistory': '聊天記錄', - 'home.lastConversation': '最近對話', - 'home.noConversation': '暫未對話', - - // 參數管理頁面文本 - 'paramManagement.pageTitle': '參數管理', - 'paramManagement.searchPlaceholder': '請輸入參數編碼或備註查詢', - 'paramManagement.search': '搜索', - 'paramManagement.paramCode': '參數編碼', - 'paramManagement.paramValue': '參數值', - 'paramManagement.remark': '備註', - 'paramManagement.operation': '操作', - 'paramManagement.hide': '隱藏', - 'paramManagement.view': '查看', - 'paramManagement.deselectAll': '取消全選', - 'paramManagement.selectAll': '全選', - 'paramManagement.add': '新增', - 'paramManagement.delete': '刪除', - 'paramManagement.edit': '編輯', - 'paramManagement.itemsPerPage': '條/頁', - 'paramManagement.firstPage': '首頁', - 'paramManagement.prevPage': '上一頁', - 'paramManagement.nextPage': '下一頁', - // 字段管理頁面文本 'providerManagement.categoryFilter': '類別篩選', 'providerManagement.searchPlaceholder': '請輸入供應器名稱查詢', @@ -633,6 +630,7 @@ export default { 'providerManagement.selectToDelete': '請先選擇需要刪除的供應器', 'providerManagement.confirmDelete': '確定要刪除選中的{count}個供應器嗎?', 'providerManagement.viewFields': '查看字段', + // 公共文本 'common.all': '全部', 'common.search': '搜索', @@ -661,6 +659,51 @@ export default { 'common.confirm': '確定', 'common.cancel': '取消', 'common.sensitive': '敏感', + + // 語言切換 + 'language.zhCN': '中文简体', + 'language.zhTW': '中文繁體', + 'language.en': 'English', + + // 首頁文本 + 'home.addAgent': '添加智能體', + 'home.greeting': '你好小智', + 'home.wish': '讓我們度過美好的一天!', + 'home.languageModel': '語言模型', + 'home.voiceModel': '音色模型', + 'home.configureRole': '配置角色', + 'home.voiceprintRecognition': '聲紋識別', + 'home.deviceManagement': '設備管理', + 'home.chatHistory': '聊天記錄', + 'home.lastConversation': '最近對話', + 'home.noConversation': '暫未對話', + 'home.justNow': '剛剛', + 'home.minutesAgo': '{minutes}分鐘前', + 'home.hoursAgo': '{hours}小時{minutes}分鐘前', + 'home.confirmDeleteAgent': '確定要刪除該智能體嗎?', + 'home.deleteSuccess': '刪除成功', + 'home.deleteFailed': '刪除失敗', + 'home.enableMemory': '請先在「配置角色」介面開啟記憶', + + // 參數管理頁面文本 + 'paramManagement.pageTitle': '參數管理', + 'paramManagement.searchPlaceholder': '請輸入參數編碼或備註查詢', + 'paramManagement.search': '搜索', + 'paramManagement.paramCode': '參數編碼', + 'paramManagement.paramValue': '參數值', + 'paramManagement.remark': '備註', + 'paramManagement.operation': '操作', + 'paramManagement.hide': '隱藏', + 'paramManagement.view': '查看', + 'paramManagement.deselectAll': '取消全選', + 'paramManagement.selectAll': '全選', + 'paramManagement.add': '新增', + 'paramManagement.delete': '刪除', + 'paramManagement.edit': '編輯', + 'paramManagement.itemsPerPage': '條/頁', + 'paramManagement.firstPage': '首頁', + 'paramManagement.prevPage': '上一頁', + 'paramManagement.nextPage': '下一頁', 'paramManagement.totalRecords': '共{total}條記錄', 'paramManagement.addParam': '新增參數', 'paramManagement.editParam': '編輯參數', @@ -676,49 +719,6 @@ export default { 'paramManagement.addSuccess': '新增成功', 'paramManagement.updateFailed': '更新失敗', 'paramManagement.addFailed': '新增失敗', - 'home.justNow': '剛剛', - 'home.minutesAgo': '{minutes}分鐘前', - 'home.hoursAgo': '{hours}小時{minutes}分鐘前', - 'home.confirmDeleteAgent': '確定要刪除該智能體嗎?', - 'home.deleteSuccess': '刪除成功', - 'home.deleteFailed': '刪除失敗', - 'home.enableMemory': '請先在「配置角色」介面開啟記憶', - - // 字典管理頁面文本 - 'dictManagement.pageTitle': '字典管理', - 'dictManagement.searchPlaceholder': '請輸入字典名稱搜索', - 'dictManagement.search': '搜索', - 'dictManagement.addDictType': '新增字典類型', - 'dictManagement.batchDeleteDictType': '批量刪除字典類型', - 'dictManagement.dictName': '字典名稱', - 'dictManagement.dictCode': '字典代碼', - 'dictManagement.description': '描述', - 'dictManagement.operation': '操作', - 'dictManagement.edit': '修改', - 'dictManagement.delete': '刪除', - 'dictManagement.dictLabel': '字典標籤', - 'dictManagement.dictValue': '字典值', - 'dictManagement.sortOrder': '排序號', - 'dictManagement.status': '狀態', - 'dictManagement.itemsPerPage': '{items}條/頁', - 'dictManagement.firstPage': '首頁', - 'dictManagement.prevPage': '上一頁', - 'dictManagement.nextPage': '下一頁', - 'dictManagement.totalRecords': '共{total}條記錄', - 'dictManagement.addDictData': '新增字典數據', - 'dictManagement.editDictType': '修改字典類型', - 'dictManagement.confirmDelete': '確定要刪除此字典類型嗎?', - 'dictManagement.confirmBatchDelete': '確定要刪除選中的{count}個字典類型嗎?', - 'dictManagement.selectFirst': '請先選擇需要刪除的字典類型', - 'dictManagement.deleteSuccess': '刪除成功', - 'dictManagement.deleteFailed': '刪除失敗', - 'dictManagement.addSuccess': '新增成功', - 'dictManagement.editSuccess': '修改成功', - 'dictManagement.getDictListFailed': '獲取字典列表失敗', - 'dictManagement.getDictDataListFailed': '獲取字典數據列表失敗', - 'dictManagement.saveFailed': '保存失敗', - 'dictManagement.loading': '拼命加載中', - 'dictManagement.select': '選擇', // 服務端管理頁面文本 'serverSideManager.pageTitle': '服務端管理', @@ -1003,29 +1003,30 @@ export default { 'providerDialog.batchDeleteFieldsSuccess': '成功刪除{count}個字段', // 預設角色管理頁面文本 - 'agentTemplateManagement.title': '預設角色管理', - 'agentTemplateManagement.templateName': '模板名稱', - 'agentTemplateManagement.action': '操作', - 'agentTemplateManagement.createTemplate': '建立模板', - 'agentTemplateManagement.editTemplate': '編輯模板', - 'agentTemplateManagement.deleteTemplate': '刪除模板', - 'agentTemplateManagement.deleteSuccess': '模板刪除成功', - 'agentTemplateManagement.batchDelete': '批次刪除', - 'agentTemplateManagement.batchDeleteSuccess': '批次刪除成功', - 'agentTemplateManagement.selectTemplate': '請選擇模板', - 'agentTemplateManagement.select': '選擇', - 'agentTemplateManagement.searchPlaceholder': '請輸入模板名稱搜尋', - 'agentTemplateManagement.search': '搜尋', - 'agentTemplateManagement.serialNumber': '序號', - 'agentTemplateManagement.selectAll': '全選', - 'agentTemplateManagement.deselectAll': '取消全選', - 'agentTemplateManagement.loading': '拼命加載中', - 'agentTemplateManagement.confirmSingleDelete': '確定要刪除這個模板嗎?', - 'agentTemplateManagement.confirmBatchDelete': '確定要刪除選中的 {count} 個模板嗎?', - 'agentTemplateManagement.deleteFailed': '模板刪除失敗', - 'agentTemplateManagement.batchDeleteFailed': '批次刪除失敗', + 'agentTemplateManagement.title': '預設角色管理', + 'agentTemplateManagement.templateName': '模板名稱', + 'agentTemplateManagement.action': '操作', + 'templateQuickConfig.saveSuccess': '配置保存成功', + 'templateQuickConfig.saveFailed': '配置保存失敗', + 'agentTemplateManagement.createTemplate': '建立模板', + 'agentTemplateManagement.editTemplate': '編輯模板', + 'agentTemplateManagement.deleteTemplate': '刪除模板', + 'agentTemplateManagement.deleteSuccess': '模板刪除成功', + 'agentTemplateManagement.batchDelete': '批次刪除', + 'agentTemplateManagement.batchDeleteSuccess': '批次刪除成功', + 'agentTemplateManagement.selectTemplate': '請選擇模板', + 'agentTemplateManagement.select': '選擇', + 'agentTemplateManagement.searchPlaceholder': '請輸入模板名稱搜尋', + 'agentTemplateManagement.search': '搜尋', + 'agentTemplateManagement.serialNumber': '序號', + 'agentTemplateManagement.selectAll': '全選', + 'agentTemplateManagement.deselectAll': '取消全選', + 'agentTemplateManagement.loading': '拼命加載中', + 'agentTemplateManagement.confirmSingleDelete': '確定要刪除這個模板嗎?', + 'agentTemplateManagement.confirmBatchDelete': '確定要刪除選中的 {count} 個模板嗎?', + 'agentTemplateManagement.deleteFailed': '模板刪除失敗', + 'agentTemplateManagement.batchDeleteFailed': '模板批次刪除失敗', 'agentTemplateManagement.deleteBackendError': '刪除失敗,請檢查後端服務是否正常', - 'agentTemplateManagement.deleteCancelled': '已取消刪除', // 模板快速配置 'templateQuickConfig.title': '模組快速設定', @@ -1042,7 +1043,6 @@ export default { 'templateQuickConfig.cancel': '取消', 'templateQuickConfig.templateNotFound': '未找到指定模板', 'templateQuickConfig.newTemplate': '新模板', - 'error': '錯誤', 'warning': '警告', 'info': '提示', 'common.networkError': '網路請求失敗' From 4608c81ea63c080855c6eef8b2e3c65a776c0e8b Mon Sep 17 00:00:00 2001 From: rainv123 <2148537152@qq.com> Date: Mon, 22 Sep 2025 15:04:49 +0800 Subject: [PATCH 10/27] =?UTF-8?q?updata=EF=BC=9A=E8=A1=A5=E5=85=85?= =?UTF-8?q?=E5=9B=BD=E9=99=85=E5=8C=96=E9=94=AE=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/xiaozhi/common/exception/ErrorCode.java | 2 ++ .../modules/model/service/impl/ModelConfigServiceImpl.java | 4 ++-- main/manager-api/src/main/resources/i18n/messages.properties | 3 ++- .../src/main/resources/i18n/messages_en_US.properties | 3 ++- .../src/main/resources/i18n/messages_zh_CN.properties | 3 ++- .../src/main/resources/i18n/messages_zh_TW.properties | 3 ++- 6 files changed, 12 insertions(+), 6 deletions(-) diff --git a/main/manager-api/src/main/java/xiaozhi/common/exception/ErrorCode.java b/main/manager-api/src/main/java/xiaozhi/common/exception/ErrorCode.java index e84fbba8..77d7be12 100644 --- a/main/manager-api/src/main/java/xiaozhi/common/exception/ErrorCode.java +++ b/main/manager-api/src/main/java/xiaozhi/common/exception/ErrorCode.java @@ -164,4 +164,6 @@ public interface ErrorCode { // 字典相关错误码 int DICT_LABEL_DUPLICATE = 10128; // 字典标签重复 + // 模型相关错误码 + int MODEL_TYPE_PROVIDE_CODE_NOT_NULL = 10129; // modelType和provideCode不能为空 } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/model/service/impl/ModelConfigServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/model/service/impl/ModelConfigServiceImpl.java index e309e45b..d66cf5aa 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/model/service/impl/ModelConfigServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/model/service/impl/ModelConfigServiceImpl.java @@ -105,7 +105,7 @@ public class ModelConfigServiceImpl extends BaseServiceImpl providerList = modelProviderService.getList(modelType, provideCode); if (CollectionUtil.isEmpty(providerList)) { @@ -124,7 +124,7 @@ public class ModelConfigServiceImpl extends BaseServiceImpl providerList = modelProviderService.getList(modelType, provideCode); if (CollectionUtil.isEmpty(providerList)) { diff --git a/main/manager-api/src/main/resources/i18n/messages.properties b/main/manager-api/src/main/resources/i18n/messages.properties index 30bdb8d1..63c9403d 100644 --- a/main/manager-api/src/main/resources/i18n/messages.properties +++ b/main/manager-api/src/main/resources/i18n/messages.properties @@ -134,4 +134,5 @@ 10123=您的mqtt密钥长度不安全,字符数需要至少8个字符且必须同时包含大小写字母 10124=您的mqtt密钥长度不安全,mqtt密钥必须同时包含大小写字母 10125=您的mqtt密钥包含弱密码 -10128=字典标签重复 \ No newline at end of file +10128=字典标签重复 +10129=modelType和provideCode不能为空 \ No newline at end of file diff --git a/main/manager-api/src/main/resources/i18n/messages_en_US.properties b/main/manager-api/src/main/resources/i18n/messages_en_US.properties index 05d05d0c..2b66a9fe 100644 --- a/main/manager-api/src/main/resources/i18n/messages_en_US.properties +++ b/main/manager-api/src/main/resources/i18n/messages_en_US.properties @@ -134,4 +134,5 @@ 10123=Your MQTT secret is not secure, it needs to be at least 8 characters and must contain both uppercase and lowercase letters 10124=Your MQTT secret is not secure, MQTT secret must contain both uppercase and lowercase letters 10125=Your MQTT secret contains weak password -10128=Dictionary label is duplicated \ No newline at end of file +10128=Dictionary label is duplicated +10129=modelType and provideCode cannot be empty \ No newline at end of file diff --git a/main/manager-api/src/main/resources/i18n/messages_zh_CN.properties b/main/manager-api/src/main/resources/i18n/messages_zh_CN.properties index 30bdb8d1..63c9403d 100644 --- a/main/manager-api/src/main/resources/i18n/messages_zh_CN.properties +++ b/main/manager-api/src/main/resources/i18n/messages_zh_CN.properties @@ -134,4 +134,5 @@ 10123=您的mqtt密钥长度不安全,字符数需要至少8个字符且必须同时包含大小写字母 10124=您的mqtt密钥长度不安全,mqtt密钥必须同时包含大小写字母 10125=您的mqtt密钥包含弱密码 -10128=字典标签重复 \ No newline at end of file +10128=字典标签重复 +10129=modelType和provideCode不能为空 \ No newline at end of file diff --git a/main/manager-api/src/main/resources/i18n/messages_zh_TW.properties b/main/manager-api/src/main/resources/i18n/messages_zh_TW.properties index eb4e4ae5..eed81085 100644 --- a/main/manager-api/src/main/resources/i18n/messages_zh_TW.properties +++ b/main/manager-api/src/main/resources/i18n/messages_zh_TW.properties @@ -134,4 +134,5 @@ 10123=您的MQTT密鑰不安全,需至少8個字符且必須同時包含大小寫字母 10124=您的MQTT密鑰不安全,MQTT密鑰必須同時包含大小寫字母 10125=您的MQTT密鑰包含弱密碼 -10128=字典標籤重複 \ No newline at end of file +10128=字典標籤重複 +10129=modelType和provideCode不能為空 \ No newline at end of file From 83329cf9dbb424ce04d9a550f5b1ca2a70e0e6bb Mon Sep 17 00:00:00 2001 From: hrz <1710360675@qq.com> Date: Mon, 22 Sep 2025 21:34:09 +0800 Subject: [PATCH 11/27] =?UTF-8?q?update:=E4=BC=98=E5=8C=96=E7=BC=96?= =?UTF-8?q?=E8=AF=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile-web | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Dockerfile-web b/Dockerfile-web index a347a88e..583d2e6b 100644 --- a/Dockerfile-web +++ b/Dockerfile-web @@ -18,9 +18,15 @@ FROM bellsoft/liberica-runtime-container:jre-21-glibc # 安装Nginx和字体库 RUN apk update && \ - apk add --no-cache nginx bash fontconfig ttf-dejavu && \ - apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/testing/ msttcorefonts-installer || true && \ - rm -rf /var/cache/apk/* + apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/testing/ \ + nginx \ + bash \ + fontconfig \ + ttf-dejavu \ + msttcorefonts-installer \ + && ACCEPT_EULA=Y apk add --no-cache msttcorefonts-installer \ + && fc-cache -f -v \ + && rm -rf /var/cache/apk/* # 更新字体缓存 RUN (printf 'YES\n' | update-ms-fonts || true) && fc-cache -f -v From 86b4e61c1c7d74ab9cb37efc71058b65911badac Mon Sep 17 00:00:00 2001 From: FAN-yeB <1442100690@qq.com> Date: Tue, 23 Sep 2025 09:06:27 +0800 Subject: [PATCH 12/27] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=AE=AF=E9=A3=9E?= =?UTF-8?q?=E6=98=9F=E7=81=AB=E5=A4=A7=E6=A8=A1=E5=9E=8B=E7=9A=84=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E7=A4=BA=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/config.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index 836fc673..e400f199 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -604,6 +604,16 @@ VLLM: url: https://dashscope.aliyuncs.com/compatible-mode/v1 # 可在这里找到你的api key https://bailian.console.aliyun.com/?apiKey=1#/api-key api_key: 你的api_key + XunfeiSparkLLM: + # 定义LLM API类型 + type: openai + # 先新建应用,在下面的地址 + # 开通应用地址:https://console.xfyun.cn/app/myapp + # 有免费额度,但也要开通服务,才能获取api_key + # 每一个模型都需要单独开通,每一个模型的api_password都不同,例如Lite模型在https://console.xfyun.cn/services/cbm 开通 + base_url: https://ark.cn-beijing.volces.com/api/v3 + model_name: lite + api_key: 你的api_password TTS: # 当前支持的type为edge、doubao,可自行适配 EdgeTTS: From 9f303d840ac5518d5828691f49891d56437c4dcb Mon Sep 17 00:00:00 2001 From: rainv123 <2148537152@qq.com> Date: Tue, 23 Sep 2025 09:49:13 +0800 Subject: [PATCH 13/27] =?UTF-8?q?fix:=E4=BF=AE=E6=94=B9=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E7=95=8C=E9=9D=A2=E7=9A=84=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E5=AF=86=E7=A0=81=E9=87=8D=E7=BD=AE=E3=80=81=E7=A6=81=E7=94=A8?= =?UTF-8?q?=E3=80=81=E5=88=A0=E9=99=A4=E7=9A=84=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/manager-web/src/i18n/en.js | 2 + main/manager-web/src/i18n/zh_CN.js | 2 + main/manager-web/src/i18n/zh_TW.js | 2 + main/manager-web/src/views/UserManagement.vue | 91 ++++++------------- 4 files changed, 32 insertions(+), 65 deletions(-) diff --git a/main/manager-web/src/i18n/en.js b/main/manager-web/src/i18n/en.js index 14f4d5a6..5d8f22f0 100644 --- a/main/manager-web/src/i18n/en.js +++ b/main/manager-web/src/i18n/en.js @@ -625,6 +625,7 @@ export default { 'common.confirm': 'Confirm', 'common.cancel': 'Cancel', 'common.sensitive': 'Sensitive', + 'common.success': 'Operation Successful', 'user.userid': 'User ID', 'user.deviceCount': 'Device Count', 'user.createDate': 'Registration Time', @@ -650,6 +651,7 @@ export default { 'user.deleteCancelled': 'Deletion cancelled', 'user.confirmResetPassword': 'A new password will be generated after reset. Continue?', 'user.resetPasswordSuccess': 'Password has been reset, please notify the user to log in with the new password', + 'user.generatedPassword': 'Generated Default Password', 'user.confirmDeleteUser': 'Are you sure you want to delete this user?', 'user.deleteUserSuccess': 'Deletion successful', 'user.operationFailed': 'Operation failed, please try again', diff --git a/main/manager-web/src/i18n/zh_CN.js b/main/manager-web/src/i18n/zh_CN.js index 27b0b9e0..09ecd331 100644 --- a/main/manager-web/src/i18n/zh_CN.js +++ b/main/manager-web/src/i18n/zh_CN.js @@ -625,6 +625,7 @@ export default { 'common.confirm': '确定', 'common.cancel': '取消', 'common.sensitive': '敏感', + 'common.success': '操作成功', 'user.userid': '用户Id', 'user.deviceCount': '设备数量', 'user.createDate': '注册时间', @@ -650,6 +651,7 @@ export default { 'user.deleteCancelled': '已取消删除', 'user.confirmResetPassword': '重置后将会生成新密码,是否继续?', 'user.resetPasswordSuccess': '密码已重置,请通知用户使用新密码登录', + 'user.generatedPassword': '生成的默认密码', 'user.confirmDeleteUser': '确定要删除该用户吗?', 'user.deleteUserSuccess': '删除成功', 'user.operationFailed': '操作失败,请重试', diff --git a/main/manager-web/src/i18n/zh_TW.js b/main/manager-web/src/i18n/zh_TW.js index a9f224f1..b53a7242 100644 --- a/main/manager-web/src/i18n/zh_TW.js +++ b/main/manager-web/src/i18n/zh_TW.js @@ -577,6 +577,7 @@ export default { 'user.deleteCancelled': '已取消刪除', 'user.confirmResetPassword': '重置後將會生成新密碼,是否繼續?', 'user.resetPasswordSuccess': '密碼已重置,請通知用戶使用新密碼登錄', + 'user.generatedPassword': '生成的默認密碼', 'user.confirmDeleteUser': '確定要刪除該用戶嗎?', 'user.deleteUserSuccess': '刪除成功', 'user.operationFailed': '操作失敗,請重試', @@ -661,6 +662,7 @@ export default { 'common.confirm': '確定', 'common.cancel': '取消', 'common.sensitive': '敏感', + 'common.success': '操作成功', 'paramManagement.totalRecords': '共{total}條記錄', 'paramManagement.addParam': '新增參數', 'paramManagement.editParam': '編輯參數', diff --git a/main/manager-web/src/views/UserManagement.vue b/main/manager-web/src/views/UserManagement.vue index c61ca6df..8132ba01 100644 --- a/main/manager-web/src/views/UserManagement.vue +++ b/main/manager-web/src/views/UserManagement.vue @@ -257,11 +257,17 @@ export default { cancelButtonText: this.$t('common.cancel'), type: 'warning' }).then(() => { - resetPassword(row.userId).then(() => { - this.$message.success(this.$t('user.resetPasswordSuccess')); - this.getList(); - }).catch(() => { - this.$message.error(this.$t('user.operationFailed')); + Api.admin.resetUserPassword(row.userid, ({ data }) => { + if (data.code === 0) { + // 显示生成的默认密码 + this.$alert(this.$t('user.resetPasswordSuccess') + '\n\n' + this.$t('user.generatedPassword') + ': ' + data.data, this.$t('common.success'), { + confirmButtonText: this.$t('common.confirm'), + dangerouslyUseHTMLString: true + }); + this.fetchUsers(); + } else { + this.$message.error(data.msg || this.$t('user.operationFailed')); + } }); }).catch(() => { this.$message.info(this.$t('common.deleteCancelled')); @@ -273,11 +279,13 @@ export default { cancelButtonText: this.$t('common.cancel'), type: 'warning' }).then(() => { - deleteUser(row.userId).then(() => { - this.$message.success(this.$t('user.deleteUserSuccess')); - this.getList(); - }).catch(() => { - this.$message.error(this.$t('user.operationFailed')); + Api.admin.deleteUser(row.userid, ({ data }) => { + if (data.code === 0) { + this.$message.success(this.$t('user.deleteUserSuccess')); + this.fetchUsers(); + } else { + this.$message.error(data.msg || this.$t('user.operationFailed')); + } }); }).catch(() => { this.$message.info(this.$t('common.deleteCancelled')); @@ -338,67 +346,20 @@ export default { // 用户取消操作 }); }, + // 这个方法已被batchDelete替代,保留用于向后兼容 handleBatchDelete() { - if (!this.selection || this.selection.length === 0) { - this.$message.warning(this.$t('user.selectUsersFirst')); - return; - } - - const userIds = this.selection.map(item => item.userId); - this.$confirm(this.$t('user.confirmDeleteSelected', { count: this.selection.length }), this.$t('common.warning'), { - confirmButtonText: this.$t('common.confirm'), - cancelButtonText: this.$t('common.cancel'), - type: 'warning' - }).then(() => { - this.loading = true; - batchDeleteUsers(userIds).then(res => { - this.loading = false; - this.getList(); - this.selection = []; - if (res.successCount === userIds.length) { - this.$message.success(this.$t('user.deleteSuccess', { count: res.successCount })); - } else if (res.successCount === 0) { - this.$message.error(this.$t('user.deleteFailed')); - } else { - this.$message.warning(this.$t('user.partialDelete', { successCount: res.successCount, failCount: res.failCount })); - } - }).catch(() => { - this.loading = false; - this.$message.error(this.$t('user.deleteError')); - }); - }).catch(() => { - this.loading = false; - this.$message.info(this.$t('user.deleteCancelled')); - }); + this.batchDelete(); }, + // This method has been fixed to use existing functionality handleBatchStatusChange(status) { - if (!this.selection || this.selection.length === 0) { + const selectedUsers = this.userList.filter(user => user.selected); + if (selectedUsers.length === 0) { this.$message.warning(this.$t('user.selectUsersFirst')); return; } - - const actionText = status === 1 ? this.$t('user.enable') : this.$t('user.disable'); - const userIds = this.selection.map(item => item.userId); - - this.$confirm(this.$t('user.confirmStatusChange', { action: actionText, count: this.selection.length }), this.$t('common.warning'), { - confirmButtonText: this.$t('common.confirm'), - cancelButtonText: this.$t('common.cancel'), - type: 'warning' - }).then(() => { - this.loading = true; - updateUserStatus(userIds, status).then(() => { - this.loading = false; - this.getList(); - this.selection = []; - this.$message.success(this.$t('user.statusChangeSuccess', { action: actionText, count: userIds.length })); - }).catch(() => { - this.loading = false; - this.$message.error(this.$t('user.operationFailed')); - }); - }).catch(() => { - this.loading = false; - this.$message.info(this.$t('common.deleteCancelled')); - }); + + // Call the existing handleChangeStatus method which already handles both single and multiple users + this.handleChangeStatus(selectedUsers, status); }, }, }; From 7067050006d943181ac1ad447cb76431bad21935 Mon Sep 17 00:00:00 2001 From: rainv123 <2148537152@qq.com> Date: Tue, 23 Sep 2025 10:03:43 +0800 Subject: [PATCH 14/27] =?UTF-8?q?Revert=20"fix:=E4=BF=AE=E6=94=B9=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E7=AE=A1=E7=90=86=E7=95=8C=E9=9D=A2=E7=9A=84=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E5=AF=86=E7=A0=81=E9=87=8D=E7=BD=AE=E3=80=81=E7=A6=81?= =?UTF-8?q?=E7=94=A8=E3=80=81=E5=88=A0=E9=99=A4=E7=9A=84=E9=80=BB=E8=BE=91?= =?UTF-8?q?"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 9f303d840ac5518d5828691f49891d56437c4dcb. --- main/manager-web/src/i18n/en.js | 2 - main/manager-web/src/i18n/zh_CN.js | 2 - main/manager-web/src/i18n/zh_TW.js | 2 - main/manager-web/src/views/UserManagement.vue | 95 +++++++++++++------ 4 files changed, 67 insertions(+), 34 deletions(-) diff --git a/main/manager-web/src/i18n/en.js b/main/manager-web/src/i18n/en.js index 5d8f22f0..14f4d5a6 100644 --- a/main/manager-web/src/i18n/en.js +++ b/main/manager-web/src/i18n/en.js @@ -625,7 +625,6 @@ export default { 'common.confirm': 'Confirm', 'common.cancel': 'Cancel', 'common.sensitive': 'Sensitive', - 'common.success': 'Operation Successful', 'user.userid': 'User ID', 'user.deviceCount': 'Device Count', 'user.createDate': 'Registration Time', @@ -651,7 +650,6 @@ export default { 'user.deleteCancelled': 'Deletion cancelled', 'user.confirmResetPassword': 'A new password will be generated after reset. Continue?', 'user.resetPasswordSuccess': 'Password has been reset, please notify the user to log in with the new password', - 'user.generatedPassword': 'Generated Default Password', 'user.confirmDeleteUser': 'Are you sure you want to delete this user?', 'user.deleteUserSuccess': 'Deletion successful', 'user.operationFailed': 'Operation failed, please try again', diff --git a/main/manager-web/src/i18n/zh_CN.js b/main/manager-web/src/i18n/zh_CN.js index 09ecd331..27b0b9e0 100644 --- a/main/manager-web/src/i18n/zh_CN.js +++ b/main/manager-web/src/i18n/zh_CN.js @@ -625,7 +625,6 @@ export default { 'common.confirm': '确定', 'common.cancel': '取消', 'common.sensitive': '敏感', - 'common.success': '操作成功', 'user.userid': '用户Id', 'user.deviceCount': '设备数量', 'user.createDate': '注册时间', @@ -651,7 +650,6 @@ export default { 'user.deleteCancelled': '已取消删除', 'user.confirmResetPassword': '重置后将会生成新密码,是否继续?', 'user.resetPasswordSuccess': '密码已重置,请通知用户使用新密码登录', - 'user.generatedPassword': '生成的默认密码', 'user.confirmDeleteUser': '确定要删除该用户吗?', 'user.deleteUserSuccess': '删除成功', 'user.operationFailed': '操作失败,请重试', diff --git a/main/manager-web/src/i18n/zh_TW.js b/main/manager-web/src/i18n/zh_TW.js index b53a7242..a9f224f1 100644 --- a/main/manager-web/src/i18n/zh_TW.js +++ b/main/manager-web/src/i18n/zh_TW.js @@ -577,7 +577,6 @@ export default { 'user.deleteCancelled': '已取消刪除', 'user.confirmResetPassword': '重置後將會生成新密碼,是否繼續?', 'user.resetPasswordSuccess': '密碼已重置,請通知用戶使用新密碼登錄', - 'user.generatedPassword': '生成的默認密碼', 'user.confirmDeleteUser': '確定要刪除該用戶嗎?', 'user.deleteUserSuccess': '刪除成功', 'user.operationFailed': '操作失敗,請重試', @@ -662,7 +661,6 @@ export default { 'common.confirm': '確定', 'common.cancel': '取消', 'common.sensitive': '敏感', - 'common.success': '操作成功', 'paramManagement.totalRecords': '共{total}條記錄', 'paramManagement.addParam': '新增參數', 'paramManagement.editParam': '編輯參數', diff --git a/main/manager-web/src/views/UserManagement.vue b/main/manager-web/src/views/UserManagement.vue index 8132ba01..c61ca6df 100644 --- a/main/manager-web/src/views/UserManagement.vue +++ b/main/manager-web/src/views/UserManagement.vue @@ -257,17 +257,11 @@ export default { cancelButtonText: this.$t('common.cancel'), type: 'warning' }).then(() => { - Api.admin.resetUserPassword(row.userid, ({ data }) => { - if (data.code === 0) { - // 显示生成的默认密码 - this.$alert(this.$t('user.resetPasswordSuccess') + '\n\n' + this.$t('user.generatedPassword') + ': ' + data.data, this.$t('common.success'), { - confirmButtonText: this.$t('common.confirm'), - dangerouslyUseHTMLString: true - }); - this.fetchUsers(); - } else { - this.$message.error(data.msg || this.$t('user.operationFailed')); - } + resetPassword(row.userId).then(() => { + this.$message.success(this.$t('user.resetPasswordSuccess')); + this.getList(); + }).catch(() => { + this.$message.error(this.$t('user.operationFailed')); }); }).catch(() => { this.$message.info(this.$t('common.deleteCancelled')); @@ -279,13 +273,11 @@ export default { cancelButtonText: this.$t('common.cancel'), type: 'warning' }).then(() => { - Api.admin.deleteUser(row.userid, ({ data }) => { - if (data.code === 0) { - this.$message.success(this.$t('user.deleteUserSuccess')); - this.fetchUsers(); - } else { - this.$message.error(data.msg || this.$t('user.operationFailed')); - } + deleteUser(row.userId).then(() => { + this.$message.success(this.$t('user.deleteUserSuccess')); + this.getList(); + }).catch(() => { + this.$message.error(this.$t('user.operationFailed')); }); }).catch(() => { this.$message.info(this.$t('common.deleteCancelled')); @@ -346,20 +338,67 @@ export default { // 用户取消操作 }); }, - // 这个方法已被batchDelete替代,保留用于向后兼容 handleBatchDelete() { - this.batchDelete(); - }, - // This method has been fixed to use existing functionality - handleBatchStatusChange(status) { - const selectedUsers = this.userList.filter(user => user.selected); - if (selectedUsers.length === 0) { + if (!this.selection || this.selection.length === 0) { this.$message.warning(this.$t('user.selectUsersFirst')); return; } - - // Call the existing handleChangeStatus method which already handles both single and multiple users - this.handleChangeStatus(selectedUsers, status); + + const userIds = this.selection.map(item => item.userId); + this.$confirm(this.$t('user.confirmDeleteSelected', { count: this.selection.length }), this.$t('common.warning'), { + confirmButtonText: this.$t('common.confirm'), + cancelButtonText: this.$t('common.cancel'), + type: 'warning' + }).then(() => { + this.loading = true; + batchDeleteUsers(userIds).then(res => { + this.loading = false; + this.getList(); + this.selection = []; + if (res.successCount === userIds.length) { + this.$message.success(this.$t('user.deleteSuccess', { count: res.successCount })); + } else if (res.successCount === 0) { + this.$message.error(this.$t('user.deleteFailed')); + } else { + this.$message.warning(this.$t('user.partialDelete', { successCount: res.successCount, failCount: res.failCount })); + } + }).catch(() => { + this.loading = false; + this.$message.error(this.$t('user.deleteError')); + }); + }).catch(() => { + this.loading = false; + this.$message.info(this.$t('user.deleteCancelled')); + }); + }, + handleBatchStatusChange(status) { + if (!this.selection || this.selection.length === 0) { + this.$message.warning(this.$t('user.selectUsersFirst')); + return; + } + + const actionText = status === 1 ? this.$t('user.enable') : this.$t('user.disable'); + const userIds = this.selection.map(item => item.userId); + + this.$confirm(this.$t('user.confirmStatusChange', { action: actionText, count: this.selection.length }), this.$t('common.warning'), { + confirmButtonText: this.$t('common.confirm'), + cancelButtonText: this.$t('common.cancel'), + type: 'warning' + }).then(() => { + this.loading = true; + updateUserStatus(userIds, status).then(() => { + this.loading = false; + this.getList(); + this.selection = []; + this.$message.success(this.$t('user.statusChangeSuccess', { action: actionText, count: userIds.length })); + }).catch(() => { + this.loading = false; + this.$message.error(this.$t('user.operationFailed')); + }); + }).catch(() => { + this.loading = false; + this.$message.info(this.$t('common.deleteCancelled')); + }); }, }, }; From 49e69ecad43185834136727c06c3c21b74e44abb Mon Sep 17 00:00:00 2001 From: rainv123 <2148537152@qq.com> Date: Tue, 23 Sep 2025 10:16:17 +0800 Subject: [PATCH 15/27] =?UTF-8?q?fix:=E4=BF=AE=E6=94=B9=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E7=95=8C=E9=9D=A2=E7=94=A8=E6=88=B7=E7=9A=84?= =?UTF-8?q?=E5=AF=86=E7=A0=81=E9=87=8D=E7=BD=AE=E3=80=81=E7=A6=81=E7=94=A8?= =?UTF-8?q?=E3=80=81=E5=88=A0=E9=99=A4=E7=9A=84=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/manager-web/src/i18n/en.js | 2 + main/manager-web/src/i18n/zh_CN.js | 2 + main/manager-web/src/i18n/zh_TW.js | 2 + main/manager-web/src/views/UserManagement.vue | 91 ++++++------------- 4 files changed, 32 insertions(+), 65 deletions(-) diff --git a/main/manager-web/src/i18n/en.js b/main/manager-web/src/i18n/en.js index 5e00c9b0..a4dbfa06 100644 --- a/main/manager-web/src/i18n/en.js +++ b/main/manager-web/src/i18n/en.js @@ -612,6 +612,7 @@ export default { 'user.deleteCancelled': 'Deletion cancelled', 'user.confirmResetPassword': 'A new password will be generated after reset. Continue?', 'user.resetPasswordSuccess': 'Password has been reset, please notify the user to log in with the new password', + 'user.generatedPassword': 'Generated Default Password', 'user.confirmDeleteUser': 'Are you sure you want to delete this user?', 'user.deleteUserSuccess': 'Deletion successful', 'user.operationFailed': 'Operation failed, please try again', @@ -648,6 +649,7 @@ export default { 'common.nextPage': 'Next Page', 'common.totalRecords': 'Total {number} records', 'common.addProvider': 'Add Provider', + 'common.success': 'Success', 'common.editProvider': 'Edit Provider', 'common.updateSuccess': 'Update Success', 'common.addSuccess': 'Add Success', diff --git a/main/manager-web/src/i18n/zh_CN.js b/main/manager-web/src/i18n/zh_CN.js index d71d51b1..26f66660 100644 --- a/main/manager-web/src/i18n/zh_CN.js +++ b/main/manager-web/src/i18n/zh_CN.js @@ -612,6 +612,7 @@ export default { 'user.deleteCancelled': '已取消删除', 'user.confirmResetPassword': '重置后将会生成新密码,是否继续?', 'user.resetPasswordSuccess': '密码已重置,请通知用户使用新密码登录', + 'user.generatedPassword': '生成的默认密码', 'user.confirmDeleteUser': '确定要删除该用户吗?', 'user.deleteUserSuccess': '删除成功', 'user.operationFailed': '操作失败,请重试', @@ -648,6 +649,7 @@ export default { 'common.nextPage': '下一页', 'common.totalRecords': '共{number}条记录', 'common.addProvider': '新增供应器', + 'common.success': '成功', 'common.editProvider': '编辑供应器', 'common.updateSuccess': '修改成功', 'common.addSuccess': '新增成功', diff --git a/main/manager-web/src/i18n/zh_TW.js b/main/manager-web/src/i18n/zh_TW.js index 173ebbd8..0c7f7bd1 100644 --- a/main/manager-web/src/i18n/zh_TW.js +++ b/main/manager-web/src/i18n/zh_TW.js @@ -612,6 +612,7 @@ export default { 'user.deleteCancelled': '已取消刪除', 'user.confirmResetPassword': '重置後將會生成新密碼,是否繼續?', 'user.resetPasswordSuccess': '密碼已重置,請通知用戶使用新密碼登錄', + 'user.generatedPassword': '生成的默認密碼', 'user.confirmDeleteUser': '確定要刪除該用戶嗎?', 'user.deleteUserSuccess': '刪除成功', 'user.operationFailed': '操作失敗,請重試', @@ -648,6 +649,7 @@ export default { 'common.nextPage': '下一頁', 'common.totalRecords': '共{number}條記錄', 'common.addProvider': '新增供應器', + 'common.success': '成功', 'common.editProvider': '編輯供應器', 'common.updateSuccess': '修改成功', 'common.addSuccess': '新增成功', diff --git a/main/manager-web/src/views/UserManagement.vue b/main/manager-web/src/views/UserManagement.vue index c61ca6df..8132ba01 100644 --- a/main/manager-web/src/views/UserManagement.vue +++ b/main/manager-web/src/views/UserManagement.vue @@ -257,11 +257,17 @@ export default { cancelButtonText: this.$t('common.cancel'), type: 'warning' }).then(() => { - resetPassword(row.userId).then(() => { - this.$message.success(this.$t('user.resetPasswordSuccess')); - this.getList(); - }).catch(() => { - this.$message.error(this.$t('user.operationFailed')); + Api.admin.resetUserPassword(row.userid, ({ data }) => { + if (data.code === 0) { + // 显示生成的默认密码 + this.$alert(this.$t('user.resetPasswordSuccess') + '\n\n' + this.$t('user.generatedPassword') + ': ' + data.data, this.$t('common.success'), { + confirmButtonText: this.$t('common.confirm'), + dangerouslyUseHTMLString: true + }); + this.fetchUsers(); + } else { + this.$message.error(data.msg || this.$t('user.operationFailed')); + } }); }).catch(() => { this.$message.info(this.$t('common.deleteCancelled')); @@ -273,11 +279,13 @@ export default { cancelButtonText: this.$t('common.cancel'), type: 'warning' }).then(() => { - deleteUser(row.userId).then(() => { - this.$message.success(this.$t('user.deleteUserSuccess')); - this.getList(); - }).catch(() => { - this.$message.error(this.$t('user.operationFailed')); + Api.admin.deleteUser(row.userid, ({ data }) => { + if (data.code === 0) { + this.$message.success(this.$t('user.deleteUserSuccess')); + this.fetchUsers(); + } else { + this.$message.error(data.msg || this.$t('user.operationFailed')); + } }); }).catch(() => { this.$message.info(this.$t('common.deleteCancelled')); @@ -338,67 +346,20 @@ export default { // 用户取消操作 }); }, + // 这个方法已被batchDelete替代,保留用于向后兼容 handleBatchDelete() { - if (!this.selection || this.selection.length === 0) { - this.$message.warning(this.$t('user.selectUsersFirst')); - return; - } - - const userIds = this.selection.map(item => item.userId); - this.$confirm(this.$t('user.confirmDeleteSelected', { count: this.selection.length }), this.$t('common.warning'), { - confirmButtonText: this.$t('common.confirm'), - cancelButtonText: this.$t('common.cancel'), - type: 'warning' - }).then(() => { - this.loading = true; - batchDeleteUsers(userIds).then(res => { - this.loading = false; - this.getList(); - this.selection = []; - if (res.successCount === userIds.length) { - this.$message.success(this.$t('user.deleteSuccess', { count: res.successCount })); - } else if (res.successCount === 0) { - this.$message.error(this.$t('user.deleteFailed')); - } else { - this.$message.warning(this.$t('user.partialDelete', { successCount: res.successCount, failCount: res.failCount })); - } - }).catch(() => { - this.loading = false; - this.$message.error(this.$t('user.deleteError')); - }); - }).catch(() => { - this.loading = false; - this.$message.info(this.$t('user.deleteCancelled')); - }); + this.batchDelete(); }, + // This method has been fixed to use existing functionality handleBatchStatusChange(status) { - if (!this.selection || this.selection.length === 0) { + const selectedUsers = this.userList.filter(user => user.selected); + if (selectedUsers.length === 0) { this.$message.warning(this.$t('user.selectUsersFirst')); return; } - - const actionText = status === 1 ? this.$t('user.enable') : this.$t('user.disable'); - const userIds = this.selection.map(item => item.userId); - - this.$confirm(this.$t('user.confirmStatusChange', { action: actionText, count: this.selection.length }), this.$t('common.warning'), { - confirmButtonText: this.$t('common.confirm'), - cancelButtonText: this.$t('common.cancel'), - type: 'warning' - }).then(() => { - this.loading = true; - updateUserStatus(userIds, status).then(() => { - this.loading = false; - this.getList(); - this.selection = []; - this.$message.success(this.$t('user.statusChangeSuccess', { action: actionText, count: userIds.length })); - }).catch(() => { - this.loading = false; - this.$message.error(this.$t('user.operationFailed')); - }); - }).catch(() => { - this.loading = false; - this.$message.info(this.$t('common.deleteCancelled')); - }); + + // Call the existing handleChangeStatus method which already handles both single and multiple users + this.handleChangeStatus(selectedUsers, status); }, }, }; From 8891a967315bc325e8f9cd4eda0e0369201e0423 Mon Sep 17 00:00:00 2001 From: FAN-yeB <1442100690@qq.com> Date: Tue, 23 Sep 2025 10:45:25 +0800 Subject: [PATCH 16/27] =?UTF-8?q?=E6=9B=B4=E6=94=B9=E4=B8=8B=E5=8F=91?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/config.yaml | 20 +-- main/xiaozhi-server/core/api/ota_handler.py | 161 +++++++------------- 2 files changed, 55 insertions(+), 126 deletions(-) diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index a946579c..807cbd2d 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -41,24 +41,10 @@ server: # 可选:设备白名单,如果设置了白名单,那么白名单的机器无论是什么token都可以连接。 #allowed_devices: # - "24:0A:C4:1D:3B:F0" # MAC地址列表 - # MQTT网关配置,用于通过OTA下发到设备 - mqtt_gateway: - # MQTT服务器地址以及端口,根据mqtt_gateway的.env文件配置,为null或者不填写时使用websocket协议 - host: null - port: null - # group_id会从设备型号动态生成,格式为GID_设备型号 - # 此配置仅作为当获取设备型号失败时可以填写的回退值 - # group_id: - - # MQTT签名密钥,用于生成MQTT连接密码,根据mqtt_gateway的.env文件配置 + # MQTT网关配置,用于通过OTA下发到设备,与智控台要求格式相同,与mqtt_gateway的配置相同 + mqtt_gateway: null + # MQTT签名密钥,用于生成MQTT连接密钥,与mqtt_gateway的配置相同 mqtt_signature_key: null - - # UDP网关配置,根据mqtt_gateway的.env文件配置 - udp_gateway: - # UDP服务器地址 - host: null - # UDP服务器端口 - port: null log: # 设置控制台输出的日志格式,时间、日志级别、标签、消息 log_format: "{time:YYMMDD HH:mm:ss}[{version}_{selected_module}][{extra[tag]}]-{level}-{message}" diff --git a/main/xiaozhi-server/core/api/ota_handler.py b/main/xiaozhi-server/core/api/ota_handler.py index f24c3f8f..8262c629 100644 --- a/main/xiaozhi-server/core/api/ota_handler.py +++ b/main/xiaozhi-server/core/api/ota_handler.py @@ -32,26 +32,8 @@ class OTAHandler(BaseHandler): self.logger.bind(tag=TAG).error(f"生成MQTT密码签名失败: {e}") return "" - def _get_websocket_url(self, local_ip: str, port: int) -> str: - """获取websocket地址 - - Args: - local_ip: 本地IP地址 - port: 端口号 - - Returns: - str: websocket地址 - """ - server_config = self.config["server"] - websocket_config = server_config.get("websocket", "") - - if "你的" not in websocket_config: - return websocket_config - else: - return f"ws://{local_ip}:{port}/xiaozhi/v1/" - async def handle_post(self, request): - """处理 OTA POST 请求""" + """处理 OTA POST 请求 - 仅下发 MQTT 配置""" try: data = await request.text() self.logger.bind(tag=TAG).debug(f"OTA请求方法: {request.method}") @@ -59,17 +41,52 @@ class OTAHandler(BaseHandler): self.logger.bind(tag=TAG).debug(f"OTA请求数据: {data}") device_id = request.headers.get("device-id", "") - if device_id: - self.logger.bind(tag=TAG).info(f"OTA请求设备ID: {device_id}") - else: + if not device_id: raise Exception("OTA请求设备ID为空") data_json = json.loads(data) server_config = self.config["server"] - port = int(server_config.get("port", 8000)) - local_ip = get_local_ip() + mqtt_endpoint = server_config.get("mqtt_gateway", "") + signature_key = server_config.get("mqtt_signature_key", "") + if not mqtt_endpoint: + raise Exception("未配置 mqtt_gateway") + + # 从设备型号获取 group_id + device_model = "default" + try: + if "device" in data_json and isinstance(data_json["device"], dict): + device_model = data_json["device"].get("model", "default") + elif "model" in data_json: + device_model = data_json["model"] + group_id = f"GID_{device_model}".replace(":", "_").replace(" ", "_") + except Exception as e: + self.logger.bind(tag=TAG).error(f"获取设备型号失败: {e}") + group_id = "GID_default" + + mac_address_safe = device_id.replace(":", "_") + mqtt_client_id = f"{group_id}@@@{mac_address_safe}@@@{mac_address_safe}" + + # 构建用户名(Base64编码的JSON) + user_data = {"ip": "unknown"} # 可根据需要扩展 + try: + user_data_json = json.dumps(user_data) + username = base64.b64encode(user_data_json.encode('utf-8')).decode('utf-8') + except Exception as e: + self.logger.bind(tag=TAG).error(f"生成用户名失败: {e}") + username = "" + + # 生成密码 + password = "" + if signature_key: + password = self.generate_password_signature(mqtt_client_id + "|" + username, signature_key) + if not password: + raise Exception("MQTT密码签名生成失败") + else: + raise Exception("未配置 mqtt_signature_key") + + # 构建返回的 MQTT 配置 return_json = { "server_time": { "timestamp": int(round(time.time() * 1000)), @@ -79,97 +96,25 @@ class OTAHandler(BaseHandler): "version": data_json["application"].get("version", "1.0.0"), "url": "", }, - } - - mqtt_gateway_config = server_config.get("mqtt_gateway", {}) - mqtt_gateway_host = mqtt_gateway_config.get("host", "") - mqtt_gateway_port = mqtt_gateway_config.get("port", "") - - - if mqtt_gateway_host: # 配置了mqtt_gateway,使用MQTT和UDP协议传输 - # 从设备型号获取group_id,与manager-api保持一致 - # 客户端ID格式:groupId@@@macAddress@@@macAddress - # 尝试从请求数据中获取设备型号 - device_model = "default" - try: - # 假设设备型号在request数据的某个字段中 - if "device" in data_json and isinstance(data_json["device"], dict): - device_model = data_json["device"].get("model", "default") - elif "model" in data_json: - device_model = data_json["model"] - # 为了保证格式一致性,进行与manager-api相同的处理 - group_id = f"GID_{device_model}".replace(":", "_").replace(" ", "_") - except Exception as e: - self.logger.bind(tag=TAG).error(f"获取设备型号失败: {e}") - # 如果获取失败,使用配置文件中的默认值 - group_id = mqtt_gateway_config.get("group_id", "GID_default").replace(":", "_") - - mac_address_safe = device_id.replace(":", "_") - mqtt_client_id = f"{group_id}@@@{mac_address_safe}@@@{mac_address_safe}" - - # 构建用户数据(包含IP等信息) - user_data = { - "ip": "unknown" - } - - # 将用户数据编码为Base64 JSON - try: - user_data_json = json.dumps(user_data) - username = base64.b64encode(user_data_json.encode('utf-8')).decode('utf-8') - except Exception as e: - self.logger.bind(tag=TAG).error(f"生成用户名失败: {e}") - username = "" - - # 获取MQTT签名密钥 - password = "" - signature_key = server_config.get("mqtt_signature_key", "") - if signature_key: - # 使用签名密钥生成密码 - password = self.generate_password_signature(mqtt_client_id + "|" + username, signature_key) - if not password: - # 如果签名生成失败,使用配置文件中的密码 - password = mqtt_gateway_config.get("password", "") - else: - # 如果没有签名密钥,使用配置文件中的密码 - password = mqtt_gateway_config.get("password", "") - self.logger.bind(tag=TAG).warning("缺少MQTT签名密钥,使用配置文件中的密码") - - # 构建MQTT配置 - endpoint = f"{mqtt_gateway_host}:{mqtt_gateway_port}" - return_json["mqtt_gateway"] = { - "endpoint": endpoint, + "mqtt": { + "endpoint": mqtt_endpoint, "client_id": mqtt_client_id, "username": username, "password": password, "publish_topic": "device-server", "subscribe_topic": f"devices/p2p/{mac_address_safe}" } - self.logger.bind(tag=TAG).info(f"为设备 {device_id} 下发MQTT网关配置") - - # 添加UDP网关配置 - if "udp_gateway" in server_config: - udp_config = server_config["udp_gateway"] - udp_host = udp_config.get("host", "") - udp_port = udp_config.get("port", "") - if udp_host: - return_json["udp_gateway"] = { - "host": udp_host, - "port": udp_port - } - self.logger.bind(tag=TAG).info(f"为设备 {device_id} 下发UDP网关配置") - - else: # 未配置mqtt_gateway,使用WebSocket协议传输 - return_json["websocket"] = { - "url": self._get_websocket_url(local_ip, port), - } - self.logger.bind(tag=TAG).info(f"未配置MQTT网关,为设备 {device_id} 下发WebSocket配置") - + } + + self.logger.bind(tag=TAG).info(f"为设备 {device_id} 下发MQTT配置") + response = web.Response( text=json.dumps(return_json, separators=(",", ":")), content_type="application/json", ) except Exception as e: - return_json = {"success": False, "message": "request error."} + self.logger.bind(tag=TAG).error(f"处理OTA请求失败: {e}") + return_json = {"success": False, "message": str(e)} response = web.Response( text=json.dumps(return_json, separators=(",", ":")), content_type="application/json", @@ -182,14 +127,12 @@ class OTAHandler(BaseHandler): """处理 OTA GET 请求""" try: server_config = self.config["server"] - local_ip = get_local_ip() - port = int(server_config.get("port", 8000)) - websocket_url = self._get_websocket_url(local_ip, port) - message = f"OTA接口运行正常,向设备发送的websocket地址是:{websocket_url}" + mqtt_endpoint = server_config.get("mqtt_gateway", "未配置") + message = f"OTA接口运行正常,MQTT网关地址:{mqtt_endpoint}" response = web.Response(text=message, content_type="text/plain") except Exception as e: self.logger.bind(tag=TAG).error(f"OTA GET请求异常: {e}") response = web.Response(text="OTA接口异常", content_type="text/plain") finally: self._add_cors_headers(response) - return response + return response \ No newline at end of file From ca400d8ea7a5c42cd9dd79b5695b3490dacb968f Mon Sep 17 00:00:00 2001 From: FAN-yeB <1442100690@qq.com> Date: Tue, 23 Sep 2025 11:29:05 +0800 Subject: [PATCH 17/27] =?UTF-8?q?Revert=20"=E6=9B=B4=E6=94=B9=E4=B8=8B?= =?UTF-8?q?=E5=8F=91=E9=85=8D=E7=BD=AE"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 8891a967315bc325e8f9cd4eda0e0369201e0423. --- main/xiaozhi-server/config.yaml | 20 ++- main/xiaozhi-server/core/api/ota_handler.py | 161 +++++++++++++------- 2 files changed, 126 insertions(+), 55 deletions(-) diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index 807cbd2d..a946579c 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -41,10 +41,24 @@ server: # 可选:设备白名单,如果设置了白名单,那么白名单的机器无论是什么token都可以连接。 #allowed_devices: # - "24:0A:C4:1D:3B:F0" # MAC地址列表 - # MQTT网关配置,用于通过OTA下发到设备,与智控台要求格式相同,与mqtt_gateway的配置相同 - mqtt_gateway: null - # MQTT签名密钥,用于生成MQTT连接密钥,与mqtt_gateway的配置相同 + # MQTT网关配置,用于通过OTA下发到设备 + mqtt_gateway: + # MQTT服务器地址以及端口,根据mqtt_gateway的.env文件配置,为null或者不填写时使用websocket协议 + host: null + port: null + # group_id会从设备型号动态生成,格式为GID_设备型号 + # 此配置仅作为当获取设备型号失败时可以填写的回退值 + # group_id: + + # MQTT签名密钥,用于生成MQTT连接密码,根据mqtt_gateway的.env文件配置 mqtt_signature_key: null + + # UDP网关配置,根据mqtt_gateway的.env文件配置 + udp_gateway: + # UDP服务器地址 + host: null + # UDP服务器端口 + port: null log: # 设置控制台输出的日志格式,时间、日志级别、标签、消息 log_format: "{time:YYMMDD HH:mm:ss}[{version}_{selected_module}][{extra[tag]}]-{level}-{message}" diff --git a/main/xiaozhi-server/core/api/ota_handler.py b/main/xiaozhi-server/core/api/ota_handler.py index 8262c629..f24c3f8f 100644 --- a/main/xiaozhi-server/core/api/ota_handler.py +++ b/main/xiaozhi-server/core/api/ota_handler.py @@ -32,8 +32,26 @@ class OTAHandler(BaseHandler): self.logger.bind(tag=TAG).error(f"生成MQTT密码签名失败: {e}") return "" + def _get_websocket_url(self, local_ip: str, port: int) -> str: + """获取websocket地址 + + Args: + local_ip: 本地IP地址 + port: 端口号 + + Returns: + str: websocket地址 + """ + server_config = self.config["server"] + websocket_config = server_config.get("websocket", "") + + if "你的" not in websocket_config: + return websocket_config + else: + return f"ws://{local_ip}:{port}/xiaozhi/v1/" + async def handle_post(self, request): - """处理 OTA POST 请求 - 仅下发 MQTT 配置""" + """处理 OTA POST 请求""" try: data = await request.text() self.logger.bind(tag=TAG).debug(f"OTA请求方法: {request.method}") @@ -41,52 +59,17 @@ class OTAHandler(BaseHandler): self.logger.bind(tag=TAG).debug(f"OTA请求数据: {data}") device_id = request.headers.get("device-id", "") - if not device_id: + if device_id: + self.logger.bind(tag=TAG).info(f"OTA请求设备ID: {device_id}") + else: raise Exception("OTA请求设备ID为空") data_json = json.loads(data) server_config = self.config["server"] - mqtt_endpoint = server_config.get("mqtt_gateway", "") - signature_key = server_config.get("mqtt_signature_key", "") + port = int(server_config.get("port", 8000)) + local_ip = get_local_ip() - if not mqtt_endpoint: - raise Exception("未配置 mqtt_gateway") - - # 从设备型号获取 group_id - device_model = "default" - try: - if "device" in data_json and isinstance(data_json["device"], dict): - device_model = data_json["device"].get("model", "default") - elif "model" in data_json: - device_model = data_json["model"] - group_id = f"GID_{device_model}".replace(":", "_").replace(" ", "_") - except Exception as e: - self.logger.bind(tag=TAG).error(f"获取设备型号失败: {e}") - group_id = "GID_default" - - mac_address_safe = device_id.replace(":", "_") - mqtt_client_id = f"{group_id}@@@{mac_address_safe}@@@{mac_address_safe}" - - # 构建用户名(Base64编码的JSON) - user_data = {"ip": "unknown"} # 可根据需要扩展 - try: - user_data_json = json.dumps(user_data) - username = base64.b64encode(user_data_json.encode('utf-8')).decode('utf-8') - except Exception as e: - self.logger.bind(tag=TAG).error(f"生成用户名失败: {e}") - username = "" - - # 生成密码 - password = "" - if signature_key: - password = self.generate_password_signature(mqtt_client_id + "|" + username, signature_key) - if not password: - raise Exception("MQTT密码签名生成失败") - else: - raise Exception("未配置 mqtt_signature_key") - - # 构建返回的 MQTT 配置 return_json = { "server_time": { "timestamp": int(round(time.time() * 1000)), @@ -96,25 +79,97 @@ class OTAHandler(BaseHandler): "version": data_json["application"].get("version", "1.0.0"), "url": "", }, - "mqtt": { - "endpoint": mqtt_endpoint, + } + + mqtt_gateway_config = server_config.get("mqtt_gateway", {}) + mqtt_gateway_host = mqtt_gateway_config.get("host", "") + mqtt_gateway_port = mqtt_gateway_config.get("port", "") + + + if mqtt_gateway_host: # 配置了mqtt_gateway,使用MQTT和UDP协议传输 + # 从设备型号获取group_id,与manager-api保持一致 + # 客户端ID格式:groupId@@@macAddress@@@macAddress + # 尝试从请求数据中获取设备型号 + device_model = "default" + try: + # 假设设备型号在request数据的某个字段中 + if "device" in data_json and isinstance(data_json["device"], dict): + device_model = data_json["device"].get("model", "default") + elif "model" in data_json: + device_model = data_json["model"] + # 为了保证格式一致性,进行与manager-api相同的处理 + group_id = f"GID_{device_model}".replace(":", "_").replace(" ", "_") + except Exception as e: + self.logger.bind(tag=TAG).error(f"获取设备型号失败: {e}") + # 如果获取失败,使用配置文件中的默认值 + group_id = mqtt_gateway_config.get("group_id", "GID_default").replace(":", "_") + + mac_address_safe = device_id.replace(":", "_") + mqtt_client_id = f"{group_id}@@@{mac_address_safe}@@@{mac_address_safe}" + + # 构建用户数据(包含IP等信息) + user_data = { + "ip": "unknown" + } + + # 将用户数据编码为Base64 JSON + try: + user_data_json = json.dumps(user_data) + username = base64.b64encode(user_data_json.encode('utf-8')).decode('utf-8') + except Exception as e: + self.logger.bind(tag=TAG).error(f"生成用户名失败: {e}") + username = "" + + # 获取MQTT签名密钥 + password = "" + signature_key = server_config.get("mqtt_signature_key", "") + if signature_key: + # 使用签名密钥生成密码 + password = self.generate_password_signature(mqtt_client_id + "|" + username, signature_key) + if not password: + # 如果签名生成失败,使用配置文件中的密码 + password = mqtt_gateway_config.get("password", "") + else: + # 如果没有签名密钥,使用配置文件中的密码 + password = mqtt_gateway_config.get("password", "") + self.logger.bind(tag=TAG).warning("缺少MQTT签名密钥,使用配置文件中的密码") + + # 构建MQTT配置 + endpoint = f"{mqtt_gateway_host}:{mqtt_gateway_port}" + return_json["mqtt_gateway"] = { + "endpoint": endpoint, "client_id": mqtt_client_id, "username": username, "password": password, "publish_topic": "device-server", "subscribe_topic": f"devices/p2p/{mac_address_safe}" } - } - - self.logger.bind(tag=TAG).info(f"为设备 {device_id} 下发MQTT配置") - + self.logger.bind(tag=TAG).info(f"为设备 {device_id} 下发MQTT网关配置") + + # 添加UDP网关配置 + if "udp_gateway" in server_config: + udp_config = server_config["udp_gateway"] + udp_host = udp_config.get("host", "") + udp_port = udp_config.get("port", "") + if udp_host: + return_json["udp_gateway"] = { + "host": udp_host, + "port": udp_port + } + self.logger.bind(tag=TAG).info(f"为设备 {device_id} 下发UDP网关配置") + + else: # 未配置mqtt_gateway,使用WebSocket协议传输 + return_json["websocket"] = { + "url": self._get_websocket_url(local_ip, port), + } + self.logger.bind(tag=TAG).info(f"未配置MQTT网关,为设备 {device_id} 下发WebSocket配置") + response = web.Response( text=json.dumps(return_json, separators=(",", ":")), content_type="application/json", ) except Exception as e: - self.logger.bind(tag=TAG).error(f"处理OTA请求失败: {e}") - return_json = {"success": False, "message": str(e)} + return_json = {"success": False, "message": "request error."} response = web.Response( text=json.dumps(return_json, separators=(",", ":")), content_type="application/json", @@ -127,12 +182,14 @@ class OTAHandler(BaseHandler): """处理 OTA GET 请求""" try: server_config = self.config["server"] - mqtt_endpoint = server_config.get("mqtt_gateway", "未配置") - message = f"OTA接口运行正常,MQTT网关地址:{mqtt_endpoint}" + local_ip = get_local_ip() + port = int(server_config.get("port", 8000)) + websocket_url = self._get_websocket_url(local_ip, port) + message = f"OTA接口运行正常,向设备发送的websocket地址是:{websocket_url}" response = web.Response(text=message, content_type="text/plain") except Exception as e: self.logger.bind(tag=TAG).error(f"OTA GET请求异常: {e}") response = web.Response(text="OTA接口异常", content_type="text/plain") finally: self._add_cors_headers(response) - return response \ No newline at end of file + return response From 14a08291909273a8bdc1dee855952e66aeba22d4 Mon Sep 17 00:00:00 2001 From: FAN-yeB <1442100690@qq.com> Date: Tue, 23 Sep 2025 11:37:28 +0800 Subject: [PATCH 18/27] =?UTF-8?q?=E6=9B=B4=E6=96=B0ota=E4=B8=8B=E5=8F=91mq?= =?UTF-8?q?tt=E5=8D=8F=E8=AE=AE=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/config.yaml | 20 ++----- .../ed76d459636c2481aec828516c1b4f54.wav | Bin 0 -> 228524 bytes main/xiaozhi-server/core/api/ota_handler.py | 51 +++++------------- 3 files changed, 17 insertions(+), 54 deletions(-) create mode 100644 main/xiaozhi-server/config/assets/wakeup_words/ed76d459636c2481aec828516c1b4f54.wav diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index a946579c..9344711a 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -41,24 +41,12 @@ server: # 可选:设备白名单,如果设置了白名单,那么白名单的机器无论是什么token都可以连接。 #allowed_devices: # - "24:0A:C4:1D:3B:F0" # MAC地址列表 - # MQTT网关配置,用于通过OTA下发到设备 - mqtt_gateway: - # MQTT服务器地址以及端口,根据mqtt_gateway的.env文件配置,为null或者不填写时使用websocket协议 - host: null - port: null - # group_id会从设备型号动态生成,格式为GID_设备型号 - # 此配置仅作为当获取设备型号失败时可以填写的回退值 - # group_id: - + # MQTT网关配置,用于通过OTA下发到设备,根据mqtt_gateway的.env文件配置,格式为host:port + mqtt_gateway: null # MQTT签名密钥,用于生成MQTT连接密码,根据mqtt_gateway的.env文件配置 mqtt_signature_key: null - - # UDP网关配置,根据mqtt_gateway的.env文件配置 - udp_gateway: - # UDP服务器地址 - host: null - # UDP服务器端口 - port: null + # UDP网关配置 + udp_gateway: null log: # 设置控制台输出的日志格式,时间、日志级别、标签、消息 log_format: "{time:YYMMDD HH:mm:ss}[{version}_{selected_module}][{extra[tag]}]-{level}-{message}" diff --git a/main/xiaozhi-server/config/assets/wakeup_words/ed76d459636c2481aec828516c1b4f54.wav b/main/xiaozhi-server/config/assets/wakeup_words/ed76d459636c2481aec828516c1b4f54.wav new file mode 100644 index 0000000000000000000000000000000000000000..1e7246be486ff639b29d24241404173e56c9beab GIT binary patch literal 228524 zcmeFaca&7c_b*x}ckZ4%z(|%XAQ=Qia#BRHfaEMml7eIr5fsTP0xAN6D3T>9k|hX| za~NQT8D@HVI`>KS_MWmAeD7NCzW455_jliK&g$-SD(t;$?@#TjQ)i!=x7xLC{lkX> z>G4|kRzpWmEbSqLaB$3O3MbPE;faR~9x!phEGYAT|A#d=Lg}*)p5Zn7jh{V#&C>oe z1?w;>@!a?oOMh0ns9+I&OyfW_mcq(CUz_E!?`JKb^!Xak(%Bi(!_WWoAJX}s|Hb9Q zQYhX3y!3zP{@2yO|2xF|S8LyY)mHsimHT;o{I|DwUK{_Xpa1WN*#E}g|LK@}zJ33V zzvs2_|A+Jc{Ve{!s`dYGE&0#Vc=ewt>>endj`uMCt8!sIb_Wo@55a4^KZ)~1+U%PB zvOAvV`8~U{d6vrLW#6nuoGL5xtPZ0eugN&^a2c0?oyYG?Sbm%qqtEI-&+%F9IJf6z z$J+dJZH#lmGFbk9r1VUFMlD{3)%zbMink|DHBRl>HKX^8+kaAsr!!vhmOkV8Oh!gI zUOO%~whhmj9C53%>t|G+mnyEU_?;lD^Qyciv3-8hEKqP`bL1S?^}50i~u<9WOsp2sP~>BX=A zlS7=!vs{+{pS9xjkUoCcHP(rLFy(pcuo9Spl<+TJmYrk%^M8#0^C|2cwZ~+{5(YKl zpj4*@9C*&IktU}9kAHDnKF>SeF9ag}ixY+chQGLtk$b#73@LH@A&+=2p0PGE4jj?` zl?>&QmkIQHmk|%sztJ6K}H#AVvbA%I8d&5--y#dYDnDx{`hS`2Kz1!*CO$Q zhLCCuQt=a|J{q7QI3hF*pFA3*ez?N4Abi45Dh8z@G#{=5@Cm~ag|tXfjTlhV;7Tw0 z=y0yn0{RqaDnamxi-1(0JF z(M8$@oXMa8rXOZ&Y@?01iJe%9rRb1|2>OxWkYV*Ng*YJ(bF4*&0;vk*DNss=5B@4p zo`8)wD1~{kB6(DbenJDOW7J7fNe0l8i3&2RMI0TF8wIIMst^}&asa2qB90|UGMqbs zS^}IEgStti1W6}paFqpu?ulYp#5)=&ZIW958Et5i91l@FM61s1zr*$Js;2{L#s*x^(<)bZF-Gf zq?hPPx|bdYy2+r~EP54MTb8^G?F)b&yc9JY1XQYk)>}Z~1+a=p?$FE77PP7eawowD zdj=2smB` zz&Q_eew&^KnH)f^EVSPZ8qEWpR)lt@k*6R}KG^;R(v&m+?lM@q5qSkLatCVN1MAp< zp9-zI1|@F+r*330sSnnC2z|62XuTZh?J`&+1!`6TI%sJ-q#|~5KsFAfq<{k#aC5&lNz+8E8czT}Kzv)nGRjv{?cW zQ32Xe7U&UZZ!YNMACTk@@O+6h222R#FR1GvwMaL(!hXR4|8&v;co1?Iti*wSlYqOQ z{zN~df6_GQ5ej((a$A6I6=;n}LO?G9IQpG_MRx!W&I4X9gO=S;Hv@EvRokQwJUAfG<$#kF*v!PgJr4 z+SLN=7y~&iM5ilgKRSc{44$?hnMF2}3uFrELB@fOYk|#*gQUAa(+|KNAwbP*G@ZUn z>p=ggK>O21!cf?bY*w(Wo%7d$?C ziTn&y(*al8Kqousc={&vt69JsEqb3W2WhKvPOd(gL@Uv4v?Yl`%RUF%&B;$>8(BtH z1MbF92b~B#lP7yYnkrzk1!N%km?Ux=0k6N&(qsZz0>6(LH z?$D|<0c5R5z9J%*z!m4Za3{!Rz>^=m8O{b;kn7NQs*&5!2X@h0<~-U4_~wG9TSGq@ z1s17AzNBZ(pxK*jBz>WeorIp;kM^aVsA`@v!{#o!m)sF z!F49TLBIQk>;ZW$n7^1S=+|Tr83%pJ3ct6=i`*zq?ddGD3|U7m z(cW}8okFWaKlXsM5*6rBvXWa%3XE}P65;tW`~tGnY-qZ{iWSHnbEZB=w*ekko6F74 zroa-aK=o5a=OJ^=(aAb*fD+(UAN&NbgKXVB$*6aEoBZKRq9$w;9pf0&*( zSDJ_ENIspvL4P*O(RaxMa+=npC&+xFnLcBOIRL1B2G-tT{%U>%_^M21gMZpXC)36B z3vLGgBA-k;fOpIy$7v#I#a-eilX*~XJ1q-1SPfPZIUO)Mg(PrSpkHRvRI(d>rVPggMUSi3RZRig{RX z3VJxjzX{$i0$#KMplAo3VO%jO+Lo&%Oy*nAr$$pV z(cEdQq${`++z~E~yGI*?r?T?bxnwiX*awnIv@YQKmU+o&Wv(Tc_+{YZ#)0Rs7{`oO zTs}X8{Avy`$CF>lb@CzUO;;I3KAje5|Cl?ueOzl&LqD$0HhwT=7|)jLXLXgf7c{{pbfB-(67&K8 zn(!Kb06caXy2$(zyuxR+i+R-CNqX@Ug_R^?{=|(3Ow2M4Xz3&@?H1f*JlJCe^qPU> zTce}8MIXh_5|$WSwS{!3IEia$%+WKrulcQj@%3~FjDQo3s+vvfLWc@JN*DRLv{17c zPq_usMPY*Zg7&9=SXd;l69;kU$=Ce%f`xX_zSR105n(wwtuHcO=cn_ZlR5N~IbN>} zRQ{3#`6+p<)zmK=Hn2@D^zYrI4LxGsG#?PJ+0bZhw1RPeB$v&d;XVTyvgmi_PsV9| z7u-lJluKGq@~8A|8ppjVtr9DcM14ohuhkab64&Uq*c4-?v{iaye64;;D+x1&Z~4&m@h4%EhIr_5Qjnbt>LXMDuJF1Hr` zFi7l-kQ1~08~4QOSfHvNImma0haa!ZW1&<`4j{rM&IM?D(tqYfwY_}-+p z{*PXft7hqa-IV^qyWpFq2_+D>MY|fSZ_FeYh27#3uCdvSPUpAqZH*tIJEMkrQ+q!;EIcK8$^4t#Aem-G z^IfyMFhq)ytHyBS2~Fj@3Qo>RJhU$Nr_$fC-&RaqEgrD^Y;UWC#I2;P*-39insZtD z(Acr)J6vC-n{tuV(<;O!Ykz?E`(DcoT?~#4ms2~azlMhg1EJNi?J-^bPzt=+(x7B_@)zy`$7#nn$2n)dy`^on^q$yV+H3jKQpI9bhVeG~g?`8EO_TKt;mN^G zk-@RUdY{<5@Vwx}@Q2~KzQ#}1J~p$QIda~Y0Y#mzrl_0bt)3SW+B+0yhomzp!Q^*6 z-#ZGFL*kcGJ6kv9f>~a>EAr0$_R+!&A=fg*QQtaQ7|iQ{T01>#__V`OSLE&3;LsZ1 z=lQz|r$iEit8;HZ?3&#$x0Uae@GUhv{2;bN80gxcIyk99Vp~sz#7?f04$)EFvPt|@ z=qzd^U>?(^8=QDtIc@#kvdg;0G9Gd|NeARKo|?=eY|yEnH(oT#&<~uyqFMTFp^N;Z z<*dTlF55aeI@?bwRirWeEa=TJbJZ`a&D2k0_oAgD<3jZVMt&vl#-}OSQy-7W%+4C0 z-x_-JEts=*vW!psD2-3`yL-EaI7d4xJ1y2PI47UYj}!Wl`m`svTg!?o^}Sm7k@tt3UXOo$_~zrKnU!-I1S^nI$|U(c zOIK%=)WUQ*wVCIOgc%9<-CtV_ek9*SxWso8M+)8eKe-LkXoto9&|X2_F1D9TEBnM> zh-z%*UBXRzJN7}ORrqO8@NUfQocqSpXm-tqD{fW0T{`P!^@`Y%oQbR!eosj%U;TxX zo=f(Z6Xzv<=lopCBb8_+^O4zsR+RF^6rq<=;5h1DZI`XhEPbs{6qk62T&256W9l|1 zY4c;(3U3!oDro8t6*SDA_o(rMM)xh5+LL|JmUJ0uVXm{^P96TjmuW{6lN0@k_3g8) z3+3tJN^_ukL>nx$Qv{(r*)J@!z3$BOOm?2Ono?`IhcZg~g?p<1qAxM-8D>QBP0wFY zu+Bfm`)2n2hr=G!d2sdN$o!AhRMLbJu7>k^+VB!jQjR5TaNMQM@6EE=DWTtUbTNE7_*yj7XU`KBAtd|~T zKAQAk?H&16v;6h+b$JBbaX4KaO7twXJFR{~dHcA8X6{PP2>+2WLi^JErl*Q}vyK{(1MG)XK}w+LARfv%#H-w~pVvrmmJ2i?xi^ za*MPHWoDN8$bH}Xva3Y$$8J&iMEfh|(;L9-@HiJR%hFn6OIshuhxSD_!5VYkv5it* z6GL(x`A@!m*_}k16Py#)G;_Cx%4|} zYaAcjMbD~4k7FHoLw!X%PCw%-a+}R;<3nz_Qp|JH*~FTr*ldHHpV)qr{@~9F=~5}N zmXvBb!lw$)7oIFUl|SK$`6%Vy)a&hTth`$xyoEm2`fC43$5WbRR4R7U{l4?>gn`Ki zTpufQX?GLmEu4dlGj1BkX>&1V;cZ^aWalKuB6~l}9{Fc^m0U(D$N$V9;xdd;;m7{r zzIS|*_jYdmM`N$|zjpTesobW9&#a{`iIjHzT_&Y$a&j$eAJ@e68fl-{Z<$f;AELrs z@}XKg)=QtmZ*x?#9haZT-5guo+Z{G#rVtfREA{0(ei5-4g`vb?l>qS{&RPCsad!4Y z+x3hKW3IhcP)fSOT`_8KuHv7(c(cs=iOU>w61%3SCkN%#`aiMRR3n$o1^TP1sS8po z+dAtE$4&cPM^(o>drwPa`J7Zz+$K1MA+%CV2nGD7{a+T8$}9G${@us7hTi)6V)F;b zX$|Qw;#URt=CaLRyqMg~rYF=bek^&TbvjvM7<9gAGw(zf$7*X6_=`#x`yN|o>pAxa z?i2QbQWNQja>SBh?JW#6!Er<$h9g1EJK|~g?BVxH-mQ7#?4{GUvLi!;jocjVfKoGK zfBF1kUpn4)H%(L1ROcDtBbc-1!#(RC>aplou?w`J?6YRreo^{5%DQekK2%CcPWxVa zFUvlD16^g-)Y^p;13P>b3bHf5yxZt*v)etdw!In3??)%|1I(Jz!PN3)5=u-@+~B#H zQY>|cd%9zHbq2~H%iAb86uI#sjof>b+8|h6U7GHFfA5+Ih0>mKEHd;{>PW@y?@2LdgjirzKyY9;9+ni ze=%+R3z5{>?w=CIm-wQ%KcR}Xgjh!SQ0T-}H>$<@Y1G`wx0Oc;rEQ&U73^#(@f0Z0g z_%r3}62sHmfyeG4?-Eb)cet&_3bm2mlkO8@!Wwa#l5F)`7AO;xs@9jR`z`mBEZaW$ znwh40qsJpN{Pn%}vsOH`+-ZKT)Rle@U-H!m9QU7&wzRcPyOBOT(UsIWWBLn=QmrnB zb)e;AxjTQ{?52OMwb5T76@~eNB-N5HD^)G^l@Ark+Sgjy60@XRdAMJ58k3@{gKGQ@>I)wm{-1T2`i=LulOI0ka3ke3nMm>hfeECU>?DQ$B!EbBnl?yQbHTT??lKzw^J7*C?yQy%yIKZoYc=gF-oa z(tkdsxa8s$54{d#ukbFk5SBSw(=HF zxvl&dI@~-hIOUIo*EvpDZTDJp_!gwA^n$HB^h10O)YROr?hXAAEav~z+cW2{OxxY; z+izs83d{-r;@cdE@!o{7$&VaQZEI7W7H^rn*HO`S&c4s~we%(bnXs3?Vt!8Rz&)bD zM@YJ4w?DExDn zIIfYq;qOFH*fAZQA`RP{@ zC%LM5yskIwRH`o=;kI$Dgi6Ag7&ywRE>|rCVM>)sU(ym88C~w!B?%THlhYkUQKAZU^^C%o{2mIvcv=PtC9PxY51- z_lG?Gw$SN+KVSAb^kH_v9k%SXyy|{C?ayLwCYhe;E+MglYp8XRxQeSHv=e;%Tf!<> z1FkPLmKIAtlk5Cf{7ktw%=d3l4%QMrjkXHk2_5y@y*6*{oRtr9?k;=SqEHX{3f$g# zG1fDTeDJ$InIHK-%Jz1_(QVUqu{6km+_n2E?b@zL|s;CDD%Ba|jy^toI>`CQ_>TF)PrZ-q4{knC3c5z>2VX2$7<@;W<9X8+RAk41 zq%TrGODUO9(NzQP|2XSg))LY;#6myiA4;Ez9l1_mS1OJWqi`>ChQBR*O&7v?-cPy^ z{WG{Q)F;6v6t1Lf%1!z5rkx~6C!`HrR3D`uGJiK;)|y5}1m_2a1~dJ?w4ZD=EnFMHXVXksqZb?5j#&X$Go_K<+gSDXZ zbiKAttrlArNe`YW*zdbnP$GBXEA4h$W<^Gon zdlq)|?aprYsCVwf@a6DhZ+^i>^IO|_OQFz4ZsX?Da*}qs&N+s;esV{glPo;}B@Lu` zQdM5y%Fzs|v6LyLl9_ZI_m=d!Sc&{-bT_-xmHIT*6PglA37ij2DNM+(lTC6a`ObtJ z_Mz6HaGaErxrX9b3DeTvOB_#fq!%GvZ}b>UCpy0DJ$qt8o9+H;i0 zbcE>eY)RXgG|f@QR@e2dr;T%vG6B}DCy9Mz0eV+E^8o*z{GC{rjwhe;ABdg#iljNL zbvHK~>5J7(k@2Cz;Ox*!|5;z!)4Zp@dBdT4p|bwlf%RsBmgSh|k{#nM8RB33 zI$?}7T)M-hkdxwQ`7QYnSB)eHv&C11OjyCMY7%pnJ}CB4_^s#q+z_)Z_qGt_zo0qVe$A)a=i){{ml_ZItVz zv#0HjZH}XY{hIZV#VY+Rq=;+8&qYNDlG4I^@&)la=|og6m#@yRGQGOjh-jH=|JZkt zD&b+_&%+0Ucl?iizZQI2urAy$Ix(_5TwUwP%kogEhwzU)+EK$*%yrax!>ZVB+7CKT zTLuWzKu4#PZ^X;+OyX1Frg%hL!%c#ifzP>FaHs!`ek0Z~maV?4rbT`T4hgOZ8{vNf z{}ed$mj+hEp2UWQSB3W(rKAG+k&q(9l!nd*u8oed*3Q=Y_GXR)*7DLuzO%FlW`f0q zKRCCfiP--lAyM;xH=7k4E{4ko`4tDm#{6D-tIy?3t+CKK9d0zZPDkbz0 z7hBfYUv^mSd#s~u!|hqNde%mAOYt``OZrNz%unP>^4DNJygfX(`W~KiCes?GWL(t- zYVEZFsxN9r)9D%dEvJ@SKkQ2j18L9Jmv;LnLqg?ELP@~`&ujz4W5%So0&wlcPg%1L31xJWD| zHW6mQy8lM*78m1=!qcU$@EqGp?i*Y56Ivy`r~aL)N4rK{v0<^iXw~Sq(T z?N$9u?7lk7EGv{0e&Z+eA4~a`*Q|RjtK@H#dX{ACa3xQgCan}y;kNLLX!5Udi^&?U z63l-8FssnF=vVN>;1m6fK3+epy%qZ{wpy*E{T!_g+G-d(63vTl2l;Dj)$|tTH>4-m zmOII1NJ*BpN}Ak3TC3Qti!5d3y3#+=cJY{aU-(JbCG6xc!1IxP@C5l9^_xR!U0TgZ z(WmG(!>?J?SX718yxMBN*gw&y(H=2by`nbL)@k+iCg%I_4EG|RAhwXg(i|nITvJr} zlvGo)$b;lV;takf_dfqfcwcy#`=0D2Z*zGNO_4z_LI_rCwrS(4gdi|2VP@k+{)Y}+OjrQ=A{7rany`Eg)mh-7X zV_`3Nxv#|OVnEm|lo3mc1BCrt2ks{K5S~ZRpbEr1WRNwqiTRZ=(s;$3VJvi;o zMi1kuUR8JKhxN(ugl&YrU!P(mo7YW&mV&6LKe?U!ulxqSweY1_UHVZxDJ&CAp}%-s zn8J7CMsw$&r{$0l@FduvTVZv4pV8H@n8)B*sb8O|ziQMnUon3(l8w)duI5E}cJrOF z+vsn83eRxX!qeezAPVRecpm*7Hmjbi0+1q@9zCg!-rtTT5OpYEi%bLZ^O=csIJJ6EFK)EzTo+NSxDb6kCZg9VFuX7%*JshdrPh<_mbnJp?h!ya>em?mcA~3o^ z)JRW=+i{V55G$~Pu7o%bJAGvO%mxq-l16jQYJh_)5Y@5;VvYKetK=|w8{%>HLJUD^ zZUw}OGy{HXNh^qGN`dH;{t!=vaa|bU8GtB(9EgJY6|A-vyy{knKIsP$IO`#vVLL=g zY=$^X8E_ncxEU8j<8&dfgDmwyn?2yXJ4C41AgZDa#5x@Y8TP<4-2$Mt1ERWg(0Vr1 z&VwirjI+23Yx9K=mGUAv2k|g_Ax280yXZ9f5#0xI7JGp2a7e|Nznu`D;fF|*3LxcO zh`?wEkxXvTuLoMy8lL>S;CWmhh|8!8F$1L`Ql}?))5k!s7L-hf=qCx<*bZuAJYON8 zJprif0IFHE7-(<3!`l*Xo)~;>_905kxogV(;%c_EUO9Vx&^YwprjA}wZNlh6jJm>TPK?YgfI1kBi-FGY%5)K`dDy9b<(NPnd((82xO659)+PLtFl(faxqBxgsAXKk{a=xF{8qkIBoT z(lJ*6`WP{dG1eHpjPwy9*ls*S$uP1RsbVRnOOzdJvve$le@qALeFvn>G=ue7yWi|dY>fRDBZDzQ+*9=J__sG(;TKCI2C#o)L_Asp zqnZ&b_>CR_@xW||wqsUA%piI&PCi}^Wk59H2XTh9Q6A)v)X}f9G$y4RuCYWs;y%tD zDJoC{quCXx5tj=Qff691P!9|Xs0G9i;slYyu32A2ts#04ooGMoL-^hYdq;v<5iO5M zK)c4#fu)&65ee)MOCbmRW%$4x_MQR0H^JU8!8Gn)YX3w15 z*e;X^xnnwF4pA5XU@9UK=`y4vJ*13f@W42KtcU6N2P!Z6L(O52WVS$Gi5A2j&U_bQ zPKGkf51`HP9I=N;XK!D~fCNM$dlLiu0UlV6^>s{V7{c<{Cy=%bNWyD8Ltloe=x-5Q z_$~+jBBs#yBJvPTh$H5G7;of-c12%;zYGD)7Kk5AWgY>oiS}fgMt!hDg%Zpz%qpm1 zreD3t6JRN{Z?uROB8SnzQHXg8fafAR@6fh$QUAY|KR5Xi%4VN2H9h;^>Sv!XAq?&_1X^{KI~TG}v2B$Q!l6 zxM6#6++uA)ua2WK^XiCL#vQSTV>V)yjdn;45rH!Wq>JMZLn^*^f*8U)^oGnAGw+Bv zgLP>5AgUn#6|V7(9ei(y`DdJ|;EW5)#W73m8pSN`&^K4wxT_do}bFC@IQ-zu4$b$^2Z*< z{44gu81O>6h-+*)>(O}5=3t0;Y-@asMp>ajU>)R%?7QA=!; zL(hU5Ld~PxC@-@GwjF8W6^^6~=}3oJ4fC)p>nDhPHsUk8Vr_qsmC&;?&x9QDT}Eaf z^qEK%IUpUTJ-kA`c%V%YDToDZ8`BngE#%BR36l$R*ci#anQfU*M%yCAxQ|A!hTasZ zu<;aq5B4_X#afIKAx&lzj?8FbJP;9RGd$QYreg2KUWel%B8?%3=9~FEjFe`0j>CUEq>U6@7yw)abzKV>^JBWkPmAc>&GZRO3(TXaz=@<&tq9U zXT2jX5w-y_kLBa{(pVDZiq}B@7XL$v>>PjD=h+#v18R-wpWUyZec}h^#jU|?z(zhq z?(_CTE3&fjUmp5C_BX~qK2|VlIO?#`4Uvln8$TFESX%sk0?V;CWU+3XFQXIZ#eNY% z@ej@cu{6{2^Jv1}iM8BOJeigkP!vuOZ)Go6MT8Dkux&vwy{TzCzxDUj2 z74(b@b=dkibw=Y^Nrq8oW#orc7)B8#I0t%m756Y~yg?oeiHHQ|>##0b8a2RLjn_y4 zsUdyzi)=25*T?~{SsuH>`5T+xvT>1Fkm-ZbMCyzx^Hg}nMt!Dv^mwRkc4voqZ1lsK z3u>M9RqS0%cj)u52GU`6XI5sb3`iMku+;(V-{>8g{a7BBV+Z;jJTL_*G2hDk5vJfL zp36KN`aO0}&1OXC(GbH(f$_pRC=W`3b4`XR=7-VCVgEtQ#>+AMG0x1-VQrM2wU}9k zjRS0!!p0cH4jW;a72`HzS4@le9hW8ki#kLp;(EeXqn_is!E-i`U>d|0;FyROWL9KU zQTv$3IH7!qK2{2;Gk#3>NDpOa9w&}VR+=H4^$|oJqKO@GyfO60bJ(1W`B;X3Hiuy2 zH1;i|g;JsYn5RH(Ge3Yxz!swYnU8`5xW*687f~}S}#m}F~kE3jy zJN_bVY!z~d%Z#%GP!{~f(Z=kF)QnI(`j zlLN7f9~^<={t@ZN$6vH35CmEbWpM-|zR@!=^fN@_IZJ2rq33HM8vc_iLtGqv>~56x z9>gd5FXrK(Vv#gho0Vqx#j?zkBJ!9&jE`IK5)9)Ex!4LEJJ3c<7tDrn#KeCw70=l> z8!P`)gREXW2X)P8#<3hvVW^B_j;)-a{@I;u+;ib>80?-7ORy`P!9Bav!~TQwGlnX} zIX**R=*1p`7H4RPdxN-@n9fjdC@srHJH-73S}Se`W_?D9Aq%yMmc?)8WtjdE6D)_# z$1s)6`JQn=1Ta(}axfoL7y^(J8z+$i%77_oK{oa>>&Hi7%)z@CHn&Ibh4&VCjk7uY z#vC?B!_hVL?-kZ#{)yp(jn)h?3=xd`vlNtqc{?_rV5^DGs|vo7`@%!!v|R!3ih9t>N8$YJyuS{dq)GaKD752eC# zOakPIxIr$AHeSW)u-y1u0#lKDTn0w{Sqjs^GkK9R)@58-4@ch2SK=Hqo*$PYE-zkV zJtiB@JmcC$bRuRE7xB5`^S^Ol#L^H^tdFrf3wDpq5Q!z@2*p_`+LArjV(7-JXZIdh zht2Wg_qy@>7QCOqJ`pH-0)kHr*)^_g#PB1P4>)%-Tfjh9EtIH34ZYI81JOs-C#$1NKIOb3z*pnHm5MRs>p?;Y! z#1hyO5s#=fL_8uD&lyG$$5<-vAs98*rxA0{)53Gcfmsf@us=j~+`8zK7*|9En-4rI z&+fd~lMTcauFdhoOe>4>g9C!#;%;Lwlo**?Jq=hiRO7GG^U)3G_VZKhR&G7m32k zD*6{(!Na}6@ChqMKKP5yF`fXU3NRh_F2gbbNWoNA4zE~QUr_`C?jcqHkp@^IA5w6i zC2vt0i!;C|1y+-JG4yfB-v{5g*A&vmGKFxC2lI5e!iOI=Gr$=Ep0QRizZ>_N=y9=S z*gjT=%@r{f=Mv2GVjXsdn2z_BXJagmA=n2|+t23WXy5p~2J-=IMu*QQaDATnCqx%} zW{m6c_@ovQXNPq?T-ifkg0*n99zVFBRzlIY6Y}x-aw7aPYH_SG|HSUC@ChK|0T1Sl znfGPmRon|;tJwT0t_`#z+6q?>aLmA5Hd?S~6VxBOOJF^gQ9=aaAM-Eql@P3nbeYc4 z8aOAzv5c+a;}zZ-AHwD~SQ}-*5=aeeF#Dt31ArVh$}yW^YmhgKlEEif zXdgs5-tS^tP)g-h|WnYiZUt=QArr(lm`{fgSGnOE`X9k--Gd69%upPI6T54#PK(h$kl2BPTR zg1EK8WDM+PG=WTlD8GqhEbPqH26jfO4e@p5fICJS;f`|WA*yc=#P)qlmqCLX*?WY~bobS8-bUkooyC%3( zJTG|8yC=C@y6x@{U8>^``{%X>R*xkG-pW<@YjmFRBJ44e7Pg1V2Iu(me1dN;yl*$( zyFPzt{zu-n1-lA%7F6+VD6AJ)8vG%2I=nFYgE~8G4qXd-rzj>>~9nbxZI z-VTeiqDylPcjvoDdIBE9bI0?Ir_)s%-yOIwcp2S=8(x=ZvVC77NA9>3>I!nVYv2@^bL;Z62$U7H+L?T4)GEhD8( zJSB$NM(Y>(B6uM9ZeYJJJ-=!GtKRRuSD#kRo%(ce{u|z*`N#6t6%6!Sf?2`t;Wg1g z+G^O*>K$U?zZdt*t>rHi%~IWd+WDh1E++CU1RjvTuP`%d3)iId6+M?0q}`ZEwU^=+6wM zgpP)DqW@@FhG7zNgUc1>Nx#Tt6o;je{hFhXGvE+hwLRq$|40aWT6)GLxD)^MoN?^{ zSqdER%BFHwx+p&350IDj{gH~{%aO97D?Z>*5c8!Lw8EUM`5{PxAfl`zlZ^ zR54sE@+dk@FKbQ!U$lvPEbNpUDjCxImfrUA&brQF&K##BflCZ~vfRBqQevLxqWc9` zU#HiRW3O-hL4H-r6n-P`n#a{X;jUpWyfJX1Am5u^(7iAdc7r&VH>#kv|Br&11!sMe zgXN$fxgw9Fujrl3OW-F~a32caOC>Fn7?Az@|Lx!^>f>N=NjiGhi)(LZjx}$^`5<) z{fuLvqoQ@Zd`xmk(}jDaD^1jYj!g^g3wgq3uyx^E1^I=q2X_Q&6z2Pm29AcEp?ksh zVP|Z#I#_FBbkcoBD`F8Q2?K?RTx0Q)#bcc-J0;@y-T8}ShxHG~dFLqi7f!)3#eT=u z$o3`pfUDBaLVvCmXNNuJ>S&)v%0^0uF9wSFmloOz%NDjP9O?J@O9vi?I)(M{Q5eV% zX{+>QYI(gqUBi6nx2~^mXnb&9;e6l7(ATk7RWZ6edO|yD zen=%d zgT?MbCwfNzMBA#3jg=49_kZJSSlGvRtiW0@FyCEJQ0Rg;a9ahg1QRsLn5~DkUFK)} zdGU4W7{5roFTV=<|3aK3Pq*4_FIbCNn>fCAeC=%O>i_&lPT zi|9GMb}TF0K72p)V(?Xehr(mt>jk3!vVV8s?BMY54^dI=t9=KN)GNt5W(D3Mt&tLi zlROoZl!MAP*c)P?CCygWvftXq{<*!M^=-2wDJdJ;4XOBP=7Rq<~K2aT7_aavoA7w=j>w{Dcii$i7Ey30D!lBBGWxAQmT z>Nd?fK$fJ-*0&v3UE3W?ZHFzX)>~pN;Sg7gNM=dBv3cA$tj&jah(8Xe29vyfz3=Cp z$v^0I`qui@!bgFDkuWUne$Mv-zrNEp$jZsp>2-ZDSt*6ZV!{yq5no*@Z+U7v=X%HS ztD~gjoa4OxW9NM9O)-x@0(<(DlZ%NZ^i`3{v25DXJQ21ReqVSkI53h`*x$Fz|8;n6 zY_O3`TfqCH7r~P*mv!!aT1ITe{XpN;wi+Q)o?bONl9xom{~F ztXI-!nL5lCj?qK9s_DjI+Qa-y>l920Tnje{>9B*VaHBfuN+nTSN zqs^82FKQdwKxw4Jw7gIxyhCkbHdHqkR`WlJHZ^v|8pdR8D*u5{4)$?d&iRb1;ne7- zW+!2c_&2u--huu(!I~Vl_ZDohvq~R(6-x!)4SP&((el+(#z?M(Tv~cl`dAzz4CAK2 z9)cG0pGf7@+*V8$&Jl+N8EIn>-eq3-!&0 znm^(VP4?dk+x5lTPtkheFVqC<>?E&yAaO>%Ro@oZL(iO~o(gS@$#k;(Gx#h)I^g;+ z=}$)oz6tdjALtX{y{v881$89}aK+@B(tYx#IoC*p_iaa*!RV0S(@?WmU89ll682cV zo;Hup66-5#r6=^u;DrKLv=(Vd)<>HKW1&^rKb9S?zV@EnH)@tPM?UA+Wpi_IeYt|Gf8zWEKc5<&6xbi0sV*^J(~3ulwAbAiFdr5sh`M<(xGMjC=m0m< zGTJhe?vHu&4D&SKQMszzv!c#3B?n4ibD#fJlW-4thE*oS?bwj(k|98>L+2Y3h4}QwT+Ly zuFm9E1LAq%KL0oD22&~84*F6*Wv=zU@ReCJ)*^Z^_J{b$a$Tt>M@0|U&TOY&)@G{J z`Je2nJ5lapu8Z~7TaXHB?eJzjSDqqI5cYDL#G2e7YKd-(zM{4z{X?t~)`JE`f&a3^&H*Hrn-Izc#L=BiU-8Ae}j zjWmFtL_2AjI?va$?6uB!4Y2Lhi#z7v81ysZZ1iq_;dpEUkxC zo7sZ&<`+sXGEeWVK7n@ytMD1N##Y`u=sQ@@PAe(ak!|(^?(6b2^@B(qGfbNpr_@g2 zZP9wBuGDcYw(quPlCq&PxzmCzr3tQi_9yZhH6z?j+aR;k|GN2P6)sB;-#C?|H{3-Kb zczmwKr*cgb=cko*<`FSAR&Qj!Zsf)Ggy#BQ^=;DY*=l-Ro=kU1@nC3xw~N1bWQw*~ z_`q7$-VJtr%7wXdqSRO!q3eO!fn?(pIb^wMKWg)uXQRW^Q=|^OnJ$a|@TsUN9^lp# zeBl3E7~xthwlX_NGnIA5^gw2)i(wIXX*}#@yj|HXS5jLAJ&|y@vUghGjNCEpy97dR zs};E0#wvfX@K|h>IbFKsVGuV;YZ#jgUjGzb$BWM5?tbE$Q02l&(TlN{quWU|(UR)3A2VtfjLLbb zpe(l-#-!3(o>0fqQJD>Qasz~@e@EU+L78uE@8~LT+050^282gMEqqPsm+-~H)6p(; z1rcrjB(MFqNa@gKtuxG=I*7%^TEwf`Xb;$tr-|7n+Cgig)l^rLOl6a`x;ZSiSX;rX z;sV%Hd9IX5mIdegP08ijD<3d#!swsrpPqkTy{t^ICEHingSHA}0?a2TgxUmms!!-C zZkT;`LLJ8{!yDbl-xWXbEzjB<+Gi6Jvm94kUe`=MH}93qQF#~1C`WngXv>X+O9@f# ze)OPGCDJ9Ic&q5!ggj}iV~s6Wt|DC!mqnI8-j(%IaFBS&$tMhPAF_<5%VP8LNBdHB zllPG-{2%sdmg@XPbEcUYI~$#@UnP0`Dy4$lP4H;zG>1?{$%X%3@JMh;9y;In8J6i@ z(Oy$GX|L<6qi`puG_(FpGmPy#j6mv7;j_jXsig2()EgSF*ON_gny^XU3lCttfdRSc z;p0*zS(M8sy`AQgrv)>;?F!O^W63mUb9;I5Htg@*n_D83mmiT|!ms;uUuX4_ty${P zl>dvpw~mtH>K3=lt9^QU+-I0!aCdhJ?j%U?0Kq*0f&~d7xCIFA0fJkC1_pNq77GTSF1ESsp{~q+nib zCf6Ewe`lUGM(V1%t%;}W0xfqG)sQ%kFUJi;P4Wvdi97DPO*Uorvss`A&Qkuodnp;L zdBfCn3AR}AI>pmxm<)OpDe-3KP-|}orkdi_kvV9;W>1h~DPQr4^oBgoj=`6TN3m1n z_tbhZLv)Bm!b!nbEW#ga9_riEk8FXqwcKES41B)SHYNI9R8b;kjjy<3?<*W6?!qeW zU}_AW27JyWab9P%tXZC_hq}-Ng$;Vv8nTShmZV|>JCtGkJ6jyE5vWyA;$%G zic}&+;{H@AdDFd>`$Ei=?}_K7Y+?+1neC(NQ4ZB@nEWUXDh znT?aPt*gqenIGE@@OJ8{`Wdxc+K9CxZvyw_K0c19!auCoXa3+3Cha2V#x*lH2S$&KKj6Yy(YQ-3DDF zRSjv7-|fJNzNn1m~}|U5+Wzuech~ z74ezEaA#LnEq4gMQGM3n<%Jon>KV9!Uk$6==L%UubMmA%M*Ciy&dJr_kGKNuT^$X%>cU4w^Dbq|`Mt~j-MiCH@=lcWMM zpB`d7X!w>pR?@X3#-m)qP*4Cm1qTy1x8!)b`5xHE9^B23@_J{pC*IUYzkMol)Rm+=Jgq0pEd1GzQ zzu?|*-@skmgWOMqILLHxm~3vCVVb47NM*5|nJq%LdldCZf6#E+*w*`w*Eww`)p=?p zd5aO*IP#Kog7|{E2CRcI+F6EGhOXLbL`O&a(t3qqg<+-l96RyZ><1#%-P3-nEV^KM z2A%mRXHDT4OE10ykpusslu9&L*D>ERsJwT@dVU;zL;J+*qHmh_CG|%5CRM9y#zd=n zYoBS0)SYzs#`mU%KJ$F$n__^M#nCq?KRSe2uDV23ldHRLa6`F1TqAKFUXxl+9>yN}2C$>t zI_txVZz?vM7dYOLw~g9>IJmBMF=;e4@l^LwAwdk0;5Q6a2d{3XPCg5Ld-|#TyZUAN zv^RwqZ|PdI6qP}2kiG_PM55%v((#$}7F7^AikB=6%B@B1^Q-5s&%BhO%Nmzs$=I7V zF$*gh;#??mTFh^l|2soIdC=X=cBOog`M!OgbdRo~SqYq#-r6Mimh=*{P&3^y%BzdM zP`ghzL+hh4d7bdrhg=Ju8raw0W-L_)QNI#)Y6a6)HCGd@KcK!r){(D>Lxnbu{nm@t zJoClk{rOzZ>#XvWcgeq`&P;J94NOkYS!w>Q5iWHOzhe8lMGz-XR+!E``?f+hQ2Q24OYj z$3y~~rRnMwX-f9GuHDW)rc{K{{hPfA=upi{cjsQna%DZwo}bk}V_xO#NO#l z;Z(;!X1b{`kO?f($4Nhy*Uqb#9hC(=XMSK76v7h$WSk5{c(wW!L`;OTl_ z+%0|~tQIcFDa>ob8SfUR`rbEa(@Ks-o$u*q82n+u)+FvPPwjdT)6h|2pf#f|NZ;wUVYK`L*F)z}>LERqj>!KIE#g9{XO>ahS6?e^p8*;+dOjhx)u~hpI>=sqO<^as>M$ zc|y|L$r5$$^wb6k6W^bEw=m&R^63mi#`~mzL@KE)B`|-L^PtWax~}@~buy~Qge+8- zx%w2Z$Ud6=bJ2F|Qc-61>qi-<`n?IX1%(Cq1#a}~9N($XZHAnTcdWGgWJDzOAx3OdtHO`!n z+&E#{2la>451o>N6IUi|h%b1vDDiGqEwc+t3;4Ch(R#Xi*J`v3^d>XSGxN>4L-LQ7 zM{vc|0?juDo$)K*jR7S=Pa`HppNpCmTx#q`&vg}+wk|nU9?Ny04Sbsi3=M4*z9+b`?*rXx@(9=5nqp0GoyN-4cTGb=SA|D}9P#}CSNRO$ z99B$hp+c#*_-_eFmnXNNF&sdtK zP5qEi{x1F5kp~AJ{`Piu{zhSt|F?CC)@7}#G;9*x#CX~rS+XzhT0#HPewMx*(7~u3 zj8SvUSS>ImY(P|0?C&wZg|spKq*^P>j()b~t~9YGHB%GsRn>IC%cN_sCRty`O3zX4 z*8HGu0RKO%5a)0koFV*H`7Hb`@=iBiS4TA)xA5L}z4=(l&0=-&{(>gCIhilg?8$QC z(8PQ3WiL8CIq_`R`+j+{n+|+aFQ)C__D@?csrxM80S3SL<%eZY$n9Sm?dU2V13u$s zO}ReIzhxL6bG^D$eSGxoKteZC{>A1hA6?;g*yN|G#>VeWXN)Ix=h(O8dSV^*TxHVL z*IPB|^aIJ)yLGQ; zJpb-h*xN?$LlduLN82RY6fmp$^`=bgeod>@Xc5$#O}3pY9F;pce_C2RpSUlqpV_)_vEnP~DE2k6n79F1g;r7#nx+QM z%Wj-#EYtpgH{|BpPum_^-S*#vq2yQeb?O`H992X>MlGoZ)>tYBB5l+1VfpR5t$24DvDb~dDzU6hy6U(2K z-6+3kIqpal+o<~lZi@a;qhGCRwN6Ig^KY*Kj;Ywe)z!Y#F~>E~_1baPIo+Kq-yv74 z(hOI8p7^vfUeGO59l^EYeD_8nQamV*mEKC9Nqxk7LQQEQ=1UABBJgl&G1t{Pwd7b{ zaWxOQh7Is7(ydh2 zQ?YCd(jofuZ5?6OFv|&N73`U+yK#fRBXCVXrYYBJt6_z{zix%z=w;TuWcQMFr3swY zIoov-zQ<3*kB~j#w|4{aHQX&wN+vQ-RR>kyGY`nl*hJwjXXWO)PYHX(S%Qh*?})J; zF27OMxqPVkHvG-hx{N4pojW*VU&@B$mdP<`Z}OH@WI4~v3F;|E!iV>39c&D#7W_kC zYyXKp7rm~ktI;v!4csm{rDph-#BgFUIhC459iq=ME7`#+PIFE3M*W$(Q1v+zO?^j< z#A)msd5{z-eJ^d520(78>clgmoZJDwEgd1RQje%5R5dC9erc;prr}wbO?zrjnoEyki`ebVT~?#& z$=+d(u`QX)l#P5x6kvDo34}l7Rf?wK$#cYJd>NJrd8BSh#nM{H_(aGJ#J+qV=R4ax z>tc(g;<4qnWw5!hB(M<6ZIk(B+LM%{X~kJn3nrEhvIV+7U_5nFHCvzWo#1=Lzf;iu z;J_e1|7Lz0eI6Kl7%uDk>W-<;G2zr3{H{z(anfQe3)d1`$Z?Pd1S3`w2}B?jPt|7T zFzcCSw2l0k{F8h~a#Sq*o_ByAOslCc2?JzTlEgnmLfj$T6=H+|d}r4ZN1APbMKX7) z_^l$eV!F9$`A;Q|{Lgc~$ZDF|IAeBB?cytDr`4ZtBHzSqWG&Tct;MUi&t~5){!ap1 z0&Vnj--Eu}y$=~9y)z8cwGGtI>BGdY_yM3qoyTVop?ECb8J{UnmEL0v@iWmB2!t=` z`*dUA+?nXB^ksSiQ^wR`a;dLK2lkaTOSA%gZj!KDc<3I)Z+3NZbhlNtQkFC3)bb1E zXUjI1NQFD|soYyxgECfRoX);gu)cJ-bt$()auB`f1lFaS;XTHuz%McA*WiPJIsUi( zVtnhG<{E$UI%znod#u_C3`HQX!JRwzut|6?;Fb-=4#*XfMY@eWB5spE($Q=d6Hn*S z)!Di1X|{{%tty_KNC%J;@b@w!g^K>-CE+XJt+SlfdD-#U=CHc0UqXI^E9S-K-DPV^ zY809Cu4Pxr{vzv4*2=slC9HX(ZID}oJs^si1noqxDW)NQF2BP8b%Q<+c<9UdHuuZ* z4)lsO)X}X`w`Gn~Ii#C7fw#b~;x6D}HI^C3R`>&aZ&DMlLf)iSQ8%a(`ZzP5ZLB(@ zqSX`CO;tmgQM7^Dji<_b$Tri?{glgcJ#=<=1lSwed?2^O{_@E3*wUbq0mWwu&gag| zu8}z^ePKp!_WAsGrR{9Hxx-Qqs+nquW}D%j$Qvia%N+C9<;kUirF)7)im*akUf-NnS>I)7GJeV0 zk#8$XEWhqpEy8aVRH3SmexmVb(<|S)0il7<{Nw#b`3XLG-hJUNi5WV#x-0u5B~oW7 z8$m%9r2#}A$d+|XR^u9iBu|ianDMH7wh7xy{Z1pQ)#}6Qz3PdoNTv ziP!KqjJI~M;XCg+eq7-2pap?H1ibTq?_1#g+$-DbfuXn7Th*F9OOJqSN+$d=x|Kde zbs}d_Q)$RDLych!DjU0!oe0m)aJS4<#*6LEb_crfAUc8Y!9SN{#SQLNd>psW6%848 zHd_x^M03ybgJrkMyvveHIu?GH7mk1Ia>!D=6L2ub`;whvhnCN z9W=*Osc?5dC%7tKp}NtBn32p;st)-j5se>_%=%Np~U@pFOs`_}ev<=@0V&NtI% zi0QKNk=HG+V*P53Ni`C#s;$XwL>V!X@}MKIf`TGT+Y zBN;*UByeISbq}mKt$wXNuf?^p?grdd<c zi9JBQ$J^lv_$u6ruOJ6g{i${IbjC}aqsh^ofnVVUG9koaF@Rs;9P60km}b9k5lTiB z{*>K0T~2KH0e}DXo87OPKmX?WqU4rk?Ws)x@zs8;*R6J+Y6Bu$1=TXHX9KZS_RS?@ za*MK?<^5WA#JxcM&Uabp^eU@k_C!yQ!Xjn_ck_9mZi{Vqy|K-;l{>1+A?#yitM3gR?e!Bm+;!PpSiY-biakJV&-OF=1dWMW9m7`r5Rv73jk&|E zH5V1vFJ4_{bqs|z^lopiJnrzs{B-5>ZLjCNU;6%3)?Kcb zk9SN!gZSoi>J|q!r5@Ot7KP@$$gP^^U3A@Y$-RMg`J_dCUVT&bhcP)JN4@$}q`RrD zz9rW=S#%RRO@?u$9~RUzbU>I_@Lm(o4i!4v&YGv0wYIO_1nH~#T06*KHFVbnGBaIY zT^GU#qh&Lf`{a#IZJ0?GeZBwOh;|uLFo3)P0V4qfW&P&XmoO?Dms=#D^ z?P^X8_udgpH|)@0cZ~yKuk=s2(6SXpB_(gIdxW|4d4t)%SLm6D;wnNFHguq|BRL)- zX;VRbQJ0EId|%3>EAW2gx6QwXPqHRZ*i`W1z0cF2xCQs$K57vkoD`hYEw#Ahi8{6l z-!Pz^wM9~u)>w32X-d7+Dw#L)Q_Qj4R_QzDSJUO_I`u;9iLt|iPpc9gOG+l@FE0Mo zc9`(f#hW$-tq&^=506+CR@=X>=AAI7qE=De!gZy={3&{i_bLCy{&Rd{48!PFu2Y4U zoa;o;531UsSWApKF@5 znYm>OixTRttj|`P9Jqr0!eK95m4C6cJr~1PH~#7SN5D5hDZ!nBW`(o~ys00F4Yu|! zo|WIMWR2qxHOp(E&t{+L+Dt;_xLp*TKH)>H_c8DKB}67qPZbmQzFU}`?K)xHRAo)pWwZQ_7e4dag#jc4{n&8QpNoQ8kN)ns~EK%p^jORg?IzUZNSgIq%^1Wc$F-C#(a zZ=>e;%wxWH-YCL~qAX|K)tH}66~S#IPgH3bIW}~e{{a1JY=v!P$%y>@`Gd*|IA6M! zVF=_rtHDG_lKEoRywvFl7v3_@<>y0QZF!NJFrxgvesI;;Mtz(0tt*8dr1chke*dib zna8qj6iTi_rm^YkD(d>(8o#ODA$YoOpYUs0QLeghhvNhBr8dQs7D7aAh*%!lHK?uc zRP_~pciGAOetE%#ttuLc9D7~2OnaM+$JcZI#m_Q!CD(a7^x59Wo1VwL8j(@e6=R%Q z)we--9Ndic7`Q^VjD;EqqqqpZlGW{CCHM)!k6&qTFaiWy|jg4S86Aj&=mNxm%(>_nw z^QPy^!%rh$G)%SIH+tW#n%!tavjMf$0j2U-vy}Z)+Ud0Z`6DW}@heq*0~=SrT0gEf z7PHU48FS9sE$?Q*Pvz%ayWmUfmSGnodq>TSxED$V`03Q}>+;-USI)ca_QhizBe5au zXzd@GR@4O7#-gd|Q3-?J4tPaAJ^WacFxdIfFQew*=FzPi)uO%n+h6A7rlzL`<-%R2 z+!Z2QJ363ebjzB%s>eotXNtjVnP=yn&!1vGAvDoU@t+X3Eqrn4-XPv@r!iYKQ7*A> zDlN=s3R24=ISc-X4bkjj{je0<*uq&E>g4Wke|bLj$(R>Sa@G*bqB}R+)^<)ab!30C zytqc1DJe8Pqv%`bWb(4{a%gCjsoGD~dPm+d){w%=x8$_R8C{&ueWlwMay7a}%<0J4 z!H>K-Hde~EJIeMIUd!KAJjm8cTF4C1zR|8_^4z=2u4J7{obq<=i$Txnmz`4waS?&n z>+vmKv@WXK%(tp@WLAg76$x<}OUh(vmDlMIXO%lOcGY?t!}v`i;wt*)*2_GeJHhIv zLj&$b-H7=vmXGY?carrMrdnGTXXQ=L|GM;vBLKV4E@98p#rPXO&ipj**No-K<31dH zJuZG|_CtQPsZG@e4SsA;w`!`_7H(`|YG(bcwgm~6?ecZ)BEQ?Aqob-Pi~LHnt(ycJ5lBy`0`XJsSX<+Q zwHdV%ro5W?RCqb6s8FSf9@fy%dRId(=!Rr0T%DGhGCikr`2*p-dY@1I;EM2JQ9Hu7 z`6aWs5NgqvY&GBIBIw>eS)qC1hTzY9-Ch$^t+9EowN}o&u5565h%H#C2j2((iM4P) zaWp8MkaatiOw_%5|03#btR>jTTI0(`LW{@-hy2F6OxYJwMyF5A-%}AR$(jj1L7{J> z4oAmDH1Yd^4snRZZ}aktHCzYHiGZXqHoRvr?)Qg2hq^8daNIRdE4g3X(cHz=2h%{t zmT6d#-M=J1=UT?f)PafOyU^qnWydLV(APB&Hu|OEcM*H&uH^%>eoLQ}eWY}nENJq* z$NO&&IT>yX&hiOUTZH2k{zU;LD%W&&kMF3EF=6k6xBI@+)uexu_POp@4wS(ire@yp zQnV7YsQWP5pUXz%zepYOVc6T4_+}r@7i^=mBkMP+-Lh)aQQ>W|ss&<-ciM@(mexpO zyRNorXz=rh9Z^z9jHwUx$Tpy`UEbZ&7;&oJFX%wHU-<4I#xz0mnrP}CVl$WREf`Q3 zQnJ9lP2SBMRIMdv^TW!I=9HyPOsEq7$J-G}Ev(JGht;g!e0s~l^#+^9SQ@5ZO{$ma zTXK_IsA}wO^V=M9CbDm2kHF*F%YwD+X8!%copzcD^-T?-!ny{3>l>!ON-vN~oINXo zi`NvS6m_%&2o0#6te##enye3VzE9C5%zxM9-T9P>_7vlj*x%~~H2l5hTWX3_jd7(|V`knEkNiR%^W*`?m76>vyRR6X^ou zve@d{hFOlAubPKjSGfG#aggsI%~fRa&M!#Yl|W?#y1q0Xi%hOQq59ZRliJ|47ljlB zmN&D-NqyM?Mqkrv_yzNU?|ZMyw4abP|Rm)Wfx zXT=(7e)kIS@%O!H+Gu#fmdd}nCfN^hj3hDp3iwCue%=x9imUlYwNX_7t8Qt0?x3?tXEy)<cDC3kmcA6F0I z16Dq9 zsZOPq67BIBkXyYE-_~{6{?eRMw$41xuHss|FSvW~xsXkHM@23B0I?@q)1Wo|;r&Yc zGxbI~C>$0$V$%sH`33C_8VZ@4Y#>`qt-$Yz5BXp|%e`KH!ZgvY)GyP1$)*!4-9wz; z+6~U{Tp8RXZkOvf*FLTs)}u{#NRC9i(e|fxkFA)m&aCs=5KupGtZ5hFuJ!Jz_%jtHMRi=ck&IQh5VG`L)6|EQaCp8)PVE0-_PKAHPSJpgh zWBVZcRr_SS)fFm4iQh=P+raIDY$olfH28gfkD;%ol-h>P7Pkp%EEm5+_hhfqB5oun zP@~}6ljU>@@e4j(zTht6+e!Z9Ir=eMg>6KAkoyarxfjm6u!in9tl^?v(XLti2X`aL zeA!4W<-c_WyX#8rh?`7-`fJSy)d%VVOd{i?yHS_%E*LEj z6`KjS;l8aAl${>Ww4%Eco1|f42;3nxQTWbXgP-PHY-j9aol*Q<-s}z+!lgK2nEQdS z7sIKV>_YVobts!g&mprQquOL*2z*b`Tu#SIi7Rw%Hb&*kj;44yKz!+*>0TtN@qyHR z`V?J<`T^f4Ugl?TKXYriS@1nsl(UM1a>l#PxqYRo@;q2S_>(wEZb=NJ7qc1gZRB_& zUfu$CiwXF7$Z+xpd}lt6x`PjawUJ9O13nCIf<1t&O4FquBhC>oiZ{g{MLXyl0vQpy;3J7x_>EQ%I_gx$DblD4intO4YX{|(km^(AWK?jTqXyJ^Y7c&9(Fab+}yZQP+#+yNqYXpZJ9jCB!a0g#MX}RPhAC#+MKSM^1d$Iv! zs<5fpV=M#rA{N7U#hbBjVNL8TY!7w|epjh3JrUo7pWi^o6XI7`2mCqaz~kPhvM@BPoM<8!NwoObFSKqu~))l_j6SyzyXsCakHRBPU9$q%Y*n*m!&d z&SU+sEl|=%+Ag)1O;~qWQQQREi7kcP8PTw^_bBWi!KT3K>8G$pxiuD#sqkd17*-DN zlUhqYkmF`8M8X8P_iPHRORfbOC8lCCVNG%ltVKQxUR){v4pN6=!y)@b0_5!21}lVj zLgtW6u&EJfIf-Rs`H+9*BAg)(`CG^)@}t}bytxtj{Rx>y7+8B93fVB4!D{vevPF6* zy@RYV+vI^5kBQi8SdmjAe-85%1*@>Wk}tzrvC-g%0PMBA7qUKO!rJs6Fn8-fdm^lj zj+39kye`6SV825APX&L?lWWPnAj?a(+y&c$J;DxvrCKacz76?JwnJuP9BU08e-3N0 z=Rn4i*^sM506h(1U3@(3`NJA-0y2Ko1J*$WMD=%&&t*5{TwDa1YRvLIIUQE3yFhAv zXqO;AhP)*Ouoiv**tik&1OOX29%fmR2f#=gfgQKy8IWJ8xjY|MR9DF1uqN9I*+6n3 zi$wvf*lZ5DT_}*tfIod<<@juvu}CZ#R*J6z&us?#t3y_k)-d~#uvVRg`R|5}!8${q zyYf1){u{{h0-0W6HF_IZyZ;uB?m%u7C-{?umFI6EKZ;Fm@p0w&a;!a;1vz3)e6(2y zG0_lKsAtNrVFu!1t#=H>RVP>}?*zNjVfA|mWQReTjBwxi8hI>eIt?Qa#M(hFo2roG z1oe9Y&)2YG-UKrl1ueDU*DQ#ToA7!CBSe$|#HvHg14Jx9Yz6{6podZl-eu@p@R7J+ zhE@NFcaRFU#Dm?*u=-x%ft17k2axs@W-Jj_=A-@ra27RK20CkOK8B*4}zv#_>5 z99HVPzynX=40{Q+T8REeKp!XqzZ`@W^*3N-;o#4XQ0@infl^=$sKK|jVN80=0veDv za==y{#6T60=M8>E>VO$Ugb+8>`oWC(fR{@^e;mZcQ^+(^2p&?{Q5vWZ2D!0t zMX3Vq)W9!591+CCLUacY0Ywe%A|a~`(jNltv>*|&){I~s%EBUpeTXE2h!=?KfSxY! z0pcF`LIeiE9HM+AR*;T38Hf>~FeFfh4aAB;j2MNaUIF!PAV?regFn2C&_V?1O6C;Q z77<;LZOHoYkGW`~U{4P=A=-7IyxE2a83Q;hWEFp+9qmX8h1q!tl(P7XFQSp>A#J5rGKp8Fkz*i^}4$5hQs3eH2 zq|k8akJg~Rh=8K7u@MnTVc;nQ5`|>0(0&j(Lm>honmm$(=r$-~(OjWAWkiULe|=Y@zh z&X4jcKDM!atW!8UNE#Zohnj-k6+cvZ&_kl}kb4kA3o((9*E~!!h4-RtQMrdcg;+(1 zBBZd1&`hD3MC2nxqfr=HD3-L)3bm_jk9wflLi35D1VxeNW0W8*3h4&PMRACtM4@OR z<`>egkPA^c%DsbHBRiC>!fQful|2+ao))MCS)oJ}Vtk^gLNd`v(a3NV5O8iG){zp& z3O@$<#B-LQvp{*Gt0Lkep~ygelou-TcoF$gxoRjBIAtFZwve{UEh<4rND`V6BvavV zAxls%^gyw#%nqtmUg%gkU*PRyEussd=tVJ)WGTnW9*XtKG9&@XQM4+OmGK}8l<}ZC zr41UtQm&Mt^2+g{J>*4nO+ow0o@b1n@u3>@K+@58m3C;avPP*z{zrP12kL{`R!UZU z9TV>ZhD7O0P<7$R=csN4`?;=~XF7*+X-r==5kr zl9m0+p8wuk>4WN(tHGyzQCsCasBGoY>hYFGzM@%~qmOE#ucFl>(Q~9+Lp*XkwP>7* z6`$68D#_DQk>inu-qE8{o8mvEM6uG-R?(pJP-YU9DD(YkbfFR@h7?$NwP$-aL|#4&`d|X$h)F4^R8bI`r31W$#bxm3^g; zr?(;ll_47xDN3EPUpeYZuPS?r2T@y3jWS}zr)Z1xdCHV=R+cH{=osxO9`nozI`&BM z$Wd%k%8}eptwVK6399kzDLp(EDCNo=D_2a^PO+zQudSGI}- z)Jl<$`YU!RwxasVmm<|;qY}fFqg8D2Y(0`aBlGA~EK+1tdcdO{)hhZFT^{YIRCy{b zJob8yJauSvp8d*^C}n8t*+c&H?4$O|!?TC%QX*fGfsQ?1LE}N~mGPkW%5y~N@A0kD z%dsHj7adoQLD5}lA3Cm#chnBe7J8r>&-T-? zD85x>D)k<{%DWOTo|;Mtid~g6gK8AHigiev=NOH)a^y-&r3CFOdOUl|u~PQ$b)Ghg z1dm6Qwu&x~Mo)j#Qt9=nA3R<|79%?=k3FrG{+>ETf~U^Y9v!27MW?5&r{~8Z|4Wp5 zJn*sGN1bR-vB1+?sq<_-_IY|%wnZ(Jqfc#D&P=8Er{x~|k!_x*XB3_hRQ5u9o>3^J z|62EH+5c((KU@2+CI8;nWAVS2|KE-kTb0@PzqS2)*8k6s{eNELfA7rw*N*4^9OwVN zBl+)?{&yYMf2Ys>y0ZUk&x!vEKl~Ga{}o>RtH}5#{{AX6_4l;%pZNQG&g)-gRR6@^ zUuCBLo_78de}B(;{i}@XpZNQ$%+%l0&VSVo3l~Mf@e}9#k`g_{>PyGEo=k>2L zs(<3|uQF4APdopKzrW|a{#8cxPyGE=X6o;0=RfiH_ngzTEZ|VPp#{X9xX#LB-KK@s$NB*^i z=g6}P$aDO^d-rHi-Yey!<4<4z+W+5Mf4Z)vQijs!eZ<0 zY=ibZ^_BacHi}nKz4G|aEj@Lfl}#Q`c-A8+GL=?JZ`8t5TWQI^Ye2{U+RI}>r4 zrzJ|6r>#dyW$C|rQ|ap&d8Iv{+UT+5(^`)P&-f}eD0`L1pZ4;UqIZu5k8Y*xKg;v< zSH_~W`q%ZAid587X{+r0>%OOlM;oeDUY;6-o1n=0v__GGww`m$)6?_*sZTxq6$zi7 zS)LkCFOQXIT7g8>_LXZMD`k2ldS=HX=hO4WQ->sZ^dcJ+N(OpYC>Tg88fB$MRD&og z3UdOHHk2duj-H6TQTeVAWj*eMEk1(l;c z#7IH4=%vt7P;W#yL7X3DG-#YCZcrP~Do(^uK}Q&rqSdFU4m}b52T4GEP=CZoK`*pb z)^{sjQQD*asHI|KWlv>AFY1SA9z6I45loPu5Yq;cZ4h% z261!{O$V`*5N!wXbPz`el~zDGqAHqUm?#aBql`7LA)nDymTL#H;9^}5TF=n zi&#jATZ8&3TqI>yP$`-{L|#H{EUTD(Qj4;{~iJ^sa@M5$H?)@C*R{k}uGw zyrI?r+W??ZAtF^Eu$fSuKfHTEz3<0%M%Xh#iT}qEJ?Qa*BNNp5KskCL$$F53dV0g2 z26}n}Lkm5_Kw=Q^wjzL$6$aZ-EE4!%(XfZMRe`V-1|>D%*dKZ-3@1dbQbw$XJros) zC4~qLIvB6|qaV;%Oz=efAH=spv?1hM6qAU&gvdvTjzvHX2{j`4+yEL-dmBikVH_OH znd{@Mpc(Q|x&(+AJM=|*(fqlgg>s!nQH*B54$*)}N*vUq$V07ph=&rGU8D=Koj9-= zosVeF3D~nk2@U#bV4QIu?L+4=0cS)pw8mguIPm&%;Rx~2kUR$LlcB8(WFmqRI)l(T z!~o}wgSblrjV?IO2J%@vaMlXoC7-SMv>+VEWj)%MO-y>wjutT8~AUCC-)4j4~1A@fROhLB!&aY z4@DO8p%%PBf{jSG1I|P=0|nr53)Jy24n)AJ_;_Wj1Cd67J}tyu_($Cm=tJif4*l&w zQ0>xy z*Vh=lC4&8>@bto-fW0+=KPP^i2XsA9fyG)lKTY6^eDFFB_M8FEpiLf$eTE&Bdjc(R zB+MxC%AY{9Lj#sONf(8*fiiu4wQd|@0NK$9w8|Xt7%vBdPg}ql`w;kncIZg~jZcl;lP1Z- zfVLO_G|xZ4M`Iz9hXN7tpnMJd5e*E)CfJw2L<|KQWJ{QZaX@u^CSQ>I$`j>>@-iTP zE|U|aKjhIsw&cMhX`pAL+yUm~F+}V}`GMR4c#FZNpfCy;Ut$d?a*%(?(Z zwGcS6?|@h71@y?-*a#STA+!y`fT$?lmv=#~tP**>{8%~xwZ|bIo$_5Ec7|amAR0RW zfsvCpf`(lAh!pkGx7a@L+;t$t9+11j*>V(S0*yNk{51pLi*=O0#)bf= zafRF-FURX+Yar`b11w#R0_rA-?FOGA>gGVuM1rJpxeBlz>p|QU!!rmQi|@trfcE>D zT%Sn9ljJ92ku)58gfGTE2S1_c{S(gJo!Ae+ha4zXlOwRDcy&Bo4#4Ka+^)ug@Gr2g z(k-zq-ioY=&k#3>$FS}AJQ(dlh|v?WD1D8Y@JPG|R#o~SdSP|&FyaC>2lOq*58`_H znz$B-trW2qt0~|RUc2q9LwMSe|pmsW9YqKQ&v znk23ipJR84Cva`x;FIK$v`Km*Y4Gbr0X_*bGp)dO;Cpc3Gs_jg&s+;xFq`4Uz|PzQ z*NGm)W4X0EQr<%)k*Q*lJ5wA^^rVXAUcwFWtz4h#M+m|xp&s6qI3>@79D$RG`!ECk z5+P5+LkW;9P8U1k3?aZ7GDsc*#K{z~6{e+GYNR;ZSzR2-jAnaCEO$p3L&T9ZF+qwH z7h>-)g6fIS5f*@N9%K9Aj9V)OVQ##JTu#8;LEjyQBthDU}JOauR+SyMon~ zEm%0-UR))7g;jxUSfkyYg>6`S!YOaU+RNRrVtk2ICT^B%;_rzXWP7oc^OgpX?Wjz0 zviwTS7siX_avb4BXJICHp*s`%hRBuA$nVIJ#3SK2*IsNwWl}xx5AG}OwfJ28yxa*} zOAL@#3qJ@EL<#kfcXKODPIQmNdNCJBjr0Tmo3I%2mRBb-<#Ez0@s1cl78B*bsLhp<<#hSI`~bg$ zEthLTmbUv`x;ToCqpFHFo|Buast~fX2Vae*RNhiw{!6|sc8FPp50`oZH`gp(11fMV zHdHz+w3Bv1p25Z99d4j}iP}Y;hCjJI${o zj?np>1M5{uxR)?))j4v!?}2}9+j)MA;0Y=tkSCekpx z8kH&a7k`D%rxbjObf5lS#)&a7nrqaLn#06-=M>jxL?`ALrk8T0`NX&41a3T@&c4F) zxEcH+;t2_PTCnNx`Lxsh8b1efUCM9ey@?a#JbV&%N(ywGmD=mnwB)GGKf=DHn-U|% zp5z5=Ex!yKOM5{iJf|4;ylb=FO&!y9pvE|F+kB|cv>a=-{q8zMM!{796V5}f%85h* zMv9EMkL*Vu5W=1D;wzS7mPlH@4c}Gjpsqq)b4BvwshxxxNZ@DOzGMq(ml(+viu37T z)MEs_qqmUGK4mlHqqZgdFosiq1-|>hcO&CecksbnHDQ@Fl?#4`ny_aCo%{4y3!hHYbiorHfoKW;&w>4upnX!{5>=YJ1qSoB{2du(fNd1MgD}flULH;;8tm> zb0)Qj{J>MhdODj_J2BTj`Lu2+lWz@m;`CLWoyxSuxg!ag(h@(*tAquvVboXbXnu&$ zl*}b!MbcrW`e|CoD(5)&2`QEB%@hhvomHeH@_TA7mL#;1#>iu^`9dx70NqVC%9NWI zU2w&IP6@8<&c2vdU0*w#8)I7{#L4MBzV0e`EV-Xw#TwL&9Q*C7g!B4P>WR3QFK{>E7OD2D zJ2NvZF57m(sGq1i?0PSC6EooN0yEi_s*ly=48j`nCi6_rA=k4C5<0jJI*zLeog`=3%~EeH#@&?OrIO^{j-&h92Sa4N>$+p(5Bt9nAd z7j(SMF`2%r>5aE`shwMhKiNz4Z*EQs$7+eJ+?lydrn?YFb|0xHB%9qe#xR)Cy9Zfi z;XXl7t;i_%81907oBkl1>sSp}w)VPSu0!H>EKNGctd?g>3%NY$rFu4fkH2RfE&psd z;{B>@P*Dg~)8`#sArQ9D@LyDo-QVz|=ws|-#}c_4rEv_AcIq!Pot$&we0xGCYZklL z*mPubO>N?~bix)O3^ZKD);afa3G56q%iV}C67P_L1XOA%gjf#qz0+$x^+$op)yPY& z8zWiGA!0AhTXGfmnW_oa+Ofg)kR8d)7KZZs$kB2DUQKwbyN=a&jdcCV-i0ii=ZlY6 zBfZSpgOaHt(X~t0L%+td*j>gwqW7!XNM`PkSc_SV4W#Q4nbyVb6Q)7B4h7>J+o`rb z4X^@xE%^u$Be#-ZxgW>b_gHrlU+MzMneLTk zZRr@ui7?Z?0PiOx<1`uX7)NX{#PO_aJGP&3I-Syb|7_PCXNc~*{&%4Li{9?Z1;Cx!+PqfQ}=~4&PCX->gn=~iY`(+V}fCy?VW9gYl=RJELY9q zpW1t{k622MwyDUay0fmaVmW?`ID*%3-*Pv`ek2pIBZAQ|--)|2sHWsOG7sA*T~*(4 zd|S>_N4)B&b~wK+d_tLhe0?^RcS}D)zK`PAX{Ec%v-rCKT5VJNhw@#{CHh8&K5|M) zj+mz2C~tP>I9$ZnUh}vg1s}-wy@M(e&yoA(Nfk+=O+b*>Ojz z)}k8b-c(*n-Jtv7YGSKc<}hNRLYk(LdL;kau?If^{|7B%L#Tc3LVgkCEv_X^Bj%`& ziRT;-q`B-CU2E^I<)P(I#E#x({W5!GsY_Ug6+^bgE|vQ|E~4S(=C;#OfkHJG~XGCSuRDf+Z4oeSd_)gr?Z zJgAK0eRU71YM4uySFwU#L8nT+-DBZzIUm(H@&~Sk!@;DQqU7%8Mf_FuRBcUuxMh=& zt6IYBbnjHvlJ>jSxO0h}fc(a|2TTY3kivtug9eSyE83bfKKGf=?_oveStY}nLdyt^ zfo7>}Ov|nQ0QjYU0DpNC@yDP*JMbdGy1J=a(65mC&77eZxIZ8W+_{@xIz8dxdn&dDo4u5R`KpUt%!p-a2D# zR@Y&1x*^^7G#6j+(o(E$Ynrd!Bu12X6;`tK@m5j+o=O$DF7s=ITk7>hYjuCQecoC} zu+IYjH{8YSfwm#WJErf&7+0D*jR=$SO3(Vr8{GR{O*9Xw z+CoPML5$F>Ts*)CT8&OM1s#4Z@}ur3uL6)K80eu?!GSGl63 zLHgOu57u1wak4+NLb@bG^DLc2&aq$PC1Y#qv}*vs&b!d3yu#(A@^;R}RBf;I#5dN_ zf*pUW>qtjgd~D0SPpHmv3-~0+)euc=4j=DKBgd&Jx`Q0bx3Q#&rwqs01c|cucFiCI)vMS9d85PN ze8U=4ab!4t#ns5Ri`b<9dXH8k#o6-tKgbc9>y*vC#NJLit8ueOxEapx_>p)S?WFA7Htt)j zCd|)OyuLUKe}mJ)esLjgr&F~G=%@|HfNg66M_sVc(hAZQh znVFfHnVA`{%#@~(v?&J7mSy4U9rySCA3bp$$JMTOSG%kEer85Iz^#>sDjQKD^_TdR zpN#@oV?s}+5v{QV^Z5{ewY_Ie~PQ;f` zHe8XG@dJeyVrPDG@VS2qTMIG`b>mVv)?LwGC-^|<%0~p}!xM9NVu2=B!_ot^cXgX= zJEBKNrdrC`x`vO5SQx(2*50@T@&d0n>5bdzCh7`lyZj*7El>}#Tzg%!o$7Dw=loQv- zb7Ojic)(lH)y7%bc|6bcCG111_nkl7{uJ@i_Rjpu`0CvIBiV)drwVuF?J4ZVpP|ke z2U*OPID1q~Ui^cUth9ep%a%HjRwq?S-kUVDq*5X+E-w6(xu-=ijnnj1n+E^(?ex@g z?=5`#drG$W;abLq&o8sAnNQxPKP~z6Tl%CQNvt&%QC_3d@$TNZYBf5z&`Q zWT%uU^SiXJ)ZvuRNq3VjCdnz*#2K-A+ic@g(=pRtqe33#vpfylEj?A7i}NOb|M&CW z_hUcUKDPaI=lzLS4PKmm+3wTY{5I8@zlcxY z|KlHLqN}~*mgA!TJUP~`jX4|pGS*V;T}h(MtBT20N>%);^!y}C;+~R|lXk}6k8Wv( zy@EDOLsI+DvtYQ-?HpQotH71F{1=rSpLz4s_|Hc_ulaD{UEo#9tE^8~a^qb;U5u-U zhY%NO8ru5BNJY4!j)di@e3=^+)>X_Zcem7+q&kUf5{8sG5%)PvF&1c#(wm5{Xo2vI z%lC)5zZ5RWPs%;wrzIZ}KJ0(*dDs2J#&5Z~109uJ+dQ*56INSy#hMjq zi>+L&LgKiTd!_c2F_hVz_II+BP&Pib__w$Lk%ZN)uc(p9wb)CvRjwet<=y@q=hl2O zuVT)FpUL0Wez9dO{*#U>W7D4ZstgicJ4&KPBamifwO&&+*{O<*e0cA>dn+s zX_rdWd&J4u(4iPWDdR8pT3 zgNmrp6C*h50b?HI0x!i(A%|nx@-DHja3?s5?dm<{`dQE?x6aRw-@?Dn&pMNt@ww#3 zIvK-0C1hhcT;abyt8fwxCmU-I7%SRZM~9ban3SHw_tdE`h|O=3oAEp2m-~DB>~~*Jf2o<7lKJk-sUOGk zN;+rw-t!KnEICV?ZLDjJ2|p2Cx#*n|vl9MFESE?mOevlgS1Kwj;%<1KFs*g6af&X7 zIZA#~JIZAtzgvHy6MsGM)0^$gF4&aUBzIJ9<2*jEc7B80dOxRq|NZ0M?@#&d-Q&4A z%3Gozb5%P~U)DI++{aov>|A7nm^-mqF`c6R4zF&%X}xKwYDqNTF_Gr>CQgrQe^I;f zr)aJGxAa~dE2ao>+z;P4&lD$9xVQjw7~IpmZ@s;|bv-5Bo!sXtM!$6@-Jzllr0_?a`-*`CjKPeLX^y;4i9zepE%XU(P`6`W@k-<)@)ns(ddR=1CX_^0q!v+^sq>UTt*1r$8hx0) zNOz>0(7&k!>LBE!2OAc#40h`8Aaj-ja)dRLPe>cZMS{fd=g0En`DOeL{uf_d7%f=D zQQ|9cs8m|!?)eO%4!;$DET2I#k%i3n59v%%j>cUVzrG%0tkC4|&55?oKpJ*#aigvM@I8t08o)%5gFzJd! z%0=Y%@)NnUlCHEtnP?2C8U4UUosKsne8eWwPu8O5Q6xQuK1DyGAJhNPS=1WHZeNUg zLG~kMXxkWK4P@oo1q#f4u(EOp_GKtjX`(!ZtdVwwP;%r9`Kf$IJ|*9i1M&y?uKWpd zrL|U8DG!w}G!X4V-%vLwbpqysY+je}c<8(RVACeZWU?vQiTnr3X-$?SpTOr035vLc zkHyR3xuB5L0;T00C`3y@Jt?mGp^rA8S!f#MT$_l-qe*Z~K(oGF1n3A`@sP`JhLvRi~@NAbVF6wU$~LGPWhDsgTXB1*kD2ArshADD41b*18L7 zMM!lEfU;o+C8sRZy(w6{`#`R#32@dWke6)<6?E~%xTK`xv<8;-9~(g!#`{`vU=*PRUt+;{j1 zF%BUj;(xh;B`{PpZ_CE{zvA7G6{v)j8Kh2 zbqZzK3hCnps7Z)#2+;_k(Wru69a7Chbj<(Qn~>@sVqQW7Pl)^ZpXMIwrx5w^znJ#_ zBWOry`_Q$7n2Zoh5n?Ao??NO-=(qn-93d^*4CRH!Tw45QP=X^N!yXOV^FJ`l?T7NuLx#RL zFp4}-r=L)=L+t`rod#zjpf^Hul?O&O38O`U`naIgLcE9-=H^ffqdzT-pk+fj`aVPM z!v8>3>i~217ML~T)ZywJ$U?amat&@!x5Df_Ozi

m+q9%(8o7c0CLk@GgQ%e-~!n zlaQtFxcU+5e;;OvrubgazHxjm+_S}CPOX7m23EkSUV;pZhauBqS8VW~xh=#b^u{KF zdc7M~kH#?LCBj^0hq@Pm5po)4@41kPnH;KpYBn189d6g-m{2a8BSb zo9CP2tL;1LTjzV?Gy2Q>tFuSgd7LFUk{5+svAuE;#o{`$7M;Vi(Jq6Wy-f@?jUP=d zEyu0#_SW`SFpbT!O|>pJA2bPuh5Gl}wwgXn1zLgqN+#%O2>q+|5q>} z*o)f~@UR`(lm0&bEN_O#=6>bs>bAMxxz~8S-ZX!Ac0Koue=6>hvy^o77#m151Dj86 zZ3V+M<1cdy>rmS_`-ZU6;S0m6+8aZj>jma2rU+vtgGRqZJ6`jEK1j_c_u{|R2sB-J zEhEUTyGN=l?HAL;jzUAeLNJ~yWKa8b{$JkKo)p(N#}da-N1XGA^RcVCXTSH6e;wo! zt|nBGXeAlJI*SjW5;O;O?Tphc-)$?y>V$U+_lL)ad&79U*Vf$j*7C&M!}vzGSldeT zl8&TC6Ps}c^ow{jL7Am|kpGg`OP~k|c3~6WJ-C>w#oY*0Wea?S5A&3C?QjfsSPE+w z-g6WY|^J6}i1;iQ$TQfW1b%&*r6>A$yXX7nhYt2b&1rd%7M+wRyiISH~QPNB?R~QTJ-jSaWT*IM&kKM*rW?TCu zUoGDg*91qo!n}em15f1Vf`ES9#+&;Fp z|EljFxVO9cu6rB19~3Ogo$>qOFFL1TUgLtTj(x5;&r-POjJ~dc4?<_8Cs$~U2HsTG z+Rx5JT#UXLcfRPo;=M|oERjPS^G4(P$VPVoK z;iAwEp8xj*iJ+E?;YtPSupihFY&e_fo8ZC=_vTg0ss1}Mr$_F%yu0~T3yv0yDr6m- z+^jDxxK_HWZYJH#Wo;Y7bo0OV9}y|B-HLuIHoN%QVm*q^jyn~zINB81Bz(Pnl{Lnk ztxwRlp?4D}@RzWLtWds7E@_TfMwk=SaU@qFaE%?yPVx_6>-%eY-@2+f_U5~C>io9l ze94vb%M`pXI8;!lu&E34&SD$#d*mYc6Jj+bGBwM&6t}rA;t>v`0pK ziue(^E-E*=cWip>nIf&?hR5!Wnh`d{cGmjc+R)m^)KveN8Aaa3C&3!-MMIU-QU&22 z*DY{>jSG|tjOQK&DzkTdd%T;x|9UC+V`t9-qF{5uD91!+V`m9xao2k1VRr@Ji$LAr z03kwLAs<8aiM#Y@17}LMKD8bUD;iNOazS*D*tW69Vh%-(ijIt$7*Q|0U07LbHsq&o zr{^`%bTM);-ds&p3dHKtHsN~kVPF#5nLQN93``E}_s{fW?D7E4R(DTzmGS%w81rSX z>Z|Yl>Z$;ijphD`z}k_J&YbT*>o+ z7k-!TDvPtz{I)=|z-rF#$Nk;dCfpf5C#d7!@q>ib(q_K9FjM}Bs>8mb494JlsRO!} zx+nS{x|zDWI=@~vjx;^9`YaEOdv!028%%>ts(ys-k>)m&LFZEgm5ST5`B&J$@xG3}ZQKxXpR!1h<_`2ay98c{!t(` zXcylq33v_CL2Lo;<0;xmyk=fAFX&awXl9qby}7K0BZ_NFnC=;S>o@2&X_hlHG-rt` z%t%9qW*HTLJl>;;+GJH?1Ku9Hi25p%x$|N-Y(2F}84w)kZOTY}!-cCdOt z*+EX#WNN=?ijyb7PM{@yR3GLhLs11fib32_-~f*5FRN#PUmGszMKjqFyCl|AD78D; z3mt}QUM!zPLy1(pyYg6y1tzTrS_w}USNL$Q0@jc&1D}h6OFS8LC`%3Jsh7fZ|7t!9 zvl`239BkXb0KBRGfq_?d3b&LB)NRcG#s{{Er;@CmMWu*J+MV=4R0J#@1F!_bpgR}5 ztW+jvV|HZ+Q=P0Y-$(PrUdkbjU}&yty;qf!H}|$l26jc5R?Bc8@+m6^9(`+k^9^)udDRNKFbZRHuuqvR?BQ zDAlV;bptkl)oiQb)PJv=mhL0Xi{2NM1RWT)@nS z9penCD}O-juiZ%arIBP7Xk=fcT&gjP}fGx0*sq>t$4Nw?U4)U~+Z z(3WbTcH-?|mpP7YV^YZ&VT0O(aB?lAnx>t)abhoT7gUToi;ZWds0|Af)qA$r`AI5dl@J$wWK3i0L9vHC8(Y7pZrtF=`1_ z1YNPB;j2DFyvoWbLti{tOG?ogf!0{;tIqy3eziSC+nnX43|k#)0y~x1p*8>uVPhO> zAiXBF@XXN;D(6*mzPwwE$M#t!XmD?)haq#! zsZ3MENtJ@fiGbG5f0eK3X=;~nGjI+!QFR!JTENZ;wA1C#qlmR?f*gYa{8eHBHCZzY z=h^d=otY;rA(QlFq!GbZU`c-@R0Vr!eJmYzzS-Pxs0YpX;Oo8&3-BC>;egt(}^ z#~&`}OAXU9nw$7yNe$e=+vw*YUMfR1BD?bU@Ylg#xL*4bHv|(^D;2A~Zb;-luvLiZ z%u)ISJV_^V3o+gh3^WiEnfsWA|1I4PlE!i7p~CN9AJIiidFwItlyi-56w|}nQnQ=; zHO}<1uAiGKRh?evT z`Hb2^nJxzLecI_fl6JCF^jnP^rCLIYFiV}q#6(?XCj{G*_lcRvqMZOa*IDs{FWN9c zw;VN*E(x!sF7y#YH*UTx)0gFk>Q;O`HjF$*D&E6lp5Yq3A4@=coXKFL_0l4~N4lbn z;_fk@w7bYNJVEWlSE4&&TV@%MXJxPpLVYxtDoZ{SQ+c0S%-|+tf;+r#@HZBfT;lH{ zyU-A=UqHTSDiA(W{O-NO|HLjBJ6Yh_`1@AMts~}$ zk30j2$f&Et7&qjOCvKAab<2e6LifNV(-&P({?04hNOG8w3mz83$%;U%9*`e^RyjOybr13TN6s;2gk7P*^I?QKtmN&I>BwEjJIPChHN(&sW! z@=5N#R7P`_9*tF$D)U?Q9?&2-VOmg8bD5uXKU!i3aPR3Y*5araTaluG4_}FOmSzz9 zHLbJ@*t&sT^n7hNT@_`sh0{`hF;|BXgPO#5;LjP*NVDt@@EAsO+u$`bSv zIP(+oE-b**M}A?HGQiM+*yx#ujbh%>%dl~Q(Q>?I9_>XHvVF3WdQYckd8T4%vX z_^!+lKH{sWi%L8(o7j(K1bavU=CJl3G@EZG&sM{A->8@Tc12HgB#IGh<*{UaeLv-r z|D^bg$=k&a#9Cm7B(9a`DwFA_ zL@o4KEJEJIeqkkn+t{s|aU)$z&EPXYAKVYj&_U_BFdU23>8ZASd!>Qe1V0RJP`s28 zydjHp9<`iEmY)LIUPP_MR3vL)1@O)(pT=;yE9h61u(e7eeu2oA)+i4MCm||1uzPBu z7Ra;kN9uJ{o#=vk$y4xS*dEl9X@q~^W3k2fe^>%(LRwI11l&kWR2pCxi443bIF~g; zJISJWZ_GrKWDOPg6fBb{q$aCoePd2zOVuHG5wZn&L-`)8D-FO$XsiEbu^>|qI$QDr@qOu|fMXrh5 z5cx2Cw#{R{WwIHM=&mpZ%1KqkjnV~f9)DQg%RlqHF0A4b{q5OSt|R$#3fnlY6`mwji3Uqb+MZY0k8?F-lS_oDgOzFG!C;bjSFFtS^nG)!^89Aafpx9}g^t3#t}m`M$A;XK zzZq8okY^XXE1eU)ZKW&31R@%g<%*Xe1k7{8@lPIQGL)ODM4B)rz(s1*ZyMOr=I_O39QjK(fO>POJPSR>FQXxA&1FH zb7})Mxd~p@sq@W~{vvwd9h7pg`ZhGowK&bP`LTJYbwju&Iysh(*%PrXY(secsHahN z!nCG0x-U#|?E_6$vIM?H@^h_#+CDAJVQT=x>F|9H_GPsmqw~Atp1X{9nIk8sNB#oO zC9WHHz_Zzv%-)tCV!hNN$^!I(yr`pq$Z2bG8$X&^`-aH#QSOL91Rb#|Tn&E{zCEm# zwUc3;rnhF3W+uH8e=BRjy0%W5C43A_@vVb#(L4~x>U}Oxbx&=N!E?ftnt240uZpqC88kua7pU$F`VVmvxFKvcUoWrQGsN@S6YF_Z_&%po&K-A7 zuq;=^k)D_9ouve^gVIR;l$=aG*Iw1PH+D1CG&L~yx1WpJ9}|eEZhsm!EviwpCSry) z)6~V-Sl?3@&^qYZ*ez+0bWfR~tm3Bxy#C|t!oYL3C%e#p&il#jbwBomxknWI%9&lz z%3m?~+}ppfa$$$S78FFU#oj_OBvBJ|pY(c1Wf-B2A|9dYpbuzJK>ERV2u|Y1@xOx2xiGeb|Gs}ExLm(?zjqYL zYm`^YwS{%Fs2af_}!JrBHc&SABXS7xMN{9iyWWC@A9iEH978!&M?|9_t4uG~Tq zY)akTLj%`+>k178D?Bgx`r?P+6mEo25go;zz;k9r4XsJk{x&9rmx^2$cGc{%?}**Zc7hKyy>fcp=J-+hcu95!3Rz!XNVKU?ZPJEKEEgUAkZSvi+jxu^BwcF zb+jogl0U8>!JFV)y~O4YnIU!@t)|bG6*$Nc8ME>p#mix z5S9h6a!olc7xW{~M{vXc>M%In-eli1u#}!IsOH7FjKD`9<8ub)i(c@OE02G|TTt(r z<;D+|iPj$0lh*4I>!LnHwvPBKe09W?up{=>wo8^DMx(xt_KwC$&n8BwtC0t7QIe#G z!UEy6xLP-# zqMvve&?aM<6Z%coI<_a4b=H>QSE7PZZ6gOoOpG`S`o}Z-21}wTOjlhC$_%p^Jo?Vc zZg~+(m9GdEzPPwq)QDNUBRHJ9&As-2W)FBzxa&A7xH7#au)_Cpymw@JhWLN_fBClf z_XWNN=ZNE!vuYSIkJzp0q(5zHZ_YApwGI!9k4la_8eThG2|FF$%I>ueHTN-^^qsW* zHRb90L_hG*9SDA>L*yuNzDUcFC{E55dk8Q1g~D0B6L*%a;Th_gre33a$j&9 zcc1rc_htLOdjmcVyMp^m@W?o-sYc^k25B$q-NvG(6f0qG9ab#-ZrIzfB4P9FuWZ$= z|Cx6gjkO?-3WX~va$UtD@06YgUvo=1&cEBY%hSLY z<8R>|={CD&x_M7EZwB;pfBy_N7j!ZsNcrD?EwRx{5t!tA9X1HxAYT9T@GSxQKF{#EFqsuTr|4NsnZKOHCyrEO+9b^@XB@)RB z#1}jptS=I1x}UL)s!=_m{*kbGy_6)Zn z&?$H-*oOa0>LhgoBJC9T{VoN*>@Zza%V@i3l6Cu;s=BhepUh5eg61sql1XFc(8HMy zbQ-;fDnmt6^{F;w0v%7jCM%F5iJDXt*@1jUq!IhUCcF%*fQ+DQuLIKRsa%U6EyW5` zg>Axr{IS5^;QGL0?qqOb@MBQJ)BJlrM_4OG%8^QU&?p;VX=)mthA+gs;f44Hd^d4{ z@DLryG;$sZRyS%ny^pR!-KN^oqnU^FEqL-Km_#OlslvoE+vrScGuexrNXC&h$zymK zY`Xdi9Yfb(T>g{^xr&quS35%T3BSY>z`P9=Q$blY2vOoCAx9_ys_1<&M@*3GN@!_rywTe>S< zkKRS~B@0Lk`Iu-zY{V0BJ>D65Xq`G3eSsP9BG7-}pC=ENZ%PlP`(m=R9sD(y!kDQc zRse6vIBAPmR;n-kD}9uj$irZCHd0Q3eY!9DjOt=OH5ViC9=H!*MARZXlQ)5rD^4-A zOtk_=j-lJq<>|&yi<49~wHP?TRO%_Yj_gK?#5$rWQGj2>zhEDLG06qhKVChK3gLd~ zp_~Tt?;kl?*2+hv@zM%tr@m5asiD*v_AyhXx6)@RN^T_oD<><7$|dEuQX0ra9{gJm ztD}KJc#W~xW&8%-hiLVOptF!=$l>HLazD9@JWcKfV)8t>lUxXWIDl+SCXoSR0dbxP z;$?_m;L{$3{{}X)62x8j4zUfEsUrG{W}wmNfs%%HDYul_%1oszaC@bdR!ViHEwo-o zrJwQ)JiLD>SxO635-o!N@dPEq9_SnpX(pfxszXGB74U5K3M&9MAs$b_>*3w;4&c`@ z3%`z^!?)pg@hA9sU=%I^m9QOOj8DTy;2m*<6~(i$EbKp^{H6jeQ3>KtgvhU+!1@`~ z`tV~@zauBCo7d0JUKYF%I^`a`6HX+U(W2aEG%;6y^vZNh=~ zDg&_!S^>8(2D^x@2A<;#(0#9fd3=j$aURRXbhsZv3?0QN{4SP`(73Y$1?Nr_~)0;b1M0By-il>P+BA761de2;L6`FZSucmuv#I;~>1Qg}vG- zh%1n;KK?`8G=|6!OMrm+3$E`Uh>WlWI|xL{d3XX{46KL)h{Tt`AvOXUu^jjv$Aj1E zdx%M3w9?|{MS4%EwT;192=gMeV!s`iCa2Sbg71 zTwq>y!R%B8c$~Xx!#{-5RG{73Lwtzq>H_Sb+5`H38`ck-fh|$T15;HV=)@|(F}3-_ z+(ZI(Qxe#lWcVZwxT7N2T6pq){)cEv2gc^5dKq4i04>xJ2$gf{D4>8&gD!RjxUdYM ziB#ZoSaqyg65?2J5Ea1#6p;p6p%~2HQBYbETvZW>je($+KOjDZA84Smz<9*~H)V&? zO2IwX1?ZsV5J^D~te+Ww8mztwV{;XP);bGM-hk+<5esEA%;@%1*#vo;QBDyTEJEJ#oSP%cPB=I(*lFj6Kbc0a_>PaBjBHE!q0rDvjGEI z5K+K<&WCz1m=Rj8IJ9_0>=yL&c5D}rVMcW_c1LXj5j~8+wYCIR^)pabPKY=%2V0>Y z#m*>Az=om2n?WtXVu77P+p!yH0}u`&o{@*>74spcMR{xiT;&1ipPevXc7%94yry~` zuc7V&UU32*2X^GAXr0;$D684nC3PA;QO$t6v?3@XQECOW58Cf4jPqn{6^)%L6 zsfCxqUcjB2jF(b}Vi)8jJX&6hcfeY!Lx?>Pt)e!T2Yyzfx*o2x8Wt%JCAvsH+=g93 z6Y%b+r?L^OHU?r81`$fI?!bqA!OF|?@v_QAY&hOWJ+HjN+;R-QhpLTcQWMc^*n_7L zyD`|Ik~L9XVk_8SpCb|ZiE3yG(;lo;UFE98D>VrX#wVlF(2DofHPTbOn!FTiti33P zYZK*dY9ZDKjUg08Qd^^Fya3ulgSS$P!ks=<9E^{WIw)I+dg!@Sk(e92gfCa(@LLeI z;5arEsNpVHc~lGR=lP7;w}83iKZB)0-+ou8;EC8;{3hJT?a0F@hN+2OQ3px+3l) zZo`=GNo__B7-Ln@TJ-@cr7lBL@HCjM8mZ5)E$U)5pV%o6BJ)HFB5oW(7t{jfFuqPv zhzz%#t|86Z6cG`lTGBy@+~q^Ed#MInkd5wGelaM ziDI#3xJ^kyIV!DIz;8+{5rwZ)F|wa}5B3Xwr3%>tqBV@eZ%JE;n(7oZid=zeV+8RC z_BJE1kHlgGWHOoot=UkmMPS%K^>0m2r5RNMABye3CgY#cb*icQ8M~sc#tE3c3)PyW zPD#hBAO}jtz9^sYcG6~gzBC#lY{aAEL?4(nn<;aNo$5numiir{L~K!q6FYGoKA4z; z4T1aWAsPfRMh4?wl?6l_ERO7pG~{$C9Dj{PD5J?dIUa17Mx@8ml@978c>tWDD%Ke{ zpf&18bPoHftWihEJb6$$MbAbSusX(}2DqI_hgN@w2B@|0>B@1?+(>LZtW3)xf=M3S z>lVZiPn1NkicWx4#)t;u^{{lLBjzgw)IQ|~aSz23E}VwwAcJ9!@uKzk73nBuRVI-g zapo#Gb@`Gzqh#zwvj}A*C0rxU`y0wi3SLT39*9;tZ(mO;Ia|j6vca zln(ri7rlqrJt}Ie!kmV_E6I42%HWlNjvl2hB&%a>;ZEPII`K?Yju?%t#J8)balJa1 zsE$ey?NAPxfZ7o^&~4a3Y(^3IbC|VuDUH;AL>IL$egxZvo?=_TpEC-43wL1Kwc&Er=;&!86rI#5b%f zIS<8C9f-PAJJ{3Cpx#lBC?DmZVl-XoHGm;6q#A1Ci5m1W@I$ys-Gsf!L3OdLQ+ms% z<=XOeutwO0GJ-AmUyx&K1^;1t1YCjH>@OV7EK>|KEH}*O%%jbptfMRkE%QtlOlys#X|#5bz6^Prnoq>T409Z8NE+dOFgdu; zKh3`s2$~Il^u$MM3o}l4PT#|n zY0fuqwiU5=xA(R84maA@hOM$uwhNZg=K7`#eKj4U@sMR{JsyWmkef<31slJX+sJP4 zPxeo6SN6m>db`RMrn-hYX;*t!F0gQu{cBje|7u_iZxzPL_2ucne2%4(7)A3*>on9h zIV}tAZ>0o6kS)Q%TM@#VrbUn>+{WIMm zW3pwRJwMD~pJTghN8w!}8rjEKUYS;#ADV9&*K51evqOF|R4S}HH^hIXnQ~|GG3WMI zWo7o0f3T;YlXB`@YkfxV8s{nJ5_fxcE?e0@n?1uuh*hNaVv@2^=}n|k8BDG=OKUJn z<}J1gcAfpcb$-}?;Y-3#S?ZgwnpT=CSc@1AX#CUyx`URWrsDnNPbdMRD!qgV6qmR< z!EElmKi56VwZ`Rief56z_I5UKg?mo0m)HdVD{q6q0bwb4;OvpwsdcekbS2GB?MU4_ z{ch8E+g@9`^^>`sZC}`N`&sij!)`-@xv_b-z8!szjG$9B1E^c-Sor}k$!pXt;^W{s z;W)pOPhpRHb08Sv9N#8?w6C+fjhpu0473hp`M&rbb0@@fX$Zt6x`;@u9zB+Ms>#%J z*6lF0wl1~`29v3hwU_;l<+Xl?F5KWV{cUWgc}AY4uQTm6t;j)O(7r5i5Xkfeyn}q#*ad7q?cL3^(XMLVl;mj^#5t-9(J7YQW|PD^G)~C zc-b_{kgjtYx>=iB2N||#R%(&)jcJTNgBn6F(4=dJG9AgrL^-lIRTS&M)ROg8hUhawd9%`G_y%Ug|Kd*IzVl zF-GW{>aH1&Smv5DbtQF;^c79xjks z1ZDGvzJ;lqG0JdR`&(-@PJwnjty!Vjri;hNU)50z3Th%0!4v$8ao z&n@tO0d_mfTg&hB{q`hyB7FD2s%8!51`qLyBu5=<@Xi z2GaP`s5535N*fm#+8ag~rvQu5K-)`~rhBU?!qlTDk?mmZ*n|@0dqNghovjF#=zrM9 z9>rD4gZuh;*L&8vD9;FA5w>O^iW?Ao0`|1#XfAdP-$)E1)9FQ;FIvC0hpwZ3xFO8= z(3oc0VhS^DhpW72tY{*Q9rRap4&7*-PJ4#wMo%R_;;*n~XfF_YySTK#+rUS5sgLr^ zazAkQ^`v-A?vpO3+w4v96=!D#_HZ@$x8hV~gR0qv3|(psB9; zkolFlk$IiDhWVO#oVl1uHjFbw7)t5CYKMXfRGJz}w8YjZ3#1i%BW`Y>Lg2DL-Aj3J z_a;{vcMI1H=Xz%!@KK)V>Fzu2*8>qgK^QHyfxXQUyd=y?-=yIM&L3@!7mB-Qljy z&Vf$WG1f8Ck>j}Tw7TzjCi@cEWDcz2;sPa0y@-z?ThJvng7&07V2CmeH(v%ta+2+n z&11{6^|bY~O}C!5JT#v%Ei{%f9Mr{Y=QFufLt?U;E|(S~`9a)XHr~I}%el9>?l@aH zZ-77fo5B4bff3&K+t;Gdx1o@$opWPL~DT~nH604OrwU|p?a zx7okhCfL?nPgt&)OPa!scl0LR0L@i;1^F4Pj^;=N><{V%R{4EiyXT~9tFzEi-m#&u zL1CxDct;WE9akk!mN(#k5$MU&(mG|6dJw-$UZ(eHMnMSL4#xjX;T9m9ZU5PZ+lSdJ z+TFJDw)xg&md@t3rYDBudYf)B*ww32W}>H>D>oKX_<`IB_MI=%+tzJ^b^E-dq2p=c zvBKwt#~ksnq7L&G^LyEH!5e~29)#9lvxq~~EM~j5i~g~pi)pm^tmTSzx-H)RH(Xh! z?U2o5#jT2YBi!$te!h;^G-P&CtBHmXZLyi85j4T(fd&3|-pZa&E|0T;^RZ)uqp4$n zgLg!`R=DeXPx%(ITevK+IG$17tKaY+bG&brNN zwzjtPHpiPBhDG`^x^tR;ncLKJ;sbUFwUkGT6Zut~DKOZtfWG_KUEICd)zDSU74GWh zig$DF{$QaW&bH%RK~jv9i=dWR5yC_6r8{XP?GXKO!%3sVbkDrXl4dP$tz!LZ8EW}r z?rNTF>R_y3umeP}WM5g&9GA;32!(KgO5jt?9Yvp5tEWuI}#a ze&YTYXl0v!3tNLr4{j62N;8$|zzGy1&r&3_SYy%E)5E^NIK(8HhMBX>DVA8vZnMtZ z-E_w2Fy!gW>7Cm9nr%#NIzV10s^k6D<4RC!A%;9`QaK7jCENV%fWq(NjrNxFHuBE* z2EEz7mwtO-C-);bOVCS;KQ_C+pEkOmR(`RC)!F?A1X4ZaMf@K^Yo!Vz()v`6l$R6tcArs-a6240=8k*~;qsVMp!U70z=a7-o5 zaLpHusv)(jG+&u{OlzhnlSL=e-Kb+^Ir0H`m1pB-yeU|f*CVgeQ@J8nmCs8Rq-qi& zeG>PHt;J-pB{u_~>sjIk@r&pe+er7N67n9|rW{tLAsu+~QeZdwg}uRh5HE;W(nmA{ z3+-=+4!Ig)rc9}J16zQbh~Z$} zi3ba8YhpW5Ks+Tb6IL>UtVPx!gG46rh`3IiBXaP4crV4~}3cp2cB~24I@Yy^jR#t#0msd-n zSfU)*EtjI+aukq1He!aF260doxSOZr>p)|ejEdv$u!?wF@ZRizv1kRvX6&QPL*pUx za|?)H+Zhc9tAq~wjuIh8<{t3Eo1?x#Bfwj5h57_s)AK){IEyQTB z&zHv+;B$cxBOomHIf!ig72B=+!Wt;;FgLmn#78-m1pmET5Z8EuS^|HC60tRS5UiWA zL^ZVvSXz$b(?F>@gT=xb*Ai=?t*cNe_&97K(MioD8ewy=ci0$w8u=EzAO@-j$#GzH z{-(~s+v8^-GG%-4XKaLh#LVh*Y`OduGpWxA53oU2h(tRE?zvK61z7}1*f3ZH-e2~8@>o^dj#G9+D!*mKo@!geaS#1UK+1}JZc@R zF>ZiJM`?r~wFJxZ9_28q1fSmoYVC|#6^$oM5W~2N+648*kE(}Z#;6Z*Td#u6Z4+7! zKAu6~Fd9N^*v+yZ>M=oGh)q)ZfrWmT@0e)`R!x3iYv!)H$fQS^$=}GvG1W z5%!7qL5=tZ{+GM3uIf@y)tUem-wmwkz41A4SMR}l!JL)^ktZIh6Cpms2YeOScB7Qi zFaxZ|o`fv%H~~*F3)N;|MJonM!QaYvd^`3Cc7&ncyNtDfyXS;D2z>;*+dcI$C_@?0 zubrTMzF<$WK6nk(3*STjM0eos&x0CIS8rhB@%|W+kEr{hWmco_C>HI+W5LH*2ik%L z1KTxt5nI(nh&5P(Xomeij?OwdZe#7knh|T!jIkZ2hMAd}nHifj%+xS5yJ2R=hMBR6 z9mk28mv_bb-us>VPwzSHdO_06{NQ>2)ARM7I1}88RQ&6 z3@F$m3^P8i&qeJ(+dG3o;HsSh-`IXo(}%-*C}tFaZU?<|JqP;1{h^z@m^i8b4gP@z zeI`PUwctj42EQf&u9m^zD~%@(0->TQ{F|$QB$7e|^h(4_xVtA4)xoFON;U#_WfU}k z=Min-F8%;dm|=Q%AW+co_Xg^caRK>=94!WXpPTR+S=u1DX7(BHz&C;xk}*c#0?vU0 zsF9`Nuhukz^}%pmgu}0W6TXK`BZ#Vp4go*A8fr*AgnustoVOy>7rHO~aciOhJbfm^ zb7%rkFM8wQL^s{0HwIqoBBO?WN$&*;@MNR3whbs6n~j>r6L5)dfxE)MdqG4CBM4yh z55U>lrq|bh6L$$0q${6+$1~q}q*np1g&lh5MQ9BCn-z#6l!(K@dz*{j!&(!IFQ6!B z(CkGT8VcOzWb~alM4Z%?7$o@VPxb9uyxs=59rsm2eXcBo^T~fo4!Fl{wNmOxaAvc# z+u#mcv_|?7$ThYmo)Z$$jvNaf_)+QoPo;dg4M&~h}Sm50hfkUksdf9szOs@NrOk#DtsG&F^U!05ng2{?yZ zG4%jig>rBL)s?EkZR17Q+h&+rTCSNzi_^N$($G4`(i6HcFNE2`2Vk^V*c;HTs)0_y zH@OZpl-FWSv5GW6TGPA&{~WxAR;A2~q-^DP$L{@Y@I^_HHG z2a|iK1zb(xsAY$>vn9lyZ~rr7VCWcoqu>p}&4RuL*SBFyQ*(KtjHxp>of;48_Ava^ z*eid5^Zab7bYO-%(>>5#=&IssTgc}(%}dN_RAh6A`PB-`7G3kK_iqn$6&FdTbqR;j z#h8X{D}J11ThK3ihJA&7e(>&4P`-lBz)t#}J=^9o&lIL}v0MRrhOR?Kqou?Jqpp5L zSuUlC#lm_CD30yoZ{jMyL{aVWX6F z@(VQ&ILh_>OMR6CNq)0upyN(K zt1DEt)wN~V)2)N7qOD6%lHG6FC^&`1mT@MXJ3?2d)9F2ILnaIS;CaZ4SAt(ADL2)- z`YUxNoas{hg}$oZ4X&VqML97!*1|+k+ScV=$!qAjpo~!;N)@DgI?dkZ6S;V=iSVGQ83f_v8Z_7u>3@CMdh^|kU6QCag%+0rE5cNi2-AvEo^{7Skj z7Wsd;O}k{kwgddAx9+ zPc(0}4>zlnnViD@;GeSx$-4RtV=mp48%S-}ml53|`8=Bp0#7&u8be8hSt~0akjg{r z{fCpt-IQ}R_k`=IGa;vDc5cyTsk+)-Nt8w_J*Z_dasSepW`wYoXQ zzRIGLnZ$bfDPNTI={8TuQxf@YCdB}gj?yVl#mRUy*0sdu{^iHfBh;le{ z=VZs^jCW@_M&vZlj4!MQ*L#BUBJf!nLssXbg^S#5rW(JJvR9siRX%S@v}nQzefZH;Rh{j{m_mOwrC$AVh9AF>Jx zE*JGF9G`t5XN2=$;I$Yo3ZhqANJnwqxT(x#_9crgm8_!hnr+2S6)u<`@Ri7?s6E|_ z8^O#)R)fV8+4)=rCX5%^aZ9CEzLG_M=C#T@UD(pmqu|f1 zL%DTaPXo2ZkG^oCTk-!p(d zF0|l-xMS=xu7|KvxXB;pW^xla%q1~Xs3nkgh|{KsXWZbf6f7;Q;i^wSnzo&g^2=!^+ftQn*(Ry((GP3h|T10@C}$BR5AJ~$C?`R z;CXNh_*z01|BEYP=dxwl;mmVt7wQdpyf1Q?-|D(j__m;~<4>ofpnXngUV-BZr0>qS zx4QleyoT1&c%vGyJi6f!rY)OI2hnAjrMzsqDePd+LLz;tkRbS&)^sJhKd6aGd!pAxnz_ru<@0E$3HI3fDT@IJV^ek^ix%vpdQ2#og5X z($`JCpqA2aX;t9FwUMj_sV#z;NSnFILV43qt{1el0{md0W@Iqsxkf^a;Nm-Rf3s^@ zif_Ty0cP-f#=<26{mhE1K-#^A_DxLklAgaD16{GtO3D~EWBifU7sCHyIvMT*ZI40a-yE7fxu7W0XB72gl4lqT-S8@WCS*oV%R2oG+ZBW0v{#a|Mgpvedi~w67heXF^qB06&-8&)$cf zWiVM8I1qp6o0V@;JHO9+!`BL^7rk7qT&kT;I^?DZe<^$+;`d*%9C zLpb9^6UA``MY9i>_Ut5fAwNmz3VLW4lVO@`K5r=py54hB68yhDrV>I|xZ_jU&&(+1 z800-SLkdCAvXoZRPhTzndEY|t#fG`cxHhr{4XE6N53UqyT6`jFe;!_2~)Xv<)6l)o8&a`Z^elU+WPc}8PR5zb7?d8{UE#OWp z&r~3HpjxOia8(Y-q+B2_4J`Bj_CEK#b*H!%xkP6J_ibmS>x|QIY3>!CJ)U2_nE{(T zUumutX`wKSWCbaKJ9nL4%asLHeILKp)Yud-uQRtYYoYMQ>&mx!PR<}s!!VK?|2&9gsZ@%ar3w!p|$xlcf$0T-(pH)Pn#NY4}{WO3h!px zaVgAHCL6}Dy(Y}K7qs2hs;$%}Qh(SlY!w&#zxjCIY+of<^)7hYc&B^!`j+|L`a1aM z%Tdw*=p<#U7vO|ellVpo!C% zT?qSA? zw10-QOBw*$Vg|Iy8v)PiU*aAbhach2WM%3iJ&~!zI+&4c3;0xSHi22rU!sR`2k9g9 z6?!QBl59?$fVo18VUEo_NYzC`wrvaONb}@o`aOk#Sx$?+Ioi0uQ+2brTX`D@h6dSW zB}8tcSj2FRmLBWvB^FLKZH+3RT#dlcwliwu61X=`WF}z`ByA05BK0@hpS)sp z(UJIX&~i1HQZx@AMf31|d<&jk?IEF4hu9{z0H;z@WI zR}GJ1({N3)49raMQ~#l8ax_{*B|_roI(yLQ&ugHit;MfAv3R=d(y!w`wLMuyF&H%l*9Gvo>QzeKRWDLGTyux1to$yep8YFbD zLdyCiPF737N}|epQ90wgcEh+NJtqc|JB`KUDcIMx!@I!oPQ-nc7DgpdY!>6%NY%6S z|Is&xLuTr-ew*m4p8!q44*teLpo%pnI-G6q|G-}`ikBQP1DyfRrR6V0j-8&!k^^+ zkZP-Cv{f_77utXL8n92Az$Bh2(nZunT4rq4^YnEv6=)8gPB=6I)q%eouKvVFB^~WH zcEc-d0!8LuY(fu>hr|a+a#qrvV0hIfw&|6if11aaHd>KCh})3Ot%;`r7yS?;d04uyAHh>K7x5QX zyw4%a>d`mj1LPyUBJP8u)C|&Ptb-AXjg$&Zj^^ESb244dBQ(rEY15$d~vA z+$Al^Rp>LMb}#77K(DI`r-RWX0_w{+;Oa8QVC^MLf7yaTT|_MaI}eMxO!iAPADYF73U37VTDf{1Oe+2BJ&GHEjaA zN|FF(ET!Jp$7`z~gZWy&hgiKKG>3w;&XAmMqCM3gz^e5UQkruhtGGm~pg&P{sun1= zxp;&ksV%uGYFAW^Jm8m&TiPeNEV92R!0MO zCLGlKA!s$sAd4bx#yY%KTc$PEw&-a<;T=q|S}0sA&(IlW6S-I&Z>&U})UvpRe%P>) z>qr+NDd7|??WF1(;p9RhK$h3bP_wl))Ghfbbw@2t-c{D&a)u3WHEPna+E%=Wn5#7; zm*O?*UpP}8g!dD-Nw3=fE6rfK*eZCRU$jebu6zs=H7>}L@IB{h zW|(|{Y=$pMkI4nnJ8p`4ga(iH+(jxN4Kh zYs6;#2M}VeP@Uo4JEdHKq+Sc7p&o=vsNuwG$mF?^f)exw(s6zE8D&aH6YeeZUAdCB+ zZlX8C)AZH410R8J;el=iWjvj98#_=-NV)GNcFL6ch8SLy!8P%UxS@U>Re`(e7|>rH zD;3BE&adP}=Ot2=ET{~z)Oe@rWO;v0>XE-DlkEPRf8blqWT*l#oc5y=$~mH!I2F3^ zBvlRH&}Jc;cmmTa2J8LdKC_d+H6|-ddkCF;Z`1|fue`hiZA7ODhECA_m&H%lHzGlA zfJ?&8DqwU${gnAM?XO8z(?xAQ^b1<3%QemTt%&+hEc?=#5FZT;lIBEBGDN$Kr)V-b zETaPFXhuGO78qktRq!m1qLNB+Vm9g`*MQHKtsNrH=)X{PwKVknaT_xgOpTsOOJ<@ zXQ(j+F9G6YCb1k{!dvnGoPiIb3Tanxs;Y<9eng)6JmS(G&MjQX%;|T(4z%Bp2sG zR6koB{EB5pB7OvJLo@w7ti@-M4DDwPuFCqvOu|VJ%67=ct|vdsGD?#N=!=OtnvEO_ zU5EO5KeY*YQCp3E6AP94WEGe{7GzM6cb#keXH$~`_UnN9sLzP$$DcfF$DHtB?$?Vt<7+GbpZQQ`)a&}WN`&_ z&6uQ}!gtVM$o|jOUZEFSJ$TpMjbG3&D5Xz?Cy9cesg1yK=>!uD&+DU&`8ZRn2ei*) z##Ef3^&`r_?8=p}+K$HkAn7Xue}*zvLk7PCbPQg=l);kVoP|IiVl}=Br-{?>Y`X_f zffQm6@Iwm-tYdIF#(*yqO4I^hvL0bGild1@D;)%%u|85jVSi5Cf&PXM?yKg|D`*A| zLjinh5;XP0L}#NZUTC~UDY^o8dUg1xX+|nIh69PRdT-$UEr1qHccU%1Hl-oQn`Q)) zEI6Nyh&1wqehQMqNf>4ofpbv{lH4nB#u;t(b1-SO09=Q$L@!(oRYbw4 zF^+(4SOn}wOOP6@R7toRSx8}WFYNwWkP+l)@Q}*@v7A9HeEtK3qIWaO!>HoL>JGKB zqKOYAr_@tUlZH!`q&?u=H&R9_ixgg6t}cOe;d<>9+KP+8PWKx515Rn@*(q>7EXl7G z`kBnaF1{z%lfTRz;Z%AaHJiFZexPTenOb%2i0n|8$-Bh`p7Ow&yyorgF74h`G`?tp ztE#7+=d=5ioAGnfX1TO7SY4+tL*K{}d_{IY?=qdWu=ei3qinXIT0x(J*M}AbMc7MQ zO9$<;dd-ixhO7$hvYzA=LsW-@LfcTB?9cE_c6D)0aW!?6DzxPvDcJ7Z?ATOT%ON?B zd0YE>`dxwbauw~g@e_TgK0=50KmKoXSLivuw$!tC4jK?t&Tg`gwpFpYEpe8;mb3hR zZVq>!>p;JU^@X+kYXpWYk~aY z10rq?a9Ng1q|{2B?(g9H?2U69phq-uwe|SmOuNEy+A|Omc8`6xV1nN|V7lGZzYyrkxKz(eYk0x`5$Xr+g58&YYB6C>o@aF^BvP7VKA4(e&H16Z|WFoOcWWp#1DOf zLdyLlzt}WT%~#&@+I8Rc-8I(Dx?8vwyLNhd`Omd&d+s3GB7&yjs0{QWYvDrJ4Q42Fm638w@u9Dpcb?~q=YucU z`_Y}{3iHf}iQ5LBqw3+%R<0i@?e?fZB1lRH?zOCtrsXG6IZy<~`O%b;8 ztpx{M(F3?KTv_%KJs9q&esCXMG|mCVwt_lKZYTZ*tayQUC7efJd*eL$?n&Mi{-fZQ zz4fIAywXGEtU5|F8zSK%r_uk?hw0tW{yz^19+e-MV3;D+{e)#QPGsJEs6mv6WDbAI9f%>x9wV6J{CpiVC3(ax#J3qLN;#YZqGF7dnHq=j}W%wZ32Ct`j!MFC7UBY(d zdc*hDpQk`Ayv;x0-T|FtJNJvpr8(vioUL162P9Q08&kAPvQJ{fcVbFlNFd5z*2{Pm z?|9!fe~P!WkMNtsvC?U2vJ#}Ez#O!i=qp->w~!6#u}maepS=RAXFUIe_wtECYttiP zqc8%##R6^~_&!yb&P)?B9ZyFv30kYBOqCl2GQ{11kia*? z^!Ju5Qbjpm`ljaU<)KNr6n(+X!G|3Qe`hhIDhWP8Xd}!tRWW4>-Gn6YO3(9W*sW{_ z<_<8T6JS1aJQ1qbS1-v=#dPs7q`5ly%6S7G?49Lp<_+^kdCfk*&mzL#k&nssRR=ig zG@6Y*g3ok{xxh?iGuQxk4jiyFVYw;7eAEQ|pR-O-3X1`~u*Y5l5Ulgb)t`^tGk5t~^zKk}t|dU^`oAs*6Q z!u`zkue*}Bjc=U)gFicPQL3yaYj0r3;6neBV`(4LgnP=ZhbKT+(s7<-qDQ%a!6P3E$fW zUw?m%KwYV_G7^$4vxp~X4EYCrkuh_Kf5=P1As}cEv$!m=R=1@xFl5@8_nQ_7Rr$l9 zgHEB#Qk`%kB0;aB9*57;Q>+q@y-A)t_Z;^h?hqGteRfW8wRN}mIJ_5ph5n{uMR|cb zLiZT`(QkZ-8pPaZN5gL)W%_AqV)@IOY~5s?XRTp9Xz6ZgY3^$B@*y0@mZ3*e8}Jf1 zGnWM>Vn4|$p7jg9{hr-!*)`iGInO&AI{$O7c71X;1pcSncOo!JdZWD8hCmBz3a(2j z^a57rY(jyMVp7bd!FO$DduIj4hb75kvLu*EQyhPf9mY6dGR=I{#i*cl1V?g_STaz> zH^ws+e2Q@{pR>B zI2Swrc76kWy^43KZ$8Wp9VyRIXXtLAiZ>;r>B~$pZUsL}m}FXF9%booy=#49U1^Pj zzDs#advkqg8uZ}OnZIa-yZ|${w*Yn6FMpJli)5fBtTz#!Ki%(L&0I9-z;#^LT|3>^ zJQ>~^{uO}+5}{;48>Bm-qFrQd=#|89eSq9P)YQ=&W4UPwx0bZZmPAVh%PR9l(=lN< z&vO3&_3a5+12mkr`U5pic_S@>t9iX|uy?xWp*z8S3a+ymE)(=55~*>)l@1f4 zj~GAU?mQ!RmMr3R|2W?`=tE_@r^7GgbWL#|cQ5vI@DB3L@qY~ThkWckwU6G5fVMrk zoT9-=s>LmZj#qJ$%e27!5vCy}!aY+2+V(79Rjucyvdb8WqNp+03+Duz-bEGVi_(0t zcc7!cGdwv19?>(^v(9tM^USjf`ebeWRA896RSHrrt8L(<5Q!wTgS<-xXpZ%=J-A^& zq+2KWAaTD$*ek>d*Z7irKVZ#U!PRO@SEiSd;DbvadZDhKM`0OXeJhir)Qkwq^T* zuUMRI!6q~7m>}i|{f#b8U#EC@PIt%mV2VjJOb{s8Z#IrR*tOU_0P@KFCT4#8OiLF#D(xQS=9CThAeRT%^mrdlY&l~c-1rLuZS zeWSF4c?Ere#!>+PdkM1!rWtLGdPFR`1t*ps(92L@jjaYrQVue&!*L7_$F=bkbOz4B zHqfbSfmdaPllVe>ljx4-LpH1e(eD5I-Hl;>bAiznl5oGF^&bz(xh2L7NX3nYeAH<; z-6X&nR4|r-I+_f<+>u5{NHG!6)qe%^B>pzm!pw$ua88{LUHUr2G-!v0Kwf&CF&but z95H%A=5zvdT8}_4xFgU2ngaizButSQNj%0foB|&~67N2dLnMJ_f0)q0XUVe zha6iSPzHBH!g4NWug1K1+>Boz$z#Noou!-1QL(Sp~rs}PRmQQ z^F&A8PE>^Mc12Rs_v!&iQ*SaxLg#Y0F`sAzX|@V_52BxLgB+tCu#-Hj1^i?Sj-O>bj z#lfgEq*Moi(rkmjxC8F12f&G_fGlwLM;m$YTBUF)&|4X!1g=Z8)SD1zlw)WNE=e3> zvw$G*4sTEo8B@uX;MkqTchp(NYG$O?i7uo2)iXr0ewMgK9oIuhC!{`BL2mPmPG|}E zwf_utMXd^{maqDBwG^(ToJNW2Z8R~^f+(jS)_YQiA*pv4e$9*2d(%sv4i@b=3NdH;(=%+u@z3j6j}l*N)99!+oSP7TLIRE z9!cDW$$KTCY0Q8ka$nqs7Y4eM!<8;5TJKDZk(NsE2~i9jgJ66S(xRepf=%8yT zgnFWFz~_M{o==oVlQj>@)-#B=;!avse?i(30X1eU9uZhb{VkUURWZwGEOn#*@|`8a z)Ro3WqYQD+UzZ-|dqIwo>&x@menwYgn37G*&>C|XxTG$<7oMyS!;*0nlhkJIB+(Q1 zgXh!}Bi6VK-E9upt$tadNhx0~PG%65jFmtwXaU+Yjp{>w^^E=m60kAUVe*ms0oKwm zYA4$JC!6%GOx!yLk*2kWP)lAy4bfYWY@(UB??2ZMg;zONsqEB;!PB@H z{Igfx0)5#y-Onslj*+=&2Y6F8&{=R3CW7`?0iOp2^$tprQmG{4xc(GPG_DvWkq4eG zd(jRp6K3%Zh1A&sqXem=|DXjiR<1`az_(!!oFkV+J)!HrMmwy2gFe7kqA60ft=c!} zKCNQ6Y7Y$(bmx3>3A&-?5%G9D`BnQ1ewEvL3_7N*C(WQIUr|WnEpDb{qNPS3aESia zFKZ$wt_XJqZ|ozeWXBjC&I2dVEMgvJp@4DRmnt)4R}O(30JHPoRc$1qKJqlmm_d1Kgvb@WjZ&Hp7lez$#c1oiKhV zYl&naRtz@|Vha#R+CrN65sIOfg914ibk>fr#+}hi0EuTKFfd*iKZsA-NWBRD>+3`~ z15MjJ>jQ+OU_*83ZEj4jY12@>1l8^$9XtUuQ70*xWW=&FYkL*du^ ztvv&3(g2uY+(pd+p21LJsr~_5iJf>3kZvdHG)~bG(HPdN<_4inAwtk|;NU7c0yaS` zcz+FuiLlFR1KIU@F#W3lSPSKl*AVqSz%|h!?Ry^f2ZtcN{x7V-7GkhQqdLYjVyD&~ z_94sBYU84DM_q&NKvv%i$@qbgi{1dw{|eB#sA_BhCJ;jglfW9%+7su9;kb}k41Bc^ z;3BOe8XB+ky67KRt>;4`n}7s7PgKU>+h}%J8yBDi@+{0^{9sH%74S)Yxqb*z{#DU# zGzxfALEs9+;LV_gkA!YQCTd5vgVlZqG&HQ_CUhN|6w`=csxHpcMu2CR4y~L|`rq0Z zNOUhJ3iTYVj=oM0hWxilzX}ryVnMIor#I4`tE6s%7g*3Aid~qD7#fu0>bz8RiSK03-4eeh2L4He@sg2bz3Kwn61!otg;0d$K-C3)c?9 ze(|$=WV7qSH(U%C!rpZk zL-FVM?E+&-GjFmzx3SO+J!h{T6l_0ktz_9?xokQsbmvww=b@SX2cAJ(1SfE#ij_}N z6_`#G?!V^i=gD!lb@p&nEBajcJFidvyuzD>qYD{mj)pi{?;>wqU!L!-SP~rhr^HHf z80{9CS;DL*EpKd1LhFQ|3wstCA968_j0g=s7LpNEH>f@|ROeeWO+(?dmc&WSA_^2Z zm~~dh7^z)^NjXa3SzxTMoBN?77!u5rb5G{p%2BhUGi+I3a<}Ja=kLs$l)u=?d$xJg z{AU8?!CCqf&h61m6aF`h?6U<;0|j?}$llPzFg~mR*bf~-4}|m$zHDn`^IET1L(OYV zQIJf2#6G1PgM#%6{UoLuU$t2<`+dCR1=TLy?RH!)xRLuJ_gAhn`&s7pjM~{V@)qQN z&FPnWtgyf9KX(hyNpFt$TGNdSL?#)=T5ZX4(8?rrw4CxVaCL}#5 z$WFsIz8xk{?|`Q93CQ!6YpR`80T zYxXYoGxp*39yZE$+A`f*-8u@+dppg2&1KBJ;gi=jbr9C^GuW3*N2)v-hMMZlmA7KD zZ-Kje(UAP3d9K_I*>5x6rPs`wowG4#QFhs!`T3I^!(8F+2cC9;DN27mhM0}7(4qV+ z(^vBiOQdb0y-iTLpab>__8L%3`C`wrVcT)*Dr=UNwH3FPvlN(1SV~zYn^%D@{Fpxq z+EyZ+LDt9jfRg=>bk{%KEfyu@x6Hei+bt&|qYj`av$IBIH_B?4rR5Yan&3!xhPe0o zHb_s^Y5G3caUNs~K>-+H{tCZ!y6vg0yKSm%w(WI+nZt@FFZQ%;PYuRnhw~nx_v}M|M+dcNf z_PX}%_GUqEgBAr9*>Bj31(gZvXK!d53r*~i=79poJ*NMHq`^YXBlYyZb`Nq~Er`z( zbGl{s&9r9R&j`wVlsO@DSZ3?&skw#ubql{2L6h0b1e!_LV`j{hx88a8~iA!(2i}TLHjRjir^tQZUC}lot?qj)b?P~ucs8R5W;F=+9=z`E~p+%t`fkL_|tW%g0 zni!f8IwNFeaD31LTZHwD={(;CoQ+{f)0)dp{|e7#N8kMCIRmrXWUbF=o)-K2(C__e zlhgV1zUc{>e`kNqnUO!F(CxVGuIJkyxFW~s{}7pwD;mho=R2BW%sH02wk7t9K`(+W zp@pG?!zVtq&*gav4?DBmwS9-hYp^@r zi>%ZW<|p?`r~^(>k@dEHfAE&j^e}5ge#HF9=}`qy4WdUz*NC=7SB$<9Wkg0t=7lc< zvh}l|-nRYFNU6e>r9z3e>Sb|-w-NB2uV)|6G}8C~?wtB8rBlkol%FXRet!KqF15+; zgJ}mcZe-8Oa~AA$4050G4UvXwrHR#e2CcD`gmvaB)(UoK5E~j4{v~2o)T!wDF-FX_ zSfN9D6KgPxP>;ya*xO8?q(ng;g^(;sbPf+)*DRpZ1M$c?-_wyw2#C zcIDTipYMLmOkR@oDCu-^#E%*&b$_l(-Tu37M(6DEd9Mnt!S~>htq4;Gn1Q@t zZfWgiuM^xXbZq#T$j;FZW4acr6!$u=YW&>zQ}H|E>&I`5dtJ;Mn*o!uc0?`=Cqst? zeXu+iZnG_^g+z1ppFl@Xbw};IpPAFrzW<6yb^UNAjsD*Ad#$7sNsgpf$q6a)&;Gwt z(#K?v&+A)Q&)Lp1DR50G1^Jy*R3iIESZ_IE+XRH)knqlt#iGB(Oe%Iau2I6>g#3gi z#oHARFMcNBUHr$mJH;l(qUhHV%fdoJ9@(0i9o&3+HUdVY*v0$65s`l@lSva(tN+xJ z|M}kQ+sFTweWSlG`(8QeRPykY+|+exUo$J`3I!34>Fz%M!{C|^MMJ1{>~q0x8D!fK z^gN_h_~ghi`0S^Og~ZoR$WE{oZ&rL@@lwTW6!W3Lp;i8~PgGoDL`NXUpk z7ymi#Yq5E;;W164>PC1%Uj@b4(o7n+lzxCJ=&XFhm+ksga4aW2b84FUEB~kVV{!84 zq@<)K$v2W${WzUcEp^QAtLahM;dvzs9gafx5dV2OQ>zNUyn}hd&oke&ezUuRO<`H# z`y$)JcOP1ejGGj9EiNlAE$&oY^|+D6y2nlf2n`((6}CP2iS3zr6hDp0!`qE-iZkH$ z>~l=WuaMIqvr~Gv-;+|`ro8%)SegJ6h6AHRGL>K8hDOOcS z8@+HmO>nye7&&BRgQ7#E&_5zhMD~x~9upIr7hA2^;$rKH4JwuyJ1ll$Of+0^sp0mp z55cYNhPk9*WzE!kVvu%Sittx*H!Zr9cR71_W?FjM@0q`1Qdj((_4B`vmVWXx$-4Yd+L!YBy&RRr zzrcC4xRM>n^}1Z+9CZqt=dZ}M=6ueYllePiaK`wI#0)<3W#-+iN;%JScjflyz{wpbM9nM$sUn? zF1t+5gPgXxT5jw7PX$+tjykuxb#L{+cBxQ#3Uj`i;K9^HrZp$?|Cm}^(ya^arr-e~ zJww&dZ(*U~^~2|e2f`+X-3d(&Neg}vG}_+Y_K(E@PSYXo1ap)ck1_E8NDnD6gC^a# z9GVI3ovcGI^cNh;&&r#a*Dp`VACd3RUse!SC;-o8lv8$f^i=e1@-GwT$@A6z(AU%; zEj@!q>|st2hMSI$|H5J){J7%KSbtCUr3WzW*rUMv`H!u@_2mWd3GeeMyodk7w*>EV zFSHOhbJ?7YtIsZ`2T`F^OE^!whb)kxx7I7_pJ0CN7+(u02KpuS^qh!Z!@TgRL?r$bdYGf&v=>6gLdN9#z1 zje%rSMKu8^%mK(5$-sSFWBiYzn+wXc17;4ECAPpP&4yF1fv@t%nK1H=7SMK~7GzWM zlD1D>fNU^1sSM-=O~yoE=}LM_{S2Hu8-dc9kJ>}3^B~caNHKaq?qM--2ady8^t*zg zw|)xfia!k5e@XjJ^~AN{JzbF2fJWX56omI^s(wTJ4$Z7vKwI4Xd~G1_$c&rnyM0>P3M3vF6gIFs*+_4f#mjU zB~?8}oK)wcZ7|`ci@t#SY6nvp8A-y?fFGwUo}jH*(BdP^)Tb)s+59{m7J zeG3v#5jU7$M1m1z4A3eO)u}ZoL@%T70LH;vG!_^BFQKMi)DJ<*sWm^tFvRNqZ)9ht zJ))F$%x|NBS%_LF9rYOc9&iTEg7QJ&@Av?;CVGhlkZ231UECcl9(ssY>L;^Rd#k-R zddRD^kwy)=1O5f)=7AzbKH_QY)oN)qRR^2JhUtR>p}u{1hG_#u=&jt%0uDNsYtDoQ zx=FISOiN_Gk!Q7bvLuaRkD$shA%`*gQ?tm+>NrrBPRJX9key)Iw6UrMEg;V8V~_`z zCqIfYl89eg3>I-+)B-U?R?T1=Lzqc$+PVKYHXyTn5z=Y5hLVB3F~qM0@R<@`wy(;>h<(7N|WF*pc)~^`d-52?J%D zC;Jk4TAZ<2D}|0w8;Dr7CwL9n=mBlRuL6ywZS*ZJlK!f7f~P_`(#v#)rspHoByKcf zm;|aaXn)ltlYECXaTlq!Xgj0;M&iay2>Hfnt36QuK&{xXp!d3DTCYV;qC<%C@a#ED zVETtPP)Rp@&~sR%R|Xm@W0cWDm5cfi{2gWLo75Fr4A~B+!?|RQ8Y0EuGVDNZCV5T? z4fIf7aFN0Xn29t?OQf2?+2xczR*DPU(lwG4&Y^Vqq<;-CTgS5|s2 zN!%Zjin0gh}V($Wf<0RxV{90SwPpzvw zBuY{$ehe&>=Gs$y1v)+nc(>Y48Yt}|`cd7;30jcSL)oHNLGP&XG=a1AvuZbBE2rQz zAZAq48o+802J=lE#@}Lf@s<9PnoHG0VMar>lYSibUi*PZI8A@3>;aB$FJhv0TF)jF zVw%25*5NsGm+8fH)DJ5awT<9QWf8Yg6xs<+S)|-qn?!~23G6+xG-_}3*Be5s?2NuK zu-#W$PQ}@fCc6U9V@>U$c2rI(AAo*8Pq`ou!bAB!=Hh}v?0_ix`sWZ?-Z*`-Emd6F8LJFljHShB^wyH)qzCx2I#WETA_a4I7PZyfjeh35aYc6 zh;@hnlS6ezhtz-N-H0}RG%%KtyUB@UYoY`=tcP)bnD%x-iWMoNh|Ht@qc78TNUN_^ZmX5hB>b-$ z@9!>G2Y$3c-K6S47BGU$HjZhf^i^aU*ODDXq)AQu`GK2(Y`F^A$=p$h#ur4tbdX43 zrsEi?jQ750qPP?_5WKd3Y}17X=&G3R`yF_qePzn>yU4dnj&Dey1~fA6(d8*Ox(Etu zBdQ;jgf6Qky$4`oHz92!XR(_Y#J=GW_ZM_7&Z!U8GI~#Slk8HwXeiZyno4ejDP?`7 z6s0Fh=lsk?G!z&qrPc9D8^s4RY)R&veqCPy?|LHj(>$KNEMN2*KD+-9-yMxLZx3!D z3_+tI>v=~0Tdk%yMQ8O7YITFg_fR{!CRtZ~FXj>3sIq!eV7uO((x7SbzpJh_=squi zh+9KjOq?Mfk`&vFoK_BU7pnDpr-}5)~ zzaui4ZS-W6O75krqo}|Wf1WXoGf`u;cG@!X6#apCB-x}M)G1Q~=BsQ47V0~qhRkT; z%t0Gz0+Nv@$J0-lKt$@#PzdP)iohprxc&f7G45!ew0Q8s9+2_ zl`yU1BFTJpy2s}@;kx4QswNYaDK~$B@4?JBeDWxrq5mXdNBW-)h%k9?C1C ztJ)Tl-;&Z?z70F8L%1vX3;Iioq0P}7_k+~-e7p*;Ft({rrK5^RrL+phQ(PWqf;5KQ z<9FD3<)UOXU+XSUmqWBN`X_>BqPgl^FXkg$ms#r6z~O*jtfhV>`cuQm=EhMylzPdZ zxUp8tKg2!4ImKxgQ{h?~4(ZW%=1t6HIn_Jb!+CcFIujooPrY>}xY0$jWuv1F*{q`kDEfH<%WlObe=9Z!sfu}$?uHg$(hoKX6 z4gLYMnP@Kl<$2@Ea(|Pi=+npp%y@Pg{mXa=vs~0bHE~+NBu!Oyl+E?F{tUJlqxWDEK=QtO|hzzKMRX_K9lGZbkF-uYr8mT*oBed@YGK z_!{OG%sl0a>$#&{(OqY>U(l=JB6L*c;O`(qh_XSmnNfGx3VRn(H`K5#;~D)g_tm_T1>fA| zWuLK&juVb^FNmX3Ro`bvc3x6}r)Z9MjrYBCVNq^j2{_Xg7h~`jGZQgAx^vX(2!F_G zvzL5DWZ~B4nzr$_mcik{W%$vmm42_u9ah+n)jJ!3sLk6_^nE)`yrj!($~(n z$2|zBjvb4p(kVEK&XOTJ` z;G(!eLI9(Oe;49>|(7k-}n z?aZwq{Y5&02F5)r`6BLY_=u1a_7ZF*e4WY!PR0TI`=CYk8o~@{w@p(o>OAT|{|9Z> zH>j#=kqnq#*W0@$kfx3S-!52hC|!3A&6|~KB;Wm3E2%@uKRJXvN@x|@M!^ z?y|Vs7Wc)yg(7ut8c)V&zWe=s#~%E#NRvz^nP;B+y3W&|jD0gS3ICk9FuAO9-SFX- zX=*(>TX-misTP{5S&vz&m@T?AtTsPVC{xA}hp3&9?W5=-`Ac9J7~9ff7a78VlGBrDa^w?XwSv4I0s_=LUwNZ6NpJl9N z7^o7FWR`Z@ff2jk*~Pz$E8$zS<2}8~CgyJa8Tgv|W$ur$#o3;7XkT+tbk{0hs>D=E zirEl4&NN*;kA6%WA-CFuR8yCUAnYZQgN>zzt9NLx7+x4(YtsoGxabGt7*k5!aK&$ESbye1;{7ha^HCE>})wd$%Q`#BCAn9kIpXa9gvDd&1Ve|Q1Cf?&NEzjMR`Q&rA zPfI>azp9s$BCE}csGCTo5LMsBuZ(`KNV+w3j10r0AfNJ9Qp8{Ka=f-~pt+4r zvHGoLIu%}pf9ef!&2~h(o_Q`ZQLNF|q(WUhFl$`;y^qOnc7Db4bA6TYEg|cZ_N5N5 zHaBs7?1GRH#u(K&ut^P||Hc)thK!No`TwMJT(5sgcf1)_-FwHC>ulkf!p4FpD~I{VRjVZZcS!o@ z_nNnNK1OCYuuI%N?V6~Hgb7t6lY1tH#U+PTx31K4>Pl1-Sf{Vycd-c!5H(Sw2DQb7 zN}(r$Z<=aRz4@A6+F7llv-63+pYTk?`7hpm<*#$inGfDeZ%(}%{c}f&$n;d(BE}}X zNV=c0De+@mP*_Xb7DFRVQ%x`O38E9P@ISqud{g;Z_zYb;>!I+KVO@fDYKoL_PVZgh ztl^^kDq(==_D8u=O6TQNO27Jc;p@crqkhjW-@$*xB4*RfXjEWL08N{Kbf!wr@tQF-bEY?JM085axmBx<5lY zLp|3QTYlO6mbS(f*74>)B>jI$Cveiakk}(7(#g*sGy+ zf*8wNT^6=n3}*lI%AOSFfP93`H2z~fYw;UjYhGg81>V2Qd&Xz;pY^r#K5+dgaplDS zxbQjb{nWRezr4yG=QPO+^jQ&is?4f-DJe2>YyuUpirp45H{`47IyCWjuwmYo?k&EH zycNH!DYCAyRX4j01JxdM5|$~7jow9e@?+G1s zQpBW6SVI5AB}pY!x>ed8tB)QURX^yBwz9H{%W+#PcDcb(hF8}Nx0M8)upovARdaL$ zk8&^l=RFUdmWs=TEpv3g4t!U>wn%TBvHnM=qJ1t>Y)6l_^@%_$=O@%o+EbND`c~N% zeJ*U2d71i)@+mOX9qBRoj`H;sfijvLrXiLA`cIm4@*g04?&gYpZ5>qU%7Q<$#{ITu zHu;h9edo8UKRe|Au28dG={RfWh@48T62~RFs+1&pr>cuGo%zFc*eQE| ziy!b7dZ&4<>AEFTH&68(xT$ZY$?OCFMCahLp1H1HrJ3V1*^HN;FMb;OZFp|q@>Z;t zz85kf_GOiOi7ONTO?*=Ibi&QJ_-HZsn|7;` zvRMk|r+R-?+$k!~{`IrgPw)58?+Y^yW~j4=7THPR(X+pE9GOgK)c2J7;-z zvtJ8-s7=#oSgWa$eboQTy~Aj^6ztguP+#7{HO;#tO6Tl+XNZG8gus-!m@g1=AN`L0q5Uc5EG~}?u!@$-SL1rMkqN3F z%_Ih=cCL9cIV65#_#Mjy!vyV1@&g`^JQV8*iQF}AEf6uf165+N*oCD%&mHMyzOqI3 z2&k*}%_o zTV+@;6Ft-&jdN`U;WMHSR4Rz88*?n;Xz-qpsXqmaZ%rUQ$>pm%ptz=W6Az%*^HIN{f|t_+wRn?J|>K8Enf9 zS{K|S_(0GfL8GiyENk>`wHBNl92rG$*d(+EkOn+rIGB!uS=8SVW-zJ1<0*6%y2iS* zz0tl-{snx^YBZp)OQ^Qn}Raq2t*a^7o^t z4%7$o2HpqX1%3J1@KhTh1Q<#|J0G!OY;6d>VMT0dT$f zl%2rXtqgSCi^^+s8dePm(g%U{oPZfI84ZQmMm`pb$6_g9NUV;wNB0A{b^;oY{sJSV zT{bIy4RW%s|u-o7i9H+DA`J}v`L8oF3BB8bq0Vs+W;L4S@%5TA+)P6C>?VWj}LpWj#9J?*bFrafkgU5RW_Buagva5nSus2L9y20~r3smI(kkji1Y|}r%PADr! z;orVhph<&NLT>>ddn;u7??P^WAaJt1ke#~+6xP?k!4`nyQeUYDo$D0H4h{x)+aJJi z9R~E=-N;n%@127*b}g8xFz~9sK)d@3kdSjB3;!G(Z(+!V|4Rqf01k!=e)13`6?m_; zz`REPAIF4&SDFjm0yU)bFTiZ3GbHf#LIS@O9B&ywpq&J+I2@?4g3=s$53lMPoF8TI zyuO38?mIk^Lz#>yFzq41N*4uw!nSaftN)*YF97F~0rHVGfIDM_c~n<;{SV-=f5K1i z0CON5@{fbzPpm)>{s~8mgn1Bwe1xM`0>{P*N7e@nUptVC&EWIf1q|a`;N=U3cL>TM zN`3g&v>Ea!83F~W8(k&@VxNcbD=e2ho8O{+6y0)ba=E6 z%13z5Lc!~|3s?-_z@HTXorxjPK`#PobZt07{a}jrU9rH=)x&WqgGVd|3m;I0;Yj`s z&GbvaZ5{{T4L{*sSqW#11oUG)7y$o<_W3t>%p~a26Y$gGfwEi;IR{3#6maO-;ayb2 ztB8diWggPs3^e{Tp}$)N{!JMCZX0L;>;+1;1V{0IyyXh`MZ@EZ&>*z@?+k`a{tjqZ zRENJ`s&L>I)WE+gkz?Thqv72qzz_Hu-V+SmfE7UfSq9UyQ^3{v4Jl3q_}nX@FHjSX z`CZ7h?}Kyoe@E;YoKwlj4`~>1T~zQaYast97)%IjBGJlXcr;lV57W9k;8ARYTmzFO z4SCW`piTz?5v+w2>^$@i^ctFilX0u^5lw?< zS{a`2S@<-J0UJnD#0CVT=5SW;h0pIMun?Ys{s;^2YhUOLv;ixk4oK2F;BimGXW0Zl zdlx*%s*oYv3*VvHz-Bs(TnBc=1^E2bRJtR-WgC3|c;%T$GkB!Gfn=~0yodATAn0kV zMhc{f;Pg}AHReF$CJqUQPh1#G4Su68c-yM?t6HxAGVN7@Huo4azTQQ>5Uz=xeDdK0`VTXR8`! zU>%YFO~MSyaHTnM4LDaDfg&A+w1Oo5Z7fY`3PcPId zi7wD1SO+Qcen5+Xy+L_0@WV&IHxTfZppD}O7WW63e{=z=&kUs-OqcG-FjJ7~sIExA z5gl4rbR&60vba(gE3d~ed^k528%gkDv}PmlK32nyU4XbKVQ6>s2ABy_q17eBH})^& zGXDV!a0ysDv|Ju1cSMc_ju2JBs-vR*La+LAsEUeZq!}h#4XDrHzb!{=a4dcR9=|(2 zOY-8ir8pH09iraybg3yYn69JSpzqXzXb$h>T=F$vgYF=%0iw%Axx0{$b>K$hDlrVt z6BnW>@GUh3&eAXr@@|5N9u)rp&*lkq5q$4-Mn1_WHU?`VKh~4 zf@+~--bvg*K9QahTcs+(2_;PIMrhy~`w)5$e}JvihK&It(luPPbPav6Cy4V{UGdGcyVmoZIbVj*~0^1k*3#a7q)Ksw>?6F)B{{^4LGI;=64ecWV z`;^p4`++vw0It%Fkq%U;yq=r@d~heE9^Rqv<>pw5&q=gcLFNECr za%7&k1-*+|fhYD#ItbbR=IC4$QMSRE*+r(Y;lf)udS_$`Hz3bp9~4!ZQDs6cAcBmR zJ`>G>4BiBsx+{PNe-&*Dw!mDNDUHVuqsvgcJRkXj)sp^3CnC2Xt1t|%tUoZF5{b-~ zZosF_CNCvNf>E@lyg@mH?}xUTN*1Mt#2Mu>-U57#!SHV0hAa69cz50^$H54qNDqN& zriJg@N6_Cpj5_6^u-5DeSE~SU)ZNMpNrZ;ga%e%Vm8PS1shX@qddWS2K7R|ylc$yM zz<|pZRmf+_gIlbQ0OE| zEigv91-+(T2nOce3zAi_U>OJrOm;$AjwIm|VBMCc2(Z@b32T-~z*z#m63kgMl|4u> z(AgFOD`|}UK^z1tvMusHw7N_o4lr!CK@8Y(Xry0-zQ-tK8u|~ka7wTn;GSLu9X$+t ztE_;;!DXZtc8Z?^6SDQlU@S!*2tE}8s0`;pRbAku0PLv%00=~h;gzq zH-@Xse-yJJDY2ReA|4PDwO02)zspD%Vhz7dN3Aj>sWF|(0J8!Es{NPVOm-n7iBlww zxWpnb)&%qQIV(8gF1XKoF>h7ZF2{JsM|(fl%Zj?r4(^+-SMGWNk%{N4@t;Idz6t$+ z##AcZThm_`X-qSIF*z+Kt!u48wtCjZ*3Z_~mNBNEx|O;<+L7wZ)K}~;VD%W}6~Zol zEvJSS|79@9pZ5LZv3i!e{`IVLeXclcALDr9SniqSj&Xl-hXppU3Bq~dgGh-N(2+zw z^#Ep(omGG9P8c>Ary3?24;qh{N14k_J&lhIhxDIxYqSQ<1ELW$ZGoKyXXsO5p}0gO zh3V{AHU*}RN5Gqd`yP5;xjTB2U2hz}${*WXyH|TvZodtVcfE7}39&|i+t@bqV$-vJ}6;{i7XkAw!o2gc5 zB6T$l1B}zn#TKpEU^!!%WF<`ljZuaO{cvr(N<*dNGUi1d%O7Dq=#dY}Bjvf$PRyTr#3$m?MSt;a4>$ zbi?%*Oz}YBVcUO{wmX(t54RnWI^Y3}h~)ql0id0f1uBE8>*U}BtEHLagbQBsOB5ftuHJObbrv>sJ)s2y2=^@wHD>UwS7yTB1SNiyuls_B)eBRdz5u3Hk3Rp z>si{TI4FNoepXo#kf`?nxh9c+gWREpf&p!$Zn7p>GXdBXYjnRg5=~RpsdvD*eNGI< z*HAyyA-bbF1k8LbjTfzPmS_5%ny>0Z8ldH3_mx3{j#~>%svE*8u-OItuek-{E^eBy zmK$|zyd!**Dux!>3T>sUoD032vu4F`cZoEEuBxf6{zkP`U00QECt@zzTgJo=;!to(-;fHJ_FmNc zi|Hud6t)B|db6B%*DRn~40GRfNR9{YcAjJ};*!c%RlH&jBW8R%q0p_=e#3v3AoB(N z4z&VPnb}wsaA6Q~3oMy1fALpKEzgN=BanJ7&E^zzYY3@4MBXbQ~ zl-@@CA=&AuQ>k!>qx$TfOm!GS)d$jVw2q69K*d2xX)re zcD<*Rxk#jIXf;WOz`fcx>KR#=fFOx71W3eDx^W@ZLaUjEsAiG}H61iH@U5`>paVAf z5`v=NsLH7VtPZru2_*>7yiT|tXl)IUCqgc&Q68bPG z!;qrCtN#W*U_X2-HR0_QFN^Q^?`t=k%e0@Ufs_n+;ZJBga*Fycl__5GVjjgej~@mZ z))z!w;u-c4m<0(^u~1j=aaH(YVIJ6FI|7ZRm6*U!1Rk`5$&iK;r>TGFTEt%YIp3T& z2`Bj@;taHlJc!>R{YR6Ub?Q{|I@$!SO)sPN%jMi%L8Dn^BDH@hXJjKz6L*MR$|rG# z{6nb0er0AcpcgOrNgiN1v0-45<^_DHlrP)^WudNi=^?EKEaPiBbRigFoS>qD^d-U9a_!9};% zAGI7aE)udzH6<5C($p=et8S5e+W*kkfzMNJll|4Fn0v*$ORQXu+H3x4Y^|Ci4EJQa z&ok?!C~PoL?Yf}vse5#LayoVrNYh35LcWe)_U(hDY;U}X`l&+ciG)wOE|w`d8jGf_ zl;Nuh$;7G1ZTS!+n|2sZqlw;tXH}rB|FxK>TBVyx?!j6jn}p%wO?n-1$Njb}*fUgF zf-lq5HEPV`wTN=df6MXJb&ILQ?eMITFPd&fjJF2#6V(QJSt(m^pWJn*qqQ}Nh<${qenr@c5s=q~_oA9W-=lS>K0?m2!wX%R{qw7wd0zPLO zc^a^v9w-mUv9NNw3Wl_sLStw_`NfXJJJWOvho`VtxPyEoc7pQYk%1ff3x2{;Jzewq#1nYrC!^msJ+}8E=sekt+2d$PIRc zoaw>9Mn4(oE%pQ>#RA||o`iPJMWBS$qgx=TaDaKo*X18bJLt`-EPj`#oGBxUHHglx zankjq^ZpaAa(6f6qdS7nF}FgRB^*~q zt5`#cdIUO$EoLLJe)M4FU;Y_iQ`jgBLwcw->W`{EVrp$Uh7<)fdUllp-5kX%#l*ys@n@h^i3&Rq77Y)kHAN9 zFFaM5HgYQMHdHpRpd;nw0UC@AmwARhuRckFGlV>=jAloQ6UnZMm47I1!PP`-WiVMC zu5k?9hwgwo#~Fe}x_~s+_SbEYpL*~6=0LJ~iFi&E79>(}9;)Jq|A?GKtbyF#Q+>YW zE_ILV>S*eI>m2E4mD$G3;1Z()X~e4dUD6Hj4fi%?Aeulo)7CY-(~QL|;$&aA`+)Zs z_c!)jQ)V^6ELy~xkl@twzVP*^gc!E&m3-?m&d=Vph>AIP+45>tO zcCPP-ud;6_Bl2m)WHn51aRMcv1@T1aDO`ojwO!=JGOQYSSSE_Kn6vCdq!pC~zOcJg zw8=$2H``lbRdRf^?^U-AU1H_xf3Fy zr1I6op{j|xfg~c-@^1-D=Z?vPA;DBhb3&Z}Y?T-CHkcSBKz6Y}Y>#hKYe*aaF%T^e z#LlB7@^^j{Fb_9KF1&=ktolx2_!{w80P+7VY6u%X8|z69)HyV@iGiY>uj3<}A3dR{ zOI1dnCC=iHiAXd@-Y0C4B9Q{oCa)tLYM1&ZYJnDqhuz`a;cSU?P@UD3sP5|CX;suA z=A5g<1*|O(FRd`#3{E%RRLz$+u)ly{IN3Xr9gY*4%S0p@YKpXsM8-OXe>^Qzw>~aP<7he;o;(5#U#wof5nV|Wl8H|;9 zBi%dwcIgBeXJ*Yo+B?`A$Pk8d&DhRN8qi!A{63aW$Lr^4UZ88Z?cOUDpB)Ri-kMn3 zZ|iCOBfOdTn)Uf+d-A=sC{R9)2#Ld!#5<(Dc#v`U`vpddH^k4}FFe|CR#zW01{S+f zkHDsbq4R+%#+a(RL~H>{b&=Sb%jWvB4TWfNp_D2OL;b2?eIlhIH24wnFZ>H%oqx)w ziKme@;NfcoHoI;}Widy52o}k^GO$9(7~BujgmUW;xE_*Ayv2GI{{`mtdrR(EL3Kqeet(+DjB1w z#52NCaPRe2Zpg6`0UX_JSPdZ1)JNyR3N3-FiUi1H@=vlRu)lX;iC9l%wzycTDYyf3 zyz{sk$lv%OyfcNUhmfV1U#%>C=rH-gUbw+iXTn&3J^?+5(G%>Ehfz~&G(+&g|}Dlr^i8aBvvYv4dNW>F6@yP0#kDbSH{%@_IxjG zu`WgvgEbH<^LzQ8$|Ee4>_%M1*2=ZvI%SiNf<<{bISlSr#{u*FFLVSxl49|TXgyBj zJ>nlHKO#@+Ug{UJO+|pgs5z;3i(h2Y^1k0;A>8;7kM` zEKyDKQul)@QC%~1JnHJ&7b+h{^BetbfYNrHc`hDDw~_-%JM=*22z_AX z*B&22j8x8wXT&%mo;l>7B@9Ko;CJyo7%*B;NemMPNS&}=_&_)xKFG(oL+oPt9hIRz zPG^u3dRiL7PvjS%lhxZy!6sH8qdI^LWijt|??aYHE~(>#XMqtz&kPmU^fH~VSATb8v5ORYErc?11 ziibJw?pYCTZ|1u0>&Aq0bA=%GwewbKY}tJ~UOu71>D?|rpm*!+<^i^KmOknYlFr}Q z{n53-cU26cTN+oJi%pq^nfeL9>UgX=p}I$}AhMBr*b-pAF49&QzLaH!!}@ zu{2Oiu?XOXtb}#Z3w50NchI0vPjHN_vMv|FUZA}{6?m|#T+xnJ6&d#8^0Ly3vT%D!+2?|Bc{K|#dmGrQuG^8&hRlfwhN)u+_9pvn(}q*0xjC$7jRN#3;D~RzMZ$NZVRl z4@-6PIej*rgl+}y-bOJ@bc)aAEST6Wz_t(<$e!d#d>6V}3gQs|cFz~b^m3v!tyF<$ zQs2?qb;-Hh{8fsW?hWn3V84))TY|kMT7vJzdnUbb#xqaGzdn zP7U#eb_;tKD%zaZ`L=Rvvgr>^I*}sBGN-&p{Z*7BD$)`Xo*gYm=G(4o>ky-*Y3v*3 z6u(HGttcz&A?qzCwg{z~ZYsHIFfByMj z$1_L$jw@~MTTYmRvg1!BMO9`a9-6|@2v7AgLz&Le-SeF9PyDAnY&c=uXQ>nv6Lj6S zCg^gI)x1Jg3%w#-2wd~l325ag`lq2VsB^^L$hx6M^B}E@+9sdk_A!0gJ%QW4W=xK7 z2<}FYsltuTE$xjO4NV*ttGPOqT!&%cw}Qx`CiczlYM#38N$yaGy)3L?Ue5fVp}*S~ zCcE#+uMK3}fU1tFjVqsuIIo?`&v7g(JyX)5-0IycZX+`_&keWD^KI#&eZ!hY^bH*n zWYuj#C$noj8=Owpb>;zT)3KJ%K}$nU1u>=@ni{G!dOfiQ`379sfdMmQskiwS@E6hA znmo&A>owB=)d$7pUuV~pR4Un7ZgmtmpF*!V$erfcRB^Q;qHKNffZW*MmA|jf3@zN~ zX-~ci?Uy{Iwx-UZs*@t#(3t@l%!NbC5?oWbaY!}N1ZJo#ZBvsr_)GZvC`)X=$Rz7w ziWZl8A}bm?=J=l|sAi`5t!;p9n7O9z6tt4yS^~yXJIu-M@tMBE?z~2-5#)q1ds^vyE)~AZyJsSI^(ogns-z;$eIaRGOoUpVGZW9_5s*V^Cp$$Uxx5-g5 z>#ya~dh(b$@@TTQF45u&nr=xn>{NT`5#(32Cd{zg%Y}S;Ak+8TM+DNimdYmTqJEE| zfxe@yayNs-&`QrJ?;P(E*D2Qs*F3veyu4s}?t~woG6v?Hu#Xaw zwI8E*Ry$j}e~sUXzrz%5p4ioGDt}S-%%0#X@Oz{mM3LsOxow0uZeo>tm5XBAg#Of} zAZTEmgS6kN=;YnT+tCQsF#QI@RAWG26MStAbWb!LRN=%Tr4q+@arZb^AJ3V9Msna8 z>W2Ca`u?i5_*$tCx7Xj%m+igj8|RDhHGtVbd(TAYyo#;%Jo~t^WyN(0GxKWxT=L^m zPMYfsGTb^RF|PirM&IiElca>EsLnDM?QKgtl&hSdJUVu>Y(hhbO!Y@=+o+@QZ4$0j zW+TsA6RB8!zWWcy{ff~pr~jdpj^yAh*+lh_-lqDhuA$4(W$GrXPZJ9jFL&2h?tbif z?tjE>ly`$A^P*}E4gDu{h@2!HuHFPz1##GEK8(%yo z@5Qg|%+Fcf%KqVN=;LDBr|zs9RLhq5KJOzdA;A{=CU~>vw%pU-(kpl>1$uLLrRuT@8eh%iZ^(7ht^TI30Zg1mx_t70a)GbG z?D3xoG~gM z+f=7a(P3|5uf~0djtre>oJ39)hX-c*dk5yR=Xgjj!=6$Ibbk}^v-Aw@cl}jERehQ2 zZ+xQsFJH*76NUmUG#S!Pjg?fG)BFc~#6jSVr(n)ESL(u_0BWr{kP$eMcxlg(QF}W!>Q@2*VQK?Ly)*fvH9*WjtU;Y+auCx|D0-Pv1^CFzr2*LYMgC*7}6-@L6B&EXsT@ZL%WJzLNo+&qKB#HU*a1F zB;to07+1g#)tFpHo77?82<)YvK~EosDaZ(%6A>0uT3B|$`(IsvHCyH0n z0R!L_bTg|73xuWoPIimGHh4)_+SALPmYyiukpDERd*+cJ=kxSVQI0kiMXawpFHxJY zvr^sY9bu6+y&fa4iVu8G9AC<3*?V~Qvkqhl{XyGDkCx_Mf$W&no0Rzh%8{ zhRqwcKPn@-PUO<4^)a)eTG}dWXJbo+y@B`Mcb+iji0~ZxxvVmx`W<-$pMq%QNFj&K4Dc|C=*G$7S49Q}dnZCeY{TopHT*bPQ)N*xp;ZyYiS&prwZmC|V_W=9; zkhZdxh24TwG7k@f@2#WacHtnuivPyn;CJ$wz}6nY#DgiIi}#1C&{^mZ9C}AZxue8X z{HwrL&^xzlZf=}p@C`p&?-Kci^`&v}4qH2D=XDJR83t}bVX{kTJH zDQghtD&J5)uBRT-8LH9XmdI7x)DFsrUqcsyn;s%Yd>`&4bDo{ZR$`-=9sYUVZmvD9 zjxM98x%asDjyuzNxMHULa;c}dQSp+JZxv?$55a+&=p;iU>-f+PkuRdPKw6p=77;Yg zqy@|N2e{*(4QFO2=Bxi4_%$vGO=K_HgKVTqR!>r|)BUG+==A#k;Ets!>BFvr=_U$x zi!#_-Y-hey=qpfsE#W+5O}>J)_${@XdP}xZEl|Hxub>kM8a)bJ?d{TSaXj3I5BJaV z=6edNb6weojB5_d|m|hun+rLh__FqE|>1 z76GU6oiEI50Z+_M_Z#5~jcfPXg$ zsQyN+RrQ8Q!oJC<7{@d0RK~>AVgv^4Y`_5NiN!-}GLt^8ex$K!+N*rzcDxzZ6ngR* zaOIfEmcYGf9e;1%I9L(ZMEACRK0z#mEFUqR9!pY6+U6i>=Guth{uGM2gw7VRvpsO|+`lrsE0 zSklUXP+y(<5x5lC7ntQeU!cxpnfe7mR2{mMD3d6xx(-EDEOXOJ^RgX|tQkT%# z-~caxM#y-jxwKYj&iCM2v2q~QU&Vb3R`2a${WZ+l%JtA?cU^KfbTliSSA419TtWTP zb*@~dEz+JGpw+Fscc0f z$$>*2?z$CE4{t0?eJi;im3J-L zlV2-$Pu{Y!fu2ijS7oc}gpmoxqL@nS;!ESak!2whtqYA|>ideFdjV79V!uD|i%plF zA!Eps8p`t6dJq_0iKbDyYMRk>cVYr?wGVR9>=|0c+NVc)5qIFgfqN!86GyEA|TmFj8lJ?wks zR|OXNvhBqMud^IKFaEOTUvQWtBgyJZEdKD!xO$1&D!XGo1}`$Esq&E<{90c-_W_sN zdxt#^w!x07qsG@kx5B$bya?+WBpX|(8{^xg*4!-rJ5N_n9IP)pioKBr_*gQV`bs~b zZS){Wvgpv4iWAI^)qp2G0~kvh;79DibwnEQEPkNZfW@zs+VG)FJKuEALibe1mx`f| z8}5s~!M+aOKf$T|(k>P)`28aN;`g@2PuNgurhcM1KBP*_!h~Zap*x=wRrkB)f>2n~+cf-2~5^9f_XY4(37{<|~ zfm+&G+Yn}}B{+Clz^CBnk3!=Rg+x(4!BV4vW|2xiAm@R_Dpt8K)aTX)&iRgbpE}Q! z^)Fdk*4{ZMRep1CP0X#CZ0ix;yi#OhMwJ~gZG%!Y z_mMArMqrLF!d=(9#$O@aAa5D&*lI3F!jgz~9OBU&TTD&5|p*HGUxJ#;n_pAhMqIM%cITYmx5g zy|D?AN30q2Z!y!g$o{#ai?4zkpjh#?>dCetQABJ()S^(?yi#)oc;Lslseve8!$2MW zwt^6MsmC;`xn_EYrG3=@_%|3 zc$e^5NPB9SE)S+PvayHuF}RFp$Vr^b+uFA@Fp(|cYN1ATlIfM@tEI&B*-)riN1PPf zFg?9@UA$wOtEZ3UisfNg6QUHd&@H(e{?D#|oQEBk9j7X8mfbF$TU@hbYjIFXufl(d zm${O$Mz;TAzQ%qJzi9n#JfrJKwnFankJ(+GO)i89!FFgzSwn)R22Bon6#T(9!!%5t zjD6;A`!)p5@fmQBP(anyT-Toj@?1@=N&QGQjp!82 z#theuql|y(`>U@I+mr@ECKD1!1IvrZwBwVc4q)W23zVep0`GfVVJ&}ef9sMwjeV`$ zCoAff4l1l#@VVe)UiZRB6;F_vwu*Q<;d*>RcyrSR;-k2P`NLPoRohYI8pEl`?v}mb zQ=-R2jtwes_&*a z4&SNzqDjyl*R2O<`AK>R;Xu|&b@?Y?iP{URu|?tt=?G;1uR$Ws$ei&WahYM)V3l)$ zFO(g|uJAu}4K43f>MBYvxSo~&yI!G*nW<|N-61iga?i*)=6R~MNGGApFS?#o1RNdx zgOP^%Mj__NEio6OdW4pm(p7cQ7u;d*epfw@#Lpw%X?L5(TRp+^f{vL&)Z?&*!XbbB z|E@oNtJ0HNsePkwZ0v4G*LGEXATrP;@?2pKhwvYTcM>Yr78^*d!~+5;Jm6j2f6NQt z8fQbt7H2hYm%uFWhHddIsu)=&mt>Thi+1Fu=iRQDg?5PWO zABh8*Jns~5T41ZBC;!w8&{v23{SMkt)qJ8MdORV+mhUhQyZyqiXOo zv)g>-UK?17l6)hDZR(oAO7!#CxTs@6Q}vz6)$(1|$Z7a+k(U?a^HhEHUrp0226GF2 zU%DJ9RoleNd@nAU-yo%<61i7>Oq;Ha(r9Tfo`d#7A{38QS^6Xn6|;o`!7aQM_6pxP zg;~m&nE?T{f42LvE6P*u^Mk8sWnhALioJ2k{yclO=hyUt`JRrHI%I5Itx7v$s)mr7 zF48t%ne(Efvoqh*QG7`KX$wRyiv1F`E999Wg*q5cd_NVGSQvP6@#02+h&V3K_ceW5#$ zE1$u)Vz&qMo`0P&&KP%ZUuCwvu#D@%Jn-G}pstn`jY`)R{*!YlAN4k*(XiKX=i`DS zn^}%icf?kKmXKfSGJ z$g&N!4E69Q|P3C3PR_5ymQ?q+4Q&I9u#5r2=)btwPGd zkOO?pb!ATZn+0C`d-;BPLcQs}AHdns38(#tdwkjHl6xf$%KMeBFM3f@mwBps6gnmL zQ_QPy*|db5E8O?xx^rF2-T(Rf$R4#Rh>YwR^)f8Ok_KmJTWKy=hr#)&(l%IWP1dED z>swXk99=Kkjkb|y@Mdl?rvPVg9CnJHq&0#;Y^?fEaxr8g7mNFa{lWy%FC7KK{bgv& zJ(cH!gD-`v;mdJHy6<}q``R(jIIY-S#Dxw_qIYdYeA(I3MEmgaS4A81>XjVg^UO-@ z{)8v7uWXA+NjL>}psv6oFYCJN%@%j7+XYRCPK#+1sj?1JFG7<=iM_$x6AF<2kPqIV zJ!7bA`qS`TlR(V`V%-Ct;ra?bDS$k}H&dCa0(F$SiDro!qo1PHB$bfO2!V^NEFMO_ zqWdHdKLzfuW(PhpTe;8NL*VsiGfx83nRZMp+dDAGJFa4OF*G@&fLw2gfm*GaQF zczO7#2vf*d{S4$H)7{_5f5PVk+7hFrX)amH!q!L4kBqThQ^g>&gm~@~TZebbR^p_l zyD`-A!)!CYQ;T>%U=NOi=EhpI4>YD;p+g8A-9~*(-Aa{2^~0N>8dJ?@S{8QLN0hc%5J={6VS>NsP-tjY@InIBawb|xm z2a`QyP-wClXmrB9z*+_s|Hp=#37yD+c#w9KIf(FzndY62^-f zsk%5!ST1zpRZM$djMwIi4+Pk^>=kB(@44rxyQwG7yU>e!8JE%dp}e`Xso+$9ww(^` z5&Fo~oLVBZ4)hIVaRY2h(rV zC7M)ysQ#Af8Rh_+!8E?Fl!$gE{wA-GHAxjYM0G|jsV-r?q`ZL6f7dhAKapDq&hvkf z4ie;AxKnVaI1aLsW!}c__U^~dH-K&K6Bxx-}9 zXw7=^D;O%~N(o}Qcu=M=vwE`8YQ1TBYUriDL>xg^qhqkg_$b(&+=WLGqsbw3zUr;I zsro4O7MrG=ld_~?N_#X0uLktL8N_?oY3)YUA!YcaujYHO4Ir)7f{o`paS~_Z$8pEF zJiZm&BaY->GOzqu-l-ms`<$o5w>dD1+0Ib>WjbY9b<@jyK$7J3z3dYhg{)O^bL7PwM#vT_K?TO0C3Zelc(Ul z??a?wGy*b4OL0!^jK& zkE-tglcIRu?w*+qmvg@3IC4%Bl#B$45+s9xpd?YjfS@ELDnTSk&I%|QB#0oA1tjNu z2bc5R?roUq{@&X8-Q)keeP(ZVrl+T;`|Yag?wWe7_iW2;x%QBNvw^P#4)U9ADZ<<3 zigMD@*q#?KGvLjDpWqwtk(Q~XfEnj9xUf2^``9k|qujxTDlJuQFSTCEKx2~kg=dBS z7W^DuxZZV7@oWJPTnEAQR_>vt0VM}ZMmv*Tz1_9k51o0XVWoE+kHP5I!Cl=|+u5;H zmTYltm4~fI{aNs*LH+#S)SAk<`by!?2dO`3(`=W)0&vK`R=`3qVqUhtqrJ_GAX&Po z|5|q1NBYmOU$k`4Mx#YN>R7DqJXYEx!rx($hZwHacr@OYugflDzG;i=ZO;Js`26kZ z>KWkK?D^Q8;}T`*jwYosrR$xT^#yC)Ua{TV-nGo}NvX|I6|?klrTa<_IKzxYt&9Ja z;0Ym}0}XY6+>1Ho19&^n0{@t33zZ+g(wv zS*{muhdaeH3N2jSalEvyqm?Vu^S%DW`w4vD?m23?8W}4TtEC8>x4jVqGhEA7x2jE* z^LSr5jtx+DTDtok3HU5f^G~*yS*~g`wThOi)@bWG%OY(Ed}XsWht}S@*4o5+2Ar%O zUWhmITjSk+@B@O8eZ3KCI1yRyvVK_Ks87^8dcK8jdd+2Vzu}qXneM6Rk?z+$!JdVl zT=xyvRz!K+>-xsC%DY={ZtOOW8+P$b{tDlfO_pKWVWkrP74d;9s@%eW=p9;rb(Y%O z(g+O5e*R7UF4|gI1Hj<@Nb6y#V(F%3E0g&+Fw50LFL6#!LRZfVD>u(53=w0cgk`t)p`)K#~1ChY$cXe+8uS8x<<>eY_d+U zF0owD9BM!H5`6e?0BN07mFJ6{#_vW0u|pJskExb;!}wVrt^bDguWWs?zR%mzQy*Gm zqx%6OQQn1YKB14oe6s3o>*?m1;_>qy_kIdS=y9S8A~<@$R8)brMRJP^p1%TQr6^y6N$wLw zzO99rw_Op*iH@^SGJN{)dp7&FRs8(%)wohBExz%y%J!OS5PI(nEu?6pd$b@$7 zdz{?^&-x+SNp+CgO7&M)D^oD@e+E2jZ_81#9lOk&U{E?M75TAfB?gM)MwF51b$gh1 zt@pHk4y;F8MTDqje61(wt@TyF-4x?bW2DjE7-dvM1n(~q>H04@L%xOB%w@6~%2Y{x zskBl?^7ec_4^Y-A;cAE)f;R}JpbpO<7U3W@Lw%%j%#J1VdrSvQ)KQ)X9^KyM%(xI?@Kt6fKwfDl9^_zh72`XWAcjz|^H z#T@amm@V#u+3pEe);^F&ft+|&5i4%JWL4P}(LTI*JHHnq7_0(&*L#R;F^-MGd)`~X zxK$4`gN=C?yc_xgkurXPx9CjZZ2|Zk7o$EVAa#GF90EqK_fej=5MAMcTr0oEykamS zd{hII<~%uFjsWvkcVd}DB!Iy(NZuBg#0&8BeF$dCv#5(dVauq9CDDL2W-)9H-ulyI zBzP|~@uqMB+A1D$pc!I>k7fr^*B9ABL{h(n_za`iGZu>XkB=Z`Ko+8>e2?715jkT5 z`xY(QiB&;vHndq5KBZq+{h&S~Y`l-M3;+XYQHs9i#B}_)SDCSu1ZKddFNu=GYAetz^Wj_#IJSPM|a|k^2pNIBNo4 zu0ePk+J;u%iCQ{|rwB#q9Z0tVd90I1u(F$jqaH-Pcm)w8lF*O+Ac<4)L_!7t3(<&P z5rp_0a8F_!Q6B`Ndt~BS?jwpwD)t59XGlyf2QfS9>9PC+ZyK-0-Kbu2B||)nd^~Fo zQm9Cw`0~DnTE2=JapBx!`qGk7Hrn;mz?aM1S* zHpEDws3w)r_K8Sgo(}<9Dxd`MzE+CCcQR5`MXS}trv~OdSvGorqrC~<8F(g*umKL))j3^|7Qu2qnyf;5zZuA@4$ zpng3lxq%NyDm%^w;i@>4m!iHzV4w0Z(<;8}30ZW#|NnBO9K1e+X*d>ul37qrFKXC} zHc)-(=y|AZsD-__6ZbtKm0IG?I*t(%b!;3ZQm_debD*D+vBl z--i_^_A#-7)Oa}zSJ39bmJ6h00z336z_~np=K)Pch#ZrP|G8+(9N#_YE|doh6y!tL zD8=zYq$$J|xxTCED$2Lam$neUigCOEX(&}Oa-|g1BeSt(;X1l9$G4gL6wxLZJ?bS& zMbUETL*>cEH|1C2D~rRIKh=otdrI@!%6v88#gi+jJ1bg>dL^O4;;U~J-~RXnqGbqk zen?^Sp~&vrhoS7lMW8Q@A5sLN9ciO0!qL)UXjO{(6N2B=;$iqtN5jzvqJaYH3l)IT zI3HHyfY(I)O2n~*|KTMT|6{P3X(Mn&JkFU%W06moFW+Dvdgxws7y3{43H9CA#4eSJ zP)}v`^P$X+UqN^-`X+Q!na#8GlzzYyJte4kfE|(oget-kZ326QC$m1OP7NT*joLN- zw{NDVqjZ&-mj0XABzZv_JwjJ`N@__5 zK9ri$QM;4OBD@ib2ze#=PiQE^`EqnSedj3c|2;-2=#F$R^A6M-sVr1F^P6NVm70#y z-Kl3%nduqLlGC&N-@avDYvya#AJvtP+Eh^wBnPMmlSHBaRv#Yy&;r5um~9vCYh`M) z0JNgn+TlKQk%XaR)NUj#CKA@|?T}ZMK(h2)0wy$Lg_jHt`8p#Zj z99G{il9JR_nRUy=eOfwCYGGJSeB2iKE~ zD)Pw{k|H_2dzAtcB+p8HSn%MR+K<}IlvUKGq{&Rgk`$wJW_wfF=?XeVTF=CS*%xS^ z@*u>}lhYGYYU6*1C(KLe8fpP*bE;8lBR{mDAL^RU;6+8GH@_)8!!gn$K}binPiRqn z*OPuSabx%OUAmsmPzuV)ialy4!UFYe(lDkZBZLI{NBzG#JNJ`m+i}uawQ#J zE)A)lQQx9F(jMv9T;Ex`inK1(kSPgClG6Q1){(q2TZ}Miwmj88*;ZDx6zQ{MpZ-el zS!85E$Kq%dG#HJMDnWahx{YkJ1YhbzpIsE|(|k$zjsfmMp}!(<70I_y-#2MYvivDu zvQR7He?0Q0RP;Z}cb;@9#@`}b$#0L1^T@&z|^lBu|leQ!YN){+- zXhI#RO@UL|C$y5pCCr-gl{6#uV3OX{8%&8x^4FBrl#Zk>{h9Sh z@<5a-sd1@CmG^7vGv*y>ul&5JM@SAUxJP-Ppj=5}2(%p`#ru5~=H|@PV-yPC?GCT`CkM8gismbQV3n0GVWP{RG zrZq~vhU7U}tAuRRdZecy43lk7NF`Ze$|6(NP<@gXCkx(e8FRd3_Afe4SCMQcZEEUH zvzAHUkxV0fNi9QHQ`>{M32Do1Es|X{njvXMXDFX?`;Tx<$TIP5%3Bj{WH*|~C7j0N zLnD|_pwzS|35|qO`X;=Z_9=}XOi4-KglOsy5xBouGP4vU$LVY&{*!#B5|Q2^G@E*a zo|$AjNpzd9w&ee0jwu05eMb_;tO1BzV9V5trj%17twvIh5JH$SKc?KGeUe6mQFH7-Elx*By3<)gwV4aaD^qV%f1@(} zXOq$|Dz|B|l2oH-pv~0X=6UK_=F?GW=^T}dWTBZO)qC8Z@Rf}{m~6Mo4upf)pQGU1SH z0@9p>K9U}$oT4){9x)M5Elqob67vXs(-oAPxo?h(sNKqsniimWmUOtO|LH90=5ncM z+5=Rk@?K)1kxEVXFts}EnZNNuE=oxVq@G3Pq7>A>OpBK!soBd&-%$Oc63_y)&+v=R znsU#ID`~7l(w6p3Tf=-tN=1@@%508-C?(y2Ak&l#1%il*{IxX|quqlAT3k zd=mpCcc~PFV{_iZM7U|`6FN)@Y>v9gN~9xnmX1@eD!goJ)e>Ql>cgxn69u$yj%p}(%AdZ?eY2OCHZ0|7=2Bk6be(yvS@V>;d1tz! z{63U7-QVVGU&3^Gi_50#kmG0SH6p`P@(c@-gp5Ndwtsp*fN&nyvPl2Q^j$O1OY zXpWf)W#uR`pUQl8vzDl|^t|P|!kl5!eSMo|LCUb1@{Fub8t<6B-h=-NVFvm|cLrBM|eC1;)6R{=#dp5!v^$2q_r6xTm z)soqFsLxYdnYItrCrJ&eXBsU~za(u+txOuw9IKdfE#W?mMzbnp3zCgU)*$K3a*b)u z-H^ly!ZEs*zR9*DO&jaGo^ru!La-JSaEvr4S&w8L(rle+gPQYpH1ek7WSN?0s8r?% ziN@=+M_48aRxaB}h7p=bzZ_lIm2M;S5vo`V{wutq+w0VtK9x0%P9{Q9*vyS@&5%ZEpw!8j!uDSd1h*Gvj>~foAeIVp*aFJXMJfD>yP8qg9%^M*G)M<+KqaysqIYtW9p{= z+R?0kQ=gH}q&&*CB(*Eq{DcV;cY!{HkR=x4+ar{i8==`musOD+mM6(V_FVZ~T={vj z?EXV7q12p9H0?gJ-w1K0RHG-NCnOBfbDQ-*eaNf{l2zqa5J?zQ3y}sR=}bM5YQXGy zR5O&i+`=K0QLU4FEbp}@=18)Uj50N!X?L0S933SIK&@+@CA~#$NUd(RIN5LIBU5T= zviVF)-n^=O?#P@kD#AHBZ`yn&RtU=^hbR}4Yi3FZ?nKhD9AA{yl&Iw>`wx+{NAjt> zRR7J~nev#v&G{U2UdPn!RFma4o!R%O|4}`fHT7Rjn>y9hx@N7H%R=f2raq?s<#V6r ze5*OTLzWG-2qDt6SqO8aX{e0}v-F#!h-vB5%n5x68H7g@Q6@$LAr)zk*c^2bjz|tt zI&)r#QioxqVypNl<@(yhiD?;_h$xpzWIvd5Xr$3eA5v)vS+qwz zj*b%U%yAjjCe;=7WwZ9m_x`Ipx|`YiP3xTYUB0X63UkJX{*xtdwwVjZO-V>L4b?oM zkoL;wtw|5j7{TllHv9W${@ z`ky43DTT{1OjbUPe@Ppgv!JA*Y3_<?-&ZG9EIX;@NFy|^vX=vK|Bs+tBX-$1a zQi)J&UQgeIGqTl84Q|Rx(!=H2x?Il`!+HaEgEaX&r%fBr9QjipGj#>wmHHs{$8t-BepBBtM=F$) zso_oeOJf()f-!Z9ndASjGpJvi77uBCs#h8_QlGK<{?naktVa@sl1doYUOh4-;}pBqNKbC`((+Pv*4uX z=@;1$RGRWqQ5v&+v`1JmkztmUj+OTzQybGWm>ab^wLQrsJUXNywZA$0K-$)noTPK< z97(-$IZ2YtoW&@YcBbqi4M(j;l941K>3K?Nwi&guIW{CYVOoTQ0J@4K7$Jf_q*Kdf zi8-TJj;$h`A!$bKNgG{F8%ZldB^@CdY1)5ge&y*&E}1x^c_Etpppp`POnlJH5tV{a zNf_GE+*d1876uj%7yBI&d^ht`;@z>PYIukJW^i#m4LTfn}fRv{=at!;lvdEGCjBl>_Z{Mink#EdQ9zJfQptVC6 zQWLinl_}T9dh`sXz37{v2ID4nWU9fR$t*d|(LaOt z%_-zkhJ1=J+nM3RZ5i&O!1t#RJPgsOp;PchDwVIJcDuuGN#baKc*oU)|ID}GHL1_8 zfR!f=%oM4z7TP@<&+S4^(P&Ep9&lCQAC)Oz!?7v8^HyL%;(2}rTU9vnh{Y2-Wk+D( z9BP`utH;JF;(s2>@E-ha@{q%0crdlW@fS#)izg%RD;gu_%PA<|MznuAJY98}glDw? zA1CA?b_D!aX?Sk(JPHH9$iHCqd4zuD2KP}Op4$aI&!hGq%MK{xZTNEaLEA0^7V@P-9ALfVE|j5~{1fp^&#|7OGWtTk+zhs& z)nHj#D2KuWcrEJ;HZ3c>m)fCjbKtkvnOo$?JR1x@<58#e*i$_925_Am2LI9oprTMd zVTIxjn2r3{ROB)ob@2*(5KqI;@Dl1{DLV`Po|Sl>2FxuQ!Y}U#o~9c6T>i{D!lSD_ zT4DgsRuUGl^4Q=*_5z-ZU$MGCc6nFHpX&2lwM6OUL! zS&OHO+PF(2R)Uqfy@qBQ1w?S{SvY|>lUSHVx2yhl@U|?Dy-euD0&m7EOx_NuFdl{PuufT0+>rRMJ zqw`H7TKiM1fFEQ4JH>{xzvUSClQm{lH*#}03oFi_F`skgzP)~!^?Q)qqLwq9Fft&e+ z9D`oG7mP{cSSmj&?#UZyfvchp3y0V24}6^5AyOglT7U;BoV|fJ2l~kRjPsp{B=f|W zf&Nzq>^kGcuaLP;Ufl@e2jmhk51o?RSqOX>ze8X62ADV|6`sp#i>;7Vh=76|o1pe< zqU~GjpK(89IqGaEPk~0T^4Fv)*0L1i5oFC?*3Y|?*GCukFIL>cMcs&;cn zY9abqr8;`aPVgrcC_frC5Vz^PN8v76ogaq$Sc_P@o_>)^cF%2N>gw7*=&TZ5~r7e)6Gcep=0&yOeu3j_0xp~M=i_(eHOO&1pH7WNS^ zjdbD%IUT%Yb;NA-U*n{*jz5uol>^>B{Eip;1!!BQ{KA?jta;x+XZ zu})bn;>0yMSoc>7!L$}8%fM*U8NKLJ9tc)Ee`6L;h9~O3Y`SbMzJ(8bh&Zbp2Q$}R zc2G|fSD6%*6-az_uiUI}Q7(9gaX-kQ-o_(v4ZSLg#8r7ue~TZ04N~mwsQ%%J;b*}F zW(5zT$^(&&{&!Ud%AdgK8Yd?y z=Z#&w8~n5<@mRjadsAACHDC|hh`61T8J zz~htweyCj{p5@uM8#(r&U@SV!URS=CiPrDXir>nfV4iv(TwaY-kABnmp3lIWe!=`l zy`3e-u*wr+vA&1(QNI^Yloj$_{s>W<1C)U%=XURhN?T>Qm=E6NJaJ!1ayC)lVRMWh zE$hTTa-+D;4(PYUQT2<`YZ}Kp!5{fw^W>xEx5%wRQ8L(h&mN^RTWRE38|x#LFws_S zLF~)%mNt0vqOFl^w9wbcfzA@e<=&;rTrB_eR?;_erp5hI$yE=(+*+hnMc{%E#Ul;x*pTy$Nr%9cL}YD5a@! z(%7Q@B*v++?3VtXvKf9RDav}o#@c(@`T2W~C=Kzx(s08f$LqU2Rz){1vR}Ze+rm3l z3w6F}Yv;CWf`3itPgtb)jGr5aqr{B)&}g) z(4I!HWwCL=sHGhe^Pzb*c(-vy?qK89xoWIDtoN2ZJzcaXtc$9{@D2uB-8yznSt;$R z(A&sWY@B7AXQAy@;WJh9xYU_OPivxkm^N7-rY&K0cmqYCt`1474N?95=erj3o}R`0 zUGH>DPf>{l>*JMI#UE^qdd)i>zW#f7W5a4|ZuQq))?1!nxk|KT6~qN&v9ixQ(>fby zU1`zv@mj22#TF^g+Q+d;+AYM*IV5`;JB$fN1MUQ0kkhlny8-WqRghcs3T}^B1r9>1 zIu2Iv`}$X+J)eylo9W-hd(rxqG1cPqwA2#yl}a5s#y*U-RK0p{{YQB~gsc6GO3Gg2 zj+hI+#yN2qDw(5?RXL)?u2V~7mT>TS##Ld}Uh#gTDDrc@SIj~m zu__0JSF0fV^YwBv?_zwV3>D`@BR zUQu0vw*&4diADkY3vYgmkaIPMzCdfr-dAsn=}IBqNg2Z?D1F&)Ji-9OB3~$D_y^Ee zu~Ju8icS%B*CBs(R0n094IWC&QCSHd3Du?pRU~t=_ z95Y^k3-EPzz}TdQiLaDfqMnFA9Hn>UQt0~?>U`r88!JN9crk(<5{2@_m*Lk#}tE2e^eh@a@Bl)&6nxz@VMg&;U`pUNKBk?05q2G7e^hsWac%;V~ zN8QW33q7NZ2a14&a7KQto>70c-PdZW1Jr?vP|jGswq$6-)wk6EOC{Tfegm!jtzX)^ zYd@7{=M1pjq};5KQe{xex#*EW zx#|hl7ttTqLnrmqlU?WZ{lH;5tk_ad*RmS9O6II*pFbOXugb&s9>hF2^+0)XFMmhb zW;WioKd5Eo)1;QwCe~?M`*_t~Q*si|$Hzy;1ZAjMvbFbZyg&P;_M>$|z-9mU{WsY| zt<9Czp8cgi}cW%UAEBJTkwdMDnxn-VZ)_~yVQPty{)$GI$;nYaha9jSWS!h$ z4!@E<&U|mIeOAQ2=$jE*$e_^NfE)HjY<9`_8GD{?est}@n-5pr-FdU-4a>DdH%jl- z%j}|`3v`Bk727!ZWR08kPB+?AFR$9Egg?U`_%*ORlW%#79Azbn(_cLDOAV_Xzd80= zbp5cV!R@SDjZ4MJS@lxGo@{tH^nStJdRPCx#4a_z{?6@$%zW1j+YeE*Dz>Y6z23=J z<~G<|XJ?f~2@}Ik`JLdsy(wjrN+*`?6LGfpLP8_YBwb7Fm#`u7bnqx6v-m{DwX~*B zH$S=cV933b_xauScYnQp`BtS{BQt*Ut_&@XUr~8TwX7O*>x`_ox=zQcpU2+~a)`%e z@gBFQoqN9V7Ps4$25pVbNPIKpo8*7PJF7X)o_T}PZ>5cVKJoF*`#tXse9-yHo6lxE z4t_BH{;}sC*CyNW#QYjP8idsOr0zdeJ(a#rIvAT4_>6ZgJ5;=)WR!C$duV+>=$(*` zQT^gRiz|vd5ZNIhNB^jJL*~5~b5lQh?tXIharehZo@~53=2pPf*|+wlHE`Dsm|S5? zy~eNXZ#bcL&8i2th7?sUY~%buF2&SOy{Ok>+QvPLei%I{{O!PY>Ls^Y zG%+VU>&8nxZCz^1)MF2u-RpVl+U0APR^L0GUCRgzZ&dkQ?OJu~)c(3EPhJ}Td-TDe z4a!d*M@g@e!)5EmLH{2kcf{w%D-{yB8r^#9>itvO{`v4>Z)g3Gbv36n`n%Dr+Oa8@5vPww zvWnq#Y`ICRTtmzuzFiN@=b;&Qu+?LTJeO>0H z7lF@CKYV#>^glB$cDfsp`!zok`&r$bM)R7e^*dBK68l%kQu}vmh&bfDQ~ZPT6{Di{ zMo?hV8%T%lvO4aLYQDdJ%wy;{=fgd>LUNUuu|Z>%$)2V~tqOYQHz}%GGR)yF z=v0u9W6#Wd(&X{Wn~!hQzS;Y6cIGL=7QVm6(MCO+B{k?4R(A}TrWqJgt*|HIGK3>0 zK$N&yR#Mu!v|_2Jm-$cLxzp;}gzJ-TML!=~IE1YTo>KXF?Q?Yo)s3y5 zn@~ACG9XY}&d*@fco9sosoYSOSephe2#OD>7E&wpfd3ZTN@H8u%A&SKnfZH)elB^G ze=+;J^dnEF-)na>^4jFc)W?CvmY3{;X4AgrUx6XJ zwegO2)ZR9zT4+Sn#ISaOL#zYYsvY+1+uE2XDSuVlRee^K#z}cGx5CZ^-U#>z z%tu|6rE8}MdGL)yv zgIDgpdbjH1O4)0jy|v|G3*(JSb1Gj>>6VGqz?-Swz2}+R8%r z=%VL&sLHyT=z;oFP&I&vaoah zlx*YWyHB?~E_*oS-X{-ty;zjD%G1-YH0o%DUN!31_@nY0mHQ;tju{x45LzKXw^RcY z$RNjj$0X-)&psu|Z&%ofh|-9t!1dNU@}xV!`Gvz)99_DkWM5u*MvoUQo^*V0{XwtC z`=9Q}*j+f&bw{ZdyesPS#Hy8!ReoNzY320f)zP!!J`erFUJLQ$8oLg;-*k7y=&i54 zM!*k&GlM$?w6N4sE_v*(jG|SAN%=*2SM%29BxE(t9Q<;5+QiiRPf{P>Ox=@r&^c7= z5OFYRcIBg0jmmqH$5mJwyEOXKsA>^=0@qvCh>EUbrC&R0y1rFr`M(xcCvr^0j-ajf zx$<3if}?)P*F|p^O)u)6TP5qumo=YPd%EV?{^vDQr@gF`JHF&~cLi-@kS)4T((5Uo zRPt78kYY=0AF~yFw*&m@Xvbu}H_FC1z zJR@gc_TQNUG8&{;c=^Y(q0gE>ef#m}FZSg@{&lT?`^-WwIw=~us zS2?zAMDsvtIV^6u{&N2A+KCZbeZSBUcgUXLjed#h9R0ZSwc@`Eyx>vnRTNpsa*k)z zN{f0r;BoUO9iDp9w-(kb>%o|Rjj;1kjpC;#pGn?Z(O&V-q{RtqWBP{9w69Ta8E4#I zx%=x4*+T6Xd#!+b!Jh}M_8)5xRhJr(uKK0_76lY;%n!<4k(HBiCu7cw`)PAuTu5#6 zvUb+d{7C0=InX{YqqjsA-jl} zO}B&I3A*5a$2yUZ(CfL9!5dOk_&)OfC3{iU(add`L0KcRf-|OP&CcwbTeCREvs||J zTM*hT>P&o2@{?p&#jJ{(k{-rP3V$nLgL)bqr-k|w(TsOe9;gA+IN^wYiIOujN-MSfBfDc1-55w2aganeXJfODl;h+F8E`A)O))M?Z=S zsW2d=W~Jgvzs5&Ky8N4HC8Dyolc+6Qs6(`E>J6otQb9eU%+;D%7F)Mz$+EAjQDI{4 z{H)bkgL6L2E6V>NZ*2C-mv^4ecz*K5wcL@e{Yq%y;;d{lBT4D; zH^Ybe9b~_FFS&mO2f1nuvR||IwZ!vK_9xq}{A8JKOR^>_ySXWeKpn?9}??c-Li_jWr50zR< ziMmuRRR>$AYFpF|yr%x6JKynsQBHxafES)Gj4!@ZoPy7H1#|Lkkcw;b8ErtCG*A>PVH!u06^h#+nM=BztzMsFSU|`|vC56swZg8Mt26B&W zzu)1&eIY^NrP1A^e~wOyydU;%h+p6>zj*uCw%gXxmNa#nC0MJXI zKR=*;(Ba^zVacIaL*59B3$umf1a=N+?^oYG)jC-DSlPsTu{*+p$R0~{hbPi|+I!P8 z&b13m&djmE+1TkW>*(%ZmV@Z>XUnEI)Utz)U{`EeclRC7r=SSCT6}~^kxM=K zo(i4~9;cTXnPM!WZVWYw^A<7dSKbC`BjL|lyb!D7`GYac;M z0Iq)`6U7I6jAl8Ok zSfUK%Gq7Iu8&+#4h&`+gW=Q7gPh_^fj{P9EAm>?po%~&SLxAC$J;40UKRioR0hjzH zaLe~%3EC^*C5x52jCr{K3)w;(;f)ZH?XGG!4vM29ft|s6S3TC#m@A{bC)gp!FWluR z;$zuv^#i>U`nNDvV-EQn_8ry`GQ9>Ob-DRfA&Stg_W+|%0%ZJWpbIKjg}pi05w^>&T1g$+bjGx{y=oMR`BNXW{9WU zQkfw(@`p+vww zb6T4wUe&IMVXPZ)xL8lpz&xS+t0MBVB~Z7kdHQUnqGz<$uk@O-!(ELXS3eYku{u~6 z?0#vMWn!OYjkmJe-Sd@lP>wVnDqqOy{9D^8#D%eoyt4O{-uiq?yyvRu2`--A{&Uo9 zmZbdYDV3*s9vrL}@AxD^rY_Mn5e>zi6|uKgDp&3T7!kFsZyEvJAu^LX;twcGvq0R%}?rW&^-8GaD?>yel`#`zq9>OhZ7p!sy@lA3rycBjbn3;T`Ey?@L zdRKfa9wX9bXK~GYR&67`K%}B=h(q=jAE^9ogtH8+*m}ect)F<4_tB<$;6>#*%Gxr6 z?^B2C&)GbY%J%ceER7%37sL3yMXiRS8Y@eq-C+Jo;RUips27^e^m(t*{6t61Y4A{yAU*f(;mdRT7> zpNWrIj-DuP$xQvQcO+)lzeYs5ci9a-RV~GOdo{jDeydysBfwd#I7F~qV~s&+2_<@x)vcez8l2mE2NAaZ;1%R@K0x_J|5+oCiFv^9 zSL~?ggdD3x8JCr3##DZX*T(pFf#){7Fdm37-i&cef?RDGBW{T>In^V07d;tkZGTC_ zvf9wKPo)Fv#>Y$3#CA5tcva~oqtpSSSlP}#@^;jc_yc90LbKNuu~wRBR8dNdo%(0W zVz--5R&4ScOJxy=mCwgYDPN}_Wyd_@M85L5oN1wV|JKUq?i60YT*@l;o}A52dTZGZ zNQbjJuj6ea23nW!3A$B$p=Nkb@haM#vd-Er_9diIMS0n~470v@qP1}zAcau83qQsfzkuLS>xVBS&LE?Rr+>c{S`$|7+2*o~X4BmdOf$=(CZvVD|W`b60S zPk#X`6jzwrNXH6WD0~U-YGaLE@D^Ea)aG;93V0=*5mm58{0>%R_KGnqUsz>r;e~|T zgB8&+>>hBn-LhT3%sb0J^*YLJtW31WipE6tDLipbO5KyISXl!;LRlg{bo(hc;6e00 zn7C&fL2NbqNOm-aC>!04mC?p?_(%ouXUbb*oU8>>lwHbc_zT_h#_?`)89S|hqU_Q; zDz(7<@xIa#{tzA6Gw*tyY~(88w%J%G{6OS-w}OGZNStSy{8+1H%uqg%-{=FB13+{i z`JDNCij+%yzjzyKu)jg#-PD?hi|n-bFQrr;r#<8QokhxS_-!<{hKrl>9eyKS`A`M^l13f0Zx+0bX21(Y z$UUe#sfJ@sX*u%)m+2#M$NP`+u6U0gu8PlW(#;YJFp@n8bfq ztIAd=!^h4&+6Oj2-C;i`_IL-YX^5-Vgr`EXJeL=NoM0KjuUjSp=Ug=qV|x- z;J@{Xv65Z3kA*HcG@>APgGG7;Mp-#zNyElhI|h^ zsZ-!5@U3wcK00mLCipJxkxjsBu!%KQ-a_=X1i9b%0*nzm*(p60E9xJq>FP^wurgW9 z(EF%MJVWGs-c&ZSNZ_j>yj=FnnaTy0ZZuGu!7GJXhk2iC^PmF?Sy!xxkAzH~YP~8; z#6x4JHo>T&g~{!n9ef(Rv2vJ0tbo5&s&+`XVSWFUuqy#Z2K?sISQ~4Su~_`1&z9F2 zSeoQp-cbIPu}8hnw`1+TC3IaF-|1bioEN(hn}4EA;)>DO9>I2a5yi^eTU^qP@M5eY zUKe-7Ya&3@wBFKVEZbN}*_S*D^RB-uXJrp~U{tX!(6@t&MCkpLDeM5eTZ+V=T8eSS z(^>9Q%lOZBgS9DLfwj6d@?WhZyXP9kg|c5Xm7l`q`&`|{p2&lamGZLILbg_fJmiYu zy8nLhgP6gtJ6p5+wifykV+Z#d6Zj0jj|RQGClkki_@5Slqam7IEV;tHRJ<$yv2*>@Lmf=B-{CNxZ)4* zv3EtLzRl81*3>s>Qyu%^Xgb^2tM=ft*=SJ%+*6yyN>)Wp)4PdLz+Jx5SIgiNpmp*U z54fkL^w(?io?1VdrKiBw=*a3>QV?48JM^dTy<2gYM|!T3jJ~;DiHFb648F$F7#g81 zo8(=>D;tj$3+5cgsPC~@kJS2jU*fg-p`xBlmVuVl@`-4xHg~_zKQ~$_o8XBwfDiN@ zP&bRsT6b}Qw?kCH0?R>X9raILh*<$YKuWg~C7zMWdHssg9L(~2mAmpOe2x;;B;Bia zW%b=N)oEfWY|nqi9DY(vuz$vX_uf-K@qW*0C}(6vV=7w`dp#2J0EdRqRb3p%7i0qbEk{o68;=vsojIH@dJ7B`UBp6M^GL3&+p9=HY@!4_<*>`)}TuZMP15nPRL% zM9;HEJ@q75<%5ho-D=ds3jaVcOznio(o4kv79;l>_m%$MK5U~}AX@V7sP$O(vG$o9 zE&pau*k49xWhi^!c#U_0f8#SY5|KW;GfkPqvW-|J7f~-4!RxFip8d8m-WZ|$E^6_S z@M=rqyS;y~lb*VKyYU4x;MsN<>33g$Pm)h=QJzW{H@!P3PN#6GYF)l=Vtzs@FkmTxw? zvESgw*NNrBiUr>`|DLC@p6X%f?>l&kzPysQg-LZEcZ$Cd3$h3n(buvrd@%>JB%?d; zB(}1@;O%6HMFMX&!*A`HcmYO%HX>HOYMh7F)7Lwj)zR&UhZz72-{hkZRdE1Narbm?uK&k6|l#by-nch4w#)G0-UZo(S`#xAfCa73uvD4R^c@@9T5$SFV8hfU~?n zX$qDyk7bVad+T9sp*jQ{$|ICR{B3zbeBvnv?_uYXoWk*idvoXKWMr(*n3vWn-N>k( zSu8;vNXaGH9Y#;=!=ovBZoy)3cnuOHB1jW z1%~Oq+L!8Po&cY4#hBx5;5p!W8}W>Epu(Is`s)67THA0K|&JYzrB`<~~` zD;(_T=cy&bEFbwj511abHu!3I`>2%I_=HA@Y4JI6KSw`~ni`c7**CI7=+{9J;H9*x zZ-71OvOFas_2zEF)zsa`W6>|@JMo@GeRzm}r#7&*#2jazfR_O&f$s!<5x6I?cHnaV zht@Q88LuKI>DS8Ilzv(CFy~z6-Irl6dZ)#tUU{7Pu;s(g?^nIQ@5y&Bk7g~%TV7DD z_)b|{w%#KBpM*4tycJbFwr~8N#OEmml`m9EPB~xUuY?70Ct|*cN)7K2GRPnCt5q*_ zL$1Evv&dDY>|aLwPC^i%TNi5ug_#e_wD8detg*fvVt z#FmSC;4bLx8s&Ui>McogbSNuuedj51che1dMww>O?3#Z>;EzE~LW)A>haL>w9x^<* zLqLMxFq_cQ_;9h*^H$mXl4Aw!bBASpl-?)p*C(Nm?T_7$W<8p7|M~4sw^!c1^}w6j zJi9dihoY&amt05X2Yz=$??!$W`%gSiJXqmMrK43BRc~LtN2Py~Y9{qah>ZUwrgg;n zpe6QJS{%IW`-pkoRxake;8^VV%4u=MxJ%umyxlO8yA8&K)0P*u0>51W^8(KWeG&X9 zI4w9iI6rWM->=s8>V3xLST@U zo@l9`WW19!r!s& zq8^5R6*$-4&hn9x0t>ruzS6TcbrP20S5adYZX@aQs}{wICpOQFe!{-!jTx{`TUv7i-h9 z(|$}H^t{Eh$4_m~6JEZXol&r*q`LEK*G7Gr(#o%AaFeiu;aQO(vF8(?B(JTwBYA&P zr^K9uwD|S$OJbKrDxnJlM%!mun`s;QK(WpHjXMYY1joyqWuskhxNf@Yc>eYNZv4gy z6shjlE?Z96-uJH^xHfQ1U_C_CzYwrK;JJTIzvt>2*tTPhj_6Z1$H?M2h0pSzIx{lkZ2IGj$c*+cdZisr%}q;wc`JQM-e0BH%c_)>IA78GYsdXB1&t5>A+%RS z*Qjsef|9 zY`W_g_e9Uv-f9LsUNIx`rCQf=&)OOMm|yw76R<;`WV3D6{ zJqHfl6Z$&+vfj~K-nuk{<_|F{1J z|CfHpZJ%2+ENiv1%0T!o-vlei+pZ?guB8WyTNE`f99OU=zkdF%yvlh^@>=9Q&K;V2 zE~j$Nh3vLDopZk_2z4xUedN~Nzj^ayie;g_uHRk1zXLo$KZf26|08l-RNu&ei13K@ zktd^~B4ub~@Q}b!0XzLV*8?{YKa$Y_lz0wE*=U{(&UfX;X$hZ1$&YGu6>WSvl_;}H#+N^^kie5Zu5?F zSB4fZ())=UA{1Vx(egLGQhUStll7?OeT%;>!tb`-0!+tI;(*(Fu3H4Vry|=@n2vr8UZHMvBhdhgJP}tSn=D%(~JHnTvm9fq{Nlu zJ>i|~dE47tuEr~dHSMjytGe88cwn25*FtB6j0;|c02hZsehZxzni+g9;IaLTE!*~w z?M-W6r3*CT9iyN4#Yh8_>Z%?BFtiFBlFu%ao zDyaxrQer)A>tQ=#9cEkOw=-Z+zzKV2>sOWn%O$Wf1S!>I7k#?-i1(J~kn6refcftG#=>BO7-O z1cE!kLU0T2!2-cu0)gOe!Gb#k2=1;4uG`&P%H_^QoxYp*eNXNg_xr}kI6v4uc30Js zIp>;dR&~{?wVo&cK>k+GOyQhdUy2kaN`MDM*-cTVWM%=YnoBwc2SxT7))If~iM%%|JZcJ`5ZEnM41TMn43n2(s+nRc^{m}qJ%eq7(H?NP&l zd*;3{lJCkL_RaIH^mX&OJqhmD9*o-tL-a*_qIW!(#qZ&^dX$1*1t;=*=5@*ub0XmG zpaCB$3>BV8uk@!>n5nwClj)Fogd;0(LU6MnC17n}+29Vr>48sNa~vtwVZayJlwM5K z)jKGUq@mIv@tJT>a0qSq0sMYpp4>&N4sR6RV3o*H%wO^~(%58fJ46#IyK_!Yj*zGM6aK@}zn75S=sKmGvMo{tgs zaoasu{{{mYu|TpZdta$m%6p%;T& zhqjKeMEHU)I1gAKm^pR})e2MO4g$+b-ub{R^QGsUXSS~{zZBYjL$%@*6-TdSHFGyx zy#2W2tYe4cmSdTtkG-<>XVY+|CY=Z4q(3TcrDftg;9RluuepJI0>4lwDP9(@Nk7Y8 z`LeP>4cGbun|D)Xh5S;=5KS;M`;2>Mo|Y|U#H6iEZkDtmd1w0D+%2A1pP9Qa4JW^_ zCA++?4Xz166(ZUcIZ)IWRX1!)urIiN=>FhjdpaEhs}Fgtg<=+-dA=;zlYiaa#W#YV zAaoEr$V;`6L{Fxz`M%|YHN>9cc1Vc>F4E(OQu<+epco0io6Yzh z@F~u1@pa?#r3!i_JQUu~orE<=6POIY}-0SYS-qOM;y@+K|(DLxx;iV%ykq?TDDSo}!{OFgV4+5J7!j*((CZlb1!!@zTc$fFpfHh*iXhY7p$|LXMu%siF2cKr0bsRrn7b6Q&O6fQ+hot@l2{?P|9;dsWM?xMmZ`8BMNvfEw< zw~2fKcSh?YVxlI-)Q_GLHrsjKREsG=-ys+3yina2=gsj=;kO92h2}zt+)!^p^)va* zwSe7xroD;lU4S*k4Usdct>y2Z;bq#EH)eW&_^aznZ&l;(fuZ6Yr9hqI)&{6L+K z{X#q?pHnX87vK!8XTD}S$tF?FiH^X6R!u1`rwA@iaYq((&3~1menC$qI*UE7M>n-E-)m-7g955s_h~r!@HG9 zST}ux{DV+|Z^xGt56efiXjtdagRDb`n(A3^I(~AwTtggBY%pcr^P*d5ftPE(VV7NT2hAdi>o@vq@`VhR6PK7^^{PCA;sYC3Hj z5U@A6L~vlhcaAUYuWVgxMJ%2X_HtkF(u7YP(tpEox^j;X_ha7D@GTK=@qpgyl(KH0nc209jh#x zC|nH^CksWy^U^)3tvE*rku?1c)4+PyR?61H*4x=2C@Ul`WOU#SdxANS{g$o6j37hw zOVVzkFFeUO)H1-&_C~G_>&E(#N9cac8@d5~fiA-iGfg+WgRA~VycMjfF9-8L-YT8p zy8lEt1W&g4;x*wZA0`YJH7QI8L*Hri*&X!(Dr<5(& zXm*ipn`5@4grkwOYM?nd7(i^^S-&xrWX=Io(_s9$)?K+L-Ip3F>1vGr8?0WnVqfAn zfiwLWy^l_Y8BlM@(NtABg#L@9u{#vcjt7Q4XK)nRdTwXd?5cILW9yAImF zvV6rhqia%4$rpHzuEJgBkIE8QtM?8N12zM5%v!7>v4b2!HKyj1L1bH)`SzTeO@6?2 zy^>x_I|8eM8|aBjSGbqfr2zGm3M8oFOQD(ghcp3LRZB^l@E3oW^ZEjO-+KPc|2}te z_QTAr>336_BsEW(neO$p)_x}E(C2Bld0jwGcp_YzDu!BILDpYPNo*!FAJ~&)c#AKF z=YT;PM(bf^^GxC(yBt7)MmeJG73~)sC0(zaE$xTQ&)_O1lOAFMK2blXzEg6PcPgvj z1B8KWKnKW%akG8oAgUpZ+GG(9@-^9)8cv>r^_?!g2Ji;mR>!FQl?n1_WvaSUJ)uO) z#ifVR@A4VhB@;wB2V(^@*7`Z8n9n z(c~!ABW89y>t<;vNm$drCRwl|?-4QpT7ef>m!Md3^qy`65vEN50SeN6Mr@#eM` z+S1fK!hFv>+^n0vGZkag>2?%Lp21D{S$&{ZL~XCEmWRsCVD3#_xwkxBel0IhzEsnJ zTTanN0P4X~%!v<$k@5rBL_JLZT|1x^(>B39zqThh~7pIqU+MXQmv@hz-cxF z=DeMNv%ZWz7S5ZCYHjtU@~bjUnW!nA;@iM|MjyTgMBruaE*HWt zK16LL6NzZzM_^@ZgiY7yXn(}UDgZ9vV7aG!O1>!vE47t{5Q{sMwMv{4pr!)b-72N75~!?#KK&ro zl{Sj0@C-6o=qt1rz7YQ8$MWO(9R8dzON@|4NPkJMo*a~^czL#bTPBs}${gh_0Et#n zN2{yVnQ%ourQTHEsdv@+>bJnWi1?9j%FX4gl1thwel1oOUE+J8l|Tp{zO1lYs4Pww zUx;<3Cz4N&R4ZwV^f}l7d<1a-#^du~(eRL6eJC#OGB6(mNjKiDb?O=6LL;bEcSIYy&yG=?t zB~CsHqdecp1=4wG2lUEL>8A8ldINovEPa6fktI!H;aAZsB}$8>j#9MrTwD%3pRa{E zz^oc2gbPK4Kp~G$njHqcS@Pt;Sg zA~}(`jAsIG*%FvRF}tGVOvQO#oI7WIY;awlH$)o6QL~pQVtcq670|$(ZUqLCax3%r3+FA*#dJ6 zI%-p4#5xI2B1p0sem2I{>8LmM#24f4eAfF2)TfW$9cFj+@@OrwWF3cSKXnga(`I@FJFLP ziva1YI9D7iZV*?Ay~O&WC_EQ5ffR$qY+*l~0riCHa7O3ym-x{zTKk&2$t~q}bBDM! z+!pR6_m=yXcLVe78)33&l`L{2rM$sgvo z@YDGP{4%%(E#W8f(fn<0D_oNT`R@E6z7szbN_{CL0VjHADM#8Qf3K`p!}KoL68tgT z5v5Xfn9l4O(`)lKOH1n;YbV=lTU+}v`)jx-xoKZ$A8c=K=WIu8Ep7L$y{$=>u5iT~ zZhmO$VcNyMg6nd3dKIjQ>p*TI(r^*5pXLKgV?+2g_)E!@E6GcwGSYjoGn|{hLWGwT z6#f*yjvvF<<#FD}VZ4J6=T+_{_n5oIUE%J5$47G&;g`?~4DYqM3S2bz1viv?#C7Kr z`IdqtY!C-XGv$@a8FZt9=x9_%>v7fM2v#qq!*4dUM7?VC|3OBW9=QDrM zPbiA&Np2yY<1`?oRMiJ)`_*ivzA{JN5AUz~i|>T-a2{UcC-E%*nA^xr=IV1XT$b;h zFVT11_ls}7Z=$b(ubHo&&+K~&D>wIh|MIr>P4Kn!P4hkSwdP9k$^3MosaO=&PkZDZ z>N>4H;7{Eq%2Gem1xzru9Zj68oe!LUI>$L{%DlQU& zg&+AK{sK3ii-k87bHUO`-=E$;yuW(KcsqD2dO1%Vd>(r?c&2$)c&>X&c>8-ddM$7# zRh7%-PV#Go^Dtt52Ckq{@UFWCz6-DqM$oUB-lm&o*-{kzecC?KQP%mpv$pFe*GktO z*Em-tSAp|{bEs2veCIf6S8c;=udPk3J1y_cPfa{nUWMsP&!c`Jw-Sf&eejfYO{26* zVD}7pwUjKDfx9|Vm;`a%ntSN$?elsscqe#6y)QlMUZB$Ljz!AzWb?rpZhA@ z7u5snlRTBZlVI%if^RlAif=De6st(Xs1Y)YIZP4qUVl4+Uwg2ii1vz@ii zaMXoSr1GvMu1&68uA#1Ct}L*+r}LSkv17m84R3a@SQ}fnfz?G}olGZo9dnX?4IIb> zX(DWR7~qa|)~2dID|_U7k|4Ga*9sSbC%X@K)z{qj$lJrKdbWC6!kya(_ZjyN_X_uH z_d-}N)!jY7-OIhjo$GGvx#KZ`%^|**zN_4Eey1=F-cfxguTb*U6}p5CCk~PVHI~U> z+nZNec3WrKzJWIezdNQmX|T1r%jwE=zH=UQE_K#)9&xaaf%bd0dbVTMXlr-NFXrD( z4_T5O!mOcxr!JCbh?{r<%u{WlkJEluw$x|KP4?J|^Y*ZJsU2 zHpS|)4z+AFGv?Z+iEv-@2G*L@qMDQafKPfCb{1@Xr%Fm?WuUxAx+)5A)tkhB;L343 zeKmYnyn|u<_=D$|=e*~xC)-m5&a-{qFy9g%%dO{v_=9{=;l40KYzM2aj>s*6S^1rM zMY|3o-iu&=KjNyue(_vefd(qFd}>RS|25wZL-GGR;!V@~gQL zjP-wEI?aZ&^_Us-b;?BzA@>lEaU8D$SBaxqj5ZSZqC=Dga;`K|N)^8oGlfM$Z@7C( z;*arj;ZAlIzZ`xS|Ku^D0o>*87s`n@#5U4(DHh&o^;4X1H`zeD4R=&;bU?$xr{Jl0 zf8r_8mb?phx+AHpR4Cn>K0v>v1v(T)&>Az%;0`dB`9L3~r@(UtMW3O1P%+dEav13# ze*>Q7BluUa>ZdGr1)dzM>jFAU#Mi(pJE29DRMwOq*=jC(o{r2N0OExPN zmG6|L$|c38lmtfS8)~Tb1B~fe0bA}E%qIH^kRI0qdvX*!rTzqS&!6EYq6*AtnM-UY zt`qMGA1oHjCh~}Pzyki3*b6i0hY^*C03sFEeNBY9S3&q|Y(HQZ)&T^^MBu62rH=z- zvkX8eTciD`wF5sYFwYDS($sr^P;?pIece#gfSEi>tE5!}OqpRYujz!Aq}k#5s5{J8 z+zNP@JfJ|f1Sa&g*beMEj5FeRDZD=37Vm`j1S@-i-kJCud^W5X841sbL-2OM=3Eso z0=8gyD$Mpg59`Y|0=M!onE%!gs|d)9Dj-(A1dPAafPFU{xR6`IU3GPsE6l(<0H2np zr2q!nEA1_CI==^Q=L}7Nx1N9j3A4x$g5zX;3C#9HoWailJyQTg#|T*a)CktMbpt%9 z2{12cIkp?-g`UH1!|Ja4z+wFo6EFg&@C@u7eBz-ExmY6h6w2&`+3r(dMdx7PcPuLiRb$Pu4SWyQ4o9eCgHh?_Pq2=*m`_e{_iRiWjrux|maZW_!OJdT}&8DW>SWxz-L0B}~T!+h|1fIis;>kasd)3Nqo$7tYdUJ7j2bK!U@q%;HU z%@$Z2=%pUe+e2YH5Jr_JVLic)0od2@-wBZADni6Zz+Gz?cq$!c7kvQv-Lk?W(6kerC^pk>T4WYKNR}DEsR!~0bw%)Z21=CR)LlwwC;+K^A%vKCVk|W zCj2c%H-HcL4rIjw3m4)Uc4BMrjd*{$3Oq3{Av2uCod+F7 zOtqOmh{0q>@^^9?*$G>NJ=KPRCr{(U;n{I3*Us~`m*O`IBh-WHP^CK-k8dQy=yA+L zsvEUU?*Kg6G4v+t0y~wSMb5y#)MDk`$^-RxrMBSV;@pyFEZ0$tmr~%J*K8QY{Y`7F z-q)(?Yqdo%|MwhV#ud|l(YniZl;`pqr5CKu>?=Ite&%}#RmIElP9;nT^V}?$1~?*T zB)iPWiIU*k;5#T<$SQ0sJBD`S)rtFLNsHIMC8&(6m06=6QMZ`N=F6t}W|ICITcdW? zOw>gt$dY8eZTgBG%@i?7mM^TwO*@IjFcW8)*q)y((1Oo*FZV%qWTrcPTXt?nKxWg_ zRVkZO4`%)-6XYIiq%+(h1$BzLQQ~pwd8H;53lGV*&a`BhhQS-a6hPkiS^P?Rt)`Lh zEiMOTePmf=4zlyE?ZID%TygsB4@@hWYVclXgXs?Sz5cyC+(&q)=e)z-)_6T6?rGxU%+kV2Y`+y&!Pg#N+Bz zeG}fFY)9Y(OF8gN*~eYYo0hpXv0+@74{Xwt)bhznQcirIL|0~G?RCJGh?T`BMl^O6 zag7f=5%8TW!M;Qtowp{vAY);EmV6vPM>eBxn@6~EBl?zlP&%mS#*m+^eX!cFvZ#{! ztr9A_yfZ!3rK^Ddu+09OHJK*KgIIz(U2QLB z_XRU3>|xOxrMV(4BF_h}2>v$YWzcu#X1-||xgQS2?a0iK(@aMkT=1K)5s|xN8WwpT z84}dTyjphy!pdte%cqM~@HO;kTTvGuWC>~;V76I^cz4Pu_#NDkj0}Z02Va$(i>arR zDklv{9-Ov4GbSS`V_kOBTzf(Dg6_gDGQn<-YEg7k*f7^!n9op`hUK(aoM83d&VQDF zluOl@TE+#WMf?%v3V$3@FKkCpGwUOAygXK1#DBs6C9#;7p6dt-x*d>X&o;X$t0o9X zJRNfHW%f@$nbs)j>*UakXW11BDtp2zA|=rj1-#e*u(bHqr@BD#DXTCpM~PY z9_y6Qt&x{QG1q*yvR;$_BhQmIEAc`?w?rlRbtaeB)$__n@O^T3q`r*5k@BN=Ak`ys zS(!fNUzL~_9^yPeWvGXR2zNKYTw0i!lz%~TnK}hsh|Y@rKK6d}nqbXVpZ-l{g)N@n z3TAo+inE9Sd%4iH5d))|hFWcN@%H@0oEpi`<2uG=#vJhE7$*vOa#K~?E9VvoG#8Ff=iC)P-umsl_5WX7X{4cLq!cWoxy#WxmOWa$JXrN95nQKyHFH{y>f3`i z3wM`VRgo*pL?3YciM0Xjg%;k`-XFvodV7ksYOdAccvRW2Q;tVOSD{Vr+0=Q7KPB!= zX`Oy4YgAs6yAhWn;Cxc{u7t<$=O(}KJT*-!!k2$szDKb+uCsvogZY|z+%UsyRY7x4 zl=PhXG3bZlb;}MZbFK&;c#V|Zld{|91PQ(Hwzh47&jJPo40Y`f=;CCpS=15nNN%N+ zwF%b}Gt(;NMTj=lC$d~gHg-q#biD&SS&mh7YZ+YaXEL>LlVAzD<#+pHZW6T%$2)VA6-5w`h=x{ zJRyHsmYNZsaXqtt-UuO^Di$=hSdX%$%FT`~6B5O^g`~XT?DM(fIZ>;{jd@cCiTzdB8)XoVnKa|c|qHGT+QvOnH zvdY`2t=2cn%e=CgL$k-_R?b_H-`Nu&=bJi)-;BLdhA(wEs1xL zvn=zMoQK}B+7-*gKzm3)#NLQ6Lg%>Jm}wTr|es5i{4K7;69RfH+O!)b=(^^fMvVg&Ik;R@Y{XCY6U16!W8T7yGTD)Em0E~r z(mwWU>lkOc>xzpFNVV;tR3(cK@$sH|LY#h?nPeSnW3BT{LFQEUJL)7+1FNNk@L%~x z`JQo8rC(H83)J38D|}VlhYD^LTz0SY;aqDzTm6QvZM_&|k4O#fYkNtql5z@^ykz$> zKo+_yH`PN3GfP>&4hRgTLsJ4CTAWmAsd_=ng62L|ya6lx7MQ*;4`W$o5X{#MCT8i+ z)SB9K?SeL4zlpt2>x*xFn_$ccFDOxvk(Zd;J}W(aa_&%hoo!|0{SvKW10zmYhRApF z%jI;;>*%@82P#`&1#TzuAic=?#3=@i4M`3Ru$IPcVrx$~-%nBq?qrkfeFHYQMEe7a zla0eWsO!b+{50=ocY9Ag4wskc=gD2nLgozdPP*?olJ|Aa(v0%yoij>i)yTPBP+A#d zTO8H6?e(upivpvWjGdL~{z^srz0z29dQE$W&1p{(Q zcqXa~mUXe!tnf?lR$p%k5D`CypH?Ttb6?PoAlOw5M8mBe# z7OrUb6d%0HdI_tkFfs<7?vu$si9Ea?z6aLN;;@?MwY~>%Ho~aA)L3dKk*lAD5zZvN zJHC-vLLMY<;Q88Wr5W6x7nh&Q7MKxxou2|DWXHK7e1C3~w{k&bUU=SUehy)_pA8um zaVzY;s~?Ta5#Ij!1M*9{>vHSmAN0dS4fcVhm*cssq3eo0!xT);R6Rm{{w$v;mC{!e zZt4Ten@pCHF!MK)Sw?;UEZumR`%OzzWI=6(oyFG>{fI1_gE6&Ck|I{>(^Rk2PFy4o zkXyievMp*mElb<2--9(??}%7<16m9(2kYT3Y5TMgV4HXZ>xK3MTEkp&0oji10f^`U z_*8g~%Y^kv+u>Dw=(M&J`lB$-1^=NwmuVn(vzi19? zrhC9hK?tn-+XGLJxxjVtlRgrjP2+(9qdJ}lD^EM(+tD+jehhFjO2QNAS$INzs22xx zA}f?_4|B2}!qea!{Wp-W!}_MvT0?y^JlhU}^IBcN5!h9|n_dE*(%b6` zAUZ7YG>=&SienA15x{z}6~@+-g--gwWq7{ z+Xt&|O}Yi%CSL}m{sin>JR5L+W`L%Jz!-4|o{JG@XKfg5xCJ~L-(j=BGcd^kTLpc? z04v5uc)o88_Fl!V!E^I&7-iT3HNJx=*I_K9J@nr;7@7D1#wK>bdichm6KM^D`EUWi zi_ip8D!`L|S8O$S(t=mVEr8B{svT#9JBJB%Kj zg1Ytfe!yFC9ymE@c%YKwNfQ`TIRfK9QCKm+FHVIg`Y>22 z=Yah^fLA>Q5HX&@%EC_2f}zlFO<)8h7)ATu zwT1CI;$cpKKHmqHY=v3iUTD7v=}6`s=zGM-Xoud;gg%sk8G?kEi-H)VfhXjNeqVn9 zqcTT9YZlab0Q#?jCtg9VTCh@jD#TtV7^8{=?wpK|G9SR`&=c?>3z5(o@_K_+HNne> zr3W!EnW0~c!e~n+h=Ou3>JbBI2*@w8Fd>`{IARz+yR*He_K&Z!oZ!)3%X|Q!e3viH( zINlI*HsXgw@sJKB(%}q5jAV$N8-0*G#A#))Ga~JXqYE)Yp*Es36xoJY;}AnI;!iRd zn@F$}v5q1hCWH0M&w+=y+zc*KgPRli1F@y~Ko?@UM4U*7jS4Y=p%jDd6fV2aYJ)Eg zabzLxG-M~@NyJeFkm5f=yj2EE8DgSCmZBW>W4^%#iliI0KCW#94;b8Ah-b^_9mLCo z+K;p#&4xufq#))k#7QVXs$ViHiCP6qiaz$31=@_F!Ow+;Sfj{~RwAxE#F%EVNg@xS zUO-+nm~s7Ftwt%Mv>_Amr5P+}s1|A&%0>Kx1hgG-Mj3pMMoUp^jWopggg6~luo(3w zibR8f4)rYJDMd_VC>5o1AKPYdHNtB%@V*yfBNwi+S#Tbr4~nAXkI|9^X?c)iq-FkB ziCidc?B_uW;;qex5{PvfG4dHKj));S|Kl-YN<-d6>}V(-arPm`NW^1?tVDf{ΝT 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 Y8-Jnfz0lXaHvU52hoL-z|119f3$Ilx&j0`b literal 0 HcmV?d00001 diff --git a/main/xiaozhi-server/core/api/ota_handler.py b/main/xiaozhi-server/core/api/ota_handler.py index f24c3f8f..b93a7ed3 100644 --- a/main/xiaozhi-server/core/api/ota_handler.py +++ b/main/xiaozhi-server/core/api/ota_handler.py @@ -81,38 +81,29 @@ class OTAHandler(BaseHandler): }, } - mqtt_gateway_config = server_config.get("mqtt_gateway", {}) - mqtt_gateway_host = mqtt_gateway_config.get("host", "") - mqtt_gateway_port = mqtt_gateway_config.get("port", "") + + mqtt_gateway_endpoint = server_config.get("mqtt_gateway") - - if mqtt_gateway_host: # 配置了mqtt_gateway,使用MQTT和UDP协议传输 - # 从设备型号获取group_id,与manager-api保持一致 - # 客户端ID格式:groupId@@@macAddress@@@macAddress + if mqtt_gateway_endpoint: # 如果配置了非空字符串 # 尝试从请求数据中获取设备型号 device_model = "default" try: - # 假设设备型号在request数据的某个字段中 if "device" in data_json and isinstance(data_json["device"], dict): device_model = data_json["device"].get("model", "default") elif "model" in data_json: device_model = data_json["model"] - # 为了保证格式一致性,进行与manager-api相同的处理 group_id = f"GID_{device_model}".replace(":", "_").replace(" ", "_") except Exception as e: self.logger.bind(tag=TAG).error(f"获取设备型号失败: {e}") - # 如果获取失败,使用配置文件中的默认值 - group_id = mqtt_gateway_config.get("group_id", "GID_default").replace(":", "_") - + group_id = "GID_default" + mac_address_safe = device_id.replace(":", "_") mqtt_client_id = f"{group_id}@@@{mac_address_safe}@@@{mac_address_safe}" - # 构建用户数据(包含IP等信息) + # 构建用户数据 user_data = { "ip": "unknown" } - - # 将用户数据编码为Base64 JSON try: user_data_json = json.dumps(user_data) username = base64.b64encode(user_data_json.encode('utf-8')).decode('utf-8') @@ -120,24 +111,19 @@ class OTAHandler(BaseHandler): self.logger.bind(tag=TAG).error(f"生成用户名失败: {e}") username = "" - # 获取MQTT签名密钥 + # 生成密码 password = "" signature_key = server_config.get("mqtt_signature_key", "") if signature_key: - # 使用签名密钥生成密码 password = self.generate_password_signature(mqtt_client_id + "|" + username, signature_key) if not password: - # 如果签名生成失败,使用配置文件中的密码 - password = mqtt_gateway_config.get("password", "") + password = "" # 签名失败则留空,由设备决定是否允许无密码 else: - # 如果没有签名密钥,使用配置文件中的密码 - password = mqtt_gateway_config.get("password", "") - self.logger.bind(tag=TAG).warning("缺少MQTT签名密钥,使用配置文件中的密码") + self.logger.bind(tag=TAG).warning("缺少MQTT签名密钥,密码留空") - # 构建MQTT配置 - endpoint = f"{mqtt_gateway_host}:{mqtt_gateway_port}" + # 构建MQTT配置(直接使用 mqtt_gateway 字符串) return_json["mqtt_gateway"] = { - "endpoint": endpoint, + "endpoint": mqtt_gateway_endpoint, "client_id": mqtt_client_id, "username": username, "password": password, @@ -146,19 +132,8 @@ class OTAHandler(BaseHandler): } self.logger.bind(tag=TAG).info(f"为设备 {device_id} 下发MQTT网关配置") - # 添加UDP网关配置 - if "udp_gateway" in server_config: - udp_config = server_config["udp_gateway"] - udp_host = udp_config.get("host", "") - udp_port = udp_config.get("port", "") - if udp_host: - return_json["udp_gateway"] = { - "host": udp_host, - "port": udp_port - } - self.logger.bind(tag=TAG).info(f"为设备 {device_id} 下发UDP网关配置") - else: # 未配置mqtt_gateway,使用WebSocket协议传输 + else: # 未配置 mqtt_gateway,下发 WebSocket return_json["websocket"] = { "url": self._get_websocket_url(local_ip, port), } @@ -192,4 +167,4 @@ class OTAHandler(BaseHandler): response = web.Response(text="OTA接口异常", content_type="text/plain") finally: self._add_cors_headers(response) - return response + return response \ No newline at end of file From ebf10828f02e129cac0f88e198c35ef0eaa8fd34 Mon Sep 17 00:00:00 2001 From: Sakura-RanChen <1908198662@qq.com> Date: Wed, 24 Sep 2025 16:35:26 +0800 Subject: [PATCH 19/27] =?UTF-8?q?update:=20=E6=9B=B4=E6=96=B0=E6=96=87?= =?UTF-8?q?=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/index-stream-integration.md | 51 +++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/docs/index-stream-integration.md b/docs/index-stream-integration.md index 875dbdd8..8af91612 100644 --- a/docs/index-stream-integration.md +++ b/docs/index-stream-integration.md @@ -1,9 +1,12 @@ # IndexStreamTTS 使用指南 ## 环境准备 -### 1. 克隆项目 (这里使用的为VLLM的版本) +### 1. 克隆项目 (注意这里使用的为VLLM1.0的Releases版本) ```bash -git clone https://github.com/Ksuriuri/index-tts-vllm.git +https://github.com/Ksuriuri/index-tts-vllm/releases/tag/IndexTTS-vLLM-1.0 +``` +进入解压后的目录 +```bash cd index-tts-vllm ``` @@ -13,7 +16,7 @@ conda create -n index-tts-vllm python=3.12 conda activate index-tts-vllm ``` -### 3. 安装PyTorch +### 3. 安装PyTorch 需要版本为2.8.0(最新版) #### 查看显卡最高支持的版本和实际安装的版本 ```bash nvidia-smi @@ -27,12 +30,11 @@ CUDA Version: 12.8 ```bash Cuda compilation tools, release 12.8, V12.8.89 ``` -#### 那么对应的安装命令 (请注意不要横跨大版本!!!) +#### 那么对应的安装命令(pytorch默认给的是12.8的驱动版本) ```bash -pip install torch==2.7.0 torchvision==0.22.0 torchaudio==2.7.0 --index-url https://download.pytorch.org/whl/cu128 +pip install torch torchvision ``` -优先建议安装 pytorch 2.7.0(对应 vllm 0.9.0),具体安装指令请参考:[pytorch 官网](https://pytorch.org/get-started/locally/]\) -若显卡不支持,请安装 pytorch 2.5.1(对应 vllm 0.7.3),并将 requirements.txt 中 vllm==0.9.0 修改为 vllm==0.7.3 +需要 pytorch 版本 2.8.0(对应 vllm 0.10.2),具体安装指令请参考:[pytorch 官网](https://pytorch.org/get-started/locally/]\) ### 4. 安装依赖 ```bash @@ -54,7 +56,7 @@ git lfs install ``` 创建模型目录,并拉取模型 ```bash -mkkdir model_dir +mkdir model_dir cd model_dir git clone https://www.modelscope.cn/IndexTeam/IndexTTS-1.5.git ``` @@ -71,6 +73,9 @@ bash convert_hf_format.sh model_dir/IndexTTS-1.5 ### 6. 更改接口适配一下项目 接口返回数据与项目不适配需要调整一下,使其直接返回音频数据 +```bash +vi api_server.py +``` ```bash @app.post("/tts", responses={ 200: {"content": {"application/octet-stream": {}}}, @@ -104,7 +109,7 @@ async def tts_api(request: Request): vi start_api.sh ``` ### 将下面内容粘贴进去并按:输入wq保存 -#### 脚本中的/home/system/indexTTS/index-tts-vllm/model_dir/IndexTTS-1.5 请自行修改为实际路径 +#### 脚本中的/home/system/index-tts-vllm/model_dir/IndexTTS-1.5 请自行修改为实际路径 ```bash # 激活conda环境 conda activate index-tts-vllm @@ -129,17 +134,35 @@ else echo "已终止进程 $PID_VLLM" fi +# 查找占用VLLM::EngineCore进程 +GPU_PIDS=$(ps aux | grep -E "VLLM|EngineCore" | grep -v grep | awk '{print $2}') + +# 检查是否找到进程号 +if [ -z "$GPU_PIDS" ]; then + echo "没有找到VLLM相关进程" +else + echo "找到VLLM相关进程,进程号为: $GPU_PIDS" + # 先尝试普通kill,等待2秒 + kill $GPU_PIDS + sleep 2 + # 检查进程是否还在 + if ps -p $GPU_PIDS > /dev/null; then + echo "进程仍在运行,强制终止..." + kill -9 $GPU_PIDS + fi + echo "已终止进程 $GPU_PIDS" +fi + # 创建tmp目录(如果不存在) mkdir -p tmp # 后台运行api_server.py,日志重定向到tmp/server.log -export VLLM_USE_V1=0 -nohup python api_server.py --model_dir /home/system/indexTTS/index-tts-vllm/model_dir/IndexTTS-1.5 --port 11996 > tmp/server.log 2>&1 & +nohup python api_server.py --model_dir /home/system/index-tts-vllm/model_dir/IndexTTS-1.5 --port 11996 > tmp/server.log 2>&1 & echo "api_server.py 已在后台运行,日志请查看 tmp/server.log" ``` 给脚本执行权限并运行脚本 ```bash -chmod +x tmp +chmod +x start_api.sh ./start_api.sh ``` 日志会在tmp/server.log中输出,可以通过以下命令查看日志情况 @@ -148,7 +171,7 @@ tail -f tmp/server.log ``` ## 音色配置 index-tts-vllm支持通过配置文件注册自定义音色,支持单音色和混合音色配置。 -在项目根目录下的assets/speaker.json文件中配置自定义音色 +在项目根目录下的assets/speaker.json文件中配置自定义音色 ### 配置格式说明 ```bash { @@ -161,5 +184,5 @@ index-tts-vllm支持通过配置文件注册自定义音色,支持单音色和 ] } ``` -### 注意 +### 注意 (需重启服务进行注册) 添加后需在智控台中添加相应的说话人(单模块则更换相应的voice) \ No newline at end of file From e77a3b79cd95ebad838008b01ec3a3273a002e6f Mon Sep 17 00:00:00 2001 From: Sakura-RanChen <1908198662@qq.com> Date: Thu, 25 Sep 2025 10:17:56 +0800 Subject: [PATCH 20/27] =?UTF-8?q?fix:=20Markdown=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/core/utils/tts.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/main/xiaozhi-server/core/utils/tts.py b/main/xiaozhi-server/core/utils/tts.py index eb383667..8b68d195 100644 --- a/main/xiaozhi-server/core/utils/tts.py +++ b/main/xiaozhi-server/core/utils/tts.py @@ -6,6 +6,27 @@ import importlib logger = setup_logging() +punctuation_set = { + ",", + ",", # 中文逗号 + 英文逗号 + "。", + ".", # 中文句号 + 英文句号 + "!", + "!", # 中文感叹号 + 英文感叹号 + "“", + "”", + '"', # 中文双引号 + 英文引号 + ":", + ":", # 中文冒号 + 英文冒号 + "-", + "-", # 英文连字符 + 中文全角横线 + "、", # 中文顿号 + "[", + "]", # 方括号 + "【", + "】", # 中文方括号 + "~", # 波浪号 +} def create_instance(class_name, *args, **kwargs): # 创建TTS实例 @@ -107,6 +128,11 @@ class MarkdownCleaner: """ 主入口方法:依序执行所有正则,移除或替换 Markdown 元素 """ + # 检查文本是否全为英文和基本标点符号 + if text and all((c.isascii() or c.isspace() or c in punctuation_set) for c in text): + # 保留原始空格,直接返回 + return text + for regex, replacement in MarkdownCleaner.REGEXES: text = regex.sub(replacement, text) return text.strip() \ No newline at end of file From e15c784bdb2511ade22534bf9e13039e2f31dc65 Mon Sep 17 00:00:00 2001 From: Sakura-RanChen <1908198662@qq.com> Date: Thu, 25 Sep 2025 10:57:12 +0800 Subject: [PATCH 21/27] =?UTF-8?q?fix:=20=E6=96=87=E6=A1=A3=E9=93=BE?= =?UTF-8?q?=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/index-stream-integration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index-stream-integration.md b/docs/index-stream-integration.md index 8af91612..caf3ae60 100644 --- a/docs/index-stream-integration.md +++ b/docs/index-stream-integration.md @@ -34,7 +34,7 @@ Cuda compilation tools, release 12.8, V12.8.89 ```bash pip install torch torchvision ``` -需要 pytorch 版本 2.8.0(对应 vllm 0.10.2),具体安装指令请参考:[pytorch 官网](https://pytorch.org/get-started/locally/]\) +需要 pytorch 版本 2.8.0(对应 vllm 0.10.2),具体安装指令请参考:[pytorch 官网](https://pytorch.org/get-started/locally/) ### 4. 安装依赖 ```bash From 9bbcf7f31abe82d1226d75e66b5d00b29ade038e Mon Sep 17 00:00:00 2001 From: hrz <1710360675@qq.com> Date: Thu, 25 Sep 2025 12:25:36 +0800 Subject: [PATCH 22/27] =?UTF-8?q?update:=E6=9B=B4=E6=96=B0=E6=96=87?= =?UTF-8?q?=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/mqtt-gateway-integration.md | 8 ++++++++ docs/paddlespeech-deploy.md | 22 ++++++++++++++++------ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/docs/mqtt-gateway-integration.md b/docs/mqtt-gateway-integration.md index 09fe83c1..0b52b880 100644 --- a/docs/mqtt-gateway-integration.md +++ b/docs/mqtt-gateway-integration.md @@ -18,6 +18,14 @@ ws://127.0.0.1:8000/xiaozhi/v1?from=mqtt_gateway ws://你宿主机局域网IP:8000/xiaozhi/v1?from=mqtt_gateway ``` +## 重要提示 + +如果你是服务器部署,需要确保服务器`1883`、`8884`、`8007`端口都对外开放。`8884`选择的协议类型是`UDP`,其他是`TCP`。 + +如果你是服务器部署,需要确保服务器`1883`、`8884`、`8007`端口都对外开放。`8884`选择的协议类型是`UDP`,其他是`TCP`。 + +如果你是服务器部署,需要确保服务器`1883`、`8884`、`8007`端口都对外开放。`8884`选择的协议类型是`UDP`,其他是`TCP`。 + ## 第一步 部署MQTT网关 1. 克隆[改造后的xiaozhi-mqtt-gateway项目](https://github.com/xinnan-tech/xiaozhi-mqtt-gateway.git): diff --git a/docs/paddlespeech-deploy.md b/docs/paddlespeech-deploy.md index 17030fab..94621eb5 100644 --- a/docs/paddlespeech-deploy.md +++ b/docs/paddlespeech-deploy.md @@ -1,5 +1,9 @@ # PaddleSpeechTTS集成xiaozhi服务 +## 重点说明 +- 优点:本地离线部署、速度快 +- 缺点:截止2025年9月25日,默认的模型是中文模型,不支持英文转语音。如果含英文会发不出声音,如需同时支持中英文需要自己训练。 + ## 一、基础环境要求 操作系统:Windows / Linux / WSL 2 @@ -16,15 +20,21 @@ git clone https://github.com/PaddlePaddle/PaddleSpeech.git ``` ### 2.建立虚拟环境 ```bash -#请根据Paddle官方支持的python版本建立环境 ```https://www.paddlepaddle.org.cn/install``` + conda create -n paddle_env python=3.10 -y conda activate paddle_env ``` -### 3.进入paddlespeech目录 +### 3.安装paddle +因CPU架构、GPU架构不同,请根据Paddle官方支持的python版本建立环境 +``` +https://www.paddlepaddle.org.cn/install +``` + +### 4.进入paddlespeech目录 ```bash cd PaddleSpeech ``` -### 4.安装paddlespeech +### 5.安装paddlespeech ```bash pip install pytest-runner -i https://pypi.tuna.tsinghua.edu.cn/simple @@ -32,17 +42,17 @@ pip install pytest-runner -i https://pypi.tuna.tsinghua.edu.cn/simple pip install paddlepaddle -i https://mirror.baidu.com/pypi/simple pip install paddlespeech -i https://pypi.tuna.tsinghua.edu.cn/simple ``` -### 5.使用命令自动下载语音模型 +### 6.使用命令自动下载语音模型 ```bash paddlespeech tts --input "你好,这是一次测试" ``` 此步骤会自动下载模型缓存至本地 .paddlespeech/models 目录 -### 6.修改tts_online_application.yaml配置 +### 7.修改tts_online_application.yaml配置 参考目录 ```"PaddleSpeech\demos\streaming_tts_server\conf\tts_online_application.yaml"``` 选择```tts_online_application.yaml```文件用编辑器打开,设置```protocol```为```websocket``` -### 7.启动服务 +### 8.启动服务 ```yaml paddlespeech_server start --config_file ./demos/streaming_tts_server/conf/tts_online_application.yaml #官方默认启动命令: From d1587cc6457174302dae66c7bcc2a0b2d1042a5a Mon Sep 17 00:00:00 2001 From: FAN-yeB <1442100690@qq.com> Date: Thu, 25 Sep 2025 14:31:23 +0800 Subject: [PATCH 23/27] =?UTF-8?q?update:=E6=9B=B4=E6=96=B0mqtt=E7=BD=91?= =?UTF-8?q?=E5=85=B3=E9=9B=86=E6=88=90=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/mqtt-gateway-integration.md | 129 +++++++++++++++++++++++++++++-- 1 file changed, 122 insertions(+), 7 deletions(-) diff --git a/docs/mqtt-gateway-integration.md b/docs/mqtt-gateway-integration.md index 0b52b880..8a8e37d7 100644 --- a/docs/mqtt-gateway-integration.md +++ b/docs/mqtt-gateway-integration.md @@ -1,11 +1,11 @@ # MQTT 网关部署教程 `xiaozhi-esp32-server`项目,可结合虾哥开源的[xiaozhi-mqtt-gateway](https://github.com/78/xiaozhi-mqtt-gateway) 项目进行简单改造,即可实现小智硬件MQTT+UDP连接。 +本教程分为两部分: +- 第一部分:全模块运行实现小智硬件MQTT+UDP连接 +- 第二部分:单模块运行xiaozhi-server实现小智硬件MQTT+UDP连接 -## 准备工作 - -查看你智控台首页底部的版本号,确认你的智控台版本是否是`0.7.7`及以上版本。如果不是,需要升级智控台。 - +## 准备阶段 准备好你的`xiaozhi-server`的`mqtt-websocket`连接地址。在你原来的`websocket地址`基础上,添加`?from=mqtt_gateway`字符,就可以得到`mqtt-websocket`连接地址 1、如果你是源码部署,你的`mqtt-websocket`地址是: @@ -26,7 +26,13 @@ ws://你宿主机局域网IP:8000/xiaozhi/v1?from=mqtt_gateway 如果你是服务器部署,需要确保服务器`1883`、`8884`、`8007`端口都对外开放。`8884`选择的协议类型是`UDP`,其他是`TCP`。 -## 第一步 部署MQTT网关 +## 全模块运行实现小智硬件MQTT+UDP连接 + +### 准备工作 + +查看你智控台首页底部的版本号,确认你的智控台版本是否是`0.7.7`及以上版本。如果不是,需要升级智控台。 + +### 第一步 部署MQTT网关 1. 克隆[改造后的xiaozhi-mqtt-gateway项目](https://github.com/xinnan-tech/xiaozhi-mqtt-gateway.git): ```bash @@ -103,7 +109,7 @@ pm2 logs xz-mqtt pm2 restart xz-mqtt ``` -## 第二步 配置智控台 +### 第二步 配置智控台 1. 在智控台顶部,点击`参数管理`,搜索`server.mqtt_gateway`,点击编辑,填入你在`.env`文件中设置的`PUBLIC_IP`+`:`+`MQTT_PORT`。类似这样 ``` @@ -135,7 +141,116 @@ curl 'http://localhost:8002/xiaozhi/ota/' \ {"server_time":{"timestamp":1757567894012,"timeZone":"Asia/Shanghai","timezone_offset":480},"activation":{"code":"460609","message":"http://xiaozhi.server.com\n460609","challenge":"11:22:33:44:55:66"},"firmware":{"version":"1.0.1","url":"http://xiaozhi.server.com:8002/xiaozhi/otaMag/download/NOT_ACTIVATED_FIRMWARE_THIS_IS_A_INVALID_URL"},"websocket":{"url":"ws://192.168.4.23:8000/xiaozhi/v1/"},"mqtt":{"endpoint":"192.168.0.7:1883","client_id":"GID_default@@@11_22_33_44_55_66@@@7b94d69a-9808-4c59-9c9b-704333b38aff","username":"eyJpcCI6IjA6MDowOjA6MDowOjA6MSJ9","password":"Y8XP9xcUhVIN9OmbCHT9ETBiYNE3l3Z07Wk46wV9PE8=","publish_topic":"device-server","subscribe_topic":"devices/p2p/11_22_33_44_55_66"}} ``` -## 第三步 重启小智设备 +### 第三步 重启小智设备 +由于MQTT信息是需要靠OTA地址下发的,因此只有你保证能正常连接服务器的OTA地址,重启唤醒即可。 + +唤醒后留意mqtt-gateway的日志,确认是否有连接成功的日志。 +``` +pm2 logs xz-mqtt +``` + +## 单模块运行xiaozhi-server实现小智硬件MQTT+UDP连接 + + +### 第一步 部署MQTT网关 + +1. 克隆[改造后的xiaozhi-mqtt-gateway项目](https://github.com/xinnan-tech/xiaozhi-mqtt-gateway.git): +```bash +git clone https://ghfast.top/https://github.com/xinnan-tech/xiaozhi-mqtt-gateway.git +cd xiaozhi-mqtt-gateway +``` + +2. 安装依赖: +```bash +npm install +npm install -g pm2 +``` + +3. 配置 `config.json`: +```bash +cp config/mqtt.json.example config/mqtt.json +``` + +4. 编辑配置文件 config/mqtt.json,把你在`本文准备阶段`的`mqtt-websocket`地址替换到`chat_servers`里。例如源码部署的`xiaozhi-server`就是如下配置: + +``` +{ + "production": { + "chat_servers": [ + "ws://127.0.0.1:8000/xiaozhi/v1?from=mqtt_gateway" + ] + }, + "debug": false, + "max_mqtt_payload_size": 8192, + "mcp_client": { + "capabilities": { + }, + "client_info": { + "name": "xiaozhi-mqtt-client", + "version": "1.0.0" + }, + "max_tools_count": 128 + } +} +``` +5. 在项目根目录创建下`.env`文件,并设置以下环境变量: +``` +PUBLIC_IP=your-ip # 服务器公网IP +MQTT_PORT=1883 # MQTT服务器端口 +UDP_PORT=8884 # UDP服务器端口 +API_PORT=8007 # 管理API端口 +MQTT_SIGNATURE_KEY=test # MQTT签名密钥 +``` +请注意`PUBLIC_IP`配置,确保其与实际公网IP一致,如果有域名就填域名。 + +`MQTT_SIGNATURE_KEY` 是用于MQTT连接认证的密钥,最好设置成复杂一点的,最好是设置成8个字符以上且同时包含大小写字母,这个密钥稍后还会用到。 + +- 注意不要用简单的密码,比如`123456`、`test`等。 +- 注意不要用简单的密码,比如`123456`、`test`等。 +- 注意不要用简单的密码,比如`123456`、`test`等。 + +6. 启动MQTT网关 +``` +# 启动服务 +pm2 start ecosystem.config.js + +# 查看日志 +pm2 logs xz-mqtt +``` + +当你看到如下日志,说明MQTT网关启动成功: +``` +0|xz-mqtt | 2025-09-11T12:14:48: MQTT 服务器正在监听端口 1883 +0|xz-mqtt | 2025-09-11T12:14:48: UDP 服务器正在监听 x.x.x.x:8884 +``` + +如果需要重启MQTT网关,执行如下命令: +``` +pm2 restart xz-mqtt +``` + +### 第二步 修改配置文件 + +打开你的`data/.config.yaml`文件,在你的`server.mqtt_gateway`填入你在`.env`文件中设置的`PUBLIC_IP`+`:`+`MQTT_PORT`。类似这样 +``` +192.168.0.7:1883 +``` +在你的`server.mqtt_signature_key`填入你在`.env`文件中设置的`MQTT_SIGNATURE_KEY`。 + +上面的配置完成后,你可以使用curl命令,验证你的ota地址是否会下发mqtt配置,把下面的`http://localhost:8002/xiaozhi/ota/`改成你的ota地址 +``` +curl 'http://localhost:8002/xiaozhi/ota/' \ + -H 'Device-Id: 11:22:33:44:55:66' \ + --data-raw $'{\n "application": {\n "version": "1.0.1",\n "elf_sha256": "1"\n },\n "board": {\n "mac": "11:22:33:44:55:66"\n }\n}' +``` + +如果返回的内容包含`mqtt`相关的配置,说明配置成功。类似这样 +``` +{"server_time":{"timestamp":1758781561083,"timeZone":"GMT+08:00","timezone_offset":480},"activation":{"code":"527111","message":"http://xiaozhi.server.com\n527111","challenge":"11:22:33:44:55:66"},"firmware":{"version":"1.0.1","url":"http://xiaozhi.server.com:8002/xiaozhi/otaMag/download/NOT_ACTIVATED_FIRMWARE_THIS_IS_A_INVALID_URL"},"websocket":{"url":"ws://192.168.1.15:8000/xiaozhi/v1/"},"mqtt":{"endpoint":"192.168.1.15:1883","client_id":"GID_default@@@11_22_33_44_55_66@@@11_22_33_44_55_66","username":"eyJpcCI6IjE5Mi4xNjguMS4xNSJ9","password":"fjAYs49zTJecWqJ3jBt+kqxVn/x7vkXRAc85ak/va7Y=","publish_topic":"device-server","subscribe_topic":"devices/p2p/11_22_33_44_55_66"}} +``` + + +### 第三步 重启小智设备 由于MQTT信息是需要靠OTA地址下发的,因此只有你保证能正常连接服务器的OTA地址,重启唤醒即可。 唤醒后留意mqtt-gateway的日志,确认是否有连接成功的日志。 From bb07015feb4dbc5d3d9cd286295c73fb99b2cbe0 Mon Sep 17 00:00:00 2001 From: hrz <1710360675@qq.com> Date: Thu, 25 Sep 2025 14:51:34 +0800 Subject: [PATCH 24/27] =?UTF-8?q?update:=E6=8A=BD=E7=A6=BB=E5=85=AC?= =?UTF-8?q?=E5=85=B1=E6=95=99=E7=A8=8B=E5=88=B0=E6=9C=80=E5=89=8D=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/mqtt-gateway-integration.md | 114 +++++-------------------------- 1 file changed, 18 insertions(+), 96 deletions(-) diff --git a/docs/mqtt-gateway-integration.md b/docs/mqtt-gateway-integration.md index 8a8e37d7..6a1beed4 100644 --- a/docs/mqtt-gateway-integration.md +++ b/docs/mqtt-gateway-integration.md @@ -1,9 +1,10 @@ # MQTT 网关部署教程 `xiaozhi-esp32-server`项目,可结合虾哥开源的[xiaozhi-mqtt-gateway](https://github.com/78/xiaozhi-mqtt-gateway) 项目进行简单改造,即可实现小智硬件MQTT+UDP连接。 -本教程分为两部分: -- 第一部分:全模块运行实现小智硬件MQTT+UDP连接 -- 第二部分:单模块运行xiaozhi-server实现小智硬件MQTT+UDP连接 +本教程分为三部分,你可以根据你是全模块部署还是单模块部署,选择对应的部分接入MQTT网关: +- 第一部分:部署MQTT网关 +- 第二部分:全模块运行实现小智硬件MQTT+UDP连接 +- 第三部分:单模块运行xiaozhi-server实现小智硬件MQTT+UDP连接 ## 准备阶段 准备好你的`xiaozhi-server`的`mqtt-websocket`连接地址。在你原来的`websocket地址`基础上,添加`?from=mqtt_gateway`字符,就可以得到`mqtt-websocket`连接地址 @@ -26,13 +27,8 @@ ws://你宿主机局域网IP:8000/xiaozhi/v1?from=mqtt_gateway 如果你是服务器部署,需要确保服务器`1883`、`8884`、`8007`端口都对外开放。`8884`选择的协议类型是`UDP`,其他是`TCP`。 -## 全模块运行实现小智硬件MQTT+UDP连接 -### 准备工作 - -查看你智控台首页底部的版本号,确认你的智控台版本是否是`0.7.7`及以上版本。如果不是,需要升级智控台。 - -### 第一步 部署MQTT网关 +## 第一部分:部署MQTT网关 1. 克隆[改造后的xiaozhi-mqtt-gateway项目](https://github.com/xinnan-tech/xiaozhi-mqtt-gateway.git): ```bash @@ -109,7 +105,9 @@ pm2 logs xz-mqtt pm2 restart xz-mqtt ``` -### 第二步 配置智控台 +## 第二部分:全模块运行实现小智硬件MQTT+UDP连接 + +查看你智控台首页底部的版本号,确认你的智控台版本是否是`0.7.7`及以上版本。如果不是,需要升级智控台。 1. 在智控台顶部,点击`参数管理`,搜索`server.mqtt_gateway`,点击编辑,填入你在`.env`文件中设置的`PUBLIC_IP`+`:`+`MQTT_PORT`。类似这样 ``` @@ -141,7 +139,6 @@ curl 'http://localhost:8002/xiaozhi/ota/' \ {"server_time":{"timestamp":1757567894012,"timeZone":"Asia/Shanghai","timezone_offset":480},"activation":{"code":"460609","message":"http://xiaozhi.server.com\n460609","challenge":"11:22:33:44:55:66"},"firmware":{"version":"1.0.1","url":"http://xiaozhi.server.com:8002/xiaozhi/otaMag/download/NOT_ACTIVATED_FIRMWARE_THIS_IS_A_INVALID_URL"},"websocket":{"url":"ws://192.168.4.23:8000/xiaozhi/v1/"},"mqtt":{"endpoint":"192.168.0.7:1883","client_id":"GID_default@@@11_22_33_44_55_66@@@7b94d69a-9808-4c59-9c9b-704333b38aff","username":"eyJpcCI6IjA6MDowOjA6MDowOjA6MSJ9","password":"Y8XP9xcUhVIN9OmbCHT9ETBiYNE3l3Z07Wk46wV9PE8=","publish_topic":"device-server","subscribe_topic":"devices/p2p/11_22_33_44_55_66"}} ``` -### 第三步 重启小智设备 由于MQTT信息是需要靠OTA地址下发的,因此只有你保证能正常连接服务器的OTA地址,重启唤醒即可。 唤醒后留意mqtt-gateway的日志,确认是否有连接成功的日志。 @@ -149,93 +146,20 @@ curl 'http://localhost:8002/xiaozhi/ota/' \ pm2 logs xz-mqtt ``` -## 单模块运行xiaozhi-server实现小智硬件MQTT+UDP连接 +## 第三部分:全模块运行实现小智硬件MQTT+UDP连接 - -### 第一步 部署MQTT网关 - -1. 克隆[改造后的xiaozhi-mqtt-gateway项目](https://github.com/xinnan-tech/xiaozhi-mqtt-gateway.git): -```bash -git clone https://ghfast.top/https://github.com/xinnan-tech/xiaozhi-mqtt-gateway.git -cd xiaozhi-mqtt-gateway -``` - -2. 安装依赖: -```bash -npm install -npm install -g pm2 -``` - -3. 配置 `config.json`: -```bash -cp config/mqtt.json.example config/mqtt.json -``` - -4. 编辑配置文件 config/mqtt.json,把你在`本文准备阶段`的`mqtt-websocket`地址替换到`chat_servers`里。例如源码部署的`xiaozhi-server`就是如下配置: - -``` -{ - "production": { - "chat_servers": [ - "ws://127.0.0.1:8000/xiaozhi/v1?from=mqtt_gateway" - ] - }, - "debug": false, - "max_mqtt_payload_size": 8192, - "mcp_client": { - "capabilities": { - }, - "client_info": { - "name": "xiaozhi-mqtt-client", - "version": "1.0.0" - }, - "max_tools_count": 128 - } -} -``` -5. 在项目根目录创建下`.env`文件,并设置以下环境变量: -``` -PUBLIC_IP=your-ip # 服务器公网IP -MQTT_PORT=1883 # MQTT服务器端口 -UDP_PORT=8884 # UDP服务器端口 -API_PORT=8007 # 管理API端口 -MQTT_SIGNATURE_KEY=test # MQTT签名密钥 -``` -请注意`PUBLIC_IP`配置,确保其与实际公网IP一致,如果有域名就填域名。 - -`MQTT_SIGNATURE_KEY` 是用于MQTT连接认证的密钥,最好设置成复杂一点的,最好是设置成8个字符以上且同时包含大小写字母,这个密钥稍后还会用到。 - -- 注意不要用简单的密码,比如`123456`、`test`等。 -- 注意不要用简单的密码,比如`123456`、`test`等。 -- 注意不要用简单的密码,比如`123456`、`test`等。 - -6. 启动MQTT网关 -``` -# 启动服务 -pm2 start ecosystem.config.js - -# 查看日志 -pm2 logs xz-mqtt -``` - -当你看到如下日志,说明MQTT网关启动成功: -``` -0|xz-mqtt | 2025-09-11T12:14:48: MQTT 服务器正在监听端口 1883 -0|xz-mqtt | 2025-09-11T12:14:48: UDP 服务器正在监听 x.x.x.x:8884 -``` - -如果需要重启MQTT网关,执行如下命令: -``` -pm2 restart xz-mqtt -``` - -### 第二步 修改配置文件 - -打开你的`data/.config.yaml`文件,在你的`server.mqtt_gateway`填入你在`.env`文件中设置的`PUBLIC_IP`+`:`+`MQTT_PORT`。类似这样 +打开你的`data/.config.yaml`文件,在`server`下找到`mqtt_gateway`填入你在`.env`文件中设置的`PUBLIC_IP`+`:`+`MQTT_PORT`。类似这样 ``` 192.168.0.7:1883 ``` -在你的`server.mqtt_signature_key`填入你在`.env`文件中设置的`MQTT_SIGNATURE_KEY`。 +在`server`下找到`mqtt_signature_key`填入你在`.env`文件中设置的`MQTT_SIGNATURE_KEY`。 + +在`server`下找到`mqtt_signature_key`填入你在`.env`文件中设置的`MQTT_SIGNATURE_KEY`。 + +在`server`下找到`udp_gateway`填入你在`.env`文件中设置的`PUBLIC_IP`+`:`+`UDP_PORT`。类似这样 +``` +192.168.0.7:8884 +``` 上面的配置完成后,你可以使用curl命令,验证你的ota地址是否会下发mqtt配置,把下面的`http://localhost:8002/xiaozhi/ota/`改成你的ota地址 ``` @@ -249,8 +173,6 @@ curl 'http://localhost:8002/xiaozhi/ota/' \ {"server_time":{"timestamp":1758781561083,"timeZone":"GMT+08:00","timezone_offset":480},"activation":{"code":"527111","message":"http://xiaozhi.server.com\n527111","challenge":"11:22:33:44:55:66"},"firmware":{"version":"1.0.1","url":"http://xiaozhi.server.com:8002/xiaozhi/otaMag/download/NOT_ACTIVATED_FIRMWARE_THIS_IS_A_INVALID_URL"},"websocket":{"url":"ws://192.168.1.15:8000/xiaozhi/v1/"},"mqtt":{"endpoint":"192.168.1.15:1883","client_id":"GID_default@@@11_22_33_44_55_66@@@11_22_33_44_55_66","username":"eyJpcCI6IjE5Mi4xNjguMS4xNSJ9","password":"fjAYs49zTJecWqJ3jBt+kqxVn/x7vkXRAc85ak/va7Y=","publish_topic":"device-server","subscribe_topic":"devices/p2p/11_22_33_44_55_66"}} ``` - -### 第三步 重启小智设备 由于MQTT信息是需要靠OTA地址下发的,因此只有你保证能正常连接服务器的OTA地址,重启唤醒即可。 唤醒后留意mqtt-gateway的日志,确认是否有连接成功的日志。 From f8447033b92838958f68b9d1c62a8d99b1d8b74b Mon Sep 17 00:00:00 2001 From: hrz <1710360675@qq.com> Date: Thu, 25 Sep 2025 14:54:05 +0800 Subject: [PATCH 25/27] =?UTF-8?q?update:=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/mqtt-gateway-integration.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/mqtt-gateway-integration.md b/docs/mqtt-gateway-integration.md index 6a1beed4..99d183bf 100644 --- a/docs/mqtt-gateway-integration.md +++ b/docs/mqtt-gateway-integration.md @@ -154,8 +154,6 @@ pm2 logs xz-mqtt ``` 在`server`下找到`mqtt_signature_key`填入你在`.env`文件中设置的`MQTT_SIGNATURE_KEY`。 -在`server`下找到`mqtt_signature_key`填入你在`.env`文件中设置的`MQTT_SIGNATURE_KEY`。 - 在`server`下找到`udp_gateway`填入你在`.env`文件中设置的`PUBLIC_IP`+`:`+`UDP_PORT`。类似这样 ``` 192.168.0.7:8884 From 7fee922796c25a29debf17e286cd8758d3dd5fd2 Mon Sep 17 00:00:00 2001 From: LiJinHui <166460433+stu-rgsze@users.noreply.github.com> Date: Thu, 25 Sep 2025 15:56:51 +0800 Subject: [PATCH 26/27] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E8=AE=BE=E5=A4=87?= =?UTF-8?q?=E6=95=B0=E9=87=8F=E6=98=BE=E7=A4=BA=EF=BC=88=E5=AE=9E=E6=97=B6?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../device/service/impl/DeviceServiceImpl.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceServiceImpl.java index f0603519..0dbe64c7 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceServiceImpl.java @@ -134,6 +134,10 @@ public class DeviceServiceImpl extends BaseServiceImpl // 清理redis缓存 redisUtils.delete(cacheDeviceKey); redisUtils.delete(deviceKey); + + // 添加:清除智能体设备数量缓存 + redisUtils.delete(RedisKeys.getAgentDeviceCountById(agentId)); + return true; } @@ -225,6 +229,13 @@ public class DeviceServiceImpl extends BaseServiceImpl @Override public void unbindDevice(Long userId, String deviceId) { + // 先查询设备信息,获取agentId + DeviceEntity device = baseDao.selectById(deviceId); + if (device != null && StringUtils.isNotBlank(device.getAgentId())) { + // 清除智能体设备数量缓存 + redisUtils.delete(RedisKeys.getAgentDeviceCountById(device.getAgentId())); + } + UpdateWrapper wrapper = new UpdateWrapper<>(); wrapper.eq("user_id", userId); wrapper.eq("id", deviceId); @@ -459,6 +470,9 @@ public class DeviceServiceImpl extends BaseServiceImpl entity.setUpdater(userId); entity.setAutoUpdate(1); baseDao.insert(entity); + + // 添加:清除智能体设备数量缓存 + redisUtils.delete(RedisKeys.getAgentDeviceCountById(dto.getAgentId())); } /** From b2dfbb79734dd440ecb6ed01685a602ab148a68d Mon Sep 17 00:00:00 2001 From: hrz <1710360675@qq.com> Date: Thu, 25 Sep 2025 16:12:02 +0800 Subject: [PATCH 27/27] =?UTF-8?q?update:=E4=BC=98=E5=8C=96=E5=88=A4?= =?UTF-8?q?=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../device/service/impl/DeviceServiceImpl.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceServiceImpl.java index 0dbe64c7..a79d1f2e 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceServiceImpl.java @@ -134,10 +134,10 @@ public class DeviceServiceImpl extends BaseServiceImpl // 清理redis缓存 redisUtils.delete(cacheDeviceKey); redisUtils.delete(deviceKey); - + // 添加:清除智能体设备数量缓存 redisUtils.delete(RedisKeys.getAgentDeviceCountById(agentId)); - + return true; } @@ -231,11 +231,14 @@ public class DeviceServiceImpl extends BaseServiceImpl public void unbindDevice(Long userId, String deviceId) { // 先查询设备信息,获取agentId DeviceEntity device = baseDao.selectById(deviceId); - if (device != null && StringUtils.isNotBlank(device.getAgentId())) { + if (device == null) { + return; + } + if (StringUtils.isNotBlank(device.getAgentId())) { // 清除智能体设备数量缓存 redisUtils.delete(RedisKeys.getAgentDeviceCountById(device.getAgentId())); } - + UpdateWrapper wrapper = new UpdateWrapper<>(); wrapper.eq("user_id", userId); wrapper.eq("id", deviceId); @@ -470,7 +473,7 @@ public class DeviceServiceImpl extends BaseServiceImpl entity.setUpdater(userId); entity.setAutoUpdate(1); baseDao.insert(entity); - + // 添加:清除智能体设备数量缓存 redisUtils.delete(RedisKeys.getAgentDeviceCountById(dto.getAgentId())); }