From 4dac9de4b29603dd9766e04ec639e65d844e08df Mon Sep 17 00:00:00 2001 From: =?utf8?q?=E7=BD=97=E6=AD=A3?= <1732036298@qq.com> Date: Thu, 18 Sep 2025 02:40:07 +0000 Subject: [PATCH] Merge #2286 into sdk650pathc0423 from refs/changes/2286/head MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Code review from 罗正 * refs/changes/2286/head: (3 commits squashed) - 雷鸟接收线程优化 - AUDIO_STREAM_VOICE_RECORD音频流暂时改为AUDIO_STREAM_VOICE_ASSISTANT - Merge branch 'sdk650pathc0423' of codeup.aliyun.com:64191456b9581d62e8f81ac0/FD230 into sdk650pathc0423 Signed-off-by: 罗正 <1732036298@qq.com> Reviewed-by: snickersun@163.com Merged-by: snickersun@163.com CR-link: https://codeup.aliyun.com/64191456b9581d62e8f81ac0/FD230/change/2286 --- .../sample/wrapper/audio_capture_wrapper.cpp | 7 +- .../zhihui/leiniao/imsl_ln/leiniao_api_at.c | 4 +- code/sdk/zhihui/leiniao/imsl_ln/ln_api.c | 106 +++++++++++++++-- code/sdk/zhihui/leiniao/imsl_ln/ln_api.h | 28 +++++ .../leiniao/imsl_ln/ln_api_device_info.c | 108 ++++++++++++++++++ .../leiniao/imsl_ln/ln_api_speech_chat.c | 7 +- .../leiniao/imsl_ln/ln_api_story_choose.c | 5 + .../leiniao/imsl_ln/ln_api_story_random.c | 5 + .../device_websocket_client_6.py | 103 ++++++++++++++++- 9 files changed, 356 insertions(+), 17 deletions(-) diff --git a/code/sdk/middleware/services/media/foundation/sample/wrapper/audio_capture_wrapper.cpp b/code/sdk/middleware/services/media/foundation/sample/wrapper/audio_capture_wrapper.cpp index 3b3aefdba..ca7b1fb26 100644 --- a/code/sdk/middleware/services/media/foundation/sample/wrapper/audio_capture_wrapper.cpp +++ b/code/sdk/middleware/services/media/foundation/sample/wrapper/audio_capture_wrapper.cpp @@ -559,7 +559,9 @@ static int32_t ActivateAudioInterrupt(AudioSession &sessionId) } MEDIA_INFO_LOG("sessionId:%d", sessionId); g_captureInterruptListener = std::make_shared(); - g_sample.interrupt = { AUDIO_STREAM_VOICE_RECORD, sessionId, g_captureInterruptListener }; + // g_sample.interrupt = { AUDIO_STREAM_VOICE_RECORD, sessionId, g_captureInterruptListener }; + g_sample.interrupt = { AUDIO_STREAM_VOICE_ASSISTANT, sessionId, g_captureInterruptListener }; + if (g_amInstance.ActivateAudioInterrupt(g_sample.interrupt) == INTERRUPT_FAILED) { MEDIA_ERR_LOG("ActivateAudioInterrupt failed"); @@ -625,7 +627,8 @@ int32_t AudioCaptureSample(int32_t argc, const char **argv) } audioCapturerInfo.sessionID = sessionId; audioCapturerInfo.inputSource = AUDIO_MIC; - audioCapturerInfo.streamType = AUDIO_STREAM_VOICE_RECORD; + // audioCapturerInfo.streamType = AUDIO_STREAM_VOICE_RECORD; + audioCapturerInfo.streamType = AUDIO_STREAM_VOICE_ASSISTANT; ret = AudioCaptureSetInfo(audioCapturerInfo); if (ret != 0) { MEDIA_ERR_LOG("AudioCapture set failed"); diff --git a/code/sdk/zhihui/leiniao/imsl_ln/leiniao_api_at.c b/code/sdk/zhihui/leiniao/imsl_ln/leiniao_api_at.c index 6b682305d..53e385dca 100644 --- a/code/sdk/zhihui/leiniao/imsl_ln/leiniao_api_at.c +++ b/code/sdk/zhihui/leiniao/imsl_ln/leiniao_api_at.c @@ -708,7 +708,8 @@ static uint32_t ln_audio_pause_at(uint8_t *para, uint32_t paraLen, char *strComm { VAD_UNUSED(para); VAD_UNUSED(paraLen); VAD_UNUSED(strCommonCmd); printf("[LN][AUDIO_PAUSE]\n"); - do_ln_http_server_send_data(LN_HTTP_SERVICE_CMD_AUDIO_PAUSE, NULL, 0); + // do_ln_http_server_send_data(LN_HTTP_SERVICE_CMD_AUDIO_PAUSE, NULL, 0); + uapi_ln_audio_download_is_cancel(1); return ERRCODE_SUCC; } @@ -718,6 +719,7 @@ static uint32_t ln_audio_resume_at(uint8_t *para, uint32_t paraLen, char *strCom VAD_UNUSED(para); VAD_UNUSED(paraLen); VAD_UNUSED(strCommonCmd); printf("[LN][AUDIO_RESUME]\n"); do_ln_http_server_send_data(LN_HTTP_SERVICE_CMD_AUDIO_RESUME, NULL, 0); + uapi_ln_audio_download_is_cancel(0); return ERRCODE_SUCC; } diff --git a/code/sdk/zhihui/leiniao/imsl_ln/ln_api.c b/code/sdk/zhihui/leiniao/imsl_ln/ln_api.c index 741732397..9b9287f71 100644 --- a/code/sdk/zhihui/leiniao/imsl_ln/ln_api.c +++ b/code/sdk/zhihui/leiniao/imsl_ln/ln_api.c @@ -162,6 +162,54 @@ const char* uapi_ln_get_user_id(void) return g_ln_device_user_id; } +/* 获取设备角色id */ +const char *uapi_ln_get_character_id(void) +{ + return g_ln_character_id; +} + +/* 获取设备角色名称 */ +const char *uapi_ln_get_character_name(void) +{ + return g_ln_character_name; +} + +/* 获取设备角色眼睛形状 */ +const char *uapi_ln_get_character_eyes_shape(void) +{ + return g_ln_avatar_eyes_shape; +} + +/* 获取设备角色头发颜色 */ +const char *uapi_ln_get_character_hair_color(void) +{ + return g_ln_avatar_hair_color; +} + +/* 获取设备角色眼睛颜色 */ +const char *uapi_ln_get_character_eyes_color(void) +{ + return g_ln_avatar_eyes_color; +} + +/* 获取设备角色头发样式 */ +const char *uapi_ln_get_character_hair_style(void) +{ + return g_ln_avatar_hair_style; +} + +/* 获取设备角色屏幕图片 */ +const char *uapi_ln_get_character_screen_image(void) +{ + return g_ln_avatar_screen_image; +} + +/* 获取设备角色屏幕图片是否为表情,默认false */ +int uapi_ln_get_character_screen_image_as_emoji(void) +{ + return g_ln_avatar_screen_image_as_emoji; +} + /* 自适应申请HttpClient缓冲(保证header>=2048),失败则逐步扩容重试 */ HTTPC_RESULT ln_http_prepare_auto(HttpClientData *clientData, int header_size, int resp_size) { @@ -201,18 +249,28 @@ static int ln_calc_file_md5_lower_hex(const char *file_path, char out_hex[33]) mbedtls_md5_free(&ctx); return -1; } - unsigned char buf[4096]; + /* 使用堆缓冲,避免在调用栈上占用4KB,降低线程峰值栈使用 */ + const size_t kBufSize = 4096; + unsigned char *buf = (unsigned char *)malloc(kBufSize); + if (!buf) + { + fclose(fp); + mbedtls_md5_free(&ctx); + return -1; + } size_t n; - while ((n = fread(buf, 1, sizeof(buf), fp)) > 0) + while ((n = fread(buf, 1, kBufSize, fp)) > 0) { if (mbedtls_md5_update(&ctx, buf, n) != 0) { fclose(fp); + free(buf); mbedtls_md5_free(&ctx); return -1; } } fclose(fp); + free(buf); if (mbedtls_md5_finish(&ctx, md5_ctx_buf) != 0) { mbedtls_md5_free(&ctx); @@ -616,7 +674,7 @@ static uint32_t ln_download_mp3_file_advanced(const char *audio_url, const char do { if (g_ln_audio_download_cancel) { - printf("[SSE] download canceled by user\n"); + printf("download canceled by user\n"); break; } HTTPC_RESULT recv_res = HttpClientRecvResponse(&client, &client_data); @@ -643,6 +701,11 @@ static uint32_t ln_download_mp3_file_advanced(const char *audio_url, const char (void)ln_audio_player_write((const uint8_t*)client_data.responseBuf, (uint32_t)client_data.contentBlockLen); } + else + { + /* EAGAIN 或 chunk 分界但无数据时,让出CPU避免忙等 */ + LOS_Msleep(10); + } } else { @@ -879,6 +942,11 @@ uint32_t ln_parse_file_and_process_data(const char *file_path, int interface_typ int success_count = 0; int error_count = 0; + if(g_ln_audio_download_cancel)//如果之前中断了播放,新的对话就恢复播放 + { + uapi_ln_audio_download_is_cancel(0); + } + while (fgets(line, sizeof(line), fp)) { if (g_ln_audio_download_cancel) { @@ -956,10 +1024,13 @@ static void ln_http_server_send_task(void const *arg) while (1) { + /* 每轮重置 msgsize,兼容底层可能改写该值的实现 */ + msgsize = sizeof(ln_http_data_mail_t); ret = osal_msg_queue_read_copy(g_ln_send_queue, (void *)&mail, &msgsize, OSAL_MSGQ_WAIT_FOREVER); if (ret != OSAL_SUCCESS) { + LOS_Msleep(100); continue; } if (mail.cmd == LN_HTTP_SERVICE_CMD_GET_DEVICE_TOKEN) @@ -1124,10 +1195,14 @@ static void ln_http_server_recv_task(void const *arg) while (1) { + /* 某些OSAL实现会在read后改写msgsize,循环开始时重置,避免下次读取异常 */ + msgsize = sizeof(ln_http_data_mail_t); ret = osal_msg_queue_read_copy(g_ln_recv_queue, (void *)&mail, &msgsize, OSAL_MSGQ_WAIT_FOREVER); if (ret != OSAL_SUCCESS) { + printf("[LN][RECV] queue read error=0x%x\n", ret); + LOS_Msleep(100); continue; } @@ -1145,6 +1220,11 @@ static void ln_http_server_recv_task(void const *arg) tag = "STORY_FINISH"; } else if (mail.cmd == LN_HTTP_RECV_DEVICE_CHARACTER) { tag = "DEVICE_CHARACTER"; + //解析角色配置信息 + if(mail.data) + { + ln_parse_character_config(mail.data, (int)mail.len); + } } else if (mail.cmd == LN_HTTP_RECV_DEVICE_USER_ID) { tag = "DEVICE_USER_ID"; } else if (mail.cmd == LN_HTTP_RECV_OTA_TASK) { @@ -1158,11 +1238,21 @@ static void ln_http_server_recv_task(void const *arg) /* 在收到 OTA_DOWNLOAD_MSG 后,解析状态并回调处理(进度/完成/失败) */ if (mail.data) { - OtaDownloadMsg m = {0}; - if (parse_ota_download_msg(mail.data, &m) == 0) { - handle_ota_download_msg(&m); - } else { - printf("OTA_DOWNLOAD_MSG: parse failed\n"); + /* 改为堆分配,进一步降低接收线程的峰值栈占用 */ + OtaDownloadMsg *msg_ptr = (OtaDownloadMsg *)malloc(sizeof(OtaDownloadMsg)); + if (msg_ptr) + { + memset(msg_ptr, 0, sizeof(OtaDownloadMsg)); + if (parse_ota_download_msg(mail.data, msg_ptr) == 0) { + handle_ota_download_msg(msg_ptr); + } else { + printf("OTA_DOWNLOAD_MSG: parse failed\n"); + } + free(msg_ptr); + } + else + { + printf("OTA_DOWNLOAD_MSG: alloc failed\n"); } } } diff --git a/code/sdk/zhihui/leiniao/imsl_ln/ln_api.h b/code/sdk/zhihui/leiniao/imsl_ln/ln_api.h index 965f7b857..ace818696 100644 --- a/code/sdk/zhihui/leiniao/imsl_ln/ln_api.h +++ b/code/sdk/zhihui/leiniao/imsl_ln/ln_api.h @@ -166,6 +166,23 @@ void uapi_ln_audio_download_is_cancel(int status); uint32_t uapi_ln_is_device_bound(void); /* 获取设备绑定用户ID */ const char* uapi_ln_get_user_id(void); +/* 获取设备角色id */ +const char *uapi_ln_get_character_id(void); +/* 获取设备角色名称 */ +const char *uapi_ln_get_character_name(void); +/* 获取设备角色眼睛形状 */ +const char *uapi_ln_get_character_eyes_shape(void); +/* 获取设备角色头发颜色 */ +const char *uapi_ln_get_character_hair_color(void); +/* 获取设备角色眼睛颜色 */ +const char *uapi_ln_get_character_eyes_color(void); +/* 获取设备角色头发样式 */ +const char *uapi_ln_get_character_hair_style(void); +/* 获取设备角色屏幕图片 */ +const char *uapi_ln_get_character_screen_image(void); +/* 获取设备角色屏幕图片是否为表情 */ +int uapi_ln_get_character_screen_image_as_emoji(void); + /* ============================== 共享全局状态(跨文件) ============================== */ extern char *g_device_token; @@ -175,6 +192,15 @@ extern char g_ln_last_package_url[512]; extern char g_ln_last_target_version[64]; extern char g_ln_last_package_md5[65]; extern int g_ln_last_package_size; +/* 设备角色信息(由 ln_parse_character_config 填充)*/ +extern char g_ln_character_id[64]; //角色id +extern char g_ln_character_name[64]; //角色名称 +extern char g_ln_avatar_eyes_shape[64]; //眼睛形状 +extern char g_ln_avatar_hair_color[64]; //头发颜色 +extern char g_ln_avatar_eyes_color[64]; //眼睛颜色 +extern char g_ln_avatar_hair_style[64]; //发型 +extern char g_ln_avatar_screen_image[256]; //屏幕图片 +extern int g_ln_avatar_screen_image_as_emoji; //屏幕图片是否为表情 /* ============================== 公共工具函数(供拆分后的模块复用) ============================== */ @@ -189,6 +215,8 @@ uint32_t ln_token_is_expired(int *expired_out); uint32_t uapi_ln_refresh_device_token(void); uint32_t uapi_ln_device_get_user_id(LnDeviceBindStatus *out_status, char *out_user_id, uint32_t out_user_id_len); +//解析设备角色配置 +void ln_parse_character_config(const char *json_buf, int json_len); /* ============================== 各接口函数声明 ============================== */ uint32_t ln_get_device_token_http(const char *device_id, const char *device_iccid, diff --git a/code/sdk/zhihui/leiniao/imsl_ln/ln_api_device_info.c b/code/sdk/zhihui/leiniao/imsl_ln/ln_api_device_info.c index 949d38364..05d5a5518 100644 --- a/code/sdk/zhihui/leiniao/imsl_ln/ln_api_device_info.c +++ b/code/sdk/zhihui/leiniao/imsl_ln/ln_api_device_info.c @@ -3,6 +3,114 @@ #include #include +/* =================== 设备角色信息存储 =================== */ +char g_ln_character_id[64] = {0}; +char g_ln_character_name[64] = {0}; +char g_ln_avatar_hair_color[64] = {0}; +char g_ln_avatar_eyes_color[64] = {0}; +char g_ln_avatar_hair_style[64] = {0}; +char g_ln_avatar_eyes_shape[64] = {0}; +char g_ln_avatar_screen_image[256] = {0}; +int g_ln_avatar_screen_image_as_emoji = 0; + +/* 安全写入帮助函数 */ +static inline void ln_safe_strcpy(char *dst, size_t dst_size, const char *src) +{ + if (!dst || dst_size == 0) return; + if (src == NULL) { + dst[0] = '\0'; + return; + } + strncpy(dst, src, dst_size - 1); + dst[dst_size - 1] = '\0'; +} + +/* 解析设备角色配置 JSON 并保存到全局变量 */ +void ln_parse_character_config(const char *json_buf, int json_len) +{ + if (!json_buf || json_len <= 0) + { + printf("[DEVICE_CHARACTER] invalid json input\n"); + return; + } + + char *text = (char *)malloc((size_t)json_len + 1); + if (!text) return; + memcpy(text, json_buf, (size_t)json_len); + text[json_len] = '\0'; + + cJSON *root = cJSON_Parse(text); + if (!root) + { + printf("[DEVICE_CHARACTER] json parse failed\n"); + free(text); + return; + } + + cJSON *errorCode = cJSON_GetObjectItem(root, "errorCode"); + if (errorCode && !cJSON_IsNull(errorCode)) + { + printf("[DEVICE_CHARACTER] server error: %s\n", + cJSON_IsString(errorCode) ? errorCode->valuestring : "unknown"); + cJSON_Delete(root); + free(text); + return; + } + + cJSON *data = cJSON_GetObjectItem(root, "data"); + if (!data || !cJSON_IsObject(data)) + { + printf("[DEVICE_CHARACTER] no data object\n"); + cJSON_Delete(root); + free(text); + return; + } + + /* 基础字段 */ + cJSON *cid = cJSON_GetObjectItem(data, "characterId"); + cJSON *cname = cJSON_GetObjectItem(data, "characterName"); + ln_safe_strcpy(g_ln_character_id, sizeof(g_ln_character_id), (cid && cJSON_IsString(cid)) ? cid->valuestring : ""); + ln_safe_strcpy(g_ln_character_name, sizeof(g_ln_character_name), (cname && cJSON_IsString(cname)) ? cname->valuestring : ""); + + /* 头像配置 */ + cJSON *avatar = cJSON_GetObjectItem(data, "characterAvatar"); + if (avatar && cJSON_IsObject(avatar)) + { + cJSON *hairColor = cJSON_GetObjectItem(avatar, "hairColor"); + cJSON *eyesColor = cJSON_GetObjectItem(avatar, "eyesColor"); + cJSON *hairStyle = cJSON_GetObjectItem(avatar, "hairStyle"); + cJSON *eyesShape = cJSON_GetObjectItem(avatar, "eyesShape"); + cJSON *screenImage = cJSON_GetObjectItem(avatar, "screenImage"); + cJSON *screenImageAsEmoji = cJSON_GetObjectItem(avatar, "screenImageAsEmoji"); + + ln_safe_strcpy(g_ln_avatar_hair_color, sizeof(g_ln_avatar_hair_color), (hairColor && cJSON_IsString(hairColor)) ? hairColor->valuestring : ""); + ln_safe_strcpy(g_ln_avatar_eyes_color, sizeof(g_ln_avatar_eyes_color), (eyesColor && cJSON_IsString(eyesColor)) ? eyesColor->valuestring : ""); + ln_safe_strcpy(g_ln_avatar_hair_style, sizeof(g_ln_avatar_hair_style), (hairStyle && cJSON_IsString(hairStyle)) ? hairStyle->valuestring : ""); + ln_safe_strcpy(g_ln_avatar_eyes_shape, sizeof(g_ln_avatar_eyes_shape), (eyesShape && cJSON_IsString(eyesShape)) ? eyesShape->valuestring : ""); + ln_safe_strcpy(g_ln_avatar_screen_image, sizeof(g_ln_avatar_screen_image), (screenImage && cJSON_IsString(screenImage)) ? screenImage->valuestring : ""); + g_ln_avatar_screen_image_as_emoji = (screenImageAsEmoji && cJSON_IsBool(screenImageAsEmoji)) ? (cJSON_IsTrue(screenImageAsEmoji) ? 1 : 0) : 0; + } + else + { + /* 缺省时清空 */ + g_ln_avatar_hair_color[0] = '\0'; + g_ln_avatar_eyes_color[0] = '\0'; + g_ln_avatar_hair_style[0] = '\0'; + g_ln_avatar_eyes_shape[0] = '\0'; + g_ln_avatar_screen_image[0] = '\0'; + g_ln_avatar_screen_image_as_emoji = 0; + } + + printf("[DEVICE_CHARACTER] parsed: id=%s\n name=%s\n hairColor=%s\n eyesColor=%s\n hairStyle=%s\n eyesShape=%s\n screenImage=%s\n asEmoji=%d\n", + g_ln_character_id, g_ln_character_name, + g_ln_avatar_hair_color, g_ln_avatar_eyes_color, + g_ln_avatar_hair_style, g_ln_avatar_eyes_shape, + g_ln_avatar_screen_image, g_ln_avatar_screen_image_as_emoji); + + cJSON_Delete(root); + free(text); +} + // 获取设备角色信息 uint32_t ln_device_get_character_http(const char *device_id, const char *device_token) diff --git a/code/sdk/zhihui/leiniao/imsl_ln/ln_api_speech_chat.c b/code/sdk/zhihui/leiniao/imsl_ln/ln_api_speech_chat.c index 03c22e2f2..2283a0304 100644 --- a/code/sdk/zhihui/leiniao/imsl_ln/ln_api_speech_chat.c +++ b/code/sdk/zhihui/leiniao/imsl_ln/ln_api_speech_chat.c @@ -165,7 +165,7 @@ uint32_t ln_speech_chat_stream_http(const char *device_id, printf("chatstream header_buf: %s\n", header_buf); /* 准备缓冲区(自适应) */ - HTTPC_RESULT prep = ln_http_prepare_auto(&clientData, 2048, LN_CLIENT_PREPARE_SIZE); + HTTPC_RESULT prep = ln_http_prepare_auto(&clientData, 2048, LN_CLIENT_PREPARE_SIZE*2); if (prep != HTTP_SUCCESS) { free(body); @@ -240,6 +240,11 @@ uint32_t ln_speech_chat_stream_http(const char *device_id, } total_written += written; } + else + { + /* 无新数据块但仍需继续时,适当让出CPU降低忙等 */ + LOS_Msleep(10); + } } else { diff --git a/code/sdk/zhihui/leiniao/imsl_ln/ln_api_story_choose.c b/code/sdk/zhihui/leiniao/imsl_ln/ln_api_story_choose.c index 54a05322b..1f2ecac1a 100644 --- a/code/sdk/zhihui/leiniao/imsl_ln/ln_api_story_choose.c +++ b/code/sdk/zhihui/leiniao/imsl_ln/ln_api_story_choose.c @@ -237,6 +237,11 @@ uint32_t ln_story_choose_http(const char *device_id, } total_written += written; } + else + { + /* 无新数据块但仍需继续时,适当让出CPU降低忙等 */ + LOS_Msleep(10); + } } else { diff --git a/code/sdk/zhihui/leiniao/imsl_ln/ln_api_story_random.c b/code/sdk/zhihui/leiniao/imsl_ln/ln_api_story_random.c index 281bbab74..2c48113cd 100644 --- a/code/sdk/zhihui/leiniao/imsl_ln/ln_api_story_random.c +++ b/code/sdk/zhihui/leiniao/imsl_ln/ln_api_story_random.c @@ -170,6 +170,11 @@ uint32_t ln_story_random_http(const char *device_id, } total_written += written; } + else + { + /* 无新数据块但仍需继续时,适当让出CPU降低忙等 */ + LOS_Msleep(10); + } } else { diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/device_websocket_client_6.py b/code/sdk/zhihui/leiniao/leiniao_python_test/device_websocket_client_6.py index 54d961839..7236dbee9 100644 --- a/code/sdk/zhihui/leiniao/leiniao_python_test/device_websocket_client_6.py +++ b/code/sdk/zhihui/leiniao/leiniao_python_test/device_websocket_client_6.py @@ -11,6 +11,8 @@ import hmac import random import string import logging +import uuid +import base64 from enum import Enum # 配置日志 @@ -23,8 +25,8 @@ logger = logging.getLogger(__name__) # 常量定义 class Constants(Enum): - # WS_BASE_URL = "wss://ambybox-api.test.leiniao.com/ambybox-device-api/v1/websocket" - WS_BASE_URL = "ws://127.0.0.1:8765" # 临时本地测试,到时候需要替换为正式环境 + WS_BASE_URL = "wss://ambybox-api.test.leiniao.com/ambybox-device-api/v1/websocket" + # WS_BASE_URL = "ws://127.0.0.1:8765" # 临时本地测试,到时候需要替换为正式环境 TOKEN_API_URL = "https://ambybox-api.test.leiniao.com/ambybox-device-api/v1/auth/getDeviceToken" UPGRADE_TEST_URL = "https://ambybox-api.test.leiniao.com/ambybox-device-api/v1/upgrade/getTask" SIGN_SECRET = "vDdXVXgPUEUM6_piUjz5AEhMvYTSFzQdPaN-ogKnSus=" @@ -34,6 +36,7 @@ class Constants(Enum): HEARTBEAT_TIMEOUT = 10 # 心跳超时(秒) CONNECT_TIMEOUT = 20 # 连接超时(秒) MAX_RECONNECT_DELAY = 60 # 最大重连延迟(秒) + AUDIO_CHUNK_SIZE = 8 * 1024 # 音频分片大小(8KB) class DeviceWebSocketClient: @@ -59,7 +62,8 @@ class DeviceWebSocketClient: "SleepModeEnd": self.handle_sleep_mode_end, "DeviceBind": self.handle_device_bind, "DeviceUnbind": self.handle_device_unbind, - "ScheduleReminder": self.handle_schedule_reminder + "ScheduleReminder": self.handle_schedule_reminder, + "VoiceChatResponse": self.handle_voice_chat_response # 新增语音响应处理器 } # 线程相关 @@ -182,7 +186,7 @@ class DeviceWebSocketClient: self.reconnect_count = 0 self._start_heartbeat() - # 发送上线通知(修正缩进和变量名) + # 发送上线通知 online_msg = json.dumps({"type": "online", "deviceId": self.device_id, "timestamp": int(time.time())}) self.ws.send(online_msg) print(f"📤 已发送上线消息: {online_msg}") @@ -303,7 +307,92 @@ class DeviceWebSocketClient: self.connected = False logger.info("WebSocket客户端已关闭") - # 命令处理器实现 + # 新增:语音对话分片发送功能 + def send_voice_chat_request(self, audio_file_path: str) -> bool: + """ + 发送语音对话请求(支持分片传输) + + Args: + audio_file_path: 音频文件本地路径 + + Returns: + 是否成功启动分片发送(不保证所有分片都发送成功) + """ + try: + # 1. 读取音频文件二进制数据 + with open(audio_file_path, 'rb') as f: + audio_bytes = f.read() + total_size = len(audio_bytes) + if total_size == 0: + logger.error("音频文件为空,无法发送") + return False + + # 2. 生成对话唯一标识(UUIDv4) + chat_id = str(uuid.uuid4()) + logger.info(f"开始处理语音对话,chatId: {chat_id},文件大小: {total_size}字节") + + # 3. 分片配置(8KB原始数据/分片) + chunk_size = Constants.AUDIO_CHUNK_SIZE.value + total_chunks = (total_size + chunk_size - 1) // chunk_size # 向上取整计算总分片数 + + # 4. 循环发送分片 + for chunk_idx in range(total_chunks): + # 计算当前分片的起止位置 + start = chunk_idx * chunk_size + end = min(start + chunk_size, total_size) + current_chunk = audio_bytes[start:end] + + # 5. 音频分片Base64编码 + audio_base64 = base64.b64encode(current_chunk).decode('utf-8') + + # 6. 构造分片消息 + message = { + "deviceId": self.device_id, + "timestamp": int(time.time()), + "messageId": f"voice_{chat_id}_{chunk_idx}", # 分片唯一ID + "command": "VoiceChatRequest", + "data": { + "audioBase64": audio_base64, + "chatId": chat_id, + "finished": (chunk_idx == total_chunks - 1) # 最后一个分片标记为完成 + } + } + + # 7. 发送分片(调用现有发送方法) + if not self.send_message(json.dumps(message)): + logger.error(f"第{chunk_idx + 1}/{total_chunks}个分片发送失败,终止传输") + return False + + logger.info(f"已发送分片 {chunk_idx + 1}/{total_chunks} " + f"(大小: {len(current_chunk)}字节,是否最后一片: {message['data']['finished']})") + + # 可选:添加微小延迟避免服务器压力 + time.sleep(0.05) + + logger.info(f"所有分片发送完成,共{total_chunks}个分片,chatId: {chat_id}") + return True + + except FileNotFoundError: + logger.error(f"音频文件不存在: {audio_file_path}") + except Exception as e: + logger.error(f"语音分片发送失败: {str(e)}") + return False + + # 新增:语音对话响应处理 + def handle_voice_chat_response(self, data: Dict[str, Any]) -> None: + """处理服务端返回的语音对话响应""" + logger.info("收到语音对话响应 (VoiceChatResponse)") + response_data = data.get("data", {}) + chat_id = response_data.get("chatId") + finished = response_data.get("finished", False) + content_data = response_data.get("data", {}) + + logger.info(f"对话ID: {chat_id}") + logger.info(f"是否处理完成: {finished}") + logger.info(f"文本响应: {content_data.get('textContent', '无')}") + logger.info(f"音频响应URL: {content_data.get('audioUrl', '无')}") + + # 原有命令处理器实现 def handle_character_change(self, data: Dict[str, Any]) -> None: logger.info("收到角色切换命令") command_data = data.get("data", {}) @@ -401,6 +490,10 @@ def main() -> NoReturn: return try: + # 示例:连接成功后发送测试音频(替换为实际音频文件路径) + time.sleep(2) # 等待连接稳定 + # ws_client.send_voice_chat_request("test_audio.mp3") # 取消注释并替换为实际音频路径 + while True: time.sleep(1) except KeyboardInterrupt: -- 2.17.1