|
|
@ -1,6 +1,5 @@ |
|
|
package org.jeecg.modules.applet.service.impl; |
|
|
package org.jeecg.modules.applet.service.impl; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import com.alibaba.fastjson.JSON; |
|
|
import com.alibaba.fastjson.JSON; |
|
|
import com.alibaba.fastjson.JSONArray; |
|
|
import com.alibaba.fastjson.JSONArray; |
|
|
import com.alibaba.fastjson.JSONObject; |
|
|
import com.alibaba.fastjson.JSONObject; |
|
|
@ -33,11 +32,16 @@ import org.jeecg.modules.demo.appletTtsTimbre.service.IAppletTtsTimbreService; |
|
|
import org.jeecg.modules.demo.appletTtsCache.entity.AppletTtsCache; |
|
|
import org.jeecg.modules.demo.appletTtsCache.entity.AppletTtsCache; |
|
|
import org.jeecg.modules.demo.appletTtsCache.service.IAppletTtsCacheService; |
|
|
import org.jeecg.modules.demo.appletTtsCache.service.IAppletTtsCacheService; |
|
|
import org.jeecg.modules.applet.util.AudioDurationUtil; |
|
|
import org.jeecg.modules.applet.util.AudioDurationUtil; |
|
|
|
|
|
import org.jeecg.modules.applet.util.HtmlUtils; |
|
|
import java.io.ByteArrayInputStream; |
|
|
import java.io.ByteArrayInputStream; |
|
|
import org.springframework.beans.factory.annotation.Autowired; |
|
|
import org.springframework.beans.factory.annotation.Autowired; |
|
|
import org.springframework.beans.factory.annotation.Value; |
|
|
import org.springframework.beans.factory.annotation.Value; |
|
|
import org.springframework.data.redis.core.RedisTemplate; |
|
|
import org.springframework.data.redis.core.RedisTemplate; |
|
|
import java.util.concurrent.TimeUnit; |
|
|
import java.util.concurrent.TimeUnit; |
|
|
|
|
|
import java.util.concurrent.ConcurrentLinkedQueue; |
|
|
|
|
|
import java.util.concurrent.atomic.AtomicInteger; |
|
|
|
|
|
import java.util.concurrent.CompletableFuture; |
|
|
|
|
|
import org.springframework.scheduling.annotation.Async; |
|
|
import org.springframework.stereotype.Service; |
|
|
import org.springframework.stereotype.Service; |
|
|
|
|
|
|
|
|
import java.util.*; |
|
|
import java.util.*; |
|
|
@ -69,6 +73,109 @@ public class AppletApiTTServiceImpl implements AppletApiTTService { |
|
|
@Autowired |
|
|
@Autowired |
|
|
private RedisTemplate<String, Object> redisTemplate; |
|
|
private RedisTemplate<String, Object> redisTemplate; |
|
|
|
|
|
|
|
|
|
|
|
// 长文本TTS并发控制 - 滑动窗口限流器 |
|
|
|
|
|
private final ConcurrentLinkedQueue<Long> requestTimestamps = new ConcurrentLinkedQueue<>(); |
|
|
|
|
|
private final AtomicInteger currentRequests = new AtomicInteger(0); |
|
|
|
|
|
private static final int MAX_REQUESTS_PER_SECOND = 10; |
|
|
|
|
|
private static final long WINDOW_SIZE_MS = 1000L; // 1秒窗口 |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 长文本TTS请求限流控制 |
|
|
|
|
|
* 使用滑动窗口算法,限制每秒最多10个请求 |
|
|
|
|
|
* @return true表示可以继续请求,false表示需要等待 |
|
|
|
|
|
*/ |
|
|
|
|
|
private boolean acquireLongTextTtsPermit() { |
|
|
|
|
|
long currentTime = System.currentTimeMillis(); |
|
|
|
|
|
|
|
|
|
|
|
// 清理过期的时间戳(超过1秒的请求记录) |
|
|
|
|
|
while (!requestTimestamps.isEmpty()) { |
|
|
|
|
|
Long oldestTime = requestTimestamps.peek(); |
|
|
|
|
|
if (oldestTime != null && currentTime - oldestTime > WINDOW_SIZE_MS) { |
|
|
|
|
|
requestTimestamps.poll(); |
|
|
|
|
|
currentRequests.decrementAndGet(); |
|
|
|
|
|
} else { |
|
|
|
|
|
break; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 检查当前窗口内的请求数量 |
|
|
|
|
|
if (currentRequests.get() < MAX_REQUESTS_PER_SECOND) { |
|
|
|
|
|
requestTimestamps.offer(currentTime); |
|
|
|
|
|
currentRequests.incrementAndGet(); |
|
|
|
|
|
return true; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return false; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 等待获取长文本TTS请求许可 |
|
|
|
|
|
* 如果当前请求数已达上限,则等待直到可以发送请求 |
|
|
|
|
|
*/ |
|
|
|
|
|
private void waitForLongTextTtsPermit() { |
|
|
|
|
|
while (!acquireLongTextTtsPermit()) { |
|
|
|
|
|
try { |
|
|
|
|
|
// 等待100毫秒后重试 |
|
|
|
|
|
Thread.sleep(100); |
|
|
|
|
|
} catch (InterruptedException e) { |
|
|
|
|
|
Thread.currentThread().interrupt(); |
|
|
|
|
|
log.warn("长文本TTS限流等待被中断: {}", e.getMessage()); |
|
|
|
|
|
break; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 创建腾讯云TTS客户端 |
|
|
|
|
|
* @return TtsClient实例 |
|
|
|
|
|
*/ |
|
|
|
|
|
private TtsClient createTtsClient() { |
|
|
|
|
|
// 创建认证对象 |
|
|
|
|
|
Credential cred = new Credential(secretId, secretKey); |
|
|
|
|
|
|
|
|
|
|
|
// 实例化一个http选项 |
|
|
|
|
|
HttpProfile httpProfile = new HttpProfile(); |
|
|
|
|
|
httpProfile.setEndpoint("tts.tencentcloudapi.com"); |
|
|
|
|
|
|
|
|
|
|
|
// 实例化一个client选项 |
|
|
|
|
|
ClientProfile clientProfile = new ClientProfile(); |
|
|
|
|
|
clientProfile.setHttpProfile(httpProfile); |
|
|
|
|
|
|
|
|
|
|
|
// 实例化要请求产品的client对象 |
|
|
|
|
|
return new TtsClient(cred, "", clientProfile); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 设置TTS请求的通用参数 |
|
|
|
|
|
* @param speed 语速 |
|
|
|
|
|
* @param voiceType 音色类型 |
|
|
|
|
|
* @param volume 音量 |
|
|
|
|
|
* @param codec 编码格式 |
|
|
|
|
|
*/ |
|
|
|
|
|
private void setCommonTtsParams(Object request, Float speed, Integer voiceType, Float volume, String codec) { |
|
|
|
|
|
try { |
|
|
|
|
|
// 使用反射设置通用参数 |
|
|
|
|
|
if (speed != null) { |
|
|
|
|
|
request.getClass().getMethod("setSpeed", Float.class).invoke(request, speed); |
|
|
|
|
|
} |
|
|
|
|
|
if (voiceType != null) { |
|
|
|
|
|
request.getClass().getMethod("setVoiceType", Long.class).invoke(request, voiceType.longValue()); |
|
|
|
|
|
} |
|
|
|
|
|
if (volume != null) { |
|
|
|
|
|
request.getClass().getMethod("setVolume", Float.class).invoke(request, volume); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
String codecValue = (codec != null && !codec.isEmpty()) ? codec : "wav"; |
|
|
|
|
|
request.getClass().getMethod("setCodec", String.class).invoke(request, codecValue); |
|
|
|
|
|
|
|
|
|
|
|
// 设置其他默认参数 |
|
|
|
|
|
request.getClass().getMethod("setModelType", Long.class).invoke(request, 1L); |
|
|
|
|
|
request.getClass().getMethod("setPrimaryLanguage", Long.class).invoke(request, 2L); |
|
|
|
|
|
request.getClass().getMethod("setSampleRate", Long.class).invoke(request, 16000L); |
|
|
|
|
|
} catch (Exception e) { |
|
|
|
|
|
log.warn("设置TTS请求参数时发生异常: {}", e.getMessage()); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
public TtsVo textToVoice(String text, Float speed, Integer voiceType, Float volume, String codec) { |
|
|
public TtsVo textToVoice(String text, Float speed, Integer voiceType, Float volume, String codec) { |
|
|
long startTime = System.currentTimeMillis(); |
|
|
long startTime = System.currentTimeMillis(); |
|
|
@ -107,19 +214,8 @@ public class AppletApiTTServiceImpl implements AppletApiTTService { |
|
|
// 2. 缓存未命中,调用腾讯云TTS接口生成音频 |
|
|
// 2. 缓存未命中,调用腾讯云TTS接口生成音频 |
|
|
log.info("TTS缓存未命中,调用腾讯云接口生成音频"); |
|
|
log.info("TTS缓存未命中,调用腾讯云接口生成音频"); |
|
|
|
|
|
|
|
|
// 创建认证对象 |
|
|
|
|
|
Credential cred = new Credential(secretId, secretKey); |
|
|
|
|
|
|
|
|
|
|
|
// 实例化一个http选项,可选的,没有特殊需求可以跳过 |
|
|
|
|
|
HttpProfile httpProfile = new HttpProfile(); |
|
|
|
|
|
httpProfile.setEndpoint("tts.tencentcloudapi.com"); |
|
|
|
|
|
|
|
|
|
|
|
// 实例化一个client选项,可选的,没有特殊需求可以跳过 |
|
|
|
|
|
ClientProfile clientProfile = new ClientProfile(); |
|
|
|
|
|
clientProfile.setHttpProfile(httpProfile); |
|
|
|
|
|
|
|
|
|
|
|
// 实例化要请求产品的client对象 |
|
|
|
|
|
TtsClient client = new TtsClient(cred, "", clientProfile); |
|
|
|
|
|
|
|
|
// 创建TTS客户端 |
|
|
|
|
|
TtsClient client = createTtsClient(); |
|
|
|
|
|
|
|
|
// 实例化一个请求对象 |
|
|
// 实例化一个请求对象 |
|
|
TextToVoiceRequest req = new TextToVoiceRequest(); |
|
|
TextToVoiceRequest req = new TextToVoiceRequest(); |
|
|
@ -128,26 +224,8 @@ public class AppletApiTTServiceImpl implements AppletApiTTService { |
|
|
req.setText(text); |
|
|
req.setText(text); |
|
|
req.setSessionId(UUID.randomUUID().toString()); |
|
|
req.setSessionId(UUID.randomUUID().toString()); |
|
|
|
|
|
|
|
|
// 设置可选参数 |
|
|
|
|
|
if (speed != null) { |
|
|
|
|
|
req.setSpeed(speed); |
|
|
|
|
|
} |
|
|
|
|
|
if (voiceType != null) { |
|
|
|
|
|
req.setVoiceType(voiceType.longValue()); |
|
|
|
|
|
} |
|
|
|
|
|
if (volume != null) { |
|
|
|
|
|
req.setVolume(volume); |
|
|
|
|
|
} |
|
|
|
|
|
if (codec != null && !codec.isEmpty()) { |
|
|
|
|
|
req.setCodec(codec); |
|
|
|
|
|
} else { |
|
|
|
|
|
req.setCodec("wav"); // 默认wav格式 |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 设置其他默认参数 |
|
|
|
|
|
req.setModelType(1L); // 默认模型 |
|
|
|
|
|
req.setPrimaryLanguage(2L); // 中文 |
|
|
|
|
|
req.setSampleRate(16000L); // 16k采样率 |
|
|
|
|
|
|
|
|
// 设置通用参数 |
|
|
|
|
|
setCommonTtsParams(req, speed, voiceType, volume, codec); |
|
|
|
|
|
|
|
|
// 返回的resp是一个TextToVoiceResponse的实例,与请求对象对应 |
|
|
// 返回的resp是一个TextToVoiceResponse的实例,与请求对象对应 |
|
|
TextToVoiceResponse resp = client.TextToVoice(req); |
|
|
TextToVoiceResponse resp = client.TextToVoice(req); |
|
|
@ -308,11 +386,13 @@ public class AppletApiTTServiceImpl implements AppletApiTTService { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@Override |
|
|
@Override |
|
|
public String createLongTextTtsTask(String text, Float speed, Integer voiceType, Float volume, String codec, String callbackUrl) { |
|
|
|
|
|
|
|
|
@Async |
|
|
|
|
|
public CompletableFuture<String> createLongTextTtsTask(String normalized, Float speed, Integer voiceType, Float volume, String codec, String callbackUrl) { |
|
|
|
|
|
log.info("开始异步创建长文本TTS任务,文本长度: {}, voiceType: {}", normalized != null ? normalized.length() : 0, voiceType); |
|
|
|
|
|
|
|
|
try { |
|
|
try { |
|
|
// 将文章内容去除HTML并计算哈希,用于缓存唯一标识 |
|
|
|
|
|
String normalized = stripHtml(text); |
|
|
|
|
|
String textHash = String.valueOf(normalized.hashCode()); |
|
|
|
|
|
|
|
|
// 计算哈希,用于缓存唯一标识 |
|
|
|
|
|
String textHash = HtmlUtils.calculateStringHash(normalized); |
|
|
|
|
|
|
|
|
// 先检查是否存在生成中的任务(相同文本哈希与音色) |
|
|
// 先检查是否存在生成中的任务(相同文本哈希与音色) |
|
|
LambdaQueryWrapper<AppletTtsCache> generatingWrapper = new LambdaQueryWrapper<>(); |
|
|
LambdaQueryWrapper<AppletTtsCache> generatingWrapper = new LambdaQueryWrapper<>(); |
|
|
@ -323,7 +403,7 @@ public class AppletApiTTServiceImpl implements AppletApiTTService { |
|
|
AppletTtsCache generating = appletTtsCacheService.getOne(generatingWrapper); |
|
|
AppletTtsCache generating = appletTtsCacheService.getOne(generatingWrapper); |
|
|
if (generating != null && generating.getTaskId() != null) { |
|
|
if (generating != null && generating.getTaskId() != null) { |
|
|
log.info("已有长文本TTS任务正在生成中,直接返回任务ID: {}", generating.getTaskId()); |
|
|
log.info("已有长文本TTS任务正在生成中,直接返回任务ID: {}", generating.getTaskId()); |
|
|
return generating.getTaskId(); |
|
|
|
|
|
|
|
|
return CompletableFuture.completedFuture(generating.getTaskId()); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// 再检查是否已有成功缓存 |
|
|
// 再检查是否已有成功缓存 |
|
|
@ -335,30 +415,21 @@ public class AppletApiTTServiceImpl implements AppletApiTTService { |
|
|
AppletTtsCache successCache = appletTtsCacheService.getOne(successWrapper); |
|
|
AppletTtsCache successCache = appletTtsCacheService.getOne(successWrapper); |
|
|
if (successCache != null && successCache.getTaskId() != null) { |
|
|
if (successCache != null && successCache.getTaskId() != null) { |
|
|
log.info("长文本TTS已完成,返回任务ID: {}", successCache.getTaskId()); |
|
|
log.info("长文本TTS已完成,返回任务ID: {}", successCache.getTaskId()); |
|
|
return successCache.getTaskId(); |
|
|
|
|
|
|
|
|
return CompletableFuture.completedFuture(successCache.getTaskId()); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 应用并发控制,等待获取请求许可 |
|
|
|
|
|
log.info("开始等待长文本TTS请求许可,textHash: {}", textHash); |
|
|
|
|
|
waitForLongTextTtsPermit(); |
|
|
|
|
|
log.info("获得长文本TTS请求许可,开始调用腾讯云API,textHash: {}", textHash); |
|
|
|
|
|
|
|
|
// 调用腾讯云异步创建任务 |
|
|
// 调用腾讯云异步创建任务 |
|
|
Credential cred = new Credential(secretId, secretKey); |
|
|
|
|
|
HttpProfile httpProfile = new HttpProfile(); |
|
|
|
|
|
httpProfile.setEndpoint("tts.tencentcloudapi.com"); |
|
|
|
|
|
ClientProfile clientProfile = new ClientProfile(); |
|
|
|
|
|
clientProfile.setHttpProfile(httpProfile); |
|
|
|
|
|
TtsClient client = new TtsClient(cred, "", clientProfile); |
|
|
|
|
|
|
|
|
TtsClient client = createTtsClient(); |
|
|
|
|
|
|
|
|
CreateTtsTaskRequest req = new CreateTtsTaskRequest(); |
|
|
CreateTtsTaskRequest req = new CreateTtsTaskRequest(); |
|
|
req.setText(text); |
|
|
|
|
|
if (speed != null) req.setSpeed(speed); |
|
|
|
|
|
if (voiceType != null) req.setVoiceType(voiceType.longValue()); |
|
|
|
|
|
if (volume != null) req.setVolume(volume); |
|
|
|
|
|
if (codec != null && !codec.isEmpty()) { |
|
|
|
|
|
req.setCodec(codec); |
|
|
|
|
|
} else { |
|
|
|
|
|
req.setCodec("wav"); |
|
|
|
|
|
} |
|
|
|
|
|
req.setModelType(1L); |
|
|
|
|
|
req.setPrimaryLanguage(2L); |
|
|
|
|
|
req.setSampleRate(16000L); |
|
|
|
|
|
|
|
|
req.setText(normalized); |
|
|
|
|
|
setCommonTtsParams(req, speed, voiceType, volume, codec); |
|
|
|
|
|
|
|
|
// 启用时间戳字幕 |
|
|
// 启用时间戳字幕 |
|
|
try { |
|
|
try { |
|
|
req.getClass().getMethod("setEnableSubtitle", Boolean.class).invoke(req, Boolean.TRUE); |
|
|
req.getClass().getMethod("setEnableSubtitle", Boolean.class).invoke(req, Boolean.TRUE); |
|
|
@ -387,7 +458,7 @@ public class AppletApiTTServiceImpl implements AppletApiTTService { |
|
|
appletTtsCacheService.save(cache); |
|
|
appletTtsCacheService.save(cache); |
|
|
|
|
|
|
|
|
log.info("长文本TTS任务创建成功,taskId: {},textHash: {}", taskId, textHash); |
|
|
log.info("长文本TTS任务创建成功,taskId: {},textHash: {}", taskId, textHash); |
|
|
return taskId; |
|
|
|
|
|
|
|
|
return CompletableFuture.completedFuture(taskId); |
|
|
} catch (Exception e) { |
|
|
} catch (Exception e) { |
|
|
log.error("创建长文本TTS任务失败: {}", e.getMessage(), e); |
|
|
log.error("创建长文本TTS任务失败: {}", e.getMessage(), e); |
|
|
throw new JeecgBootException("创建长文本TTS任务失败: " + e.getMessage()); |
|
|
throw new JeecgBootException("创建长文本TTS任务失败: " + e.getMessage()); |
|
|
@ -397,12 +468,7 @@ public class AppletApiTTServiceImpl implements AppletApiTTService { |
|
|
@Override |
|
|
@Override |
|
|
public AppletTtsCache queryLongTextTtsTaskStatus(String taskId) { |
|
|
public AppletTtsCache queryLongTextTtsTaskStatus(String taskId) { |
|
|
try { |
|
|
try { |
|
|
Credential cred = new Credential(secretId, secretKey); |
|
|
|
|
|
HttpProfile httpProfile = new HttpProfile(); |
|
|
|
|
|
httpProfile.setEndpoint("tts.tencentcloudapi.com"); |
|
|
|
|
|
ClientProfile clientProfile = new ClientProfile(); |
|
|
|
|
|
clientProfile.setHttpProfile(httpProfile); |
|
|
|
|
|
TtsClient client = new TtsClient(cred, "", clientProfile); |
|
|
|
|
|
|
|
|
TtsClient client = createTtsClient(); |
|
|
|
|
|
|
|
|
DescribeTtsTaskStatusRequest req = new DescribeTtsTaskStatusRequest(); |
|
|
DescribeTtsTaskStatusRequest req = new DescribeTtsTaskStatusRequest(); |
|
|
req.setTaskId(taskId); |
|
|
req.setTaskId(taskId); |
|
|
@ -473,70 +539,10 @@ public class AppletApiTTServiceImpl implements AppletApiTTService { |
|
|
if (text == null || text.trim().isEmpty()) { |
|
|
if (text == null || text.trim().isEmpty()) { |
|
|
throw new JeecgBootException("页面内容为空,无法创建TTS任务"); |
|
|
throw new JeecgBootException("页面内容为空,无法创建TTS任务"); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
try { |
|
|
try { |
|
|
// 检查是否存在相同页面与音色的生成中任务 |
|
|
|
|
|
LambdaQueryWrapper<AppletTtsCache> generatingWrapper = new LambdaQueryWrapper<>(); |
|
|
|
|
|
generatingWrapper.eq(AppletTtsCache::getPageId, pageId) |
|
|
|
|
|
.eq(AppletTtsCache::getVoiceType, voiceType) |
|
|
|
|
|
.eq(AppletTtsCache::getState, 0); |
|
|
|
|
|
AppletTtsCache generating = appletTtsCacheService.getOne(generatingWrapper); |
|
|
|
|
|
if (generating != null && generating.getTaskId() != null) { |
|
|
|
|
|
log.info("页面长文本TTS任务生成中,返回任务ID: {}", generating.getTaskId()); |
|
|
|
|
|
return generating.getTaskId(); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 检查是否已有成功缓存 |
|
|
|
|
|
LambdaQueryWrapper<AppletTtsCache> successWrapper = new LambdaQueryWrapper<>(); |
|
|
|
|
|
successWrapper.eq(AppletTtsCache::getPageId, pageId) |
|
|
|
|
|
.eq(AppletTtsCache::getVoiceType, voiceType) |
|
|
|
|
|
.eq(AppletTtsCache::getSuccess, "Y") |
|
|
|
|
|
.eq(AppletTtsCache::getState, 1); |
|
|
|
|
|
AppletTtsCache successCache = appletTtsCacheService.getOne(successWrapper); |
|
|
|
|
|
if (successCache != null && successCache.getTaskId() != null) { |
|
|
|
|
|
log.info("页面长文本TTS已完成,返回任务ID: {}", successCache.getTaskId()); |
|
|
|
|
|
return successCache.getTaskId(); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 创建腾讯云任务 |
|
|
|
|
|
Credential cred = new Credential(secretId, secretKey); |
|
|
|
|
|
HttpProfile httpProfile = new HttpProfile(); |
|
|
|
|
|
httpProfile.setEndpoint("tts.tencentcloudapi.com"); |
|
|
|
|
|
ClientProfile clientProfile = new ClientProfile(); |
|
|
|
|
|
clientProfile.setHttpProfile(httpProfile); |
|
|
|
|
|
TtsClient client = new TtsClient(cred, "", clientProfile); |
|
|
|
|
|
|
|
|
|
|
|
CreateTtsTaskRequest req = new CreateTtsTaskRequest(); |
|
|
|
|
|
req.setText(text); |
|
|
|
|
|
if (voiceType != null) req.setVoiceType(voiceType.longValue()); |
|
|
|
|
|
req.setCodec("wav"); |
|
|
|
|
|
req.setModelType(1L); |
|
|
|
|
|
req.setPrimaryLanguage(2L); |
|
|
|
|
|
req.setSampleRate(16000L); |
|
|
|
|
|
req.setEnableSubtitle(true); |
|
|
|
|
|
req.setCallbackUrl(TtscallbackUrl); |
|
|
|
|
|
|
|
|
|
|
|
CreateTtsTaskResponse resp = client.CreateTtsTask(req); |
|
|
|
|
|
String taskId = resp.getData() != null ? resp.getData().getTaskId() : null; |
|
|
|
|
|
if (taskId == null || taskId.isEmpty()) { |
|
|
|
|
|
throw new JeecgBootException("创建页面长文本TTS任务失败,未返回任务ID"); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 写入缓存,标记生成中 |
|
|
|
|
|
AppletTtsCache cache = new AppletTtsCache(); |
|
|
|
|
|
cache.setTaskId(taskId); |
|
|
|
|
|
cache.setText(text); |
|
|
|
|
|
cache.setVoiceType(voiceType); |
|
|
|
|
|
cache.setPageId(pageId); |
|
|
|
|
|
cache.setSuccess("N"); |
|
|
|
|
|
cache.setState(0); |
|
|
|
|
|
cache.setCreateTime(new java.util.Date()); |
|
|
|
|
|
appletTtsCacheService.save(cache); |
|
|
|
|
|
|
|
|
|
|
|
log.info("页面长文本TTS任务创建成功,taskId: {}", taskId); |
|
|
|
|
|
return taskId; |
|
|
|
|
|
|
|
|
return createLongTextTtsTask(text, null, voiceType, null, null, null).get(); |
|
|
} catch (Exception e) { |
|
|
} catch (Exception e) { |
|
|
log.error("创建页面长文本TTS任务失败: {}", e.getMessage(), e); |
|
|
|
|
|
|
|
|
log.error("创建页面长文本TTS任务失败,pageId: {}, voiceType: {}, error: {}", pageId, voiceType, e.getMessage(), e); |
|
|
throw new JeecgBootException("创建页面长文本TTS任务失败: " + e.getMessage()); |
|
|
throw new JeecgBootException("创建页面长文本TTS任务失败: " + e.getMessage()); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
@ -546,32 +552,6 @@ public class AppletApiTTServiceImpl implements AppletApiTTService { |
|
|
*/ |
|
|
*/ |
|
|
@Override |
|
|
@Override |
|
|
public String extractTextByPageId(String pageId) { |
|
|
public String extractTextByPageId(String pageId) { |
|
|
// 旧实现(按标题、content纯文本及重点单词拼接)已注释: |
|
|
|
|
|
// AppletCoursePage page = appletCoursePageService.getById(pageId); |
|
|
|
|
|
// if (page == null) { |
|
|
|
|
|
// throw new JeecgBootException("未找到页面: " + pageId); |
|
|
|
|
|
// } |
|
|
|
|
|
// StringBuilder oldSb = new StringBuilder(); |
|
|
|
|
|
// if (page.getTitle() != null) { |
|
|
|
|
|
// oldSb.append(page.getTitle()).append("\n"); |
|
|
|
|
|
// } |
|
|
|
|
|
// if (page.getContent() != null) { |
|
|
|
|
|
// oldSb.append(stripHtml(page.getContent())).append("\n"); |
|
|
|
|
|
// } |
|
|
|
|
|
// List<AppletCoursePageWord> oldWords = appletCoursePageWordService |
|
|
|
|
|
// .lambdaQuery() |
|
|
|
|
|
// .eq(AppletCoursePageWord::getPageId, pageId) |
|
|
|
|
|
// .list(); |
|
|
|
|
|
// if (oldWords != null && !oldWords.isEmpty()) { |
|
|
|
|
|
// for (AppletCoursePageWord w : oldWords) { |
|
|
|
|
|
// if (w.getWord() != null && !w.getWord().isEmpty()) { |
|
|
|
|
|
// oldSb.append(w.getWord()).append(". "); |
|
|
|
|
|
// } |
|
|
|
|
|
// } |
|
|
|
|
|
// } |
|
|
|
|
|
// return oldSb.toString().trim(); |
|
|
|
|
|
|
|
|
|
|
|
// 新实现:按页面编辑所定义的 JSON 结构解析页面内容,仅提取文本组件 |
|
|
|
|
|
AppletCoursePage page = appletCoursePageService.getById(pageId); |
|
|
AppletCoursePage page = appletCoursePageService.getById(pageId); |
|
|
if (page == null) { |
|
|
if (page == null) { |
|
|
throw new JeecgBootException("未找到页面: " + pageId); |
|
|
throw new JeecgBootException("未找到页面: " + pageId); |
|
|
@ -612,17 +592,6 @@ public class AppletApiTTServiceImpl implements AppletApiTTService { |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
private String stripHtml(String html) { |
|
|
|
|
|
try { |
|
|
|
|
|
String noTag = html.replaceAll("<[^>]*>", " "); |
|
|
|
|
|
noTag = noTag.replace(" ", " "); |
|
|
|
|
|
return noTag.replaceAll("\\s+", " ").trim(); |
|
|
|
|
|
} catch (Exception e) { |
|
|
|
|
|
return html; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@Override |
|
|
@Override |
|
|
public boolean handleTtsCallback(String taskId, String audioBase64, Integer sampleRate, String codec, boolean success, String message) { |
|
|
public boolean handleTtsCallback(String taskId, String audioBase64, Integer sampleRate, String codec, boolean success, String message) { |
|
|
try { |
|
|
try { |
|
|
@ -679,8 +648,7 @@ public class AppletApiTTServiceImpl implements AppletApiTTService { |
|
|
public Map<Integer, Object> selectMapByHtml(String content) { |
|
|
public Map<Integer, Object> selectMapByHtml(String content) { |
|
|
HashMap<Integer, Object> map = new HashMap<>(); |
|
|
HashMap<Integer, Object> map = new HashMap<>(); |
|
|
|
|
|
|
|
|
int i = stripHtml(content).hashCode(); |
|
|
|
|
|
String textHash = String.valueOf(i); |
|
|
|
|
|
|
|
|
String textHash = HtmlUtils.calculateStringHash(HtmlUtils.stripHtml(content)); |
|
|
|
|
|
|
|
|
List<AppletTtsTimbre> list = appletTtsTimbreService |
|
|
List<AppletTtsTimbre> list = appletTtsTimbreService |
|
|
.lambdaQuery() |
|
|
.lambdaQuery() |
|
|
@ -745,8 +713,10 @@ public class AppletApiTTServiceImpl implements AppletApiTTService { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@Override |
|
|
@Override |
|
|
public void generateLongTextForArticleContentAllTimbres(String content) { |
|
|
|
|
|
|
|
|
public String generateLongTextForArticleContentAllTimbres(String content) { |
|
|
try { |
|
|
try { |
|
|
|
|
|
String value = HtmlUtils.stripHtml(content); |
|
|
|
|
|
String contentHashcode = HtmlUtils.calculateStringHash(value); |
|
|
List<AppletTtsTimbre> timbres = appletTtsTimbreService |
|
|
List<AppletTtsTimbre> timbres = appletTtsTimbreService |
|
|
.lambdaQuery() |
|
|
.lambdaQuery() |
|
|
.eq(AppletTtsTimbre::getStatus, "Y") |
|
|
.eq(AppletTtsTimbre::getStatus, "Y") |
|
|
@ -755,14 +725,49 @@ public class AppletApiTTServiceImpl implements AppletApiTTService { |
|
|
String callbackUrl = TtscallbackUrl; |
|
|
String callbackUrl = TtscallbackUrl; |
|
|
for (AppletTtsTimbre timbre : timbres) { |
|
|
for (AppletTtsTimbre timbre : timbres) { |
|
|
try { |
|
|
try { |
|
|
createLongTextTtsTask(content, null, timbre.getVoiceType(), null, "wav", callbackUrl); |
|
|
|
|
|
|
|
|
// 异步调用,不等待结果 |
|
|
|
|
|
createLongTextTtsTask(value, null, timbre.getVoiceType(), null, "wav", callbackUrl); |
|
|
} catch (Exception ex) { |
|
|
} catch (Exception ex) { |
|
|
log.warn("创建文章内容长文本TTS任务失败 voiceType {}: {}", timbre.getVoiceType(), ex.getMessage()); |
|
|
log.warn("创建文章内容长文本TTS任务失败 voiceType {}: {}", timbre.getVoiceType(), ex.getMessage()); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
return contentHashcode; |
|
|
} catch (Exception e) { |
|
|
} catch (Exception e) { |
|
|
log.error("遍历音色创建文章长文本TTS任务失败: {}", e.getMessage(), e); |
|
|
log.error("遍历音色创建文章长文本TTS任务失败: {}", e.getMessage(), e); |
|
|
} |
|
|
} |
|
|
|
|
|
return null; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@Override |
|
|
|
|
|
public String generateLongTextForCoursePageContentAllTimbres(String pageId) { |
|
|
|
|
|
try { |
|
|
|
|
|
// 提取页面文本内容 |
|
|
|
|
|
String content = extractTextByPageId(pageId); |
|
|
|
|
|
|
|
|
|
|
|
if (content == null || content.trim().isEmpty()) { |
|
|
|
|
|
log.warn("课程页面内容为空,无法生成TTS,pageId: {}", pageId); |
|
|
|
|
|
return null; |
|
|
|
|
|
} |
|
|
|
|
|
String contentHashcode = HtmlUtils.calculateStringHash(content); |
|
|
|
|
|
List<AppletTtsTimbre> timbres = appletTtsTimbreService |
|
|
|
|
|
.lambdaQuery() |
|
|
|
|
|
.eq(AppletTtsTimbre::getStatus, "Y") |
|
|
|
|
|
.select(AppletTtsTimbre::getVoiceType) |
|
|
|
|
|
.list(); |
|
|
|
|
|
String callbackUrl = TtscallbackUrl; |
|
|
|
|
|
for (AppletTtsTimbre timbre : timbres) { |
|
|
|
|
|
try { |
|
|
|
|
|
// 异步调用,不等待结果 |
|
|
|
|
|
createLongTextTtsTask(content, null, timbre.getVoiceType(), null, "wav", callbackUrl); |
|
|
|
|
|
} catch (Exception ex) { |
|
|
|
|
|
log.warn("创建课程页面内容长文本TTS任务失败 pageId {}, voiceType {}: {}", pageId, timbre.getVoiceType(), ex.getMessage()); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
return contentHashcode; |
|
|
|
|
|
} catch (Exception e) { |
|
|
|
|
|
log.error("遍历音色创建课程页面长文本TTS任务失败,pageId {}: {}", pageId, e.getMessage(), e); |
|
|
|
|
|
} |
|
|
|
|
|
return null; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
@ -828,4 +833,45 @@ public class AppletApiTTServiceImpl implements AppletApiTTService { |
|
|
return duration; |
|
|
return duration; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@Override |
|
|
|
|
|
public Map<String, Long> getAudioBuildStatus(String contentHashcode) { |
|
|
|
|
|
Map<String, Long> result = new HashMap<>(); |
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
|
if (contentHashcode == null || contentHashcode.isEmpty()) { |
|
|
|
|
|
result.put("total", 0L); |
|
|
|
|
|
result.put("completed", 0L); |
|
|
|
|
|
return result; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 获取所有启用的音色数量作为总数 |
|
|
|
|
|
List<AppletTtsTimbre> enabledTimbres = appletTtsTimbreService |
|
|
|
|
|
.lambdaQuery() |
|
|
|
|
|
.eq(AppletTtsTimbre::getStatus, "Y") |
|
|
|
|
|
.list(); |
|
|
|
|
|
long total = enabledTimbres.size(); |
|
|
|
|
|
|
|
|
|
|
|
// 查询已完成的TTS任务数量 |
|
|
|
|
|
long completed = appletTtsCacheService |
|
|
|
|
|
.lambdaQuery() |
|
|
|
|
|
.eq(AppletTtsCache::getText, contentHashcode) |
|
|
|
|
|
// .eq(AppletTtsCache::getSuccess, "Y") |
|
|
|
|
|
// .eq(AppletTtsCache::getState, 1) |
|
|
|
|
|
.count(); |
|
|
|
|
|
|
|
|
|
|
|
result.put("total", total); |
|
|
|
|
|
result.put("completed", completed); |
|
|
|
|
|
|
|
|
|
|
|
log.debug("获取音频构建状态成功,contentHashcode: {}, total: {}, completed: {}", |
|
|
|
|
|
contentHashcode, total, completed); |
|
|
|
|
|
|
|
|
|
|
|
} catch (Exception e) { |
|
|
|
|
|
log.error("获取音频构建状态失败,contentHashcode: {}, error: {}", contentHashcode, e.getMessage(), e); |
|
|
|
|
|
result.put("total", 0L); |
|
|
|
|
|
result.put("completed", 0L); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return result; |
|
|
|
|
|
} |
|
|
} |
|
|
} |