update:合并非tts代码

This commit is contained in:
hrz
2025-05-21 13:18:12 +08:00
parent ede2bc6a4e
commit 851365fb58
65 changed files with 5423 additions and 1943 deletions
@@ -1,97 +0,0 @@
import random
import threading
import time
from typing import Set
class AuthCodeGenerator:
_instance = None
_instance_lock = threading.Lock()
def __new__(cls):
if not cls._instance:
with cls._instance_lock:
if not cls._instance:
cls._instance = super(AuthCodeGenerator, cls).__new__(cls)
# 初始化随机种子
random.seed(time.time())
return cls._instance
def __init__(self):
# 确保 __init__ 只被调用一次
if not hasattr(self, '_initialized'):
self._used_codes: Set[str] = set()
self._code_timestamps = {}
self._lock = threading.Lock()
self._code_timeout = 3 * 24 * 60 * 60
self._initialized = True
@classmethod
def get_instance(cls):
"""获取AuthCodeGenerator的单例实例"""
return cls()
def generate_code(self) -> str:
"""
生成6位数字认证码,确保不重复
返回: 6位数字字符串
"""
with self._lock:
self._clean_expired_codes() # 清理过期code
while True:
# 使用时间戳和已用码数量作为种子,确保每次生成不同的随机数
seed = int(time.time() * 1000) + len(self._used_codes)
random.seed(seed)
# 生成6位随机数字
code = ''.join(str(random.randint(0, 9)) for _ in range(6))
# 检查是否已存在
if code not in self._used_codes:
self._used_codes.add(code)
self._code_timestamps[code] = time.time()
return code
def remove_code(self, code: str) -> bool:
"""
删除已使用的认证码
参数:
code: 要删除的认证码
返回:
bool: 删除成功返回True,码不存在返回False
"""
print('remove_code', code)
with self._lock:
if code in self._used_codes:
self._used_codes.remove(code)
if code in self._code_timestamps:
del self._code_timestamps[code]
return True
return False
def is_code_used(self, code: str) -> bool:
"""
检查认证码是否已被使用
参数:
code: 要检查的认证码
返回:
bool: 如果码存在返回True,否则返回False
"""
with self._lock:
return code in self._used_codes
def clear_codes(self):
"""清空所有已使用的认证码"""
with self._lock:
self._used_codes.clear()
self._code_timestamps.clear()
def _clean_expired_codes(self):
"""清理过期的认证码"""
current_time = time.time()
expired_codes = [
code for code, timestamp in self._code_timestamps.items()
if (current_time - timestamp) > self._code_timeout
]
for code in expired_codes:
self._used_codes.remove(code)
del self._code_timestamps[code]
+24 -8
View File
@@ -4,7 +4,14 @@ from datetime import datetime
class Message:
def __init__(self, role: str, content: str = None, uniq_id: str = None, tool_calls = None, tool_call_id=None):
def __init__(
self,
role: str,
content: str = None,
uniq_id: str = None,
tool_calls=None,
tool_call_id=None,
):
self.uniq_id = uniq_id if uniq_id is not None else str(uuid.uuid4())
self.role = role
self.content = content
@@ -16,7 +23,7 @@ class Dialogue:
def __init__(self):
self.dialogue: List[Message] = []
# 获取当前时间
self.current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
self.current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
def put(self, message: Message):
self.dialogue.append(message)
@@ -25,7 +32,15 @@ class Dialogue:
if m.tool_calls is not None:
dialogue.append({"role": m.role, "tool_calls": m.tool_calls})
elif m.role == "tool":
dialogue.append({"role": m.role, "tool_call_id": m.tool_call_id, "content": m.content})
dialogue.append(
{
"role": m.role,
"tool_call_id": (
str(uuid.uuid4()) if m.tool_call_id is None else m.tool_call_id
),
"content": m.content,
}
)
else:
dialogue.append({"role": m.role, "content": m.content})
@@ -44,23 +59,24 @@ class Dialogue:
else:
self.put(Message(role="system", content=new_content))
def get_llm_dialogue_with_memory(self, memory_str: str = None) -> List[Dict[str, str]]:
def get_llm_dialogue_with_memory(
self, memory_str: str = None
) -> List[Dict[str, str]]:
if memory_str is None or len(memory_str) == 0:
return self.get_llm_dialogue()
# 构建带记忆的对话
dialogue = []
# 添加系统提示和记忆
system_message = next(
(msg for msg in self.dialogue if msg.role == "system"), None
)
if system_message:
enhanced_system_prompt = (
f"{system_message.content}\n\n"
f"相关记忆:\n{memory_str}"
f"以下是用户的历史记忆:\n```\n{memory_str}\n```"
)
dialogue.append({"role": "system", "content": enhanced_system_prompt})
@@ -1,39 +0,0 @@
import asyncio
from typing import Dict
from config.logger import setup_logging
TAG = __name__
logger = setup_logging()
class FileLockManager:
_instance = None
_locks: Dict[str, asyncio.Lock] = {}
def __new__(cls):
if cls._instance is None:
cls._instance = super(FileLockManager, cls).__new__(cls)
return cls._instance
@classmethod
def get_lock(cls, file_path: str) -> asyncio.Lock:
"""获取指定文件的锁"""
if file_path not in cls._locks:
cls._locks[file_path] = asyncio.Lock()
return cls._locks[file_path]
@classmethod
async def acquire_lock(cls, file_path: str):
"""获取锁"""
lock = cls.get_lock(file_path)
await lock.acquire()
logger.bind(tag=TAG).debug(f"Acquired lock for {file_path}")
@classmethod
def release_lock(cls, file_path: str):
"""释放锁"""
if file_path in cls._locks:
try:
cls._locks[file_path].release()
logger.bind(tag=TAG).debug(f"Released lock for {file_path}")
except RuntimeError as e:
logger.bind(tag=TAG).warning(f"Failed to release lock for {file_path}: {e}")
+6 -5
View File
@@ -2,16 +2,17 @@ import os
import sys
import importlib
from config.logger import setup_logging
from core.utils.util import read_config, get_project_dir
logger = setup_logging()
def create_instance(class_name, *args, **kwargs):
if os.path.exists(os.path.join('core', 'providers', 'memory', class_name, f'{class_name}.py')):
lib_name = f'core.providers.memory.{class_name}.{class_name}'
if os.path.exists(
os.path.join("core", "providers", "memory", class_name, f"{class_name}.py")
):
lib_name = f"core.providers.memory.{class_name}.{class_name}"
if lib_name not in sys.modules:
sys.modules[lib_name] = importlib.import_module(f'{lib_name}')
sys.modules[lib_name] = importlib.import_module(f"{lib_name}")
return sys.modules[lib_name].MemoryProvider(*args, **kwargs)
raise ValueError(f"不支持的记忆服务类型: {class_name}")
@@ -0,0 +1,50 @@
import datetime
from typing import Dict, Tuple
# 全局字典,用于存储每个设备的每日输出字数
_device_daily_output: Dict[Tuple[str, datetime.date], int] = {}
# 记录最后一次检查的日期
_last_check_date: datetime.date = None
def reset_device_output():
"""
重置所有设备的每日输出字数
每天0点调用此函数
"""
_device_daily_output.clear()
def get_device_output(device_id: str) -> int:
"""
获取设备当日的输出字数
"""
current_date = datetime.datetime.now().date()
return _device_daily_output.get((device_id, current_date), 0)
def add_device_output(device_id: str, char_count: int):
"""
增加设备的输出字数
"""
current_date = datetime.datetime.now().date()
global _last_check_date
# 如果是第一次调用或者日期发生变化,清空计数器
if _last_check_date is None or _last_check_date != current_date:
_device_daily_output.clear()
_last_check_date = current_date
current_count = _device_daily_output.get((device_id, current_date), 0)
_device_daily_output[(device_id, current_date)] = current_count + char_count
def check_device_output_limit(device_id: str, max_output_size: int) -> bool:
"""
检查设备是否超过输出限制
:return: True 如果超过限制,False 如果未超过
"""
if not device_id:
return False
current_output = get_device_output(device_id)
return current_output >= max_output_size
+807 -28
View File
@@ -1,19 +1,40 @@
import os
import json
import yaml
import socket
import subprocess
import logging
import re
import os
import numpy as np
import requests
import opuslib_next
from pydub import AudioSegment
from typing import Dict, Any
from core.utils import tts, llm, intent, memory, vad, asr
import copy
def get_project_dir():
"""获取项目根目录"""
return (
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+ "/"
)
TAG = __name__
emoji_map = {
"neutral": "😶",
"happy": "🙂",
"laughing": "😆",
"funny": "😂",
"sad": "😔",
"angry": "😠",
"crying": "😭",
"loving": "😍",
"embarrassed": "😳",
"surprised": "😲",
"shocked": "😱",
"thinking": "🤔",
"winking": "😉",
"cool": "😎",
"relaxed": "😌",
"delicious": "🤤",
"kissy": "😘",
"confident": "😏",
"sleepy": "😴",
"silly": "😜",
"confused": "🙄",
}
def get_local_ip():
@@ -72,7 +93,7 @@ def is_private_ip(ip_addr):
return False # IP address format error or insufficient segments
def get_ip_info(ip_addr):
def get_ip_info(ip_addr, logger):
try:
if is_private_ip(ip_addr):
ip_addr = ""
@@ -81,16 +102,10 @@ def get_ip_info(ip_addr):
ip_info = {"city": resp.get("city")}
return ip_info
except Exception as e:
logging.error(f"Error getting client ip info: {e}")
logger.bind(tag=TAG).error(f"Error getting client ip info: {e}")
return {}
def read_config(config_path):
with open(config_path, "r", encoding="utf-8") as file:
config = yaml.safe_load(file)
return config
def write_json_file(file_path, data):
"""将数据写入 JSON 文件"""
with open(file_path, "w", encoding="utf-8") as file:
@@ -103,13 +118,14 @@ def is_punctuation_or_emoji(char):
punctuation_set = {
"",
",", # 中文逗号 + 英文逗号
"",
".", # 中文句号 + 英文句号
"",
"!", # 中文感叹号 + 英文感叹号
"-",
"", # 英文连字符 + 中文全角横线
"", # 中文顿号
"",
"",
'"', # 中文双引号 + 英文引号
"",
":", # 中文冒号 + 英文冒号
}
if char.isspace() or char in punctuation_set:
return True
@@ -169,15 +185,30 @@ def remove_punctuation_and_length(text):
def check_model_key(modelType, modelKey):
if "" in modelKey:
logging.error(
"你还没配置"
+ modelType
+ "的密钥,请在配置文件中配置密钥,否则无法正常工作"
raise ValueError(
"你还没配置" + modelType + "的密钥,请检查一下所使用的LLM是否配置了密钥"
)
return False
return True
def parse_string_to_list(value, separator=";"):
"""
将输入值转换为列表
Args:
value: 输入值,可以是 None、字符串或列表
separator: 分隔符,默认为分号
Returns:
list: 处理后的列表
"""
if value is None or value == "":
return []
elif isinstance(value, str):
return [item.strip() for item in value.split(separator) if item.strip()]
elif isinstance(value, list):
return value
return []
def check_ffmpeg_installed():
ffmpeg_installed = False
try:
@@ -208,7 +239,755 @@ def check_ffmpeg_installed():
def extract_json_from_string(input_string):
"""提取字符串中的 JSON 部分"""
pattern = r"(\{.*\})"
match = re.search(pattern, input_string)
match = re.search(pattern, input_string, re.DOTALL) # 添加 re.DOTALL
if match:
return match.group(1) # 返回提取的 JSON 字符串
return None
def initialize_modules(
logger,
config: Dict[str, Any],
init_vad=False,
init_asr=False,
init_llm=False,
init_tts=False,
init_memory=False,
init_intent=False,
) -> Dict[str, Any]:
"""
初始化所有模块组件
Args:
config: 配置字典
Returns:
Dict[str, Any]: 包含所有初始化后的模块的字典
"""
modules = {}
# 初始化TTS模块
if init_tts:
select_tts_module = config["selected_module"]["TTS"]
tts_type = (
select_tts_module
if "type" not in config["TTS"][select_tts_module]
else config["TTS"][select_tts_module]["type"]
)
modules["tts"] = tts.create_instance(
tts_type,
config["TTS"][select_tts_module],
str(config.get("delete_audio", True)).lower() in ("true", "1", "yes"),
)
logger.bind(tag=TAG).info(f"初始化组件: tts成功 {select_tts_module}")
# 初始化LLM模块
if init_llm:
select_llm_module = config["selected_module"]["LLM"]
llm_type = (
select_llm_module
if "type" not in config["LLM"][select_llm_module]
else config["LLM"][select_llm_module]["type"]
)
modules["llm"] = llm.create_instance(
llm_type,
config["LLM"][select_llm_module],
)
logger.bind(tag=TAG).info(f"初始化组件: llm成功 {select_llm_module}")
# 初始化Intent模块
if init_intent:
select_intent_module = config["selected_module"]["Intent"]
intent_type = (
select_intent_module
if "type" not in config["Intent"][select_intent_module]
else config["Intent"][select_intent_module]["type"]
)
modules["intent"] = intent.create_instance(
intent_type,
config["Intent"][select_intent_module],
)
logger.bind(tag=TAG).info(f"初始化组件: intent成功 {select_intent_module}")
# 初始化Memory模块
if init_memory:
select_memory_module = config["selected_module"]["Memory"]
memory_type = (
select_memory_module
if "type" not in config["Memory"][select_memory_module]
else config["Memory"][select_memory_module]["type"]
)
modules["memory"] = memory.create_instance(
memory_type,
config["Memory"][select_memory_module],
config.get("summaryMemory", None),
)
logger.bind(tag=TAG).info(f"初始化组件: memory成功 {select_memory_module}")
# 初始化VAD模块
if init_vad:
select_vad_module = config["selected_module"]["VAD"]
vad_type = (
select_vad_module
if "type" not in config["VAD"][select_vad_module]
else config["VAD"][select_vad_module]["type"]
)
modules["vad"] = vad.create_instance(
vad_type,
config["VAD"][select_vad_module],
)
logger.bind(tag=TAG).info(f"初始化组件: vad成功 {select_vad_module}")
# 初始化ASR模块
if init_asr:
select_asr_module = config["selected_module"]["ASR"]
asr_type = (
select_asr_module
if "type" not in config["ASR"][select_asr_module]
else config["ASR"][select_asr_module]["type"]
)
modules["asr"] = asr.create_instance(
asr_type,
config["ASR"][select_asr_module],
str(config.get("delete_audio", True)).lower() in ("true", "1", "yes"),
)
logger.bind(tag=TAG).info(f"初始化组件: asr成功 {select_asr_module}")
return modules
def analyze_emotion(text):
"""
分析文本情感并返回对应的emoji名称(支持中英文)
"""
if not text or not isinstance(text, str):
return "neutral"
original_text = text
text = text.lower().strip()
# 检查是否包含现有emoji
for emotion, emoji in emoji_map.items():
if emoji in original_text:
return emotion
# 标点符号分析
has_exclamation = "!" in original_text or "" in original_text
has_question = "?" in original_text or "" in original_text
has_ellipsis = "..." in original_text or "" in original_text
# 定义情感关键词映射(中英文扩展版)
emotion_keywords = {
"happy": [
"开心",
"高兴",
"快乐",
"愉快",
"幸福",
"满意",
"",
"",
"不错",
"完美",
"棒极了",
"太好了",
"好呀",
"好的",
"happy",
"joy",
"great",
"good",
"nice",
"awesome",
"fantastic",
"wonderful",
],
"laughing": [
"哈哈",
"哈哈哈",
"呵呵",
"嘿嘿",
"嘻嘻",
"笑死",
"太好笑了",
"笑死我了",
"lol",
"lmao",
"haha",
"hahaha",
"hehe",
"rofl",
"funny",
"laugh",
],
"funny": [
"搞笑",
"滑稽",
"",
"幽默",
"笑点",
"段子",
"笑话",
"太逗了",
"hilarious",
"joke",
"comedy",
],
"sad": [
"伤心",
"难过",
"悲哀",
"悲伤",
"忧郁",
"郁闷",
"沮丧",
"失望",
"想哭",
"难受",
"不开心",
"",
"呜呜",
"sad",
"upset",
"unhappy",
"depressed",
"sorrow",
"gloomy",
],
"angry": [
"生气",
"愤怒",
"气死",
"讨厌",
"烦人",
"可恶",
"烦死了",
"恼火",
"暴躁",
"火大",
"愤怒",
"气炸了",
"angry",
"mad",
"annoyed",
"furious",
"pissed",
"hate",
],
"crying": [
"哭泣",
"泪流",
"大哭",
"伤心欲绝",
"泪目",
"流泪",
"哭死",
"哭晕",
"想哭",
"泪崩",
"cry",
"crying",
"tears",
"sob",
"weep",
],
"loving": [
"爱你",
"喜欢",
"",
"亲爱的",
"宝贝",
"么么哒",
"抱抱",
"想你",
"思念",
"最爱",
"亲亲",
"喜欢你",
"love",
"like",
"adore",
"darling",
"sweetie",
"honey",
"miss you",
"heart",
],
"embarrassed": [
"尴尬",
"不好意思",
"害羞",
"脸红",
"难为情",
"社死",
"丢脸",
"出丑",
"embarrassed",
"awkward",
"shy",
"blush",
],
"surprised": [
"惊讶",
"吃惊",
"天啊",
"哇塞",
"",
"居然",
"竟然",
"没想到",
"出乎意料",
"surprise",
"wow",
"omg",
"oh my god",
"amazing",
"unbelievable",
],
"shocked": [
"震惊",
"吓到",
"惊呆了",
"不敢相信",
"震撼",
"吓死",
"恐怖",
"害怕",
"吓人",
"shocked",
"shocking",
"scared",
"frightened",
"terrified",
"horror",
],
"thinking": [
"思考",
"考虑",
"想一下",
"琢磨",
"沉思",
"冥想",
"",
"思考中",
"在想",
"think",
"thinking",
"consider",
"ponder",
"meditate",
],
"winking": [
"调皮",
"眨眼",
"你懂的",
"坏笑",
"邪恶",
"奸笑",
"使眼色",
"wink",
"teasing",
"naughty",
"mischievous",
],
"cool": [
"",
"",
"厉害",
"棒极了",
"真棒",
"牛逼",
"",
"优秀",
"杰出",
"出色",
"完美",
"cool",
"awesome",
"amazing",
"great",
"impressive",
"perfect",
],
"relaxed": [
"放松",
"舒服",
"惬意",
"悠闲",
"轻松",
"舒适",
"安逸",
"自在",
"relax",
"relaxed",
"comfortable",
"cozy",
"chill",
"peaceful",
],
"delicious": [
"好吃",
"美味",
"",
"",
"可口",
"香甜",
"大餐",
"大快朵颐",
"流口水",
"垂涎",
"delicious",
"yummy",
"tasty",
"yum",
"appetizing",
"mouthwatering",
],
"kissy": [
"亲亲",
"么么",
"",
"mua",
"muah",
"亲一下",
"飞吻",
"kiss",
"xoxo",
"hug",
"muah",
"smooch",
],
"confident": [
"自信",
"肯定",
"确定",
"毫无疑问",
"当然",
"必须的",
"毫无疑问",
"确信",
"坚信",
"confident",
"sure",
"certain",
"definitely",
"positive",
],
"sleepy": [
"",
"睡觉",
"晚安",
"想睡",
"好累",
"疲惫",
"疲倦",
"困了",
"想休息",
"睡意",
"sleep",
"sleepy",
"tired",
"exhausted",
"bedtime",
"good night",
],
"silly": [
"",
"",
"",
"",
"",
"",
"憨憨",
"傻乎乎",
"呆萌",
"silly",
"stupid",
"dumb",
"foolish",
"goofy",
"ridiculous",
],
"confused": [
"疑惑",
"不明白",
"不懂",
"困惑",
"疑问",
"为什么",
"怎么回事",
"啥意思",
"不清楚",
"confused",
"puzzled",
"doubt",
"question",
"what",
"why",
"how",
],
}
# 特殊句型判断(中英文)
# 赞美他人
if any(
phrase in text
for phrase in [
"你真",
"你好",
"您真",
"你真棒",
"你好厉害",
"你太强了",
"你真好",
"你真聪明",
"you are",
"you're",
"you look",
"you seem",
"so smart",
"so kind",
]
):
return "loving"
# 自我赞美
if any(
phrase in text
for phrase in [
"我真",
"我最",
"我太棒了",
"我厉害",
"我聪明",
"我优秀",
"i am",
"i'm",
"i feel",
"so good",
"so happy",
]
):
return "cool"
# 晚安/睡觉相关
if any(
phrase in text
for phrase in [
"睡觉",
"晚安",
"睡了",
"好梦",
"休息了",
"去睡了",
"sleep",
"good night",
"bedtime",
"go to bed",
]
):
return "sleepy"
# 疑问句
if has_question and not has_exclamation:
return "thinking"
# 强烈情感(感叹号)
if has_exclamation and not has_question:
# 检查是否是积极内容
positive_words = (
emotion_keywords["happy"]
+ emotion_keywords["laughing"]
+ emotion_keywords["cool"]
)
if any(word in text for word in positive_words):
return "laughing"
# 检查是否是消极内容
negative_words = (
emotion_keywords["angry"]
+ emotion_keywords["sad"]
+ emotion_keywords["crying"]
)
if any(word in text for word in negative_words):
return "angry"
return "surprised"
# 省略号(表示犹豫或思考)
if has_ellipsis:
return "thinking"
# 关键词匹配(带权重)
emotion_scores = {emotion: 0 for emotion in emoji_map.keys()}
# 给匹配到的关键词加分
for emotion, keywords in emotion_keywords.items():
for keyword in keywords:
if keyword in text:
emotion_scores[emotion] += 1
# 给长文本中的重复关键词额外加分
if len(text) > 20: # 长文本
for emotion, keywords in emotion_keywords.items():
for keyword in keywords:
emotion_scores[emotion] += text.count(keyword) * 0.5
# 根据分数选择最可能的情感
max_score = max(emotion_scores.values())
if max_score == 0:
return "happy" # 默认
# 可能有多个情感同分,根据上下文选择最合适的
top_emotions = [e for e, s in emotion_scores.items() if s == max_score]
# 如果多个情感同分,使用以下优先级
priority_order = [
"laughing",
"crying",
"angry",
"surprised",
"shocked", # 强烈情感优先
"loving",
"happy",
"funny",
"cool", # 积极情感
"sad",
"embarrassed",
"confused", # 消极情感
"thinking",
"winking",
"relaxed", # 中性情感
"delicious",
"kissy",
"confident",
"sleepy",
"silly", # 特殊场景
]
for emotion in priority_order:
if emotion in top_emotions:
return emotion
return top_emotions[0] # 如果都不在优先级列表里,返回第一个
def audio_to_data(audio_file_path, is_opus=True):
# 获取文件后缀名
file_type = os.path.splitext(audio_file_path)[1]
if file_type:
file_type = file_type.lstrip(".")
# 读取音频文件,-nostdin 参数:不要从标准输入读取数据,否则FFmpeg会阻塞
audio = AudioSegment.from_file(
audio_file_path, format=file_type, parameters=["-nostdin"]
)
# 转换为单声道/16kHz采样率/16位小端编码(确保与编码器匹配)
audio = audio.set_channels(1).set_frame_rate(16000).set_sample_width(2)
# 音频时长(秒)
duration = len(audio) / 1000.0
# 获取原始PCM数据(16位小端)
raw_data = audio.raw_data
# 初始化Opus编码器
encoder = opuslib_next.Encoder(16000, 1, opuslib_next.APPLICATION_AUDIO)
# 编码参数
frame_duration = 60 # 60ms per frame
frame_size = int(16000 * frame_duration / 1000) # 960 samples/frame
datas = []
# 按帧处理所有音频数据(包括最后一帧可能补零)
for i in range(0, len(raw_data), frame_size * 2): # 16bit=2bytes/sample
# 获取当前帧的二进制数据
chunk = raw_data[i : i + frame_size * 2]
# 如果最后一帧不足,补零
if len(chunk) < frame_size * 2:
chunk += b"\x00" * (frame_size * 2 - len(chunk))
if is_opus:
# 转换为numpy数组处理
np_frame = np.frombuffer(chunk, dtype=np.int16)
# 编码Opus数据
frame_data = encoder.encode(np_frame.tobytes(), frame_size)
else:
frame_data = chunk if isinstance(chunk, bytes) else bytes(chunk)
datas.append(frame_data)
return datas, duration
def check_vad_update(before_config, new_config):
if (
new_config.get("selected_module") is None
or new_config["selected_module"].get("VAD") is None
):
return False
update_vad = False
current_vad_module = before_config["selected_module"]["VAD"]
new_vad_module = new_config["selected_module"]["VAD"]
current_vad_type = (
current_vad_module
if "type" not in before_config["VAD"][current_vad_module]
else before_config["VAD"][current_vad_module]["type"]
)
new_vad_type = (
new_vad_module
if "type" not in new_config["VAD"][new_vad_module]
else new_config["VAD"][new_vad_module]["type"]
)
update_vad = current_vad_type != new_vad_type
return update_vad
def check_asr_update(before_config, new_config):
if (
new_config.get("selected_module") is None
or new_config["selected_module"].get("ASR") is None
):
return False
update_asr = False
current_asr_module = before_config["selected_module"]["ASR"]
new_asr_module = new_config["selected_module"]["ASR"]
current_asr_type = (
current_asr_module
if "type" not in before_config["ASR"][current_asr_module]
else before_config["ASR"][current_asr_module]["type"]
)
new_asr_type = (
new_asr_module
if "type" not in new_config["ASR"][new_asr_module]
else new_config["ASR"][new_asr_module]["type"]
)
update_asr = current_asr_type != new_asr_type
return update_asr
def filter_sensitive_info(config: dict) -> dict:
"""
过滤配置中的敏感信息
Args:
config: 原始配置字典
Returns:
过滤后的配置字典
"""
sensitive_keys = [
"api_key",
"personal_access_token",
"access_token",
"token",
"secret",
"access_key_secret",
"secret_key",
]
def _filter_dict(d: dict) -> dict:
filtered = {}
for k, v in d.items():
if any(sensitive in k.lower() for sensitive in sensitive_keys):
filtered[k] = "***"
elif isinstance(v, dict):
filtered[k] = _filter_dict(v)
elif isinstance(v, list):
filtered[k] = [_filter_dict(i) if isinstance(i, dict) else i for i in v]
else:
filtered[k] = v
return filtered
return _filter_dict(copy.deepcopy(config))
+12 -70
View File
@@ -1,77 +1,19 @@
from abc import ABC, abstractmethod
import importlib
import os
import sys
from core.providers.vad.base import VADProviderBase
from config.logger import setup_logging
import opuslib_next
import time
import numpy as np
import torch
TAG = __name__
logger = setup_logging()
class VAD(ABC):
@abstractmethod
def is_vad(self, conn, data):
"""检测音频数据中的语音活动"""
pass
def create_instance(class_name: str, *args, **kwargs) -> VADProviderBase:
"""工厂方法创建VAD实例"""
if os.path.exists(os.path.join("core", "providers", "vad", f"{class_name}.py")):
lib_name = f"core.providers.vad.{class_name}"
if lib_name not in sys.modules:
sys.modules[lib_name] = importlib.import_module(f"{lib_name}")
return sys.modules[lib_name].VADProvider(*args, **kwargs)
class SileroVAD(VAD):
def __init__(self, config):
logger.bind(tag=TAG).info("SileroVAD", config)
self.model, self.utils = torch.hub.load(repo_or_dir=config["model_dir"],
source='local',
model='silero_vad',
force_reload=False)
(get_speech_timestamps, _, _, _, _) = self.utils
self.decoder = opuslib_next.Decoder(16000, 1)
self.vad_threshold = config.get("threshold")
self.silence_threshold_ms = config.get("min_silence_duration_ms")
def is_vad(self, conn, opus_packet):
try:
pcm_frame = self.decoder.decode(opus_packet, 960)
conn.client_audio_buffer.extend(pcm_frame) # 将新数据加入缓冲区
# 处理缓冲区中的完整帧(每次处理512采样点)
client_have_voice = False
while len(conn.client_audio_buffer) >= 512 * 2:
# 提取前512个采样点(1024字节)
chunk = conn.client_audio_buffer[:512 * 2]
conn.client_audio_buffer = conn.client_audio_buffer[512 * 2:]
# 转换为模型需要的张量格式
audio_int16 = np.frombuffer(chunk, dtype=np.int16)
audio_float32 = audio_int16.astype(np.float32) / 32768.0
audio_tensor = torch.from_numpy(audio_float32)
# 检测语音活动
speech_prob = self.model(audio_tensor, 16000).item()
client_have_voice = speech_prob >= self.vad_threshold
# 如果之前有声音,但本次没有声音,且与上次有声音的时间查已经超过了静默阈值,则认为已经说完一句话
if conn.client_have_voice and not client_have_voice:
stop_duration = time.time() * 1000 - conn.client_have_voice_last_time
if stop_duration >= self.silence_threshold_ms:
conn.client_voice_stop = True
if client_have_voice:
conn.client_have_voice = True
conn.client_have_voice_last_time = time.time() * 1000
return client_have_voice
except opuslib_next.OpusError as e:
logger.bind(tag=TAG).info(f"解码错误: {e}")
except Exception as e:
logger.bind(tag=TAG).error(f"Error processing audio packet: {e}")
def create_instance(class_name, *args, **kwargs) -> VAD:
# 获取类对象
cls_map = {
"SileroVAD": SileroVAD,
# 可扩展其他SileroVAD实现
}
if cls := cls_map.get(class_name):
return cls(*args, **kwargs)
raise ValueError(f"不支持的SileroVAD类型: {class_name}")
raise ValueError(f"不支持的VAD类型: {class_name},请检查该配置的type是否设置正确")