package com.rj.service; import com.rj.config.SiliconFlowConfig; import com.rj.dto.SiliconFlowTtsRequest; import com.rj.dto.SiliconFlowTtsResponse; import com.rj.entity.TtsRequestLog; import com.rj.utils.MinIOUrlGenerator; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.*; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Base64; /** * SiliconFlow TTS 服务类 * * @author rj * @date 2025-01-02 */ @Slf4j @Service public class SiliconFlowTtsService { @Autowired private SiliconFlowConfig config; @Autowired private RestTemplate restTemplate; @Autowired private com.rj.service.MinIOService minioService; @Autowired private ITtsRequestLogService ttsRequestLogService; @Autowired private MinIOUrlGenerator urlGenerator; private static final String TTS_ENDPOINT = "/audio/speech"; /** * 文本转语音 * * @param request TTS请求 * @return TTS响应 */ public SiliconFlowTtsResponse textToSpeech(SiliconFlowTtsRequest request) { long startTime = System.currentTimeMillis(); TtsRequestLog requestLog = new TtsRequestLog(); try { if (!config.isEnabled()) { return SiliconFlowTtsResponse.error("SiliconFlow服务未启用"); } if (request.getModel() == null || request.getModel().trim().isEmpty()) { request.setModel(config.getDefaultTtsModel()); } // 确保voice参数正确设置 if (request.getVoice() == null || request.getVoice().trim().isEmpty()) { // 根据模型设置默认voice参数 FunAudioLLM/CosyVoice2-0.5B:claire String model = request.getModel(); if (model != null && model.contains("fnlp/MOSS-TTSD-v0.5")) { request.setVoice("fnlp/MOSS-TTSD-v0.5:claire"); } else { request.setVoice("FunAudioLLM/CosyVoice2-0.5B:claire"); // 默认使用claire } } if (request.getInput() == null || request.getInput().trim().isEmpty()) { return SiliconFlowTtsResponse.error("输入文本不能为空"); } String url = config.getBaseUrl() + TTS_ENDPOINT; // 设置请求头 HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setBearerAuth(config.getApiKey()); // 创建请求实体 HttpEntity requestEntity = new HttpEntity<>(request, headers); // 详细记录请求参数 log.info("发送TTS请求到: {}", url); log.info("TTS请求参数 - 模型: {}, 输入文本: {}, 输入长度: {}, 语音: {}, 语速: {}, 音调: {}, 音量: {}, 格式: {}, 采样率: {}", request.getModel(), request.getInput(), request.getInput().length(), request.getVoice(), request.getSpeed(), request.getPitch(), request.getVolume(), request.getFormat(), request.getSampleRate() ); // 发送请求 - SiliconFlow API返回音频流,不是JSON ResponseEntity response = restTemplate.exchange( url, HttpMethod.POST, requestEntity, byte[].class ); if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) { byte[] audioBytes = response.getBody(); String audioBase64 = Base64.getEncoder().encodeToString(audioBytes); // 估算音频时长(MP3格式) double estimatedDuration = calculateAudioDuration(audioBytes.length, "mp3"); // 上传到MinIO String fileName = uploadAudioToMinIO(audioBytes, request.getInput()); // 生成MinIO直接访问URL String minioUrl = null; if (fileName != null) { minioUrl = minioService.getFileUrl(fileName); } // 生成临时访问URL(15分钟有效) MinIOUrlGenerator.UrlInfo tempUrlInfo = null; if (fileName != null) { tempUrlInfo = urlGenerator.generateShortTermUrl(fileName); } SiliconFlowTtsResponse ttsResponse = new SiliconFlowTtsResponse(); ttsResponse.setSuccess(true); ttsResponse.setAudio(audioBase64); ttsResponse.setFormat("mp3"); // SiliconFlow默认返回mp3格式 ttsResponse.setSampleRate(44100); // 默认采样率 ttsResponse.setDuration(estimatedDuration); ttsResponse.setMinioUrl(minioUrl); // 直接MinIO访问URL // 添加临时访问URL(短链) if (tempUrlInfo != null) { ttsResponse.setShortUrl(tempUrlInfo.getUrl()); ttsResponse.setShortUrlExpireTime(tempUrlInfo.getExpiresAt()); } // 保存请求日志到数据库 saveTtsRequestLog(requestLog, request, ttsResponse, startTime, null, fileName); log.info("TTS请求成功,音频数据长度: {} bytes, 估算时长: {:.2f}秒, MinIO URL: {}, 临时URL: {}", audioBytes.length, estimatedDuration, minioUrl, tempUrlInfo != null ? tempUrlInfo.getUrl() : "无"); return ttsResponse; } else { log.error("TTS请求失败,状态码: {}", response.getStatusCode()); return SiliconFlowTtsResponse.error("TTS请求失败,状态码: " + response.getStatusCode()); } } catch (Exception e) { log.error("TTS请求异常: {}", e.getMessage(), e); // 保存错误日志到数据库 SiliconFlowTtsResponse errorResponse = SiliconFlowTtsResponse.error("TTS请求异常: " + e.getMessage()); saveTtsRequestLog(requestLog, request, errorResponse, startTime, e.getMessage(), null); return errorResponse; } } /** * 简单文本转语音 * * @param text 要转换的文本 * @return TTS响应 */ public SiliconFlowTtsResponse textToSpeech(String text) { SiliconFlowTtsRequest request = new SiliconFlowTtsRequest(); request.setModel(config.getDefaultTtsModel()); request.setInput(text); // voice参数会在textToSpeech方法中自动设置 return textToSpeech(request); } /** * 文本转语音并返回音频字节数组 * * @param request TTS请求 * @return 音频字节数组 */ public byte[] textToSpeechBytes(SiliconFlowTtsRequest request) { SiliconFlowTtsResponse response = textToSpeech(request); if (response.isSuccess() && response.getAudio() != null) { try { return Base64.getDecoder().decode(response.getAudio()); } catch (Exception e) { log.error("Base64解码失败: {}", e.getMessage()); return null; } } return null; } /** * 简单文本转语音并返回音频字节数组 * * @param text 要转换的文本 * @return 音频字节数组 */ public byte[] textToSpeechBytes(String text) { SiliconFlowTtsRequest request = new SiliconFlowTtsRequest(); request.setModel(config.getDefaultTtsModel()); request.setInput(text); // voice参数会在textToSpeech方法中自动设置 return textToSpeechBytes(request); } /** * 检查服务状态 * * @return 是否可用 */ public boolean isServiceAvailable() { return config.isEnabled() && config.getApiKey() != null && !config.getApiKey().trim().isEmpty(); } /** * 计算音频时长 * * @param audioSizeBytes 音频文件大小(字节) * @param format 音频格式 * @return 估算的音频时长(秒) */ private double calculateAudioDuration(long audioSizeBytes, String format) { if (audioSizeBytes <= 0) { return 0.0; } // 根据格式和文件大小估算时长 // 这是一个粗略的估算,实际时长可能因编码参数而异 double estimatedDuration; switch (format.toLowerCase()) { case "mp3": // MP3 128kbps 大约 16KB/秒,但实际可能因编码而异 // 使用更保守的估算:约 20KB/秒 estimatedDuration = audioSizeBytes / 20000.0; break; case "wav": // WAV 44.1kHz 16bit 立体声 = 176KB/秒 estimatedDuration = audioSizeBytes / 176000.0; break; case "opus": // Opus 通常 64kbps,约 8KB/秒 estimatedDuration = audioSizeBytes / 8000.0; break; default: // 默认估算 estimatedDuration = audioSizeBytes / 20000.0; break; } // 确保时长在合理范围内 return Math.max(0.1, Math.min(estimatedDuration, 3600.0)); // 0.1秒到1小时 } /** * 上传音频到MinIO * * @param audioBytes 音频字节数组 * @param inputText 输入文本(用于生成文件名) * @return 文件名 */ private String uploadAudioToMinIO(byte[] audioBytes, String inputText) { try { // 生成文件名:tts_时间戳_文本hash.mp3 String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss")); String textHash = String.valueOf(inputText.hashCode()).replace("-", ""); String fileName = String.format("tts_%s_%s.mp3", timestamp, textHash); // 上传到MinIO minioService.uploadFile(audioBytes, fileName, "audio/mpeg"); log.info("音频上传到MinIO成功: {}, 大小: {} bytes", fileName, audioBytes.length); return fileName; } catch (Exception e) { log.error("音频上传到MinIO失败: {}", e.getMessage(), e); return null; // 上传失败不影响TTS功能 } } /** * 保存TTS请求日志到数据库 * * @param requestLog 请求日志对象 * @param request TTS请求 * @param response TTS响应 * @param startTime 开始时间 * @param errorMessage 错误信息 * @param audioFileName 音频文件名 */ private void saveTtsRequestLog(TtsRequestLog requestLog, SiliconFlowTtsRequest request, SiliconFlowTtsResponse response, long startTime, String errorMessage, String audioFileName) { try { long processingTime = System.currentTimeMillis() - startTime; // 设置请求参数 requestLog.setRequestTime(LocalDateTime.now()); requestLog.setModel(request.getModel()); requestLog.setInputText(request.getInput()); requestLog.setInputLength(request.getInput() != null ? request.getInput().length() : 0); requestLog.setVoice(request.getVoice()); requestLog.setSpeed(request.getSpeed()); requestLog.setPitch(request.getPitch()); requestLog.setVolume(request.getVolume()); requestLog.setFormat(request.getFormat()); requestLog.setSampleRate(request.getSampleRate()); requestLog.setCreatorName(request.getCreatorName()); requestLog.setCreatorPhone(request.getCreatorPhone()); requestLog.setAudioName(request.getAudioName()); // 设置响应信息 requestLog.setStatus(response.isSuccess() ? "SUCCESS" : "FAILED"); requestLog.setDuration(response.getDuration()); requestLog.setMinioUrl(response.getMinioUrl()); // 临时访问URL requestLog.setAudioName(audioFileName); // 音频文件名 requestLog.setShortUrl(response.getShortUrl()); // 短链URL(与MinIO URL相同) requestLog.setShortUrlExpireTime(response.getShortUrlExpireTime()); requestLog.setProcessingTimeMs(processingTime); requestLog.setCreateTime(LocalDateTime.now()); requestLog.setUpdateTime(LocalDateTime.now()); // 设置音频文件大小 if (response.getAudio() != null) { try { byte[] audioBytes = Base64.getDecoder().decode(response.getAudio()); requestLog.setAudioSizeBytes((long) audioBytes.length); } catch (Exception e) { log.warn("计算音频文件大小失败: {}", e.getMessage()); } } // 设置错误信息 if (errorMessage != null) { requestLog.setErrorMessage(errorMessage); } else if (!response.isSuccess()) { requestLog.setErrorMessage(response.getMessage()); requestLog.setErrorCode(response.getErrorCode()); } // 保存到数据库 ttsRequestLogService.saveTtsRequestLog(requestLog); log.info("TTS请求日志已保存: ID={}, 状态={}, 处理时间={}ms", requestLog.getId(), requestLog.getStatus(), processingTime); } catch (Exception e) { log.error("保存TTS请求日志失败: {}", e.getMessage(), e); } } }