362 lines
14 KiB
Java
362 lines
14 KiB
Java
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<SiliconFlowTtsRequest> 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<byte[]> 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);
|
||
}
|
||
}
|
||
}
|