Merge pull request #1805 from xinnan-tech/hot-fix

update:修复二次声纹注册bug
This commit is contained in:
欣南科技
2025-07-11 11:57:25 +08:00
committed by GitHub
3 changed files with 77 additions and 63 deletions
@@ -1,5 +1,7 @@
package xiaozhi.modules.agent.dto; package xiaozhi.modules.agent.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data; import lombok.Data;
/** /**
@@ -10,6 +12,7 @@ public class IdentifyVoicePrintResponse {
/** /**
* 最匹配的声纹id * 最匹配的声纹id
*/ */
@JsonProperty("speaker_id")
private String speakerId; private String speakerId;
/** /**
* 声纹的分数 * 声纹的分数
@@ -1,17 +1,29 @@
package xiaozhi.modules.agent.service.impl; package xiaozhi.modules.agent.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import java.net.URI;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import java.net.URISyntaxException;
import lombok.AllArgsConstructor; import java.util.List;
import lombok.extern.slf4j.Slf4j; import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.core.io.ByteArrayResource; 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.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate; import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate; 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.constant.Constant;
import xiaozhi.common.exception.RenException; import xiaozhi.common.exception.RenException;
import xiaozhi.common.utils.ConvertUtils; import xiaozhi.common.utils.ConvertUtils;
@@ -27,12 +39,6 @@ import xiaozhi.modules.agent.service.AgentVoicePrintService;
import xiaozhi.modules.agent.vo.AgentVoicePrintVO; import xiaozhi.modules.agent.vo.AgentVoicePrintVO;
import xiaozhi.modules.sys.service.SysParamsService; 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 * @author zjy
*/ */
@@ -48,17 +54,19 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
// Springboot提供的编程事务类 // Springboot提供的编程事务类
private final TransactionTemplate transactionTemplate; private final TransactionTemplate transactionTemplate;
// 识别度 // 识别度
private final Double RECOGNITION = 0.3; private final Double RECOGNITION = 0.5;
@Override @Override
public boolean insert(AgentVoicePrintSaveDTO dto) { 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); IdentifyVoicePrintResponse response = identifyVoicePrint(dto.getAgentId(), resource);
if(response != null && response.getScore() > RECOGNITION){ if (response != null && response.getScore() > RECOGNITION) {
throw new RenException("此声音已经注册为声纹过了"); // 根据识别出的声纹ID查询对应的用户信息
AgentVoicePrintEntity existingVoicePrint = baseMapper.selectById(response.getSpeakerId());
String existingUserName = existingVoicePrint != null ? existingVoicePrint.getSourceName() : "未知用户";
throw new RenException("此声音声纹对应的人(" + existingUserName + ")已经注册,请选择其他声音注册");
} }
AgentVoicePrintEntity entity = ConvertUtils.sourceToTarget(dto, AgentVoicePrintEntity.class); AgentVoicePrintEntity entity = ConvertUtils.sourceToTarget(dto, AgentVoicePrintEntity.class);
// 开启事务 // 开启事务
@@ -74,12 +82,12 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
// 发送注册声纹请求 // 发送注册声纹请求
registerVoicePrint(entity.getId(), resource); registerVoicePrint(entity.getId(), resource);
return true; return true;
} catch (RenException e) { } catch (RenException e) {
status.setRollbackOnly(); // 标记事务回滚 status.setRollbackOnly(); // 标记事务回滚
throw e; throw e;
} catch (Exception e) { } catch (Exception e) {
status.setRollbackOnly(); // 标记事务回滚 status.setRollbackOnly(); // 标记事务回滚
log.error("保存声纹错误原因:{}",e.getMessage()); log.error("保存声纹错误原因:{}", e.getMessage());
throw new RenException("保存声纹错误,请联系管理员"); throw new RenException("保存声纹错误,请联系管理员");
} }
})); }));
@@ -92,9 +100,9 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
try { try {
// 删除声纹,按照指定当前登录用户和智能体 // 删除声纹,按照指定当前登录用户和智能体
int row = baseMapper.delete(new LambdaQueryWrapper<AgentVoicePrintEntity>() int row = baseMapper.delete(new LambdaQueryWrapper<AgentVoicePrintEntity>()
.eq(AgentVoicePrintEntity::getId,voicePrintId) .eq(AgentVoicePrintEntity::getId, voicePrintId)
.eq(AgentVoicePrintEntity::getCreator,userId)); .eq(AgentVoicePrintEntity::getCreator, userId));
if(row != 1){ if (row != 1) {
status.setRollbackOnly(); // 标记事务回滚 status.setRollbackOnly(); // 标记事务回滚
return false; return false;
} }
@@ -105,14 +113,12 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
throw e; throw e;
} catch (Exception e) { } catch (Exception e) {
status.setRollbackOnly(); // 标记事务回滚 status.setRollbackOnly(); // 标记事务回滚
log.error("删除声纹错误原因:{}",e.getMessage()); log.error("删除声纹错误原因:{}", e.getMessage());
throw new RenException("删除声纹错误,请联系管理员"); throw new RenException("删除声纹错误,请联系管理员");
} }
})); }));
} }
@Override @Override
public List<AgentVoicePrintVO> list(Long userId, String agentId) { public List<AgentVoicePrintVO> list(Long userId, String agentId) {
// 按照指定当前登录用户和智能体查找数据 // 按照指定当前登录用户和智能体查找数据
@@ -121,17 +127,17 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
.eq(AgentVoicePrintEntity::getCreator, userId)); .eq(AgentVoicePrintEntity::getCreator, userId));
return list.stream().map(entity -> { return list.stream().map(entity -> {
// 遍历转换成AgentVoicePrintVO类型 // 遍历转换成AgentVoicePrintVO类型
return ConvertUtils.sourceToTarget(entity, AgentVoicePrintVO.class); return ConvertUtils.sourceToTarget(entity, AgentVoicePrintVO.class);
}).toList(); }).toList();
} }
@Override @Override
public boolean update(Long userId, AgentVoicePrintUpdateDTO dto) { public boolean update(Long userId, AgentVoicePrintUpdateDTO dto) {
AgentVoicePrintEntity agentVoicePrintEntity = AgentVoicePrintEntity agentVoicePrintEntity = baseMapper
baseMapper.selectOne(new LambdaQueryWrapper<AgentVoicePrintEntity>() .selectOne(new LambdaQueryWrapper<AgentVoicePrintEntity>()
.eq(AgentVoicePrintEntity::getId, dto.getId()) .eq(AgentVoicePrintEntity::getId, dto.getId())
.eq(AgentVoicePrintEntity::getCreator, userId)); .eq(AgentVoicePrintEntity::getCreator, userId));
if (agentVoicePrintEntity == null) { if (agentVoicePrintEntity == null) {
return false; return false;
} }
@@ -142,15 +148,18 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
ByteArrayResource resource; ByteArrayResource resource;
// audioId不等于空,且audioId和之前的保存的音频id不一样,则需要重新获取音频数据生成声纹 // audioId不等于空,且audioId和之前的保存的音频id不一样,则需要重新获取音频数据生成声纹
if (!StringUtils.isEmpty(audioId) && !audioId.equals(agentVoicePrintEntity.getAudioId())) { if (!StringUtils.isEmpty(audioId) && !audioId.equals(agentVoicePrintEntity.getAudioId())) {
resource = getVoicePrintAudioWAV(agentId,audioId); resource = getVoicePrintAudioWAV(agentId, audioId);
// 识别一下此声音是否注册过 // 识别一下此声音是否注册过
IdentifyVoicePrintResponse response = identifyVoicePrint(agentId, resource); IdentifyVoicePrintResponse response = identifyVoicePrint(agentId, resource);
// 返回分数高于RECOGNITION说明这个声纹已经有了 // 返回分数高于RECOGNITION说明这个声纹已经有了
if(response != null && response.getScore() > RECOGNITION){ if (response != null && response.getScore() > RECOGNITION) {
// 判断返回的id如果不是要修改的声纹id,说明这个声纹id,现在要注册的声音已经存在且不是原来的声纹,不允许修改 // 判断返回的id如果不是要修改的声纹id,说明这个声纹id,现在要注册的声音已经存在且不是原来的声纹,不允许修改
if(!response.getSpeakerId().equals(dto.getId())){ if (!response.getSpeakerId().equals(dto.getId())) {
throw new RenException("此次修改不允许,此声音已经注册为声纹了"); // 根据识别出的声纹ID查询对应的用户信息
AgentVoicePrintEntity existingVoicePrint = baseMapper.selectById(response.getSpeakerId());
String existingUserName = existingVoicePrint != null ? existingVoicePrint.getSourceName() : "未知用户";
throw new RenException("此次修改不允许,此声音已经注册为声纹了(" + existingUserName + "");
} }
} }
} else { } else {
@@ -161,11 +170,11 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
try { try {
AgentVoicePrintEntity entity = ConvertUtils.sourceToTarget(dto, AgentVoicePrintEntity.class); AgentVoicePrintEntity entity = ConvertUtils.sourceToTarget(dto, AgentVoicePrintEntity.class);
int row = baseMapper.updateById(entity); int row = baseMapper.updateById(entity);
if (row != 1){ if (row != 1) {
status.setRollbackOnly(); // 标记事务回滚 status.setRollbackOnly(); // 标记事务回滚
return false; return false;
} }
if(resource != null){ if (resource != null) {
String id = entity.getId(); String id = entity.getId();
// 先注销之前这个声纹id上的声纹向量 // 先注销之前这个声纹id上的声纹向量
cancelVoicePrint(id); cancelVoicePrint(id);
@@ -178,14 +187,12 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
throw e; throw e;
} catch (Exception e) { } catch (Exception e) {
status.setRollbackOnly(); // 标记事务回滚 status.setRollbackOnly(); // 标记事务回滚
log.error("修改声纹错误原因:{}",e.getMessage()); log.error("修改声纹错误原因:{}", e.getMessage());
throw new RenException("修改声纹错误,请联系管理员"); throw new RenException("修改声纹错误,请联系管理员");
} }
})); }));
} }
/** /**
* 获取生纹接口URI对象 * 获取生纹接口URI对象
* *
@@ -204,6 +211,7 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
/** /**
* 获取声纹地址基础路径 * 获取声纹地址基础路径
*
* @param uri 声纹地址uri * @param uri 声纹地址uri
* @return 基础路径 * @return 基础路径
*/ */
@@ -211,7 +219,7 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
String protocol = uri.getScheme(); String protocol = uri.getScheme();
String host = uri.getHost(); String host = uri.getHost();
int port = uri.getPort(); int port = uri.getPort();
return "%s://%s:%s".formatted(protocol,host,port); return "%s://%s:%s".formatted(protocol, host, port);
} }
/** /**
@@ -231,13 +239,13 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
/** /**
* 获取声纹音频资源数据 * 获取声纹音频资源数据
* *
* @param audioId 音频Id * @param audioId 音频Id
* @return 声纹音频资源数据 * @return 声纹音频资源数据
*/ */
private ByteArrayResource getVoicePrintAudioWAV(String agentId,String audioId) { private ByteArrayResource getVoicePrintAudioWAV(String agentId, String audioId) {
// 判断这个音频是否属于当前智能体 // 判断这个音频是否属于当前智能体
boolean b = agentChatHistoryService.isAudioOwnedByAgent(audioId, agentId); boolean b = agentChatHistoryService.isAudioOwnedByAgent(audioId, agentId);
if(!b){ if (!b) {
throw new RenException("音频数据不属于这个智能体"); throw new RenException("音频数据不属于这个智能体");
} }
// 获取到音频数据 // 获取到音频数据
@@ -256,15 +264,16 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
} }
/** /**
* 发送注册声纹http请求 * 发送注册声纹http请求
* @param id 声纹id *
* @param id 声纹id
* @param resource 声纹音频资源 * @param resource 声纹音频资源
*/ */
private void registerVoicePrint(String id, ByteArrayResource resource) { private void registerVoicePrint(String id, ByteArrayResource resource) {
// 处理声纹接口地址,获取前缀 // 处理声纹接口地址,获取前缀
URI uri = getVoicePrintURI(); URI uri = getVoicePrintURI();
String baseUrl = getBaseUrl(uri); String baseUrl = getBaseUrl(uri);
String requestUrl = baseUrl + "/voiceprint/register"; String requestUrl = baseUrl + "/voiceprint/register";
// 创建请求体 // 创建请求体
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>(); MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("speaker_id", id); body.add("speaker_id", id);
@@ -285,7 +294,7 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
} }
// 检查响应内容 // 检查响应内容
String responseBody = response.getBody(); String responseBody = response.getBody();
if(responseBody == null || !responseBody.contains("true")){ if (responseBody == null || !responseBody.contains("true")) {
log.error("声纹注册失败,请求处理失败内容:{}", responseBody == null ? "空内容" : responseBody); log.error("声纹注册失败,请求处理失败内容:{}", responseBody == null ? "空内容" : responseBody);
throw new RenException("声纹保存失败,请求处理失败"); throw new RenException("声纹保存失败,请求处理失败");
} }
@@ -293,12 +302,13 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
/** /**
* 发送注销声纹的请求 * 发送注销声纹的请求
*
* @param voicePrintId 声纹id * @param voicePrintId 声纹id
*/ */
private void cancelVoicePrint(String voicePrintId) { private void cancelVoicePrint(String voicePrintId) {
URI uri = getVoicePrintURI(); URI uri = getVoicePrintURI();
String baseUrl = getBaseUrl(uri); String baseUrl = getBaseUrl(uri);
String requestUrl = baseUrl + "/voiceprint/" + voicePrintId; String requestUrl = baseUrl + "/voiceprint/" + voicePrintId;
// 创建请求头 // 创建请求头
HttpHeaders headers = new HttpHeaders(); HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", getAuthorization(uri)); headers.set("Authorization", getAuthorization(uri));
@@ -306,46 +316,47 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(headers); HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(headers);
// 发送 POST 请求 // 发送 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) { if (response.getStatusCode() != HttpStatus.OK) {
log.error("声纹注销失败,请求路径:{}", requestUrl); log.error("声纹注销失败,请求路径:{}", requestUrl);
throw new RenException("声纹注销失败,请求不成功"); throw new RenException("声纹注销失败,请求不成功");
} }
// 检查响应内容 // 检查响应内容
String responseBody = response.getBody(); String responseBody = response.getBody();
if(responseBody == null || !responseBody.contains("true")){ if (responseBody == null || !responseBody.contains("true")) {
log.error("声纹注销失败,请求处理失败内容:{}", responseBody == null ? "空内容" : responseBody); log.error("声纹注销失败,请求处理失败内容:{}", responseBody == null ? "空内容" : responseBody);
throw new RenException("声纹注销失败,请求处理失败"); throw new RenException("声纹注销失败,请求处理失败");
} }
} }
/** /**
* 发送识别声纹http请求 * 发送识别声纹http请求
* @param agentId 智能体id *
* @param agentId 智能体id
* @param resource 声纹音频资源 * @param resource 声纹音频资源
* @return 返回识别数据 * @return 返回识别数据
*/ */
private IdentifyVoicePrintResponse identifyVoicePrint(String agentId, ByteArrayResource resource) { private IdentifyVoicePrintResponse identifyVoicePrint(String agentId, ByteArrayResource resource) {
// 获取该智能体所有注册的声纹 // 获取该智能体所有注册的声纹
List<AgentVoicePrintEntity> agentVoicePrintList = baseMapper.selectList(new LambdaQueryWrapper<AgentVoicePrintEntity>() List<AgentVoicePrintEntity> agentVoicePrintList = baseMapper
.select(AgentVoicePrintEntity::getId) .selectList(new LambdaQueryWrapper<AgentVoicePrintEntity>()
.eq(AgentVoicePrintEntity::getAgentId,agentId)); .select(AgentVoicePrintEntity::getId)
.eq(AgentVoicePrintEntity::getAgentId, agentId));
// 声纹数量为0,说明还没注册过声纹不需要发生识别请求 // 声纹数量为0,说明还没注册过声纹不需要发生识别请求
if(agentVoicePrintList.isEmpty()) { if (agentVoicePrintList.isEmpty()) {
return null; return null;
} }
// 处理声纹接口地址,获取前缀 // 处理声纹接口地址,获取前缀
URI uri = getVoicePrintURI(); URI uri = getVoicePrintURI();
String baseUrl = getBaseUrl(uri); String baseUrl = getBaseUrl(uri);
String requestUrl = baseUrl + "/voiceprint/identify"; String requestUrl = baseUrl + "/voiceprint/identify";
// 创建请求体 // 创建请求体
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>(); MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
// 创建speaker_id参数
//创建speaker_id参数
String speakerIds = agentVoicePrintList.stream() String speakerIds = agentVoicePrintList.stream()
.map(AgentVoicePrintEntity::getId) .map(AgentVoicePrintEntity::getId)
.collect(Collectors.joining(",")); .collect(Collectors.joining(","));
@@ -367,7 +378,7 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
} }
// 检查响应内容 // 检查响应内容
String responseBody = response.getBody(); String responseBody = response.getBody();
if(responseBody != null ){ if (responseBody != null) {
return JsonUtils.parseObject(responseBody, IdentifyVoicePrintResponse.class); return JsonUtils.parseObject(responseBody, IdentifyVoicePrintResponse.class);
} }
return null; return null;
@@ -29,8 +29,8 @@
</el-form-item> </el-form-item>
<el-form-item label="描述" prop="introduce" class="form-item remark-item"> <el-form-item label="描述" prop="introduce" class="form-item remark-item">
<el-input type="textarea" v-model="form.introduce" placeholder="请输入描述" :rows="3" <el-input type="textarea" v-model="form.introduce" placeholder="请输入描述" :rows="3" class="custom-textarea"
class="custom-textarea"></el-input> maxlength="100" show-word-limit></el-input>
</el-form-item> </el-form-item>
</el-form> </el-form>