小程序端直接把音频转成文本
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
package com.rj.config;
|
package com.rj.config;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.web.client.RestTemplate;
|
import org.springframework.web.client.RestTemplate;
|
||||||
@@ -18,9 +17,4 @@ public class RestTemplateConfig {
|
|||||||
public RestTemplate restTemplate() {
|
public RestTemplate restTemplate() {
|
||||||
return new RestTemplate();
|
return new RestTemplate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
|
||||||
public ObjectMapper objectMapper() {
|
|
||||||
return new ObjectMapper();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
package com.rj.controller;
|
package com.rj.controller;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import com.rj.dto.AsrRequest;
|
||||||
|
import com.rj.dto.AsrResponse;
|
||||||
import com.rj.entity.AudioManagementSegments;
|
import com.rj.entity.AudioManagementSegments;
|
||||||
import com.rj.service.IAudioManagementSegmentsService;
|
import com.rj.service.IAudioManagementSegmentsService;
|
||||||
|
import com.rj.service.ITtsRequestLogService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
@@ -17,10 +21,15 @@ import org.springframework.http.ResponseEntity;
|
|||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.nio.file.Files;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import org.springframework.mock.web.MockMultipartFile;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
import com.rj.service.MinIOService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>
|
* <p>
|
||||||
@@ -149,6 +158,201 @@ public class AudioManagementSegmentsController {
|
|||||||
return ResponseEntity.internalServerError().build();
|
return ResponseEntity.internalServerError().build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@Autowired
|
||||||
|
private ITtsRequestLogService ttsRequestLogService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MinIOService minIOService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转文本请求
|
||||||
|
*/
|
||||||
|
@PostMapping("/transcribe/{id}")
|
||||||
|
@Operation(summary = "转文本", description = "分段音频文件转文本")
|
||||||
|
public ResponseEntity<Map<String, Object>> transcribeSegmentById(
|
||||||
|
@Parameter(description = "分段ID", required = true)
|
||||||
|
@PathVariable String id) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
try {
|
||||||
|
// 先查询记录是否存在
|
||||||
|
AudioManagementSegments segment = audioManagementSegmentsService.getById(id);
|
||||||
|
if (segment == null) {
|
||||||
|
result.put("success", false);
|
||||||
|
result.put("message", "录音分段不存在,ID: " + id);
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查本地文件路径是否存在
|
||||||
|
String audioFilePath = segment.getAudioFilePath();
|
||||||
|
if (audioFilePath == null || audioFilePath.trim().isEmpty()) {
|
||||||
|
result.put("success", false);
|
||||||
|
result.put("message", "音频文件路径为空");
|
||||||
|
return ResponseEntity.badRequest().body(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
File audioFile = new File(audioFilePath);
|
||||||
|
if (!audioFile.exists() || !audioFile.isFile()) {
|
||||||
|
result.put("success", false);
|
||||||
|
result.put("message", "音频文件不存在: " + audioFilePath);
|
||||||
|
return ResponseEntity.badRequest().body(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO 1: 上传文件到MinIO
|
||||||
|
String minioUrl = null;
|
||||||
|
try {
|
||||||
|
// 读取本地文件
|
||||||
|
byte[] fileBytes = Files.readAllBytes(audioFile.toPath());
|
||||||
|
String fileName = segment.getAudioFileOriginalName() != null
|
||||||
|
? segment.getAudioFileOriginalName()
|
||||||
|
: audioFile.getName();
|
||||||
|
|
||||||
|
// 获取文件扩展名,确定contentType
|
||||||
|
String contentType = getContentType(fileName);
|
||||||
|
|
||||||
|
// 创建MultipartFile对象
|
||||||
|
MultipartFile multipartFile = new MockMultipartFile(
|
||||||
|
"file",
|
||||||
|
fileName,
|
||||||
|
contentType,
|
||||||
|
fileBytes
|
||||||
|
);
|
||||||
|
|
||||||
|
// 上传到MinIO
|
||||||
|
minioUrl = minIOService.uploadFile(multipartFile);
|
||||||
|
log.info("文件上传到MinIO成功,ID: {}, minioUrl: {}", id, minioUrl);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("上传文件到MinIO失败,ID: {}, error: {}", id, e.getMessage(), e);
|
||||||
|
result.put("success", false);
|
||||||
|
result.put("message", "上传文件到MinIO失败: " + e.getMessage());
|
||||||
|
return ResponseEntity.internalServerError().body(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据id修改audio_management_segments 表字段: needTranscribe为 1, transcribeRequestTime为当前时间
|
||||||
|
AudioManagementSegments updateEntity = new AudioManagementSegments();
|
||||||
|
updateEntity.setNeedTranscribe(true);
|
||||||
|
updateEntity.setTranscribeRequestTime(LocalDateTime.now());
|
||||||
|
updateEntity.setAudioFileUrl(minioUrl);
|
||||||
|
|
||||||
|
LambdaUpdateWrapper<AudioManagementSegments> updateWrapper = new LambdaUpdateWrapper<>();
|
||||||
|
updateWrapper.eq(AudioManagementSegments::getId, id);
|
||||||
|
|
||||||
|
boolean success = audioManagementSegmentsService.update(updateEntity, updateWrapper);
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
result.put("success", false);
|
||||||
|
result.put("message", "更新转文本状态失败");
|
||||||
|
return ResponseEntity.badRequest().body(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从minioUrl中提取objectName,生成presignedUrl
|
||||||
|
String presignedUrl = null;
|
||||||
|
try {
|
||||||
|
String objectName = extractObjectNameFromUrl(minioUrl);
|
||||||
|
if (objectName != null) {
|
||||||
|
// 生成7天有效期的预签名URL
|
||||||
|
presignedUrl = minIOService.getPresignedUrl(objectName, 7 * 24 * 60 * 60);
|
||||||
|
log.info("生成预签名URL成功,ID: {}, presignedUrl: {}", id, presignedUrl);
|
||||||
|
} else {
|
||||||
|
// 如果无法提取objectName,直接使用minioUrl
|
||||||
|
presignedUrl = minioUrl;
|
||||||
|
log.warn("无法从minioUrl提取objectName,使用原始URL: {}", minioUrl);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("生成预签名URL失败,使用原始URL: {}", e.getMessage(), e);
|
||||||
|
presignedUrl = minioUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO 2: 启用异步线程处理转文本
|
||||||
|
final String finalPresignedUrl = presignedUrl;
|
||||||
|
final String segmentId = id;
|
||||||
|
CompletableFuture.runAsync(() -> {
|
||||||
|
try {
|
||||||
|
log.info("开始异步转文本处理,ID: {}", segmentId);
|
||||||
|
|
||||||
|
AsrRequest asrRequest = new AsrRequest();
|
||||||
|
asrRequest.setAudioUrl(finalPresignedUrl);
|
||||||
|
asrRequest.setAudioName(segment.getAudioFileOriginalName());
|
||||||
|
asrRequest.setModel("paraformer-v2"); // 使用默认模型
|
||||||
|
asrRequest.setFormat("wav"); // 默认格式
|
||||||
|
asrRequest.setSampleRate(16000); // 默认采样率
|
||||||
|
asrRequest.setEnablePunctuation(true); // 启用标点符号
|
||||||
|
asrRequest.setEnableNumberConversion(true); // 启用数字转换
|
||||||
|
asrRequest.setEnableSpeakerDiarization(false); // 不启用说话人分离
|
||||||
|
|
||||||
|
// 调用ASR服务进行语音识别
|
||||||
|
AsrResponse asrResponse = ttsRequestLogService.speechToText(asrRequest);
|
||||||
|
|
||||||
|
if (asrResponse != null && asrResponse.isSuccess() && asrResponse.getText() != null) {
|
||||||
|
// 更新转文本结果到数据库
|
||||||
|
AudioManagementSegments textUpdateEntity = new AudioManagementSegments();
|
||||||
|
textUpdateEntity.setRecordingText(asrResponse.getText());
|
||||||
|
textUpdateEntity.setTranscribeEndTime(LocalDateTime.now());
|
||||||
|
|
||||||
|
LambdaUpdateWrapper<AudioManagementSegments> textUpdateWrapper = new LambdaUpdateWrapper<>();
|
||||||
|
textUpdateWrapper.eq(AudioManagementSegments::getId, segmentId);
|
||||||
|
|
||||||
|
boolean updateSuccess = audioManagementSegmentsService.update(textUpdateEntity, textUpdateWrapper);
|
||||||
|
if (updateSuccess) {
|
||||||
|
log.info("转文本结果保存成功,ID: {}, 文本长度: {}", segmentId, asrResponse.getText().length());
|
||||||
|
} else {
|
||||||
|
log.error("转文本结果保存失败,ID: {}", segmentId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
String errorMsg = asrResponse != null ? asrResponse.getMessage() : "ASR响应为空";
|
||||||
|
log.error("转文本失败,ID: {}, error: {}", segmentId, errorMsg);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("异步转文本处理异常,ID: {}, error: {}", segmentId, e.getMessage(), e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
result.put("success", true);
|
||||||
|
result.put("message", "转文本请求已提交,正在后台处理");
|
||||||
|
result.put("data", updateEntity);
|
||||||
|
return ResponseEntity.ok(result);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("转文本请求异常,ID: {}, error: {}", id, e.getMessage(), e);
|
||||||
|
result.put("success", false);
|
||||||
|
result.put("message", "转文本请求异常:" + e.getMessage());
|
||||||
|
return ResponseEntity.internalServerError().body(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从MinIO URL中提取对象名
|
||||||
|
* @param url MinIO文件URL
|
||||||
|
* @return 对象名
|
||||||
|
*/
|
||||||
|
private String extractObjectNameFromUrl(String url) {
|
||||||
|
if (url == null || url.trim().isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 去掉查询参数
|
||||||
|
String urlWithoutParams = url;
|
||||||
|
if (url.contains("?")) {
|
||||||
|
urlWithoutParams = url.substring(0, url.indexOf("?"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按"/"分割URL,获取最后一个部分作为对象名
|
||||||
|
String[] parts = urlWithoutParams.split("/");
|
||||||
|
if (parts.length > 0) {
|
||||||
|
// 获取最后一个非空部分作为对象名
|
||||||
|
for (int i = parts.length - 1; i >= 0; i--) {
|
||||||
|
if (parts[i] != null && !parts[i].trim().isEmpty()) {
|
||||||
|
return parts[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("从URL提取对象名失败: {}", url, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 判断文件是否为音频文件
|
* 判断文件是否为音频文件
|
||||||
|
|||||||
@@ -149,5 +149,17 @@ public class AudioManagementSegments implements Serializable {
|
|||||||
@TableField("summary")
|
@TableField("summary")
|
||||||
private String summary;
|
private String summary;
|
||||||
|
|
||||||
|
@Schema(description = "需要转文本(是或否,默认:否)")
|
||||||
|
@TableField("need_transcribe")
|
||||||
|
private Boolean needTranscribe;
|
||||||
|
|
||||||
|
@Schema(description = "需要转文本请求的时间")
|
||||||
|
@TableField("transcribe_request_time")
|
||||||
|
private LocalDateTime transcribeRequestTime;
|
||||||
|
|
||||||
|
@Schema(description = "需要转文本结束的时间")
|
||||||
|
@TableField("transcribe_end_time")
|
||||||
|
private LocalDateTime transcribeEndTime;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,9 @@
|
|||||||
<result column="audio_file_extension" property="audioFileExtension"/>
|
<result column="audio_file_extension" property="audioFileExtension"/>
|
||||||
<result column="recording_text" property="recordingText"/>
|
<result column="recording_text" property="recordingText"/>
|
||||||
<result column="summary" property="summary"/>
|
<result column="summary" property="summary"/>
|
||||||
|
<result column="need_transcribe" property="needTranscribe"/>
|
||||||
|
<result column="transcribe_request_time" property="transcribeRequestTime"/>
|
||||||
|
<result column="transcribe_end_time" property="transcribeEndTime"/>
|
||||||
</resultMap>
|
</resultMap>
|
||||||
|
|
||||||
<!-- 通用查询结果列 -->
|
<!-- 通用查询结果列 -->
|
||||||
@@ -67,7 +70,10 @@
|
|||||||
audio_file_original_name,
|
audio_file_original_name,
|
||||||
audio_file_extension,
|
audio_file_extension,
|
||||||
recording_text,
|
recording_text,
|
||||||
summary
|
summary,
|
||||||
|
need_transcribe,
|
||||||
|
transcribe_request_time,
|
||||||
|
transcribe_end_time
|
||||||
</sql>
|
</sql>
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
Reference in New Issue
Block a user