文字转语音

This commit is contained in:
spllzh
2025-10-04 19:52:06 +08:00
parent 24c00c22ab
commit 06730b656e
51 changed files with 5157 additions and 8 deletions

View File

@@ -0,0 +1,357 @@
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参数
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);
}
// 生成临时访问URL15分钟有效
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);
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());
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 错误信息
*/
private void saveTtsRequestLog(TtsRequestLog requestLog, SiliconFlowTtsRequest request,
SiliconFlowTtsResponse response, long startTime, String errorMessage) {
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.setStatus(response.isSuccess() ? "SUCCESS" : "FAILED");
requestLog.setDuration(response.getDuration());
requestLog.setMinioUrl(response.getMinioUrl()); // 临时访问URL
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);
}
}
}