Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package xiaozhi.modules.agent.dto;

import com.fasterxml.jackson.annotation.JsonProperty;

import lombok.Data;

/**
Expand All @@ -10,6 +12,7 @@ public class IdentifyVoicePrintResponse {
/**
* 最匹配的声纹id
*/
@JsonProperty("speaker_id")
private String speakerId;
/**
* 声纹的分数
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,29 @@
package xiaozhi.modules.agent.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.*;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;

import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.utils.ConvertUtils;
Expand All @@ -27,12 +39,6 @@
import xiaozhi.modules.agent.vo.AgentVoicePrintVO;
import xiaozhi.modules.sys.service.SysParamsService;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.List;
import java.util.stream.Collectors;

/**
* @author zjy
*/
Expand All @@ -48,17 +54,19 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
// Springboot提供的编程事务类
private final TransactionTemplate transactionTemplate;
// 识别度
private final Double RECOGNITION = 0.3;

private final Double RECOGNITION = 0.5;

@Override
public boolean insert(AgentVoicePrintSaveDTO dto) {
// 获取音频数据
ByteArrayResource resource = getVoicePrintAudioWAV(dto.getAgentId(),dto.getAudioId());
ByteArrayResource resource = getVoicePrintAudioWAV(dto.getAgentId(), dto.getAudioId());
// 识别一下此声音是否注册过
IdentifyVoicePrintResponse response = identifyVoicePrint(dto.getAgentId(), resource);
if(response != null && response.getScore() > RECOGNITION){
throw new RenException("此声音已经注册为声纹过了");
if (response != null && response.getScore() > RECOGNITION) {
// 根据识别出的声纹ID查询对应的用户信息
AgentVoicePrintEntity existingVoicePrint = baseMapper.selectById(response.getSpeakerId());
String existingUserName = existingVoicePrint != null ? existingVoicePrint.getSourceName() : "未知用户";
throw new RenException("此声音声纹对应的人(" + existingUserName + ")已经注册,请选择其他声音注册");
}
AgentVoicePrintEntity entity = ConvertUtils.sourceToTarget(dto, AgentVoicePrintEntity.class);
// 开启事务
Expand All @@ -74,12 +82,12 @@ public boolean insert(AgentVoicePrintSaveDTO dto) {
// 发送注册声纹请求
registerVoicePrint(entity.getId(), resource);
return true;
} catch (RenException e) {
} catch (RenException e) {
status.setRollbackOnly(); // 标记事务回滚
throw e;
} catch (Exception e) {
status.setRollbackOnly(); // 标记事务回滚
log.error("保存声纹错误原因:{}",e.getMessage());
log.error("保存声纹错误原因:{}", e.getMessage());
throw new RenException("保存声纹错误,请联系管理员");
}
}));
Expand All @@ -92,9 +100,9 @@ public boolean delete(Long userId, String voicePrintId) {
try {
// 删除声纹,按照指定当前登录用户和智能体
int row = baseMapper.delete(new LambdaQueryWrapper<AgentVoicePrintEntity>()
.eq(AgentVoicePrintEntity::getId,voicePrintId)
.eq(AgentVoicePrintEntity::getCreator,userId));
if(row != 1){
.eq(AgentVoicePrintEntity::getId, voicePrintId)
.eq(AgentVoicePrintEntity::getCreator, userId));
if (row != 1) {
status.setRollbackOnly(); // 标记事务回滚
return false;
}
Expand All @@ -105,14 +113,12 @@ public boolean delete(Long userId, String voicePrintId) {
throw e;
} catch (Exception e) {
status.setRollbackOnly(); // 标记事务回滚
log.error("删除声纹错误原因:{}",e.getMessage());
log.error("删除声纹错误原因:{}", e.getMessage());
throw new RenException("删除声纹错误,请联系管理员");
}
}));
}



@Override
public List<AgentVoicePrintVO> list(Long userId, String agentId) {
// 按照指定当前登录用户和智能体查找数据
Expand All @@ -121,17 +127,17 @@ public List<AgentVoicePrintVO> list(Long userId, String agentId) {
.eq(AgentVoicePrintEntity::getCreator, userId));
return list.stream().map(entity -> {
// 遍历转换成AgentVoicePrintVO类型
return ConvertUtils.sourceToTarget(entity, AgentVoicePrintVO.class);
return ConvertUtils.sourceToTarget(entity, AgentVoicePrintVO.class);
}).toList();

}

