mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-26 09:03:54 +08:00
Merge branch 'main' into py_test
This commit is contained in:
@@ -0,0 +1,72 @@
|
|||||||
|
登录AutoDL,租赁镜像
|
||||||
|
选择镜像:
|
||||||
|
```
|
||||||
|
PyTorch / 2.1.0 / 3.10(ubuntu22.04) / cuda 12.1
|
||||||
|
```
|
||||||
|
|
||||||
|
机器开机后,设置学术加速
|
||||||
|
```
|
||||||
|
source /etc/network_turbo
|
||||||
|
```
|
||||||
|
|
||||||
|
进入工作目录
|
||||||
|
```
|
||||||
|
cd autodl-tmp/
|
||||||
|
```
|
||||||
|
|
||||||
|
拉取项目
|
||||||
|
```
|
||||||
|
git clone https://gitclone.com/github.com/fishaudio/fish-speech.git ; cd fish-speech
|
||||||
|
```
|
||||||
|
|
||||||
|
安装依赖
|
||||||
|
```
|
||||||
|
pip install -e.
|
||||||
|
```
|
||||||
|
|
||||||
|
如果报错,安装portaudio
|
||||||
|
```
|
||||||
|
apt-get install portaudio19-dev -y
|
||||||
|
```
|
||||||
|
|
||||||
|
安装后执行
|
||||||
|
```
|
||||||
|
pip install torch==2.3.1 torchvision==0.18.1 torchaudio==2.3.1 --index-url https://download.pytorch.org/whl/cu121
|
||||||
|
```
|
||||||
|
|
||||||
|
下载模型
|
||||||
|
```
|
||||||
|
cd tools
|
||||||
|
python download_models.py
|
||||||
|
```
|
||||||
|
|
||||||
|
下载完模型后运行接口
|
||||||
|
```
|
||||||
|
python -m tools.api_server --listen 0.0.0.0:6006
|
||||||
|
```
|
||||||
|
|
||||||
|
然后用浏览器去到aotodl实例页面
|
||||||
|
```
|
||||||
|
https://autodl.com/console/instance/list
|
||||||
|
```
|
||||||
|
|
||||||
|
如下图点击你刚才机器的`自定义服务`按钮,开启端口转发服务
|
||||||
|

