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 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 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); } } }