@Override
public boolean update(Long userId, AgentVoicePrintUpdateDTO dto) {
AgentVoicePrintEntity agentVoicePrintEntity =
baseMapper.selectOne(new LambdaQueryWrapper<AgentVoicePrintEntity>()
.eq(AgentVoicePrintEntity::getId, dto.getId())
.eq(AgentVoicePrintEntity::getCreator, userId));
AgentVoicePrintEntity agentVoicePrintEntity = baseMapper
.selectOne(new LambdaQueryWrapper<AgentVoicePrintEntity>()
.eq(AgentVoicePrintEntity::getId, dto.getId())
.eq(AgentVoicePrintEntity::getCreator, userId));
if (agentVoicePrintEntity == null) {
return false;
}
Expand All @@ -142,15 +148,18 @@ public boolean update(Long userId, AgentVoicePrintUpdateDTO dto) {
ByteArrayResource resource;
// audioId不等于空,且audioId和之前的保存的音频id不一样,则需要重新获取音频数据生成声纹
if (!StringUtils.isEmpty(audioId) && !audioId.equals(agentVoicePrintEntity.getAudioId())) {
resource = getVoicePrintAudioWAV(agentId,audioId);
resource = getVoicePrintAudioWAV(agentId, audioId);

// 识别一下此声音是否注册过
IdentifyVoicePrintResponse response = identifyVoicePrint(agentId, resource);
// 返回分数高于RECOGNITION说明这个声纹已经有了
if(response != null && response.getScore() > RECOGNITION){
if (response != null && response.getScore() > RECOGNITION) {
// 判断返回的id如果不是要修改的声纹id,说明这个声纹id,现在要注册的声音已经存在且不是原来的声纹,不允许修改
if(!response.getSpeakerId().equals(dto.getId())){
throw new RenException("此次修改不允许,此声音已经注册为声纹了");
if (!response.getSpeakerId().equals(dto.getId())) {
// 根据识别出的声纹ID查询对应的用户信息
AgentVoicePrintEntity existingVoicePrint = baseMapper.selectById(response.getSpeakerId());
String existingUserName = existingVoicePrint != null ? existingVoicePrint.getSourceName() : "未知用户";
throw new RenException("此次修改不允许,此声音已经注册为声纹了(" + existingUserName + ")");
}
}
} else {
Expand All @@ -161,11 +170,11 @@ public boolean update(Long userId, AgentVoicePrintUpdateDTO dto) {
try {
AgentVoicePrintEntity entity = ConvertUtils.sourceToTarget(dto, AgentVoicePrintEntity.class);
int row = baseMapper.updateById(entity);
if (row != 1){
if (row != 1) {
status.setRollbackOnly(); // 标记事务回滚
return false;
}
if(resource != null){
if (resource != null) {
String id = entity.getId();
// 先注销之前这个声纹id上的声纹向量
cancelVoicePrint(id);
Expand All @@ -178,14 +187,12 @@ public boolean update(Long userId, AgentVoicePrintUpdateDTO dto) {
throw e;
} catch (Exception e) {
status.setRollbackOnly(); // 标记事务回滚
log.error("修改声纹错误原因:{}",e.getMessage());
log.error("修改声纹错误原因:{}", e.getMessage());
throw new RenException("修改声纹错误,请联系管理员");
}
}));
}



/**
* 获取生纹接口URI对象
*
Expand All @@ -204,14 +211,15 @@ private URI getVoicePrintURI() {

/**
* 获取声纹地址基础路径
*
* @param uri 声纹地址uri
* @return 基础路径
*/
private String getBaseUrl(URI uri) {
String protocol = uri.getScheme();
String host = uri.getHost();
int port = uri.getPort();
return "%s://%s:%s".formatted(protocol,host,port);
return "%s://%s:%s".formatted(protocol, host, port);
}

/**
Expand All @@ -231,13 +239,13 @@ private String getAuthorization(URI uri) {
/**
* 获取声纹音频资源数据
*
* @param audioId 音频Id
* @param audioId 音频Id
* @return 声纹音频资源数据
*/
private ByteArrayResource getVoicePrintAudioWAV(String agentId,String audioId) {
private ByteArrayResource getVoicePrintAudioWAV(String agentId, String audioId) {
// 判断这个音频是否属于当前智能体
boolean b = agentChatHistoryService.isAudioOwnedByAgent(audioId, agentId);
if(!b){
if (!b) {
throw new RenException("音频数据不属于这个智能体");
}
// 获取到音频数据
Expand All @@ -256,15 +264,16 @@ public String getFilename() {
}

/**
* 发送注册声纹http请求
* @param id 声纹id
* 发送注册声纹http请求
*
* @param id 声纹id
* @param resource 声纹音频资源
*/
private void registerVoicePrint(String id, ByteArrayResource resource) {
// 处理声纹接口地址,获取前缀
URI uri = getVoicePrintURI();
String baseUrl = getBaseUrl(uri);
String requestUrl = baseUrl + "/voiceprint/register";
String requestUrl = baseUrl + "/voiceprint/register";
// 创建请求体
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("speaker_id", id);
Expand All @@ -285,67 +294,69 @@ private void registerVoicePrint(String id, ByteArrayResource resource) {
}
// 检查响应内容
String responseBody = response.getBody();
if(responseBody == null || !responseBody.contains("true")){
if (responseBody == null || !responseBody.contains("true")) {
log.error("声纹注册失败,请求处理失败内容:{}", responseBody == null ? "空内容" : responseBody);
throw new RenException("声纹保存失败,请求处理失败");
}
}

/**
* 发送注销声纹的请求
*
* @param voicePrintId 声纹id
*/
private void cancelVoicePrint(String voicePrintId) {
URI uri = getVoicePrintURI();
String baseUrl = getBaseUrl(uri);
String requestUrl = baseUrl + "/voiceprint/" + voicePrintId;
String requestUrl = baseUrl + "/voiceprint/" + voicePrintId;
// 创建请求头
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", getAuthorization(uri));
// 创建请求体
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(headers);

// 发送 POST 请求
ResponseEntity<String> response = restTemplate.exchange(requestUrl, HttpMethod.DELETE, requestEntity, String.class);
ResponseEntity<String> response = restTemplate.exchange(requestUrl, HttpMethod.DELETE, requestEntity,
String.class);
if (response.getStatusCode() != HttpStatus.OK) {
log.error("声纹注销失败,请求路径:{}", requestUrl);
throw new RenException("声纹注销失败,请求不成功");
}
// 检查响应内容
String responseBody = response.getBody();
if(responseBody == null || !responseBody.contains("true")){
if (responseBody == null || !responseBody.contains("true")) {
log.error("声纹注销失败,请求处理失败内容:{}", responseBody == null ? "空内容" : responseBody);
throw new RenException("声纹注销失败,请求处理失败");
}
}


/**
* 发送识别声纹http请求
* @param agentId 智能体id
* 发送识别声纹http请求
*
* @param agentId 智能体id
* @param resource 声纹音频资源
* @return 返回识别数据
*/
private IdentifyVoicePrintResponse identifyVoicePrint(String agentId, ByteArrayResource resource) {

// 获取该智能体所有注册的声纹
List<AgentVoicePrintEntity> agentVoicePrintList = baseMapper.selectList(new LambdaQueryWrapper<AgentVoicePrintEntity>()
.select(AgentVoicePrintEntity::getId)
.eq(AgentVoicePrintEntity::getAgentId,agentId));
List<AgentVoicePrintEntity> agentVoicePrintList = baseMapper
.selectList(new LambdaQueryWrapper<AgentVoicePrintEntity>()
.select(AgentVoicePrintEntity::getId)
.eq(AgentVoicePrintEntity::getAgentId, agentId));

// 声纹数量为0,说明还没注册过声纹不需要发生识别请求
if(agentVoicePrintList.isEmpty()) {
return null;
if (agentVoicePrintList.isEmpty()) {
return null;
}
// 处理声纹接口地址,获取前缀
URI uri = getVoicePrintURI();
String baseUrl = getBaseUrl(uri);
String requestUrl = baseUrl + "/voiceprint/identify";
String requestUrl = baseUrl + "/voiceprint/identify";
// 创建请求体
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();


//创建speaker_id参数
// 创建speaker_id参数
String speakerIds = agentVoicePrintList.stream()
.map(AgentVoicePrintEntity::getId)
.collect(Collectors.joining(","));
Expand All @@ -367,7 +378,7 @@ private IdentifyVoicePrintResponse identifyVoicePrint(String agentId, ByteArrayR
}
// 检查响应内容
String responseBody = response.getBody();
if(responseBody != null ){
if (responseBody != null) {
return JsonUtils.parseObject(responseBody, IdentifyVoicePrintResponse.class);
}
return null;
Expand Down
4 changes: 2 additions & 2 deletions main/manager-web/src/components/VoicePrintDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@
</el-form-item>

<el-form-item label="描述" prop="introduce" class="form-item remark-item">
<el-input type="textarea" v-model="form.introduce" placeholder="请输入描述" :rows="3"
class="custom-textarea"></el-input>
<el-input type="textarea" v-model="form.introduce" placeholder="请输入描述" :rows="3" class="custom-textarea"
maxlength="100" show-word-limit></el-input>
</el-form-item>
</el-form>

Expand Down