mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-27 01:23:55 +08:00
update:python单模块部署声纹识别对接
This commit is contained in:
@@ -137,6 +137,15 @@ plugins:
|
|||||||
- ".wav"
|
- ".wav"
|
||||||
- ".p3"
|
- ".p3"
|
||||||
refresh_time: 300 # 刷新音乐列表的时间间隔,单位为秒
|
refresh_time: 300 # 刷新音乐列表的时间间隔,单位为秒
|
||||||
|
# 声纹识别配置
|
||||||
|
voiceprint:
|
||||||
|
# 声纹接口地址
|
||||||
|
url:
|
||||||
|
# 说话人配置:speaker_id,名称,描述
|
||||||
|
speakers:
|
||||||
|
- "test1,张三,张三是一个程序员"
|
||||||
|
- "test2,李四,李四是一个产品经理"
|
||||||
|
- "test3,王五,王五是一个设计师"
|
||||||
|
|
||||||
# #####################################################################################
|
# #####################################################################################
|
||||||
# ################################以下是角色模型配置######################################
|
# ################################以下是角色模型配置######################################
|
||||||
|
|||||||
@@ -595,8 +595,33 @@ class ConnectionHandler:
|
|||||||
self.logger.bind(tag=TAG).info(f"大模型收到用户消息: {query}")
|
self.logger.bind(tag=TAG).info(f"大模型收到用户消息: {query}")
|
||||||
self.llm_finish_task = False
|
self.llm_finish_task = False
|
||||||
|
|
||||||
|
# 检查是否是JSON格式的消息(包含说话人信息)
|
||||||
|
enhanced_query = query
|
||||||
|
try:
|
||||||
|
if query.strip().startswith('{') and query.strip().endswith('}'):
|
||||||
|
data = json.loads(query)
|
||||||
|
if 'speaker' in data and 'content' in data:
|
||||||
|
# 直接使用JSON格式,不重新格式化
|
||||||
|
enhanced_query = query
|
||||||
|
self.logger.bind(tag=TAG).info(f"识别到说话人: {data['speaker']}")
|
||||||
|
else:
|
||||||
|
# 如果有说话人信息但不是JSON格式,按原逻辑处理
|
||||||
|
if hasattr(self, 'current_speaker') and self.current_speaker:
|
||||||
|
enhanced_query = f"[说话人: {self.current_speaker}] {query}"
|
||||||
|
self.logger.bind(tag=TAG).info(f"识别到说话人: {self.current_speaker}")
|
||||||
|
else:
|
||||||
|
# 如果有说话人信息但不是JSON格式,按原逻辑处理
|
||||||
|
if hasattr(self, 'current_speaker') and self.current_speaker:
|
||||||
|
enhanced_query = f"[说话人: {self.current_speaker}] {query}"
|
||||||
|
self.logger.bind(tag=TAG).info(f"识别到说话人: {self.current_speaker}")
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
# JSON解析失败,按原逻辑处理
|
||||||
|
if hasattr(self, 'current_speaker') and self.current_speaker:
|
||||||
|
enhanced_query = f"[说话人: {self.current_speaker}] {query}"
|
||||||
|
self.logger.bind(tag=TAG).info(f"识别到说话人: {self.current_speaker}")
|
||||||
|
|
||||||
if not tool_call:
|
if not tool_call:
|
||||||
self.dialogue.put(Message(role="user", content=query))
|
self.dialogue.put(Message(role="user", content=enhanced_query))
|
||||||
|
|
||||||
# Define intent functions
|
# Define intent functions
|
||||||
functions = None
|
functions = None
|
||||||
@@ -609,7 +634,7 @@ class ConnectionHandler:
|
|||||||
memory_str = None
|
memory_str = None
|
||||||
if self.memory is not None:
|
if self.memory is not None:
|
||||||
future = asyncio.run_coroutine_threadsafe(
|
future = asyncio.run_coroutine_threadsafe(
|
||||||
self.memory.query_memory(query), self.loop
|
self.memory.query_memory(enhanced_query), self.loop
|
||||||
)
|
)
|
||||||
memory_str = future.result()
|
memory_str = future.result()
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from core.utils.output_counter import check_device_output_limit
|
|||||||
from core.handle.abortHandle import handleAbortMessage
|
from core.handle.abortHandle import handleAbortMessage
|
||||||
import time
|
import time
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import json
|
||||||
from core.handle.sendAudioHandle import SentenceType
|
from core.handle.sendAudioHandle import SentenceType
|
||||||
from core.utils.util import audio_to_data
|
from core.utils.util import audio_to_data
|
||||||
|
|
||||||
@@ -38,6 +39,31 @@ async def resume_vad_detection(conn):
|
|||||||
|
|
||||||
|
|
||||||
async def startToChat(conn, text):
|
async def startToChat(conn, text):
|
||||||
|
# 检查输入是否是JSON格式(包含说话人信息)
|
||||||
|
speaker_name = None
|
||||||
|
actual_text = text
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 尝试解析JSON格式的输入
|
||||||
|
if text.strip().startswith('{') and text.strip().endswith('}'):
|
||||||
|
data = json.loads(text)
|
||||||
|
if 'speaker' in data and 'content' in data:
|
||||||
|
speaker_name = data['speaker']
|
||||||
|
actual_text = data['content']
|
||||||
|
conn.logger.bind(tag=TAG).info(f"解析到说话人信息: {speaker_name}")
|
||||||
|
|
||||||
|
# 直接使用JSON格式的文本,不解析
|
||||||
|
actual_text = text
|
||||||
|
except (json.JSONDecodeError, KeyError):
|
||||||
|
# 如果解析失败,继续使用原始文本
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 保存说话人信息到连接对象
|
||||||
|
if speaker_name:
|
||||||
|
conn.current_speaker = speaker_name
|
||||||
|
else:
|
||||||
|
conn.current_speaker = None
|
||||||
|
|
||||||
if conn.need_bind:
|
if conn.need_bind:
|
||||||
await check_bind_device(conn)
|
await check_bind_device(conn)
|
||||||
return
|
return
|
||||||
@@ -52,16 +78,16 @@ async def startToChat(conn, text):
|
|||||||
if conn.client_is_speaking:
|
if conn.client_is_speaking:
|
||||||
await handleAbortMessage(conn)
|
await handleAbortMessage(conn)
|
||||||
|
|
||||||
# 首先进行意图分析
|
# 首先进行意图分析,使用实际文本内容
|
||||||
intent_handled = await handle_user_intent(conn, text)
|
intent_handled = await handle_user_intent(conn, actual_text)
|
||||||
|
|
||||||
if intent_handled:
|
if intent_handled:
|
||||||
# 如果意图已被处理,不再进行聊天
|
# 如果意图已被处理,不再进行聊天
|
||||||
return
|
return
|
||||||
|
|
||||||
# 意图未被处理,继续常规聊天流程
|
# 意图未被处理,继续常规聊天流程,使用实际文本内容
|
||||||
await send_stt_message(conn, text)
|
await send_stt_message(conn, actual_text)
|
||||||
conn.executor.submit(conn.chat, text)
|
conn.executor.submit(conn.chat, actual_text)
|
||||||
|
|
||||||
|
|
||||||
async def no_voice_close_connect(conn, have_voice):
|
async def no_voice_close_connect(conn, have_voice):
|
||||||
|
|||||||
@@ -76,7 +76,6 @@ async def sendAudio(conn, audios, pre_buffer=True):
|
|||||||
frame_duration = 60 # 帧时长(毫秒),匹配 Opus 编码
|
frame_duration = 60 # 帧时长(毫秒),匹配 Opus 编码
|
||||||
start_time = time.perf_counter()
|
start_time = time.perf_counter()
|
||||||
play_position = 0
|
play_position = 0
|
||||||
last_reset_time = time.perf_counter() # 记录最后的重置时间
|
|
||||||
|
|
||||||
# 仅当第一句话时执行预缓冲
|
# 仅当第一句话时执行预缓冲
|
||||||
if pre_buffer:
|
if pre_buffer:
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
import os
|
import os
|
||||||
import wave
|
import wave
|
||||||
import copy
|
|
||||||
import uuid
|
import uuid
|
||||||
import queue
|
import queue
|
||||||
import asyncio
|
import asyncio
|
||||||
import traceback
|
import traceback
|
||||||
import threading
|
import threading
|
||||||
import opuslib_next
|
import opuslib_next
|
||||||
|
import json
|
||||||
|
import io
|
||||||
|
import aiohttp
|
||||||
|
import time
|
||||||
|
import concurrent.futures
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from config.logger import setup_logging
|
from config.logger import setup_logging
|
||||||
from typing import Optional, Tuple, List
|
from urllib.parse import urlparse, parse_qs
|
||||||
|
from typing import Optional, Tuple, List, Dict, Any
|
||||||
from core.handle.receiveAudioHandle import startToChat
|
from core.handle.receiveAudioHandle import startToChat
|
||||||
from core.handle.reportHandle import enqueue_asr_report
|
from core.handle.reportHandle import enqueue_asr_report
|
||||||
from core.utils.util import remove_punctuation_and_length
|
from core.utils.util import remove_punctuation_and_length
|
||||||
@@ -18,16 +23,145 @@ from core.handle.receiveAudioHandle import handleAudioMessage
|
|||||||
TAG = __name__
|
TAG = __name__
|
||||||
logger = setup_logging()
|
logger = setup_logging()
|
||||||
|
|
||||||
|
# 创建全局线程池执行器用于CPU密集型操作
|
||||||
|
executor = concurrent.futures.ThreadPoolExecutor(max_workers=4)
|
||||||
|
|
||||||
|
class VoiceprintProvider:
|
||||||
|
"""声纹识别服务提供者"""
|
||||||
|
|
||||||
|
def __init__(self, config: dict):
|
||||||
|
self.original_url = config.get("url", "")
|
||||||
|
self.speakers = config.get("speakers", [])
|
||||||
|
self.speaker_map = self._parse_speakers()
|
||||||
|
|
||||||
|
# 解析API地址和密钥
|
||||||
|
self.api_url = None
|
||||||
|
self.api_key = None
|
||||||
|
self.speaker_ids = []
|
||||||
|
|
||||||
|
if not self.original_url:
|
||||||
|
logger.bind(tag=TAG).warning("声纹识别URL未配置,声纹识别将被禁用")
|
||||||
|
self.enabled = False
|
||||||
|
else:
|
||||||
|
# 解析URL和key
|
||||||
|
parsed_url = urlparse(self.original_url)
|
||||||
|
base_url = f"{parsed_url.scheme}://{parsed_url.netloc}"
|
||||||
|
|
||||||
|
# 从查询参数中提取key
|
||||||
|
query_params = parse_qs(parsed_url.query)
|
||||||
|
self.api_key = query_params.get('key', [''])[0]
|
||||||
|
|
||||||
|
if not self.api_key:
|
||||||
|
logger.bind(tag=TAG).error("URL中未找到key参数,声纹识别将被禁用")
|
||||||
|
self.enabled = False
|
||||||
|
else:
|
||||||
|
# 构造identify接口地址
|
||||||
|
self.api_url = f"{base_url}/voiceprint/identify"
|
||||||
|
|
||||||
|
# 提取speaker_ids
|
||||||
|
for speaker_str in self.speakers:
|
||||||
|
try:
|
||||||
|
parts = speaker_str.split(",", 2)
|
||||||
|
if len(parts) >= 1:
|
||||||
|
speaker_id = parts[0].strip()
|
||||||
|
self.speaker_ids.append(speaker_id)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 检查是否有有效的说话人配置
|
||||||
|
if not self.speaker_ids:
|
||||||
|
logger.bind(tag=TAG).warning("未配置有效的说话人,声纹识别将被禁用")
|
||||||
|
self.enabled = False
|
||||||
|
else:
|
||||||
|
self.enabled = True
|
||||||
|
logger.bind(tag=TAG).info(f"声纹识别已配置: API={self.api_url}, 说话人={len(self.speaker_ids)}个")
|
||||||
|
|
||||||
|
def _parse_speakers(self) -> Dict[str, Dict[str, str]]:
|
||||||
|
"""解析说话人配置"""
|
||||||
|
speaker_map = {}
|
||||||
|
for speaker_str in self.speakers:
|
||||||
|
try:
|
||||||
|
parts = speaker_str.split(",", 2)
|
||||||
|
if len(parts) >= 3:
|
||||||
|
speaker_id, name, description = parts[0].strip(), parts[1].strip(), parts[2].strip()
|
||||||
|
speaker_map[speaker_id] = {
|
||||||
|
"name": name,
|
||||||
|
"description": description
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.bind(tag=TAG).warning(f"解析说话人配置失败: {speaker_str}, 错误: {e}")
|
||||||
|
return speaker_map
|
||||||
|
|
||||||
|
async def identify_speaker(self, audio_data: bytes, session_id: str) -> Optional[str]:
|
||||||
|
"""识别说话人"""
|
||||||
|
if not self.enabled or not self.api_url or not self.api_key:
|
||||||
|
logger.bind(tag=TAG).debug("声纹识别功能已禁用或未配置,跳过识别")
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
api_start_time = time.monotonic()
|
||||||
|
|
||||||
|
# 准备请求头
|
||||||
|
headers = {
|
||||||
|
'Authorization': f'Bearer {self.api_key}',
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
|
||||||
|
# 准备multipart/form-data数据
|
||||||
|
data = aiohttp.FormData()
|
||||||
|
data.add_field('speaker_ids', ','.join(self.speaker_ids))
|
||||||
|
data.add_field('file', audio_data, filename='audio.wav', content_type='audio/wav')
|
||||||
|
|
||||||
|
timeout = aiohttp.ClientTimeout(total=10)
|
||||||
|
|
||||||
|
# 网络请求
|
||||||
|
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||||
|
async with session.post(self.api_url, headers=headers, data=data) as response:
|
||||||
|
|
||||||
|
if response.status == 200:
|
||||||
|
result = await response.json()
|
||||||
|
speaker_id = result.get("speaker_id")
|
||||||
|
score = result.get("score", 0)
|
||||||
|
total_elapsed_time = time.monotonic() - api_start_time
|
||||||
|
|
||||||
|
logger.bind(tag=TAG).info(f"声纹识别耗时: {total_elapsed_time:.3f}s")
|
||||||
|
|
||||||
|
# 置信度检查
|
||||||
|
if score < 0.5:
|
||||||
|
logger.bind(tag=TAG).warning(f"声纹识别置信度较低: {score:.3f}")
|
||||||
|
|
||||||
|
if speaker_id and speaker_id in self.speaker_map:
|
||||||
|
result_name = self.speaker_map[speaker_id]["name"]
|
||||||
|
return result_name
|
||||||
|
else:
|
||||||
|
logger.bind(tag=TAG).warning(f"未识别的说话人ID: {speaker_id}")
|
||||||
|
return "未知说话人"
|
||||||
|
else:
|
||||||
|
logger.bind(tag=TAG).error(f"声纹识别API错误: HTTP {response.status}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
elapsed = time.monotonic() - api_start_time
|
||||||
|
logger.bind(tag=TAG).error(f"声纹识别超时: {elapsed:.3f}s")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
elapsed = time.monotonic() - api_start_time
|
||||||
|
logger.bind(tag=TAG).error(f"声纹识别失败: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class ASRProviderBase(ABC):
|
class ASRProviderBase(ABC):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass
|
self.voiceprint_provider = None
|
||||||
|
|
||||||
|
def init_voiceprint(self, voiceprint_config: dict):
|
||||||
|
"""初始化声纹识别"""
|
||||||
|
if voiceprint_config:
|
||||||
|
self.voiceprint_provider = VoiceprintProvider(voiceprint_config)
|
||||||
|
logger.bind(tag=TAG).info("声纹识别模块已初始化")
|
||||||
|
|
||||||
# 打开音频通道
|
# 打开音频通道
|
||||||
# 这里默认是非流式的处理方式
|
|
||||||
# 流式处理方式请在子类中重写
|
|
||||||
async def open_audio_channels(self, conn):
|
async def open_audio_channels(self, conn):
|
||||||
# tts 消化线程
|
|
||||||
conn.asr_priority_thread = threading.Thread(
|
conn.asr_priority_thread = threading.Thread(
|
||||||
target=self.asr_text_priority_thread, args=(conn,), daemon=True
|
target=self.asr_text_priority_thread, args=(conn,), daemon=True
|
||||||
)
|
)
|
||||||
@@ -52,41 +186,173 @@ class ASRProviderBase(ABC):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# 接收音频
|
# 接收音频
|
||||||
# 这里默认是非流式的处理方式
|
|
||||||
# 流式处理方式请在子类中重写
|
|
||||||
async def receive_audio(self, conn, audio, audio_have_voice):
|
async def receive_audio(self, conn, audio, audio_have_voice):
|
||||||
if conn.client_listen_mode == "auto" or conn.client_listen_mode == "realtime":
|
if conn.client_listen_mode == "auto" or conn.client_listen_mode == "realtime":
|
||||||
have_voice = audio_have_voice
|
have_voice = audio_have_voice
|
||||||
else:
|
else:
|
||||||
have_voice = conn.client_have_voice
|
have_voice = conn.client_have_voice
|
||||||
# 如果本次没有声音,本段也没声音,就把声音丢弃了
|
|
||||||
conn.asr_audio.append(audio)
|
conn.asr_audio.append(audio)
|
||||||
if have_voice == False and conn.client_have_voice == False:
|
if not have_voice and not conn.client_have_voice:
|
||||||
conn.asr_audio = conn.asr_audio[-10:]
|
conn.asr_audio = conn.asr_audio[-10:]
|
||||||
return
|
return
|
||||||
|
|
||||||
# 如果本段有声音,且已经停止了
|
|
||||||
if conn.client_voice_stop:
|
if conn.client_voice_stop:
|
||||||
asr_audio_task = copy.deepcopy(conn.asr_audio)
|
asr_audio_task = conn.asr_audio.copy()
|
||||||
conn.asr_audio.clear()
|
conn.asr_audio.clear()
|
||||||
|
|
||||||
# 音频太短了,无法识别
|
|
||||||
conn.reset_vad_states()
|
conn.reset_vad_states()
|
||||||
|
|
||||||
if len(asr_audio_task) > 15:
|
if len(asr_audio_task) > 15:
|
||||||
await self.handle_voice_stop(conn, asr_audio_task)
|
await self.handle_voice_stop(conn, asr_audio_task)
|
||||||
|
|
||||||
# 处理语音停止
|
# 处理语音停止
|
||||||
async def handle_voice_stop(self, conn, asr_audio_task):
|
async def handle_voice_stop(self, conn, asr_audio_task: List[bytes]):
|
||||||
raw_text, _ = await self.speech_to_text(
|
"""并行处理ASR和声纹识别"""
|
||||||
asr_audio_task, conn.session_id, conn.audio_format
|
try:
|
||||||
) # 确保ASR模块返回原始文本
|
total_start_time = time.monotonic()
|
||||||
conn.logger.bind(tag=TAG).info(f"识别文本: {raw_text}")
|
|
||||||
text_len, _ = remove_punctuation_and_length(raw_text)
|
# 准备音频数据
|
||||||
self.stop_ws_connection()
|
if conn.audio_format == "pcm":
|
||||||
if text_len > 0:
|
pcm_data = asr_audio_task
|
||||||
# 使用自定义模块进行上报
|
else:
|
||||||
await startToChat(conn, raw_text)
|
pcm_data = self.decode_opus(asr_audio_task)
|
||||||
enqueue_asr_report(conn, raw_text, asr_audio_task)
|
|
||||||
|
combined_pcm_data = b"".join(pcm_data)
|
||||||
|
|
||||||
|
# 预先准备WAV数据
|
||||||
|
wav_data = None
|
||||||
|
if self.voiceprint_provider and combined_pcm_data:
|
||||||
|
wav_data = self._pcm_to_wav(combined_pcm_data)
|
||||||
|
|
||||||
|
|
||||||
|
# 定义ASR任务
|
||||||
|
def run_asr():
|
||||||
|
start_time = time.monotonic()
|
||||||
|
try:
|
||||||
|
import asyncio
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
try:
|
||||||
|
result = loop.run_until_complete(
|
||||||
|
self.speech_to_text(asr_audio_task, conn.session_id, conn.audio_format)
|
||||||
|
)
|
||||||
|
end_time = time.monotonic()
|
||||||
|
logger.bind(tag=TAG).info(f"ASR耗时: {end_time - start_time:.3f}s")
|
||||||
|
return result
|
||||||
|
finally:
|
||||||
|
loop.close()
|
||||||
|
except Exception as e:
|
||||||
|
end_time = time.monotonic()
|
||||||
|
logger.bind(tag=TAG).error(f"ASR失败: {e}")
|
||||||
|
return ("", None)
|
||||||
|
|
||||||
|
# 定义声纹识别任务
|
||||||
|
def run_voiceprint():
|
||||||
|
if not wav_data:
|
||||||
|
return None
|
||||||
|
start_time = time.monotonic()
|
||||||
|
try:
|
||||||
|
import asyncio
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
try:
|
||||||
|
result = loop.run_until_complete(
|
||||||
|
self.voiceprint_provider.identify_speaker(wav_data, conn.session_id)
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
finally:
|
||||||
|
loop.close()
|
||||||
|
except Exception as e:
|
||||||
|
logger.bind(tag=TAG).error(f"声纹识别失败: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 使用线程池执行器并行运行
|
||||||
|
parallel_start_time = time.monotonic()
|
||||||
|
|
||||||
|
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as thread_executor:
|
||||||
|
asr_future = thread_executor.submit(run_asr)
|
||||||
|
|
||||||
|
if self.voiceprint_provider and wav_data:
|
||||||
|
voiceprint_future = thread_executor.submit(run_voiceprint)
|
||||||
|
|
||||||
|
# 等待两个线程都完成
|
||||||
|
asr_result = asr_future.result(timeout=15)
|
||||||
|
voiceprint_result = voiceprint_future.result(timeout=15)
|
||||||
|
|
||||||
|
results = {"asr": asr_result, "voiceprint": voiceprint_result}
|
||||||
|
else:
|
||||||
|
asr_result = asr_future.result(timeout=15)
|
||||||
|
results = {"asr": asr_result, "voiceprint": None}
|
||||||
|
|
||||||
|
parallel_execution_time = time.monotonic() - parallel_start_time
|
||||||
|
|
||||||
|
# 处理结果
|
||||||
|
raw_text, file_path = results.get("asr", ("", None))
|
||||||
|
speaker_name = results.get("voiceprint", None)
|
||||||
|
|
||||||
|
# 记录识别结果
|
||||||
|
if raw_text:
|
||||||
|
logger.bind(tag=TAG).info(f"识别文本: {raw_text}")
|
||||||
|
if speaker_name:
|
||||||
|
logger.bind(tag=TAG).info(f"识别说话人: {speaker_name}")
|
||||||
|
|
||||||
|
# 性能监控
|
||||||
|
total_time = time.monotonic() - total_start_time
|
||||||
|
logger.bind(tag=TAG).info(f"总处理耗时: {total_time:.3f}s")
|
||||||
|
|
||||||
|
# 检查文本长度
|
||||||
|
text_len, _ = remove_punctuation_and_length(raw_text)
|
||||||
|
self.stop_ws_connection()
|
||||||
|
|
||||||
|
if text_len > 0:
|
||||||
|
# 构建包含说话人信息的JSON字符串
|
||||||
|
enhanced_text = self._build_enhanced_text(raw_text, speaker_name)
|
||||||
|
|
||||||
|
# 使用自定义模块进行上报
|
||||||
|
await startToChat(conn, enhanced_text)
|
||||||
|
enqueue_asr_report(conn, enhanced_text, 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 _build_enhanced_text(self, text: str, speaker_name: Optional[str]) -> str:
|
||||||
|
"""构建包含说话人信息的文本"""
|
||||||
|
if speaker_name:
|
||||||
|
return json.dumps({
|
||||||
|
"speaker": speaker_name,
|
||||||
|
"content": text
|
||||||
|
}, ensure_ascii=False)
|
||||||
|
else:
|
||||||
|
return text
|
||||||
|
|
||||||
|
def _pcm_to_wav(self, pcm_data: bytes) -> bytes:
|
||||||
|
"""将PCM数据转换为WAV格式"""
|
||||||
|
if len(pcm_data) == 0:
|
||||||
|
logger.bind(tag=TAG).warning("PCM数据为空,无法转换WAV")
|
||||||
|
return b""
|
||||||
|
|
||||||
|
# 确保数据长度是偶数(16位音频)
|
||||||
|
if len(pcm_data) % 2 != 0:
|
||||||
|
pcm_data = pcm_data[:-1]
|
||||||
|
|
||||||
|
# 创建WAV文件头
|
||||||
|
wav_buffer = io.BytesIO()
|
||||||
|
try:
|
||||||
|
with wave.open(wav_buffer, 'wb') as wav_file:
|
||||||
|
wav_file.setnchannels(1) # 单声道
|
||||||
|
wav_file.setsampwidth(2) # 16位
|
||||||
|
wav_file.setframerate(16000) # 16kHz采样率
|
||||||
|
wav_file.writeframes(pcm_data)
|
||||||
|
|
||||||
|
wav_buffer.seek(0)
|
||||||
|
wav_data = wav_buffer.read()
|
||||||
|
|
||||||
|
return wav_data
|
||||||
|
except Exception as e:
|
||||||
|
logger.bind(tag=TAG).error(f"WAV转换失败: {e}")
|
||||||
|
return b""
|
||||||
|
|
||||||
def stop_ws_connection(self):
|
def stop_ws_connection(self):
|
||||||
pass
|
pass
|
||||||
@@ -99,7 +365,7 @@ class ASRProviderBase(ABC):
|
|||||||
|
|
||||||
with wave.open(file_path, "wb") as wf:
|
with wave.open(file_path, "wb") as wf:
|
||||||
wf.setnchannels(1)
|
wf.setnchannels(1)
|
||||||
wf.setsampwidth(2) # 2 bytes = 16-bit
|
wf.setsampwidth(2)
|
||||||
wf.setframerate(16000)
|
wf.setframerate(16000)
|
||||||
wf.writeframes(b"".join(pcm_data))
|
wf.writeframes(b"".join(pcm_data))
|
||||||
|
|
||||||
@@ -113,27 +379,29 @@ class ASRProviderBase(ABC):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def decode_opus(opus_data: List[bytes]) -> bytes:
|
def decode_opus(opus_data: List[bytes]) -> List[bytes]:
|
||||||
"""将Opus音频数据解码为PCM数据"""
|
"""将Opus音频数据解码为PCM数据"""
|
||||||
try:
|
try:
|
||||||
decoder = opuslib_next.Decoder(16000, 1) # 16kHz, 单声道
|
decoder = opuslib_next.Decoder(16000, 1)
|
||||||
pcm_data = []
|
pcm_data = []
|
||||||
buffer_size = 960 # 每次处理960个采样点
|
buffer_size = 960 # 每次处理960个采样点 (60ms at 16kHz)
|
||||||
|
|
||||||
for opus_packet in opus_data:
|
for i, opus_packet in enumerate(opus_data):
|
||||||
try:
|
try:
|
||||||
# 使用较小的缓冲区大小进行处理
|
if not opus_packet or len(opus_packet) == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
pcm_frame = decoder.decode(opus_packet, buffer_size)
|
pcm_frame = decoder.decode(opus_packet, buffer_size)
|
||||||
if pcm_frame:
|
if pcm_frame and len(pcm_frame) > 0:
|
||||||
pcm_data.append(pcm_frame)
|
pcm_data.append(pcm_frame)
|
||||||
|
|
||||||
except opuslib_next.OpusError as e:
|
except opuslib_next.OpusError as e:
|
||||||
logger.bind(tag=TAG).warning(f"Opus解码错误,跳过当前数据包: {e}")
|
logger.bind(tag=TAG).warning(f"Opus解码错误,跳过数据包 {i}: {e}")
|
||||||
continue
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.bind(tag=TAG).error(f"音频处理错误: {e}", exc_info=True)
|
logger.bind(tag=TAG).error(f"音频处理错误,数据包 {i}: {e}")
|
||||||
continue
|
|
||||||
|
|
||||||
return pcm_data
|
return pcm_data
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.bind(tag=TAG).error(f"音频解码过程发生错误: {e}", exc_info=True)
|
logger.bind(tag=TAG).error(f"音频解码过程发生错误: {e}")
|
||||||
return []
|
return []
|
||||||
|
|||||||
@@ -74,8 +74,14 @@ class Dialogue:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if system_message:
|
if system_message:
|
||||||
|
# 构建增强的系统提示,包含说话人处理指导
|
||||||
|
speaker_guidance = "\n\n[说话人识别功能说明]\n" \
|
||||||
|
"当用户消息包含 [说话人: 姓名] 前缀时,表示系统已识别出说话人身份。\n" \
|
||||||
|
"请根据说话人的身份特征(如果之前有相关信息)来调整回应风格和内容。\n" \
|
||||||
|
"你可以称呼说话人的名字,并参考他们的特点进行个性化回应。"
|
||||||
|
|
||||||
enhanced_system_prompt = (
|
enhanced_system_prompt = (
|
||||||
f"{system_message.content}\n\n"
|
f"{system_message.content}{speaker_guidance}\n\n"
|
||||||
f"以下是用户的历史记忆:\n```\n{memory_str}\n```"
|
f"以下是用户的历史记忆:\n```\n{memory_str}\n```"
|
||||||
)
|
)
|
||||||
dialogue.append({"role": "system", "content": enhanced_system_prompt})
|
dialogue.append({"role": "system", "content": enhanced_system_prompt})
|
||||||
|
|||||||
@@ -125,4 +125,13 @@ def initialize_asr(config):
|
|||||||
config["ASR"][select_asr_module],
|
config["ASR"][select_asr_module],
|
||||||
str(config.get("delete_audio", True)).lower() in ("true", "1", "yes"),
|
str(config.get("delete_audio", True)).lower() in ("true", "1", "yes"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 初始化声纹识别功能
|
||||||
|
voiceprint_config = config.get("plugins", {}).get("voiceprint")
|
||||||
|
if voiceprint_config and voiceprint_config.get("url") and voiceprint_config.get("speakers"):
|
||||||
|
new_asr.init_voiceprint(voiceprint_config)
|
||||||
|
logger.bind(tag=TAG).info("ASR模块声纹识别功能已启用")
|
||||||
|
else:
|
||||||
|
logger.bind(tag=TAG).info("ASR模块声纹识别功能已禁用")
|
||||||
|
|
||||||
return new_asr
|
return new_asr
|
||||||
|
|||||||
Reference in New Issue
Block a user