mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-25 16:43:55 +08:00
update:上报聊天音频
This commit is contained in:
@@ -177,5 +177,5 @@ public interface Constant {
|
|||||||
/**
|
/**
|
||||||
* 版本号
|
* 版本号
|
||||||
*/
|
*/
|
||||||
public static final String VERSION = "0.3.13";
|
public static final String VERSION = "0.3.14";
|
||||||
}
|
}
|
||||||
+2
-2
@@ -26,6 +26,6 @@ public class AgentChatHistoryReportDTO {
|
|||||||
@Schema(description = "聊天内容", example = "你好呀")
|
@Schema(description = "聊天内容", example = "你好呀")
|
||||||
@NotBlank
|
@NotBlank
|
||||||
private String content;
|
private String content;
|
||||||
@Schema(description = "文件数据(opus编码)", example = "")
|
@Schema(description = "base64编码的opus音频数据", example = "")
|
||||||
private String opusDataBase64;
|
private String audioBase64;
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-5
@@ -1,5 +1,7 @@
|
|||||||
package xiaozhi.modules.agent.service.biz.impl;
|
package xiaozhi.modules.agent.service.biz.impl;
|
||||||
|
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@@ -43,12 +45,11 @@ public class AgentChatHistoryBizServiceImpl implements AgentChatHistoryBizServic
|
|||||||
|
|
||||||
// 1. base64解码report.getOpusDataBase64(),存入ai_agent_chat_audio表
|
// 1. base64解码report.getOpusDataBase64(),存入ai_agent_chat_audio表
|
||||||
String audioId = null;
|
String audioId = null;
|
||||||
if (report.getOpusDataBase64() != null && !report.getOpusDataBase64().isEmpty()) {
|
if (report.getAudioBase64() != null && !report.getAudioBase64().isEmpty()) {
|
||||||
try {
|
try {
|
||||||
// TODO: 需要考虑保留什么格式的音频数据,比如是opus还是wave
|
byte[] audioData = Base64.getDecoder().decode(report.getAudioBase64());
|
||||||
// byte[] audioData = Base64.getDecoder().decode(report.getOpusDataBase64());
|
audioId = agentChatAudioService.saveAudio(audioData);
|
||||||
// audioId = agentChatAudioService.saveAudio(audioData);
|
log.info("音频数据保存成功,audioId={}", audioId);
|
||||||
// log.info("音频数据保存成功,audioId={}", audioId);
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("音频数据保存失败", e);
|
log.error("音频数据保存失败", e);
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from loguru import logger
|
|||||||
from config.config_loader import load_config
|
from config.config_loader import load_config
|
||||||
from config.settings import check_config_file
|
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):
|
def get_module_abbreviation(module_name, module_dict):
|
||||||
|
|||||||
@@ -146,24 +146,12 @@ def get_agent_models(
|
|||||||
|
|
||||||
|
|
||||||
def report(
|
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]:
|
) -> Optional[Dict]:
|
||||||
"""带熔断的业务方法示例"""
|
"""带熔断的业务方法示例"""
|
||||||
if not content or not ManageApiClient._instance:
|
if not content or not ManageApiClient._instance:
|
||||||
return None
|
return None
|
||||||
try:
|
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(
|
return ManageApiClient._instance._execute_request(
|
||||||
"POST",
|
"POST",
|
||||||
f"/agent/chat-history/report",
|
f"/agent/chat-history/report",
|
||||||
@@ -172,7 +160,9 @@ def report(
|
|||||||
"sessionId": session_id,
|
"sessionId": session_id,
|
||||||
"chatType": chat_type,
|
"chatType": chat_type,
|
||||||
"content": content,
|
"content": content,
|
||||||
"opusDataBase64": opus_data_base64,
|
"audioBase64": (
|
||||||
|
base64.b64encode(audio).decode("utf-8") if audio else None
|
||||||
|
),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -9,6 +9,11 @@ TTS上报功能已集成到ConnectionHandler类中。
|
|||||||
具体实现请参考core/connection.py中的相关代码。
|
具体实现请参考core/connection.py中的相关代码。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import uuid
|
||||||
|
import wave
|
||||||
|
import opuslib_next
|
||||||
|
|
||||||
from config.logger import setup_logging
|
from config.logger import setup_logging
|
||||||
from config.manage_api_client import report
|
from config.manage_api_client import report
|
||||||
|
|
||||||
@@ -26,18 +31,69 @@ def report_tts(conn, type, text, opus_data):
|
|||||||
opus_data: opus音频数据
|
opus_data: opus音频数据
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
if opus_data:
|
||||||
|
audio_data = opus_to_wav(opus_data)
|
||||||
|
else:
|
||||||
|
audio_data = None
|
||||||
# 执行上报
|
# 执行上报
|
||||||
report(
|
report(
|
||||||
mac_address=conn.device_id,
|
mac_address=conn.device_id,
|
||||||
session_id=conn.session_id,
|
session_id=conn.session_id,
|
||||||
chat_type=type,
|
chat_type=type,
|
||||||
content=text,
|
content=text,
|
||||||
opus_data=opus_data,
|
audio=audio_data,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.bind(tag=TAG).error(f"TTS上报失败: {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):
|
def enqueue_tts_report(conn, type, text, opus_data):
|
||||||
if not conn.read_config_from_api:
|
if not conn.read_config_from_api:
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user