diff --git a/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java b/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java index 07d746b8..779fea83 100644 --- a/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java +++ b/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java @@ -177,5 +177,5 @@ public interface Constant { /** * 版本号 */ - public static final String VERSION = "0.3.13"; + public static final String VERSION = "0.3.14"; } \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentChatHistoryReportDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentChatHistoryReportDTO.java index 8227260d..bdaed950 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentChatHistoryReportDTO.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentChatHistoryReportDTO.java @@ -26,6 +26,6 @@ public class AgentChatHistoryReportDTO { @Schema(description = "聊天内容", example = "你好呀") @NotBlank private String content; - @Schema(description = "文件数据(opus编码)", example = "") - private String opusDataBase64; + @Schema(description = "base64编码的opus音频数据", example = "") + private String audioBase64; } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/biz/impl/AgentChatHistoryBizServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/biz/impl/AgentChatHistoryBizServiceImpl.java index edd96256..b839e9e4 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/biz/impl/AgentChatHistoryBizServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/biz/impl/AgentChatHistoryBizServiceImpl.java @@ -1,5 +1,7 @@ package xiaozhi.modules.agent.service.biz.impl; +import java.util.Base64; + import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -43,12 +45,11 @@ public class AgentChatHistoryBizServiceImpl implements AgentChatHistoryBizServic // 1. base64解码report.getOpusDataBase64(),存入ai_agent_chat_audio表 String audioId = null; - if (report.getOpusDataBase64() != null && !report.getOpusDataBase64().isEmpty()) { + if (report.getAudioBase64() != null && !report.getAudioBase64().isEmpty()) { try { - // TODO: 需要考虑保留什么格式的音频数据,比如是opus还是wave - // byte[] audioData = Base64.getDecoder().decode(report.getOpusDataBase64()); - // audioId = agentChatAudioService.saveAudio(audioData); - // log.info("音频数据保存成功,audioId={}", audioId); + byte[] audioData = Base64.getDecoder().decode(report.getAudioBase64()); + audioId = agentChatAudioService.saveAudio(audioData); + log.info("音频数据保存成功,audioId={}", audioId); } catch (Exception e) { log.error("音频数据保存失败", e); return false; diff --git a/main/xiaozhi-server/config/logger.py b/main/xiaozhi-server/config/logger.py index f20dc0ad..396260f4 100644 --- a/main/xiaozhi-server/config/logger.py +++ b/main/xiaozhi-server/config/logger.py @@ -4,7 +4,7 @@ from loguru import logger from config.config_loader import load_config from config.settings import check_config_file -SERVER_VERSION = "0.3.13" +SERVER_VERSION = "0.3.14" def get_module_abbreviation(module_name, module_dict): diff --git a/main/xiaozhi-server/config/manage_api_client.py b/main/xiaozhi-server/config/manage_api_client.py index 7de4c8fe..fe3da63c 100644 --- a/main/xiaozhi-server/config/manage_api_client.py +++ b/main/xiaozhi-server/config/manage_api_client.py @@ -146,24 +146,12 @@ def get_agent_models( def report( - mac_address: str, session_id: str, chat_type: int, content: str, opus_data + mac_address: str, session_id: str, chat_type: int, content: str, audio ) -> Optional[Dict]: """带熔断的业务方法示例""" if not content or not ManageApiClient._instance: return None try: - # 处理opus_data为列表的情况 - if isinstance(opus_data, list): - # 将列表中的所有bytes数据合并 - combined_data = b"".join(opus_data) - else: - combined_data = opus_data - - # 将二进制数据转换为Base64编码的字符串 - opus_data_base64 = ( - base64.b64encode(combined_data).decode("utf-8") if combined_data else None - ) - return ManageApiClient._instance._execute_request( "POST", f"/agent/chat-history/report", @@ -172,7 +160,9 @@ def report( "sessionId": session_id, "chatType": chat_type, "content": content, - "opusDataBase64": opus_data_base64, + "audioBase64": ( + base64.b64encode(audio).decode("utf-8") if audio else None + ), }, ) except Exception as e: diff --git a/main/xiaozhi-server/core/handle/ttsReportHandle.py b/main/xiaozhi-server/core/handle/ttsReportHandle.py index 3aec8a32..5442a79f 100644 --- a/main/xiaozhi-server/core/handle/ttsReportHandle.py +++ b/main/xiaozhi-server/core/handle/ttsReportHandle.py @@ -9,6 +9,11 @@ TTS上报功能已集成到ConnectionHandler类中。 具体实现请参考core/connection.py中的相关代码。 """ +import os +import uuid +import wave +import opuslib_next + from config.logger import setup_logging from config.manage_api_client import report @@ -26,18 +31,69 @@ def report_tts(conn, type, text, opus_data): opus_data: opus音频数据 """ try: + if opus_data: + audio_data = opus_to_wav(opus_data) + else: + audio_data = None # 执行上报 report( mac_address=conn.device_id, session_id=conn.session_id, chat_type=type, content=text, - opus_data=opus_data, + audio=audio_data, ) except Exception as e: logger.bind(tag=TAG).error(f"TTS上报失败: {e}") +def opus_to_wav(opus_data): + """将Opus数据转换为WAV格式的字节流 + + Args: + output_dir: 输出目录(保留参数以保持接口兼容) + opus_data: opus音频数据 + + Returns: + bytes: WAV格式的音频数据 + """ + decoder = opuslib_next.Decoder(16000, 1) # 16kHz, 单声道 + pcm_data = [] + + for opus_packet in opus_data: + try: + pcm_frame = decoder.decode(opus_packet, 960) # 960 samples = 60ms + pcm_data.append(pcm_frame) + except opuslib_next.OpusError as e: + logger.bind(tag=TAG).error(f"Opus解码错误: {e}", exc_info=True) + + if not pcm_data: + raise ValueError("没有有效的PCM数据") + + # 创建WAV文件头 + pcm_data_bytes = b"".join(pcm_data) + num_samples = len(pcm_data_bytes) // 2 # 16-bit samples + + # WAV文件头 + wav_header = bytearray() + wav_header.extend(b"RIFF") # ChunkID + wav_header.extend((36 + len(pcm_data_bytes)).to_bytes(4, "little")) # ChunkSize + wav_header.extend(b"WAVE") # Format + wav_header.extend(b"fmt ") # Subchunk1ID + wav_header.extend((16).to_bytes(4, "little")) # Subchunk1Size + wav_header.extend((1).to_bytes(2, "little")) # AudioFormat (PCM) + wav_header.extend((1).to_bytes(2, "little")) # NumChannels + wav_header.extend((16000).to_bytes(4, "little")) # SampleRate + wav_header.extend((32000).to_bytes(4, "little")) # ByteRate + wav_header.extend((2).to_bytes(2, "little")) # BlockAlign + wav_header.extend((16).to_bytes(2, "little")) # BitsPerSample + wav_header.extend(b"data") # Subchunk2ID + wav_header.extend(len(pcm_data_bytes).to_bytes(4, "little")) # Subchunk2Size + + # 返回完整的WAV数据 + return bytes(wav_header) + pcm_data_bytes + + def enqueue_tts_report(conn, type, text, opus_data): if not conn.read_config_from_api: return