|
||||||
|
|
||||||
|
端口转发服务设置完成后,你本地电脑打开网址`http://localhost:6006/`,就可以访问fish-speech的接口了
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
如果你是单模块部署,核心配置如下
|
||||||
|
```
|
||||||
|
selected_module:
|
||||||
|
TTS: FishSpeech
|
||||||
|
TTS:
|
||||||
|
FishSpeech:
|
||||||
|
reference_audio: ["config/assets/wakeup_words.wav",]
|
||||||
|
reference_text: ["哈啰啊,我是小智啦,声音好听的台湾女孩一枚,超开心认识你耶,最近在忙啥,别忘了给我来点有趣的料哦,我超爱听八卦的啦",]
|
||||||
|
api_key: "123"
|
||||||
|
api_url: "http://127.0.0.1:6006/v1/tts"
|
||||||
|
```
|
||||||
|
|
||||||
|
然后重启服务
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 429 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 248 KiB |
@@ -1,5 +1,7 @@
|
|||||||
package xiaozhi.common.constant;
|
package xiaozhi.common.constant;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 常量
|
* 常量
|
||||||
* Copyright (c) 人人开源 All rights reserved.
|
* Copyright (c) 人人开源 All rights reserved.
|
||||||
@@ -109,6 +111,11 @@ public interface Constant {
|
|||||||
*/
|
*/
|
||||||
String FILE_EXTENSION_SEG = ".";
|
String FILE_EXTENSION_SEG = ".";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 无记忆
|
||||||
|
*/
|
||||||
|
String MEMORY_NO_MEM = "Memory_nomem";
|
||||||
|
|
||||||
enum SysBaseParam {
|
enum SysBaseParam {
|
||||||
/**
|
/**
|
||||||
* 系统全称
|
* 系统全称
|
||||||
@@ -174,8 +181,23 @@ public interface Constant {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
enum ChatHistoryConfEnum {
|
||||||
|
IGNORE(0, "不记录"),
|
||||||
|
RECORD_TEXT(1, "记录文本"),
|
||||||
|
RECORD_TEXT_AUDIO(2, "文本音频都记录");
|
||||||
|
|
||||||
|
private final int code;
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
ChatHistoryConfEnum(int code, String name) {
|
||||||
|
this.code = code;
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 版本号
|
* 版本号
|
||||||
*/
|
*/
|
||||||
public static final String VERSION = "0.4.2";
|
public static final String VERSION = "0.4.3";
|
||||||
}
|
}
|
||||||
+14
-1
@@ -109,6 +109,7 @@ public class AgentController {
|
|||||||
entity.setMemModelId(template.getMemModelId());
|
entity.setMemModelId(template.getMemModelId());
|
||||||
entity.setIntentModelId(template.getIntentModelId());
|
entity.setIntentModelId(template.getIntentModelId());
|
||||||
entity.setSystemPrompt(template.getSystemPrompt());
|
entity.setSystemPrompt(template.getSystemPrompt());
|
||||||
|
entity.setChatHistoryConf(template.getChatHistoryConf());
|
||||||
entity.setLangCode(template.getLangCode());
|
entity.setLangCode(template.getLangCode());
|
||||||
entity.setLanguage(template.getLanguage());
|
entity.setLanguage(template.getLanguage());
|
||||||
}
|
}
|
||||||
@@ -166,6 +167,9 @@ public class AgentController {
|
|||||||
if (dto.getSystemPrompt() != null) {
|
if (dto.getSystemPrompt() != null) {
|
||||||
existingEntity.setSystemPrompt(dto.getSystemPrompt());
|
existingEntity.setSystemPrompt(dto.getSystemPrompt());
|
||||||
}
|
}
|
||||||
|
if (dto.getChatHistoryConf() != null) {
|
||||||
|
existingEntity.setChatHistoryConf(dto.getChatHistoryConf());
|
||||||
|
}
|
||||||
if (dto.getLangCode() != null) {
|
if (dto.getLangCode() != null) {
|
||||||
existingEntity.setLangCode(dto.getLangCode());
|
existingEntity.setLangCode(dto.getLangCode());
|
||||||
}
|
}
|
||||||
@@ -183,6 +187,15 @@ public class AgentController {
|
|||||||
|
|
||||||
agentService.updateById(existingEntity);
|
agentService.updateById(existingEntity);
|
||||||
|
|
||||||
|
// 更新记忆策略
|
||||||
|
if (existingEntity.getMemModelId() == null || existingEntity.getMemModelId().equals(Constant.MEMORY_NO_MEM)) {
|
||||||
|
// 删除所有记录
|
||||||
|
agentChatHistoryService.deleteByAgentId(existingEntity.getId(), true, true);
|
||||||
|
} else if (existingEntity.getChatHistoryConf() != null && existingEntity.getChatHistoryConf() == 1) {
|
||||||
|
// 删除音频数据
|
||||||
|
agentChatHistoryService.deleteByAgentId(existingEntity.getId(), true, false);
|
||||||
|
}
|
||||||
|
|
||||||
return new Result<>();
|
return new Result<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,7 +206,7 @@ public class AgentController {
|
|||||||
// 先删除关联的设备
|
// 先删除关联的设备
|
||||||
deviceService.deleteByAgentId(id);
|
deviceService.deleteByAgentId(id);
|
||||||
// 删除关联的聊天记录
|
// 删除关联的聊天记录
|
||||||
agentChatHistoryService.deleteByAgentId(id);
|
agentChatHistoryService.deleteByAgentId(id, true, true);
|
||||||
// 再删除智能体
|
// 再删除智能体
|
||||||
agentService.deleteById(id);
|
agentService.deleteById(id);
|
||||||
return new Result<>();
|
return new Result<>();
|
||||||
|
|||||||
@@ -28,4 +28,11 @@ public interface AiAgentChatHistoryDao extends BaseMapper<AgentChatHistoryEntity
|
|||||||
* @param agentId 智能体ID
|
* @param agentId 智能体ID
|
||||||
*/
|
*/
|
||||||
void deleteHistoryByAgentId(String agentId);
|
void deleteHistoryByAgentId(String agentId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据智能体ID删除音频ID
|
||||||
|
*
|
||||||
|
* @param agentId 智能体ID
|
||||||
|
*/
|
||||||
|
void deleteAudioIdByAgentId(String agentId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ public class AgentDTO {
|
|||||||
@Schema(description = "大语言模型名称", example = "llm_model_01")
|
@Schema(description = "大语言模型名称", example = "llm_model_01")
|
||||||
private String llmModelName;
|
private String llmModelName;
|
||||||
|
|
||||||
|
@Schema(description = "记忆模型ID", example = "mem_model_01")
|
||||||
|
private String memModelId;
|
||||||
|
|
||||||
@Schema(description = "角色设定参数", example = "你是一个专业的客服助手,负责回答用户问题并提供帮助")
|
@Schema(description = "角色设定参数", example = "你是一个专业的客服助手,负责回答用户问题并提供帮助")
|
||||||
private String systemPrompt;
|
private String systemPrompt;
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ public class AgentUpdateDTO implements Serializable {
|
|||||||
@Schema(description = "角色设定参数", example = "你是一个专业的客服助手,负责回答用户问题并提供帮助", required = false)
|
@Schema(description = "角色设定参数", example = "你是一个专业的客服助手,负责回答用户问题并提供帮助", required = false)
|
||||||
private String systemPrompt;
|
private String systemPrompt;
|
||||||
|
|
||||||
|
@Schema(description = "聊天记录配置(0不记录 1仅记录文本 2记录文本和语音)", example = "3", required = false)
|
||||||
|
private Integer chatHistoryConf;
|
||||||
|
|
||||||
@Schema(description = "语言编码", example = "zh_CN", required = false)
|
@Schema(description = "语言编码", example = "zh_CN", required = false)
|
||||||
private String langCode;
|
private String langCode;
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ public class AgentEntity {
|
|||||||
@Schema(description = "意图模型标识")
|
@Schema(description = "意图模型标识")
|
||||||
private String intentModelId;
|
private String intentModelId;
|
||||||
|
|
||||||
|
@Schema(description = "聊天记录配置(0不记录 1仅记录文本 2记录文本和语音)")
|
||||||
|
private Integer chatHistoryConf;
|
||||||
|
|
||||||
@Schema(description = "角色设定参数")
|
@Schema(description = "角色设定参数")
|
||||||
private String systemPrompt;
|
private String systemPrompt;
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,11 @@ public class AgentTemplateEntity implements Serializable {
|
|||||||
*/
|
*/
|
||||||
private String intentModelId;
|
private String intentModelId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 聊天记录配置(0不记录 1仅记录文本 2记录文本和语音)
|
||||||
|
*/
|
||||||
|
private Integer chatHistoryConf;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 角色设定参数
|
* 角色设定参数
|
||||||
*/
|
*/
|
||||||
|
|||||||
+4
-2
@@ -39,7 +39,9 @@ public interface AgentChatHistoryService extends IService<AgentChatHistoryEntity
|
|||||||
/**
|
/**
|
||||||
* 根据智能体ID删除聊天记录
|
* 根据智能体ID删除聊天记录
|
||||||
*
|
*
|
||||||
* @param agentId 智能体ID
|
* @param agentId 智能体ID
|
||||||
|
* @param deleteAudio 是否删除音频
|
||||||
|
* @param deleteText 是否删除文本
|
||||||
*/
|
*/
|
||||||
void deleteByAgentId(String agentId);
|
void deleteByAgentId(String agentId, Boolean deleteAudio, Boolean deleteText);
|
||||||
}
|
}
|
||||||
|
|||||||
+39
-15
@@ -2,12 +2,14 @@ package xiaozhi.modules.agent.service.biz.impl;
|
|||||||
|
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import xiaozhi.common.constant.Constant;
|
||||||
import xiaozhi.common.redis.RedisKeys;
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
import xiaozhi.common.redis.RedisUtils;
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
import xiaozhi.modules.agent.dto.AgentChatHistoryReportDTO;
|
import xiaozhi.modules.agent.dto.AgentChatHistoryReportDTO;
|
||||||
@@ -47,8 +49,33 @@ public class AgentChatHistoryBizServiceImpl implements AgentChatHistoryBizServic
|
|||||||
Byte chatType = report.getChatType();
|
Byte chatType = report.getChatType();
|
||||||
log.info("小智设备聊天上报请求: macAddress={}, type={}", macAddress, chatType);
|
log.info("小智设备聊天上报请求: macAddress={}, type={}", macAddress, chatType);
|
||||||
|
|
||||||
// 1. base64解码report.getOpusDataBase64(),存入ai_agent_chat_audio表
|
// 根据设备MAC地址查询对应的默认智能体,判断是否需要上报
|
||||||
|
AgentEntity agentEntity = agentService.getDefaultAgentByMacAddress(macAddress);
|
||||||
|
if (agentEntity == null) {
|
||||||
|
return Boolean.FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
Integer chatHistoryConf = agentEntity.getChatHistoryConf();
|
||||||
|
String agentId = agentEntity.getId();
|
||||||
|
|
||||||
|
if (Objects.equals(chatHistoryConf, Constant.ChatHistoryConfEnum.RECORD_TEXT.getCode())) {
|
||||||
|
saveChatText(report, agentId, macAddress, null);
|
||||||
|
} else if (Objects.equals(chatHistoryConf, Constant.ChatHistoryConfEnum.RECORD_TEXT_AUDIO.getCode())) {
|
||||||
|
String audioId = saveChatAudio(report);
|
||||||
|
saveChatText(report, agentId, macAddress, audioId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新设备最后对话时间
|
||||||
|
redisUtils.set(RedisKeys.getAgentDeviceLastConnectedAtById(agentId), new Date());
|
||||||
|
return Boolean.TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* base64解码report.getOpusDataBase64(),存入ai_agent_chat_audio表
|
||||||
|
*/
|
||||||
|
private String saveChatAudio(AgentChatHistoryReportDTO report) {
|
||||||
String audioId = null;
|
String audioId = null;
|
||||||
|
|
||||||
if (report.getAudioBase64() != null && !report.getAudioBase64().isEmpty()) {
|
if (report.getAudioBase64() != null && !report.getAudioBase64().isEmpty()) {
|
||||||
try {
|
try {
|
||||||
byte[] audioData = Base64.getDecoder().decode(report.getAudioBase64());
|
byte[] audioData = Base64.getDecoder().decode(report.getAudioBase64());
|
||||||
@@ -56,20 +83,18 @@ public class AgentChatHistoryBizServiceImpl implements AgentChatHistoryBizServic
|
|||||||
log.info("音频数据保存成功,audioId={}", audioId);
|
log.info("音频数据保存成功,audioId={}", audioId);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("音频数据保存失败", e);
|
log.error("音频数据保存失败", e);
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return audioId;
|
||||||
|
}
|
||||||
|
|
||||||
// 2. 组装上报数据
|
/**
|
||||||
// 2.1 根据设备MAC地址查询对应的默认智能体,判断是否需要上报
|
* 组装上报数据
|
||||||
AgentEntity agentEntity = agentService.getDefaultAgentByMacAddress(macAddress);
|
*/
|
||||||
if (agentEntity == null) {
|
private void saveChatText(AgentChatHistoryReportDTO report, String agentId, String macAddress, String audioId) {
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String agentId = agentEntity.getId();
|
|
||||||
log.info("设备 {} 对应智能体 {} 上报", macAddress, agentEntity.getId());
|
|
||||||
|
|
||||||
// 2.2 构建聊天记录实体
|
// 构建聊天记录实体
|
||||||
AgentChatHistoryEntity entity = AgentChatHistoryEntity.builder()
|
AgentChatHistoryEntity entity = AgentChatHistoryEntity.builder()
|
||||||
.macAddress(macAddress)
|
.macAddress(macAddress)
|
||||||
.agentId(agentId)
|
.agentId(agentId)
|
||||||
@@ -79,10 +104,9 @@ public class AgentChatHistoryBizServiceImpl implements AgentChatHistoryBizServic
|
|||||||
.audioId(audioId)
|
.audioId(audioId)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// 3. 保存数据
|
// 保存数据
|
||||||
agentChatHistoryService.save(entity);
|
agentChatHistoryService.save(entity);
|
||||||
// 4. 更新设备最后对话时间
|
|
||||||
redisUtils.set(RedisKeys.getAgentDeviceLastConnectedAtById(agentId), new Date());
|
log.info("设备 {} 对应智能体 {} 上报成功", macAddress, agentId);
|
||||||
return Boolean.TRUE;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-3
@@ -78,8 +78,16 @@ public class AgentChatHistoryServiceImpl extends ServiceImpl<AiAgentChatHistoryD
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void deleteByAgentId(String agentId) {
|
public void deleteByAgentId(String agentId, Boolean deleteAudio, Boolean deleteText) {
|
||||||
baseMapper.deleteAudioByAgentId(agentId);
|
if (deleteAudio) {
|
||||||
baseMapper.deleteHistoryByAgentId(agentId);
|
baseMapper.deleteAudioByAgentId(agentId);
|
||||||
|
}
|
||||||
|
if (deleteAudio && !deleteText) {
|
||||||
|
baseMapper.deleteAudioIdByAgentId(agentId);
|
||||||
|
}
|
||||||
|
if (deleteText) {
|
||||||
|
baseMapper.deleteHistoryByAgentId(agentId);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-1
@@ -13,6 +13,7 @@ import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
|||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
import xiaozhi.common.constant.Constant;
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
import xiaozhi.common.redis.RedisKeys;
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
import xiaozhi.common.redis.RedisUtils;
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
@@ -46,7 +47,15 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AgentEntity getAgentById(String id) {
|
public AgentEntity getAgentById(String id) {
|
||||||
return agentDao.selectById(id);
|
AgentEntity agent = agentDao.selectById(id);
|
||||||
|
if (agent != null && agent.getMemModelId() != null && agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)) {
|
||||||
|
agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.IGNORE.getCode());
|
||||||
|
} else if (agent != null && agent.getMemModelId() != null
|
||||||
|
&& !agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)
|
||||||
|
&& agent.getChatHistoryConf() == null) {
|
||||||
|
agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.RECORD_TEXT_AUDIO.getCode());
|
||||||
|
}
|
||||||
|
return agent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -93,6 +102,9 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
|||||||
// 获取 LLM 模型名称
|
// 获取 LLM 模型名称
|
||||||
dto.setLlmModelName(modelConfigService.getModelNameById(agent.getLlmModelId()));
|
dto.setLlmModelName(modelConfigService.getModelNameById(agent.getLlmModelId()));
|
||||||
|
|
||||||
|
// 获取记忆模型名称
|
||||||
|
dto.setMemModelId(agent.getMemModelId());
|
||||||
|
|
||||||
// 获取 TTS 音色名称
|
// 获取 TTS 音色名称
|
||||||
dto.setTtsVoiceName(timbreModelService.getTimbreNameById(agent.getTtsVoiceId()));
|
dto.setTtsVoiceName(timbreModelService.getTimbreNameById(agent.getTtsVoiceId()));
|
||||||
|
|
||||||
|
|||||||
+12
@@ -9,6 +9,7 @@ import org.apache.commons.lang3.StringUtils;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
import xiaozhi.common.constant.Constant;
|
||||||
import xiaozhi.common.exception.ErrorCode;
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
import xiaozhi.common.exception.RenException;
|
import xiaozhi.common.exception.RenException;
|
||||||
import xiaozhi.common.redis.RedisKeys;
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
@@ -108,6 +109,17 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
// 获取单台设备每天最多输出字数
|
// 获取单台设备每天最多输出字数
|
||||||
String deviceMaxOutputSize = sysParamsService.getValue("device_max_output_size", true);
|
String deviceMaxOutputSize = sysParamsService.getValue("device_max_output_size", true);
|
||||||
result.put("device_max_output_size", deviceMaxOutputSize);
|
result.put("device_max_output_size", deviceMaxOutputSize);
|
||||||
|
|
||||||
|
// 获取聊天记录配置
|
||||||
|
Integer chatHistoryConf = agent.getChatHistoryConf();
|
||||||
|
if (agent.getMemModelId() != null && agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)) {
|
||||||
|
chatHistoryConf = Constant.ChatHistoryConfEnum.IGNORE.getCode();
|
||||||
|
} else if (agent.getMemModelId() != null
|
||||||
|
&& !agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)
|
||||||
|
&& agent.getChatHistoryConf() == null) {
|
||||||
|
chatHistoryConf = Constant.ChatHistoryConfEnum.RECORD_TEXT_AUDIO.getCode();
|
||||||
|
}
|
||||||
|
result.put("chat_history_conf", chatHistoryConf);
|
||||||
// 如果客户端已实例化模型,则不返回
|
// 如果客户端已实例化模型,则不返回
|
||||||
String alreadySelectedVadModelId = (String) selectedModule.get("VAD");
|
String alreadySelectedVadModelId = (String) selectedModule.get("VAD");
|
||||||
if (alreadySelectedVadModelId != null && alreadySelectedVadModelId.equals(agent.getVadModelId())) {
|
if (alreadySelectedVadModelId != null && alreadySelectedVadModelId.equals(agent.getVadModelId())) {
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
-- 更新模型供应器表
|
||||||
|
UPDATE `ai_model_provider` SET fields = '[{"key": "host", "type": "string", "label": "服务地址"}, {"key": "port", "type": "number", "label": "端口号"}, {"key": "type", "type": "string", "label": "服务类型"}, {"key": "is_ssl", "type": "boolean", "label": "是否使用SSL"}, {"key": "api_key", "type": "string", "label": "API密钥"}, {"key": "output_dir", "type": "string", "label": "输出目录"}]' WHERE id = 'SYSTEM_ASR_FunASRServer';
|
||||||
|
|
||||||
|
-- 更新模型配置表
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
config_json = '{"host": "127.0.0.1", "port": 10096, "type": "fun_server", "is_ssl": true, "api_key": "none", "output_dir": "tmp/"}',
|
||||||
|
`doc_link` = 'https://github.com/modelscope/FunASR/blob/main/runtime/docs/SDK_advanced_guide_online_zh.md',
|
||||||
|
`remark` = '独立部署FunASR,使用FunASR的API服务,只需要五句话
|
||||||
|
第一句:mkdir -p ./funasr-runtime-resources/models
|
||||||
|
第二句:sudo docker run -p 10096:10095 -it --privileged=true -v $PWD/funasr-runtime-resources/models:/workspace/models registry.cn-hangzhou.aliyuncs.com/funasr_repo/funasr:funasr-runtime-sdk-online-cpu-0.1.12
|
||||||
|
上一句话执行后会进入到容器,继续第三句:cd FunASR/runtime
|
||||||
|
不要退出容器,继续在容器中执行第四句:nohup bash run_server_2pass.sh --download-model-dir /workspace/models --vad-dir damo/speech_fsmn_vad_zh-cn-16k-common-onnx --model-dir damo/speech_paraformer-large-vad-punc_asr_nat-zh-cn-16k-common-vocab8404-onnx --online-model-dir damo/speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-online-onnx --punc-dir damo/punc_ct-transformer_zh-cn-common-vad_realtime-vocab272727-onnx --lm-dir damo/speech_ngram_lm_zh-cn-ai-wesp-fst --itn-dir thuduj12/fst_itn_zh --hotword /workspace/models/hotwords.txt > log.txt 2>&1 &
|
||||||
|
上一句话执行后会进入到容器,继续第五句:tail -f log.txt
|
||||||
|
第五句话执行完后,会看到模型下载日志,下载完后就可以连接使用了
|
||||||
|
以上是使用CPU推理,如果有GPU,详细参考:https://github.com/modelscope/FunASR/blob/main/runtime/docs/SDK_advanced_guide_online_zh.md' WHERE `id` = 'ASR_FunASRServer';
|
||||||
|
|
||||||
|
-- FishSpeech配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = 'https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docs/fish-speech-integration.md',
|
||||||
|
`remark` = 'FishSpeech配置说明:
|
||||||
|
1. 需要本地部署FishSpeech服务
|
||||||
|
2. 支持自定义音色
|
||||||
|
3. 本地推理,无需网络连接
|
||||||
|
4. 输出文件保存在tmp/目录
|
||||||
|
5. 可参照教程https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docs/fish-speech-integration.md' WHERE `id` = 'TTS_FishSpeech';
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
-- 添加聊天记录配置字段
|
||||||
|
ALTER TABLE `ai_agent`
|
||||||
|
ADD COLUMN `chat_history_conf` tinyint NOT NULL DEFAULT 0 COMMENT '聊天记录配置(0不记录 1仅记录文本 2记录文本和语音)' AFTER `system_prompt`;
|
||||||
|
|
||||||
|
ALTER TABLE `ai_agent_template`
|
||||||
|
ADD COLUMN `chat_history_conf` tinyint NOT NULL DEFAULT 0 COMMENT '聊天记录配置(0不记录 1仅记录文本 2记录文本和语音)' AFTER `system_prompt`;
|
||||||
@@ -114,3 +114,17 @@ databaseChangeLog:
|
|||||||
- sqlFile:
|
- sqlFile:
|
||||||
encoding: utf8
|
encoding: utf8
|
||||||
path: classpath:db/changelog/202505091409.sql
|
path: classpath:db/changelog/202505091409.sql
|
||||||
|
- changeSet:
|
||||||
|
id: 202505091555
|
||||||
|
author: whosmyqueen
|
||||||
|
changes:
|
||||||
|
- sqlFile:
|
||||||
|
encoding: utf8
|
||||||
|
path: classpath:db/changelog/202505091555.sql
|
||||||
|
- changeSet:
|
||||||
|
id: 202505111914
|
||||||
|
author: hrz
|
||||||
|
changes:
|
||||||
|
- sqlFile:
|
||||||
|
encoding: utf8
|
||||||
|
path: classpath:db/changelog/202505111914.sql
|
||||||
|
|||||||
@@ -28,11 +28,17 @@
|
|||||||
SELECT audio_id
|
SELECT audio_id
|
||||||
FROM ai_agent_chat_history
|
FROM ai_agent_chat_history
|
||||||
WHERE agent_id = #{agentId}
|
WHERE agent_id = #{agentId}
|
||||||
);
|
)
|
||||||
</delete>
|
</delete>
|
||||||
|
|
||||||
|
<update id="deleteAudioIdByAgentId">
|
||||||
|
UPDATE ai_agent_chat_history
|
||||||
|
SET audio_id = NULL
|
||||||
|
WHERE agent_id = #{agentId}
|
||||||
|
</update>
|
||||||
|
|
||||||
<delete id="deleteHistoryByAgentId">
|
<delete id="deleteHistoryByAgentId">
|
||||||
DELETE FROM ai_agent_chat_history
|
DELETE FROM ai_agent_chat_history
|
||||||
WHERE agent_id = #{agentId};
|
WHERE agent_id = #{agentId}
|
||||||
</delete>
|
</delete>
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
@@ -50,9 +50,9 @@ export default {
|
|||||||
}).send();
|
}).send();
|
||||||
},
|
},
|
||||||
// 获取智能体配置
|
// 获取智能体配置
|
||||||
getDeviceConfig(deviceId, callback) {
|
getDeviceConfig(agentId, callback) {
|
||||||
RequestService.sendRequest()
|
RequestService.sendRequest()
|
||||||
.url(`${getServiceUrl()}/agent/${deviceId}`)
|
.url(`${getServiceUrl()}/agent/${agentId}`)
|
||||||
.method('GET')
|
.method('GET')
|
||||||
.success((res) => {
|
.success((res) => {
|
||||||
RequestService.clearRequestTime();
|
RequestService.clearRequestTime();
|
||||||
@@ -61,7 +61,7 @@ export default {
|
|||||||
.fail((err) => {
|
.fail((err) => {
|
||||||
console.error('获取配置失败:', err);
|
console.error('获取配置失败:', err);
|
||||||
RequestService.reAjaxFun(() => {
|
RequestService.reAjaxFun(() => {
|
||||||
this.getDeviceConfig(deviceId, callback);
|
this.getDeviceConfig(agentId, callback);
|
||||||
});
|
});
|
||||||
}).send();
|
}).send();
|
||||||
},
|
},
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 101 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 88 KiB |
@@ -26,8 +26,12 @@
|
|||||||
<div class="settings-btn" @click="handleDeviceManage">
|
<div class="settings-btn" @click="handleDeviceManage">
|
||||||
设备管理({{ device.deviceCount }})
|
设备管理({{ device.deviceCount }})
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-btn" @click="handleChatHistory">
|
<div class="settings-btn" @click="handleChatHistory"
|
||||||
聊天记录
|
:class="{ 'disabled-btn': device.memModelId === 'Memory_nomem' }">
|
||||||
|
<el-tooltip v-if="device.memModelId === 'Memory_nomem'" content="未开启记忆" placement="top">
|
||||||
|
<span>聊天记录</span>
|
||||||
|
</el-tooltip>
|
||||||
|
<span v-else>聊天记录</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="version-info">
|
<div class="version-info">
|
||||||
@@ -77,6 +81,9 @@ export default {
|
|||||||
this.$router.push({ path: '/device-management', query: { agentId: this.device.agentId } });
|
this.$router.push({ path: '/device-management', query: { agentId: this.device.agentId } });
|
||||||
},
|
},
|
||||||
handleChatHistory() {
|
handleChatHistory() {
|
||||||
|
if (this.device.memModelId === 'Memory_nomem') {
|
||||||
|
return
|
||||||
|
}
|
||||||
this.$emit('chat-history', { agentId: this.device.agentId, agentName: this.device.agentName })
|
this.$emit('chat-history', { agentId: this.device.agentId, agentName: this.device.agentName })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,6 +127,12 @@ export default {
|
|||||||
color: #979db1;
|
color: #979db1;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.disabled-btn {
|
||||||
|
background: #e6e6e6;
|
||||||
|
color: #999;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog :title="title" :visible.sync="dialogVisible" width="30%" @close="handleClose" @open="handleOpen">
|
<el-dialog :title="title" :visible.sync="dialogVisible" @close="handleClose" @open="handleOpen">
|
||||||
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
|
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
|
||||||
<el-form-item label="固件名称" prop="firmwareName">
|
<el-form-item label="固件名称" prop="firmwareName">
|
||||||
<el-input v-model="form.firmwareName" placeholder="请输入固件名称(板子+版本号)"></el-input>
|
<el-input v-model="form.firmwareName" placeholder="请输入固件名称(板子+版本号)"></el-input>
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
<el-progress v-if="isUploading || uploadStatus === 'success'" :percentage="uploadProgress"
|
<el-progress v-if="isUploading || uploadStatus === 'success'" :percentage="uploadProgress"
|
||||||
:status="uploadStatus"></el-progress>
|
:status="uploadStatus"></el-progress>
|
||||||
<div class="hint-text">
|
<div class="hint-text">
|
||||||
<span>温馨提示:请上传xiaozhi.bin文件,而不是merged-binary.bin文件</span>
|
<span>温馨提示:请上传合并前的xiaozhi.bin文件,而不是合并后的merged-binary.bin文件</span>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="备注" prop="remark">
|
<el-form-item label="备注" prop="remark">
|
||||||
@@ -229,7 +229,6 @@ export default {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
color: #979db1;
|
font-size: 14px;
|
||||||
font-size: 11px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -0,0 +1,410 @@
|
|||||||
|
<template>
|
||||||
|
<el-drawer :visible.sync="dialogVisible" direction="rtl" size="50%" :wrapperClosable="false" :withHeader="false">
|
||||||
|
<!-- 自定义标题区域 -->
|
||||||
|
<div class="custom-header">
|
||||||
|
<div class="header-left">
|
||||||
|
<h3 class="bold-title">功能管理</h3>
|
||||||
|
</div>
|
||||||
|
<button class="custom-close-btn" @click="closeDialog">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="function-manager">
|
||||||
|
<!-- 左侧:未选功能 -->
|
||||||
|
<div class="function-column">
|
||||||
|
<div class="column-header">
|
||||||
|
<h4 class="column-title">未选功能</h4>
|
||||||
|
<el-button type="text" @click="selectAll" class="select-all-btn">全选</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="function-list">
|
||||||
|
<div v-for="func in unselected" :key="func.name" class="function-item">
|
||||||
|
<el-checkbox :label="func.name" v-model="selectedNames" @change="(val) => handleCheckboxChange(func, val)" @click.native.stop></el-checkbox>
|
||||||
|
<div class="func-tag" @click="handleFunctionClick(func)">
|
||||||
|
<div class="color-dot" :style="{backgroundColor: getFunctionColor(func.name)}"></div>
|
||||||
|
<span>{{ func.name }}</span>
|
||||||
|
</div>
|
||||||
|
<el-tooltip class="item" effect="dark" :content="func.description || '暂无功能描述'" placement="top">
|
||||||
|
<img src="@/assets/home/info.png" alt="" class="info-icon">
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 中间:已选功能 -->
|
||||||
|
<div class="function-column">
|
||||||
|
<div class="column-header">
|
||||||
|
<h4 class="column-title">已选功能</h4>
|
||||||
|
<el-button type="text" @click="deselectAll" class="select-all-btn">全选</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="function-list">
|
||||||
|
<div v-for="func in selectedList" :key="func.name" class="function-item">
|
||||||
|
<el-checkbox :label="func.name" v-model="selectedNames" @change="(val) => handleCheckboxChange(func, val)" @click.native.stop></el-checkbox>
|
||||||
|
<div class="func-tag" @click="handleFunctionClick(func)">
|
||||||
|
<div class="color-dot" :style="{backgroundColor: getFunctionColor(func.name)}"></div>
|
||||||
|
<span>{{ func.name }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧:参数配置 -->
|
||||||
|
<div class="params-column">
|
||||||
|
<h4 v-if="currentFunction" class="column-title">参数配置 - {{ currentFunction.name }}</h4>
|
||||||
|
<div v-if="currentFunction" class="params-container">
|
||||||
|
<el-form :model="currentFunction" size="mini" class="param-form" v-loading="loading" element-loading-text="拼命加载中" element-loading-spinner="el-icon-loading" element-loading-background="rgba(255, 255, 255, 0.7)">
|
||||||
|
<el-form-item v-for="(value, key) in currentFunction.params" :key="key" :label="key" class="param-item">
|
||||||
|
<el-input v-model="currentFunction.params[key]" size="mini" class="param-input" @change="(val) => handleParamChange(currentFunction, key, val)"/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
<div v-else class="empty-tip">请选择已配置的功能进行参数设置</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="drawer-footer">
|
||||||
|
<el-button @click="closeDialog">取消</el-button>
|
||||||
|
<el-button type="primary" @click="saveSelection">保存配置</el-button>
|
||||||
|
</div>
|
||||||
|
</el-drawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: Boolean,
|
||||||
|
functions: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
dialogVisible: this.value,
|
||||||
|
selectedNames: [],
|
||||||
|
currentFunction: null,
|
||||||
|
modifiedFunctions: {},
|
||||||
|
allFunctions: [
|
||||||
|
{name: '天气', params: {city: '北京'}, description: '查看指定城市的天气情况'},
|
||||||
|
{name: '新闻', params: {type: '科技'}, description: '获取最新科技类新闻资讯'},
|
||||||
|
{name: '工具', params: {category: '常用'}, description: '提供常用工具集合'},
|
||||||
|
{name: '退出', params: {}, description: '退出当前系统'},
|
||||||
|
{name: '音乐', params: {genre: '流行'}, description: '播放流行音乐'},
|
||||||
|
{name: '翻译', params: {from: '中文', to: '英文'}, description: '提供中英文互译功能'},
|
||||||
|
{name: '计算', params: {precision: '2'}, description: '提供精确计算功能'},
|
||||||
|
{name: '日历', params: {view: '月'}, description: '查看月历视图'}
|
||||||
|
],
|
||||||
|
functionColorMap: [
|
||||||
|
'#FF6B6B', '#4ECDC4', '#45B7D1',
|
||||||
|
'#96CEB4', '#FFEEAD', '#D4A5A5', '#A2836E'
|
||||||
|
],
|
||||||
|
tempFunctions: {},
|
||||||
|
// 添加一个标志位来跟踪是否已经保存
|
||||||
|
hasSaved: false,
|
||||||
|
loading: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
selectedList() {
|
||||||
|
return this.allFunctions.filter(f => this.selectedNames.includes(f.name));
|
||||||
|
},
|
||||||
|
unselected() {
|
||||||
|
return this.allFunctions.filter(f => !this.selectedNames.includes(f.name));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
value(newVal) {
|
||||||
|
this.dialogVisible = newVal;
|
||||||
|
if (newVal) {
|
||||||
|
this.selectedNames = this.functions.map(f => f.name);
|
||||||
|
this.currentFunction = this.selectedList[0] || null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dialogVisible(newVal) {
|
||||||
|
this.$emit('input', newVal);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleFunctionClick(func) {
|
||||||
|
if (this.selectedNames.includes(func.name)) {
|
||||||
|
this.loading = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
const tempFunc = this.tempFunctions[func.name];
|
||||||
|
this.currentFunction = tempFunc ? tempFunc : JSON.parse(JSON.stringify(func));
|
||||||
|
this.loading = false;
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleParamChange(func, key, value) {
|
||||||
|
if (!this.tempFunctions[func.name]) {
|
||||||
|
this.tempFunctions[func.name] = JSON.parse(JSON.stringify(func));
|
||||||
|
}
|
||||||
|
this.tempFunctions[func.name].params[key] = value;
|
||||||
|
},
|
||||||
|
handleCheckboxChange(func, checked) {
|
||||||
|
if (checked) {
|
||||||
|
if (!this.selectedNames.includes(func.name)) {
|
||||||
|
this.selectedNames = [...this.selectedNames, func.name];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.selectedNames = this.selectedNames.filter(name => name !== func.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.selectedList.length > 0) {
|
||||||
|
this.currentFunction = this.selectedList[0];
|
||||||
|
} else {
|
||||||
|
this.currentFunction = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
selectAll() {
|
||||||
|
this.selectedNames = [...this.allFunctions.map(f => f.name)];
|
||||||
|
if (this.selectedList.length > 0) {
|
||||||
|
this.currentFunction = JSON.parse(JSON.stringify(this.selectedList[0]));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
deselectAll() {
|
||||||
|
this.selectedNames = [];
|
||||||
|
this.currentFunction = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
closeDialog() {
|
||||||
|
this.tempFunctions = {};
|
||||||
|
this.selectedNames = this.functions.map(f => f.name);
|
||||||
|
this.currentFunction = null;
|
||||||
|
this.dialogVisible = false;
|
||||||
|
this.$emit('input', false);
|
||||||
|
this.$emit('dialog-closed', false);
|
||||||
|
},
|
||||||
|
|
||||||
|
saveSelection() {
|
||||||
|
Object.keys(this.tempFunctions).forEach(name => {
|
||||||
|
this.modifiedFunctions[name] = JSON.parse(JSON.stringify(this.tempFunctions[name]));
|
||||||
|
});
|
||||||
|
this.tempFunctions = {};
|
||||||
|
this.hasSaved = true;
|
||||||
|
|
||||||
|
const selected = this.selectedList.map(f => {
|
||||||
|
const modified = this.modifiedFunctions[f.name];
|
||||||
|
return modified || f;
|
||||||
|
}).map(f => ({
|
||||||
|
...f,
|
||||||
|
params: JSON.parse(JSON.stringify(f.params))
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.$emit('update-functions', selected);
|
||||||
|
this.dialogVisible = false;
|
||||||
|
this.$message.success('配置保存成功');
|
||||||
|
// 通知父组件对话框已关闭且已保存
|
||||||
|
this.$emit('dialog-closed', true);
|
||||||
|
},
|
||||||
|
|
||||||
|
getFunctionColor(name) {
|
||||||
|
const hash = [...name].reduce((acc, char) => acc + char.charCodeAt(0), 0);
|
||||||
|
return this.functionColorMap[hash % 7];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.function-manager {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(120px, 0.5fr) minmax(120px, 0.5fr) minmax(200px, 2fr);
|
||||||
|
gap: 12px;
|
||||||
|
height: calc(70vh - 60px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-header {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px 24px;
|
||||||
|
border-bottom: 1px solid #EBEEF5;
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bold-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-all-btn {
|
||||||
|
padding: 0;
|
||||||
|
height: auto;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.function-column {
|
||||||
|
position: relative;
|
||||||
|
width: auto;
|
||||||
|
padding: 10px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border-right: 1px solid #EBEEF5;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.function-column::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.function-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.function-item {
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin: 4px 0;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.params-column {
|
||||||
|
min-width: 280px;
|
||||||
|
padding: 10px;
|
||||||
|
overflow-y: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.params-column::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-title {
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.func-tag {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
flex-grow: 1;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-dot {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
margin-right: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-form {
|
||||||
|
::v-deep .el-form-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.el-form-item__label {
|
||||||
|
font-size: 14px !important;
|
||||||
|
color: #606266;
|
||||||
|
text-align: left;
|
||||||
|
padding-right: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-form-item__content {
|
||||||
|
margin-left: 0 !important;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
.el-input__inner {
|
||||||
|
text-align: left;
|
||||||
|
padding-left: 8px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.params-container {
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
min-width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-tip {
|
||||||
|
padding: 20px;
|
||||||
|
color: #909399;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-footer {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
border-top: 1px solid #e8e8e8;
|
||||||
|
padding: 10px 16px;
|
||||||
|
text-align: center;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin-right: 1vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-close-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
right: 10px;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid #cfcfcf;
|
||||||
|
background: none;
|
||||||
|
font-size: 30px;
|
||||||
|
font-weight: lighter;
|
||||||
|
color: #cfcfcf;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 0;
|
||||||
|
outline: none;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-close-btn:hover {
|
||||||
|
color: #409EFF;
|
||||||
|
border-color: #409EFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .el-checkbox__label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -60,12 +60,44 @@
|
|||||||
<div class="form-column">
|
<div class="form-column">
|
||||||
<el-form-item v-for="(model, index) in models" :key="`model-${index}`" :label="model.label"
|
<el-form-item v-for="(model, index) in models" :key="`model-${index}`" :label="model.label"
|
||||||
class="model-item">
|
class="model-item">
|
||||||
<el-select v-model="form.model[model.key]" filterable placeholder="请选择" class="form-select">
|
<div class="model-select-wrapper">
|
||||||
<el-option v-for="(item, optionIndex) in modelOptions[model.type]"
|
<el-select v-model="form.model[model.key]" filterable placeholder="请选择" class="form-select"
|
||||||
:key="`option-${index}-${optionIndex}`" :label="item.label" :value="item.value" />
|
@change="handleModelChange(model.type, $event)">
|
||||||
</el-select>
|
<el-option v-for="(item, optionIndex) in modelOptions[model.type]"
|
||||||
|
:key="`option-${index}-${optionIndex}`" :label="item.label" :value="item.value" />
|
||||||
|
</el-select>
|
||||||
|
<div v-if="showFunctionIcons(model.type)" class="function-icons">
|
||||||
|
<el-tooltip v-for="func in currentFunctions" :key="func.name" effect="dark" placement="top"
|
||||||
|
popper-class="custom-tooltip">
|
||||||
|
<div slot="content">
|
||||||
|
<div><strong>功能名称:</strong> {{ func.name }}</div>
|
||||||
|
<div v-if="Object.keys(func.params).length > 0">
|
||||||
|
<strong>参数配置:</strong>
|
||||||
|
<div v-for="(value, key) in func.params" :key="key">
|
||||||
|
{{ key }}: {{ value }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else>无参数配置</div>
|
||||||
|
</div>
|
||||||
|
<div class="icon-dot" :style="{ backgroundColor: getFunctionColor(func.name) }">
|
||||||
|
{{ func.name.charAt(0) }}
|
||||||
|
</div>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-button class="edit-function-btn" @click="showFunctionDialog = true"
|
||||||
|
:class="{ 'active-btn': showFunctionDialog }">
|
||||||
|
编辑功能
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<div v-if="model.type === 'Memory' && form.model.memModelId !== 'Memory_nomem'"
|
||||||
|
class="chat-history-options">
|
||||||
|
<el-radio-group v-model="form.chatHistoryConf" @change="updateChatHistoryConf">
|
||||||
|
<el-radio-button :label="1">上报文字</el-radio-button>
|
||||||
|
<el-radio-button :label="2">上报文字+语音</el-radio-button>
|
||||||
|
</el-radio-group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="角色音色:">
|
<el-form-item label="角色音色">
|
||||||
<el-select v-model="form.ttsVoiceId" placeholder="请选择" class="form-select">
|
<el-select v-model="form.ttsVoiceId" placeholder="请选择" class="form-select">
|
||||||
<el-option v-for="(item, index) in voiceOptions" :key="`voice-${index}`" :label="item.label"
|
<el-option v-for="(item, index) in voiceOptions" :key="`voice-${index}`" :label="item.label"
|
||||||
:value="item.value" />
|
:value="item.value" />
|
||||||
@@ -75,27 +107,31 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
</el-card>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<function-dialog v-model="showFunctionDialog" :functions="currentFunctions"
|
||||||
|
@update-functions="handleUpdateFunctions" @dialog-closed="handleDialogClosed" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Api from '@/apis/api';
|
import Api from '@/apis/api';
|
||||||
|
import FunctionDialog from "@/components/FunctionDialog.vue";
|
||||||
import HeaderBar from "@/components/HeaderBar.vue";
|
import HeaderBar from "@/components/HeaderBar.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'RoleConfigPage',
|
name: 'RoleConfigPage',
|
||||||
components: { HeaderBar },
|
components: { HeaderBar, FunctionDialog },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
form: {
|
form: {
|
||||||
agentCode: "",
|
agentCode: "",
|
||||||
agentName: "",
|
agentName: "",
|
||||||
ttsVoiceId: "",
|
ttsVoiceId: "",
|
||||||
|
chatHistoryConf: 0,
|
||||||
systemPrompt: "",
|
systemPrompt: "",
|
||||||
langCode: "",
|
langCode: "",
|
||||||
language: "",
|
language: "",
|
||||||
@@ -121,6 +157,18 @@ export default {
|
|||||||
templates: [],
|
templates: [],
|
||||||
loadingTemplate: false,
|
loadingTemplate: false,
|
||||||
voiceOptions: [],
|
voiceOptions: [],
|
||||||
|
showFunctionDialog: false,
|
||||||
|
currentFunctions: [],
|
||||||
|
functionColorMap: [
|
||||||
|
'#FF6B6B', '#4ECDC4', '#45B7D1',
|
||||||
|
'#96CEB4', '#FFEEAD', '#D4A5A5', '#A2836E'
|
||||||
|
],
|
||||||
|
allFunctions: [
|
||||||
|
{ name: '天气', params: {} },
|
||||||
|
{ name: '新闻', params: {} },
|
||||||
|
{ name: '工具', params: {} },
|
||||||
|
{ name: '退出', params: {} }
|
||||||
|
],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -136,12 +184,14 @@ export default {
|
|||||||
llmModelId: this.form.model.llmModelId,
|
llmModelId: this.form.model.llmModelId,
|
||||||
ttsModelId: this.form.model.ttsModelId,
|
ttsModelId: this.form.model.ttsModelId,
|
||||||
ttsVoiceId: this.form.ttsVoiceId,
|
ttsVoiceId: this.form.ttsVoiceId,
|
||||||
|
chatHistoryConf: this.form.chatHistoryConf,
|
||||||
memModelId: this.form.model.memModelId,
|
memModelId: this.form.model.memModelId,
|
||||||
intentModelId: this.form.model.intentModelId,
|
intentModelId: this.form.model.intentModelId,
|
||||||
systemPrompt: this.form.systemPrompt,
|
systemPrompt: this.form.systemPrompt,
|
||||||
langCode: this.form.langCode,
|
langCode: this.form.langCode,
|
||||||
language: this.form.language,
|
language: this.form.language,
|
||||||
sort: this.form.sort
|
sort: this.form.sort,
|
||||||
|
functions: this.currentFunctions
|
||||||
};
|
};
|
||||||
Api.agent.updateAgentConfig(this.$route.query.agentId, configData, ({ data }) => {
|
Api.agent.updateAgentConfig(this.$route.query.agentId, configData, ({ data }) => {
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
@@ -167,6 +217,7 @@ export default {
|
|||||||
agentCode: "",
|
agentCode: "",
|
||||||
agentName: "",
|
agentName: "",
|
||||||
ttsVoiceId: "",
|
ttsVoiceId: "",
|
||||||
|
chatHistoryConf: 0,
|
||||||
systemPrompt: "",
|
systemPrompt: "",
|
||||||
langCode: "",
|
langCode: "",
|
||||||
language: "",
|
language: "",
|
||||||
@@ -180,12 +231,12 @@ export default {
|
|||||||
intentModelId: "",
|
intentModelId: "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.currentFunctions = [];
|
||||||
this.$message.success({
|
this.$message.success({
|
||||||
message: '配置已重置',
|
message: '配置已重置',
|
||||||
showClose: true
|
showClose: true
|
||||||
})
|
})
|
||||||
}).catch(() => {
|
}).catch(() => { });
|
||||||
});
|
|
||||||
},
|
},
|
||||||
fetchTemplates() {
|
fetchTemplates() {
|
||||||
Api.agent.getAgentTemplate(({ data }) => {
|
Api.agent.getAgentTemplate(({ data }) => {
|
||||||
@@ -220,6 +271,7 @@ export default {
|
|||||||
...this.form,
|
...this.form,
|
||||||
agentName: templateData.agentName || this.form.agentName,
|
agentName: templateData.agentName || this.form.agentName,
|
||||||
ttsVoiceId: templateData.ttsVoiceId || this.form.ttsVoiceId,
|
ttsVoiceId: templateData.ttsVoiceId || this.form.ttsVoiceId,
|
||||||
|
chatHistoryConf: templateData.chatHistoryConf || this.form.chatHistoryConf,
|
||||||
systemPrompt: templateData.systemPrompt || this.form.systemPrompt,
|
systemPrompt: templateData.systemPrompt || this.form.systemPrompt,
|
||||||
langCode: templateData.langCode || this.form.langCode,
|
langCode: templateData.langCode || this.form.langCode,
|
||||||
model: {
|
model: {
|
||||||
@@ -247,6 +299,7 @@ export default {
|
|||||||
intentModelId: data.data.intentModelId
|
intentModelId: data.data.intentModelId
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
this.currentFunctions = data.data.functions || [];
|
||||||
} else {
|
} else {
|
||||||
this.$message.error(data.msg || '获取配置失败');
|
this.$message.error(data.msg || '获取配置失败');
|
||||||
}
|
}
|
||||||
@@ -281,7 +334,56 @@ export default {
|
|||||||
this.voiceOptions = [];
|
this.voiceOptions = [];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
getFunctionColor(name) {
|
||||||
|
const hash = [...name].reduce((acc, char) => acc + char.charCodeAt(0), 0);
|
||||||
|
return this.functionColorMap[hash % 7];
|
||||||
|
},
|
||||||
|
showFunctionIcons(type) {
|
||||||
|
// TODO 暂时不放出来
|
||||||
|
return false;
|
||||||
|
// return type === 'Intent' &&
|
||||||
|
// this.form.model.intentModelId !== 'Intent_nointent';
|
||||||
|
},
|
||||||
|
handleModelChange(type, value) {
|
||||||
|
if (type === 'Intent' && value !== 'Intent_nointent') {
|
||||||
|
this.fetchFunctionList();
|
||||||
|
}
|
||||||
|
if (type === 'Memory' && value === 'Memory_nomem') {
|
||||||
|
this.form.chatHistoryConf = 0;
|
||||||
|
}
|
||||||
|
if (type === 'Memory' && value !== 'Memory_nomem' && (this.form.chatHistoryConf === 0 || this.form.chatHistoryConf === null)) {
|
||||||
|
this.form.chatHistoryConf = 2;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fetchFunctionList() {
|
||||||
|
// 使用假数据代替API调用
|
||||||
|
return new Promise(resolve => {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.currentFunctions = [
|
||||||
|
{ name: '天气', params: { city: '北京' } },
|
||||||
|
{ name: '新闻', params: { type: '科技' } }
|
||||||
|
];
|
||||||
|
resolve();
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleUpdateFunctions(selected) {
|
||||||
|
this.currentFunctions = selected;
|
||||||
|
console.log('保存的功能列表:', selected);
|
||||||
|
this.$message.success('功能配置已保存');
|
||||||
|
},
|
||||||
|
handleDialogClosed(saved) {
|
||||||
|
if (!saved) {
|
||||||
|
// 如果未保存,恢复原始功能列表
|
||||||
|
this.currentFunctions = JSON.parse(JSON.stringify(this.originalFunctions));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateChatHistoryConf() {
|
||||||
|
if (this.form.model.memModelId === 'Memory_nomem') {
|
||||||
|
this.form.chatHistoryConf = 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
'form.model.ttsModelId': {
|
'form.model.ttsModelId': {
|
||||||
@@ -308,6 +410,9 @@ export default {
|
|||||||
const agentId = this.$route.query.agentId;
|
const agentId = this.$route.query.agentId;
|
||||||
if (agentId) {
|
if (agentId) {
|
||||||
this.fetchAgentConfig(agentId);
|
this.fetchAgentConfig(agentId);
|
||||||
|
this.fetchFunctionList().then(() => {
|
||||||
|
this.originalFunctions = JSON.parse(JSON.stringify(this.currentFunctions));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
this.fetchModelOptions();
|
this.fetchModelOptions();
|
||||||
this.fetchTemplates();
|
this.fetchTemplates();
|
||||||
@@ -509,8 +614,35 @@ export default {
|
|||||||
height: 19px;
|
height: 19px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.model-select-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.function-icons {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: auto;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-dot {
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-right: 8px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
::v-deep .el-form-item__label {
|
::v-deep .el-form-item__label {
|
||||||
font-size: 10px !important;
|
font-size: 12px !important;
|
||||||
color: #3d4566 !important;
|
color: #3d4566 !important;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
@@ -551,4 +683,25 @@ export default {
|
|||||||
color: #409EFF;
|
color: #409EFF;
|
||||||
border-color: #409EFF;
|
border-color: #409EFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.edit-function-btn {
|
||||||
|
background: #e6ebff;
|
||||||
|
color: #5778ff;
|
||||||
|
border: 1px solid #adbdff;
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-function-btn.active-btn {
|
||||||
|
background: #5778ff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-history-options {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 250px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -143,6 +143,13 @@ prompt: |
|
|||||||
- 长篇大论,叽叽歪歪
|
- 长篇大论,叽叽歪歪
|
||||||
- 长时间严肃对话
|
- 长时间严肃对话
|
||||||
|
|
||||||
|
# 结束语prompt
|
||||||
|
end_prompt:
|
||||||
|
enable: true # 是否开启结束语
|
||||||
|
# 结束语
|
||||||
|
prompt: |
|
||||||
|
请你以“时间过得真快”未来头,用富有感情、依依不舍的话来结束这场对话吧。!
|
||||||
|
|
||||||
# 具体处理时选择的模块(The module selected for specific processing)
|
# 具体处理时选择的模块(The module selected for specific processing)
|
||||||
selected_module:
|
selected_module:
|
||||||
# 语音活动检测模块,默认使用SileroVAD模型
|
# 语音活动检测模块,默认使用SileroVAD模型
|
||||||
@@ -221,7 +228,7 @@ ASR:
|
|||||||
FunASRServer:
|
FunASRServer:
|
||||||
# 独立部署FunASR,使用FunASR的API服务,只需要五句话
|
# 独立部署FunASR,使用FunASR的API服务,只需要五句话
|
||||||
# 第一句:mkdir -p ./funasr-runtime-resources/models
|
# 第一句:mkdir -p ./funasr-runtime-resources/models
|
||||||
# 第二句:sudo docker run -d -p 10096:10095 --privileged=true -v $PWD/funasr-runtime-resources/models:/workspace/models registry.cn-hangzhou.aliyuncs.com/funasr_repo/funasr:funasr-runtime-sdk-online-cpu-0.1.12
|
# 第二句:sudo docker run -p 10096:10095 -it --privileged=true -v $PWD/funasr-runtime-resources/models:/workspace/models registry.cn-hangzhou.aliyuncs.com/funasr_repo/funasr:funasr-runtime-sdk-online-cpu-0.1.12
|
||||||
# 上一句话执行后会进入到容器,继续第三句:cd FunASR/runtime
|
# 上一句话执行后会进入到容器,继续第三句:cd FunASR/runtime
|
||||||
# 不要退出容器,继续在容器中执行第四句:nohup bash run_server_2pass.sh --download-model-dir /workspace/models --vad-dir damo/speech_fsmn_vad_zh-cn-16k-common-onnx --model-dir damo/speech_paraformer-large-vad-punc_asr_nat-zh-cn-16k-common-vocab8404-onnx --online-model-dir damo/speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-online-onnx --punc-dir damo/punc_ct-transformer_zh-cn-common-vad_realtime-vocab272727-onnx --lm-dir damo/speech_ngram_lm_zh-cn-ai-wesp-fst --itn-dir thuduj12/fst_itn_zh --hotword /workspace/models/hotwords.txt > log.txt 2>&1 &
|
# 不要退出容器,继续在容器中执行第四句:nohup bash run_server_2pass.sh --download-model-dir /workspace/models --vad-dir damo/speech_fsmn_vad_zh-cn-16k-common-onnx --model-dir damo/speech_paraformer-large-vad-punc_asr_nat-zh-cn-16k-common-vocab8404-onnx --online-model-dir damo/speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-online-onnx --punc-dir damo/punc_ct-transformer_zh-cn-common-vad_realtime-vocab272727-onnx --lm-dir damo/speech_ngram_lm_zh-cn-ai-wesp-fst --itn-dir thuduj12/fst_itn_zh --hotword /workspace/models/hotwords.txt > log.txt 2>&1 &
|
||||||
# 上一句话执行后会进入到容器,继续第五句:tail -f log.txt
|
# 上一句话执行后会进入到容器,继续第五句:tail -f log.txt
|
||||||
@@ -231,6 +238,7 @@ ASR:
|
|||||||
host: 127.0.0.1
|
host: 127.0.0.1
|
||||||
port: 10096
|
port: 10096
|
||||||
is_ssl: true
|
is_ssl: true
|
||||||
|
api_key: none
|
||||||
output_dir: tmp/
|
output_dir: tmp/
|
||||||
SherpaASR:
|
SherpaASR:
|
||||||
type: sherpa_onnx_local
|
type: sherpa_onnx_local
|
||||||
@@ -475,20 +483,13 @@ TTS:
|
|||||||
speed: 1
|
speed: 1
|
||||||
output_dir: tmp/
|
output_dir: tmp/
|
||||||
FishSpeech:
|
FishSpeech:
|
||||||
# 定义TTS API类型
|
# 参照教程:https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docs/fish-speech-integration.md
|
||||||
#启动tts方法:
|
|
||||||
#python -m tools.api_server
|
|
||||||
#--listen 0.0.0.0:8080
|
|
||||||
#--llama-checkpoint-path "checkpoints/fish-speech-1.5"
|
|
||||||
#--decoder-checkpoint-path "checkpoints/fish-speech-1.5/firefly-gan-vq-fsq-8x1024-21hz-generator.pth"
|
|
||||||
#--decoder-config-name firefly_gan_vq
|
|
||||||
#--compile
|
|
||||||
type: fishspeech
|
type: fishspeech
|
||||||
output_dir: tmp/
|
output_dir: tmp/
|
||||||
response_format: wav
|
response_format: wav
|
||||||
reference_id: null
|
reference_id: null
|
||||||
reference_audio: ["/tmp/test.wav",]
|
reference_audio: ["config/assets/wakeup_words.wav",]
|
||||||
reference_text: ["你弄来这些吟词宴曲来看,还是这些混话来欺负我。",]
|
reference_text: ["哈啰啊,我是小智啦,声音好听的台湾女孩一枚,超开心认识你耶,最近在忙啥,别忘了给我来点有趣的料哦,我超爱听八卦的啦",]
|
||||||
normalize: true
|
normalize: true
|
||||||
max_new_tokens: 1024
|
max_new_tokens: 1024
|
||||||
chunk_length: 200
|
chunk_length: 200
|
||||||
|
|||||||
@@ -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.4.2"
|
SERVER_VERSION = "0.4.3"
|
||||||
|
|
||||||
|
|
||||||
def get_module_abbreviation(module_name, module_dict):
|
def get_module_abbreviation(module_name, module_dict):
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ from core.mcp.manager import MCPManager
|
|||||||
from config.config_loader import get_private_config_from_api
|
from config.config_loader import get_private_config_from_api
|
||||||
from config.manage_api_client import DeviceNotFoundException, DeviceBindException
|
from config.manage_api_client import DeviceNotFoundException, DeviceBindException
|
||||||
from core.utils.output_counter import add_device_output
|
from core.utils.output_counter import add_device_output
|
||||||
from core.handle.ttsReportHandle import enqueue_tts_report, report_tts
|
from core.handle.reportHandle import enqueue_tts_report, report
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
|
|
||||||
@@ -75,6 +75,7 @@ class ConnectionHandler:
|
|||||||
self.prompt = None
|
self.prompt = None
|
||||||
self.welcome_msg = None
|
self.welcome_msg = None
|
||||||
self.max_output_size = 0
|
self.max_output_size = 0
|
||||||
|
self.chat_history_conf = 0
|
||||||
|
|
||||||
# 客户端状态相关
|
# 客户端状态相关
|
||||||
self.client_abort = False
|
self.client_abort = False
|
||||||
@@ -88,8 +89,11 @@ class ConnectionHandler:
|
|||||||
self.executor = ThreadPoolExecutor(max_workers=10)
|
self.executor = ThreadPoolExecutor(max_workers=10)
|
||||||
|
|
||||||
# 上报线程
|
# 上报线程
|
||||||
self.tts_report_queue = queue.Queue()
|
self.report_queue = queue.Queue()
|
||||||
self.tts_report_thread = None
|
self.report_thread = None
|
||||||
|
# TODO(haotian): 2025/5/12 可以通过修改此处,调节asr的上报和tts的上报
|
||||||
|
self.report_asr_enable = self.read_config_from_api
|
||||||
|
self.report_tts_enable = self.read_config_from_api
|
||||||
|
|
||||||
# 依赖的组件
|
# 依赖的组件
|
||||||
self.vad = None
|
self.vad = None
|
||||||
@@ -264,11 +268,15 @@ class ConnectionHandler:
|
|||||||
self.logger.bind(tag=TAG).info("收到服务器重启指令,准备执行...")
|
self.logger.bind(tag=TAG).info("收到服务器重启指令,准备执行...")
|
||||||
|
|
||||||
# 发送确认响应
|
# 发送确认响应
|
||||||
await self.websocket.send(json.dumps({
|
await self.websocket.send(
|
||||||
"type": "server_response",
|
json.dumps(
|
||||||
"status": "success",
|
{
|
||||||
"message": "服务器重启中..."
|
"type": "server_response",
|
||||||
}))
|
"status": "success",
|
||||||
|
"message": "服务器重启中...",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# 异步执行重启操作
|
# 异步执行重启操作
|
||||||
def restart_server():
|
def restart_server():
|
||||||
@@ -280,7 +288,7 @@ class ConnectionHandler:
|
|||||||
stdin=sys.stdin,
|
stdin=sys.stdin,
|
||||||
stdout=sys.stdout,
|
stdout=sys.stdout,
|
||||||
stderr=sys.stderr,
|
stderr=sys.stderr,
|
||||||
start_new_session=True
|
start_new_session=True,
|
||||||
)
|
)
|
||||||
os._exit(0)
|
os._exit(0)
|
||||||
|
|
||||||
@@ -289,11 +297,15 @@ class ConnectionHandler:
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.bind(tag=TAG).error(f"重启失败: {str(e)}")
|
self.logger.bind(tag=TAG).error(f"重启失败: {str(e)}")
|
||||||
await self.websocket.send(json.dumps({
|
await self.websocket.send(
|
||||||
"type": "server_response",
|
json.dumps(
|
||||||
"status": "error",
|
{
|
||||||
"message": f"Restart failed: {str(e)}"
|
"type": "server_response",
|
||||||
}))
|
"status": "error",
|
||||||
|
"message": f"Restart failed: {str(e)}",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def _initialize_components(self):
|
def _initialize_components(self):
|
||||||
"""初始化组件"""
|
"""初始化组件"""
|
||||||
@@ -320,11 +332,13 @@ class ConnectionHandler:
|
|||||||
"""初始化ASR和TTS上报线程"""
|
"""初始化ASR和TTS上报线程"""
|
||||||
if not self.read_config_from_api or self.need_bind:
|
if not self.read_config_from_api or self.need_bind:
|
||||||
return
|
return
|
||||||
if self.tts_report_thread is None or not self.tts_report_thread.is_alive():
|
if self.chat_history_conf == 0:
|
||||||
self.tts_report_thread = threading.Thread(
|
return
|
||||||
target=self._tts_report_worker, daemon=True
|
if self.report_thread is None or not self.report_thread.is_alive():
|
||||||
|
self.report_thread = threading.Thread(
|
||||||
|
target=self._report_worker, daemon=True
|
||||||
)
|
)
|
||||||
self.tts_report_thread.start()
|
self.report_thread.start()
|
||||||
self.logger.bind(tag=TAG).info("TTS上报线程已启动")
|
self.logger.bind(tag=TAG).info("TTS上报线程已启动")
|
||||||
|
|
||||||
def _initialize_private_config(self):
|
def _initialize_private_config(self):
|
||||||
@@ -393,7 +407,8 @@ class ConnectionHandler:
|
|||||||
self.config["prompt"] = private_config["prompt"]
|
self.config["prompt"] = private_config["prompt"]
|
||||||
if private_config.get("device_max_output_size", None) is not None:
|
if private_config.get("device_max_output_size", None) is not None:
|
||||||
self.max_output_size = int(private_config["device_max_output_size"])
|
self.max_output_size = int(private_config["device_max_output_size"])
|
||||||
|
if private_config.get("chat_history_conf", None) is not None:
|
||||||
|
self.chat_history_conf = int(private_config["chat_history_conf"])
|
||||||
try:
|
try:
|
||||||
modules = initialize_modules(
|
modules = initialize_modules(
|
||||||
self.logger,
|
self.logger,
|
||||||
@@ -852,7 +867,7 @@ class ConnectionHandler:
|
|||||||
if future is None:
|
if future is None:
|
||||||
continue
|
continue
|
||||||
text = None
|
text = None
|
||||||
opus_datas, tts_file = [], None
|
audio_datas, tts_file = [], None
|
||||||
try:
|
try:
|
||||||
self.logger.bind(tag=TAG).debug("正在处理TTS任务...")
|
self.logger.bind(tag=TAG).debug("正在处理TTS任务...")
|
||||||
tts_timeout = int(self.config.get("tts_timeout", 10))
|
tts_timeout = int(self.config.get("tts_timeout", 10))
|
||||||
@@ -874,8 +889,8 @@ class ConnectionHandler:
|
|||||||
audio_datas, _ = self.tts.audio_to_pcm_data(tts_file)
|
audio_datas, _ = self.tts.audio_to_pcm_data(tts_file)
|
||||||
else:
|
else:
|
||||||
audio_datas, _ = self.tts.audio_to_opus_data(tts_file)
|
audio_datas, _ = self.tts.audio_to_opus_data(tts_file)
|
||||||
# 在这里上报TTS数据(使用文件路径)
|
# 在这里上报TTS数据
|
||||||
enqueue_tts_report(self, 2, text, audio_datas)
|
enqueue_tts_report(self, text, audio_datas)
|
||||||
else:
|
else:
|
||||||
self.logger.bind(tag=TAG).error(
|
self.logger.bind(tag=TAG).error(
|
||||||
f"TTS出错:文件不存在{tts_file}"
|
f"TTS出错:文件不存在{tts_file}"
|
||||||
@@ -931,13 +946,12 @@ class ConnectionHandler:
|
|||||||
f"audio_play_priority priority_thread: {text} {e}"
|
f"audio_play_priority priority_thread: {text} {e}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def _tts_report_worker(self):
|
def _report_worker(self):
|
||||||
"""TTS上报工作线程"""
|
"""聊天记录上报工作线程"""
|
||||||
|
|
||||||
while not self.stop_event.is_set():
|
while not self.stop_event.is_set():
|
||||||
try:
|
try:
|
||||||
# 从队列获取数据,设置超时以便定期检查停止事件
|
# 从队列获取数据,设置超时以便定期检查停止事件
|
||||||
item = self.tts_report_queue.get(timeout=1)
|
item = self.report_queue.get(timeout=1)
|
||||||
if item is None: # 检测毒丸对象
|
if item is None: # 检测毒丸对象
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -945,18 +959,18 @@ class ConnectionHandler:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# 执行上报(传入二进制数据)
|
# 执行上报(传入二进制数据)
|
||||||
report_tts(self, type, text, audio_data)
|
report(self, type, text, audio_data)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.bind(tag=TAG).error(f"TTS上报线程异常: {e}")
|
self.logger.bind(tag=TAG).error(f"聊天记录上报线程异常: {e}")
|
||||||
finally:
|
finally:
|
||||||
# 标记任务完成
|
# 标记任务完成
|
||||||
self.tts_report_queue.task_done()
|
self.report_queue.task_done()
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
continue
|
continue
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.bind(tag=TAG).error(f"TTS上报工作线程异常: {e}")
|
self.logger.bind(tag=TAG).error(f"聊天记录上报工作线程异常: {e}")
|
||||||
|
|
||||||
self.logger.bind(tag=TAG).info("TTS上报线程已退出")
|
self.logger.bind(tag=TAG).info("聊天记录上报线程已退出")
|
||||||
|
|
||||||
def speak_and_play(self, text, text_index=0):
|
def speak_and_play(self, text, text_index=0):
|
||||||
if text is None or len(text) <= 0:
|
if text is None or len(text) <= 0:
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from core.utils.util import remove_punctuation_and_length
|
|||||||
from core.handle.sendAudioHandle import send_stt_message
|
from core.handle.sendAudioHandle import send_stt_message
|
||||||
from core.handle.intentHandler import handle_user_intent
|
from core.handle.intentHandler import handle_user_intent
|
||||||
from core.utils.output_counter import check_device_output_limit
|
from core.utils.output_counter import check_device_output_limit
|
||||||
from core.handle.ttsReportHandle import enqueue_tts_report
|
from core.handle.reportHandle import enqueue_asr_report
|
||||||
from core.utils.util import audio_to_data
|
from core.utils.util import audio_to_data
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
@@ -44,7 +44,7 @@ async def handleAudioMessage(conn, audio):
|
|||||||
text_len, _ = remove_punctuation_and_length(text)
|
text_len, _ = remove_punctuation_and_length(text)
|
||||||
if text_len > 0:
|
if text_len > 0:
|
||||||
# 使用自定义模块进行上报
|
# 使用自定义模块进行上报
|
||||||
enqueue_tts_report(conn, 1, text, copy.deepcopy(conn.asr_audio))
|
enqueue_asr_report(conn, text, copy.deepcopy(conn.asr_audio))
|
||||||
|
|
||||||
await startToChat(conn, text)
|
await startToChat(conn, text)
|
||||||
else:
|
else:
|
||||||
@@ -98,9 +98,14 @@ async def no_voice_close_connect(conn):
|
|||||||
conn.close_after_chat = True
|
conn.close_after_chat = True
|
||||||
conn.client_abort = False
|
conn.client_abort = False
|
||||||
conn.asr_server_receive = False
|
conn.asr_server_receive = False
|
||||||
prompt = (
|
end_prompt = conn.config.get("end_prompt", {})
|
||||||
"请你以“时间过得真快”未来头,用富有感情、依依不舍的话来结束这场对话吧。"
|
if end_prompt and end_prompt.get("enable", True) is False:
|
||||||
)
|
conn.logger.bind(tag=TAG).info("结束对话,无需发送结束提示语")
|
||||||
|
await conn.close()
|
||||||
|
return
|
||||||
|
prompt = end_prompt.get("prompt")
|
||||||
|
if not prompt:
|
||||||
|
prompt = "请你以“时间过得真快”未来头,用富有感情、依依不舍的话来结束这场对话吧。!"
|
||||||
await startToChat(conn, prompt)
|
await startToChat(conn, prompt)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+47
-12
@@ -11,13 +11,13 @@ TTS上报功能已集成到ConnectionHandler类中。
|
|||||||
|
|
||||||
import opuslib_next
|
import opuslib_next
|
||||||
|
|
||||||
from config.manage_api_client import report
|
from config.manage_api_client import report as manage_report
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
|
|
||||||
|
|
||||||
def report_tts(conn, type, text, opus_data):
|
def report(conn, type, text, opus_data):
|
||||||
"""执行TTS上报操作
|
"""执行聊天记录上报操作
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
conn: 连接对象
|
conn: 连接对象
|
||||||
@@ -31,7 +31,7 @@ def report_tts(conn, type, text, opus_data):
|
|||||||
else:
|
else:
|
||||||
audio_data = None
|
audio_data = None
|
||||||
# 执行上报
|
# 执行上报
|
||||||
report(
|
manage_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,
|
||||||
@@ -39,7 +39,7 @@ def report_tts(conn, type, text, opus_data):
|
|||||||
audio=audio_data,
|
audio=audio_data,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
conn.logger.bind(tag=TAG).error(f"TTS上报失败: {e}")
|
conn.logger.bind(tag=TAG).error(f"聊天记录上报失败: {e}")
|
||||||
|
|
||||||
|
|
||||||
def opus_to_wav(conn, opus_data):
|
def opus_to_wav(conn, opus_data):
|
||||||
@@ -89,8 +89,10 @@ def opus_to_wav(conn, opus_data):
|
|||||||
return bytes(wav_header) + pcm_data_bytes
|
return bytes(wav_header) + pcm_data_bytes
|
||||||
|
|
||||||
|
|
||||||
def enqueue_tts_report(conn, type, text, opus_data):
|
def enqueue_tts_report(conn, text, opus_data):
|
||||||
if not conn.read_config_from_api or conn.need_bind:
|
if not conn.read_config_from_api or conn.need_bind or not conn.report_tts_enable:
|
||||||
|
return
|
||||||
|
if conn.chat_history_conf == 0:
|
||||||
return
|
return
|
||||||
"""将TTS数据加入上报队列
|
"""将TTS数据加入上报队列
|
||||||
|
|
||||||
@@ -101,10 +103,43 @@ def enqueue_tts_report(conn, type, text, opus_data):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# 使用连接对象的队列,传入文本和二进制数据而非文件路径
|
# 使用连接对象的队列,传入文本和二进制数据而非文件路径
|
||||||
conn.tts_report_queue.put((type, text, opus_data))
|
if conn.chat_history_conf == 2:
|
||||||
|
conn.report_queue.put((2, text, opus_data))
|
||||||
conn.logger.bind(tag=TAG).debug(
|
conn.logger.bind(tag=TAG).debug(
|
||||||
f"TTS数据已加入上报队列: {conn.device_id}, 音频大小: {len(opus_data)} "
|
f"TTS数据已加入上报队列: {conn.device_id}, 音频大小: {len(opus_data)} "
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
conn.report_queue.put((2, text, None))
|
||||||
|
conn.logger.bind(tag=TAG).debug(
|
||||||
|
f"TTS数据已加入上报队列: {conn.device_id}, 不上报音频"
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
conn.logger.bind(tag=TAG).error(f"加入TTS上报队列失败: {text}, {e}")
|
conn.logger.bind(tag=TAG).error(f"加入TTS上报队列失败: {text}, {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def enqueue_asr_report(conn, text, opus_data):
|
||||||
|
if not conn.read_config_from_api or conn.need_bind or not conn.report_asr_enable:
|
||||||
|
return
|
||||||
|
if conn.chat_history_conf == 0:
|
||||||
|
return
|
||||||
|
"""将ASR数据加入上报队列
|
||||||
|
|
||||||
|
Args:
|
||||||
|
conn: 连接对象
|
||||||
|
text: 合成文本
|
||||||
|
opus_data: opus音频数据
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 使用连接对象的队列,传入文本和二进制数据而非文件路径
|
||||||
|
if conn.chat_history_conf == 2:
|
||||||
|
conn.report_queue.put((1, text, opus_data))
|
||||||
|
conn.logger.bind(tag=TAG).debug(
|
||||||
|
f"ASR数据已加入上报队列: {conn.device_id}, 音频大小: {len(opus_data)} "
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
conn.report_queue.put((1, text, None))
|
||||||
|
conn.logger.bind(tag=TAG).debug(
|
||||||
|
f"ASR数据已加入上报队列: {conn.device_id}, 不上报音频"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
conn.logger.bind(tag=TAG).error(f"加入ASR上报队列失败: {text}, {e}")
|
||||||
@@ -5,7 +5,7 @@ from core.utils.util import remove_punctuation_and_length
|
|||||||
from core.handle.receiveAudioHandle import startToChat, handleAudioMessage
|
from core.handle.receiveAudioHandle import startToChat, handleAudioMessage
|
||||||
from core.handle.sendAudioHandle import send_stt_message, send_tts_message
|
from core.handle.sendAudioHandle import send_stt_message, send_tts_message
|
||||||
from core.handle.iotHandle import handleIotDescriptors, handleIotStatus
|
from core.handle.iotHandle import handleIotDescriptors, handleIotStatus
|
||||||
from core.handle.ttsReportHandle import enqueue_tts_report
|
from core.handle.reportHandle import enqueue_asr_report
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
@@ -56,11 +56,11 @@ async def handleTextMessage(conn, message):
|
|||||||
await send_tts_message(conn, "stop", None)
|
await send_tts_message(conn, "stop", None)
|
||||||
elif is_wakeup_words:
|
elif is_wakeup_words:
|
||||||
# 上报纯文字数据(复用ASR上报功能,但不提供音频数据)
|
# 上报纯文字数据(复用ASR上报功能,但不提供音频数据)
|
||||||
enqueue_tts_report(conn, 1, "嘿,你好呀", [])
|
enqueue_asr_report(conn, "嘿,你好呀", [])
|
||||||
await startToChat(conn, "嘿,你好呀")
|
await startToChat(conn, "嘿,你好呀")
|
||||||
else:
|
else:
|
||||||
# 上报纯文字数据(复用ASR上报功能,但不提供音频数据)
|
# 上报纯文字数据(复用ASR上报功能,但不提供音频数据)
|
||||||
enqueue_tts_report(conn, 1, text, [])
|
enqueue_asr_report(conn, text, [])
|
||||||
# 否则需要LLM对文字内容进行答复
|
# 否则需要LLM对文字内容进行答复
|
||||||
await startToChat(conn, text)
|
await startToChat(conn, text)
|
||||||
elif msg_json["type"] == "iot":
|
elif msg_json["type"] == "iot":
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ class ASRProvider(ASRProviderBase):
|
|||||||
) -> Tuple[Optional[str], Optional[str]]:
|
) -> Tuple[Optional[str], Optional[str]]:
|
||||||
"""将语音数据转换为文本"""
|
"""将语音数据转换为文本"""
|
||||||
if not opus_data:
|
if not opus_data:
|
||||||
logger.bind(tag=TAG).warn("音频数据为空!")
|
logger.bind(tag=TAG).warning("音频数据为空!")
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
file_path = None
|
file_path = None
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import wave
|
|||||||
import websockets
|
import websockets
|
||||||
from config.logger import setup_logging
|
from config.logger import setup_logging
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import re
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
logger = setup_logging()
|
logger = setup_logging()
|
||||||
@@ -24,7 +25,12 @@ class ASRProvider(ASRProviderBase):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
self.host = config.get("host", "localhost")
|
self.host = config.get("host", "localhost")
|
||||||
self.port = config.get("port", 10095)
|
self.port = config.get("port", 10095)
|
||||||
self.is_ssl = config.get("is_ssl", True)
|
self.api_key = config.get("api_key", "none")
|
||||||
|
self.is_ssl = str(config.get("is_ssl", True)).lower() in (
|
||||||
|
"true",
|
||||||
|
"1",
|
||||||
|
"yes",
|
||||||
|
)
|
||||||
self.output_dir = config.get("output_dir")
|
self.output_dir = config.get("output_dir")
|
||||||
self.delete_audio_file = delete_audio_file
|
self.delete_audio_file = delete_audio_file
|
||||||
self.uri = (
|
self.uri = (
|
||||||
@@ -130,9 +136,13 @@ class ASRProvider(ASRProviderBase):
|
|||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
file_path = self.save_audio_to_file(pcm_data, session_id)
|
file_path = self.save_audio_to_file(pcm_data, session_id)
|
||||||
|
auth_header = {"Authorization": "Bearer; {}".format(self.api_key)}
|
||||||
async with websockets.connect(
|
async with websockets.connect(
|
||||||
self.uri, subprotocols=["binary"], ping_interval=None, ssl=self.ssl_context
|
self.uri,
|
||||||
|
additional_headers=auth_header,
|
||||||
|
subprotocols=["binary"],
|
||||||
|
ping_interval=None,
|
||||||
|
ssl=self.ssl_context,
|
||||||
) as ws:
|
) as ws:
|
||||||
try:
|
try:
|
||||||
# Use asyncio to handle WebSocket communication
|
# Use asyncio to handle WebSocket communication
|
||||||
@@ -157,6 +167,9 @@ class ASRProvider(ASRProviderBase):
|
|||||||
|
|
||||||
# Get the result from the receive task
|
# Get the result from the receive task
|
||||||
result = receive_task.result()
|
result = receive_task.result()
|
||||||
|
match = re.match(r"<\|(.*?)\|><\|(.*?)\|><\|(.*?)\|>(.*)", result)
|
||||||
|
if match:
|
||||||
|
result = match.group(4).strip()
|
||||||
return (
|
return (
|
||||||
result,
|
result,
|
||||||
file_path,
|
file_path,
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ class ASRProvider(ASRProviderBase):
|
|||||||
) -> Tuple[Optional[str], Optional[str]]:
|
) -> Tuple[Optional[str], Optional[str]]:
|
||||||
"""将语音数据转换为文本"""
|
"""将语音数据转换为文本"""
|
||||||
if not opus_data:
|
if not opus_data:
|
||||||
logger.bind(tag=TAG).warn("音频数据为空!")
|
logger.bind(tag=TAG).warning("音频数据为空!")
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
file_path = None
|
file_path = None
|
||||||
@@ -230,7 +230,7 @@ class ASRProvider(ASRProviderBase):
|
|||||||
if "Response" in response_json and "Result" in response_json["Response"]:
|
if "Response" in response_json and "Result" in response_json["Response"]:
|
||||||
return response_json["Response"]["Result"]
|
return response_json["Response"]["Result"]
|
||||||
else:
|
else:
|
||||||
logger.bind(tag=TAG).warn(f"响应中没有识别结果: {response_json}")
|
logger.bind(tag=TAG).warning(f"响应中没有识别结果: {response_json}")
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -61,5 +61,6 @@ class LLMProvider(LLMProviderBase):
|
|||||||
yield "【LLM服务响应异常】"
|
yield "【LLM服务响应异常】"
|
||||||
|
|
||||||
def response_with_functions(self, session_id, dialogue, functions=None):
|
def response_with_functions(self, session_id, dialogue, functions=None):
|
||||||
logger.bind(tag=TAG).info(f"阿里百练暂未实现完整的工具调用(function call)")
|
logger.bind(tag=TAG).error(
|
||||||
return self.response(session_id, dialogue)
|
f"阿里百练暂未实现完整的工具调用(function call),建议使用其他意图识别"
|
||||||
|
)
|
||||||
|
|||||||
@@ -66,5 +66,6 @@ class LLMProvider(LLMProviderBase):
|
|||||||
yield "【服务响应异常】"
|
yield "【服务响应异常】"
|
||||||
|
|
||||||
def response_with_functions(self, session_id, dialogue, functions=None):
|
def response_with_functions(self, session_id, dialogue, functions=None):
|
||||||
logger.bind(tag=TAG).info(f"fastgpt暂未实现完整的工具调用(function call)")
|
logger.bind(tag=TAG).error(
|
||||||
return self.response(session_id, dialogue)
|
f"fastgpt暂未实现完整的工具调用(function call),建议使用其他意图识别"
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,140 +1,205 @@
|
|||||||
import google.generativeai as genai
|
import os, json, uuid
|
||||||
from core.utils.util import check_model_key
|
from types import SimpleNamespace
|
||||||
from core.providers.llm.base import LLMProviderBase
|
from typing import Any, Dict, List
|
||||||
from config.logger import setup_logging
|
|
||||||
import requests
|
|
||||||
import json
|
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from google import generativeai as genai
|
||||||
|
from google.generativeai import types, GenerationConfig
|
||||||
|
|
||||||
|
from core.providers.llm.base import LLMProviderBase
|
||||||
|
from core.utils.util import check_model_key
|
||||||
|
from config.logger import setup_logging
|
||||||
|
from google.generativeai.types import GenerateContentResponse
|
||||||
|
from requests import RequestException
|
||||||
|
|
||||||
|
log = setup_logging()
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
logger = setup_logging()
|
|
||||||
|
|
||||||
|
def test_proxy(proxy_url: str, test_url: str) -> bool:
|
||||||
|
try:
|
||||||
|
resp = requests.get(test_url, proxies={"http": proxy_url, "https": proxy_url})
|
||||||
|
return 200 <= resp.status_code < 400
|
||||||
|
except RequestException:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def setup_proxy_env(http_proxy: str | None, https_proxy: str | None):
|
||||||
|
"""
|
||||||
|
分别测试 HTTP 和 HTTPS 代理是否可用,并设置环境变量。
|
||||||
|
如果 HTTPS 代理不可用但 HTTP 可用,会将 HTTPS_PROXY 也指向 HTTP。
|
||||||
|
"""
|
||||||
|
test_http_url = "http://www.google.com"
|
||||||
|
test_https_url = "https://www.google.com"
|
||||||
|
|
||||||
|
ok_http = ok_https = False
|
||||||
|
|
||||||
|
if http_proxy:
|
||||||
|
ok_http = test_proxy(http_proxy, test_http_url)
|
||||||
|
if ok_http:
|
||||||
|
os.environ["HTTP_PROXY"] = http_proxy
|
||||||
|
log.bind(tag=TAG).info(f"配置提供的Gemini HTTPS代理连通成功: {http_proxy}")
|
||||||
|
else:
|
||||||
|
log.bind(tag=TAG).warning(f"配置提供的Gemini HTTP代理不可用: {http_proxy}")
|
||||||
|
|
||||||
|
if https_proxy:
|
||||||
|
ok_https = test_proxy(https_proxy, test_https_url)
|
||||||
|
if ok_https:
|
||||||
|
os.environ["HTTPS_PROXY"] = https_proxy
|
||||||
|
log.bind(tag=TAG).info(f"配置提供的Gemini HTTPS代理连通成功: {https_proxy}")
|
||||||
|
else:
|
||||||
|
log.bind(tag=TAG).warning(
|
||||||
|
f"配置提供的Gemini HTTPS代理不可用: {https_proxy}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 如果https_proxy不可用,但http_proxy可用且能走通https,则复用http_proxy作为https_proxy
|
||||||
|
if ok_http and not ok_https:
|
||||||
|
if test_proxy(http_proxy, test_https_url):
|
||||||
|
os.environ["HTTPS_PROXY"] = http_proxy
|
||||||
|
ok_https = True
|
||||||
|
log.bind(tag=TAG).info(f"复用HTTP代理作为HTTPS代理: {http_proxy}")
|
||||||
|
|
||||||
|
if not ok_http and not ok_https:
|
||||||
|
log.bind(tag=TAG).error(
|
||||||
|
f"Gemini 代理设置失败: HTTP 和 HTTPS 代理都不可用,请检查配置"
|
||||||
|
)
|
||||||
|
raise RuntimeError("HTTP 和 HTTPS 代理都不可用,请检查配置")
|
||||||
|
|
||||||
|
|
||||||
class LLMProvider(LLMProviderBase):
|
class LLMProvider(LLMProviderBase):
|
||||||
def __init__(self, config):
|
def __init__(self, cfg: Dict[str, Any]):
|
||||||
"""初始化Gemini LLM Provider"""
|
self.model_name = cfg.get("model_name", "gemini-2.0-flash")
|
||||||
self.model_name = config.get("model_name", "gemini-1.5-pro")
|
self.api_key = cfg["api_key"]
|
||||||
self.api_key = config.get("api_key")
|
http_proxy = cfg.get("http_proxy")
|
||||||
self.http_proxy = config.get("http_proxy")
|
https_proxy = cfg.get("https_proxy")
|
||||||
self.https_proxy = config.get("https_proxy")
|
|
||||||
have_key = check_model_key("LLM", self.api_key)
|
|
||||||
|
|
||||||
if not have_key:
|
if not check_model_key("LLM", self.api_key):
|
||||||
return
|
raise ValueError("无效的Gemini API Key,请检查是否配置正确")
|
||||||
|
|
||||||
try:
|
if http_proxy or https_proxy:
|
||||||
# 初始化Gemini客户端
|
log.bind(tag=TAG).info(
|
||||||
# 配置代理(如果提供了代理配置)
|
f"检测到Gemini代理配置,开始测试代理连通性和设置代理环境..."
|
||||||
self.proxies = None
|
)
|
||||||
if self.http_proxy is not "" or self.https_proxy is not "":
|
setup_proxy_env(http_proxy, https_proxy)
|
||||||
|
log.bind(tag=TAG).info(
|
||||||
|
f"Gemini 代理设置成功 - HTTP: {http_proxy}, HTTPS: {https_proxy}"
|
||||||
|
)
|
||||||
|
genai.configure(api_key=self.api_key)
|
||||||
|
self.model = genai.GenerativeModel(self.model_name)
|
||||||
|
|
||||||
self.proxies = {
|
self.gen_cfg = GenerationConfig(
|
||||||
"http": self.http_proxy,
|
temperature=0.7,
|
||||||
"https": self.https_proxy,
|
top_p=0.9,
|
||||||
}
|
top_k=40,
|
||||||
logger.bind(tag=TAG).info(f"Gemini set proxys:{self.proxies}")
|
max_output_tokens=2048,
|
||||||
# 使用猴子补丁修改 google-generativeai 库的请求会话
|
)
|
||||||
|
|
||||||
# 使用 session 对象配置 genai
|
@staticmethod
|
||||||
|
def _build_tools(funcs: List[Dict[str, Any]] | None):
|
||||||
genai.configure(api_key=self.api_key)
|
if not funcs:
|
||||||
self.model = genai.GenerativeModel(self.model_name)
|
return None
|
||||||
|
return [
|
||||||
# 设置生成参数
|
types.Tool(
|
||||||
self.generation_config = {
|
function_declarations=[
|
||||||
"temperature": 0.7,
|
types.FunctionDeclaration(
|
||||||
"top_p": 0.9,
|
name=f["function"]["name"],
|
||||||
"top_k": 40,
|
description=f["function"]["description"],
|
||||||
"max_output_tokens": 2048,
|
parameters=f["function"]["parameters"],
|
||||||
}
|
)
|
||||||
self.chat = None
|
for f in funcs
|
||||||
except Exception as e:
|
]
|
||||||
logger.bind(tag=TAG).error(f"Gemini初始化失败: {e}")
|
)
|
||||||
self.model = None
|
]
|
||||||
|
|
||||||
|
# Gemini文档提到,无需维护session-id,直接用dialogue拼接而成
|
||||||
def response(self, session_id, dialogue):
|
def response(self, session_id, dialogue):
|
||||||
"""生成Gemini对话响应"""
|
yield from self._generate(dialogue, None)
|
||||||
if not self.model:
|
|
||||||
yield "【Gemini服务未正确初始化】"
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 处理对话历史
|
|
||||||
chat_history = []
|
|
||||||
for msg in dialogue[:-1]: # 历史对话
|
|
||||||
role = "model" if msg["role"] == "assistant" else "user"
|
|
||||||
content = msg["content"].strip()
|
|
||||||
if content:
|
|
||||||
chat_history.append({"role": role, "parts": [{"text": content}]})
|
|
||||||
|
|
||||||
# 获取当前消息
|
|
||||||
current_msg = dialogue[-1]["content"]
|
|
||||||
|
|
||||||
# 构建请求体
|
|
||||||
request_body = {
|
|
||||||
"contents": chat_history
|
|
||||||
+ [{"role": "user", "parts": [{"text": current_msg}]}],
|
|
||||||
"generationConfig": self.generation_config,
|
|
||||||
}
|
|
||||||
|
|
||||||
# 构建请求URL
|
|
||||||
url = f"https://generativelanguage.googleapis.com/v1beta/models/{self.model_name}:generateContent?key={self.api_key}"
|
|
||||||
|
|
||||||
# 构建请求头
|
|
||||||
headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
}
|
|
||||||
|
|
||||||
# 发送POST请求,经测试手动 request 无法使用 stream 模式
|
|
||||||
if self.proxies:
|
|
||||||
response = requests.post(
|
|
||||||
url,
|
|
||||||
headers=headers,
|
|
||||||
json=request_body,
|
|
||||||
stream=False,
|
|
||||||
proxies=self.proxies,
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
data = response.json() # 直接解析JSON
|
|
||||||
if "candidates" in data and data["candidates"]:
|
|
||||||
yield data["candidates"][0]["content"]["parts"][0]["text"]
|
|
||||||
else:
|
|
||||||
yield "未找到候选回复。"
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
yield f"JSON解码错误:{e}"
|
|
||||||
except Exception as e:
|
|
||||||
yield f"发生错误:{e}"
|
|
||||||
else:
|
|
||||||
logger.bind(tag=TAG).info(f"Gemini stream mode ")
|
|
||||||
chat = self.model.start_chat(history=chat_history)
|
|
||||||
|
|
||||||
# 发送消息并获取流式响应
|
|
||||||
response = chat.send_message(
|
|
||||||
current_msg, stream=True, generation_config=self.generation_config
|
|
||||||
)
|
|
||||||
# 处理流式响应
|
|
||||||
for chunk in response:
|
|
||||||
if hasattr(chunk, "text") and chunk.text:
|
|
||||||
yield chunk.text
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
error_msg = str(e)
|
|
||||||
logger.bind(tag=TAG).error(f"Gemini响应生成错误: {error_msg}")
|
|
||||||
|
|
||||||
# 针对不同错误返回友好提示
|
|
||||||
if "Rate limit" in error_msg:
|
|
||||||
yield "【Gemini服务请求太频繁,请稍后再试】"
|
|
||||||
elif "Invalid API key" in error_msg:
|
|
||||||
yield "【Gemini API key无效】"
|
|
||||||
else:
|
|
||||||
yield f"【Gemini服务响应异常: {error_msg}】"
|
|
||||||
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
yield f"请求失败:{e}"
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
yield f"JSON解码错误:{e}"
|
|
||||||
except Exception as e:
|
|
||||||
yield f"发生错误:{e}"
|
|
||||||
|
|
||||||
def response_with_functions(self, session_id, dialogue, functions=None):
|
def response_with_functions(self, session_id, dialogue, functions=None):
|
||||||
logger.bind(tag=TAG).info(f"gemini暂未实现完整的工具调用(function call)")
|
yield from self._generate(dialogue, self._build_tools(functions))
|
||||||
return self.response(session_id, dialogue)
|
|
||||||
|
def _generate(self, dialogue, tools):
|
||||||
|
role_map = {"assistant": "model", "user": "user"}
|
||||||
|
contents: list = []
|
||||||
|
# 拼接对话
|
||||||
|
for m in dialogue:
|
||||||
|
r = m["role"]
|
||||||
|
|
||||||
|
if r == "assistant" and "tool_calls" in m:
|
||||||
|
tc = m["tool_calls"][0]
|
||||||
|
contents.append(
|
||||||
|
{
|
||||||
|
"role": "model",
|
||||||
|
"parts": [
|
||||||
|
{
|
||||||
|
"function_call": {
|
||||||
|
"name": tc["function"]["name"],
|
||||||
|
"args": json.loads(tc["function"]["arguments"]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if r == "tool":
|
||||||
|
contents.append(
|
||||||
|
{
|
||||||
|
"role": "model",
|
||||||
|
"parts": [{"text": str(m.get("content", ""))}],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
contents.append(
|
||||||
|
{
|
||||||
|
"role": role_map.get(r, "user"),
|
||||||
|
"parts": [{"text": str(m.get("content", ""))}],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
stream: GenerateContentResponse = self.model.generate_content(
|
||||||
|
contents=contents,
|
||||||
|
generation_config=self.gen_cfg,
|
||||||
|
tools=tools,
|
||||||
|
stream=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
for chunk in stream:
|
||||||
|
cand = chunk.candidates[0]
|
||||||
|
for part in cand.content.parts:
|
||||||
|
# a) 函数调用-通常是最后一段话才是函数调用
|
||||||
|
if getattr(part, "function_call", None):
|
||||||
|
fc = part.function_call
|
||||||
|
yield None, [
|
||||||
|
SimpleNamespace(
|
||||||
|
id=uuid.uuid4().hex,
|
||||||
|
type="function",
|
||||||
|
function=SimpleNamespace(
|
||||||
|
name=fc.name,
|
||||||
|
arguments=json.dumps(
|
||||||
|
dict(fc.args), ensure_ascii=False
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
return
|
||||||
|
# b) 普通文本
|
||||||
|
if getattr(part, "text", None):
|
||||||
|
yield part.text if tools is None else (part.text, None)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if tools is not None:
|
||||||
|
yield None, None # function‑mode 结束,返回哑包
|
||||||
|
|
||||||
|
# 关闭stream,预留后续打断对话功能的功能方法,官方文档推荐打断对话要关闭上一个流,可以有效减少配额计费和资源占用
|
||||||
|
@staticmethod
|
||||||
|
def _safe_finish_stream(stream: GenerateContentResponse):
|
||||||
|
if hasattr(stream, "resolve"):
|
||||||
|
stream.resolve() # Gemini SDK version ≥ 0.5.0
|
||||||
|
elif hasattr(stream, "close"):
|
||||||
|
stream.close() # Gemini SDK version < 0.5.0
|
||||||
|
else:
|
||||||
|
for _ in stream: # 兜底耗尽
|
||||||
|
pass
|
||||||
|
|||||||
@@ -66,7 +66,6 @@ class LLMProvider(LLMProviderBase):
|
|||||||
logger.bind(tag=TAG).error(f"生成响应时出错: {e}")
|
logger.bind(tag=TAG).error(f"生成响应时出错: {e}")
|
||||||
|
|
||||||
def response_with_functions(self, session_id, dialogue, functions=None):
|
def response_with_functions(self, session_id, dialogue, functions=None):
|
||||||
logger.bind(tag=TAG).info(
|
logger.bind(tag=TAG).error(
|
||||||
f"homeassistant不支持(function call),建议使用意图识别使用:nointent"
|
f"homeassistant不支持(function call),建议使用其他意图识别"
|
||||||
)
|
)
|
||||||
return self.response(session_id, dialogue)
|
|
||||||
|
|||||||
@@ -85,16 +85,22 @@ class TTSProvider(TTSProviderBase):
|
|||||||
def __init__(self, config, delete_audio_file):
|
def __init__(self, config, delete_audio_file):
|
||||||
super().__init__(config, delete_audio_file)
|
super().__init__(config, delete_audio_file)
|
||||||
|
|
||||||
self.reference_id = config.get("reference_id")
|
self.reference_id = (
|
||||||
|
None if not config.get("reference_id") else config.get("reference_id")
|
||||||
|
)
|
||||||
self.reference_audio = parse_string_to_list(config.get("reference_audio"))
|
self.reference_audio = parse_string_to_list(config.get("reference_audio"))
|
||||||
self.reference_text = parse_string_to_list(config.get("reference_text"))
|
self.reference_text = parse_string_to_list(config.get("reference_text"))
|
||||||
self.format = config.get("format", "wav")
|
self.format = config.get("response_format", "wav")
|
||||||
|
|
||||||
self.api_key = config.get("api_key", "YOUR_API_KEY")
|
self.api_key = config.get("api_key", "YOUR_API_KEY")
|
||||||
have_key = check_model_key("FishSpeech TTS", self.api_key)
|
have_key = check_model_key("FishSpeech TTS", self.api_key)
|
||||||
if not have_key:
|
if not have_key:
|
||||||
return
|
return
|
||||||
self.normalize = config.get("normalize", True)
|
self.normalize = str(config.get("normalize", True)).lower() in (
|
||||||
|
"true",
|
||||||
|
"1",
|
||||||
|
"yes",
|
||||||
|
)
|
||||||
|
|
||||||
# 处理空字符串的情况
|
# 处理空字符串的情况
|
||||||
channels = config.get("channels", "1")
|
channels = config.get("channels", "1")
|
||||||
|
|||||||
@@ -930,7 +930,6 @@ def check_vad_update(before_config, new_config):
|
|||||||
if "type" not in new_config["VAD"][new_vad_module]
|
if "type" not in new_config["VAD"][new_vad_module]
|
||||||
else new_config["VAD"][new_vad_module]["type"]
|
else new_config["VAD"][new_vad_module]["type"]
|
||||||
)
|
)
|
||||||
print(f"前vad:{current_vad_type},后vad:{new_vad_type}")
|
|
||||||
update_vad = current_vad_type != new_vad_type
|
update_vad = current_vad_type != new_vad_type
|
||||||
return update_vad
|
return update_vad
|
||||||
|
|
||||||
@@ -954,6 +953,5 @@ def check_asr_update(before_config, new_config):
|
|||||||
if "type" not in new_config["ASR"][new_asr_module]
|
if "type" not in new_config["ASR"][new_asr_module]
|
||||||
else new_config["ASR"][new_asr_module]["type"]
|
else new_config["ASR"][new_asr_module]["type"]
|
||||||
)
|
)
|
||||||
print(f"前asr:{current_asr_type},后asr:{new_asr_type}")
|
|
||||||
update_asr = current_asr_type != new_asr_type
|
update_asr = current_asr_type != new_asr_type
|
||||||
return update_asr
|
return update_asr
|
||||||
|
|||||||
Reference in New Issue
Block a user