Compare commits
2 Commits
31561465a8
...
8f35c605d1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f35c605d1 | ||
|
|
1167707631 |
@@ -1,6 +1,5 @@
|
||||
package com.rj.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
@@ -18,9 +17,4 @@ public class RestTemplateConfig {
|
||||
public RestTemplate restTemplate() {
|
||||
return new RestTemplate();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ObjectMapper objectMapper() {
|
||||
return new ObjectMapper();
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
package com.rj.controller;
|
||||
|
||||
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.rj.dto.AsrRequest;
|
||||
import com.rj.dto.AsrResponse;
|
||||
import com.rj.entity.AudioManagementSegments;
|
||||
import com.rj.service.IAudioManagementSegmentsService;
|
||||
import com.rj.service.ITtsRequestLogService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
@@ -17,10 +21,15 @@ import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
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>
|
||||
@@ -149,6 +158,201 @@ public class AudioManagementSegmentsController {
|
||||
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")
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.alibaba.dashscope.audio.asr.transcription.*;
|
||||
import com.rj.entity.TtsRequestLog;
|
||||
import com.rj.mapper.TtsRequestLogMapper;
|
||||
import com.rj.service.ITtsRequestLogService;
|
||||
import com.rj.service.MinIOService;
|
||||
import com.rj.utils.MinIOUrlGenerator;
|
||||
import com.rj.dto.AsrRequest;
|
||||
import com.rj.dto.AsrResponse;
|
||||
@@ -12,9 +13,13 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@@ -36,6 +41,9 @@ public class TtsRequestLogServiceImpl implements ITtsRequestLogService {
|
||||
@Autowired
|
||||
private MinIOUrlGenerator urlGenerator;
|
||||
|
||||
@Autowired
|
||||
private MinIOService minIOService;
|
||||
|
||||
@Value("${dashscope.api.key}")
|
||||
private String apiKey;
|
||||
|
||||
@@ -111,6 +119,175 @@ public class TtsRequestLogServiceImpl implements ITtsRequestLogService {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 音频转文本的公共方法
|
||||
* 支持本地文件路径或URL
|
||||
*
|
||||
* @param audioPathOrUrl 音频文件路径(本地路径)或URL
|
||||
* @return 转换后的文本内容,失败时返回null
|
||||
*/
|
||||
public String transcribeAudioToText(String audioPathOrUrl) {
|
||||
return transcribeAudioToText(audioPathOrUrl, defaultAsrModel);
|
||||
}
|
||||
|
||||
/**
|
||||
* 音频转文本的公共方法
|
||||
* 支持本地文件路径或URL
|
||||
*
|
||||
* @param audioPathOrUrl 音频文件路径(本地路径)或URL
|
||||
* @param model ASR模型名称,如果为null则使用默认模型
|
||||
* @return 转换后的文本内容,失败时返回null
|
||||
*/
|
||||
public String transcribeAudioToText(String audioPathOrUrl, String model) {
|
||||
if (audioPathOrUrl == null || audioPathOrUrl.trim().isEmpty()) {
|
||||
log.error("音频文件路径或URL不能为空");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isAsrServiceAvailable()) {
|
||||
log.error("ASR服务不可用,请检查配置");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// 判断是URL还是本地文件路径
|
||||
String audioUrl = audioPathOrUrl;
|
||||
|
||||
// 判断是否为URL(以http://或https://开头)
|
||||
if (!audioPathOrUrl.startsWith("http://") && !audioPathOrUrl.startsWith("https://")) {
|
||||
log.info("检测到本地文件路径: {}", audioPathOrUrl);
|
||||
|
||||
// 检查文件是否存在
|
||||
File file = new File(audioPathOrUrl);
|
||||
if (!file.exists() || !file.isFile()) {
|
||||
log.error("本地文件不存在或不是文件: {}", audioPathOrUrl);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 上传本地文件到MinIO获取URL
|
||||
try {
|
||||
String fileName = file.getName();
|
||||
String contentType = getContentType(fileName);
|
||||
|
||||
// 读取文件内容
|
||||
byte[] fileBytes = Files.readAllBytes(file.toPath());
|
||||
|
||||
// 创建MultipartFile对象
|
||||
MultipartFile multipartFile = new MockMultipartFile(
|
||||
"file",
|
||||
fileName,
|
||||
contentType,
|
||||
fileBytes
|
||||
);
|
||||
|
||||
// 上传到MinIO
|
||||
audioUrl = minIOService.uploadFile(multipartFile);
|
||||
log.info("本地文件已上传到MinIO,URL: {}", audioUrl);
|
||||
} catch (Exception e) {
|
||||
log.error("上传本地文件到MinIO失败: {}", e.getMessage(), e);
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
log.info("使用音频URL: {}", audioUrl);
|
||||
}
|
||||
|
||||
// 使用默认模型或指定模型
|
||||
String asrModel = (model != null && !model.trim().isEmpty()) ? model : defaultAsrModel;
|
||||
|
||||
log.info("开始语音识别 - 音频URL: {}, 模型: {}", audioUrl, asrModel);
|
||||
|
||||
// 创建转写请求参数
|
||||
TranscriptionParam param = TranscriptionParam.builder()
|
||||
.apiKey(apiKey)
|
||||
.model(asrModel)
|
||||
.fileUrls(Arrays.asList(audioUrl))
|
||||
.build();
|
||||
|
||||
// 创建转写对象
|
||||
Transcription transcription = new Transcription();
|
||||
|
||||
// 提交转写请求
|
||||
TranscriptionResult result = transcription.asyncCall(param);
|
||||
String taskId = result.getTaskId();
|
||||
log.info("ASR任务已提交 - TaskId: {}", taskId);
|
||||
|
||||
// 等待任务完成
|
||||
TranscriptionQueryParam queryParam = TranscriptionQueryParam.FromTranscriptionParam(param, taskId);
|
||||
result = transcription.wait(queryParam);
|
||||
log.info("语音识别完成 - TaskId: {}", taskId);
|
||||
|
||||
if (result.getResults() != null && !result.getResults().isEmpty()) {
|
||||
// 解析识别结果
|
||||
StringBuilder fullText = new StringBuilder();
|
||||
|
||||
// 遍历所有结果,获取transcriptionUrl并下载识别文本
|
||||
for (TranscriptionTaskResult taskResult : result.getResults()) {
|
||||
if (taskResult.getTranscriptionUrl() != null) {
|
||||
try {
|
||||
// 从transcriptionUrl下载识别结果
|
||||
String transcriptionResult = downloadTranscriptionResult(taskResult.getTranscriptionUrl());
|
||||
|
||||
if (transcriptionResult != null && !transcriptionResult.trim().isEmpty()) {
|
||||
// 解析transcription结果JSON
|
||||
String extractedText = parseTranscriptionResult(transcriptionResult);
|
||||
if (extractedText != null && !extractedText.trim().isEmpty()) {
|
||||
if (fullText.length() > 0) {
|
||||
fullText.append(" ");
|
||||
}
|
||||
fullText.append(extractedText);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("下载transcription结果失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String resultText = fullText.toString().trim();
|
||||
if (resultText.isEmpty()) {
|
||||
log.warn("ASR识别结果为空 - TaskId: {}", taskId);
|
||||
return null;
|
||||
}
|
||||
|
||||
log.info("语音识别完成 - 识别文本长度: {}, TaskId: {}", resultText.length(), taskId);
|
||||
return resultText;
|
||||
} else {
|
||||
log.warn("ASR识别结果为空 - TaskId: {}", taskId);
|
||||
return null;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("语音识别失败: {}", e.getMessage(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据文件名获取Content-Type
|
||||
*/
|
||||
private String getContentType(String fileName) {
|
||||
if (fileName == null) {
|
||||
return "application/octet-stream";
|
||||
}
|
||||
String lowerName = fileName.toLowerCase();
|
||||
if (lowerName.endsWith(".mp3")) {
|
||||
return "audio/mpeg";
|
||||
} else if (lowerName.endsWith(".wav")) {
|
||||
return "audio/wav";
|
||||
} else if (lowerName.endsWith(".m4a")) {
|
||||
return "audio/mp4";
|
||||
} else if (lowerName.endsWith(".aac")) {
|
||||
return "audio/aac";
|
||||
} else if (lowerName.endsWith(".ogg")) {
|
||||
return "audio/ogg";
|
||||
} else if (lowerName.endsWith(".flac")) {
|
||||
return "audio/flac";
|
||||
} else if (lowerName.endsWith(".wma")) {
|
||||
return "audio/x-ms-wma";
|
||||
}
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
@Override
|
||||
public AsrResponse speechToText(AsrRequest request) {
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
@@ -34,6 +34,9 @@
|
||||
<result column="audio_file_extension" property="audioFileExtension"/>
|
||||
<result column="recording_text" property="recordingText"/>
|
||||
<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>
|
||||
|
||||
<!-- 通用查询结果列 -->
|
||||
@@ -67,7 +70,10 @@
|
||||
audio_file_original_name,
|
||||
audio_file_extension,
|
||||
recording_text,
|
||||
summary
|
||||
summary,
|
||||
need_transcribe,
|
||||
transcribe_request_time,
|
||||
transcribe_end_time
|
||||
</sql>
|
||||
|
||||
</mapper>
|
||||
|
||||
Reference in New Issue
Block a user