578 lines
25 KiB
Java
578 lines
25 KiB
Java
package com.rj.service.impl;
|
||
|
||
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;
|
||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||
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;
|
||
import java.util.List;
|
||
|
||
/**
|
||
* TTS请求日志服务实现类
|
||
*
|
||
* @author rj
|
||
* @date 2025-01-02
|
||
*/
|
||
@Slf4j
|
||
@Service
|
||
public class TtsRequestLogServiceImpl implements ITtsRequestLogService {
|
||
|
||
@Autowired
|
||
private TtsRequestLogMapper ttsRequestLogMapper;
|
||
|
||
@Autowired
|
||
private MinIOUrlGenerator urlGenerator;
|
||
|
||
@Autowired
|
||
private MinIOService minIOService;
|
||
|
||
@Value("${dashscope.api.key}")
|
||
private String apiKey;
|
||
|
||
@Value("${asr.enabled:true}")
|
||
private boolean asrEnabled;
|
||
|
||
@Value("${asr.default-model:paraformer-v2}")
|
||
private String defaultAsrModel;
|
||
|
||
@Override
|
||
public boolean saveTtsRequestLog(TtsRequestLog requestLog) {
|
||
try {
|
||
int result = ttsRequestLogMapper.insert(requestLog);
|
||
log.info("TTS请求日志保存成功: {}", requestLog.getId());
|
||
return result > 0;
|
||
} catch (Exception e) {
|
||
log.error("TTS请求日志保存失败: {}", e.getMessage(), e);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
@Override
|
||
public TtsRequestLog getTtsRequestLogById(String id) {
|
||
try {
|
||
return ttsRequestLogMapper.selectById(id);
|
||
} catch (Exception e) {
|
||
log.error("查询TTS请求日志失败: {}", e.getMessage(), e);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
@Override
|
||
public boolean setShortUrlByAudioName(MinIOUrlGenerator.UrlInfo urlInfo ) {
|
||
try {
|
||
if (urlInfo == null || urlInfo.getFileName() == null || urlInfo.getFileName().trim().isEmpty()) {
|
||
log.error("音频名称不能为空");
|
||
return false;
|
||
}
|
||
|
||
// 根据音频名称查询日志记录
|
||
LambdaQueryWrapper<TtsRequestLog> queryWrapper = new LambdaQueryWrapper<>();
|
||
queryWrapper.eq(TtsRequestLog::getAudioName, urlInfo.getFileName());
|
||
TtsRequestLog requestLog = ttsRequestLogMapper.selectOne(queryWrapper);
|
||
|
||
if (requestLog == null) {
|
||
log.error("未找到音频名称为 {} 的日志记录", urlInfo.getFileName());
|
||
return false;
|
||
}
|
||
|
||
// 更新日志记录的短链接信息
|
||
requestLog.setShortUrl(urlInfo.getUrl());
|
||
requestLog.setShortUrlExpireTime(urlInfo.getExpiresAt());
|
||
requestLog.setUpdateTime(LocalDateTime.now());
|
||
|
||
int result = ttsRequestLogMapper.updateById(requestLog);
|
||
if (result > 0) {
|
||
log.info("成功为音频 {} 设置短链接: {}, 过期时间: {}",
|
||
urlInfo.getFileName(), urlInfo.getUrl(), urlInfo.getFormattedExpiresAt());
|
||
return true;
|
||
} else {
|
||
log.error("更新音频 {} 的短链接失败", urlInfo.getFileName());
|
||
return false;
|
||
}
|
||
|
||
} catch (Exception e) {
|
||
log.error("根据音频名称设置短链接失败: 音频名称={}, 错误={}", urlInfo.getFileName(), e.getMessage(), e);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
@Override
|
||
public boolean setShortUrlByAudioName(String audioName) {
|
||
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();
|
||
|
||
try {
|
||
if (!isAsrServiceAvailable()) {
|
||
return AsrResponse.error("ASR服务不可用,请检查配置");
|
||
}
|
||
|
||
if (request.getAudioUrl() == null || request.getAudioUrl().trim().isEmpty()) {
|
||
return AsrResponse.error("音频文件URL不能为空");
|
||
}
|
||
|
||
// 设置默认值
|
||
if (request.getModel() == null || request.getModel().trim().isEmpty()) {
|
||
request.setModel(defaultAsrModel);
|
||
}
|
||
if (request.getFormat() == null || request.getFormat().trim().isEmpty()) {
|
||
request.setFormat("wav");
|
||
}
|
||
if (request.getSampleRate() == null) {
|
||
request.setSampleRate(16000);
|
||
}
|
||
if (request.getEnablePunctuation() == null) {
|
||
request.setEnablePunctuation(true);
|
||
}
|
||
if (request.getEnableNumberConversion() == null) {
|
||
request.setEnableNumberConversion(true);
|
||
}
|
||
if (request.getEnableSpeakerDiarization() == null) {
|
||
request.setEnableSpeakerDiarization(false);
|
||
}
|
||
if (request.getSpeakerCount() == null) {
|
||
request.setSpeakerCount(1);
|
||
}
|
||
|
||
log.info("开始语音识别 - 音频URL: {}, 模型: {}, 格式: {}, 采样率: {}",
|
||
request.getAudioUrl(), request.getModel(), request.getFormat(), request.getSampleRate());
|
||
|
||
// 创建转写请求参数
|
||
TranscriptionParam param = TranscriptionParam.builder()
|
||
.apiKey(apiKey)
|
||
.model(request.getModel())
|
||
.fileUrls(Arrays.asList(request.getAudioUrl()))
|
||
.build();
|
||
|
||
// 创建转写对象
|
||
Transcription transcription = new Transcription();
|
||
|
||
// 提交转写请求
|
||
TranscriptionResult result = transcription.asyncCall(param);
|
||
String taskId = result.getTaskId();
|
||
String requestId = result.getRequestId();
|
||
|
||
log.info("ASR任务已提交 - TaskId: {}, RequestId: {}", taskId, requestId);
|
||
|
||
// 等待任务完成
|
||
TranscriptionQueryParam queryParam = TranscriptionQueryParam.FromTranscriptionParam(param, taskId);
|
||
result = transcription.wait(queryParam);
|
||
log.info("语音识别完成 - result: {} ", result);
|
||
long processingTime = System.currentTimeMillis() - startTime;
|
||
|
||
if (result.getResults() != null && !result.getResults().isEmpty()) {
|
||
// 解析识别结果 - 从results中获取transcriptionUrl并下载识别结果
|
||
String fullText = "";
|
||
Double duration = 0.0;
|
||
List<AsrResponse.AsrResult> asrResults = new ArrayList<>();
|
||
|
||
try {
|
||
// 从usage中获取时长信息
|
||
if (result.getUsage() != null) {
|
||
// 从usage对象中获取duration,usage通常是一个JsonObject
|
||
try {
|
||
String usageStr = result.getUsage().toString();
|
||
if (usageStr.contains("\"duration\"")) {
|
||
int durationStart = usageStr.indexOf("\"duration\":") + 11;
|
||
int durationEnd = usageStr.indexOf(",", durationStart);
|
||
if (durationEnd == -1) {
|
||
durationEnd = usageStr.indexOf("}", durationStart);
|
||
}
|
||
if (durationStart > 10 && durationEnd > durationStart) {
|
||
duration = Double.parseDouble(usageStr.substring(durationStart, durationEnd).trim());
|
||
}
|
||
}
|
||
} catch (Exception e) {
|
||
log.warn("解析usage中的duration失败: {}", e.getMessage());
|
||
}
|
||
}
|
||
|
||
// 遍历所有结果,获取transcriptionUrl并下载识别文本
|
||
for (TranscriptionTaskResult taskResult : result.getResults()) {
|
||
if (taskResult.getTranscriptionUrl() != null) {
|
||
try {
|
||
// 从transcriptionUrl下载识别结果
|
||
String transcriptionResult = downloadTranscriptionResult(taskResult.getTranscriptionUrl());
|
||
// log.info("transcriptionResult: {}" , transcriptionResult);
|
||
|
||
if (transcriptionResult != null && !transcriptionResult.trim().isEmpty()) {
|
||
// 解析transcription结果JSON
|
||
String extractedText = parseTranscriptionResult(transcriptionResult);
|
||
if (extractedText != null && !extractedText.trim().isEmpty()) {
|
||
fullText += extractedText;
|
||
|
||
// 输出识别文本到控制台
|
||
System.out.println("=== 语音识别结果 ===");
|
||
System.out.println("任务ID: " + taskId);
|
||
System.out.println("识别文本: " + extractedText);
|
||
System.out.println("==================");
|
||
|
||
// 创建结果对象
|
||
AsrResponse.AsrResult asrResult = new AsrResponse.AsrResult();
|
||
asrResult.setText(extractedText);
|
||
asrResult.setStartTime(0.0);
|
||
asrResult.setEndTime(duration);
|
||
asrResults.add(asrResult);
|
||
}
|
||
}
|
||
} catch (Exception e) {
|
||
log.warn("下载transcription结果失败: {}", e.getMessage());
|
||
}
|
||
}
|
||
}
|
||
|
||
} catch (Exception e) {
|
||
log.warn("解析ASR结果时出错: {}", e.getMessage());
|
||
}
|
||
|
||
if (fullText.trim().isEmpty()) {
|
||
log.warn("ASR识别结果为空 - TaskId: {}", taskId);
|
||
return AsrResponse.error("语音识别结果为空");
|
||
}
|
||
|
||
log.info("语音识别完成 - 识别文本长度: {}, 处理时间: {}ms", fullText.length(), processingTime);
|
||
|
||
// 如果提供了TTS日志ID,则保存识别文本到数据库
|
||
if (request.getId() != null && !request.getId().trim().isEmpty()) {
|
||
saveRecognizedTextToDatabase(request.getId(), fullText);
|
||
}
|
||
|
||
return AsrResponse.success(fullText, asrResults, duration, processingTime, taskId, requestId);
|
||
} else {
|
||
log.warn("ASR识别结果为空 - TaskId: {}", taskId);
|
||
return AsrResponse.error("语音识别结果为空");
|
||
}
|
||
|
||
} catch (Exception e) {
|
||
log.error("语音识别失败: {}", e.getMessage(), e);
|
||
return AsrResponse.error("语音识别失败: " + e.getMessage());
|
||
}
|
||
}
|
||
|
||
@Override
|
||
public boolean isAsrServiceAvailable() {
|
||
return asrEnabled && apiKey != null && !apiKey.trim().isEmpty();
|
||
}
|
||
|
||
/**
|
||
* 下载transcription结果
|
||
*/
|
||
private String downloadTranscriptionResult(String transcriptionUrl) {
|
||
try {
|
||
log.info("开始下载transcription结果,URL: {}", transcriptionUrl);
|
||
|
||
// 使用简单的URL连接下载transcription结果,避免HttpClient依赖问题
|
||
java.net.URL url = new java.net.URL(transcriptionUrl);
|
||
java.net.HttpURLConnection connection = (java.net.HttpURLConnection) url.openConnection();
|
||
|
||
// 设置请求头
|
||
connection.setRequestMethod("GET");
|
||
connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
|
||
connection.setRequestProperty("Accept", "application/json");
|
||
connection.setConnectTimeout(30000); // 30秒连接超时
|
||
connection.setReadTimeout(60000); // 60秒读取超时
|
||
|
||
int responseCode = connection.getResponseCode();
|
||
if (responseCode == 200) {
|
||
// 读取响应内容
|
||
StringBuilder result = new StringBuilder();
|
||
try (java.io.BufferedReader reader = new java.io.BufferedReader(
|
||
new java.io.InputStreamReader(connection.getInputStream(), java.nio.charset.StandardCharsets.UTF_8))) {
|
||
String line;
|
||
while ((line = reader.readLine()) != null) {
|
||
result.append(line).append("\n");
|
||
}
|
||
}
|
||
|
||
String responseBody = result.toString().trim();
|
||
log.info("成功下载transcription结果,长度: {} 字符", responseBody.length());
|
||
return responseBody;
|
||
} else {
|
||
log.warn("下载transcription结果失败,状态码: {}", responseCode);
|
||
return null;
|
||
}
|
||
} catch (Exception e) {
|
||
log.error("下载transcription结果异常: {}", e.getMessage(), e);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 解析transcription结果JSON,提取识别文本
|
||
*/
|
||
private String parseTranscriptionResult(String transcriptionJson) {
|
||
try {
|
||
// 使用Jackson解析JSON
|
||
ObjectMapper mapper = new ObjectMapper();
|
||
JsonNode rootNode = mapper.readTree(transcriptionJson);
|
||
|
||
log.info("开始解析transcription JSON");
|
||
|
||
// 根据阿里云ASR返回的JSON结构解析
|
||
// 阿里云返回的结构: {"transcripts": [{"text": "识别的文本", "sentences": [...]}]}
|
||
JsonNode transcriptsNode = rootNode.get("transcripts");
|
||
if (transcriptsNode != null && transcriptsNode.isArray() && transcriptsNode.size() > 0) {
|
||
JsonNode firstTranscript = transcriptsNode.get(0);
|
||
JsonNode textNode = firstTranscript.get("text");
|
||
if (textNode != null && !textNode.isNull()) {
|
||
String text = textNode.asText();
|
||
log.info("成功提取识别文本: {}", text);
|
||
return text;
|
||
}
|
||
}
|
||
|
||
// 如果没有transcripts字段,尝试其他可能的字段结构
|
||
JsonNode resultsNode = rootNode.get("results");
|
||
if (resultsNode != null && resultsNode.isArray()) {
|
||
StringBuilder textBuilder = new StringBuilder();
|
||
for (JsonNode resultNode : resultsNode) {
|
||
JsonNode textNode = resultNode.get("text");
|
||
if (textNode != null && !textNode.isNull()) {
|
||
String text = textNode.asText();
|
||
textBuilder.append(text).append(" ");
|
||
log.info("提取到文本片段: {}", text);
|
||
}
|
||
}
|
||
String fullText = textBuilder.toString().trim();
|
||
if (!fullText.isEmpty()) {
|
||
log.info("完整识别文本: {}", fullText);
|
||
return fullText;
|
||
}
|
||
}
|
||
|
||
// 如果没有results字段,尝试直接获取text字段
|
||
JsonNode textNode = rootNode.get("text");
|
||
if (textNode != null && !textNode.isNull()) {
|
||
String text = textNode.asText();
|
||
log.info("直接获取文本: {}", text);
|
||
return text;
|
||
}
|
||
|
||
// 如果都没有,记录JSON结构用于调试
|
||
log.warn("未找到预期的文本字段,JSON结构: {}", rootNode.toString());
|
||
return null;
|
||
} catch (Exception e) {
|
||
log.error("解析transcription结果JSON失败: {}", e.getMessage(), e);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 保存识别文本到数据库
|
||
*/
|
||
private void saveRecognizedTextToDatabase(String ttsLogId, String recognizedText) {
|
||
try {
|
||
// 根据ID查询TTS请求日志
|
||
TtsRequestLog ttsLog = ttsRequestLogMapper.selectById(ttsLogId);
|
||
if (ttsLog != null) {
|
||
// 将识别文本保存到inputText字段
|
||
ttsLog.setInputText(recognizedText);
|
||
ttsLog.setInputLength(recognizedText.length());
|
||
ttsLog.setUpdateTime(LocalDateTime.now());
|
||
|
||
// 保存到数据库
|
||
int updateResult = ttsRequestLogMapper.updateById(ttsLog);
|
||
if (updateResult > 0) {
|
||
log.info("成功保存识别文本到数据库,TTS日志ID: {}, 文本长度: {}", ttsLogId, recognizedText.length());
|
||
} else {
|
||
log.warn("保存识别文本到数据库失败,TTS日志ID: {}", ttsLogId);
|
||
}
|
||
} else {
|
||
log.warn("未找到对应的TTS请求日志,ID: {}", ttsLogId);
|
||
}
|
||
} catch (Exception e) {
|
||
log.error("保存识别文本到数据库异常: {}", e.getMessage(), e);
|
||
}
|
||
}
|
||
}
|